aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Wilkins <samwilkins333@gmail.com>2019-07-10 17:40:02 -0400
committerSam Wilkins <samwilkins333@gmail.com>2019-07-10 17:40:02 -0400
commit6ca470c067c7620cd9da7d8b8c1d553ee46f5d1c (patch)
tree2a2d492e0c07b3a29cc36eb5b144787647410d5a
parent892608273cdfeba4cfb55c5c604bee4361b3be0e (diff)
parent2cd8ac79b4731ca98aafe8a92fa6fb132fe9e86f (diff)
merged with master and prototype initialization refactor, still in progress and deathly buggy
-rw-r--r--.gitignore2
-rw-r--r--deploy/assets/downarrow.pngbin0 -> 3363 bytes
-rw-r--r--deploy/assets/loading.gifbin0 -> 114208 bytes
-rw-r--r--deploy/index.html1
-rw-r--r--package.json24
-rw-r--r--solr/conf/schema.xml2
-rw-r--r--solr/conf/solrconfig.xml2
-rw-r--r--src/Utils.ts4
-rw-r--r--src/client/DocServer.ts75
-rw-r--r--src/client/documents/Documents.ts503
-rw-r--r--src/client/goldenLayout.js11
-rw-r--r--src/client/northstar/dash-nodes/HistogramBox.tsx5
-rw-r--r--src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx2
-rw-r--r--src/client/util/ClientUtils.ts.temp3
-rw-r--r--src/client/util/DocumentManager.ts137
-rw-r--r--src/client/util/DragManager.ts173
-rw-r--r--src/client/util/History.ts6
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx376
-rw-r--r--src/client/util/Import & Export/ImportMetadataEntry.tsx149
-rw-r--r--src/client/util/LinkManager.ts242
-rw-r--r--src/client/util/ProsemirrorCopy/prompt.js179
-rw-r--r--src/client/util/ProsemirrorExampleTransfer.ts (renamed from src/client/util/ProsemirrorKeymap.ts)10
-rw-r--r--src/client/util/RichTextSchema.tsx177
-rw-r--r--src/client/util/Scripting.ts56
-rw-r--r--src/client/util/SearchUtil.ts61
-rw-r--r--src/client/util/SelectionManager.ts79
-rw-r--r--src/client/util/SerializationHelper.ts14
-rw-r--r--src/client/util/TooltipTextMenu.scss94
-rw-r--r--src/client/util/TooltipTextMenu.tsx449
-rw-r--r--src/client/util/UndoManager.ts1
-rw-r--r--src/client/util/request-image-size.js75
-rw-r--r--src/client/util/type_decls.d50
-rw-r--r--src/client/views/ContextMenu.scss49
-rw-r--r--src/client/views/ContextMenu.tsx188
-rw-r--r--src/client/views/ContextMenuItem.tsx60
-rw-r--r--src/client/views/DocumentDecorations.scss22
-rw-r--r--src/client/views/DocumentDecorations.tsx246
-rw-r--r--src/client/views/EditableView.tsx53
-rw-r--r--src/client/views/GlobalKeyHandler.ts161
-rw-r--r--src/client/views/InkingCanvas.tsx19
-rw-r--r--src/client/views/InkingControl.scss10
-rw-r--r--src/client/views/InkingControl.tsx83
-rw-r--r--src/client/views/InkingStroke.tsx4
-rw-r--r--src/client/views/Main.scss88
-rw-r--r--src/client/views/Main.tsx2
-rw-r--r--src/client/views/MainOverlayTextBox.scss7
-rw-r--r--src/client/views/MainOverlayTextBox.tsx82
-rw-r--r--src/client/views/MainView.tsx366
-rw-r--r--src/client/views/OverlayView.tsx47
-rw-r--r--src/client/views/PresentationView.tsx159
-rw-r--r--src/client/views/ScriptBox.scss17
-rw-r--r--src/client/views/ScriptBox.tsx44
-rw-r--r--src/client/views/SearchBox.scss102
-rw-r--r--src/client/views/SearchBox.tsx8
-rw-r--r--src/client/views/SearchItem.tsx73
-rw-r--r--src/client/views/TemplateMenu.tsx25
-rw-r--r--src/client/views/Templates.tsx65
-rw-r--r--src/client/views/_nodeModuleOverrides.scss19
-rw-r--r--src/client/views/collections/CollectionBaseView.scss11
-rw-r--r--src/client/views/collections/CollectionBaseView.tsx113
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx653
-rw-r--r--src/client/views/collections/CollectionPDFView.tsx85
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx196
-rw-r--r--src/client/views/collections/CollectionStackingView.scss52
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx283
-rw-r--r--src/client/views/collections/CollectionSubView.tsx191
-rw-r--r--src/client/views/collections/CollectionTreeView.scss120
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx617
-rw-r--r--src/client/views/collections/CollectionVideoView.tsx5
-rw-r--r--src/client/views/collections/CollectionView.tsx16
-rw-r--r--src/client/views/collections/DockedFrameRenderer.tsx116
-rw-r--r--src/client/views/collections/ParentDocumentSelector.tsx12
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss1
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx28
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx128
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx12
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss16
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx306
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss2
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx113
-rw-r--r--src/client/views/document_templates/caption_toggle/DetailedCaptionToggle.tsx10
-rw-r--r--src/client/views/globalCssVariables.scss3
-rw-r--r--src/client/views/globalCssVariables.scss.d.ts1
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx175
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx32
-rw-r--r--src/client/views/nodes/DocumentView.scss1
-rw-r--r--src/client/views/nodes/DocumentView.tsx402
-rw-r--r--src/client/views/nodes/FieldView.tsx87
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss2
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx289
-rw-r--r--src/client/views/nodes/IconBox.tsx6
-rw-r--r--src/client/views/nodes/ImageBox.tsx142
-rw-r--r--src/client/views/nodes/KeyValueBox.scss6
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx154
-rw-r--r--src/client/views/nodes/KeyValuePair.scss39
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx82
-rw-r--r--src/client/views/nodes/LinkBox.scss66
-rw-r--r--src/client/views/nodes/LinkBox.tsx86
-rw-r--r--src/client/views/nodes/LinkEditor.scss149
-rw-r--r--src/client/views/nodes/LinkEditor.tsx342
-rw-r--r--src/client/views/nodes/LinkMenu.scss144
-rw-r--r--src/client/views/nodes/LinkMenu.tsx53
-rw-r--r--src/client/views/nodes/LinkMenuGroup.tsx92
-rw-r--r--src/client/views/nodes/LinkMenuItem.tsx118
-rw-r--r--src/client/views/nodes/PDFBox.scss113
-rw-r--r--src/client/views/nodes/PDFBox.tsx511
-rw-r--r--src/client/views/nodes/VideoBox.tsx19
-rw-r--r--src/client/views/nodes/WebBox.tsx9
-rw-r--r--src/client/views/pdf/Annotation.tsx153
-rw-r--r--src/client/views/pdf/PDFAnnotationLayer.tsx24
-rw-r--r--src/client/views/pdf/PDFMenu.scss32
-rw-r--r--src/client/views/pdf/PDFMenu.tsx268
-rw-r--r--src/client/views/pdf/PDFViewer.scss131
-rw-r--r--src/client/views/pdf/PDFViewer.tsx715
-rw-r--r--src/client/views/pdf/Page.tsx416
-rw-r--r--src/client/views/presentationview/PresentationElement.tsx402
-rw-r--r--src/client/views/presentationview/PresentationList.tsx104
-rw-r--r--src/client/views/presentationview/PresentationView.scss (renamed from src/client/views/PresentationView.scss)14
-rw-r--r--src/client/views/presentationview/PresentationView.tsx794
-rw-r--r--src/client/views/search/CheckBox.scss59
-rw-r--r--src/client/views/search/CheckBox.tsx131
-rw-r--r--src/client/views/search/CollectionFilters.scss20
-rw-r--r--src/client/views/search/CollectionFilters.tsx83
-rw-r--r--src/client/views/search/FieldFilters.scss5
-rw-r--r--src/client/views/search/FieldFilters.tsx41
-rw-r--r--src/client/views/search/FilterBox.scss108
-rw-r--r--src/client/views/search/FilterBox.tsx384
-rw-r--r--src/client/views/search/IconBar.scss12
-rw-r--r--src/client/views/search/IconBar.tsx83
-rw-r--r--src/client/views/search/IconButton.scss52
-rw-r--r--src/client/views/search/IconButton.tsx192
-rw-r--r--src/client/views/search/NaviconButton.scss69
-rw-r--r--src/client/views/search/NaviconButton.tsx35
-rw-r--r--src/client/views/search/Pager.scss47
-rw-r--r--src/client/views/search/Pager.tsx78
-rw-r--r--src/client/views/search/SearchBox.scss64
-rw-r--r--src/client/views/search/SearchBox.tsx194
-rw-r--r--src/client/views/search/SearchItem.scss187
-rw-r--r--src/client/views/search/SearchItem.tsx267
-rw-r--r--src/client/views/search/SelectorContextMenu.scss15
-rw-r--r--src/client/views/search/ToggleBar.scss36
-rw-r--r--src/client/views/search/ToggleBar.tsx86
-rw-r--r--src/debug/Repl.tsx5
-rw-r--r--src/debug/Test.tsx1
-rw-r--r--src/debug/Viewer.tsx11
-rw-r--r--src/fields/ScriptField.ts101
-rw-r--r--src/mobile/ImageUpload.tsx4
-rw-r--r--src/new_fields/Doc.ts126
-rw-r--r--src/new_fields/List.ts5
-rw-r--r--src/new_fields/Proxy.ts6
-rw-r--r--src/new_fields/RichTextField.ts2
-rw-r--r--src/new_fields/Schema.ts42
-rw-r--r--src/new_fields/ScriptField.ts96
-rw-r--r--src/new_fields/Types.ts11
-rw-r--r--src/new_fields/URLField.ts11
-rw-r--r--src/new_fields/util.ts73
-rw-r--r--src/scraping/acm/.gitignore2
-rwxr-xr-xsrc/scraping/acm/chromedriverbin0 -> 10256192 bytes
-rw-r--r--src/scraping/acm/index.js333
-rw-r--r--src/scraping/acm/results.txt55
-rw-r--r--src/scraping/buxton/scraper.py77
-rw-r--r--src/server/GarbageCollector.ts124
-rw-r--r--src/server/RouteStore.ts1
-rw-r--r--src/server/Search.ts48
-rw-r--r--src/server/authentication/controllers/user_controller.ts22
-rw-r--r--src/server/authentication/models/current_user_utils.ts44
-rw-r--r--src/server/database.ts22
-rw-r--r--src/server/index.ts188
-rw-r--r--src/server/updateSearch.ts34
-rw-r--r--test/test.ts127
-rw-r--r--tsconfig.json3
171 files changed, 14030 insertions, 4015 deletions
diff --git a/.gitignore b/.gitignore
index a499c39a3..5d3100dc6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,5 @@ node_modules
package-lock.json
dist/
.DS_Store
+.env
+ClientUtils.ts \ No newline at end of file
diff --git a/deploy/assets/downarrow.png b/deploy/assets/downarrow.png
new file mode 100644
index 000000000..3c59ff5b1
--- /dev/null
+++ b/deploy/assets/downarrow.png
Binary files differ
diff --git a/deploy/assets/loading.gif b/deploy/assets/loading.gif
new file mode 100644
index 000000000..0652bf021
--- /dev/null
+++ b/deploy/assets/loading.gif
Binary files differ
diff --git a/deploy/index.html b/deploy/index.html
index 532b995f8..f4a019b71 100644
--- a/deploy/index.html
+++ b/deploy/index.html
@@ -6,6 +6,7 @@
<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>
+ <script src="https://rawgithub.com/juliangarnier/anime/master/anime.min.js"></script>
</head>
<body>
diff --git a/package.json b/package.json
index df0767b50..51d1bab5d 100644
--- a/package.json
+++ b/package.json
@@ -4,9 +4,9 @@
"description": "",
"main": "index.js",
"scripts": {
- "start": "ts-node-dev -- src/server/index.ts",
- "debug": "ts-node-dev --inspect -- src/server/index.ts",
- "build": "webpack --env production",
+ "start": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev -- src/server/index.ts",
+ "debug": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --inspect -- src/server/index.ts",
+ "build": "cross-env NODE_OPTIONS=--max_old_space_size=4096 webpack --env production",
"test": "mocha -r ts-node/register test/**/*.ts",
"tsc": "tsc"
},
@@ -19,9 +19,11 @@
"awesome-typescript-loader": "^5.2.1",
"chai": "^4.2.0",
"copy-webpack-plugin": "^4.6.0",
+ "cross-env": "^5.2.0",
"css-loader": "^2.1.1",
"file-loader": "^3.0.1",
"fork-ts-checker-webpack-plugin": "^1.0.2",
+ "jsdom": "^15.1.1",
"mocha": "^5.2.0",
"sass-loader": "^7.1.0",
"scss-loader": "0.0.1",
@@ -41,26 +43,32 @@
"dependencies": {
"@fortawesome/fontawesome-free-solid": "^5.0.13",
"@fortawesome/fontawesome-svg-core": "^1.2.15",
+ "@fortawesome/free-brands-svg-icons": "^5.9.0",
+ "@fortawesome/free-regular-svg-icons": "^5.9.0",
"@fortawesome/free-solid-svg-icons": "^5.7.2",
"@fortawesome/react-fontawesome": "^0.1.4",
"@hig/flyout": "^1.0.3",
"@hig/theme-context": "^2.1.3",
"@hig/theme-data": "^2.3.3",
"@trendmicro/react-dropdown": "^1.3.0",
+ "@types/animejs": "^2.0.2",
"@types/async": "^2.4.1",
"@types/bcrypt-nodejs": "0.0.30",
"@types/bluebird": "^3.5.25",
"@types/body-parser": "^1.17.0",
+ "@types/classnames": "^2.2.8",
"@types/connect-flash": "0.0.34",
"@types/cookie-parser": "^1.4.1",
"@types/cookie-session": "^2.0.36",
"@types/d3-format": "^1.3.1",
+ "@types/dotenv": "^6.1.1",
"@types/express": "^4.16.1",
"@types/express-flash": "0.0.0",
"@types/express-session": "^1.15.12",
"@types/express-validator": "^3.0.0",
"@types/formidable": "^1.0.31",
"@types/jquery": "^3.3.29",
+ "@types/jquery-awesome-cursor": "^0.3.0",
"@types/jsonwebtoken": "^8.3.2",
"@types/lodash": "^4.14.121",
"@types/mobile-detect": "^1.3.4",
@@ -70,6 +78,7 @@
"@types/nodemailer": "^4.6.6",
"@types/passport": "^1.0.0",
"@types/passport-local": "^1.0.33",
+ "@types/pdfjs-dist": "^2.0.0",
"@types/prosemirror-commands": "^1.0.1",
"@types/prosemirror-history": "^1.0.1",
"@types/prosemirror-inputrules": "^1.0.2",
@@ -100,6 +109,7 @@
"bluebird": "^3.5.3",
"body-parser": "^1.18.3",
"bootstrap": "^4.3.1",
+ "canvas": "^2.5.0",
"class-transformer": "^0.2.0",
"connect-flash": "^0.1.1",
"connect-mongo": "^2.0.3",
@@ -107,6 +117,7 @@
"cookie-session": "^2.0.0-beta.3",
"crypto-browserify": "^3.11.0",
"d3-format": "^1.3.2",
+ "dotenv": "^8.0.0",
"express": "^4.16.4",
"express-flash": "0.0.2",
"express-session": "^1.15.6",
@@ -119,7 +130,9 @@
"html-to-image": "^0.1.0",
"i": "^0.3.6",
"image-data-uri": "^2.0.0",
+ "image-size": "^0.7.4",
"imagesloaded": "^4.1.4",
+ "jquery-awesome-cursor": "^0.3.1",
"jsonwebtoken": "^8.5.0",
"jsx-to-string": "^1.4.0",
"lodash": "^4.17.11",
@@ -138,6 +151,8 @@
"p-limit": "^2.2.0",
"passport": "^0.4.0",
"passport-local": "^1.0.0",
+ "pdfjs-dist": "^2.0.943",
+ "probe-image-size": "^4.0.0",
"prosemirror-commands": "^1.0.7",
"prosemirror-example-setup": "^1.0.1",
"prosemirror-history": "^1.0.4",
@@ -151,6 +166,7 @@
"pug": "^2.0.3",
"raw-loader": "^1.0.0",
"react": "^16.8.4",
+ "react-anime": "^2.2.0",
"react-bootstrap": "^1.0.0-beta.5",
"react-bootstrap-dropdown-menu": "^1.1.15",
"react-color": "^2.17.0",
@@ -168,13 +184,13 @@
"react-split-pane": "^0.1.85",
"react-table": "^6.9.2",
"request": "^2.88.0",
- "request-image-size": "^2.1.0",
"request-promise": "^4.2.4",
"serializr": "^1.5.1",
"sharp": "^0.22.1",
"socket.io": "^2.2.0",
"socket.io-client": "^2.2.0",
"solr-node": "^1.1.3",
+ "standard-http-error": "^2.0.1",
"typescript-collections": "^1.3.2",
"url-loader": "^1.1.2",
"uuid": "^3.3.2",
diff --git a/solr/conf/schema.xml b/solr/conf/schema.xml
index 30e8daa65..8610786af 100644
--- a/solr/conf/schema.xml
+++ b/solr/conf/schema.xml
@@ -8,7 +8,7 @@
<filter class="solr.StopFilterFactory" words="stopwords.txt"/>
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.PorterStemFilterFactory"/>
- <filter class="solr.NGramFilterFactory" minGramSize="3" maxGramSize="12"/>
+ <filter class="solr.NGramFilterFactory" minGramSize="2" maxGramSize="12"/>
</analyzer>
<analyzer type="query">
<tokenizer class="solr.StandardTokenizerFactory"/>
diff --git a/solr/conf/solrconfig.xml b/solr/conf/solrconfig.xml
index 90eff5363..0d8792749 100644
--- a/solr/conf/solrconfig.xml
+++ b/solr/conf/solrconfig.xml
@@ -695,7 +695,7 @@
-->
<lst name="defaults">
<str name="echoParams">explicit</str>
- <int name="rows">10</int>
+ <int name="rows">10000000</int>
<str name="df">text</str>
<!-- Default search field
<str name="df">text</str>
diff --git a/src/Utils.ts b/src/Utils.ts
index 657bd673e..64abd7fc1 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -15,7 +15,7 @@ export class Utils {
return v5(seed, v5.URL);
}
- public static GetScreenTransform(ele: HTMLElement): { scale: number, translateX: number, translateY: number } {
+ public static GetScreenTransform(ele?: HTMLElement): { scale: number, translateX: number, translateY: number } {
if (!ele) {
return { scale: 1, translateX: 1, translateY: 1 };
}
@@ -89,7 +89,7 @@ export class Utils {
}
}
- public static addServerHandler<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, handler: (args: T) => any) {
+ public static AddServerHandler<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, handler: (args: T) => any) {
socket.on(message.Message, this.loggingCallback('Incoming', handler, message.Name));
}
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 067d89c51..e8f1aa1b8 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -29,13 +29,44 @@ export namespace DocServer {
// indicates whether or not a document is currently being udpated, and, if so, its id
let updatingId: string | undefined;
+ /**
+ * A convenience method. Prepends the full path (i.e. http://localhost:1050) to the
+ * requested extension
+ * @param extension the specified sub-path to append to the window origin
+ */
+ export function prepend(extension: string): string {
+ return window.location.origin + extension;
+ }
+
+ export namespace Control {
+
+ let _isReadOnly = false;
+ export function makeReadOnly() {
+ if (_isReadOnly) return;
+ _isReadOnly = true;
+ _CreateField = field => {
+ _cache[field[Id]] = field;
+ };
+ _UpdateField = emptyFunction;
+ _RespondToUpdate = emptyFunction;
+ }
+
+ export function makeEditable() {
+ if (!_isReadOnly) return;
+ location.reload();
+ }
+
+ export function isReadOnly() { return _isReadOnly; }
+
+ }
+
export namespace Util {
/**
* Whenever the server sends us its handshake message on our
* websocket, we use the above function to return the handshake.
*/
- Utils.addServerHandler(_socket, MessageStore.Foo, onConnection);
+ Utils.AddServerHandler(_socket, MessageStore.Foo, onConnection);
/**
* This function emits a message (with this client's
@@ -47,15 +78,6 @@ export namespace DocServer {
}
/**
- * A convenience method. Prepends the full path (i.e. http://localhost:1050) to the
- * requested extension
- * @param extension the specified sub-path to append to the window origin
- */
- export function prepend(extension: string): string {
- return window.location.origin + extension;
- }
-
- /**
* Emits a message to the server that wipes
* all documents in the database.
*/
@@ -73,10 +95,11 @@ export namespace DocServer {
* the document's url.
*/
export function makeReadOnly() {
- // replaces default functionality with no-ops
- _createField = emptyFunction;
- _emitFieldUpdate = emptyFunction;
- _respondToUpdate = emptyFunction;
+ _CreateField = field => {
+ _cache[field[Id]] = field;
+ };
+ _UpdateField = emptyFunction;
+ _RespondToUpdate = emptyFunction;
}
}
@@ -89,7 +112,7 @@ export namespace DocServer {
* the server if the document has not been cached.
* @param id the id of the requested document
*/
- export async function getRefField(id: string): Promise<Opt<RefField>> {
+ export async function GetRefField(id: string): Promise<Opt<RefField>> {
// an initial pass through the cache to determine whether the document needs to be fetched,
// is already in the process of being fetched or already exists in the
// cache
@@ -140,7 +163,7 @@ export namespace DocServer {
* the server if the document has not been cached.
* @param ids the ids that map to the reqested documents
*/
- export async function getRefFields(ids: string[]): Promise<{ [id: string]: Opt<RefField> }> {
+ export async function GetRefFields(ids: string[]): Promise<{ [id: string]: Opt<RefField> }> {
const requestedIds: string[] = [];
const waitingIds: string[] = [];
const promises: Promise<Opt<RefField>>[] = [];
@@ -246,8 +269,8 @@ export namespace DocServer {
* calling the same function throughout the code base (such as in Util.makeReadonly())
* @param field the [RefField] to be serialized and sent to the server to be stored in the database
*/
- export function createField(field: RefField) {
- _createField(field);
+ export function CreateField(field: RefField) {
+ _CreateField(field);
}
/**
@@ -256,7 +279,7 @@ export namespace DocServer {
* and finally sends that seruialized data to the server.
* @param field the [RefField] to be serialized and sent to the server to be stored in the database
*/
- let _createField = (field: RefField) => {
+ let _CreateField = (field: RefField) => {
_cache[field[Id]] = field;
const initialState = SerializationHelper.Serialize(field);
Utils.emit(_socket, MessageStore.CreateField, initialState);
@@ -272,8 +295,8 @@ export namespace DocServer {
* @param updatedState the new value of the document. At some point, this
* should actually be a proper diff, to improve efficiency
*/
- export function emitFieldUpdate(id: string, updatedState: any) {
- _emitFieldUpdate(id, updatedState);
+ export function UpdateField(id: string, updatedState: any) {
+ _UpdateField(id, updatedState);
}
/**
@@ -283,7 +306,7 @@ export namespace DocServer {
* @param updatedState the new value of the document. At some point, this
* should actually be a proper diff, to improve efficiency
*/
- let _emitFieldUpdate = (id: string, updatedState: any) => {
+ let _UpdateField = (id: string, updatedState: any) => {
// don't emit a duplicate message if the server is already
// (asynchronously) still updating this document's state.
if (id === updatingId) {
@@ -301,7 +324,7 @@ export namespace DocServer {
* Whenever the client receives an update, execute the
* current behavior.
*/
- Utils.addServerHandler(_socket, MessageStore.UpdateField, respondToUpdate);
+ Utils.AddServerHandler(_socket, MessageStore.UpdateField, RespondToUpdate);
/**
* A wrapper around the function local variable _respondToUpdate.
@@ -310,8 +333,8 @@ export namespace DocServer {
* @param diff kept as [any], but actually the [Diff] object sent from the server containing
* the [Doc]'s id and its new state
*/
- function respondToUpdate(diff: any) {
- _respondToUpdate(diff);
+ function RespondToUpdate(diff: any) {
+ _RespondToUpdate(diff);
}
/**
@@ -321,7 +344,7 @@ export namespace DocServer {
* @param diff kept as [any], but actually the [Diff] object sent from the server containing
* the [Doc]'s id and its new state
*/
- let _respondToUpdate = (diff: any) => {
+ let _RespondToUpdate = (diff: any) => {
const id = diff.id;
// to be valid, the Diff object must reference
// a document's id
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 782c133ff..31e7eef2c 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -25,7 +25,7 @@ import { OmitKeys } from "../../Utils";
import { ImageField, VideoField, AudioField, PdfField, WebField } from "../../new_fields/URLField";
import { HtmlField } from "../../new_fields/HtmlField";
import { List } from "../../new_fields/List";
-import { Cast, NumCast } from "../../new_fields/Types";
+import { Cast, NumCast, StrCast, ToConstructor, InterfaceValue } from "../../new_fields/Types";
import { IconField } from "../../new_fields/IconField";
import { listSpec } from "../../new_fields/Schema";
import { DocServer } from "../DocServer";
@@ -35,13 +35,57 @@ import { DateField } from "../../new_fields/DateField";
import { UndoManager } from "../util/UndoManager";
import { RouteStore } from "../../server/RouteStore";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
-var requestImageSize = require('request-image-size');
+import { LinkManager } from "../util/LinkManager";
+import { DocumentManager } from "../util/DocumentManager";
+import DirectoryImportBox from "../util/Import & Export/DirectoryImportBox";
+import { Scripting } from "../util/Scripting";
+var requestImageSize = require('../util/request-image-size');
var path = require('path');
+export enum DocTypes {
+ NONE = "none",
+ TEXT = "text",
+ HIST = "histogram",
+ IMG = "image",
+ WEB = "web",
+ COL = "collection",
+ KVP = "kvp",
+ VID = "video",
+ AUDIO = "audio",
+ PDF = "pdf",
+ ICON = "icon",
+ IMPORT = "import",
+ LINK = "link"
+}
+
+export namespace DocTypeUtils {
+
+ export function values(includeNone: boolean = true): string[] {
+ let types = Object.values(DocTypes);
+ return includeNone ? types : types.filter(key => key !== DocTypes.NONE);
+ }
+
+ export function keys(includeNone: boolean = true): string[] {
+ let types = Object.keys(DocTypes);
+ return includeNone ? types : types.filter(key => key !== DocTypes.NONE);
+ }
+
+ export function toObject<T extends string>(o: Array<T>): { [K in T]: K } {
+ return o.reduce((res, key) => {
+ res[key] = key;
+ return res;
+ }, Object.create(null));
+ }
+
+ const Types = toObject(values());
+
+ export type All = keyof typeof Types;
+}
+
export interface DocumentOptions {
x?: number;
y?: number;
- ink?: InkField;
+ type?: string;
width?: number;
height?: number;
nativeWidth?: number;
@@ -51,7 +95,6 @@ export interface DocumentOptions {
panY?: number;
page?: number;
scale?: number;
- baseLayout?: string;
layout?: string;
templates?: List<string>;
viewType?: number;
@@ -60,7 +103,7 @@ export interface DocumentOptions {
backgroundLayout?: string;
curPage?: number;
documentText?: string;
- borderRounding?: number;
+ borderRounding?: string;
schemaColumns?: List<string>;
dockingConfig?: string;
dbDoc?: Doc;
@@ -71,17 +114,62 @@ export namespace Docs {
export namespace Prototypes {
- // the complete list of document prototypes and their ids
- export let textProto: Doc; const textProtoId = "textProto";
- export let histoProto: Doc; const histoProtoId = "histoProto";
- export let imageProto: Doc; const imageProtoId = "imageProto";
- export let webProto: Doc; const webProtoId = "webProto";
- export let collProto: Doc; const collProtoId = "collectionProto";
- export let kvpProto: Doc; const kvpProtoId = "kvpProto";
- export let videoProto: Doc; const videoProtoId = "videoProto";
- export let audioProto: Doc; const audioProtoId = "audioProto";
- export let pdfProto: Doc; const pdfProtoId = "pdfProto";
- export let iconProto: Doc; const iconProtoId = "iconProto";
+ type PrototypeTemplate = { options?: Partial<DocumentOptions>, primary: string, background?: string };
+ type TemplateMap = Map<DocTypeUtils.All, PrototypeTemplate>;
+ type PrototypeMap = Map<DocTypeUtils.All, Doc>;
+
+ const LayoutMap: TemplateMap = new Map([
+ [DocTypes.TEXT, {
+ options: { height: 150, backgroundColor: "#f1efeb" },
+ primary: FormattedTextBox.LayoutString()
+ }],
+ [DocTypes.HIST, {
+ options: { nativeWidth: 600, curPage: 0 },
+ primary: CollectionView.LayoutString("annotations"),
+ background: HistogramBox.LayoutString()
+ }],
+ [DocTypes.IMG, {
+ options: { height: 300, backgroundColor: "black" },
+ primary: CollectionView.LayoutString("annotations"),
+ background: ImageBox.LayoutString()
+ }],
+ [DocTypes.WEB, {
+ options: { height: 300 },
+ primary: WebBox.LayoutString()
+ }],
+ [DocTypes.COL, {
+ options: { panX: 0, panY: 0, scale: 1, width: 500, height: 500 },
+ primary: CollectionView.LayoutString()
+ }],
+ [DocTypes.KVP, {
+ options: { height: 150 },
+ primary: KeyValueBox.LayoutString()
+ }],
+ [DocTypes.VID, {
+ options: { nativeWidth: 600, curPage: 0 },
+ primary: CollectionVideoView.LayoutString("annotations"),
+ background: VideoBox.LayoutString()
+ }],
+ [DocTypes.AUDIO, {
+ options: { height: 150 },
+ primary: AudioBox.LayoutString()
+ }],
+ [DocTypes.PDF, {
+ options: { nativeWidth: 1200, curPage: 1 },
+ primary: CollectionPDFView.LayoutString("annotations"),
+ background: PDFBox.LayoutString()
+ }],
+ [DocTypes.ICON, {
+ options: { width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE) },
+ primary: IconBox.LayoutString()
+ }],
+ [DocTypes.IMPORT, {
+ options: { height: 150 },
+ primary: DirectoryImportBox.LayoutString()
+ }]
+ ]);
+
+ const PrototypeMap: PrototypeMap = new Map();
/**
* This function loads or initializes the prototype for each docment type.
@@ -96,21 +184,32 @@ export namespace Docs {
*/
export async function initialize(): Promise<void> {
// non-guid string ids for each document prototype
- let protoIds = [textProtoId, histoProtoId, collProtoId, imageProtoId, webProtoId, kvpProtoId, videoProtoId, audioProtoId, pdfProtoId, iconProtoId];
- // fetch the actual prototype documents from the server
- let actualProtos = await DocServer.getRefFields(protoIds);
+ let prototypeIds: string[] = DocTypeUtils.values(false).map(type => type + "Proto");
+ let defaultOptions: DocumentOptions = {
+ x: 0,
+ y: 0,
+ width: 300
+ };
+
+ // fetch the actual prototype documents from the server
+ let actualProtos = await DocServer.GetRefFields(prototypeIds);
// initialize prototype documents
- textProto = actualProtos[textProtoId] as Doc || CreateTextProto();
- histoProto = actualProtos[histoProtoId] as Doc || CreateHistogramProto();
- collProto = actualProtos[collProtoId] as Doc || CreateCollectionProto();
- imageProto = actualProtos[imageProtoId] as Doc || CreateImageProto();
- webProto = actualProtos[webProtoId] as Doc || CreateWebProto();
- kvpProto = actualProtos[kvpProtoId] as Doc || CreateKVPProto();
- videoProto = actualProtos[videoProtoId] as Doc || CreateVideoProto();
- audioProto = actualProtos[audioProtoId] as Doc || CreateAudioProto();
- pdfProto = actualProtos[pdfProtoId] as Doc || CreatePdfProto();
- iconProto = actualProtos[iconProtoId] as Doc || CreateIconProto();
+ prototypeIds.map(id => {
+ let existing = actualProtos[id] as Doc;
+ if (existing) {
+ PrototypeMap.set(id, existing);
+ } else {
+ let template = LayoutMap.get(id.replace("Proto", ""));
+ if (template) {
+ PrototypeMap.set(id, buildPrototype(template, id, defaultOptions));
+ }
+ }
+ });
+ }
+
+ export function get(type: string) {
+ return PrototypeMap.get(type)!;
}
/**
@@ -125,122 +224,14 @@ export namespace Docs {
* @param options any value specified in the DocumentOptions object likewise
* becomes the default value for that key for all delegates
*/
- function buildPrototype(protoId: string, title: string, layout: string, options: DocumentOptions): Doc {
- return Doc.assign(new Doc(protoId, true), { ...options, title: title, layout: layout, baseLayout: layout });
- }
-
- // INDIVIDUAL INITIALIZERS
-
- function CreateImageProto(): Doc {
- let defaultAttrs = {
- x: 0,
- y: 0,
- nativeWidth: 600,
- width: 300,
- backgroundLayout: ImageBox.LayoutString(),
- curPage: 0
- };
- return buildPrototype(imageProtoId, "IMAGE_PROTO", CollectionView.LayoutString("annotations"), defaultAttrs);
- }
-
- function CreateHistogramProto(): Doc {
- let defaultAttrs = {
- x: 0,
- y: 0,
- width: 300,
- height: 300,
- backgroundColor: "black",
- backgroundLayout:
- HistogramBox.LayoutString()
- };
- return buildPrototype(histoProtoId, "HISTO PROTO", CollectionView.LayoutString("annotations"), defaultAttrs);
- }
-
- function CreateIconProto(): Doc {
- let defaultAttrs = {
- x: 0,
- y: 0,
- width: Number(MINIMIZED_ICON_SIZE),
- height: Number(MINIMIZED_ICON_SIZE)
- };
- return buildPrototype(iconProtoId, "ICON_PROTO", IconBox.LayoutString(), defaultAttrs);
- }
-
- function CreateTextProto(): Doc {
- let defaultAttrs = {
- x: 0,
- y: 0,
- width: 300,
- height: 150,
- backgroundColor: "#f1efeb"
- };
- return buildPrototype(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(), defaultAttrs);
- }
-
- function CreatePdfProto(): Doc {
- let defaultAttrs = {
- x: 0,
- y: 0,
- nativeWidth: 1200,
- width: 300,
- backgroundLayout: PDFBox.LayoutString(),
- curPage: 1
- };
- return buildPrototype(pdfProtoId, "PDF_PROTO", CollectionPDFView.LayoutString("annotations"), defaultAttrs);
- }
-
- function CreateWebProto(): Doc {
- let defaultAttrs = {
- x: 0,
- y: 0,
- width: 300,
- height: 300
- };
- return buildPrototype(webProtoId, "WEB_PROTO", WebBox.LayoutString(), defaultAttrs);
- }
-
- function CreateCollectionProto(): Doc {
- let defaultAttrs = {
- panX: 0,
- panY: 0,
- scale: 1,
- width: 500,
- height: 500
- };
- return buildPrototype(collProtoId, "COLLECTION_PROTO", CollectionView.LayoutString("data"), defaultAttrs);
- }
-
- function CreateKVPProto(): Doc {
- let defaultAttrs = {
- x: 0,
- y: 0,
- width: 300,
- height: 150
- };
- return buildPrototype(kvpProtoId, "KVP_PROTO", KeyValueBox.LayoutString(), defaultAttrs);
+ function buildPrototype(template: PrototypeTemplate, prototypeId: string, defaultOptions: DocumentOptions): Doc {
+ let primary = template.primary;
+ let background = template.background;
+ let options = { ...defaultOptions, ...(template.options || {}), title: prototypeId.toUpperCase().replace("PROTO", "_PROTO") };
+ background && (options = { ...options, backgroundLayout: background, });
+ return Doc.assign(new Doc(prototypeId, true), { ...options, layout: primary, baseLayout: primary });
}
- function CreateVideoProto(): Doc {
- let defaultAttrs = {
- x: 0,
- y: 0,
- nativeWidth: 600,
- width: 300,
- backgroundLayout: VideoBox.LayoutString(),
- curPage: 0
- };
- return buildPrototype(videoProtoId, "VIDEO_PROTO", CollectionVideoView.LayoutString("annotations"), defaultAttrs);
- }
-
- function CreateAudioProto(): Doc {
- let defaultAttrs = {
- x: 0,
- y: 0,
- width: 300,
- height: 150
- };
- return buildPrototype(audioProtoId, "AUDIO_PROTO", AudioBox.LayoutString(), defaultAttrs);
- }
}
/**
@@ -269,7 +260,7 @@ export namespace Docs {
* only when creating a DockDocument from the current user's already existing
* main document.
*/
- function CreateInstanceFromProto(proto: Doc, data: Field, options: DocumentOptions, delegId?: string) {
+ export function InstanceFromProto(proto: Doc, data: Field, options: DocumentOptions, delegId?: string) {
const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys);
if (!("author" in protoProps)) {
@@ -305,7 +296,7 @@ export namespace Docs {
}
export function ImageDocument(url: string, options: DocumentOptions = {}) {
- let inst = CreateInstanceFromProto(Prototypes.imageProto, new ImageField(new URL(url)), { title: path.basename(url), ...options });
+ let inst = InstanceFromProto(Prototypes.get(DocTypes.IMG), new ImageField(new URL(url)), { title: path.basename(url), ...options });
requestImageSize(window.origin + RouteStore.corsProxy + "/" + url)
.then((size: any) => {
let aspect = size.height / size.width;
@@ -317,37 +308,30 @@ export namespace Docs {
})
.catch((err: any) => console.log(err));
return inst;
-
- // let doc = SetInstanceOptions(GetImagePrototype(), { ...options, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] },
- // [new URL(url), ImageField]);
- // doc.SetText(KeyStore.Caption, "my caption...");
- // doc.SetText(KeyStore.BackgroundLayout, EmbeddedCaption());
- // doc.SetText(KeyStore.OverlayLayout, FixedCaption());
- // return doc;
}
export function VideoDocument(url: string, options: DocumentOptions = {}) {
- return CreateInstanceFromProto(Prototypes.videoProto, new VideoField(new URL(url)), options);
+ return InstanceFromProto(Prototypes.get(DocTypes.VID), new VideoField(new URL(url)), options);
}
export function AudioDocument(url: string, options: DocumentOptions = {}) {
- return CreateInstanceFromProto(Prototypes.audioProto, new AudioField(new URL(url)), options);
+ return InstanceFromProto(Prototypes.get(DocTypes.AUDIO), new AudioField(new URL(url)), options);
}
export function HistogramDocument(histoOp: HistogramOperation, options: DocumentOptions = {}) {
- return CreateInstanceFromProto(Prototypes.histoProto, new HistogramField(histoOp), options);
+ return InstanceFromProto(Prototypes.get(DocTypes.HIST), new HistogramField(histoOp), options);
}
export function TextDocument(options: DocumentOptions = {}) {
- return CreateInstanceFromProto(Prototypes.textProto, "", options);
+ return InstanceFromProto(Prototypes.get(DocTypes.TEXT), "", options);
}
export function IconDocument(icon: string, options: DocumentOptions = {}) {
- return CreateInstanceFromProto(Prototypes.iconProto, new IconField(icon), options);
+ return InstanceFromProto(Prototypes.get(DocTypes.ICON), new IconField(icon), options);
}
export function PdfDocument(url: string, options: DocumentOptions = {}) {
- return CreateInstanceFromProto(Prototypes.pdfProto, new PdfField(new URL(url)), options);
+ return InstanceFromProto(Prototypes.get(DocTypes.PDF), new PdfField(new URL(url)), options);
}
export async function DBDocument(url: string, options: DocumentOptions = {}, columnOptions: DocumentOptions = {}) {
@@ -363,7 +347,7 @@ export namespace Docs {
CurrentUserUtils.AddNorthstarSchema(schema, schemaDoc);
const docs = schemaDocuments;
CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => {
- DocServer.getRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => {
+ DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => {
if (field instanceof Doc) {
docs.push(field);
} else {
@@ -382,38 +366,42 @@ export namespace Docs {
}
export function WebDocument(url: string, options: DocumentOptions = {}) {
- return CreateInstanceFromProto(Prototypes.webProto, new WebField(new URL(url)), options);
+ return InstanceFromProto(Prototypes.get(DocTypes.WEB), new WebField(new URL(url)), options);
}
export function HtmlDocument(html: string, options: DocumentOptions = {}) {
- return CreateInstanceFromProto(Prototypes.webProto, new HtmlField(html), options);
+ return InstanceFromProto(Prototypes.get(DocTypes.WEB), new HtmlField(html), options);
}
export function KVPDocument(document: Doc, options: DocumentOptions = {}) {
- return CreateInstanceFromProto(Prototypes.kvpProto, document, { title: document.title + ".kvp", ...options });
+ return InstanceFromProto(Prototypes.get(DocTypes.KVP), document, { title: document.title + ".kvp", ...options });
}
export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, makePrototype: boolean = true) {
if (!makePrototype) {
- return MakeDataDelegate(Prototypes.collProto, { ...options, viewType: CollectionViewType.Freeform }, new List(documents));
+ return MakeDataDelegate(Prototypes.get(DocTypes.COL), { ...options, viewType: CollectionViewType.Freeform }, new List(documents));
}
- return CreateInstanceFromProto(Prototypes.collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Freeform });
+ return InstanceFromProto(Prototypes.get(DocTypes.COL), new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Freeform });
}
export function SchemaDocument(schemaColumns: string[], documents: Array<Doc>, options: DocumentOptions) {
- return CreateInstanceFromProto(Prototypes.collProto, new List(documents), { schemaColumns: new List(schemaColumns), ...options, viewType: CollectionViewType.Schema });
+ return InstanceFromProto(Prototypes.get(DocTypes.COL), new List(documents), { schemaColumns: new List(schemaColumns), ...options, viewType: CollectionViewType.Schema });
}
export function TreeDocument(documents: Array<Doc>, options: DocumentOptions) {
- return CreateInstanceFromProto(Prototypes.collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Tree });
+ return InstanceFromProto(Prototypes.get(DocTypes.COL), new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Tree });
}
export function StackingDocument(documents: Array<Doc>, options: DocumentOptions) {
- return CreateInstanceFromProto(Prototypes.collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Stacking });
+ return InstanceFromProto(Prototypes.get(DocTypes.COL), new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Stacking });
}
export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
- return CreateInstanceFromProto(Prototypes.collProto, new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id);
+ return InstanceFromProto(Prototypes.get(DocTypes.COL), new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id);
+ }
+
+ export function DirectoryImportDocument(options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocTypes.COL), new List<Doc>(), options);
}
export type DocConfig = {
@@ -427,43 +415,88 @@ export namespace Docs {
{
type: type,
content: [
- ...configs.map(config => CollectionDockingView.makeDocumentConfig(config.doc, config.initialWidth))
+ ...configs.map(config => CollectionDockingView.makeDocumentConfig(config.doc, undefined, config.initialWidth))
]
}
]
};
return DockDocument(configs.map(c => c.doc), JSON.stringify(layoutConfig), options, id);
}
+ }
+
+ export namespace Get {
+
+ export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> {
+ let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined;
+ if (type.indexOf("image") !== -1) {
+ ctor = Docs.Create.ImageDocument;
+ }
+ if (type.indexOf("video") !== -1) {
+ ctor = Docs.Create.VideoDocument;
+ }
+ if (type.indexOf("audio") !== -1) {
+ ctor = Docs.Create.AudioDocument;
+ }
+ if (type.indexOf("pdf") !== -1) {
+ ctor = Docs.Create.PdfDocument;
+ options.nativeWidth = 1200;
+ }
+ if (type.indexOf("excel") !== -1) {
+ ctor = Docs.Create.DBDocument;
+ options.dropAction = "copy";
+ }
+ if (type.indexOf("html") !== -1) {
+ if (path.includes(window.location.hostname)) {
+ let s = path.split('/');
+ let id = s[s.length - 1];
+ return DocServer.GetRefField(id).then(field => {
+ if (field instanceof Doc) {
+ let alias = Doc.MakeAlias(field);
+ alias.x = options.x || 0;
+ alias.y = options.y || 0;
+ alias.width = options.width || 300;
+ alias.height = options.height || options.width || 300;
+ return alias;
+ }
+ return undefined;
+ });
+ }
+ ctor = Docs.Create.WebDocument;
+ options = { height: options.width, ...options, title: path, nativeWidth: undefined };
+ }
+ return ctor ? ctor(path, options) : undefined;
+ }
+
+ }
+
+ export namespace Templating {
export function CaptionDocument(doc: Doc) {
const captionDoc = Doc.MakeAlias(doc);
- captionDoc.overlayLayout = Templating.FixedCaption();
+ captionDoc.overlayLayout = FixedCaption();
captionDoc.width = Cast(doc.width, "number", 0);
captionDoc.height = Cast(doc.height, "number", 0);
return captionDoc;
}
- }
-
- export namespace Templating {
- // example of custom display string for an image that shows a caption.
+ /**
+ * An example of custom display string for an image that shows a caption.
+ */
export function EmbeddedCaption() {
- return `<div style="height:100%">
- <div style="position:relative; margin:auto; height:85%; width:85%;" >`
- + ImageBox.LayoutString() +
- `</div>
- <div style="position:relative; height:15%; text-align:center; ">`
- + FormattedTextBox.LayoutString("caption") +
- `</div>
- </div>`;
+ return (
+ `<div style="height:100%">
+ <div style="position:relative; margin:auto; height:85%; width:85%;" >${ImageBox.LayoutString()}</div>
+ <div style="position:relative; height:15%; text-align:center; ">${FormattedTextBox.LayoutString("caption")}</div>
+ </div>`
+ );
}
export function FixedCaption(fieldName: string = "caption") {
- return `<div style="position:absolute; height:30px; bottom:0; width:100%">
- <div style="position:absolute; width:100%; height:100%; text-align:center;bottom:0;">`
- + FormattedTextBox.LayoutString(fieldName) +
- `</div>
- </div>`;
+ return (
+ `<div style="position:absolute; height:30px; bottom:0; width:100%">
+ <div style="position:absolute; width:100%; height:100%; text-align:center;bottom:0;">${FormattedTextBox.LayoutString(fieldName)}</div>
+ </div>`
+ );
}
export function OuterCaption() {
@@ -473,7 +506,16 @@ export namespace Docs {
{layout}
</div>
<div style="height:(100% + 25px); width:100%; position:absolute">
- <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
+ <FormattedTextBox
+ doc={Document}
+ DocumentViewForField={DocumentView}
+ bindings={bindings}
+ fieldKey={"caption"}
+ isSelected={isSelected}
+ select={select}
+ selectOnLoad={SelectOnLoad}
+ renderDepth={renderDepth
+ />
</div>
</div>
`);
@@ -486,66 +528,55 @@ export namespace Docs {
{layout}
</div>
<div style="height:25px; width:100%; position:absolute">
- <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
- </div>
- </div>
- `);
- }
-
- /*
- this template requires an additional style setting on the collectionView-cont to make the layout relative
- .collectionView-cont {
- position: relative;
- width: 100%;
- height: 100%;
- }
- */
- export function PercentCaption() {
- return (`
- <div>
- <div style="margin:auto; height:85%; width:85%;">
- {layout}
- </div>
- <div style="height:15%; width:100%; position:absolute">
- <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
+ <FormattedTextBox
+ doc={Document}
+ DocumentViewForField={DocumentView}
+ bindings={bindings}
+ fieldKey={"caption"}
+ isSelected={isSelected}
+ select={select}
+ selectOnLoad={SelectOnLoad}
+ renderDepth={renderDepth
+ />
</div>
</div>
`);
}
-
}
}
export namespace DocUtils {
- export function MakeLink(source: Doc, target: Doc) {
- let protoSrc = source.proto ? source.proto : source;
- let protoTarg = target.proto ? target.proto : target;
+ export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", tags: string = "Default") {
+ if (LinkManager.Instance.doesLinkExist(source, target)) return;
+ let sv = DocumentManager.Instance.getDocumentView(source);
+ if (sv && sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === target) return;
+ if (target === CurrentUserUtils.UserDocument) return;
+
UndoManager.RunInBatch(() => {
- let linkDoc = Docs.Create.TextDocument({ width: 100, height: 30, borderRounding: -1 });
- //let linkDoc = new Doc;
- linkDoc.proto!.title = "-link name-";
- linkDoc.proto!.linkDescription = "";
- linkDoc.proto!.linkTags = "Default";
-
- linkDoc.proto!.linkedTo = target;
- linkDoc.proto!.linkedToPage = target.curPage;
- linkDoc.proto!.linkedFrom = source;
- linkDoc.proto!.linkedFromPage = source.curPage;
-
- let linkedFrom = Cast(protoTarg.linkedFromDocs, listSpec(Doc));
- if (!linkedFrom) {
- protoTarg.linkedFromDocs = linkedFrom = new List<Doc>();
- }
- linkedFrom.push(linkDoc);
+ let linkDoc = Docs.Create.TextDocument({ width: 100, height: 30, borderRounding: "100%" });
+ linkDoc.type = DocTypes.LINK;
+ let linkDocProto = Doc.GetProto(linkDoc);
+
+ linkDocProto.context = targetContext;
+ linkDocProto.title = title === "" ? source.title + " to " + target.title : title;
+ linkDocProto.linkDescription = description;
+ linkDocProto.linkTags = tags;
+ linkDocProto.type = DocTypes.LINK;
+
+ linkDocProto.anchor1 = source;
+ linkDocProto.anchor1Page = source.curPage;
+ linkDocProto.anchor1Groups = new List<Doc>([]);
+ linkDocProto.anchor2 = target;
+ linkDocProto.anchor2Page = target.curPage;
+ linkDocProto.anchor2Groups = new List<Doc>([]);
+
+ LinkManager.Instance.addLink(linkDoc);
- let linkedTo = Cast(protoSrc.linkedToDocs, listSpec(Doc));
- if (!linkedTo) {
- protoSrc.linkedToDocs = linkedTo = new List<Doc>();
- }
- linkedTo.push(linkDoc);
return linkDoc;
}, "make link");
}
-} \ No newline at end of file
+}
+
+Scripting.addGlobal("Docs", Docs);
diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
index 54c9c6068..28c009645 100644
--- a/src/client/goldenLayout.js
+++ b/src/client/goldenLayout.js
@@ -2271,6 +2271,17 @@
this._dragListener.on('dragStop', this._createDragListener, this);
},
+ destroy: function () {
+ this._dragListener.destroy();
+ this._element = null;
+ this._itemConfig = null;
+ this._dragListener = null;
+ const index = this._layoutManager._dragSources.indexOf(this);
+ if (index > -1) {
+ this._layoutManager._dragSources.splice(index, 1);
+ }
+ },
+
/**
* Callback for the DragListener's dragStart event
*
diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx
index d7732ee86..b81eafbee 100644
--- a/src/client/northstar/dash-nodes/HistogramBox.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBox.tsx
@@ -36,7 +36,7 @@ export class HistogramBox extends React.Component<FieldViewProps> {
@computed public get HistogramResult(): HistogramResult { return this.HistoOp.Result as HistogramResult; }
@observable public SizeConverter: SizeConverter = new SizeConverter();
- @computed get createOperationParamsCache() { trace(); return this.HistoOp.CreateOperationParameters(); }
+ @computed get createOperationParamsCache() { return this.HistoOp.CreateOperationParameters(); }
@computed get BinRanges() { return this.HistogramResult ? this.HistogramResult.binRanges : undefined; }
@computed get ChartType() {
return !this.BinRanges ? ChartType.SinglePoint : this.BinRanges[0] instanceof AggregateBinRange ?
@@ -125,8 +125,7 @@ export class HistogramBox extends React.Component<FieldViewProps> {
let mapped = brushingDocs.map((brush, i) => {
brush.backgroundColor = StyleConstants.BRUSH_COLORS[i % StyleConstants.BRUSH_COLORS.length];
let brushed = DocListCast(brush.brushingDocs);
- if (!brushed.length)
- return null;
+ if (!brushed.length) return null;
return { l: brush, b: brushed[0][Id] === proto[Id] ? brushed[1] : brushed[0] };
});
runInAction(() => this.HistoOp.BrushLinks.splice(0, this.HistoOp.BrushLinks.length, ...mapped.filter(m => m) as { l: Doc, b: Doc }[]));
diff --git a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
index 350987695..5a16b3782 100644
--- a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
@@ -62,7 +62,6 @@ export class HistogramBoxPrimitives extends React.Component<HistogramPrimitivesP
}
private renderGridLinesAndLabels(axis: number) {
- trace();
if (!this.props.HistoBox.SizeConverter.Initialized) {
return (null);
}
@@ -111,7 +110,6 @@ export class HistogramBoxPrimitives extends React.Component<HistogramPrimitivesP
x={transXpercent} width={`${widthXpercent}`} y={transYpercent} height={`${heightYpercent}`} fill={color ? `${LABColor.RGBtoHexString(color)}` : "transparent"} />);
}
render() {
- trace();
return <div className="histogramboxprimitives-container">
{this.xaxislines}
{this.yaxislines}
diff --git a/src/client/util/ClientUtils.ts.temp b/src/client/util/ClientUtils.ts.temp
new file mode 100644
index 000000000..f9fad5ed9
--- /dev/null
+++ b/src/client/util/ClientUtils.ts.temp
@@ -0,0 +1,3 @@
+export namespace ClientUtils {
+ export const RELEASE = "mode";
+} \ No newline at end of file
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 72d9f7e76..acd8dcef7 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,7 +1,7 @@
-import { computed, observable } from 'mobx';
+import { computed, observable, action } from 'mobx';
import { DocumentView } from '../views/nodes/DocumentView';
import { Doc, DocListCast, Opt } from '../../new_fields/Doc';
-import { FieldValue, Cast, NumCast, BoolCast } from '../../new_fields/Types';
+import { FieldValue, Cast, NumCast, BoolCast, StrCast } from '../../new_fields/Types';
import { listSpec } from '../../new_fields/Schema';
import { undoBatch } from './UndoManager';
import { CollectionDockingView } from '../views/collections/CollectionDockingView';
@@ -9,6 +9,8 @@ import { CollectionView } from '../views/collections/CollectionView';
import { CollectionPDFView } from '../views/collections/CollectionPDFView';
import { CollectionVideoView } from '../views/collections/CollectionVideoView';
import { Id } from '../../new_fields/FieldSymbols';
+import { LinkManager } from './LinkManager';
+import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
export class DocumentManager {
@@ -30,14 +32,37 @@ export class DocumentManager {
// this.DocumentViews = new Array<DocumentView>();
}
+ //gets all views
+ public getDocumentViewsById(id: string) {
+ let toReturn: DocumentView[] = [];
+ DocumentManager.Instance.DocumentViews.map(view => {
+ if (view.props.Document[Id] === id) {
+ toReturn.push(view);
+ }
+ });
+ if (toReturn.length === 0) {
+ DocumentManager.Instance.DocumentViews.map(view => {
+ let doc = view.props.Document.proto;
+ if (doc && doc[Id]) {
+ if (doc[Id] === id) { toReturn.push(view); }
+ }
+ });
+ }
+ return toReturn;
+ }
+
+ public getAllDocumentViews(doc: Doc) {
+ return this.getDocumentViewsById(doc[Id]);
+ }
+
public getDocumentViewById(id: string, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null {
let toReturn: DocumentView | null = null;
let passes = preferredCollection ? [preferredCollection, undefined] : [undefined];
- for (let i = 0; i < passes.length; i++) {
+ for (let pass of passes) {
DocumentManager.Instance.DocumentViews.map(view => {
- if (view.props.Document[Id] === id && (!passes[i] || view.props.ContainingCollectionView === preferredCollection)) {
+ if (view.props.Document[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) {
toReturn = view;
return;
}
@@ -45,7 +70,7 @@ export class DocumentManager {
if (!toReturn) {
DocumentManager.Instance.DocumentViews.map(view => {
let doc = view.props.Document.proto;
- if (doc && doc[Id] === id && (!passes[i] || view.props.ContainingCollectionView === preferredCollection)) {
+ if (doc && doc[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) {
toReturn = view;
}
});
@@ -66,13 +91,11 @@ export class DocumentManager {
//gets document view that is in a freeform canvas collection
DocumentManager.Instance.DocumentViews.map(view => {
let doc = view.props.Document;
- // if (view.props.ContainingCollectionView instanceof CollectionFreeFormView) {
if (doc === toFind) {
toReturn.push(view);
} else {
- let docSrc = FieldValue(doc.proto);
- if (docSrc && Object.is(docSrc, toFind)) {
+ if (Doc.AreProtosEqual(doc, toFind)) {
toReturn.push(view);
}
}
@@ -83,39 +106,27 @@ export class DocumentManager {
@computed
public get LinkedDocumentViews() {
- return DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
- let linksList = DocListCast(dv.props.Document.linkedToDocs);
- if (linksList && linksList.length) {
- pairs.push(...linksList.reduce((pairs, link) => {
- if (link) {
- let linkToDoc = FieldValue(Cast(link.linkedTo, Doc));
- if (linkToDoc) {
- DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 =>
- pairs.push({ a: dv, b: docView1, l: link }));
- }
- }
- return pairs;
- }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
- }
- linksList = DocListCast(dv.props.Document.linkedFromDocs);
- if (linksList && linksList.length) {
- pairs.push(...linksList.reduce((pairs, link) => {
- if (link) {
- let linkFromDoc = FieldValue(Cast(link.linkedFrom, Doc));
- if (linkFromDoc) {
- DocumentManager.Instance.getDocumentViews(linkFromDoc).map(docView1 =>
- pairs.push({ a: dv, b: docView1, l: link }));
- }
- }
- return pairs;
- }, pairs));
- }
+ let pairs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
+ let linksList = LinkManager.Instance.getAllRelatedLinks(dv.props.Document);
+ pairs.push(...linksList.reduce((pairs, link) => {
+ if (link) {
+ let linkToDoc = LinkManager.Instance.getOppositeAnchor(link, dv.props.Document);
+ DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
+ pairs.push({ a: dv, b: docView1, l: link });
+ });
+ }
+ return pairs;
+ }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
+ // }
return pairs;
}, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
+
+ return pairs;
}
+
@undoBatch
- public jumpToDocument = async (docDelegate: Doc, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number): Promise<void> => {
+ public jumpToDocument = async (docDelegate: Doc, willZoom: boolean, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number, docContext?: Doc): Promise<void> => {
let doc = Doc.GetProto(docDelegate);
const contextDoc = await Cast(doc.annotationOn, Doc);
if (contextDoc) {
@@ -123,28 +134,68 @@ export class DocumentManager {
const curPage = NumCast(contextDoc.curPage, page);
if (page !== curPage) contextDoc.curPage = page;
}
+
let docView: DocumentView | null;
// using forceDockFunc as a flag for splitting linked to doc to the right...can change later if needed
if (!forceDockFunc && (docView = DocumentManager.Instance.getDocumentView(doc))) {
docView.props.Document.libraryBrush = true;
if (linkPage !== undefined) docView.props.Document.curPage = linkPage;
- docView.props.focus(docView.props.Document);
+ docView.props.focus(docView.props.Document, willZoom);
} else {
if (!contextDoc) {
- const actualDoc = Doc.MakeAlias(docDelegate);
- actualDoc.libraryBrush = true;
- if (linkPage !== undefined) actualDoc.curPage = linkPage;
- (dockFunc || CollectionDockingView.AddRightSplit)(actualDoc);
+ if (docContext) {
+ let targetContextView: DocumentView | null;
+ if (!forceDockFunc && docContext && (targetContextView = DocumentManager.Instance.getDocumentView(docContext))) {
+ docContext.panTransformType = "Ease";
+ targetContextView.props.focus(docDelegate, willZoom);
+ } else {
+ (dockFunc || CollectionDockingView.Instance.AddRightSplit)(docContext, undefined);
+ setTimeout(() => {
+ this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage);
+ }, 10);
+ }
+ } else {
+ const actualDoc = Doc.MakeAlias(docDelegate);
+ actualDoc.libraryBrush = true;
+ if (linkPage !== undefined) actualDoc.curPage = linkPage;
+ (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc, undefined);
+ }
} else {
let contextView: DocumentView | null;
docDelegate.libraryBrush = true;
if (!forceDockFunc && (contextView = DocumentManager.Instance.getDocumentView(contextDoc))) {
contextDoc.panTransformType = "Ease";
- contextView.props.focus(contextDoc);
+ contextView.props.focus(docDelegate, willZoom);
} else {
- (dockFunc || CollectionDockingView.AddRightSplit)(contextDoc);
+ (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc, undefined);
+ setTimeout(() => {
+ this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage);
+ }, 10);
}
}
}
}
+
+ @action
+ zoomIntoScale = (docDelegate: Doc, scale: number) => {
+ let doc = Doc.GetProto(docDelegate);
+
+ let docView: DocumentView | null;
+ docView = DocumentManager.Instance.getDocumentView(doc);
+ if (docView) {
+ docView.props.zoomToScale(scale);
+ }
+ }
+
+ getScaleOfDocView = (docDelegate: Doc) => {
+ let doc = Doc.GetProto(docDelegate);
+
+ let docView: DocumentView | null;
+ docView = DocumentManager.Instance.getDocumentView(doc);
+ if (docView) {
+ return docView.props.getScale();
+ } else {
+ return 1;
+ }
+ }
} \ No newline at end of file
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 7625b0463..cb71db2c5 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,62 +1,99 @@
import { action, runInAction } from "mobx";
-import { Doc, DocListCastAsync } from "../../new_fields/Doc";
+import { Doc } from "../../new_fields/Doc";
import { Cast } from "../../new_fields/Types";
+import { URLField } from "../../new_fields/URLField";
import { emptyFunction } from "../../Utils";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import * as globalCssVariables from "../views/globalCssVariables.scss";
+import { DocumentManager } from "./DocumentManager";
+import { LinkManager } from "./LinkManager";
+import { SelectionManager } from "./SelectionManager";
export type dropActionType = "alias" | "copy" | undefined;
-export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc>, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType) {
+export function SetupDrag(
+ _reference: React.RefObject<HTMLElement>,
+ docFunc: () => Doc | Promise<Doc>,
+ moveFunc?: DragManager.MoveFunction,
+ dropAction?: dropActionType,
+ options?: any,
+ dontHideOnDrop?: boolean,
+ dragStarted?: () => void
+) {
let onRowMove = async (e: PointerEvent) => {
e.stopPropagation();
e.preventDefault();
document.removeEventListener("pointermove", onRowMove);
document.removeEventListener('pointerup', onRowUp);
- var dragData = new DragManager.DocumentDragData([await docFunc()]);
+ let doc = await docFunc();
+ var dragData = new DragManager.DocumentDragData([doc], [undefined]);
dragData.dropAction = dropAction;
dragData.moveDocument = moveFunc;
+ dragData.options = options;
+ dragData.dontHideOnDrop = dontHideOnDrop;
DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y);
+ dragStarted && dragStarted();
};
let onRowUp = (): void => {
document.removeEventListener("pointermove", onRowMove);
document.removeEventListener('pointerup', onRowUp);
};
let onItemDown = async (e: React.PointerEvent) => {
- // if (this.props.isSelected() || this.props.isTopMost) {
if (e.button === 0) {
e.stopPropagation();
- if (e.shiftKey && CollectionDockingView.TopLevel) {
- CollectionDockingView.TopLevel.StartOtherDrag([await docFunc()], e);
+ if (e.shiftKey && CollectionDockingView.Instance) {
+ e.persist();
+ CollectionDockingView.Instance.StartOtherDrag({
+ pageX: e.pageX,
+ pageY: e.pageY,
+ preventDefault: emptyFunction,
+ button: 0
+ }, [await docFunc()]);
} else {
document.addEventListener("pointermove", onRowMove);
document.addEventListener("pointerup", onRowUp);
}
}
- //}
};
return onItemDown;
}
+export async function DragLinkAsDocument(dragEle: HTMLElement, x: number, y: number, linkDoc: Doc, sourceDoc: Doc) {
+ let draggeddoc = LinkManager.Instance.getOppositeAnchor(linkDoc, sourceDoc);
+
+ let moddrag = await Cast(draggeddoc.annotationOn, Doc);
+ let dragdocs = moddrag ? [moddrag] : [draggeddoc];
+ let dragData = new DragManager.DocumentDragData(dragdocs, dragdocs);
+ dragData.dropAction = "alias" as dropActionType;
+ DragManager.StartLinkedDocumentDrag([dragEle], sourceDoc, dragData, x, y, {
+ handlers: {
+ dragComplete: action(emptyFunction),
+ },
+ hideSource: false
+ });
+}
+
export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc) {
let srcTarg = sourceDoc.proto;
let draggedDocs: Doc[] = [];
- let draggedFromDocs: Doc[] = [];
+
if (srcTarg) {
- let linkToDocs = await DocListCastAsync(srcTarg.linkedToDocs);
- let linkFromDocs = await DocListCastAsync(srcTarg.linkedFromDocs);
- if (linkToDocs) draggedDocs = linkToDocs.map(linkDoc => Cast(linkDoc.linkedTo, Doc) as Doc);
- if (linkFromDocs) draggedFromDocs = linkFromDocs.map(linkDoc => Cast(linkDoc.linkedFrom, Doc) as Doc);
+ let linkDocs = LinkManager.Instance.getAllRelatedLinks(srcTarg);
+ if (linkDocs) {
+ draggedDocs = linkDocs.map(link => {
+ return LinkManager.Instance.getOppositeAnchor(link, sourceDoc);
+ });
+ }
}
- draggedDocs.push(...draggedFromDocs);
if (draggedDocs.length) {
let moddrag: Doc[] = [];
for (const draggedDoc of draggedDocs) {
let doc = await Cast(draggedDoc.annotationOn, Doc);
if (doc) moddrag.push(doc);
}
- let dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs);
- DragManager.StartDocumentDrag([dragEle], dragData, x, y, {
+ let dragdocs = moddrag.length ? moddrag : draggedDocs;
+ let dragData = new DragManager.DocumentDragData(dragdocs, dragdocs);
+ DragManager.StartLinkedDocumentDrag([dragEle], sourceDoc, dragData, x, y, {
handlers: {
dragComplete: action(emptyFunction),
},
@@ -86,6 +123,10 @@ export namespace DragManager {
handlers: DragHandlers;
hideSource: boolean | (() => boolean);
+
+ dragHasStarted?: () => void;
+
+ withoutShiftDrag?: boolean;
}
export interface DragDropDisposer {
@@ -137,13 +178,15 @@ export namespace DragManager {
export type MoveFunction = (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
export class DocumentDragData {
- constructor(dragDoc: Doc[]) {
+ constructor(dragDoc: Doc[], dragDataDocs: (Doc | undefined)[]) {
this.draggedDocuments = dragDoc;
+ this.draggedDataDocs = dragDataDocs;
this.droppedDocuments = dragDoc;
this.xOffset = 0;
this.yOffset = 0;
}
draggedDocuments: Doc[];
+ draggedDataDocs: (Doc | undefined)[];
droppedDocuments: Doc[];
xOffset: number;
yOffset: number;
@@ -153,17 +196,64 @@ export namespace DragManager {
[id: string]: any;
}
+ export class AnnotationDragData {
+ constructor(dragDoc: Doc, annotationDoc: Doc, dropDoc: Doc) {
+ this.dragDocument = dragDoc;
+ this.dropDocument = dropDoc;
+ this.annotationDocument = annotationDoc;
+ this.xOffset = this.yOffset = 0;
+ }
+ dragDocument: Doc;
+ annotationDocument: Doc;
+ dropDocument: Doc;
+ xOffset: number;
+ yOffset: number;
+ dropAction: dropActionType;
+ userDropAction: dropActionType;
+ }
+
export let StartDragFunctions: (() => void)[] = [];
export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
runInAction(() => StartDragFunctions.map(func => func()));
StartDrag(eles, dragData, downX, downY, options,
- (dropData: { [id: string]: any }) =>
+ (dropData: { [id: string]: any }) => {
(dropData.droppedDocuments = dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ?
dragData.draggedDocuments.map(d => Doc.MakeAlias(d)) :
dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ?
dragData.draggedDocuments.map(d => Doc.MakeCopy(d, true)) :
- dragData.draggedDocuments));
+ dragData.draggedDocuments
+ );
+ });
+ }
+
+ export function StartLinkedDocumentDrag(eles: HTMLElement[], sourceDoc: Doc, dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
+
+ runInAction(() => StartDragFunctions.map(func => func()));
+ StartDrag(eles, dragData, downX, downY, options,
+ (dropData: { [id: string]: any }) => {
+ // dropData.droppedDocuments =
+ let droppedDocuments: Doc[] = dragData.draggedDocuments.reduce((droppedDocs: Doc[], d) => {
+ let dvs = DocumentManager.Instance.getDocumentViews(d);
+
+ if (dvs.length) {
+ let inContext = dvs.filter(dv => dv.props.ContainingCollectionView === SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView);
+ if (inContext.length) {
+ inContext.forEach(dv => droppedDocs.push(dv.props.Document));
+ } else {
+ droppedDocs.push(Doc.MakeAlias(d));
+ }
+ } else {
+ droppedDocs.push(Doc.MakeAlias(d));
+ }
+ return droppedDocs;
+ }, []);
+ dropData.droppedDocuments = droppedDocuments;
+ });
+ }
+
+ export function StartAnnotationDrag(eles: HTMLElement[], dragData: AnnotationDragData, downX: number, downY: number, options?: DragOptions) {
+ StartDrag(eles, dragData, downX, downY, options);
}
export class LinkDragData {
@@ -178,27 +268,44 @@ export namespace DragManager {
[id: string]: any;
}
+ export class EmbedDragData {
+ constructor(embeddableSourceDoc: Doc) {
+ this.embeddableSourceDoc = embeddableSourceDoc;
+ this.urlField = embeddableSourceDoc.data instanceof URLField ? embeddableSourceDoc.data : undefined;
+ }
+ embeddableSourceDoc: Doc;
+ urlField?: URLField;
+ [id: string]: any;
+ }
+
export function StartLinkDrag(ele: HTMLElement, dragData: LinkDragData, downX: number, downY: number, options?: DragOptions) {
StartDrag([ele], dragData, downX, downY, options);
}
+ export function StartEmbedDrag(ele: HTMLElement, dragData: EmbedDragData, downX: number, downY: number, options?: DragOptions) {
+ StartDrag([ele], dragData, downX, downY, options);
+ }
+
export let AbortDrag: () => void = emptyFunction;
function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: { [id: string]: any }) => void) {
+ eles = eles.filter(e => e);
if (!dragDiv) {
dragDiv = document.createElement("div");
dragDiv.className = "dragManager-dragDiv";
dragDiv.style.pointerEvents = "none";
DragManager.Root().appendChild(dragDiv);
}
-
+ SelectionManager.SetIsDragging(true);
let scaleXs: number[] = [];
let scaleYs: number[] = [];
let xs: number[] = [];
let ys: number[] = [];
const docs: Doc[] =
- dragData instanceof DocumentDragData ? dragData.draggedDocuments : [];
+ dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnnotationDragData ? [dragData.dragDocument] : [];
+ const datadocs: (Doc | undefined)[] =
+ dragData instanceof DocumentDragData ? dragData.draggedDataDocs : dragData instanceof AnnotationDragData ? [dragData.dragDocument] : [];
let dragElements = eles.map(ele => {
const w = ele.offsetWidth,
h = ele.offsetHeight;
@@ -213,11 +320,13 @@ export namespace DragManager {
scaleYs.push(scaleY);
let dragElement = ele.cloneNode(true) as HTMLElement;
dragElement.style.opacity = "0.7";
+ dragElement.style.borderRadius = getComputedStyle(ele).borderRadius;
dragElement.style.position = "absolute";
dragElement.style.margin = "0";
dragElement.style.top = "0";
dragElement.style.bottom = "";
dragElement.style.left = "0";
+ dragElement.style.transition = "none";
dragElement.style.color = "black";
dragElement.style.transformOrigin = "0 0";
dragElement.style.zIndex = globalCssVariables.contextMenuZindex;// "1000";
@@ -241,6 +350,16 @@ export namespace DragManager {
// pdfBox.replaceChild(img, pdfBox.children[0])
// }
// }
+ let set = dragElement.getElementsByTagName('*');
+ if (dragElement.hasAttribute("style")) (dragElement as any).style.pointerEvents = "none";
+ // tslint:disable-next-line: prefer-for-of
+ for (let i = 0; i < set.length; i++) {
+ if (set[i].hasAttribute("style")) {
+ let s = set[i];
+ (s as any).style.pointerEvents = "none";
+ }
+ }
+
dragDiv.appendChild(dragElement);
return dragElement;
@@ -259,19 +378,18 @@ export namespace DragManager {
let lastX = downX;
let lastY = downY;
const moveHandler = (e: PointerEvent) => {
- e.stopPropagation();
- e.preventDefault();
+ e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop
if (dragData instanceof DocumentDragData) {
dragData.userDropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
}
- if (e.shiftKey && CollectionDockingView.TopLevel) {
+ if (((options && !options.withoutShiftDrag) || !options) && e.shiftKey && CollectionDockingView.Instance) {
AbortDrag();
- CollectionDockingView.TopLevel.StartOtherDrag(docs, {
+ CollectionDockingView.Instance.StartOtherDrag({
pageX: e.pageX,
pageY: e.pageY,
preventDefault: emptyFunction,
button: 0
- });
+ }, docs, datadocs);
}
//TODO: Why can't we use e.movementX and e.movementY?
let moveX = e.pageX - lastX;
@@ -284,6 +402,7 @@ export namespace DragManager {
};
let hideDragElements = () => {
+ SelectionManager.SetIsDragging(false);
dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));
eles.map(ele => (ele.hidden = false));
};
@@ -309,7 +428,7 @@ export namespace DragManager {
}
function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) {
- let removed = dragEles.map(dragEle => {
+ let removed = dragData.dontHideOnDrop ? [] : dragEles.map(dragEle => {
// let parent = dragEle.parentElement;
// if (parent) parent.removeChild(dragEle);
let ret = [dragEle, dragEle.style.width, dragEle.style.height];
@@ -335,7 +454,7 @@ export namespace DragManager {
x: e.x,
y: e.y,
data: dragData,
- mods: e.altKey ? "AltKey" : ""
+ mods: e.altKey ? "AltKey" : e.ctrlKey ? "CtrlKey" : ""
}
})
);
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index 94bfcbe09..545ea8629 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -88,7 +88,7 @@ export namespace HistoryUtil {
}
export function createUrl(params: ParsedUrl): string {
- let baseUrl = DocServer.Util.prepend(`/${params.type}`);
+ let baseUrl = DocServer.prepend(`/${params.type}`);
switch (params.type) {
case "doc":
const initializers = encodeURIComponent(JSON.stringify(params.initializers));
@@ -103,7 +103,7 @@ export namespace HistoryUtil {
}
export async function initDoc(id: string, initializer: DocInitializerList) {
- const doc = await DocServer.getRefField(id);
+ const doc = await DocServer.GetRefField(id);
if (!(doc instanceof Doc)) {
return;
}
@@ -111,7 +111,7 @@ export namespace HistoryUtil {
}
async function onDocUrl(url: DocUrl) {
- const field = await DocServer.getRefField(url.docId);
+ const field = await DocServer.GetRefField(url.docId);
await Promise.all(Object.keys(url.initializers).map(id => initDoc(id, url.initializers[id])));
if (field instanceof Doc) {
MainView.Instance.openWorkspace(field, true);
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
new file mode 100644
index 000000000..a810db0fa
--- /dev/null
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx
@@ -0,0 +1,376 @@
+import "fs";
+import React = require("react");
+import { Doc, Opt, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
+import { DocServer } from "../../DocServer";
+import { RouteStore } from "../../../server/RouteStore";
+import { action, observable, autorun, runInAction, computed } from "mobx";
+import { FieldViewProps, FieldView } from "../../views/nodes/FieldView";
+import Measure, { ContentRect } from "react-measure";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faArrowUp, faTag, faPlus } from '@fortawesome/free-solid-svg-icons';
+import { Docs, DocumentOptions } from "../../documents/Documents";
+import { observer } from "mobx-react";
+import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from "./ImportMetadataEntry";
+import { Utils } from "../../../Utils";
+import { DocumentManager } from "../DocumentManager";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { List } from "../../../new_fields/List";
+import { Cast, BoolCast, NumCast } from "../../../new_fields/Types";
+import { listSpec } from "../../../new_fields/Schema";
+
+const unsupported = ["text/html", "text/plain"];
+
+@observer
+export default class DirectoryImportBox extends React.Component<FieldViewProps> {
+ private selector = React.createRef<HTMLInputElement>();
+ @observable private top = 0;
+ @observable private left = 0;
+ private dimensions = 50;
+
+ @observable private entries: ImportMetadataEntry[] = [];
+
+ @observable private quota = 1;
+ @observable private remaining = 1;
+
+ @observable private uploading = false;
+ @observable private removeHover = false;
+
+ public static LayoutString() { return FieldView.LayoutString(DirectoryImportBox); }
+
+ constructor(props: FieldViewProps) {
+ super(props);
+ library.add(faArrowUp, faTag, faPlus);
+ let doc = this.props.Document;
+ this.editingMetadata = this.editingMetadata || false;
+ this.persistent = this.persistent || false;
+ !Cast(doc.data, listSpec(Doc)) && (doc.data = new List<Doc>());
+ }
+
+ @computed
+ private get editingMetadata() {
+ return BoolCast(this.props.Document.editingMetadata);
+ }
+
+ private set editingMetadata(value: boolean) {
+ this.props.Document.editingMetadata = value;
+ }
+
+ @computed
+ private get persistent() {
+ return BoolCast(this.props.Document.persistent);
+ }
+
+ private set persistent(value: boolean) {
+ this.props.Document.persistent = value;
+ }
+
+ handleSelection = async (e: React.ChangeEvent<HTMLInputElement>) => {
+ runInAction(() => this.uploading = true);
+
+ let promises: Promise<void>[] = [];
+ let docs: Doc[] = [];
+
+ let files = e.target.files;
+ if (!files || files.length === 0) return;
+
+ let directory = (files.item(0) as any).webkitRelativePath.split("/", 1);
+
+ let validated: File[] = [];
+ for (let i = 0; i < files.length; i++) {
+ let file = files.item(i);
+ file && !unsupported.includes(file.type) && validated.push(file);
+ }
+
+ runInAction(() => this.quota = validated.length);
+
+ let sizes = [];
+ let modifiedDates = [];
+
+ for (let uploaded_file of validated) {
+ let formData = new FormData();
+ formData.append('file', uploaded_file);
+ let dropFileName = uploaded_file ? uploaded_file.name : "-empty-";
+ let type = uploaded_file.type;
+
+ sizes.push(uploaded_file.size);
+ modifiedDates.push(uploaded_file.lastModified);
+
+ runInAction(() => this.remaining++);
+
+ let prom = fetch(DocServer.prepend(RouteStore.upload), {
+ method: 'POST',
+ body: formData
+ }).then(async (res: Response) => {
+ (await res.json()).map(action((file: any) => {
+ let docPromise = Docs.Get.DocumentFromType(type, DocServer.prepend(file), { nativeWidth: 300, width: 300, title: dropFileName });
+ docPromise.then(doc => {
+ doc && docs.push(doc) && runInAction(() => this.remaining--);
+ });
+ }));
+ });
+ promises.push(prom);
+ }
+
+ await Promise.all(promises);
+
+ for (let i = 0; i < docs.length; i++) {
+ let doc = docs[i];
+ doc.size = sizes[i];
+ doc.modified = modifiedDates[i];
+ this.entries.forEach(entry => {
+ let target = entry.onDataDoc ? Doc.GetProto(doc) : doc;
+ target[entry.key] = entry.value;
+ });
+ }
+
+ let doc = this.props.Document;
+ let height: number = NumCast(doc.height) || 0;
+ let offset: number = this.persistent ? (height === 0 ? 0 : height + 30) : 0;
+ let options: DocumentOptions = {
+ title: `Import of ${directory}`,
+ width: 1105,
+ height: 500,
+ x: NumCast(doc.x),
+ y: NumCast(doc.y) + offset
+ };
+ let parent = this.props.ContainingCollectionView;
+ if (parent) {
+ let importContainer = Docs.Create.StackingDocument(docs, options);
+ importContainer.singleColumn = false;
+ Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer);
+ !this.persistent && this.props.removeDocument && this.props.removeDocument(doc);
+ DocumentManager.Instance.jumpToDocument(importContainer, true);
+
+ }
+
+ runInAction(() => {
+ this.uploading = false;
+ this.quota = 1;
+ this.remaining = 1;
+ });
+ }
+
+ componentDidMount() {
+ this.selector.current!.setAttribute("directory", "");
+ this.selector.current!.setAttribute("webkitdirectory", "");
+ }
+
+ @action
+ preserveCentering = (rect: ContentRect) => {
+ let bounds = rect.offset!;
+ if (bounds.width === 0 || bounds.height === 0) {
+ return;
+ }
+ let offset = this.dimensions / 2;
+ this.left = bounds.width / 2 - offset;
+ this.top = bounds.height / 2 - offset;
+ }
+
+ @action
+ addMetadataEntry = async () => {
+ let entryDoc = new Doc();
+ entryDoc.checked = false;
+ entryDoc.key = keyPlaceholder;
+ entryDoc.value = valuePlaceholder;
+ Doc.AddDocToList(this.props.Document, "data", entryDoc);
+ }
+
+ @action
+ remove = async (entry: ImportMetadataEntry) => {
+ let metadata = await DocListCastAsync(this.props.Document.data);
+ if (metadata) {
+ let index = this.entries.indexOf(entry);
+ if (index !== -1) {
+ runInAction(() => this.entries.splice(index, 1));
+ index = metadata.indexOf(entry.props.Document);
+ if (index !== -1) {
+ metadata.splice(index, 1);
+ }
+ }
+
+ }
+ }
+
+ render() {
+ let dimensions = 50;
+ let entries = DocListCast(this.props.Document.data);
+ let isEditing = this.editingMetadata;
+ let remaining = this.remaining;
+ let quota = this.quota;
+ let uploading = this.uploading;
+ let showRemoveLabel = this.removeHover;
+ let persistent = this.persistent;
+ let percent = `${100 - (remaining / quota * 100)}`;
+ percent = percent.split(".")[0];
+ percent = percent.startsWith("100") ? "99" : percent;
+ let marginOffset = (percent.length === 1 ? 5 : 0) - 1.6;
+ return (
+ <Measure offset onResize={this.preserveCentering}>
+ {({ measureRef }) =>
+ <div ref={measureRef} style={{ width: "100%", height: "100%", pointerEvents: "all" }} >
+ <input
+ id={"selector"}
+ ref={this.selector}
+ onChange={this.handleSelection}
+ type="file"
+ style={{
+ position: "absolute",
+ display: "none"
+ }} />
+ <label
+ htmlFor={"selector"}
+ style={{
+ opacity: isEditing ? 0 : 1,
+ pointerEvents: isEditing ? "none" : "all",
+ transition: "0.4s ease opacity"
+ }}
+ >
+ <div style={{
+ width: dimensions,
+ height: dimensions,
+ borderRadius: "50%",
+ background: "black",
+ position: "absolute",
+ left: this.left,
+ top: this.top
+ }} />
+ <div style={{
+ position: "absolute",
+ left: this.left + 12.6,
+ top: this.top + 11,
+ opacity: uploading ? 0 : 1,
+ transition: "0.4s opacity ease"
+ }}>
+ <FontAwesomeIcon icon={faArrowUp} color="#FFFFFF" size={"2x"} />
+ </div>
+ <img
+ style={{
+ width: 80,
+ height: 80,
+ transition: "0.4s opacity ease",
+ opacity: uploading ? 0.7 : 0,
+ position: "absolute",
+ top: this.top - 15,
+ left: this.left - 15
+ }}
+ src={"/assets/loading.gif"}></img>
+ </label>
+ <input
+ type={"checkbox"}
+ onChange={e => runInAction(() => this.persistent = e.target.checked)}
+ style={{
+ margin: 0,
+ position: "absolute",
+ left: 10,
+ bottom: 10,
+ opacity: isEditing || uploading ? 0 : 1,
+ transition: "0.4s opacity ease",
+ pointerEvents: isEditing || uploading ? "none" : "all"
+ }}
+ checked={this.persistent}
+ onPointerEnter={action(() => this.removeHover = true)}
+ onPointerLeave={action(() => this.removeHover = false)}
+ />
+ <p
+ style={{
+ position: "absolute",
+ left: 27,
+ bottom: 8.4,
+ fontSize: 12,
+ opacity: showRemoveLabel ? 1 : 0,
+ transition: "0.4s opacity ease"
+ }}>Template will be <span style={{ textDecoration: "underline", textDecorationColor: persistent ? "green" : "red", color: persistent ? "green" : "red" }}>{persistent ? "kept" : "removed"}</span> after upload</p>
+ <div
+ style={{
+ transition: "0.4s opacity ease",
+ opacity: uploading ? 1 : 0,
+ pointerEvents: "none",
+ position: "absolute",
+ left: 10,
+ top: this.top + 12.3,
+ fontSize: 18,
+ color: "white",
+ marginLeft: this.left + marginOffset
+ }}>{percent}%</div>
+ <div
+ style={{
+ position: "absolute",
+ top: 10,
+ right: 10,
+ borderRadius: "50%",
+ width: 25,
+ height: 25,
+ background: "black",
+ pointerEvents: uploading ? "none" : "all",
+ opacity: uploading ? 0 : 1,
+ transition: "0.4s opacity ease"
+ }}
+ title={isEditing ? "Back to Upload" : "Add Metadata"}
+ onClick={action(() => this.editingMetadata = !this.editingMetadata)}
+ />
+ <FontAwesomeIcon
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ right: isEditing ? 16.3 : 14.5,
+ top: isEditing ? 15.4 : 16,
+ opacity: uploading ? 0 : 1,
+ transition: "0.4s opacity ease"
+ }}
+ icon={isEditing ? faArrowUp : faTag}
+ color="#FFFFFF"
+ size={"1x"}
+ />
+ <div
+ style={{
+ transition: "0.4s ease opacity",
+ width: "100%",
+ height: "100%",
+ pointerEvents: isEditing ? "all" : "none",
+ opacity: isEditing ? 1 : 0,
+ overflowY: "scroll"
+ }}
+ >
+ <div
+ style={{
+ borderRadius: "50%",
+ width: 25,
+ height: 25,
+ marginLeft: 10,
+ position: "absolute",
+ right: 41,
+ top: 10
+ }}
+ title={"Add Metadata Entry"}
+ onClick={this.addMetadataEntry}
+ >
+ <FontAwesomeIcon
+ style={{
+ pointerEvents: "none",
+ marginLeft: 6.4,
+ marginTop: 5.2
+ }}
+ icon={faPlus}
+ size={"1x"}
+ />
+ </div>
+ <p style={{ paddingLeft: 10, paddingTop: 8, paddingBottom: 7 }} >Add metadata to your import...</p>
+ <hr style={{ margin: "6px 10px 12px 10px" }} />
+ {entries.map(doc =>
+ <ImportMetadataEntry
+ Document={doc}
+ key={doc[Id]}
+ remove={this.remove}
+ ref={(el) => { if (el) this.entries.push(el); }}
+ next={this.addMetadataEntry}
+ />
+ )}
+ </div>
+ </div>
+ }
+ </Measure>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/util/Import & Export/ImportMetadataEntry.tsx b/src/client/util/Import & Export/ImportMetadataEntry.tsx
new file mode 100644
index 000000000..f5198c39b
--- /dev/null
+++ b/src/client/util/Import & Export/ImportMetadataEntry.tsx
@@ -0,0 +1,149 @@
+import React = require("react");
+import { observer } from "mobx-react";
+import { EditableView } from "../../views/EditableView";
+import { observable, action, computed } from "mobx";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faPlus } from "@fortawesome/free-solid-svg-icons";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { Opt, Doc } from "../../../new_fields/Doc";
+import { StrCast, BoolCast } from "../../../new_fields/Types";
+
+interface KeyValueProps {
+ Document: Doc;
+ remove: (self: ImportMetadataEntry) => void;
+ next: () => void;
+}
+
+export const keyPlaceholder = "Key";
+export const valuePlaceholder = "Value";
+
+@observer
+export default class ImportMetadataEntry extends React.Component<KeyValueProps> {
+
+ private keyRef = React.createRef<EditableView>();
+ private valueRef = React.createRef<EditableView>();
+ private checkRef = React.createRef<HTMLInputElement>();
+
+ constructor(props: KeyValueProps) {
+ super(props);
+ library.add(faPlus);
+ }
+
+ @computed
+ public get valid() {
+ return (this.key.length > 0 && this.key !== keyPlaceholder) && (this.value.length > 0 && this.value !== valuePlaceholder);
+ }
+
+ @computed
+ private get backing() {
+ return this.props.Document;
+ }
+
+ @computed
+ public get onDataDoc() {
+ return BoolCast(this.backing.checked);
+ }
+
+ public set onDataDoc(value: boolean) {
+ this.backing.checked = value;
+ }
+
+ @computed
+ public get key() {
+ return StrCast(this.backing.key);
+ }
+
+ public set key(value: string) {
+ this.backing.key = value;
+ }
+
+ @computed
+ public get value() {
+ return StrCast(this.backing.value);
+ }
+
+ public set value(value: string) {
+ this.backing.value = value;
+ }
+
+ @action
+ updateKey = (newKey: string) => {
+ this.key = newKey;
+ this.keyRef.current && this.keyRef.current.setIsFocused(false);
+ this.valueRef.current && this.valueRef.current.setIsFocused(true);
+ this.key.length === 0 && (this.key = keyPlaceholder);
+ return true;
+ }
+
+ @action
+ updateValue = (newValue: string, shiftDown: boolean) => {
+ this.value = newValue;
+ this.valueRef.current && this.valueRef.current.setIsFocused(false);
+ this.value.length > 0 && shiftDown && this.props.next();
+ this.value.length === 0 && (this.value = valuePlaceholder);
+ return true;
+ }
+
+ render() {
+ let keyValueStyle: React.CSSProperties = {
+ paddingLeft: 10,
+ width: "50%",
+ opacity: this.valid ? 1 : 0.5,
+ };
+ return (
+ <div
+ style={{
+ display: "flex",
+ flexDirection: "row",
+ paddingBottom: 5,
+ paddingRight: 5,
+ justifyContent: "center",
+ alignItems: "center",
+ alignContent: "center"
+ }}
+ >
+ <input
+ onChange={e => this.onDataDoc = e.target.checked}
+ ref={this.checkRef}
+ style={{ margin: "0 10px 0 15px" }}
+ type="checkbox"
+ title={"Add to Data Document?"}
+ checked={this.onDataDoc}
+ />
+ <div className={"key_container"} style={keyValueStyle}>
+ <EditableView
+ ref={this.keyRef}
+ contents={this.key}
+ SetValue={this.updateKey}
+ GetValue={() => ""}
+ oneLine={true}
+ />
+ </div>
+ <div
+ className={"value_container"}
+ style={keyValueStyle}>
+ <EditableView
+ ref={this.valueRef}
+ contents={this.value}
+ SetValue={this.updateValue}
+ GetValue={() => ""}
+ oneLine={true}
+ />
+ </div>
+ <div onClick={() => this.props.remove(this)} title={"Delete Entry"}>
+ <FontAwesomeIcon
+ icon={faPlus}
+ color={"red"}
+ size={"1x"}
+ style={{
+ marginLeft: 15,
+ marginRight: 15,
+ transform: "rotate(45deg)"
+ }}
+ />
+ </div>
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
new file mode 100644
index 000000000..d6ea6013b
--- /dev/null
+++ b/src/client/util/LinkManager.ts
@@ -0,0 +1,242 @@
+import { observable, action } from "mobx";
+import { StrCast, Cast, FieldValue } from "../../new_fields/Types";
+import { Doc, DocListCast } from "../../new_fields/Doc";
+import { listSpec } from "../../new_fields/Schema";
+import { List } from "../../new_fields/List";
+import { Id } from "../../new_fields/FieldSymbols";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+
+
+/*
+ * link doc:
+ * - anchor1: doc
+ * - anchor1page: number
+ * - anchor1groups: list of group docs representing the groups anchor1 categorizes this link/anchor2 in
+ * - anchor2: doc
+ * - anchor2page: number
+ * - anchor2groups: list of group docs representing the groups anchor2 categorizes this link/anchor1 in
+ *
+ * group doc:
+ * - type: string representing the group type/name/category
+ * - metadata: doc representing the metadata kvps
+ *
+ * metadata doc:
+ * - user defined kvps
+ */
+export class LinkManager {
+
+ private static _instance: LinkManager;
+ public static get Instance(): LinkManager {
+ return this._instance || (this._instance = new this());
+ }
+ private constructor() {
+ }
+
+ // the linkmanagerdoc stores a list of docs representing all linkdocs in 'allLinks' and a list of strings representing all group types in 'allGroupTypes'
+ // lists of strings representing the metadata keys for each group type is stored under a key that is the same as the group type
+ public get LinkManagerDoc(): Doc | undefined {
+ return FieldValue(Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc));
+ }
+
+ public getAllLinks(): Doc[] {
+ return LinkManager.Instance.LinkManagerDoc ? LinkManager.Instance.LinkManagerDoc.allLinks ? DocListCast(LinkManager.Instance.LinkManagerDoc.allLinks) : [] : [];
+ }
+
+ public addLink(linkDoc: Doc): boolean {
+ let linkList = LinkManager.Instance.getAllLinks();
+ linkList.push(linkDoc);
+ if (LinkManager.Instance.LinkManagerDoc) {
+ LinkManager.Instance.LinkManagerDoc.allLinks = new List<Doc>(linkList);
+ return true;
+ }
+ return false;
+ }
+
+ public deleteLink(linkDoc: Doc): boolean {
+ let linkList = LinkManager.Instance.getAllLinks();
+ let index = LinkManager.Instance.getAllLinks().indexOf(linkDoc);
+ if (index > -1) {
+ linkList.splice(index, 1);
+ if (LinkManager.Instance.LinkManagerDoc) {
+ LinkManager.Instance.LinkManagerDoc.allLinks = new List<Doc>(linkList);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // finds all links that contain the given anchor
+ public getAllRelatedLinks(anchor: Doc): Doc[] {//List<Doc> {
+ let related = LinkManager.Instance.getAllLinks().filter(link => {
+ let protomatch1 = Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, null));
+ let protomatch2 = Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, null));
+ return protomatch1 || protomatch2;
+ });
+ return related;
+ }
+
+ public deleteAllLinksOnAnchor(anchor: Doc) {
+ let related = LinkManager.Instance.getAllRelatedLinks(anchor);
+ related.forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc));
+ }
+
+ public addGroupType(groupType: string): boolean {
+ if (LinkManager.Instance.LinkManagerDoc) {
+ LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>([]);
+ let groupTypes = LinkManager.Instance.getAllGroupTypes();
+ groupTypes.push(groupType);
+ LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>(groupTypes);
+ return true;
+ }
+ return false;
+ }
+
+ // removes all group docs from all links with the given group type
+ public deleteGroupType(groupType: string): boolean {
+ if (LinkManager.Instance.LinkManagerDoc) {
+ if (LinkManager.Instance.LinkManagerDoc[groupType]) {
+ let groupTypes = LinkManager.Instance.getAllGroupTypes();
+ let index = groupTypes.findIndex(type => type.toUpperCase() === groupType.toUpperCase());
+ if (index > -1) groupTypes.splice(index, 1);
+ LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>(groupTypes);
+ LinkManager.Instance.LinkManagerDoc[groupType] = undefined;
+ LinkManager.Instance.getAllLinks().forEach(async linkDoc => {
+ const anchor1 = await Cast(linkDoc.anchor1, Doc);
+ const anchor2 = await Cast(linkDoc.anchor2, Doc);
+ anchor1 && LinkManager.Instance.removeGroupFromAnchor(linkDoc, anchor1, groupType);
+ anchor2 && LinkManager.Instance.removeGroupFromAnchor(linkDoc, anchor2, groupType);
+ });
+ }
+ return true;
+ } else return false;
+ }
+
+ public getAllGroupTypes(): string[] {
+ if (LinkManager.Instance.LinkManagerDoc) {
+ if (LinkManager.Instance.LinkManagerDoc.allGroupTypes) {
+ return Cast(LinkManager.Instance.LinkManagerDoc.allGroupTypes, listSpec("string"), []);
+ } else {
+ LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>([]);
+ return [];
+ }
+ }
+ return [];
+ }
+
+ // gets the groups associates with an anchor in a link
+ public getAnchorGroups(linkDoc: Doc, anchor?: Doc): Array<Doc> {
+ if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, null))) {
+ return DocListCast(linkDoc.anchor1Groups);
+ } else {
+ return DocListCast(linkDoc.anchor2Groups);
+ }
+ }
+
+ // sets the groups of the given anchor in the given link
+ public setAnchorGroups(linkDoc: Doc, anchor: Doc, groups: Doc[]) {
+ if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, null))) {
+ linkDoc.anchor1Groups = new List<Doc>(groups);
+ } else {
+ linkDoc.anchor2Groups = new List<Doc>(groups);
+ }
+ }
+
+ public addGroupToAnchor(linkDoc: Doc, anchor: Doc, groupDoc: Doc, replace: boolean = false) {
+ let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
+ let index = groups.findIndex(gDoc => {
+ return StrCast(groupDoc.type).toUpperCase() === StrCast(gDoc.type).toUpperCase();
+ });
+ if (index > -1 && replace) {
+ groups[index] = groupDoc;
+ }
+ if (index === -1) {
+ groups.push(groupDoc);
+ }
+ LinkManager.Instance.setAnchorGroups(linkDoc, anchor, groups);
+ }
+
+ // removes group doc of given group type only from given anchor on given link
+ public removeGroupFromAnchor(linkDoc: Doc, anchor: Doc, groupType: string) {
+ let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
+ let newGroups = groups.filter(groupDoc => StrCast(groupDoc.type).toUpperCase() !== groupType.toUpperCase());
+ LinkManager.Instance.setAnchorGroups(linkDoc, anchor, newGroups);
+ }
+
+ // returns map of group type to anchor's links in that group type
+ public getRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> {
+ let related = this.getAllRelatedLinks(anchor);
+ let anchorGroups = new Map<string, Array<Doc>>();
+ related.forEach(link => {
+ let groups = LinkManager.Instance.getAnchorGroups(link, anchor);
+
+ if (groups.length > 0) {
+ groups.forEach(groupDoc => {
+ let groupType = StrCast(groupDoc.type);
+ if (groupType === "") {
+ let group = anchorGroups.get("*");
+ anchorGroups.set("*", group ? [...group, link] : [link]);
+ } else {
+ let group = anchorGroups.get(groupType);
+ anchorGroups.set(groupType, group ? [...group, link] : [link]);
+ }
+ });
+ } else {
+ // if link is in no groups then put it in default group
+ let group = anchorGroups.get("*");
+ anchorGroups.set("*", group ? [...group, link] : [link]);
+ }
+
+ });
+ return anchorGroups;
+ }
+
+ // gets a list of strings representing the keys of the metadata associated with the given group type
+ public getMetadataKeysInGroup(groupType: string): string[] {
+ if (LinkManager.Instance.LinkManagerDoc) {
+ return LinkManager.Instance.LinkManagerDoc[groupType] ? Cast(LinkManager.Instance.LinkManagerDoc[groupType], listSpec("string"), []) : [];
+ }
+ return [];
+ }
+
+ public setMetadataKeysForGroup(groupType: string, keys: string[]): boolean {
+ if (LinkManager.Instance.LinkManagerDoc) {
+ LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>(keys);
+ return true;
+ }
+ return false;
+ }
+
+ // returns a list of all metadata docs associated with the given group type
+ public getAllMetadataDocsInGroup(groupType: string): Array<Doc> {
+ let md: Doc[] = [];
+ let allLinks = LinkManager.Instance.getAllLinks();
+ allLinks.forEach(linkDoc => {
+ let anchor1Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor1, Doc, null));
+ let anchor2Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor2, Doc, null));
+ anchor1Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) { const meta = Cast(groupDoc.metadata, Doc, null); meta && md.push(meta); } });
+ anchor2Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) { const meta = Cast(groupDoc.metadata, Doc, null); meta && md.push(meta); } });
+ });
+ return md;
+ }
+
+ // checks if a link with the given anchors exists
+ public doesLinkExist(anchor1: Doc, anchor2: Doc): boolean {
+ let allLinks = LinkManager.Instance.getAllLinks();
+ let index = allLinks.findIndex(linkDoc => {
+ return (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor1) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor2)) ||
+ (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor2) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor1));
+ });
+ return index !== -1;
+ }
+
+ // finds the opposite anchor of a given anchor in a link
+ //TODO This should probably return undefined if there isn't an opposite anchor
+ //TODO This should also await the return value of the anchor so we don't filter out promises
+ public getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc {
+ if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, null))) {
+ return Cast(linkDoc.anchor2, Doc, null);
+ } else {
+ return Cast(linkDoc.anchor1, Doc, null);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/util/ProsemirrorCopy/prompt.js b/src/client/util/ProsemirrorCopy/prompt.js
new file mode 100644
index 000000000..b9068195f
--- /dev/null
+++ b/src/client/util/ProsemirrorCopy/prompt.js
@@ -0,0 +1,179 @@
+const prefix = "ProseMirror-prompt"
+
+export function openPrompt(options) {
+ let wrapper = document.body.appendChild(document.createElement("div"))
+ wrapper.className = prefix
+ wrapper.style.zIndex = 1000;
+ wrapper.style.width = 250;
+ wrapper.style.textAlign = "center";
+
+ let mouseOutside = e => { if (!wrapper.contains(e.target)) close() }
+ setTimeout(() => window.addEventListener("mousedown", mouseOutside), 50)
+ let close = () => {
+ window.removeEventListener("mousedown", mouseOutside)
+ if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper)
+ }
+
+ let domFields = []
+ for (let name in options.fields) domFields.push(options.fields[name].render())
+
+ let submitButton = document.createElement("button")
+ submitButton.type = "submit"
+ submitButton.className = prefix + "-submit"
+ submitButton.textContent = "OK"
+ let cancelButton = document.createElement("button")
+ cancelButton.type = "button"
+ cancelButton.className = prefix + "-cancel"
+ cancelButton.textContent = "Cancel"
+ cancelButton.addEventListener("click", close)
+
+ let form = wrapper.appendChild(document.createElement("form"))
+ let title = document.createElement("h5")
+ title.style.marginBottom = 15
+ title.style.marginTop = 10
+ if (options.title) form.appendChild(title).textContent = options.title
+ domFields.forEach(field => {
+ form.appendChild(document.createElement("div")).appendChild(field)
+ })
+ let b = document.createElement("div");
+ b.style.marginTop = 15;
+ let buttons = form.appendChild(b)
+ // buttons.className = prefix + "-buttons"
+ buttons.appendChild(submitButton)
+ buttons.appendChild(document.createTextNode(" "))
+ buttons.appendChild(cancelButton)
+
+ let box = wrapper.getBoundingClientRect()
+ wrapper.style.top = options.flyout_top + "px"
+ wrapper.style.left = options.flyout_left + "px"
+
+ let submit = () => {
+ let params = getValues(options.fields, domFields)
+ if (params) {
+ close()
+ options.callback(params)
+ }
+ }
+
+ form.addEventListener("submit", e => {
+ e.preventDefault()
+ submit()
+ })
+
+ form.addEventListener("keydown", e => {
+ if (e.keyCode == 27) {
+ e.preventDefault()
+ close()
+ } else if (e.keyCode == 13 && !(e.ctrlKey || e.metaKey || e.shiftKey)) {
+ e.preventDefault()
+ submit()
+ } else if (e.keyCode == 9) {
+ window.setTimeout(() => {
+ if (!wrapper.contains(document.activeElement)) close()
+ }, 500)
+ }
+ })
+
+ let input = form.elements[0]
+ if (input) input.focus()
+}
+
+function getValues(fields, domFields) {
+ let result = Object.create(null), i = 0
+ for (let name in fields) {
+ let field = fields[name], dom = domFields[i++]
+ let value = field.read(dom), bad = field.validate(value)
+ if (bad) {
+ reportInvalid(dom, bad)
+ return null
+ }
+ result[name] = field.clean(value)
+ }
+ return result
+}
+
+function reportInvalid(dom, message) {
+ // FIXME this is awful and needs a lot more work
+ let parent = dom.parentNode
+ let msg = parent.appendChild(document.createElement("div"))
+ msg.style.left = (dom.offsetLeft + dom.offsetWidth + 2) + "px"
+ msg.style.top = (dom.offsetTop - 5) + "px"
+ msg.className = "ProseMirror-invalid"
+ msg.textContent = message
+ setTimeout(() => parent.removeChild(msg), 1500)
+}
+
+// ::- The type of field that `FieldPrompt` expects to be passed to it.
+export class Field {
+ // :: (Object)
+ // Create a field with the given options. Options support by all
+ // field types are:
+ //
+ // **`value`**`: ?any`
+ // : The starting value for the field.
+ //
+ // **`label`**`: string`
+ // : The label for the field.
+ //
+ // **`required`**`: ?bool`
+ // : Whether the field is required.
+ //
+ // **`validate`**`: ?(any) → ?string`
+ // : A function to validate the given value. Should return an
+ // error message if it is not valid.
+ constructor(options) { this.options = options }
+
+ // render:: (state: EditorState, props: Object) → dom.Node
+ // Render the field to the DOM. Should be implemented by all subclasses.
+
+ // :: (dom.Node) → any
+ // Read the field's value from its DOM node.
+ read(dom) { return dom.value }
+
+ // :: (any) → ?string
+ // A field-type-specific validation function.
+ validateType(_value) { }
+
+ validate(value) {
+ if (!value && this.options.required)
+ return "Required field"
+ return this.validateType(value) || (this.options.validate && this.options.validate(value))
+ }
+
+ clean(value) {
+ return this.options.clean ? this.options.clean(value) : value
+ }
+}
+
+// ::- A field class for single-line text fields.
+export class TextField extends Field {
+ render() {
+ let input = document.createElement("input")
+ input.type = "text"
+ input.placeholder = this.options.label
+ input.value = this.options.value || ""
+ input.autocomplete = "off"
+ input.style.marginBottom = 4
+ input.style.border = "1px solid black"
+ input.style.padding = "4px 4px"
+ return input
+ }
+}
+
+
+// ::- A field class for dropdown fields based on a plain `<select>`
+// tag. Expects an option `options`, which should be an array of
+// `{value: string, label: string}` objects, or a function taking a
+// `ProseMirror` instance and returning such an array.
+export class SelectField extends Field {
+ render() {
+ let select = document.createElement("select")
+ this.options.options.forEach(o => {
+ let opt = select.appendChild(document.createElement("option"))
+ opt.value = o.value
+ opt.selected = o.value == this.options.value
+ opt.label = o.label
+ })
+ return select
+ }
+}
diff --git a/src/client/util/ProsemirrorKeymap.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index 00d086b97..fa9e2e5af 100644
--- a/src/client/util/ProsemirrorKeymap.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -1,4 +1,4 @@
-import { Schema } from "prosemirror-model";
+import { Schema, NodeType } from "prosemirror-model";
import {
wrapIn, setBlockType, chainCommands, toggleMark, exitCode,
joinUp, joinDown, lift, selectParentNode
@@ -7,6 +7,8 @@ import { wrapInList, splitListItem, liftListItem, sinkListItem } from "prosemirr
import { undo, redo } from "prosemirror-history";
import { undoInputRule } from "prosemirror-inputrules";
import { Transaction, EditorState } from "prosemirror-state";
+import { TooltipTextMenu } from "./TooltipTextMenu";
+import { Statement } from "../northstar/model/idea/idea";
const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
@@ -50,7 +52,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
}
if (type = schema.nodes.bullet_list) {
- bind("Ctrl-b", wrapInList(type));
+ bind("Ctrl-.", wrapInList(type));
}
if (type = schema.nodes.ordered_list) {
bind("Ctrl-n", wrapInList(type));
@@ -96,5 +98,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
});
}
+ bind("Mod-s", TooltipTextMenu.insertStar);
+
return keys;
-} \ No newline at end of file
+}
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 3e3e98206..2a57180d3 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -1,10 +1,11 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray, NodeType } from "prosemirror-model";
-import { joinUp, lift, setBlockType, toggleMark, wrapIn } from 'prosemirror-commands';
+import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray, NodeType, Slice, Mark, Node } from "prosemirror-model";
+import { joinUp, lift, setBlockType, toggleMark, wrapIn, selectNodeForward, deleteSelection } from 'prosemirror-commands';
import { redo, undo } from 'prosemirror-history';
import { orderedList, bulletList, listItem, } from 'prosemirror-schema-list';
-import { EditorState, Transaction, NodeSelection, } from "prosemirror-state";
+import { EditorState, Transaction, NodeSelection, TextSelection, Selection, } from "prosemirror-state";
import { EditorView, } from "prosemirror-view";
+import { View } from '@react-pdf/renderer';
const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
@@ -26,6 +27,14 @@ export const nodes: { [index: string]: NodeSpec } = {
toDOM() { return pDOM; }
},
+ // starmine: {
+ // inline: true,
+ // attrs: { oldtext: { default: "" } },
+ // group: "inline",
+ // toDOM() { return ["star", "㊉"]; },
+ // parseDOM: [{ tag: "star" }]
+ // },
+
// :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks.
blockquote: {
content: "block+",
@@ -77,6 +86,30 @@ export const nodes: { [index: string]: NodeSpec } = {
group: "inline"
},
+ star: {
+ inline: true,
+ attrs: {
+ visibility: { default: false },
+ text: { default: undefined },
+ textslice: { default: undefined },
+ textlen: { default: 0 }
+
+ },
+ group: "inline",
+ toDOM(node) {
+ const attrs = { style: `width: 40px` };
+ return ["span", { ...node.attrs, ...attrs }];
+ },
+ // parseDOM: [{
+ // tag: "star", getAttrs(dom: any) {
+ // return {
+ // visibility: dom.getAttribute("visibility"),
+ // oldtext: dom.getAttribute("oldtext"),
+ // oldtextlen: dom.getAttribute("oldtextlen"),
+ // }
+ // }
+ // }]
+ },
// :: NodeSpec An inline image (`<img>`) node. Supports `src`,
// `alt`, and `href` attributes. The latter two default to the empty
// string.
@@ -107,6 +140,32 @@ export const nodes: { [index: string]: NodeSpec } = {
}
},
+ video: {
+ inline: true,
+ attrs: {
+ src: {},
+ width: { default: "100px" },
+ alt: { default: null },
+ title: { default: null }
+ },
+ group: "inline",
+ draggable: true,
+ parseDOM: [{
+ tag: "video[src]", getAttrs(dom: any) {
+ return {
+ src: dom.getAttribute("src"),
+ title: dom.getAttribute("title"),
+ alt: dom.getAttribute("alt"),
+ width: Math.min(100, Number(dom.getAttribute("width"))),
+ };
+ }
+ }],
+ toDOM(node) {
+ const attrs = { style: `width: ${node.attrs.width}` };
+ return ["video", { ...node.attrs, ...attrs }];
+ }
+ },
+
// :: NodeSpec A hard line break, represented in the DOM as `<br>`.
hard_break: {
inline: true,
@@ -222,6 +281,15 @@ export const marks: { [index: string]: MarkSpec } = {
toDOM: () => ['sup']
},
+ highlight: {
+ parseDOM: [{ style: 'background: #d9dbdd' }],
+ toDOM() {
+ return ['span', {
+ style: 'color: blue'
+ }];
+ }
+ },
+
// :: MarkSpec Code font mark. Represented as a `<code>` element.
code: {
@@ -280,7 +348,29 @@ export const marks: { [index: string]: MarkSpec } = {
}]
},
+ pFontColor: {
+ attrs: {
+ color: { default: "yellow" }
+ },
+ parseDOM: [{ style: 'background: #d9dbdd' }],
+ toDOM: (node) => {
+ return ['span', {
+ style: `color: ${node.attrs.color}`
+ }];
+ }
+ },
+
/** FONT SIZES */
+ pFontSize: {
+ attrs: {
+ fontSize: { default: 10 }
+ },
+ inclusive: false,
+ parseDOM: [{ style: 'font-size: 10px;' }],
+ toDOM: (node) => ['span', {
+ style: `font-size: ${node.attrs.fontSize}px;`
+ }]
+ },
p10: {
parseDOM: [{ style: 'font-size: 10px;' }],
@@ -407,6 +497,75 @@ export class ImageResizeView {
this._handle.style.display = "none";
}
}
+
+export class SummarizedView {
+ // TODO: highlight text that is summarized. to find end of region, walk along mark
+ _collapsed: HTMLElement;
+ _view: any;
+ constructor(node: any, view: any, getPos: any) {
+ this._collapsed = document.createElement("span");
+ this._collapsed.textContent = "㊉";
+ this._collapsed.style.opacity = "0.5";
+ this._collapsed.style.position = "relative";
+ this._collapsed.style.width = "40px";
+ this._collapsed.style.height = "20px";
+ let self = this;
+ this._view = view;
+ this._collapsed.onpointerdown = function (e: any) {
+ if (node.attrs.visibility) {
+ node.attrs.visibility = !node.attrs.visibility;
+ let y = getPos();
+ let { from, to } = self.updateSummarizedText(y + 1, view.state.schema.marks.highlight);
+ let length = to - from;
+ let newSelection = TextSelection.create(view.state.doc, y + 1, y + 1 + length);
+ // update attrs of node
+ node.attrs.text = newSelection.content();
+ node.attrs.textslice = newSelection.content().toJSON();
+ view.dispatch(view.state.tr.setSelection(newSelection).deleteSelection(view.state, () => { }));
+ self._collapsed.textContent = "㊉";
+ } else {
+ node.attrs.visibility = !node.attrs.visibility;
+ let y = getPos();
+ let mark = view.state.schema.mark(view.state.schema.marks.highlight);
+ view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, y + 1, y + 1)));
+ const from = view.state.selection.from;
+ let size = node.attrs.text.size;
+ view.dispatch(view.state.tr.replaceSelection(node.attrs.text).addMark(from, from + size, mark).removeStoredMark(mark));
+ self._collapsed.textContent = "㊀";
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ };
+ (this as any).dom = this._collapsed;
+
+ }
+ selectNode() {
+ }
+
+ updateSummarizedText(start?: any, mark?: any) {
+ let $start = this._view.state.doc.resolve(start);
+ let endPos = start;
+
+ let _mark = this._view.state.schema.mark(this._view.state.schema.marks.highlight);
+ let visited = new Set();
+ for (let i: number = start + 1; i < this._view.state.doc.nodeSize - 1; i++) {
+ let skip = false;
+ this._view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => {
+ if (node.isLeaf && !visited.has(node) && !skip) {
+ if (node.marks.includes(_mark)) {
+ visited.add(node);
+ endPos = i + node.nodeSize - 1;
+ }
+ else skip = true;
+ }
+ });
+ }
+ return { from: start, to: endPos };
+ }
+
+ deselectNode() {
+ }
+}
// :: Schema
// This schema rougly corresponds to the document schema used by
// [CommonMark](http://commonmark.org/), minus the list elements,
@@ -415,4 +574,14 @@ export class ImageResizeView {
//
// To reuse elements from this schema, extend or read from its
// `spec.nodes` and `spec.marks` [properties](#model.Schema.spec).
-export const schema = new Schema({ nodes, marks }); \ No newline at end of file
+export const schema = new Schema({ nodes, marks });
+
+const fromJson = schema.nodeFromJSON;
+
+schema.nodeFromJSON = (json: any) => {
+ let node = fromJson(json);
+ if (json.type === "star") {
+ node.attrs.text = Slice.fromJSON(schema, node.attrs.textslice);
+ }
+ return node;
+}; \ No newline at end of file
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index beaf5cb03..3156c4f43 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -7,11 +7,7 @@ let ts = (window as any).ts;
// @ts-ignore
import * as typescriptlib from '!!raw-loader!./type_decls.d';
-import { Docs } from "../documents/Documents";
import { Doc, Field } from '../../new_fields/Doc';
-import { ImageField, PdfField, VideoField, AudioField } from '../../new_fields/URLField';
-import { List } from '../../new_fields/List';
-import { RichTextField } from '../../new_fields/RichTextField';
export interface ScriptSucccess {
success: true;
@@ -37,17 +33,46 @@ export interface CompileError {
errors: any[];
}
-export type CompileResult = CompiledScript | CompileError;
+export namespace Scripting {
+ export function addGlobal(global: { name: string }): void;
+ export function addGlobal(name: string, global: any): void;
+ export function addGlobal(nameOrGlobal: any, global?: any) {
+ let n: string;
+ let obj: any;
+ if (global !== undefined && typeof nameOrGlobal === "string") {
+ n = nameOrGlobal;
+ obj = global;
+ } else if (nameOrGlobal && typeof nameOrGlobal.name === "string") {
+ n = nameOrGlobal.name;
+ obj = nameOrGlobal;
+ } else {
+ throw new Error("Must either register an object with a name, or give a name and an object");
+ }
+ if (scriptingGlobals.hasOwnProperty(n)) {
+ throw new Error(`Global with name ${n} is already registered, choose another name`);
+ }
+ scriptingGlobals[n] = obj;
+ }
+}
+
+export function scriptingGlobal(constructor: { new(...args: any[]): any }) {
+ Scripting.addGlobal(constructor);
+}
+const scriptingGlobals: { [name: string]: any } = {};
+
+export type CompileResult = CompiledScript | CompileError;
function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult {
const errors = diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error);
if ((options.typecheck !== false && errors) || !script) {
return { compiled: false, errors: diagnostics };
}
- let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField];
- let paramNames = ["Docs", ...fieldTypes.map(fn => fn.name)];
- let params: any[] = [Docs, ...fieldTypes];
+ let paramNames = Object.keys(scriptingGlobals);
+ let params = paramNames.map(key => scriptingGlobals[key]);
+ // let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField, ScriptField, ComputedField, CompileScript];
+ // let paramNames = ["Docs", ...fieldTypes.map(fn => fn.name)];
+ // let params: any[] = [Docs, ...fieldTypes];
let compiledFunction = new Function(...paramNames, `return ${script}`);
let { capturedVariables = {} } = options;
let run = (args: { [name: string]: any } = {}): ScriptResult => {
@@ -63,10 +88,20 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
}
}
let thisParam = args.this || capturedVariables.this;
+ let batch: { end(): void } | undefined = undefined;
try {
+ if (!options.editable) {
+ batch = Doc.MakeReadOnly();
+ }
const result = compiledFunction.apply(thisParam, params).apply(thisParam, argsArray);
+ if (batch) {
+ batch.end();
+ }
return { success: true, result };
} catch (error) {
+ if (batch) {
+ batch.end();
+ }
return { success: false, error };
}
};
@@ -132,6 +167,7 @@ export interface ScriptOptions {
params?: { [name: string]: string };
capturedVariables?: { [name: string]: Field };
typecheck?: boolean;
+ editable?: boolean;
}
export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult {
@@ -167,4 +203,6 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
let diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics);
return Run(outputText, paramNames, diagnostics, script, options);
-} \ No newline at end of file
+}
+
+Scripting.addGlobal(CompileScript); \ No newline at end of file
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index 9dd9acbb7..338628960 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -4,23 +4,64 @@ import { Doc } from '../../new_fields/Doc';
import { Id } from '../../new_fields/FieldSymbols';
export namespace SearchUtil {
- export function Search(query: string, returnDocs: true): Promise<Doc[]>;
- export function Search(query: string, returnDocs: false): Promise<string[]>;
+ export interface IdSearchResult {
+ ids: string[];
+ numFound: number;
+ }
+
+ export interface DocSearchResult {
+ docs: Doc[];
+ numFound: number;
+ }
+
+ export function Search(query: string, returnDocs: true): Promise<DocSearchResult>;
+ export function Search(query: string, returnDocs: false): Promise<IdSearchResult>;
export async function Search(query: string, returnDocs: boolean) {
- const ids = JSON.parse(await rp.get(DocServer.Util.prepend("/search"), {
+ const result: IdSearchResult = JSON.parse(await rp.get(DocServer.prepend("/search"), {
qs: { query }
}));
if (!returnDocs) {
- return ids;
+ return result;
}
- const docMap = await DocServer.getRefFields(ids);
- return ids.map((id: string) => docMap[id]).filter((doc: any) => doc instanceof Doc);
+ const { ids, numFound } = result;
+ const docMap = await DocServer.GetRefFields(ids);
+ const docs = ids.map((id: string) => docMap[id]).filter((doc: any) => doc instanceof Doc);
+ return { docs, numFound };
}
- export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]> {
- const proto = await Doc.GetT(doc, "proto", Doc, true);
- const protoId = (proto || doc)[Id];
- return Search(`proto_i:"${protoId}"`, true);
+ export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]>;
+ export async function GetAliasesOfDocument(doc: Doc, returnDocs: false): Promise<string[]>;
+ export async function GetAliasesOfDocument(doc: Doc, returnDocs = true): Promise<Doc[] | string[]> {
+ const proto = Doc.GetProto(doc);
+ const protoId = proto[Id];
+ if (returnDocs) {
+ return (await Search(`proto_i:"${protoId}"`, returnDocs)).docs;
+ } else {
+ return (await Search(`proto_i:"${protoId}"`, returnDocs)).ids;
+ }
// return Search(`{!join from=id to=proto_i}id:${protoId}`, true);
}
+
+ export async function GetViewsOfDocument(doc: Doc): Promise<Doc[]> {
+ const results = await Search(`proto_i:"${doc[Id]}"`, true);
+ return results.docs;
+ }
+
+ export async function GetContextsOfDocument(doc: Doc): Promise<{ contexts: Doc[], aliasContexts: Doc[] }> {
+ const docContexts = (await Search(`data_l:"${doc[Id]}"`, true)).docs;
+ const aliases = await GetAliasesOfDocument(doc, false);
+ const aliasContexts = (await Promise.all(aliases.map(doc => Search(`data_l:"${doc}"`, true))));
+ const contexts = { contexts: docContexts, aliasContexts: [] as Doc[] };
+ aliasContexts.forEach(result => contexts.aliasContexts.push(...result.docs));
+ return contexts;
+ }
+
+ export async function GetContextIdsOfDocument(doc: Doc): Promise<{ contexts: string[], aliasContexts: string[] }> {
+ const docContexts = (await Search(`data_l:"${doc[Id]}"`, false)).ids;
+ const aliases = await GetAliasesOfDocument(doc, false);
+ const aliasContexts = (await Promise.all(aliases.map(doc => Search(`data_l:"${doc}"`, false))));
+ const contexts = { contexts: docContexts, aliasContexts: [] as string[] };
+ aliasContexts.forEach(result => contexts.aliasContexts.push(...result.ids));
+ return contexts;
+ }
} \ No newline at end of file
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 8c92c2023..9efef888d 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,49 +1,65 @@
-import { observable, action } from "mobx";
-import { Doc } from "../../new_fields/Doc";
+import { observable, action, runInAction, IReactionDisposer, reaction, autorun } from "mobx";
+import { Doc, Opt } from "../../new_fields/Doc";
import { DocumentView } from "../views/nodes/DocumentView";
import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
-import { NumCast } from "../../new_fields/Types";
+import { NumCast, StrCast } from "../../new_fields/Types";
+import { InkingControl } from "../views/InkingControl";
export namespace SelectionManager {
+
class Manager {
- @observable
- SelectedDocuments: Array<DocumentView> = [];
+
+ @observable IsDragging: boolean = false;
+ @observable SelectedDocuments: Array<DocumentView> = [];
+
@action
- SelectDoc(doc: DocumentView, ctrlPressed: boolean): void {
+ SelectDoc(docView: DocumentView, ctrlPressed: boolean): void {
// if doc is not in SelectedDocuments, add it
- if (!ctrlPressed) {
- this.DeselectAll();
- }
+ if (manager.SelectedDocuments.indexOf(docView) === -1) {
+ if (!ctrlPressed) {
+ this.DeselectAll();
+ }
- if (manager.SelectedDocuments.indexOf(doc) === -1) {
- manager.SelectedDocuments.push(doc);
- doc.props.whenActiveChanged(true);
+ manager.SelectedDocuments.push(docView);
+ // console.log(manager.SelectedDocuments);
+ docView.props.whenActiveChanged(true);
+ }
+ }
+ @action
+ DeselectDoc(docView: DocumentView): void {
+ let ind = manager.SelectedDocuments.indexOf(docView);
+ if (ind !== -1) {
+ manager.SelectedDocuments.splice(ind, 1);
+ docView.props.whenActiveChanged(false);
}
}
-
@action
DeselectAll(): void {
manager.SelectedDocuments.map(dv => dv.props.whenActiveChanged(false));
manager.SelectedDocuments = [];
FormattedTextBox.InputBoxOverlay = undefined;
}
- @action
- ReselectAll() {
- let sdocs = manager.SelectedDocuments.map(d => d);
- manager.SelectedDocuments = [];
- return sdocs;
- }
- @action
- ReselectAll2(sdocs: DocumentView[]) {
- sdocs.map(s => SelectionManager.SelectDoc(s, true));
- }
}
const manager = new Manager();
+ reaction(() => manager.SelectedDocuments, sel => {
+ let targetColor = "#FFFFFF";
+ if (sel.length > 0) {
+ let firstView = sel[0];
+ let doc = firstView.props.Document;
+ let targetDoc = doc.isTemplate ? doc : Doc.GetProto(doc);
+ let stored = StrCast(targetDoc.backgroundColor);
+ stored.length > 0 && (targetColor = stored);
+ }
+ InkingControl.Instance.updateSelectedColor(targetColor);
+ }, { fireImmediately: true });
- export function SelectDoc(doc: DocumentView, ctrlPressed: boolean): void {
- manager.SelectDoc(doc, ctrlPressed);
+ export function DeselectDoc(docView: DocumentView): void {
+ manager.DeselectDoc(docView);
+ }
+ export function SelectDoc(docView: DocumentView, ctrlPressed: boolean): void {
+ manager.SelectDoc(docView, ctrlPressed);
}
export function IsSelected(doc: DocumentView): boolean {
@@ -62,14 +78,13 @@ export namespace SelectionManager {
if (found) manager.SelectDoc(found, false);
}
- export function ReselectAll() {
- let sdocs = manager.ReselectAll();
- setTimeout(() => manager.ReselectAll2(sdocs), 0);
- }
+ export function SetIsDragging(dragging: boolean) { runInAction(() => manager.IsDragging = dragging); }
+ export function GetIsDragging() { return manager.IsDragging; }
+
export function SelectedDocuments(): Array<DocumentView> {
- return manager.SelectedDocuments;
+ return manager.SelectedDocuments.slice();
}
- export function ViewsSortedVertically(): DocumentView[] {
+ export function ViewsSortedHorizontally(): DocumentView[] {
let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => {
if (NumCast(doc1.props.Document.x) > NumCast(doc2.props.Document.x)) return 1;
if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
@@ -77,7 +92,7 @@ export namespace SelectionManager {
});
return sorted;
}
- export function ViewsSortedHorizontally(): DocumentView[] {
+ export function ViewsSortedVertically(): DocumentView[] {
let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => {
if (NumCast(doc1.props.Document.y) > NumCast(doc2.props.Document.y)) return 1;
if (NumCast(doc1.props.Document.y) < NumCast(doc2.props.Document.y)) return -1;
diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts
index 7ded85e43..a7246d7c4 100644
--- a/src/client/util/SerializationHelper.ts
+++ b/src/client/util/SerializationHelper.ts
@@ -45,13 +45,17 @@ export namespace SerializationHelper {
throw Error(`type '${obj.__type}' not registered. Make sure you register it using a @Deserializable decorator`);
}
- const value = deserialize(serializationTypes[obj.__type], obj);
+ const type = serializationTypes[obj.__type];
+ const value = deserialize(type.ctor, obj);
+ if (type.afterDeserialize) {
+ type.afterDeserialize(value);
+ }
serializing -= 1;
return value;
}
}
-let serializationTypes: { [name: string]: any } = {};
+let serializationTypes: { [name: string]: { ctor: { new(): any }, afterDeserialize?: (obj: any) => void } } = {};
let reverseMap: { [ctor: string]: string } = {};
export interface DeserializableOpts {
@@ -59,9 +63,9 @@ export interface DeserializableOpts {
withFields(fields: string[]): Function;
}
-export function Deserializable(name: string): DeserializableOpts;
+export function Deserializable(name: string, afterDeserialize?: (obj: any) => void): DeserializableOpts;
export function Deserializable(constructor: { new(...args: any[]): any }): void;
-export function Deserializable(constructor: { new(...args: any[]): any } | string): DeserializableOpts | void {
+export function Deserializable(constructor: { new(...args: any[]): any } | string, afterDeserialize?: (obj: any) => void): DeserializableOpts | void {
function addToMap(name: string, ctor: { new(...args: any[]): any }) {
const schema = getDefaultModelSchema(ctor) as any;
if (schema.targetClass !== ctor) {
@@ -69,7 +73,7 @@ export function Deserializable(constructor: { new(...args: any[]): any } | strin
setDefaultModelSchema(ctor, newSchema);
}
if (!(name in serializationTypes)) {
- serializationTypes[name] = ctor;
+ serializationTypes[name] = { ctor, afterDeserialize };
reverseMap[ctor.name] = name;
} else {
throw new Error(`Name ${name} has already been registered as deserializable`);
diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss
index 437da0d63..40ac3abb9 100644
--- a/src/client/util/TooltipTextMenu.scss
+++ b/src/client/util/TooltipTextMenu.scss
@@ -18,6 +18,7 @@
.ProseMirror-menuitem {
margin-right: 3px;
display: inline-block;
+ z-index: 100000;
}
.ProseMirror-menuseparator {
@@ -36,7 +37,7 @@
position: relative;
padding-right: 15px;
margin: 3px;
- background: #333333;
+ background: white;
border-radius: 3px;
text-align: center;
}
@@ -59,7 +60,6 @@
}
.ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu {
- position: absolute;
background: $dark-color;
color:white;
border: 1px solid rgb(223, 223, 223);
@@ -67,8 +67,10 @@
}
.ProseMirror-menu-dropdown-menu {
- z-index: 15;
+ z-index: 100000;
min-width: 6em;
+ background: white;
+ position: absolute;
}
.linking {
@@ -79,10 +81,11 @@
cursor: pointer;
padding: 2px 8px 2px 4px;
width: auto;
+ z-index: 100000;
}
.ProseMirror-menu-dropdown-item:hover {
- background: #2e2b2b;
+ background: white;
}
.ProseMirror-menu-submenu-wrap {
@@ -132,7 +135,7 @@
position: relative;
min-height: 1em;
color: white;
- padding: 1px 6px;
+ padding: 10px 10px;
top: 0; left: 0; right: 0;
border-bottom: 1px solid silver;
background:$dark-color;
@@ -155,7 +158,7 @@
}
.ProseMirror-icon svg {
- fill: currentColor;
+ fill:white;
height: 1em;
}
@@ -184,7 +187,7 @@
position: fixed;
border-radius: 3px;
z-index: 11;
- box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2);
+ box-shadow: -.5px 2px 5px white(255, 255, 255, 0.2);
}
.ProseMirror-prompt h5 {
@@ -196,7 +199,7 @@
.ProseMirror-prompt input[type="text"],
.ProseMirror-prompt textarea {
- background: #eee;
+ background: white;
border: none;
outline: none;
}
@@ -232,16 +235,20 @@
}
.tooltipMenu {
- position: absolute;
- z-index: 200;
- background: $dark-color;
+ position: relative;
+ z-index: 2000;
+ background: #121721;
border: 1px solid silver;
- border-radius: 4px;
- padding: 2px 10px;
- margin-bottom: 7px;
- -webkit-transform: translateX(-50%);
- transform: translateX(-50%);
+ border-radius: 15px;
+ //height: 60px;
+ //padding: 2px 10px;
+ //margin-top: 100px;
+ //-webkit-transform: translateX(-50%);
+ //transform: translateX(-50%);
+ transform: translateY(-85px);
pointer-events: all;
+ height: 30px;
+ width:550px;
.ProseMirror-example-setup-style hr {
padding: 2px 10px;
border: none;
@@ -257,34 +264,34 @@
}
}
-.tooltipMenu:before {
- content: "";
- height: 0; width: 0;
- position: absolute;
- left: 50%;
- margin-left: -5px;
- bottom: -6px;
- border: 5px solid transparent;
- border-bottom-width: 0;
- border-top-color: silver;
- }
- .tooltipMenu:after {
- content: "";
- height: 0; width: 0;
- position: absolute;
- left: 50%;
- margin-left: -5px;
- bottom: -4.5px;
- border: 5px solid transparent;
- border-bottom-width: 0;
- border-top-color: $dark-color;
- }
+// .tooltipMenu:before {
+// content: "";
+// height: 0; width: 0;
+// position: absolute;
+// left: 50%;
+// margin-left: -5px;
+// bottom: -6px;
+// border: 5px solid transparent;
+// border-bottom-width: 0;
+// border-top-color: silver;
+// }
+// .tooltipMenu:after {
+// content: "";
+// height: 0; width: 0;
+// position: absolute;
+// left: 50%;
+// margin-left: -5px;
+// bottom: -4.5px;
+// border: 5px solid transparent;
+// border-bottom-width: 0;
+// border-top-color: $dark-color;
+// }
.menuicon {
display: inline-block;
- border-right: 1px solid rgba(0, 0, 0, 0.2);
+ border-right: 1px solid white(0, 0, 0, 0.2);
//color: rgb(19, 18, 18);
- color: white;
+ color: rgb(226, 21, 21);
line-height: 1;
padding: 0px 2px;
margin: 1px;
@@ -302,3 +309,10 @@
font-size: 12px;
padding-right: 0px;
}
+ .summarize{
+ //margin-left: 15px;
+ color: white;
+ height: 20px;
+ // background-color: white;
+ text-align: center;
+ } \ No newline at end of file
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 36219a99e..4fcc16ddd 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -1,51 +1,38 @@
-import { action, IReactionDisposer, reaction } from "mobx";
-import { Dropdown, DropdownSubmenu, MenuItem, MenuItemSpec, renderGrouped, icons, } from "prosemirror-menu"; //no import css
-import { baseKeymap, lift } from "prosemirror-commands";
-import { history, redo, undo } from "prosemirror-history";
-import { keymap } from "prosemirror-keymap";
-import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state";
+import { action } from "mobx";
+import { Dropdown, MenuItem, icons, } from "prosemirror-menu"; //no import css
+import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { schema } from "./RichTextSchema";
-import { Schema, NodeType, MarkType, Mark } from "prosemirror-model";
-import React = require("react");
+import { Schema, NodeType, MarkType, Mark, ResolvedPos } from "prosemirror-model";
+import { Node as ProsNode } from "prosemirror-model";
import "./TooltipTextMenu.scss";
-const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands");
+const { toggleMark, setBlockType } = require("prosemirror-commands");
import { library } from '@fortawesome/fontawesome-svg-core';
-import { wrapInList, bulletList, liftListItem, listItem, } from 'prosemirror-schema-list';
-import { liftTarget, RemoveMarkStep, AddMarkStep } from 'prosemirror-transform';
-import {
- faListUl,
-} from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { wrapInList, liftListItem, } from 'prosemirror-schema-list';
+import { faListUl } from '@fortawesome/free-solid-svg-icons';
import { FieldViewProps } from "../views/nodes/FieldView";
-import { throwStatement } from "babel-types";
-import { View } from "@react-pdf/renderer";
+const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js");
import { DragManager } from "./DragManager";
import { Doc, Opt, Field } from "../../new_fields/Doc";
-import { Utils } from "../northstar/utils/Utils";
import { DocServer } from "../DocServer";
-import { CollectionFreeFormDocumentView } from "../views/nodes/CollectionFreeFormDocumentView";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import { DocumentManager } from "./DocumentManager";
import { Id } from "../../new_fields/FieldSymbols";
-
-const SVG = "http://www.w3.org/2000/svg";
+import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
//appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc.
export class TooltipTextMenu {
public tooltip: HTMLElement;
- private num_icons = 0;
private view: EditorView;
private fontStyles: MarkType[];
private fontSizes: MarkType[];
private listTypes: NodeType[];
- private editorProps: FieldViewProps;
- private state: EditorState;
+ private editorProps: FieldViewProps & FormattedTextBoxProps;
private fontSizeToNum: Map<MarkType, number>;
private fontStylesToName: Map<MarkType, string>;
private listTypeToIcon: Map<NodeType, string>;
- private fontSizeIndicator: HTMLSpanElement = document.createElement("span");
+
private linkEditor?: HTMLDivElement;
private linkText?: HTMLDivElement;
private linkDrag?: HTMLImageElement;
@@ -54,26 +41,34 @@ export class TooltipTextMenu {
private fontStyleDom?: Node;
private listTypeBtnDom?: Node;
- constructor(view: EditorView, editorProps: FieldViewProps) {
+ private _activeMarks: Mark[] = [];
+
+ private _collapseBtn?: MenuItem;
+
+ constructor(view: EditorView, editorProps: FieldViewProps & FormattedTextBoxProps) {
this.view = view;
- this.state = view.state;
this.editorProps = editorProps;
this.tooltip = document.createElement("div");
this.tooltip.className = "tooltipMenu";
+ // this.createCollapse();
+ // if (this._collapseBtn) {
+ // this.tooltip.appendChild(this._collapseBtn.render(this.view).dom);
+ // }
//add the div which is the tooltip
- view.dom.parentNode!.parentNode!.appendChild(this.tooltip);
+ //view.dom.parentNode!.parentNode!.appendChild(this.tooltip);
//add additional icons
library.add(faListUl);
//add the buttons to the tooltip
let items = [
- { command: toggleMark(schema.marks.strong), dom: this.icon("B", "strong") },
- { command: toggleMark(schema.marks.em), dom: this.icon("i", "em") },
- { command: toggleMark(schema.marks.underline), dom: this.icon("U", "underline") },
- { command: toggleMark(schema.marks.strikethrough), dom: this.icon("S", "strikethrough") },
- { command: toggleMark(schema.marks.superscript), dom: this.icon("s", "superscript") },
- { command: toggleMark(schema.marks.subscript), dom: this.icon("s", "subscript") },
+ { command: toggleMark(schema.marks.strong), dom: this.icon("B", "strong", "Bold") },
+ { command: toggleMark(schema.marks.em), dom: this.icon("i", "em", "Italic") },
+ { command: toggleMark(schema.marks.underline), dom: this.icon("U", "underline", "Underline") },
+ { command: toggleMark(schema.marks.strikethrough), dom: this.icon("S", "strikethrough", "Strikethrough") },
+ { command: toggleMark(schema.marks.superscript), dom: this.icon("s", "superscript", "Superscript") },
+ { command: toggleMark(schema.marks.subscript), dom: this.icon("s", "subscript", "Subscript") },
+ { command: toggleMark(schema.marks.highlight), dom: this.icon("H", 'blue', 'Blue') }
// { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") },
// { command: wrapInList(schema.nodes.ordered_list), dom: this.icon("1)", "bullets") },
// { command: lift, dom: this.icon("<", "lift") },
@@ -86,10 +81,14 @@ export class TooltipTextMenu {
dom.addEventListener("pointerdown", e => {
e.preventDefault();
view.focus();
- command(view.state, view.dispatch, view);
+ if (dom.contains(e.target as Node)) {
+ e.stopPropagation();
+ command(view.state, view.dispatch, view);
+ }
});
});
+ this.updateLinkMenu();
//list of font styles
this.fontStylesToName = new Map();
@@ -112,6 +111,8 @@ export class TooltipTextMenu {
this.fontSizeToNum.set(schema.marks.p32, 32);
this.fontSizeToNum.set(schema.marks.p48, 48);
this.fontSizeToNum.set(schema.marks.p72, 72);
+ this.fontSizeToNum.set(schema.marks.pFontSize, 10);
+ this.fontSizeToNum.set(schema.marks.pFontSize, 10);
this.fontSizes = Array.from(this.fontSizeToNum.keys());
//list types
@@ -120,7 +121,21 @@ export class TooltipTextMenu {
this.listTypeToIcon.set(schema.nodes.ordered_list, "1)");
this.listTypes = Array.from(this.listTypeToIcon.keys());
+ this.tooltip.appendChild(this.createLink().render(this.view).dom);
+
+ this.tooltip.appendChild(this.createStar().render(this.view).dom);
+
+
+
+ this.updateListItemDropdown(":", this.listTypeBtnDom);
+
this.update(view, undefined);
+
+ //view.dom.parentNode!.parentNode!.insertBefore(this.tooltip, view.dom.parentNode);
+
+ // quick and dirty null check
+ const outer_div = this.editorProps.outer_div;
+ outer_div && outer_div(this.tooltip);
}
//label of dropdown will change to given label
@@ -131,15 +146,18 @@ export class TooltipTextMenu {
//font SIZES
let fontSizeBtns: MenuItem[] = [];
this.fontSizeToNum.forEach((number, mark) => {
- fontSizeBtns.push(this.dropdownMarkBtn(String(number), "width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes));
+ fontSizeBtns.push(this.dropdownMarkBtn(String(number), "color: black; width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes));
});
- if (this.fontSizeDom) { this.tooltip.removeChild(this.fontSizeDom); }
- this.fontSizeDom = (new Dropdown(cut(fontSizeBtns), {
+ let newfontSizeDom = (new Dropdown(cut(fontSizeBtns), {
label: label,
- css: "color:white; min-width: 60px; padding-left: 5px; margin-right: 0;"
+ css: "color:black; min-width: 60px; padding-left: 5px; margin-right: 0;"
}) as MenuItem).render(this.view).dom;
- this.tooltip.appendChild(this.fontSizeDom);
+ if (this.fontSizeDom) { this.tooltip.replaceChild(newfontSizeDom, this.fontSizeDom); }
+ else {
+ this.tooltip.appendChild(newfontSizeDom);
+ }
+ this.fontSizeDom = newfontSizeDom;
}
//label of dropdown will change to given label
@@ -150,22 +168,26 @@ export class TooltipTextMenu {
//font STYLES
let fontBtns: MenuItem[] = [];
this.fontStylesToName.forEach((name, mark) => {
- fontBtns.push(this.dropdownMarkBtn(name, "font-family: " + name + ", sans-serif; width: 125px;", mark, this.view, this.changeToMarkInGroup, this.fontStyles));
+ fontBtns.push(this.dropdownMarkBtn(name, "color: black; font-family: " + name + ", sans-serif; width: 125px;", mark, this.view, this.changeToMarkInGroup, this.fontStyles));
});
- if (this.fontStyleDom) { this.tooltip.removeChild(this.fontStyleDom); }
- this.fontStyleDom = (new Dropdown(cut(fontBtns), {
+ let newfontStyleDom = (new Dropdown(cut(fontBtns), {
label: label,
- css: "color:white; width: 125px; margin-left: -3px; padding-left: 2px;"
+ css: "color:black; width: 125px; margin-left: -3px; padding-left: 2px;"
}) as MenuItem).render(this.view).dom;
+ if (this.fontStyleDom) { this.tooltip.replaceChild(newfontStyleDom, this.fontStyleDom); }
+ else {
+ this.tooltip.appendChild(newfontStyleDom);
+ }
+ this.fontStyleDom = newfontStyleDom;
- this.tooltip.appendChild(this.fontStyleDom);
}
updateLinkMenu() {
if (!this.linkEditor || !this.linkText) {
this.linkEditor = document.createElement("div");
- this.linkEditor.style.color = "white";
+ this.linkEditor.className = "ProseMirror-icon menuicon";
+ this.linkEditor.style.color = "black";
this.linkText = document.createElement("div");
this.linkText.style.cssFloat = "left";
this.linkText.style.marginRight = "5px";
@@ -178,8 +200,8 @@ export class TooltipTextMenu {
this.linkText.onpointerdown = (e: PointerEvent) => { e.stopPropagation(); };
let linkBtn = document.createElement("div");
linkBtn.textContent = ">>";
- linkBtn.style.width = "20px";
- linkBtn.style.height = "20px";
+ linkBtn.style.width = "10px";
+ linkBtn.style.height = "10px";
linkBtn.style.color = "white";
linkBtn.style.cssFloat = "left";
linkBtn.onpointerdown = (e: PointerEvent) => {
@@ -187,14 +209,14 @@ export class TooltipTextMenu {
let link = node && node.marks.find(m => m.type.name === "link");
if (link) {
let href: string = link.attrs.href;
- if (href.indexOf(DocServer.Util.prepend("/doc/")) === 0) {
- let docid = href.replace(DocServer.Util.prepend("/doc/"), "");
- DocServer.getRefField(docid).then(action((f: Opt<Field>) => {
+ if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
+ let docid = href.replace(DocServer.prepend("/doc/"), "");
+ DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
if (f instanceof Doc) {
if (DocumentManager.Instance.getDocumentView(f)) {
- DocumentManager.Instance.getDocumentView(f)!.props.focus(f);
+ DocumentManager.Instance.getDocumentView(f)!.props.focus(f, false);
}
- else if (CollectionDockingView.TopLevel) CollectionDockingView.AddRightSplit(f);
+ else if (CollectionDockingView.Instance) CollectionDockingView.Instance.AddRightSplit(f, undefined);
}
}));
}
@@ -205,9 +227,10 @@ export class TooltipTextMenu {
};
this.linkDrag = document.createElement("img");
this.linkDrag.src = "https://seogurusnyc.com/wp-content/uploads/2016/12/link-1.png";
- this.linkDrag.style.width = "20px";
- this.linkDrag.style.height = "20px";
- this.linkDrag.style.color = "white";
+ this.linkDrag.style.width = "15px";
+ this.linkDrag.style.height = "15px";
+ this.linkDrag.title = "Drag to create link";
+ this.linkDrag.style.color = "black";
this.linkDrag.style.background = "black";
this.linkDrag.style.cssFloat = "left";
this.linkDrag.onpointerdown = (e: PointerEvent) => {
@@ -218,15 +241,15 @@ export class TooltipTextMenu {
handlers: {
dragComplete: action(() => {
let m = dragData.droppedDocuments;
- this.makeLink(DocServer.Util.prepend("/doc/" + m[0][Id]));
+ this.makeLink(DocServer.prepend("/doc/" + m[0][Id]));
}),
},
hideSource: false
});
};
this.linkEditor.appendChild(this.linkDrag);
- this.linkEditor.appendChild(this.linkText);
- this.linkEditor.appendChild(linkBtn);
+ // this.linkEditor.appendChild(this.linkText);
+ // this.linkEditor.appendChild(linkBtn);
this.tooltip.appendChild(this.linkEditor);
}
@@ -253,22 +276,31 @@ export class TooltipTextMenu {
link = node && node.marks.find(m => m.type.name === "link");
}
+ public static insertStar(state: EditorState<any>, dispatch: any) {
+ let newNode = schema.nodes.star.create({ visibility: false, text: state.selection.content(), textslice: state.selection.content().toJSON(), textlen: state.selection.to - state.selection.from });
+ if (dispatch) {
+ //console.log(newNode.attrs.text.toString());
+ dispatch(state.tr.replaceSelectionWith(newNode));
+ }
+ return true;
+ }
+
//will display a remove-list-type button if selection is in list, otherwise will show list type dropdown
- updateListItemDropdown(label: string, listTypeBtn: Node) {
+ updateListItemDropdown(label: string, listTypeBtn: any) {
//remove old btn
if (listTypeBtn) { this.tooltip.removeChild(listTypeBtn); }
//Make a dropdown of all list types
let toAdd: MenuItem[] = [];
this.listTypeToIcon.forEach((icon, type) => {
- toAdd.push(this.dropdownNodeBtn(icon, "width: 40px;", type, this.view, this.listTypes, this.changeToNodeType));
+ toAdd.push(this.dropdownNodeBtn(icon, "color: black; width: 40px;", type, this.view, this.listTypes, this.changeToNodeType));
});
//option to remove the list formatting
- toAdd.push(this.dropdownNodeBtn("X", "width: 40px;", undefined, this.view, this.listTypes, this.changeToNodeType));
+ toAdd.push(this.dropdownNodeBtn("X", "color: black; width: 40px;", undefined, this.view, this.listTypes, this.changeToNodeType));
listTypeBtn = (new Dropdown(toAdd, {
label: label,
- css: "color:white; width: 40px;"
+ css: "color:black; width: 40px;"
}) as MenuItem).render(this.view).dom;
//add this new button and return it
@@ -277,8 +309,8 @@ export class TooltipTextMenu {
}
//for a specific grouping of marks (passed in), remove all and apply the passed-in one to the selected text
- changeToMarkInGroup(markType: MarkType, view: EditorView, fontMarks: MarkType[]) {
- let { empty, $cursor, ranges } = view.state.selection as TextSelection;
+ changeToMarkInGroup = (markType: MarkType, view: EditorView, fontMarks: MarkType[]) => {
+ let { $cursor, ranges } = view.state.selection as TextSelection;
let state = view.state;
let dispatch = view.dispatch;
@@ -290,25 +322,34 @@ export class TooltipTextMenu {
dispatch(state.tr.removeStoredMark(type));
}
} else {
- let has = false, tr = state.tr;
+ let has = false;
for (let i = 0; !has && i < ranges.length; i++) {
let { $from, $to } = ranges[i];
has = state.doc.rangeHasMark($from.pos, $to.pos, type);
}
for (let i of ranges) {
- let { $from, $to } = i;
if (has) {
toggleMark(type)(view.state, view.dispatch, view);
}
}
}
}
- }); //actually apply font
+ });
+ // fontsize
+ if (markType.name[0] === 'p') {
+ let size = this.fontSizeToNum.get(markType);
+ if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
+ }
+ else {
+ let fontName = this.fontStylesToName.get(markType);
+ if (fontName) { this.updateFontStyleDropdown(fontName); }
+ }
+ //actually apply font
return toggleMark(markType)(view.state, view.dispatch, view);
}
//remove all node typeand apply the passed-in one to the selected text
- changeToNodeType(nodeType: NodeType | undefined, view: EditorView, allNodes: NodeType[]) {
+ changeToNodeType(nodeType: NodeType | undefined, view: EditorView) {
//remove old
liftListItem(schema.nodes.list_item)(view.state, view.dispatch);
if (nodeType) { //add new
@@ -325,13 +366,114 @@ export class TooltipTextMenu {
execEvent: "",
class: "menuicon",
css: css,
- enable(state) { return true; },
+ enable() { return true; },
run() {
changeToMarkInGroup(markType, view, groupMarks);
}
});
}
+ createStar() {
+ return new MenuItem({
+ title: "Summarize",
+ label: "Summarize",
+ icon: icons.join,
+ css: "color:white;",
+ class: "summarize",
+ execEvent: "",
+ run: (state, dispatch) => {
+ TooltipTextMenu.insertStar(state, dispatch);
+ }
+
+ });
+ }
+
+ createCollapse() {
+ this._collapseBtn = new MenuItem({
+ title: "Collapse",
+ //label: "Collapse",
+ icon: icons.join,
+ execEvent: "",
+ css: "color:white;",
+ class: "summarize",
+ run: () => {
+ this.collapseToolTip();
+ }
+ });
+ }
+
+ collapseToolTip() {
+ if (this._collapseBtn) {
+ if (this._collapseBtn.spec.title === "Collapse") {
+ // const newcollapseBtn = new MenuItem({
+ // title: "Expand",
+ // icon: icons.join,
+ // execEvent: "",
+ // css: "color:white;",
+ // class: "summarize",
+ // run: (state, dispatch, view) => {
+ // this.collapseToolTip();
+ // }
+ // });
+ // this.tooltip.replaceChild(newcollapseBtn.render(this.view).dom, this._collapseBtn.render(this.view).dom);
+ // this._collapseBtn = newcollapseBtn;
+ this.tooltip.style.width = "30px";
+ this._collapseBtn.spec.title = "Expand";
+ this._collapseBtn.render(this.view);
+ }
+ else {
+ this._collapseBtn.spec.title = "Collapse";
+ this.tooltip.style.width = "550px";
+ this._collapseBtn.render(this.view);
+ }
+ }
+ }
+
+ createLink() {
+ let markType = schema.marks.link;
+ return new MenuItem({
+ title: "Add or remove link",
+ label: "Add or remove link",
+ execEvent: "",
+ icon: icons.link,
+ css: "color:white;",
+ class: "menuicon",
+ enable(state) { return !state.selection.empty; },
+ run: (state, dispatch, view) => {
+ // to remove link
+ let curLink = "";
+ if (this.markActive(state, markType)) {
+
+ let { from, $from, to, empty } = state.selection;
+ let node = state.doc.nodeAt(from);
+ node && node.marks.map(m => {
+ m.type === markType && (curLink = m.attrs.href);
+ });
+ //toggleMark(markType)(state, dispatch);
+ //return true;
+ }
+ // to create link
+ openPrompt({
+ title: "Create a link",
+ fields: {
+ href: new TextField({
+ value: curLink,
+ label: "Link Target",
+ required: true
+ }),
+ title: new TextField({ label: "Title" })
+ },
+ callback(attrs: any) {
+ toggleMark(markType, attrs)(view.state, view.dispatch);
+ view.focus();
+ },
+ flyout_top: 0,
+ flyout_left: 0
+ });
+ }
+ });
+ }
+
//makes a button for the drop down FOR NODE TYPES
//css is the style you want applied to the button
dropdownNodeBtn(label: string, css: string, nodeType: NodeType | undefined, view: EditorView, groupNodes: NodeType[], changeToNodeInGroup: (nodeType: NodeType<any> | undefined, view: EditorView, groupNodes: NodeType[]) => any) {
@@ -341,18 +483,24 @@ export class TooltipTextMenu {
execEvent: "",
class: "menuicon",
css: css,
- enable(state) { return true; },
+ enable() { return true; },
run() {
changeToNodeInGroup(nodeType, view, groupNodes);
}
});
}
+ markActive = function (state: EditorState<any>, type: MarkType<Schema<string, string>>) {
+ let { from, $from, to, empty } = state.selection;
+ if (empty) return type.isInSet(state.storedMarks || $from.marks());
+ else return state.doc.rangeHasMark(from, to, type);
+ };
+
// Helper function to create menu icons
- icon(text: string, name: string) {
+ icon(text: string, name: string, title: string = name) {
let span = document.createElement("span");
span.className = name + " menuicon";
- span.title = name;
+ span.title = title;
span.textContent = text;
span.style.color = "white";
return span;
@@ -395,6 +543,20 @@ export class TooltipTextMenu {
};
}
+ getMarksInSelection(state: EditorState<any>, targets: MarkType<any>[]) {
+ let found: Mark<any>[] = [];
+ let { from, to } = state.selection as TextSelection;
+ state.doc.nodesBetween(from, to, (node) => {
+ let marks = node.marks;
+ if (marks) {
+ marks.forEach(m => {
+ if (targets.includes(m.type)) found.push(m);
+ });
+ }
+ });
+ return found;
+ }
+
//updates the tooltip menu when the selection changes
update(view: EditorView, lastState: EditorState | undefined) {
let state = view.state;
@@ -404,76 +566,121 @@ export class TooltipTextMenu {
// Hide the tooltip if the selection is empty
if (state.selection.empty) {
- this.tooltip.style.display = "none";
- return;
+ //this.tooltip.style.display = "none";
+ //return;
}
- // Otherwise, reposition it and update its content
- this.tooltip.style.display = "";
- let { from, to } = state.selection;
- let start = view.coordsAtPos(from), end = view.coordsAtPos(to);
- // The box in which the tooltip is positioned, to use as base
- let box = this.tooltip.offsetParent!.getBoundingClientRect();
- // Find a center-ish x position from the selection endpoints (when
- // crossing lines, end may be more to the left)
- let left = Math.max((start.left + end.left) / 2, start.left + 3);
- this.tooltip.style.left = (left - box.left) * this.editorProps.ScreenToLocalTransform().Scale + "px";
- let width = Math.abs(start.left - end.left) / 2 * this.editorProps.ScreenToLocalTransform().Scale;
- let mid = Math.min(start.left, end.left) + width;
-
- this.tooltip.style.width = 225 + "px";
- this.tooltip.style.bottom = (box.bottom - start.top) * this.editorProps.ScreenToLocalTransform().Scale + "px";
//UPDATE LIST ITEM DROPDOWN
- this.listTypeBtnDom = this.updateListItemDropdown(":", this.listTypeBtnDom!);
//UPDATE FONT STYLE DROPDOWN
let activeStyles = this.activeMarksOnSelection(this.fontStyles);
- if (activeStyles.length === 1) {
- // if we want to update something somewhere with active font name
- let fontName = this.fontStylesToName.get(activeStyles[0]);
- if (fontName) { this.updateFontStyleDropdown(fontName); }
- } else if (activeStyles.length === 0) {
- //crimson on default
- this.updateFontStyleDropdown("Crimson Text");
- } else {
- this.updateFontStyleDropdown("Various");
+ if (activeStyles !== undefined) {
+ // activeStyles.forEach((markType) => {
+ // this._activeMarks.push(this.view.state.schema.mark(markType));
+ // });
+ if (activeStyles.length === 1) {
+ // if we want to update something somewhere with active font name
+ let fontName = this.fontStylesToName.get(activeStyles[0]);
+ if (fontName) { this.updateFontStyleDropdown(fontName); }
+ } else if (activeStyles.length === 0) {
+ //crimson on default
+ this.updateFontStyleDropdown("Crimson Text");
+ } else {
+ this.updateFontStyleDropdown("Various");
+ }
}
//UPDATE FONT SIZE DROPDOWN
let activeSizes = this.activeMarksOnSelection(this.fontSizes);
- if (activeSizes.length === 1) { //if there's only one active font size
- let size = this.fontSizeToNum.get(activeSizes[0]);
- if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
- } else if (activeSizes.length === 0) {
- //should be 14 on default
- this.updateFontSizeDropdown("14 pt");
- } else { //multiple font sizes selected
- this.updateFontSizeDropdown("Various");
+ if (activeSizes !== undefined) {
+ if (activeSizes.length === 1) { //if there's only one active font size
+ // activeSizes.forEach((markType) => {
+ // this._activeMarks.push(this.view.state.schema.mark(markType));
+ // });
+ let size = this.fontSizeToNum.get(activeSizes[0]);
+ if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
+ } else if (activeSizes.length === 0) {
+ //should be 14 on default
+ this.updateFontSizeDropdown("14 pt");
+ } else { //multiple font sizes selected
+ this.updateFontSizeDropdown("Various");
+ }
}
-
- this.updateLinkMenu();
+ this.view.dispatch(this.view.state.tr.setStoredMarks(this._activeMarks));
}
//finds all active marks on selection in given group
activeMarksOnSelection(markGroup: MarkType[]) {
//current selection
- let { empty, $cursor, ranges } = this.view.state.selection as TextSelection;
+ let { empty, ranges } = this.view.state.selection as TextSelection;
let state = this.view.state;
let dispatch = this.view.dispatch;
-
- let activeMarks = markGroup.filter(mark => {
- if (dispatch) {
- let has = false, tr = state.tr;
- for (let i = 0; !has && i < ranges.length; i++) {
- let { $from, $to } = ranges[i];
- return state.doc.rangeHasMark($from.pos, $to.pos, mark);
+ let activeMarks: MarkType[];
+ if (!empty) {
+ activeMarks = markGroup.filter(mark => {
+ if (dispatch) {
+ let has = false;
+ for (let i = 0; !has && i < ranges.length; i++) {
+ let { $from, $to } = ranges[i];
+ return state.doc.rangeHasMark($from.pos, $to.pos, mark);
+ }
+ }
+ return false;
+ });
+ }
+ else {
+ const pos = this.view.state.selection.$from;
+ const ref_node: ProsNode = this.reference_node(pos);
+ if (ref_node !== null && ref_node !== this.view.state.doc) {
+ if (ref_node.isText) {
+ }
+ else {
+ return [];
}
+
+ this._activeMarks = ref_node.marks;
+
+ activeMarks = markGroup.filter(mark_type => {
+ if (dispatch) {
+ let mark = state.schema.mark(mark_type);
+ return ref_node.marks.includes(mark);
+ }
+ return false;
+ });
}
- return false;
- });
+ else {
+ return [];
+ }
+
+ }
return activeMarks;
}
- destroy() { this.tooltip.remove(); }
+ reference_node(pos: ResolvedPos<any>): ProsNode {
+ let ref_node: ProsNode = this.view.state.doc;
+ if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) {
+ ref_node = pos.nodeAfter;
+ }
+ else if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) {
+ ref_node = pos.nodeBefore;
+ }
+ else if (pos.pos > 0) {
+ let skip = false;
+ for (let i: number = pos.pos - 1; i > 0; i--) {
+ this.view.state.doc.nodesBetween(i, pos.pos, (node: ProsNode) => {
+ if (node.isLeaf && !skip) {
+ ref_node = node;
+ skip = true;
+ }
+
+ });
+ }
+ }
+ return ref_node;
+ }
+
+ destroy() {
+ this.tooltip.remove();
+ }
}
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index c0ed015bd..156390fd3 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -94,6 +94,7 @@ export namespace UndoManager {
}
export function PrintBatches(): void {
+ console.log("Open Undo Batches:");
GetOpenBatches().forEach(batch => console.log(batch.batchName));
}
diff --git a/src/client/util/request-image-size.js b/src/client/util/request-image-size.js
new file mode 100644
index 000000000..27605d167
--- /dev/null
+++ b/src/client/util/request-image-size.js
@@ -0,0 +1,75 @@
+/**
+ * request-image-size: Detect image dimensions via request.
+ * Licensed under the MIT license.
+ *
+ * https://github.com/FdezRomero/request-image-size
+ * © 2017 Rodrigo Fernández Romero
+ *
+ * Based on the work of Johannes J. Schmidt
+ * https://github.com/jo/http-image-size
+ */
+
+const request = require('request');
+const imageSize = require('image-size');
+const HttpError = require('standard-http-error');
+
+module.exports = function requestImageSize(options) {
+ let opts = {
+ encoding: null
+ };
+
+ if (options && typeof options === 'object') {
+ opts = Object.assign(options, opts);
+ } else if (options && typeof options === 'string') {
+ opts = Object.assign({
+ uri: options
+ }, opts);
+ } else {
+ return Promise.reject(new Error('You should provide an URI string or a "request" options object.'));
+ }
+
+ opts.encoding = null;
+
+ return new Promise((resolve, reject) => {
+ const req = request(opts);
+
+ req.on('response', res => {
+ if (res.statusCode >= 400) {
+ return reject(new HttpError(res.statusCode, res.statusMessage));
+ }
+
+ let buffer = new Buffer([]);
+ let size;
+ let imageSizeError;
+
+ res.on('data', chunk => {
+ buffer = Buffer.concat([buffer, chunk]);
+
+ try {
+ size = imageSize(buffer);
+ } catch (err) {
+ imageSizeError = err;
+ return;
+ }
+
+ if (size) {
+ resolve(size);
+ return req.abort();
+ }
+ });
+
+ res.on('error', err => reject(err));
+
+ res.on('end', () => {
+ if (!size) {
+ return reject(imageSizeError);
+ }
+
+ size.downloaded = buffer.length;
+ return resolve(size);
+ });
+ });
+
+ req.on('error', err => reject(err));
+ });
+}; \ No newline at end of file
diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d
index 557f6f574..2cbe1dd40 100644
--- a/src/client/util/type_decls.d
+++ b/src/client/util/type_decls.d
@@ -141,17 +141,12 @@ declare abstract class RefField {
readonly [Id]: FieldId;
constructor();
- // protected [HandleUpdate]?(diff: any): void;
-
- // abstract [ToScriptString](): string;
}
-declare abstract class ObjectField {
- protected [OnUpdate](diff?: any): void;
- private [Parent]?: RefField | ObjectField;
- // abstract [Copy](): ObjectField;
+declare type FieldId = string;
- // abstract [ToScriptString](): string;
+declare abstract class ObjectField {
+ abstract [Copy](): ObjectField;
}
declare abstract class URLField extends ObjectField {
@@ -161,32 +156,51 @@ declare abstract class URLField extends ObjectField {
constructor(url: URL);
}
-declare class AudioField extends URLField { }
-declare class VideoField extends URLField { }
-declare class ImageField extends URLField { }
-declare class WebField extends URLField { }
-declare class PdfField extends URLField { }
+declare class AudioField extends URLField { [Copy](): ObjectField; }
+declare class VideoField extends URLField { [Copy](): ObjectField; }
+declare class ImageField extends URLField { [Copy](): ObjectField; }
+declare class WebField extends URLField { [Copy](): ObjectField; }
+declare class PdfField extends URLField { [Copy](): ObjectField; }
-declare type FieldId = string;
+declare const ComputedField: any;
+declare const CompileScript: any;
+// @ts-ignore
+declare type Extract<T, U> = T extends U ? T : never;
declare type Field = number | string | boolean | ObjectField | RefField;
+declare type FieldWaiting<T extends RefField = RefField> = T extends undefined ? never : Promise<T | undefined>;
+declare type FieldResult<T extends Field = Field> = Opt<T> | FieldWaiting<Extract<T, RefField>>;
declare type Opt<T> = T | undefined;
declare class Doc extends RefField {
constructor();
- [key: string]: Field | undefined;
+ [key: string]: FieldResult;
// [ToScriptString](): string;
}
declare class ListImpl<T extends Field> extends ObjectField {
constructor(fields?: T[]);
[index: number]: T | (T extends RefField ? Promise<T> : never);
- // [ToScriptString](): string;
- // [Copy](): ObjectField;
+ [Copy](): ObjectField;
}
// @ts-ignore
declare const console: any;
-declare const Documents: any;
+interface DocumentOptions { }
+
+declare const Docs: {
+ ImageDocument(url: string, options?: DocumentOptions): Doc;
+ VideoDocument(url: string, options?: DocumentOptions): Doc;
+ // HistogramDocument(url:string, options?:DocumentOptions);
+ TextDocument(options?: DocumentOptions): Doc;
+ PdfDocument(url: string, options?: DocumentOptions): Doc;
+ WebDocument(url: string, options?: DocumentOptions): Doc;
+ HtmlDocument(html: string, options?: DocumentOptions): Doc;
+ KVPDocument(document: Doc, options?: DocumentOptions): Doc;
+ FreeformDocument(documents: Doc[], options?: DocumentOptions): Doc;
+ SchemaDocument(columns: string[], documents: Doc[], options?: DocumentOptions): Doc;
+ TreeDocument(documents: Doc[], options?: DocumentOptions): Doc;
+ StackingDocument(documents: Doc[], options?: DocumentOptions): Doc;
+};
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index 7e066d53a..254163b53 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -8,19 +8,19 @@
flex-direction: column;
}
-.contextMenu-item:first-child {
- background: $intermediate-color;
- color: $light-color;
-}
+// .contextMenu-item:first-child {
+// background: $intermediate-color;
+// color: $light-color;
+// }
-.contextMenu-item:first-child::placeholder {
- color: $light-color;
-}
+// .contextMenu-item:first-child::placeholder {
+// color: $light-color;
+// }
-.contextMenu-item:first-child:hover {
- background: $intermediate-color;
- color: $light-color;
-}
+// .contextMenu-item:first-child:hover {
+// background: $intermediate-color;
+// color: $light-color;
+// }
.contextMenu-subMenu-cont {
position: absolute;
@@ -53,6 +53,33 @@
font-size: 20px;
}
+.contextMenu-itemSelected {
+ background: rgb(136, 136, 136)
+}
+
+.contextMenu-group {
+ // width: 11vw; //10vw
+ height: 30px; //2vh
+ background: rgb(200, 200, 200);
+ display: flex; //comment out to allow search icon to be inline with search text
+ justify-content: left;
+ align-items: center;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ transition: all .1s;
+ border-width: .11px;
+ border-style: none;
+ border-color: $intermediate-color; // rgb(187, 186, 186);
+ border-bottom-style: solid;
+ // padding: 10px 0px 10px 0px;
+ white-space: nowrap;
+ font-size: 20px;
+}
+
.contextMenu-item:hover {
transition: all 0.1s;
background: $lighter-alt-accent;
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index da374455e..c163c56a0 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -1,11 +1,12 @@
import React = require("react");
-import { ContextMenuItem, ContextMenuProps } from "./ContextMenuItem";
-import { observable, action } from "mobx";
-import { observer } from "mobx-react"
-import "./ContextMenu.scss"
+import { ContextMenuItem, ContextMenuProps, OriginalMenuProps } from "./ContextMenuItem";
+import { observable, action, computed } from "mobx";
+import { observer } from "mobx-react";
+import "./ContextMenu.scss";
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSearch, faCircle } from '@fortawesome/free-solid-svg-icons';
+import Measure from "react-measure";
library.add(faSearch);
library.add(faCircle);
@@ -14,29 +15,27 @@ library.add(faCircle);
export class ContextMenu extends React.Component {
static Instance: ContextMenu;
- @observable private _items: Array<ContextMenuProps> = [{ description: "test", event: (e: React.MouseEvent) => e.preventDefault(), icon: "smile" }];
+ @observable private _items: Array<ContextMenuProps> = [];
@observable private _pageX: number = 0;
@observable private _pageY: number = 0;
- @observable private _display: string = "none";
+ @observable private _display: boolean = false;
@observable private _searchString: string = "";
// afaik displaymenu can be called before all the items are added to the menu, so can't determine in displayMenu what the height of the menu will be
@observable private _yRelativeToTop: boolean = true;
+ @observable selectedIndex = -1;
-
- private ref: React.RefObject<HTMLDivElement>;
+ @observable private _width: number = 0;
+ @observable private _height: number = 0;
constructor(props: Readonly<{}>) {
super(props);
- this.ref = React.createRef();
-
ContextMenu.Instance = this;
}
@action
clearItems() {
this._items = [];
- this._display = "none";
}
@action
@@ -50,63 +49,174 @@ export class ContextMenu extends React.Component {
return this._items;
}
+ static readonly buffer = 20;
+ get pageX() {
+ const x = this._pageX;
+ if (x < 0) {
+ return 0;
+ }
+ const width = this._width;
+ if (x + width > window.innerWidth - ContextMenu.buffer) {
+ return window.innerWidth - ContextMenu.buffer - width;
+ }
+ return x;
+ }
+
+ get pageY() {
+ const y = this._pageY;
+ if (y < 0) {
+ return 0;
+ }
+ const height = this._height;
+ if (y + height > window.innerHeight - ContextMenu.buffer) {
+ return window.innerHeight - ContextMenu.buffer - height;
+ }
+ return y;
+ }
+
@action
displayMenu(x: number, y: number) {
//maxX and maxY will change if the UI/font size changes, but will work for any amount
//of items added to the menu
- let maxX = window.innerWidth - 150;
- let maxY = window.innerHeight - ((this._items.length + 1/*for search box*/) * 34 + 30);
- this._pageX = x > maxX ? maxX : x;
- this._pageY = y > maxY ? maxY : y;
+ this._pageX = x;
+ this._pageY = y;
this._searchString = "";
- this._display = "flex";
+ this._display = true;
}
- intersects = (x: number, y: number): boolean => {
- if (this.ref.current && this._display !== "none") {
- let menuSize = { width: this.ref.current.getBoundingClientRect().width, height: this.ref.current.getBoundingClientRect().height };
-
- let upperLeft = { x: this._pageX, y: this._yRelativeToTop ? this._pageY : window.innerHeight - (this._pageY + menuSize.height) };
- let bottomRight = { x: this._pageX + menuSize.width, y: this._yRelativeToTop ? this._pageY + menuSize.height : window.innerHeight - this._pageY };
+ @action
+ closeMenu = () => {
+ this.clearItems();
+ this._display = false;
+ }
- if (x >= upperLeft.x && x <= bottomRight.x) {
- if (y >= upperLeft.y && y <= bottomRight.y) {
- return true;
+ @computed get filteredItems(): (OriginalMenuProps | string[])[] {
+ const searchString = this._searchString.toLowerCase().split(" ");
+ const matches = (descriptions: string[]): boolean => {
+ return searchString.every(s => descriptions.some(desc => desc.toLowerCase().includes(s)));
+ };
+ const flattenItems = (items: ContextMenuProps[], groupFunc: (groupName: any) => string[]) => {
+ let eles: (OriginalMenuProps | string[])[] = [];
+
+ const leaves: OriginalMenuProps[] = [];
+ for (const item of items) {
+ const description = item.description;
+ const path = groupFunc(description);
+ if ("subitems" in item) {
+ const children = flattenItems(item.subitems, name => [...groupFunc(description), name]);
+ if (children.length || matches(path)) {
+ eles.push(path);
+ eles = eles.concat(children);
+ }
+ } else {
+ if (!matches(path)) {
+ continue;
+ }
+ leaves.push(item);
}
}
- }
- return false;
+
+ eles = [...leaves, ...eles];
+
+ return eles;
+ };
+ return flattenItems(this._items, name => [name]);
}
- @action
- closeMenu = () => {
- this.clearItems();
+ @computed get flatItems(): OriginalMenuProps[] {
+ return this.filteredItems.filter(item => !Array.isArray(item)) as OriginalMenuProps[];
}
- render() {
- let style = this._yRelativeToTop ? { left: this._pageX, top: this._pageY, display: this._display } :
- { left: this._pageX, bottom: this._pageY, display: this._display };
+ @computed get filteredViews() {
+ const createGroupHeader = (contents: any) => {
+ return (
+ <div className="contextMenu-group">
+ <div className="contextMenu-description">{contents}</div>
+ </div>
+ );
+ };
+ const createItem = (item: ContextMenuProps, selected: boolean) => <ContextMenuItem {...item} key={item.description} closeMenu={this.closeMenu} selected={selected} />;
+ let itemIndex = 0;
+ return this.filteredItems.map(value => {
+ if (Array.isArray(value)) {
+ return createGroupHeader(value.join(" -> "));
+ } else {
+ return createItem(value, itemIndex++ === this.selectedIndex);
+ }
+ });
+ }
+ @computed get menuItems() {
+ if (!this._searchString) {
+ return this._items.map(item => <ContextMenuItem {...item} key={item.description} closeMenu={this.closeMenu} />);
+ }
+ return this.filteredViews;
+ }
- return (
- <div className="contextMenu-cont" style={style} ref={this.ref}>
+ render() {
+ if (!this._display) {
+ return null;
+ }
+ let style = this._yRelativeToTop ? { left: this.pageX, top: this.pageY } :
+ { left: this.pageX, bottom: this.pageY };
+
+ const contents = (
+ <>
<span>
<span className="icon-background">
<FontAwesomeIcon icon="search" size="lg" />
</span>
- <input className="contextMenu-item contextMenu-description" type="text" placeholder="Search . . ." value={this._searchString} onChange={this.onChange} />
+ <input className="contextMenu-item contextMenu-description" type="text" placeholder="Search . . ." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus />
</span>
- {this._items.filter(prop => prop.description.toLowerCase().indexOf(this._searchString.toLowerCase()) !== -1).
- map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.closeMenu} />)}
- </div>
+ {this.menuItems}
+ </>
);
+ return (
+ <Measure offset onResize={action((r: any) => { this._width = r.offset.width; this._height = r.offset.height; })}>
+ {({ measureRef }) => (
+ <div className="contextMenu-cont" style={style} ref={measureRef}>
+ {contents}
+ </div>
+ )
+ }
+ </Measure>
+ );
+ }
+
+ @action
+ onKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "ArrowDown") {
+ if (this.selectedIndex < this.flatItems.length - 1) {
+ this.selectedIndex++;
+ }
+ e.preventDefault();
+ } else if (e.key === "ArrowUp") {
+ if (this.selectedIndex > 0) {
+ this.selectedIndex--;
+ }
+ e.preventDefault();
+ } else if (e.key === "Enter") {
+ const item = this.flatItems[this.selectedIndex];
+ item.event();
+ this.closeMenu();
+ }
}
@action
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this._searchString = e.target.value;
+ if (!this._searchString) {
+ this.selectedIndex = -1;
+ }
+ else {
+ if (this.selectedIndex === -1) {
+ this.selectedIndex = 0;
+ } else {
+ this.selectedIndex = Math.min(this.flatItems.length - 1, this.selectedIndex);
+ }
+ }
}
} \ No newline at end of file
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index fcda0db89..9bbb97d7e 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -1,12 +1,15 @@
import React = require("react");
import { observable, action } from "mobx";
import { observer } from "mobx-react";
-import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
+import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+library.add(faAngleRight);
+
export interface OriginalMenuProps {
description: string;
- event: (e: React.MouseEvent<HTMLDivElement>) => void;
+ event: () => void;
icon?: IconProp; //maybe should be optional (icon?)
closeMenu?: () => void;
}
@@ -14,16 +17,14 @@ export interface OriginalMenuProps {
export interface SubmenuProps {
description: string;
subitems: ContextMenuProps[];
+ icon?: IconProp; //maybe should be optional (icon?)
closeMenu?: () => void;
}
-export interface ContextMenuItemProps {
- type: ContextMenuProps | SubmenuProps;
-}
export type ContextMenuProps = OriginalMenuProps | SubmenuProps;
@observer
-export class ContextMenuItem extends React.Component<ContextMenuProps> {
+export class ContextMenuItem extends React.Component<ContextMenuProps & { selected?: boolean }> {
@observable private _items: Array<ContextMenuProps> = [];
@observable private overItem = false;
@@ -36,33 +37,64 @@ export class ContextMenuItem extends React.Component<ContextMenuProps> {
handleEvent = (e: React.MouseEvent<HTMLDivElement>) => {
if ("event" in this.props) {
- this.props.event(e);
+ this.props.event();
this.props.closeMenu && this.props.closeMenu();
}
}
+ currentTimeout?: any;
+ static readonly timeout = 300;
+ onPointerEnter = () => {
+ if (this.currentTimeout) {
+ clearTimeout(this.currentTimeout);
+ this.currentTimeout = undefined;
+ }
+ if (this.overItem) {
+ return;
+ }
+ this.currentTimeout = setTimeout(action(() => this.overItem = true), ContextMenuItem.timeout);
+ }
+
+ onPointerLeave = () => {
+ if (this.currentTimeout) {
+ clearTimeout(this.currentTimeout);
+ this.currentTimeout = undefined;
+ }
+ if (!this.overItem) {
+ return;
+ }
+ this.currentTimeout = setTimeout(action(() => this.overItem = false), ContextMenuItem.timeout);
+ }
+
render() {
if ("event" in this.props) {
return (
- <div className="contextMenu-item" onClick={this.handleEvent}>
- <span className="icon-background">
- {this.props.icon ? <FontAwesomeIcon icon={this.props.icon} size="sm" /> : <FontAwesomeIcon icon="circle" size="sm" />}
- </span>
+ <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onClick={this.handleEvent}>
+ {this.props.icon ? (
+ <span className="icon-background">
+ <FontAwesomeIcon icon={this.props.icon} size="sm" />
+ </span>
+ ) : null}
<div className="contextMenu-description">
{this.props.description}
</div>
</div>
);
- }
- else {
+ } else if ("subitems" in this.props) {
let submenu = !this.overItem ? (null) :
<div className="contextMenu-subMenu-cont" style={{ marginLeft: "100.5%", left: "0px" }}>
{this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)}
</div>;
return (
- <div className="contextMenu-item" onMouseEnter={action(() => { this.overItem = true; })} onMouseLeave={action(() => this.overItem = false)}>
+ <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onMouseEnter={this.onPointerEnter} onMouseLeave={this.onPointerLeave}>
+ {this.props.icon ? (
+ <span className="icon-background">
+ <FontAwesomeIcon icon={this.props.icon} size="sm" />
+ </span>
+ ) : null}
<div className="contextMenu-description">
{this.props.description}
+ <FontAwesomeIcon icon={faAngleRight} size="lg" style={{ position: "absolute", right: "5px" }} />
</div>
{submenu}
</div>
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index ba9f32d7d..2d430512b 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -26,6 +26,14 @@ $linkGap : 3px;
opacity: 0.8;
}
+ .documentDecorations-radius {
+ pointer-events: auto;
+ background: black;
+ opacity: 0.8;
+ transform: translate(10px, 10px);
+ grid-row: 4;
+ }
+
#documentDecorations-topLeftResizer,
#documentDecorations-leftResizer,
#documentDecorations-bottomLeftResizer {
@@ -44,11 +52,25 @@ $linkGap : 3px;
grid-column-start: 5;
grid-column-end: 7;
}
+
+ #documentDecorations-borderRadius{
+ grid-column-start: 5;
+ grid-column-end: 7;
+ border-radius: 100%;
+ .borderRadiusTooltip{
+ width:10px;
+ height:10px;
+ position:absolute;
+ }
+ }
#documentDecorations-topLeftResizer,
#documentDecorations-bottomRightResizer {
cursor: nwse-resize;
}
+ #documentDecorations-bottomRightResizer {
+ grid-row:4;
+ }
#documentDecorations-topRightResizer,
#documentDecorations-bottomLeftResizer {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 787033455..2e26a2286 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,33 +1,35 @@
-import { action, computed, observable, runInAction, untracked, reaction } from "mobx";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faLink } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, computed, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
+import { Doc } from "../../new_fields/Doc";
+import { List } from "../../new_fields/List";
+import { BoolCast, Cast, NumCast, StrCast } from "../../new_fields/Types";
+import { URLField } from '../../new_fields/URLField';
import { emptyFunction, Utils } from "../../Utils";
+import { Docs } from "../documents/Documents";
+import { DocumentManager } from "../util/DocumentManager";
import { DragLinksAsDocuments, DragManager } from "../util/DragManager";
import { SelectionManager } from "../util/SelectionManager";
-import { undoBatch } from "../util/UndoManager";
+import { undoBatch, UndoManager } from "../util/UndoManager";
+import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
+import { CollectionView } from "./collections/CollectionView";
import './DocumentDecorations.scss';
import { DocumentView, PositionDocument } from "./nodes/DocumentView";
+import { FieldView } from "./nodes/FieldView";
+import { FormattedTextBox } from "./nodes/FormattedTextBox";
+import { IconBox } from "./nodes/IconBox";
import { LinkMenu } from "./nodes/LinkMenu";
import { TemplateMenu } from "./TemplateMenu";
-import React = require("react");
import { Template, Templates } from "./Templates";
-import { CompileScript } from "../util/Scripting";
-import { IconBox } from "./nodes/IconBox";
-import { Cast, FieldValue, NumCast, StrCast } from "../../new_fields/Types";
-import { Doc, FieldResult } from "../../new_fields/Doc";
-import { listSpec } from "../../new_fields/Schema";
-import { Docs } from "../documents/Documents";
-import { List } from "../../new_fields/List";
+import React = require("react");
+import { RichTextField } from '../../new_fields/RichTextField';
+import { LinkManager } from '../util/LinkManager';
+import { ObjectField } from '../../new_fields/ObjectField';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
-import { faLink } from '@fortawesome/free-solid-svg-icons';
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
-import { CollectionView } from "./collections/CollectionView";
-import { DocumentManager } from "../util/DocumentManager";
-import { FormattedTextBox } from "./nodes/FormattedTextBox";
-import { FieldView } from "./nodes/FieldView";
library.add(faLink);
@@ -42,9 +44,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
private _titleHeight = 20;
private _linkButton = React.createRef<HTMLDivElement>();
private _linkerButton = React.createRef<HTMLDivElement>();
+ private _embedButton = React.createRef<HTMLDivElement>();
+ private _tooltipoff = React.createRef<HTMLDivElement>();
+ private _textDoc?: Doc;
private _downX = 0;
private _downY = 0;
private _iconDoc?: Doc = undefined;
+ private _resizeUndo?: UndoManager.Batch;
@observable private _minimizedX = 0;
@observable private _minimizedY = 0;
@observable private _title: string = "";
@@ -72,6 +78,32 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
if (text[0] === '#') {
this._fieldKey = text.slice(1, text.length);
this._title = this.selectionTitle;
+ } else if (text.startsWith(">")) {
+ let fieldTemplateView = SelectionManager.SelectedDocuments()[0];
+ SelectionManager.DeselectAll();
+ let fieldTemplate = fieldTemplateView.props.Document;
+ let docTemplate = fieldTemplateView.props.ContainingCollectionView!.props.Document;
+ let metaKey = text.slice(1, text.length);
+
+ // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)
+ let backgroundLayout = StrCast(fieldTemplate.backgroundLayout);
+ let layout = StrCast(fieldTemplate.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
+ if (backgroundLayout) {
+ layout = StrCast(fieldTemplate.layout).replace(/fieldKey={"annotations"}/, `fieldKey={"${metaKey}"} fieldExt={"annotations"}`);
+ backgroundLayout = backgroundLayout.replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
+ }
+ let nw = Cast(fieldTemplate.nativeWidth, "number");
+ let nh = Cast(fieldTemplate.nativeHeight, "number");
+
+ fieldTemplate.title = metaKey;
+ fieldTemplate.layout = layout;
+ fieldTemplate.backgroundLayout = backgroundLayout;
+ fieldTemplate.nativeWidth = nw;
+ fieldTemplate.nativeHeight = nh;
+ fieldTemplate.embed = true;
+ fieldTemplate.isTemplate = true;
+ fieldTemplate.templates = new List<string>([Templates.TitleBar(metaKey)]);
+ fieldTemplate.proto = Doc.GetProto(docTemplate);
}
else {
if (SelectionManager.SelectedDocuments().length > 0) {
@@ -121,7 +153,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
@computed
get Bounds(): { x: number, y: number, b: number, r: number } {
return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => {
- if (documentView.props.isTopMost) {
+ if (documentView.props.renderDepth === 0) {
return bounds;
}
let transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse();
@@ -140,7 +172,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
document.addEventListener("pointermove", this.onBackgroundMove);
document.addEventListener("pointerup", this.onBackgroundUp);
e.stopPropagation();
- e.preventDefault();
}
@action
@@ -148,7 +179,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let dragDocView = SelectionManager.SelectedDocuments()[0];
const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0);
const [xoff, yoff] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.x - left, e.y - top);
- let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document));
+ let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document), SelectionManager.SelectedDocuments().map(dv => dv.props.DataDoc ? dv.props.DataDoc : dv.props.Document));
dragData.xOffset = xoff;
dragData.yOffset = yoff;
dragData.moveDocument = SelectionManager.SelectedDocuments()[0].props.moveDocument;
@@ -309,6 +340,37 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
iconDoc.y = where[1] + NumCast(selView.props.Document.y);
}
+ _radiusDown = [0, 0];
+ @action
+ onRadiusDown = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ if (e.button === 0) {
+ this._radiusDown = [e.clientX, e.clientY];
+ this._isPointerDown = true;
+ this._resizeUndo = UndoManager.StartBatch("DocDecs set radius");
+ document.removeEventListener("pointermove", this.onRadiusMove);
+ document.removeEventListener("pointerup", this.onRadiusUp);
+ document.addEventListener("pointermove", this.onRadiusMove);
+ document.addEventListener("pointerup", this.onRadiusUp);
+ }
+ }
+
+ onRadiusMove = (e: PointerEvent): void => {
+ let dist = Math.sqrt((e.clientX - this._radiusDown[0]) * (e.clientX - this._radiusDown[0]) + (e.clientY - this._radiusDown[1]) * (e.clientY - this._radiusDown[1]));
+ SelectionManager.SelectedDocuments().map(dv => dv.props.Document.borderRounding = Doc.GetProto(dv.props.Document).borderRounding = `${Math.min(100, dist)}%`);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ onRadiusUp = (e: PointerEvent): void => {
+ e.stopPropagation();
+ e.preventDefault();
+ this._isPointerDown = false;
+ this._resizeUndo && this._resizeUndo.end();
+ document.removeEventListener("pointermove", this.onRadiusMove);
+ document.removeEventListener("pointerup", this.onRadiusUp);
+ }
+
@action
onPointerDown = (e: React.PointerEvent): void => {
e.stopPropagation();
@@ -316,6 +378,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
this._isPointerDown = true;
this._resizing = e.currentTarget.id;
this.Interacting = true;
+ this._resizeUndo = UndoManager.StartBatch("DocDecs resize");
document.removeEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
@@ -330,12 +393,27 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
document.removeEventListener("pointerup", this.onLinkerButtonUp);
document.addEventListener("pointerup", this.onLinkerButtonUp);
}
+
+ onEmbedButtonDown = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ document.removeEventListener("pointermove", this.onEmbedButtonMoved);
+ document.addEventListener("pointermove", this.onEmbedButtonMoved);
+ document.removeEventListener("pointerup", this.onEmbedButtonUp);
+ document.addEventListener("pointerup", this.onEmbedButtonUp);
+ }
+
onLinkerButtonUp = (e: PointerEvent): void => {
document.removeEventListener("pointermove", this.onLinkerButtonMoved);
document.removeEventListener("pointerup", this.onLinkerButtonUp);
e.stopPropagation();
}
+ onEmbedButtonUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onEmbedButtonMoved);
+ document.removeEventListener("pointerup", this.onEmbedButtonUp);
+ e.stopPropagation();
+ }
+
@action
onLinkerButtonMoved = (e: PointerEvent): void => {
if (this._linkerButton.current !== null) {
@@ -355,6 +433,25 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
e.stopPropagation();
}
+ @action
+ onEmbedButtonMoved = (e: PointerEvent): void => {
+ if (this._embedButton.current !== null) {
+ document.removeEventListener("pointermove", this.onEmbedButtonMoved);
+ document.removeEventListener("pointerup", this.onEmbedButtonUp);
+
+ let dragDocView = SelectionManager.SelectedDocuments()[0];
+ let dragData = new DragManager.EmbedDragData(dragDocView.props.Document);
+
+ DragManager.StartEmbedDrag(dragDocView.ContentDiv!, dragData, e.x, e.y, {
+ handlers: {
+ dragComplete: action(emptyFunction),
+ },
+ hideSource: false
+ });
+ }
+ e.stopPropagation();
+ }
+
onLinkButtonDown = (e: React.PointerEvent): void => {
e.stopPropagation();
document.removeEventListener("pointermove", this.onLinkButtonMoved);
@@ -429,31 +526,47 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
runInAction(() => FormattedTextBox.InputBoxOverlay = undefined);
SelectionManager.SelectedDocuments().forEach(element => {
- const rect = element.ContentDiv ? element.ContentDiv.getBoundingClientRect() : new DOMRect();
-
- if (rect.width !== 0 && (dX != 0 || dY != 0 || dW != 0 || dH != 0)) {
+ if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
let doc = PositionDocument(element.props.Document);
- let docHeightBefore = doc.height;
let nwidth = doc.nativeWidth || 0;
let nheight = doc.nativeHeight || 0;
- let zoomBasis = NumCast(doc.zoomBasis, 1);
- let width = (doc.width || 0) / zoomBasis;
- let height = (doc.height || (nheight / nwidth * width)) / zoomBasis;
- let scale = width / rect.width;
+ let width = (doc.width || 0);
+ let height = (doc.height || (nheight / nwidth * width));
+ let scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling();
let actualdW = Math.max(width + (dW * scale), 20);
let actualdH = Math.max(height + (dH * scale), 20);
doc.x = (doc.x || 0) + dX * (actualdW - width);
doc.y = (doc.y || 0) + dY * (actualdH - height);
+ let proto = Doc.GetProto(element.props.Document);
+ let fixedAspect = e.ctrlKey || (!BoolCast(proto.ignoreAspect, false) && nwidth && nheight);
+ if (fixedAspect && (!nwidth || !nheight)) {
+ proto.nativeWidth = nwidth = doc.width || 0;
+ proto.nativeHeight = nheight = doc.height || 0;
+ proto.ignoreAspect = true;
+ }
if (nwidth > 0 && nheight > 0) {
if (Math.abs(dW) > Math.abs(dH)) {
- doc.zoomBasis = zoomBasis * width / actualdW;
+ if (!fixedAspect) {
+ Doc.SetInPlace(element.props.Document, "nativeWidth", actualdW / (doc.width || 1) * (doc.nativeWidth || 0), true);
+ }
+ doc.width = actualdW;
+ if (fixedAspect) doc.height = nheight / nwidth * doc.width;
+ else doc.height = actualdH;
+ Doc.SetInPlace(element.props.Document, "nativeHeight", (doc.height || 0) / doc.width * (doc.nativeWidth || 0), true);
}
else {
- doc.zoomBasis = zoomBasis * height / actualdH;
+ if (!fixedAspect) {
+ Doc.SetInPlace(element.props.Document, "nativeHeight", actualdH / (doc.height || 1) * (doc.nativeHeight || 0), true);
+ }
+ doc.height = actualdH;
+ if (fixedAspect) doc.width = nwidth / nheight * doc.height;
+ else doc.width = actualdW;
+ Doc.SetInPlace(element.props.Document, "nativeWidth", (doc.width || 0) / doc.height * (doc.nativeHeight || 0), true);
}
} else {
- doc.width = zoomBasis * actualdW;
- if (docHeightBefore === doc.height) doc.height = zoomBasis * actualdH;
+ dW && (doc.width = actualdW);
+ dH && (doc.height = actualdH);
+ Doc.SetInPlace(element.props.Document, "autoHeight", undefined, true);
}
}
});
@@ -464,10 +577,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
e.stopPropagation();
this._resizing = "";
this.Interacting = false;
- SelectionManager.ReselectAll();
if (e.button === 0) {
e.preventDefault();
this._isPointerDown = false;
+ this._resizeUndo && this._resizeUndo.end();
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
}
@@ -496,6 +609,50 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
// e.stopPropagation();
// }
+ considerEmbed = () => {
+ let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document;
+ let canEmbed = thisDoc.data && thisDoc.data instanceof URLField;
+ if (!canEmbed) return (null);
+ return (
+ <div className="linkButtonWrapper">
+ <div style={{ paddingTop: 3, marginLeft: 30 }} title="Drag Embed" className="linkButton-linker" ref={this._embedButton} onPointerDown={this.onEmbedButtonDown}>
+ <FontAwesomeIcon className="fa-image" icon="image" size="sm" />
+ </div>
+ </div>
+ );
+ }
+
+ considerTooltip = () => {
+ let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document;
+ let isTextDoc = thisDoc.data && thisDoc.data instanceof RichTextField;
+ if (!isTextDoc) return null;
+ this._textDoc = thisDoc;
+ return (
+ <div className="tooltipwrapper">
+ <div style={{ paddingTop: 3, marginLeft: 30 }} title="Hide Tooltip" className="linkButton-linker" ref={this._tooltipoff} onPointerDown={this.onTooltipOff}>
+ {/* <FontAwesomeIcon className="fa-image" icon="image" size="sm" /> */}
+ T
+ </div>
+ </div>
+
+ );
+ }
+
+ onTooltipOff = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ if (this._textDoc) {
+ if (this._tooltipoff.current) {
+ if (this._tooltipoff.current.title === "Hide Tooltip") {
+ this._tooltipoff.current.title = "Show Tooltip";
+ this._textDoc.tooltip = "hi";
+ }
+ else {
+ this._tooltipoff.current.title = "Hide Tooltip";
+ }
+ }
+ }
+ }
+
render() {
var bounds = this.Bounds;
let seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined;
@@ -510,9 +667,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let linkButton = null;
if (SelectionManager.SelectedDocuments().length > 0) {
let selFirst = SelectionManager.SelectedDocuments()[0];
- let linkToSize = Cast(selFirst.props.Document.linkedToDocs, listSpec(Doc), []).length;
- let linkFromSize = Cast(selFirst.props.Document.linkedFromDocs, listSpec(Doc), []).length;
- let linkCount = linkToSize + linkFromSize;
+ let linkCount = LinkManager.Instance.getAllRelatedLinks(selFirst.props.Document).length;
linkButton = (<Flyout
anchorPoint={anchorPoints.RIGHT_TOP}
content={<LinkMenu docView={selFirst}
@@ -523,11 +678,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let templates: Map<Template, boolean> = new Map();
Array.from(Object.values(Templates.TemplateList)).map(template => {
- let sorted = SelectionManager.ViewsSortedVertically().slice().sort((doc1, doc2) => {
- if (NumCast(doc1.props.Document.x) > NumCast(doc2.props.Document.x)) return 1;
- if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
- return 0;
- });
+ let sorted = SelectionManager.ViewsSortedVertically(); // slice().sort((doc1, doc2) => {
+ // if (NumCast(doc1.props.Document.y) > NumCast(doc2.props.Document.x)) return 1;
+ // if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
+ // return 0;
+ // });
let docTemps = sorted.reduce((res: string[], doc: DocumentView, i) => {
let temps = doc.props.Document.templates;
if (temps instanceof List) {
@@ -555,7 +710,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
left: bounds.x - this._resizeBorderWidth / 2,
top: bounds.y - this._resizeBorderWidth / 2,
pointerEvents: this.Interacting ? "none" : "all",
- zIndex: SelectionManager.SelectedDocuments().length > 1 ? 1000 : 0,
+ zIndex: SelectionManager.SelectedDocuments().length > 1 ? 900 : 0,
}} onPointerDown={this.onBackgroundDown} onContextMenu={(e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); }} >
</div>
<div className="documentDecorations-container" style={{
@@ -570,7 +725,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
{this._edtingTitle ?
<input ref={this.keyinput} className="title" type="text" name="dynbox" value={this._title} onBlur={this.titleBlur} onChange={this.titleChanged} onKeyPress={this.titleEntered} /> :
<div className="title" onPointerDown={this.onTitleDown} ><span>{`${this.selectionTitle}`}</span></div>}
- <div className="documentDecorations-closeButton" onPointerDown={this.onCloseDown}>X</div>
+ <div className="documentDecorations-closeButton" title="Close Document" onPointerDown={this.onCloseDown}>X</div>
<div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-topResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-topRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
@@ -580,6 +735,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
<div id="documentDecorations-bottomLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-bottomResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+ <div id="documentDecorations-borderRadius" className="documentDecorations-radius" onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}><span className="borderRadiusTooltip" title="Drag Corner Radius"></span></div>
<div className="link-button-container">
<div className="linkButtonWrapper">
<div title="View Links" className="linkFlyout" ref={this._linkButton}> {linkButton} </div>
@@ -590,6 +746,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
</div>
</div>
<TemplateMenu docs={SelectionManager.ViewsSortedVertically()} templates={templates} />
+ {this.considerEmbed()}
+ {/* {this.considerTooltip()} */}
</div>
</div >
</div>
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index c946d68e1..989fb1be9 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -14,17 +14,23 @@ export interface EditableProps {
* @param value - The string entered by the user to set the value to
* @returns `true` if setting the value was successful, `false` otherwise
* */
- SetValue(value: string): boolean;
+ SetValue(value: string, shiftDown?: boolean): boolean;
OnFillDown?(value: string): void;
+ OnTab?(): void;
+
/**
* The contents to render when not editing
*/
contents: any;
+ fontStyle?: string;
+ fontSize?: number;
height?: number;
display?: string;
oneLine?: boolean;
+ editing?: boolean;
+ onClick?: (e: React.MouseEvent) => boolean;
}
/**
@@ -34,40 +40,59 @@ export interface EditableProps {
*/
@observer
export class EditableView extends React.Component<EditableProps> {
- @observable
- editing: boolean = false;
+ @observable _editing: boolean = false;
+
+ constructor(props: EditableProps) {
+ super(props);
+ this._editing = this.props.editing ? true : false;
+ }
@action
onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
- if (e.key === "Enter") {
+ if (e.key === "Tab") {
+ this.props.OnTab && this.props.OnTab();
+ } else if (e.key === "Enter") {
if (!e.ctrlKey) {
- if (this.props.SetValue(e.currentTarget.value)) {
- this.editing = false;
+ if (this.props.SetValue(e.currentTarget.value, e.shiftKey)) {
+ this._editing = false;
}
} else if (this.props.OnFillDown) {
this.props.OnFillDown(e.currentTarget.value);
- this.editing = false;
+ this._editing = false;
}
} else if (e.key === "Escape") {
- this.editing = false;
+ this._editing = false;
}
}
@action
onClick = (e: React.MouseEvent) => {
- this.editing = true;
+ if (!this.props.onClick || !this.props.onClick(e)) {
+ this._editing = true;
+ }
+ e.stopPropagation();
+ }
+
+ stopPropagation(e: React.SyntheticEvent) {
e.stopPropagation();
}
+ @action
+ setIsFocused = (value: boolean) => {
+ this._editing = value;
+ }
+
render() {
- if (this.editing) {
- return <input className="editableView-input" defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus onBlur={action(() => this.editing = false)}
- style={{ display: this.props.display }} />;
+ if (this._editing) {
+ return <input className="editableView-input" defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus
+ onBlur={action(() => this._editing = false)} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation}
+ style={{ display: this.props.display, fontSize: this.props.fontSize }} />;
} else {
return (
- <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }}
+ <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`}
+ style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }}
onClick={this.onClick} >
- <span>{this.props.contents}</span>
+ <span style={{ fontStyle: this.props.fontStyle }}>{this.props.contents}</span>
</div>
);
}
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
new file mode 100644
index 000000000..0d95bb96c
--- /dev/null
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -0,0 +1,161 @@
+import { UndoManager, undoBatch } from "../util/UndoManager";
+import { SelectionManager } from "../util/SelectionManager";
+import { CollectionDockingView } from "./collections/CollectionDockingView";
+import { MainView } from "./MainView";
+import { DragManager } from "../util/DragManager";
+import { action } from "mobx";
+
+const modifiers = ["control", "meta", "shift", "alt"];
+type KeyHandler = (keycode: string) => KeyControlInfo;
+type KeyControlInfo = {
+ preventDefault: boolean,
+ stopPropagation: boolean
+};
+
+export default class KeyManager {
+ public static Instance: KeyManager = new KeyManager();
+ private router = new Map<string, KeyHandler>();
+
+ constructor() {
+ let isMac = navigator.platform.toLowerCase().indexOf("mac") >= 0;
+
+ // SHIFT CONTROL ALT META
+ this.router.set("0000", this.unmodified);
+ this.router.set(isMac ? "0001" : "0100", this.ctrl);
+ this.router.set(isMac ? "0100" : "0010", this.alt);
+ this.router.set(isMac ? "1001" : "1100", this.ctrl_shift);
+ }
+
+ public handle = (e: KeyboardEvent) => {
+ let keyname = e.key.toLowerCase();
+ this.handleGreedy(keyname);
+
+ if (modifiers.includes(keyname)) {
+ return;
+ }
+
+ let bit = (value: boolean) => value ? "1" : "0";
+ let modifierIndex = bit(e.shiftKey) + bit(e.ctrlKey) + bit(e.altKey) + bit(e.metaKey);
+
+ let handleConstrained = this.router.get(modifierIndex);
+ if (!handleConstrained) {
+ return;
+ }
+
+ let control = handleConstrained(keyname);
+
+ control.stopPropagation && e.stopPropagation();
+ control.preventDefault && e.preventDefault();
+ }
+
+ private handleGreedy = action((keyname: string) => {
+ switch (keyname) {
+ }
+ });
+
+ private unmodified = action((keyname: string) => {
+ switch (keyname) {
+ case "escape":
+ if (MainView.Instance.isPointerDown) {
+ DragManager.AbortDrag();
+ } else {
+ if (CollectionDockingView.Instance.HasFullScreen()) {
+ CollectionDockingView.Instance.CloseFullScreen();
+ } else {
+ SelectionManager.DeselectAll();
+ }
+ }
+ MainView.Instance.toggleColorPicker(true);
+ break;
+ case "delete":
+ case "backspace":
+ SelectionManager.SelectedDocuments().map(docView => {
+ let doc = docView.props.Document;
+ let remove = docView.props.removeDocument;
+ remove && remove(doc);
+ });
+ break;
+ }
+
+ return {
+ stopPropagation: false,
+ preventDefault: false
+ };
+ });
+
+ private alt = action((keyname: string) => {
+ let stopPropagation = true;
+ let preventDefault = true;
+
+ switch (keyname) {
+ case "n":
+ let toggle = MainView.Instance.addMenuToggle.current!;
+ toggle.checked = !toggle.checked;
+ break;
+ }
+
+ return {
+ stopPropagation: stopPropagation,
+ preventDefault: preventDefault
+ };
+ });
+
+ private ctrl = action((keyname: string) => {
+ let stopPropagation = true;
+ let preventDefault = true;
+
+ switch (keyname) {
+ case "arrowright":
+ MainView.Instance.mainFreeform && CollectionDockingView.Instance.AddRightSplit(MainView.Instance.mainFreeform, undefined);
+ break;
+ case "arrowleft":
+ MainView.Instance.mainFreeform && CollectionDockingView.Instance.CloseRightSplit(MainView.Instance.mainFreeform);
+ break;
+ case "f":
+ MainView.Instance.isSearchVisible = !MainView.Instance.isSearchVisible;
+ break;
+ case "o":
+ let target = SelectionManager.SelectedDocuments()[0];
+ target && target.fullScreenClicked();
+ break;
+ case "r":
+ preventDefault = false;
+ break;
+ case "y":
+ UndoManager.Redo();
+ break;
+ case "z":
+ UndoManager.Undo();
+ break;
+ case "a":
+ case "c":
+ case "v":
+ case "x":
+ stopPropagation = false;
+ preventDefault = false;
+ break;
+ }
+
+ return {
+ stopPropagation: stopPropagation,
+ preventDefault: preventDefault
+ };
+ });
+
+ private ctrl_shift = action((keyname: string) => {
+ let stopPropagation = true;
+ let preventDefault = true;
+
+ switch (keyname) {
+ case "z":
+ UndoManager.Redo();
+ break;
+ }
+
+ return {
+ stopPropagation: stopPropagation,
+ preventDefault: preventDefault
+ };
+ });
+
+} \ No newline at end of file
diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
index 42ab08001..fd7e5b07d 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -14,6 +14,7 @@ import { Cast, PromiseValue, NumCast } from "../../new_fields/Types";
interface InkCanvasProps {
getScreenTransform: () => Transform;
Document: Doc;
+ inkFieldKey: string;
children: () => JSX.Element[];
}
@@ -40,7 +41,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
}
componentDidMount() {
- PromiseValue(Cast(this.props.Document.ink, InkField)).then(ink => runInAction(() => {
+ PromiseValue(Cast(this.props.Document[this.props.inkFieldKey], InkField)).then(ink => runInAction(() => {
if (ink) {
let bounds = Array.from(ink.inkData).reduce(([mix, max, miy, may], [id, strokeData]) =>
strokeData.pathData.reduce(([mix, max, miy, may], p) =>
@@ -55,12 +56,12 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
@computed
get inkData(): Map<string, StrokeData> {
- let map = Cast(this.props.Document.ink, InkField);
+ let map = Cast(this.props.Document[this.props.inkFieldKey], InkField);
return !map ? new Map : new Map(map.inkData);
}
set inkData(value: Map<string, StrokeData>) {
- Doc.SetOnPrototype(this.props.Document, "ink", new InkField(value));
+ Doc.GetProto(this.props.Document)[this.props.inkFieldKey] = new InkField(value);
}
@action
@@ -74,7 +75,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
e.stopPropagation();
e.preventDefault();
- this.previousState = this.inkData;
+ this.previousState = new Map(this.inkData);
if (InkingControl.Instance.selectedTool !== InkTool.Eraser) {
// start the new line, saves a uuid to represent the field of the stroke
@@ -106,10 +107,10 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
const batch = UndoManager.StartBatch("One ink stroke");
const oldState = this.previousState || new Map;
this.previousState = undefined;
- const newState = this.inkData;
+ const newState = new Map(this.inkData);
UndoManager.AddEvent({
undo: () => this.inkData = oldState,
- redo: () => this.inkData = newState,
+ redo: () => this.inkData = newState
});
batch.end();
}
@@ -134,9 +135,13 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
return { x, y };
}
- @undoBatch
@action
removeLine = (id: string): void => {
+ if (!this.previousState) {
+ this.previousState = new Map(this.inkData);
+ document.addEventListener("pointermove", this.onPointerMove, true);
+ document.addEventListener("pointerup", this.onPointerUp, true);
+ }
let data = this.inkData;
data.delete(id);
this.inkData = data;
diff --git a/src/client/views/InkingControl.scss b/src/client/views/InkingControl.scss
index ba4ec41af..465e14d07 100644
--- a/src/client/views/InkingControl.scss
+++ b/src/client/views/InkingControl.scss
@@ -1,8 +1,6 @@
@import "globalCssVariables";
.inking-control {
- position: absolute;
- left: 70px;
- bottom: 70px;
+ bottom: 20px;
margin: 0;
padding: 0;
display: flex;
@@ -63,10 +61,9 @@
margin-top: 4px;
}
.ink-panel {
- margin: 6px 12px 6px 0;
- height: 30px;
+ height: 24px;
vertical-align: middle;
- line-height: 36px;
+ line-height: 28px;
padding: 0 10px;
color: $intermediate-color;
&:first {
@@ -114,7 +111,6 @@
border-radius: 11px;
width: 22px;
height: 22px;
- margin-top: 6px;
cursor: pointer;
text-align: center; // span {
// color: $light-color;
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index d456f531f..c7f7bdb66 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -1,15 +1,17 @@
-import { observable, action, computed } from "mobx";
-
-import { CirclePicker, ColorResult } from 'react-color';
+import { observable, action, computed, runInAction } from "mobx";
+import { ColorResult } from 'react-color';
import React = require("react");
import { observer } from "mobx-react";
import "./InkingControl.scss";
import { library } from '@fortawesome/fontawesome-svg-core';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPen, faHighlighter, faEraser, faBan } from '@fortawesome/free-solid-svg-icons';
import { SelectionManager } from "../util/SelectionManager";
import { InkTool } from "../../new_fields/InkField";
import { Doc } from "../../new_fields/Doc";
+import { undoBatch, UndoManager } from "../util/UndoManager";
+import { StrCast } from "../../new_fields/Types";
+import { FormattedTextBox } from "./nodes/FormattedTextBox";
+import { MainOverlayTextBox } from "./MainOverlayTextBox";
library.add(faPen, faHighlighter, faEraser, faBan);
@@ -19,8 +21,7 @@ export class InkingControl extends React.Component {
@observable private _selectedTool: InkTool = InkTool.None;
@observable private _selectedColor: string = "rgb(244, 67, 54)";
@observable private _selectedWidth: string = "25";
- @observable private _open: boolean = false;
- @observable private _colorPickerDisplay = false;
+ @observable public _open: boolean = false;
constructor(props: Readonly<{}>) {
super(props);
@@ -31,13 +32,37 @@ export class InkingControl extends React.Component {
switchTool = (tool: InkTool): void => {
this._selectedTool = tool;
}
+ decimalToHexString(number: number) {
+ if (number < 0) {
+ number = 0xFFFFFFFF + number + 1;
+ }
- @action
- switchColor = (color: ColorResult): void => {
- this._selectedColor = color.hex;
- SelectionManager.SelectedDocuments().forEach(doc => Doc.GetProto(doc.props.Document).backgroundColor = color.hex);
+ return number.toString(16).toUpperCase();
}
+ @undoBatch
+ switchColor = action((color: ColorResult): void => {
+ this._selectedColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff");
+ if (InkingControl.Instance.selectedTool === InkTool.None) {
+ if (MainOverlayTextBox.Instance.SetColor(color.hex)) return;
+ let selected = SelectionManager.SelectedDocuments();
+ let oldColors = selected.map(view => {
+ let targetDoc = view.props.Document.isTemplate ? view.props.Document : Doc.GetProto(view.props.Document);
+ let oldColor = StrCast(targetDoc.backgroundColor);
+ targetDoc.backgroundColor = this._selectedColor;
+ return {
+ target: targetDoc,
+ previous: oldColor
+ };
+ });
+ let captured = this._selectedColor;
+ UndoManager.AddEvent({
+ undo: () => oldColors.forEach(pair => pair.target.backgroundColor = pair.previous),
+ redo: () => oldColors.forEach(pair => pair.target.backgroundColor = captured)
+ });
+ }
+ });
+
@action
switchWidth = (width: string): void => {
this._selectedWidth = width;
@@ -53,50 +78,24 @@ export class InkingControl extends React.Component {
return this._selectedColor;
}
+ @action
+ updateSelectedColor(value: string) {
+ this._selectedColor = value;
+ }
+
@computed
get selectedWidth() {
return this._selectedWidth;
}
- selected = (tool: InkTool) => {
- if (this._selectedTool === tool) {
- return { color: "#61aaa3" };
- }
- return {};
- }
-
@action
toggleDisplay = () => {
this._open = !this._open;
+ this.switchTool(this._open ? InkTool.Pen : InkTool.None);
}
-
-
- @action
- toggleColorPicker = () => {
- this._colorPickerDisplay = !this._colorPickerDisplay;
- }
-
render() {
return (
<ul className="inking-control" style={this._open ? { display: "flex" } : { display: "none" }}>
- <li className="ink-tools ink-panel">
- <div className="ink-tool-buttons">
- <button onClick={() => this.switchTool(InkTool.Pen)} style={this.selected(InkTool.Pen)}><FontAwesomeIcon icon="pen" size="lg" title="Pen" /></button>
- <button onClick={() => this.switchTool(InkTool.Highlighter)} style={this.selected(InkTool.Highlighter)}><FontAwesomeIcon icon="highlighter" size="lg" title="Highlighter" /></button>
- <button onClick={() => this.switchTool(InkTool.Eraser)} style={this.selected(InkTool.Eraser)}><FontAwesomeIcon icon="eraser" size="lg" title="Eraser" /></button>
- <button onClick={() => this.switchTool(InkTool.None)} style={this.selected(InkTool.None)}><FontAwesomeIcon icon="ban" size="lg" title="Pointer" /></button>
- </div>
- </li>
- <li className="ink-color ink-panel">
- <label>COLOR: </label>
- <div className="ink-color-display" style={{ backgroundColor: this._selectedColor }}
- onClick={() => this.toggleColorPicker()}>
- {/* {this._colorPickerDisplay ? <span>&#9660;</span> : <span>&#9650;</span>} */}
- </div>
- <div className="ink-color-picker" style={this._colorPickerDisplay ? { display: "block" } : { display: "none" }}>
- <CirclePicker onChange={this.switchColor} circleSize={22} width={"220"} />
- </div>
- </li>
<li className="ink-size ink-panel">
<label htmlFor="stroke-width">SIZE: </label>
<input type="text" min="1" max="100" value={this._selectedWidth} name="stroke-width"
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 37b1f5899..b8d428d31 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -28,6 +28,8 @@ export class InkingStroke extends React.Component<StrokeProps> {
deleteStroke = (e: React.PointerEvent): void => {
if (InkingControl.Instance.selectedTool === InkTool.Eraser && e.buttons === 1) {
this.props.deleteCallback(this.props.id);
+ e.stopPropagation();
+ e.preventDefault();
}
}
@@ -50,7 +52,7 @@ export class InkingStroke extends React.Component<StrokeProps> {
render() {
let pathStyle = this.createStyle();
let pathData = this.parseData(this.props.line);
- let pathlength = this.props.count; // bcz: this is needed to force reactions to the line data changes
+ let pathlength = this.props.count; // bcz: this is needed to force reactions to the line's data changes
let marker = this.props.tool === InkTool.Highlighter ? "-marker" : "";
let pointerEvents: any = InkingControl.Instance.selectedTool === InkTool.Eraser ? "all" : "none";
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index 57a53c999..f52e3b658 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -20,18 +20,11 @@ div {
-ms-user-select: none;
}
-#dash-title {
- position: absolute;
- right: 46.5%;
- letter-spacing: 3px;
- top: 9px;
- font-size: 12px;
- color: $alt-accent;
- z-index: 9999;
-}
+
.jsx-parser {
width: 100%;
+ height:100%;
pointer-events: none;
border-radius: inherit;
}
@@ -43,8 +36,8 @@ p {
::-webkit-scrollbar {
-webkit-appearance: none;
- height: 10px;
- width: 10px;
+ height: 8px;
+ width: 8px;
}
::-webkit-scrollbar-thumb {
@@ -118,22 +111,42 @@ button:hover {
//toolbar stuff
#toolbar {
position: absolute;
- bottom: 62px;
- left: 24px;
+ right: 8px;
+ top: 5px;
.toolbar-button {
display: block;
margin-bottom: 10px;
}
}
+.toolbar-color-picker {
+ background-color: $light-color;
+ border-radius: 5px;
+ padding: 12px;
+ position: absolute;
+ bottom: 36px;
+ left: -3px;
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
+}
+.toolbar-color-button {
+ border-radius: 11px;
+ width: 22px;
+ height: 22px;
+ cursor: pointer;
+ text-align: center; // span {
+ // color: $light-color;
+ // font-size: 8px;
+ // user-select: none;
+ // }
+}
// add nodes menu. Note that the + button is actually an input label, not an actual button.
#add-nodes-menu {
position: absolute;
bottom: 22px;
- left: 24px;
+ left: 250px;
- label {
+ > label {
background: $dark-color;
color: $light-color;
display: inline-block;
@@ -155,15 +168,15 @@ button:hover {
transform: scale(1.15);
}
- input {
+ > input {
display: none;
}
- input:not(:checked)~#add-options-content {
+ > input:not(:checked)~#add-options-content {
display: none;
}
- input:checked~label {
+ > input:checked~label {
transform: rotate(45deg);
transition: transform 0.5s;
cursor: pointer;
@@ -180,7 +193,7 @@ button:hover {
position: absolute;
top: 0;
left: 0;
- overflow: scroll;
+ overflow: auto;
z-index: 1;
}
@@ -190,6 +203,7 @@ button:hover {
position: absolute;
top: 0;
left: 0;
+ overflow: hidden;
}
#add-options-content {
@@ -207,8 +221,42 @@ ul#add-options-list {
list-style: none;
padding: 5 0 0 0;
- li {
+ > li {
display: inline-block;
padding: 0;
}
+}
+
+.mainView-libraryFlyout {
+ height: 100%;
+ position: absolute;
+ display: flex;
+ flex-direction:column;
+}
+
+.mainView-libraryHandle {
+ opacity: 0.6;
+ width: 20px;
+ height: 40px;
+ top: 50%;
+ border-radius: 20px;
+ position: absolute;
+ z-index: 1;
+ background: gray;
+}
+
+.mainView-workspace {
+ height:200px;
+ position:relative;
+ display:flex;
+}
+.mainView-library {
+ height:75%;
+ position:relative;
+ display:flex;
+}
+.mainView-recentlyClosed {
+ height:25%;
+ position:relative;
+ display:flex;
} \ No newline at end of file
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 98b14f9c8..932a6375f 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -8,4 +8,4 @@ import * as React from 'react';
await Docs.Prototypes.initialize();
await CurrentUserUtils.loadCurrentUser();
ReactDOM.render(<MainView />, document.getElementById('root'));
-})();
+})(); \ No newline at end of file
diff --git a/src/client/views/MainOverlayTextBox.scss b/src/client/views/MainOverlayTextBox.scss
index f6a746e63..f636ca070 100644
--- a/src/client/views/MainOverlayTextBox.scss
+++ b/src/client/views/MainOverlayTextBox.scss
@@ -1,7 +1,7 @@
@import "globalCssVariables";
.mainOverlayTextBox-textInput {
background-color: rgba(248, 6, 6, 0.001);
- width: 200px;
+ width: 400px;
height: 200px;
position:absolute;
overflow: visible;
@@ -17,4 +17,9 @@
top: 0;
left: 0;
}
+}
+.mainOverlayTextBox-.unscaled_div{
+ z-index: 10000;
+ position: absolute;
+ pointer-events: none;
} \ No newline at end of file
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index 718979123..d8aaea259 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -1,14 +1,15 @@
import { action, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
+import "normalize.css";
import * as React from 'react';
-import { emptyFunction, returnTrue, returnZero } from '../../Utils';
+import { Doc } from '../../new_fields/Doc';
+import { BoolCast } from '../../new_fields/Types';
+import { emptyFunction, returnTrue, returnZero, Utils } from '../../Utils';
import { DragManager } from '../util/DragManager';
import { Transform } from '../util/Transform';
-import "normalize.css";
+import { CollectionDockingView } from './collections/CollectionDockingView';
import "./MainOverlayTextBox.scss";
import { FormattedTextBox } from './nodes/FormattedTextBox';
-import { CollectionDockingView } from './collections/CollectionDockingView';
-import { Doc } from '../../new_fields/Doc';
interface MainOverlayTextBoxProps {
}
@@ -17,11 +18,28 @@ interface MainOverlayTextBoxProps {
export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> {
public static Instance: MainOverlayTextBox;
@observable _textXf: () => Transform = () => Transform.Identity();
- private _textFieldKey: string = "data";
+ public TextFieldKey: string = "data";
private _textColor: string | null = null;
private _textHideOnLeave?: boolean;
private _textTargetDiv: HTMLDivElement | undefined;
private _textProxyDiv: React.RefObject<HTMLDivElement>;
+ private _textBottom: boolean | undefined;
+ private _textAutoHeight: boolean | undefined;
+ private _setouterdiv = (outerdiv: HTMLElement | null) => { this._outerdiv = outerdiv; this.updateTooltip(); };
+ private _outerdiv: HTMLElement | null = null;
+ private _textBox: FormattedTextBox | undefined;
+ private _tooltip?: HTMLElement;
+ @observable public TextDoc?: Doc;
+ @observable public TextDataDoc?: Doc;
+
+ updateTooltip = () => {
+ this._outerdiv && this._tooltip && !this._outerdiv.contains(this._tooltip) && this._outerdiv.appendChild(this._tooltip);
+ }
+
+ public SetColor(color: string) {
+ return this._textBox && this._textBox.setFontColor(color);
+ }
+
constructor(props: MainOverlayTextBoxProps) {
super(props);
@@ -29,21 +47,35 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
MainOverlayTextBox.Instance = this;
reaction(() => FormattedTextBox.InputBoxOverlay,
(box?: FormattedTextBox) => {
- if (box) this.setTextDoc(box.props.fieldKey, box.CurrentDiv, box.props.ScreenToLocalTransform);
- else this.setTextDoc();
+ this._textBox = box;
+ if (box) {
+ this.TextDoc = box.props.Document;
+ this.TextDataDoc = box.props.DataDoc;
+ let sxf = Utils.GetScreenTransform(box ? box.CurrentDiv : undefined);
+ let xf = () => { box.props.ScreenToLocalTransform(); return new Transform(-sxf.translateX, -sxf.translateY, 1 / sxf.scale); };
+ this.setTextDoc(box.props.fieldKey, box.CurrentDiv, xf, BoolCast(box.props.Document.autoHeight, false) || box.props.height === "min-content");
+ }
+ else {
+ this.TextDoc = undefined;
+ this.TextDataDoc = undefined;
+ this.setTextDoc();
+ }
});
}
@action
- private setTextDoc(textFieldKey?: string, div?: HTMLDivElement, tx?: () => Transform) {
+ private setTextDoc(textFieldKey?: string, div?: HTMLDivElement, tx?: () => Transform, autoHeight?: boolean) {
if (this._textTargetDiv) {
this._textTargetDiv.style.color = this._textColor;
}
- this._textFieldKey = textFieldKey!;
- this._textXf = tx ? tx : () => Transform.Identity();
+ this._textAutoHeight = autoHeight;
+ this.TextFieldKey = textFieldKey!;
+ let txf = tx ? tx : () => Transform.Identity();
+ this._textXf = txf;
this._textTargetDiv = div;
this._textHideOnLeave = FormattedTextBox.InputBoxOverlay && FormattedTextBox.InputBoxOverlay.props.hideOnLeave;
if (div) {
+ this._textBottom = div.parentElement && div.parentElement.style.bottom ? true : false;
this._textColor = (getComputedStyle(div) as any).color;
div.style.color = "transparent";
}
@@ -64,10 +96,10 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
}
@action
textBoxMove = (e: PointerEvent) => {
- if (e.movementX > 1 || e.movementY > 1) {
+ if ((e.movementX > 1 || e.movementY > 1) && FormattedTextBox.InputBoxOverlay) {
document.removeEventListener("pointermove", this.textBoxMove);
document.removeEventListener('pointerup', this.textBoxUp);
- let dragData = new DragManager.DocumentDragData(FormattedTextBox.InputBoxOverlay ? [FormattedTextBox.InputBoxOverlay.props.Document] : []);
+ let dragData = new DragManager.DocumentDragData([FormattedTextBox.InputBoxOverlay.props.Document], [FormattedTextBox.InputBoxOverlay.props.DataDoc]);
const [left, top] = this._textXf().inverse().transformPoint(0, 0);
dragData.xOffset = e.clientX - left;
dragData.yOffset = e.clientY - top;
@@ -84,21 +116,31 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
document.removeEventListener('pointerup', this.textBoxUp);
}
- addDocTab = (doc: Doc, location: string) => {
+ addDocTab = (doc: Doc, dataDoc: Doc | undefined, location: string) => {
if (true) { // location === "onRight") { need to figure out stack to add "inTab"
- CollectionDockingView.AddRightSplit(doc);
+ CollectionDockingView.Instance.AddRightSplit(doc, dataDoc);
}
}
render() {
+ this.TextDoc; this.TextDataDoc;
if (FormattedTextBox.InputBoxOverlay && this._textTargetDiv) {
let textRect = this._textTargetDiv.getBoundingClientRect();
let s = this._textXf().Scale;
- return <div className="mainOverlayTextBox-textInput" style={{ transform: `translate(${textRect.left}px, ${textRect.top}px) scale(${1 / s},${1 / s})`, width: "auto", height: "auto" }} >
- <div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll}
- style={{ width: `${textRect.width * s}px`, height: `${textRect.height * s}px` }}>
- <FormattedTextBox fieldKey={this._textFieldKey} hideOnLeave={this._textHideOnLeave} isOverlay={true} Document={FormattedTextBox.InputBoxOverlay.props.Document} isSelected={returnTrue} select={emptyFunction} isTopMost={true}
- selectOnLoad={true} ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue}
- ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} addDocTab={this.addDocTab} />
+ let location = this._textBottom ? textRect.bottom : textRect.top;
+ let hgt = this._textAutoHeight || this._textBottom ? "auto" : this._textTargetDiv.clientHeight;
+ return <div ref={this._setouterdiv} className="mainOverlayTextBox-unscaled_div" style={{ transform: `translate(${textRect.left}px, ${location}px)` }} >
+ <div className="mainOverlayTextBox-textInput" style={{ transform: `scale(${1 / s},${1 / s})`, width: "auto", height: "0px" }} >
+ <div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll}
+ style={{ width: `${textRect.width * s}px`, height: "0px" }}>
+ <div style={{ height: hgt, width: "100%", position: "absolute", bottom: this._textBottom ? "0px" : undefined }}>
+ <FormattedTextBox color={`${this._textColor}`} fieldKey={this.TextFieldKey} fieldExt="" hideOnLeave={this._textHideOnLeave} isOverlay={true}
+ Document={FormattedTextBox.InputBoxOverlay.props.Document}
+ DataDoc={FormattedTextBox.InputBoxOverlay.props.DataDoc}
+ isSelected={returnTrue} select={emptyFunction} renderDepth={0} selectOnLoad={true}
+ ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue}
+ ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} addDocTab={this.addDocTab} outer_div={(tooltip: HTMLElement) => { this._tooltip = tooltip; this.updateTooltip(); }} />
+ </div>
+ </div>
</div>
</ div>;
}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 734961b56..1f628228a 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,73 +1,91 @@
import { IconName, library } from '@fortawesome/fontawesome-svg-core';
-import { faClone, faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faTree, faUndoAlt, faBell } from '@fortawesome/free-solid-svg-icons';
+import { faArrowDown, faArrowUp, faClone, faCheck, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faThumbtack, faTree, faUndoAlt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, configure, observable, runInAction, trace } from 'mobx';
+import { action, computed, configure, observable, runInAction, reaction, trace } from 'mobx';
import { observer } from 'mobx-react';
import "normalize.css";
import * as React from 'react';
+import { SketchPicker } from 'react-color';
import Measure from 'react-measure';
import * as request from 'request';
+import { Doc, DocListCast, Opt, HeightSym } from '../../new_fields/Doc';
+import { Id } from '../../new_fields/FieldSymbols';
+import { InkTool } from '../../new_fields/InkField';
+import { List } from '../../new_fields/List';
+import { listSpec } from '../../new_fields/Schema';
+import { Cast, FieldValue, NumCast } from '../../new_fields/Types';
import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
import { RouteStore } from '../../server/RouteStore';
-import { emptyFunction, returnTrue, Utils, returnOne, returnZero } from '../../Utils';
+import { emptyFunction, returnOne, returnTrue } from '../../Utils';
+import { DocServer } from '../DocServer';
import { Docs } from '../documents/Documents';
-import { SetupDrag, DragManager } from '../util/DragManager';
+import { SetupDrag } from '../util/DragManager';
+import { HistoryUtil } from '../util/History';
import { Transform } from '../util/Transform';
import { UndoManager } from '../util/UndoManager';
-import { PresentationView } from './PresentationView';
+import { CollectionBaseView } from './collections/CollectionBaseView';
import { CollectionDockingView } from './collections/CollectionDockingView';
import { ContextMenu } from './ContextMenu';
import { DocumentDecorations } from './DocumentDecorations';
+import KeyManager from './GlobalKeyHandler';
import { InkingControl } from './InkingControl';
import "./Main.scss";
import { MainOverlayTextBox } from './MainOverlayTextBox';
import { DocumentView } from './nodes/DocumentView';
+import { OverlayView } from './OverlayView';
+import PDFMenu from './pdf/PDFMenu';
+import { PresentationView } from './presentationview/PresentationView';
import { PreviewCursor } from './PreviewCursor';
-import { SearchBox } from './SearchBox';
-import { SelectionManager } from '../util/SelectionManager';
-import { FieldResult, Field, Doc, Opt, DocListCast } from '../../new_fields/Doc';
-import { Cast, FieldValue, StrCast, PromiseValue } from '../../new_fields/Types';
-import { DocServer } from '../DocServer';
-import { listSpec } from '../../new_fields/Schema';
-import { Id } from '../../new_fields/FieldSymbols';
-import { HistoryUtil } from '../util/History';
-import { CollectionBaseView } from './collections/CollectionBaseView';
-import { timingSafeEqual } from 'crypto';
-import * as _ from "lodash";
+import { FilterBox } from './search/FilterBox';
+import { CollectionTreeView } from './collections/CollectionTreeView';
@observer
export class MainView extends React.Component {
public static Instance: MainView;
+ @observable addMenuToggle = React.createRef<HTMLInputElement>();
@observable private _workspacesShown: boolean = false;
@observable public pwidth: number = 0;
@observable public pheight: number = 0;
@computed private get mainContainer(): Opt<Doc> {
return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc));
}
- @computed private get mainFreeform(): Opt<Doc> {
+ @computed get mainFreeform(): Opt<Doc> {
let docs = DocListCast(this.mainContainer!.data);
return (docs && docs.length > 1) ? docs[1] : undefined;
}
- private globalDisplayFlags = observable({
- jumpToVisible: false
- });
-
+ public isPointerDown = false;
private set mainContainer(doc: Opt<Doc>) {
if (doc) {
if (!("presentationView" in doc)) {
- doc.presentationView = Docs.Create.TreeDocument([], { title: "Presentation" });
+ doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })]);
}
CurrentUserUtils.UserDocument.activeWorkspace = doc;
}
}
componentWillMount() {
- document.removeEventListener("keydown", this.globalKeyHandler);
- document.addEventListener("keydown", this.globalKeyHandler);
+ window.removeEventListener("keydown", KeyManager.Instance.handle);
+ window.addEventListener("keydown", KeyManager.Instance.handle);
+
+ reaction(() => {
+ let workspaces = CurrentUserUtils.UserDocument.workspaces;
+ let recent = CurrentUserUtils.UserDocument.recentlyClosed;
+ if (!(recent instanceof Doc)) return 0;
+ if (!(workspaces instanceof Doc)) return 0;
+ let workspacesDoc = workspaces;
+ let recentDoc = recent;
+ let libraryHeight = this.getPHeight() - workspacesDoc[HeightSym]() - recentDoc[HeightSym]() - 20 + CurrentUserUtils.UserDocument[HeightSym]() * 0.00001;
+ return libraryHeight;
+ }, (libraryHeight: number) => {
+ if (libraryHeight && Math.abs(CurrentUserUtils.UserDocument[HeightSym]() - libraryHeight) > 5) {
+ CurrentUserUtils.UserDocument.height = libraryHeight;
+ }
+ (Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc) as Doc).allowClear = true;
+ }, { fireImmediately: true });
}
componentWillUnMount() {
- document.removeEventListener("keydown", this.globalKeyHandler);
+ window.removeEventListener("keydown", KeyManager.Instance.handle);
}
constructor(props: Readonly<{}>) {
@@ -95,6 +113,7 @@ export class MainView extends React.Component {
}
library.add(faFont);
+ library.add(faExclamation);
library.add(faImage);
library.add(faFilePdf);
library.add(faObjectGroup);
@@ -106,7 +125,13 @@ export class MainView extends React.Component {
library.add(faFilm);
library.add(faMusic);
library.add(faTree);
- library.add(faClone)
+ library.add(faClone);
+ library.add(faCut);
+ library.add(faCommentAlt);
+ library.add(faThumbtack);
+ library.add(faCheck);
+ library.add(faArrowDown);
+ library.add(faArrowUp);
this.initEventListeners();
this.initAuthenticationRouters();
}
@@ -115,18 +140,12 @@ export class MainView extends React.Component {
// window.addEventListener("pointermove", (e) => this.reportLocation(e))
window.addEventListener("drop", (e) => e.preventDefault(), false); // drop event handler
window.addEventListener("dragover", (e) => e.preventDefault(), false); // drag event handler
- window.addEventListener("keydown", (e) => {
- if (e.key === "Escape") {
- DragManager.AbortDrag();
- SelectionManager.DeselectAll();
- }
- }, false); // drag event handler
// click interactions for the context menu
- document.addEventListener("pointerdown", action(function (e: PointerEvent) {
-
+ document.addEventListener("pointerdown", action((e: PointerEvent) => {
+ this.isPointerDown = true;
const targets = document.elementsFromPoint(e.x, e.y);
if (targets && targets.length && targets[0].className.toString().indexOf("contextMenu") === -1) {
- ContextMenu.Instance.clearItems();
+ ContextMenu.Instance.closeMenu();
}
}), true);
}
@@ -141,22 +160,27 @@ export class MainView extends React.Component {
this.createNewWorkspace();
}
} else {
- DocServer.getRefField(CurrentUserUtils.MainDocId).then(field =>
+ DocServer.GetRefField(CurrentUserUtils.MainDocId).then(field =>
field instanceof Doc ? this.openWorkspace(field) :
this.createNewWorkspace(CurrentUserUtils.MainDocId));
}
}
+
@action
createNewWorkspace = async (id?: string) => {
- const list = Cast(CurrentUserUtils.UserDocument.data, listSpec(Doc));
+ let workspaces = Cast(CurrentUserUtils.UserDocument.workspaces, Doc);
+ if (!(workspaces instanceof Doc)) return;
+ const list = Cast((CurrentUserUtils.UserDocument.workspaces as Doc).data, listSpec(Doc));
if (list) {
let freeformDoc = Docs.Create.FreeformDocument([], { x: 0, y: 400, width: this.pwidth * .7, height: this.pheight, title: `WS collection ${list.length + 1}` });
- let configs = [
- { doc: CurrentUserUtils.UserDocument, initialWidth: 150 },
- { doc: freeformDoc, initialWidth: 600 }
- ]
- let mainDoc = Docs.Create.StandardCollectionDockingDocument(configs, { title: `Workspace ${list.length + 1}` }, id);
+ var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc, freeformDoc, 600)] }] };
+ let mainDoc = Docs.Create.DockDocument([CurrentUserUtils.UserDocument, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` }, id);
+ if (!CurrentUserUtils.UserDocument.linkManagerDoc) {
+ let linkManagerDoc = new Doc();
+ linkManagerDoc.allLinks = new List<Doc>([]);
+ CurrentUserUtils.UserDocument.linkManagerDoc = linkManagerDoc;
+ }
list.push(mainDoc);
// bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
setTimeout(() => {
@@ -167,8 +191,6 @@ export class MainView extends React.Component {
}
}
- @observable _notifsCol: Opt<Doc>;
-
@action
openWorkspace = async (doc: Doc, fromHistory = false) => {
CurrentUserUtils.MainDocId = doc[Id];
@@ -180,16 +202,16 @@ export class MainView extends React.Component {
if (col) {
const l = Cast(col.data, listSpec(Doc));
if (l) {
- runInAction(() => this._notifsCol = col);
+ runInAction(() => CollectionTreeView.NotifsCol = col);
}
}
}, 100);
}
- openNotifsCol = () => {
- if (this._notifsCol && CollectionDockingView.TopLevel) {
- CollectionDockingView.AddRightSplit(this._notifsCol);
- }
+ onDrop = (e: React.DragEvent<HTMLDivElement>) => {
+ e.preventDefault();
+ e.stopPropagation();
+ console.log("Drop");
}
@action
@@ -203,116 +225,202 @@ export class MainView extends React.Component {
getPHeight = () => {
return this.pheight;
}
- @computed
- get mainContent() {
+
+ @observable flyoutWidth: number = 250;
+ @computed get dockingContent() {
+ let flyoutWidth = this.flyoutWidth;
let mainCont = this.mainContainer;
- let content = !mainCont ? (null) :
- <DocumentView Document={mainCont}
- addDocument={undefined}
- addDocTab={emptyFunction}
- removeDocument={undefined}
- ScreenToLocalTransform={Transform.Identity}
- ContentScaling={returnOne}
- PanelWidth={this.getPWidth}
- PanelHeight={this.getPHeight}
- isTopMost={true}
- selectOnLoad={false}
- focus={emptyFunction}
- parentActive={returnTrue}
- whenActiveChanged={emptyFunction}
- bringToFront={emptyFunction}
- ContainingCollectionView={undefined} />;
- const pres = mainCont ? FieldValue(Cast(mainCont.presentationView, Doc)) : undefined;
+ let castRes = mainCont ? FieldValue(Cast(mainCont.presentationView, listSpec(Doc))) : undefined;
return <Measure offset onResize={this.onResize}>
{({ measureRef }) =>
- <div ref={measureRef} id="mainContent-div">
- {content}
- {pres ? <PresentationView Document={pres} key="presentation" /> : null}
+ <div ref={measureRef} id="mainContent-div" style={{ width: `calc(100% - ${flyoutWidth}px`, transform: `translate(${flyoutWidth}px, 0px)` }} onDrop={this.onDrop}>
+ {!mainCont ? (null) :
+ <DocumentView Document={mainCont}
+ DataDoc={undefined}
+ addDocument={undefined}
+ addDocTab={emptyFunction}
+ removeDocument={undefined}
+ ScreenToLocalTransform={Transform.Identity}
+ ContentScaling={returnOne}
+ PanelWidth={this.getPWidth}
+ PanelHeight={this.getPHeight}
+ renderDepth={0}
+ selectOnLoad={false}
+ focus={emptyFunction}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ ContainingCollectionView={undefined}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}
+ />}
+ {castRes ? <PresentationView Documents={castRes} key="presentation" /> : null}
</div>
}
</Measure>;
}
+ _downsize = 0;
+ onPointerDown = (e: React.PointerEvent) => {
+ this._downsize = e.clientX;
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointerup", this.onPointerUp);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ @action
+ onPointerMove = (e: PointerEvent) => {
+ this.flyoutWidth = Math.max(e.clientX, 0);
+ }
+ @action
+ onPointerUp = (e: PointerEvent) => {
+ this.isPointerDown = false;
+ if (Math.abs(e.clientX - this._downsize) < 4) {
+ if (this.flyoutWidth < 5) this.flyoutWidth = 250;
+ else this.flyoutWidth = 0;
+ }
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ }
+ flyoutWidthFunc = () => this.flyoutWidth;
+ addDocTabFunc = (doc: Doc) => {
+ if (doc.dockingConfig) {
+ this.openWorkspace(doc);
+ } else {
+ CollectionDockingView.Instance.AddRightSplit(doc, undefined);
+ }
+ }
+ @computed
+ get flyout() {
+ let sidebar = CurrentUserUtils.UserDocument.sidebar;
+ if (!(sidebar instanceof Doc)) return (null);
+ let sidebarDoc = sidebar;
+ return <DocumentView
+ Document={sidebarDoc}
+ DataDoc={undefined}
+ addDocument={undefined}
+ addDocTab={this.addDocTabFunc}
+ removeDocument={undefined}
+ ScreenToLocalTransform={Transform.Identity}
+ ContentScaling={returnOne}
+ PanelWidth={this.flyoutWidthFunc}
+ PanelHeight={this.getPHeight}
+ renderDepth={0}
+ selectOnLoad={false}
+ focus={emptyFunction}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ ContainingCollectionView={undefined}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}>
+ </DocumentView>;
+ }
+ @computed
+ get mainContent() {
+ return <div>
+ <div className="mainView-libraryHandle"
+ style={{ left: `${this.flyoutWidth - 10}px` }}
+ onPointerDown={this.onPointerDown}>
+ <span title="library View Dragger" style={{ width: "100%", height: "100%", position: "absolute" }} />
+ </div>
+ <div className="mainView-libraryFlyout" style={{ width: `${this.flyoutWidth}px` }}>
+ {this.flyout}
+ </div>
+ {this.dockingContent}
+ </div>;
+ }
+
+ selected = (tool: InkTool) => {
+ if (!InkingControl.Instance || InkingControl.Instance.selectedTool === InkTool.None) return { display: "none" };
+ if (InkingControl.Instance.selectedTool === tool) {
+ return { color: "#61aaa3", fontSize: "50%" };
+ }
+ return { fontSize: "50%" };
+ }
+
+ onColorClick = (e: React.MouseEvent) => {
+ let target = (e.nativeEvent as any).path[0];
+ let parent = (e.nativeEvent as any).path[1];
+ if (target.localName === "input" || parent.localName === "span") {
+ e.stopPropagation();
+ }
+ }
+
+
+ @observable private _colorPickerDisplay = false;
/* for the expandable add nodes menu. Not included with the miscbuttons because once it expands it expands the whole div with it, making canvas interactions limited. */
nodesMenu() {
let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
- let pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c27211_sample_explain.pdf";
- let weburl = "https://cs.brown.edu/courses/cs166/";
- let audiourl = "http://techslides.com/demos/samples/sample.mp3";
- let videourl = "http://techslides.com/demos/sample-videos/small.mp4";
- let addTextNode = action(() => Docs.Create.TextDocument({ borderRounding: -1, width: 200, height: 200, title: "a text note" }));
- let addColNode = action(() => Docs.Create.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));
let addDockingNode = action(() => Docs.Create.StandardCollectionDockingDocument([{ doc: addColNode(), initialWidth: 200 }], { width: 200, height: 200, title: "a nested docking freeform collection" }));
let addSchemaNode = action(() => Docs.Create.SchemaDocument(["title"], [], { width: 200, height: 200, title: "a schema collection" }));
- let addTreeNode = action(() => CurrentUserUtils.UserDocument);
//let addTreeNode = action(() => Docs.TreeDocument([CurrentUserUtils.UserDocument], { width: 250, height: 400, title: "Library:" + CurrentUserUtils.email, dropAction: "alias" }));
// let addTreeNode = action(() => Docs.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", dropAction: "copy" }));
- let addVideoNode = action(() => Docs.Create.VideoDocument(videourl, { width: 200, title: "video node" }));
- let addPDFNode = action(() => Docs.Create.PdfDocument(pdfurl, { width: 200, height: 200, title: "a pdf doc" }));
+ let addColNode = action(() => Docs.Create.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));
+ let addTreeNode = action(() => CurrentUserUtils.UserDocument);
let addImageNode = action(() => Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }));
- let addWebNode = action(() => Docs.Create.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" }));
- let addAudioNode = action(() => Docs.Create.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" }));
+ let addImportCollectionNode = action(() => Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 }));
let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [
- [React.createRef<HTMLDivElement>(), "font", "Add Textbox", addTextNode],
[React.createRef<HTMLDivElement>(), "image", "Add Image", addImageNode],
- [React.createRef<HTMLDivElement>(), "file-pdf", "Add PDF", addPDFNode],
- [React.createRef<HTMLDivElement>(), "film", "Add Video", addVideoNode],
- [React.createRef<HTMLDivElement>(), "music", "Add Audio", addAudioNode],
- [React.createRef<HTMLDivElement>(), "globe-asia", "Add Web Clipping", addWebNode],
[React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode],
[React.createRef<HTMLDivElement>(), "tree", "Add Tree", addTreeNode],
[React.createRef<HTMLDivElement>(), "table", "Add Schema", addSchemaNode],
- [React.createRef<HTMLDivElement>(), "clone", "Add Docking Frame", addDockingNode]
+ [React.createRef<HTMLDivElement>(), "clone", "Add Docking Frame", addDockingNode],
+ [React.createRef<HTMLDivElement>(), "arrow-up", "Import Directory", addImportCollectionNode],
];
- return < div id="add-nodes-menu" >
- <input type="checkbox" id="add-menu-toggle" />
+ return < div id="add-nodes-menu" style={{ left: this.flyoutWidth + 5 }} >
+ <input type="checkbox" id="add-menu-toggle" ref={this.addMenuToggle} />
<label htmlFor="add-menu-toggle" title="Add Node"><p>+</p></label>
<div id="add-options-content">
<ul id="add-options-list">
+ <li key="search"><button className="add-button round-button" title="Search" onClick={this.toggleSearch}><FontAwesomeIcon icon="search" size="sm" /></button></li>
+ <li key="undo"><button className="add-button round-button" title="Undo" onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button></li>
+ <li key="redo"><button className="add-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button></li>
+ <li key="color"><button className="add-button round-button" title="Select Color" onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} >
+ <div className="toolbar-color-picker" onClick={this.onColorClick} style={this._colorPickerDisplay ? { color: "black", display: "block" } : { color: "black", display: "none" }}>
+ <SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} />
+ </div>
+ </div></button></li>
{btns.map(btn =>
<li key={btn[1]} ><div ref={btn[0]}>
<button className="round-button add-button" title={btn[2]} onPointerDown={SetupDrag(btn[0], btn[3])}>
<FontAwesomeIcon icon={btn[1]} size="sm" />
</button>
</div></li>)}
+ <li key="undoTest"><button className="add-button round-button" onClick={() => UndoManager.PrintBatches()}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>
+ <li key="ink" style={{ paddingRight: "6px" }}><button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /> </button></li>
+ <li key="pen"><button onClick={() => InkingControl.Instance.switchTool(InkTool.Pen)} style={this.selected(InkTool.Pen)}><FontAwesomeIcon icon="pen" size="lg" title="Pen" /></button></li>
+ <li key="marker"><button onClick={() => InkingControl.Instance.switchTool(InkTool.Highlighter)} style={this.selected(InkTool.Highlighter)}><FontAwesomeIcon icon="highlighter" size="lg" title="Pen" /></button></li>
+ <li key="eraser"><button onClick={() => InkingControl.Instance.switchTool(InkTool.Eraser)} style={this.selected(InkTool.Eraser)}><FontAwesomeIcon icon="eraser" size="lg" title="Pen" /></button></li>
+ <li key="inkControls"><InkingControl /></li>
</ul>
</div>
</div >;
}
+
+
+ @action
+ toggleColorPicker = (close = false) => {
+ this._colorPickerDisplay = close ? false : !this._colorPickerDisplay;
+ }
+
/* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */
@computed
get miscButtons() {
- const length = this._notifsCol ? DocListCast(this._notifsCol.data).length : 0;
- const notifsRef = React.createRef<HTMLDivElement>();
- const dragNotifs = action(() => this._notifsCol!);
let logoutRef = React.createRef<HTMLDivElement>();
return [
- <button className="clear-db-button" key="clear-db" onClick={e => e.shiftKey && e.altKey && DocServer.Util.deleteDatabase()}>Clear Database</button>,
- <div id="toolbar" key="toolbar">
- <div ref={notifsRef}>
- <button className="toolbar-button round-button" title="Notifs"
- onClick={this.openNotifsCol} onPointerDown={this._notifsCol ? SetupDrag(notifsRef, dragNotifs) : emptyFunction}>
- <FontAwesomeIcon icon={faBell} size="sm" />
- </button>
- <div className="main-notifs-badge" style={length > 0 ? { "display": "initial" } : { "display": "none" }}>
- {length}
- </div>
- </div>
- <button className="toolbar-button round-button" title="Search" onClick={this.toggleSearch}><FontAwesomeIcon icon="search" size="sm" /></button>
- <button className="toolbar-button round-button" title="Undo" onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button>
- <button className="toolbar-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button>
- <button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /></button>
- </div >,
- this.isSearchVisible ? <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <SearchBox /> </div> : null,
+ this.isSearchVisible ? <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <FilterBox /> </div> : null,
<div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}>
- <button onClick={() => request.get(DocServer.Util.prepend(RouteStore.logout), emptyFunction)}>Log Out</button></div>
+ <button onClick={() => window.location.assign(DocServer.prepend(RouteStore.logout))}>Log Out</button></div>
];
}
@@ -323,36 +431,6 @@ export class MainView extends React.Component {
this.isSearchVisible = !this.isSearchVisible;
}
- @action
- globalKeyHandler = (e: KeyboardEvent) => {
- if (e.key === "Control" || !e.ctrlKey) return;
-
- e.preventDefault();
- e.stopPropagation();
-
- switch (e.key) {
- case "ArrowRight":
- if (this.mainFreeform) {
- CollectionDockingView.AddRightSplit(this.mainFreeform!);
- }
- break;
- case "ArrowLeft":
- if (this.mainFreeform) {
- CollectionDockingView.CloseRightSplit(this.mainFreeform!);
- }
- break;
- case "o":
- this.globalDisplayFlags.jumpToVisible = true;
- break;
- case "escape":
- _.mapValues(this.globalDisplayFlags, () => false)
- break;
- }
- }
-
- renderJumpTo = () => {
- return <div>JUMP TO</div>;
- }
render() {
return (
@@ -363,10 +441,10 @@ export class MainView extends React.Component {
<ContextMenu />
{this.nodesMenu()}
{this.miscButtons}
- <InkingControl />
+ <PDFMenu />
<MainOverlayTextBox />
- {this.globalDisplayFlags.jumpToVisible ? this.renderJumpTo() : (null)}
- </div>
+ <OverlayView />
+ </div >
);
}
}
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
new file mode 100644
index 000000000..f8fc94274
--- /dev/null
+++ b/src/client/views/OverlayView.tsx
@@ -0,0 +1,47 @@
+import * as React from "react";
+import { observer } from "mobx-react";
+import { observable, action } from "mobx";
+import { Utils } from "../../Utils";
+
+export type OverlayDisposer = () => void;
+
+export type OverlayElementOptions = {
+ x: number;
+ y: number;
+ width?: number;
+ height?: number;
+};
+
+@observer
+export class OverlayView extends React.Component {
+ public static Instance: OverlayView;
+ @observable.shallow
+ private _elements: { ele: JSX.Element, id: string, options: OverlayElementOptions }[] = [];
+
+ constructor(props: any) {
+ super(props);
+ if (!OverlayView.Instance) {
+ OverlayView.Instance = this;
+ }
+ }
+
+ @action
+ addElement(ele: JSX.Element, options: OverlayElementOptions): OverlayDisposer {
+ const eleWithPosition = { ele, options, id: Utils.GenerateGuid() };
+ this._elements.push(eleWithPosition);
+ return action(() => {
+ const index = this._elements.indexOf(eleWithPosition);
+ if (index !== -1) this._elements.splice(index, 1);
+ });
+ }
+
+ render() {
+ return (
+ <div>
+ {this._elements.map(({ ele, options: { x, y, width, height }, id }) => (
+ <div key={id} style={{ position: "absolute", transform: `translate(${x}px, ${y}px)`, width, height }}>{ele}</div>
+ ))}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/PresentationView.tsx b/src/client/views/PresentationView.tsx
deleted file mode 100644
index 9d5798ff1..000000000
--- a/src/client/views/PresentationView.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-import { observer } from "mobx-react";
-import React = require("react");
-import { observable, action, runInAction, reaction } from "mobx";
-import "./PresentationView.scss";
-import { DocumentManager } from "../util/DocumentManager";
-import { Utils } from "../../Utils";
-import { Doc, DocListCast, DocListCastAsync } from "../../new_fields/Doc";
-import { listSpec } from "../../new_fields/Schema";
-import { Cast, NumCast, FieldValue, PromiseValue, StrCast, BoolCast } from "../../new_fields/Types";
-import { Id } from "../../new_fields/FieldSymbols";
-import { List } from "../../new_fields/List";
-import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
-
-export interface PresViewProps {
- Document: Doc;
-}
-
-interface PresListProps extends PresViewProps {
- deleteDocument(index: number): void;
- gotoDocument(index: number): void;
-}
-
-
-@observer
-/**
- * Component that takes in a document prop and a boolean whether it's collapsed or not.
- */
-class PresentationViewList extends React.Component<PresListProps> {
-
-
- /**
- * Renders a single child document. It will just append a list element.
- * @param document The document to render.
- */
- renderChild = (document: Doc, index: number) => {
- let title = document.title;
-
- //to get currently selected presentation doc
- let selected = NumCast(this.props.Document.selectedDoc, 0);
-
- let className = "presentationView-item";
- if (selected === index) {
- //this doc is selected
- className += " presentationView-selected";
- }
- let onEnter = (e: React.PointerEvent) => { document.libraryBrush = true; }
- let onLeave = (e: React.PointerEvent) => { document.libraryBrush = false; }
- return (
- <div className={className} key={document[Id] + index}
- onPointerEnter={onEnter} onPointerLeave={onLeave}
- style={{
- outlineColor: "maroon",
- outlineStyle: "dashed",
- outlineWidth: BoolCast(document.libraryBrush, false) || BoolCast(document.protoBrush, false) ? `1px` : "0px",
- }}
- onClick={e => { this.props.gotoDocument(index); e.stopPropagation(); }}>
- <strong className="presentationView-name">
- {`${index + 1}. ${title}`}
- </strong>
- <button className="presentation-icon" onClick={e => { this.props.deleteDocument(index); e.stopPropagation(); }}>X</button>
- </div>
- );
-
- }
-
- render() {
- const children = DocListCast(this.props.Document.data);
-
- return (
- <div className="presentationView-listCont">
- {children.map(this.renderChild)}
- </div>
- );
- }
-}
-
-
-@observer
-export class PresentationView extends React.Component<PresViewProps> {
- public static Instance: PresentationView;
-
- //observable means render is re-called every time variable is changed
- @observable
- collapsed: boolean = false;
- closePresentation = action(() => this.props.Document.width = 0);
- next = () => {
- const current = NumCast(this.props.Document.selectedDoc);
- this.gotoDocument(current + 1);
-
- }
- back = () => {
- const current = NumCast(this.props.Document.selectedDoc);
- this.gotoDocument(current - 1);
- }
-
- @action
- public RemoveDoc = (index: number) => {
- const value = FieldValue(Cast(this.props.Document.data, listSpec(Doc)));
- if (value) {
- value.splice(index, 1);
- }
- }
-
- public gotoDocument = async (index: number) => {
- const list = FieldValue(Cast(this.props.Document.data, listSpec(Doc)));
- if (!list) {
- return;
- }
- if (index < 0 || index >= list.length) {
- return;
- }
-
- this.props.Document.selectedDoc = index;
- const doc = await list[index];
- DocumentManager.Instance.jumpToDocument(doc);
- }
-
- //initilize class variables
- constructor(props: PresViewProps) {
- super(props);
- PresentationView.Instance = this;
- }
-
- /**
- * Adds a document to the presentation view
- **/
- @action
- public PinDoc(doc: Doc) {
- //add this new doc to props.Document
- const data = Cast(this.props.Document.data, listSpec(Doc));
- if (data) {
- data.push(doc);
- } else {
- this.props.Document.data = new List([doc]);
- }
-
- this.props.Document.width = 300;
- }
-
- render() {
- let titleStr = StrCast(this.props.Document.title);
- let width = NumCast(this.props.Document.width);
-
- //TODO: next and back should be icons
- return (
- <div className="presentationView-cont" style={{ width: width, overflow: "hidden" }}>
- <div className="presentationView-heading">
- <div className="presentationView-title">{titleStr}</div>
- <button className='presentation-icon' onClick={this.closePresentation}>X</button>
- </div>
- <div className="presentation-buttons">
- <button className="presentation-button" onClick={this.back}>back</button>
- <button className="presentation-button" onClick={this.next}>next</button>
- </div>
- <PresentationViewList Document={this.props.Document} deleteDocument={this.RemoveDoc} gotoDocument={this.gotoDocument} />
- </div>
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/ScriptBox.scss b/src/client/views/ScriptBox.scss
new file mode 100644
index 000000000..28326624a
--- /dev/null
+++ b/src/client/views/ScriptBox.scss
@@ -0,0 +1,17 @@
+.scriptBox-outerDiv {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.scriptBox-toolbar {
+ width: 100%;
+}
+
+.scriptBox-textArea {
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+ resize: none;
+} \ No newline at end of file
diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx
new file mode 100644
index 000000000..fa236c2da
--- /dev/null
+++ b/src/client/views/ScriptBox.tsx
@@ -0,0 +1,44 @@
+import * as React from "react";
+import { observer } from "mobx-react";
+import { observable, action } from "mobx";
+
+import "./ScriptBox.scss";
+
+export interface ScriptBoxProps {
+ onSave: (text: string, onError: (error: string) => void) => void;
+ onCancel?: () => void;
+ initialText?: string;
+}
+
+@observer
+export class ScriptBox extends React.Component<ScriptBoxProps> {
+ @observable
+ private _scriptText: string;
+
+ constructor(props: ScriptBoxProps) {
+ super(props);
+ this._scriptText = props.initialText || "";
+ }
+
+ @action
+ onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
+ this._scriptText = e.target.value;
+ }
+
+ @action
+ onError = (error: string) => {
+ console.log(error);
+ }
+
+ render() {
+ return (
+ <div className="scriptBox-outerDiv">
+ <div className="scriptBox-toolbar">
+ <button onClick={e => { this.props.onSave(this._scriptText, this.onError); e.stopPropagation(); }}>Save</button>
+ <button onClick={e => { this.props.onCancel && this.props.onCancel(); e.stopPropagation(); }}>Cancel</button>
+ </div>
+ <textarea className="scriptBox-textarea" onChange={this.onChange} value={this._scriptText}></textarea>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/SearchBox.scss b/src/client/views/SearchBox.scss
deleted file mode 100644
index b38e6091d..000000000
--- a/src/client/views/SearchBox.scss
+++ /dev/null
@@ -1,102 +0,0 @@
-@import "globalCssVariables";
-
-.searchBox-bar {
- height: 32px;
- display: flex;
- justify-content: flex-end;
- align-items: center;
- padding-left: 2px;
- padding-right: 2px;
-
- .searchBox-input {
- width: 130px;
- -webkit-transition: width 0.4s;
- transition: width 0.4s;
- align-self: stretch;
- }
-
- .searchBox-input:focus {
- width: 500px;
- outline: 3px solid lightblue;
- }
-
- .searchBox-barChild {
- flex: 0 1 auto;
- margin-left: 2px;
- margin-right: 2px;
- }
-
- .searchBox-filter {
- align-self: stretch;
- }
-
- .searchBox-submit {
- color: $dark-color;
- }
-
- .searchBox-submit:hover {
- color: $main-accent;
- transform: scale(1.05);
- cursor: pointer;
- }
-}
-
-.searchBox-results {
- margin-left: 27px; //Is there a better way to do this?
-}
-
-.filter-form {
- background: $dark-color;
- height: 400px;
- width: 400px;
- position: relative;
- right: 1px;
- color: $light-color;
- padding: 10px;
- flex-direction: column;
-}
-
-#header {
- text-transform: uppercase;
- letter-spacing: 2px;
- font-size: 100%;
- height: 40px;
-}
-
-#option {
- height: 20px;
-}
-
-.searchBox-results {
- top: 300px;
- display: flex;
- flex-direction: column;
-
- .search-item {
- width: 500px;
- height: 50px;
- background: $light-color-secondary;
- display: flex;
- justify-content: space-between;
- align-items: center;
- transition: all 0.1s;
- border-width: 0.11px;
- border-style: none;
- border-color: $intermediate-color;
- border-bottom-style: solid;
- padding: 10px;
- white-space: nowrap;
- font-size: 13px;
- }
-
- .search-item:hover {
- transition: all 0.1s;
- background: $lighter-alt-accent;
- }
-
- .search-title {
- text-transform: uppercase;
- text-align: left;
- width: 8vw;
- }
-} \ No newline at end of file
diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx
index 973715876..6995e3c7d 100644
--- a/src/client/views/SearchBox.tsx
+++ b/src/client/views/SearchBox.tsx
@@ -11,7 +11,7 @@ import { library } from '@fortawesome/fontawesome-svg-core';
// import * as express from 'express';
import { Search } from '../../server/Search';
import * as rp from 'request-promise';
-import { SearchItem } from './SearchItem';
+import { SearchItem } from './search/SearchItem';
import { isString } from 'util';
import { constant } from 'async';
import { DocServer } from '../DocServer';
@@ -56,13 +56,13 @@ export class SearchBox extends React.Component {
@action
getResults = async (query: string) => {
- let response = await rp.get(DocServer.Util.prepend('/search'), {
+ let response = await rp.get(DocServer.prepend('/search'), {
qs: {
query
}
});
let res: string[] = JSON.parse(response);
- const fields = await DocServer.getRefFields(res);
+ const fields = await DocServer.GetRefFields(res);
const docs: Doc[] = [];
for (const id of res) {
const field = fields[id];
@@ -74,7 +74,7 @@ export class SearchBox extends React.Component {
}
public static async convertDataUri(imageUri: string, returnedFilename: string) {
try {
- let posting = DocServer.Util.prepend(RouteStore.dataUriToImage);
+ let posting = DocServer.prepend(RouteStore.dataUriToImage);
const returnedUri = await rp.post(posting, {
body: {
uri: imageUri,
diff --git a/src/client/views/SearchItem.tsx b/src/client/views/SearchItem.tsx
deleted file mode 100644
index 01c7316d6..000000000
--- a/src/client/views/SearchItem.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import React = require("react");
-import { Doc } from "../../new_fields/Doc";
-import { DocumentManager } from "../util/DocumentManager";
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Cast } from "../../new_fields/Types";
-import { FieldView, FieldViewProps } from './nodes/FieldView';
-import { computed } from "mobx";
-import { IconField } from "../../new_fields/IconField";
-import { SetupDrag } from "../util/DragManager";
-
-
-export interface SearchProps {
- doc: Doc;
-}
-
-library.add(faCaretUp);
-library.add(faObjectGroup);
-library.add(faStickyNote);
-library.add(faFilePdf);
-library.add(faFilm);
-
-export class SearchItem extends React.Component<SearchProps> {
-
- onClick = () => {
- DocumentManager.Instance.jumpToDocument(this.props.doc);
- }
-
- //needs help
- // @computed get layout(): string { const field = Cast(this.props.doc[fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; }
-
-
- public static DocumentIcon(layout: string) {
- let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf :
- layout.indexOf("ImageBox") !== -1 ? faImage :
- layout.indexOf("Formatted") !== -1 ? faStickyNote :
- layout.indexOf("Video") !== -1 ? faFilm :
- layout.indexOf("Collection") !== -1 ? faObjectGroup :
- faCaretUp;
- return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />;
- }
- onPointerEnter = (e: React.PointerEvent) => {
- this.props.doc.libraryBrush = true;
- Doc.SetOnPrototype(this.props.doc, "protoBrush", true);
- }
- onPointerLeave = (e: React.PointerEvent) => {
- this.props.doc.libraryBrush = false;
- Doc.SetOnPrototype(this.props.doc, "protoBrush", false);
- }
-
- collectionRef = React.createRef<HTMLDivElement>();
- startDocDrag = () => {
- let doc = this.props.doc;
- const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
- if (isProto) {
- return Doc.MakeDelegate(doc);
- } else {
- return Doc.MakeAlias(doc);
- }
- }
- render() {
- return (
- <div className="search-item" ref={this.collectionRef} id="result"
- onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
- onClick={this.onClick} onPointerDown={SetupDrag(this.collectionRef, this.startDocDrag)} >
- <div className="search-title" id="result" >title: {this.props.doc.title}</div>
- {/* <div className="search-type" id="result" >Type: {this.props.doc.layout}</div> */}
- {/* <div className="search-type" >{SearchItem.DocumentIcon(this.layout)}</div> */}
- </div>
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index e5b679e24..1b32f0ddd 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -1,12 +1,12 @@
-import { observable, computed, action, trace } from "mobx";
-import React = require("react");
+import { action, observable } from "mobx";
import { observer } from "mobx-react";
+import { Doc } from "../../new_fields/Doc";
+import { List } from "../../new_fields/List";
import './DocumentDecorations.scss';
-import { Template } from "./Templates";
import { DocumentView } from "./nodes/DocumentView";
-import { List } from "../../new_fields/List";
-import { Doc } from "../../new_fields/Doc";
-import { NumCast } from "../../new_fields/Types";
+import { Template } from "./Templates";
+import React = require("react");
+import { undoBatch } from "../util/UndoManager";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -40,13 +40,14 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
super(props);
}
+ @undoBatch
@action
toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => {
if (event.target.checked) {
if (template.Name === "Bullet") {
let topDocView = this.props.docs[0];
topDocView.addTemplate(template);
- topDocView.props.Document.subBulletDocs = new List<Doc>(this.props.docs.filter(v => v !== topDocView).map(v => v.props.Document.proto!));
+ topDocView.props.Document.subBulletDocs = new List<Doc>(this.props.docs.filter(v => v !== topDocView).map(v => v.props.Document));
} else {
this.props.docs.map(d => d.addTemplate(template));
}
@@ -63,6 +64,13 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
}
}
+ @undoBatch
+ @action
+ clearTemplates = (event: React.MouseEvent) => {
+ this.props.docs.map(d => d.clearTemplates());
+ Array.from(this.props.templates.keys()).map(t => this.props.templates.set(t, false));
+ }
+
@action
componentWillReceiveProps(nextProps: TemplateMenuProps) {
// this._templates = nextProps.templates;
@@ -80,9 +88,10 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
return (
<div className="templating-menu" >
- <div className="templating-button" onClick={() => this.toggleTemplateActivity()}>+</div>
+ <div title="Template Options" className="templating-button" onClick={() => this.toggleTemplateActivity()}>+</div>
<ul id="template-list" style={{ display: this._hidden ? "none" : "block" }}>
{templateMenu}
+ <button style={{ display: this._hidden ? "none" : "block" }} onClick={this.clearTemplates}>Clear</button>
</ul>
</div>
);
diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx
index e4a9258b7..236704fa2 100644
--- a/src/client/views/Templates.tsx
+++ b/src/client/views/Templates.tsx
@@ -41,54 +41,59 @@ export namespace Templates {
export const Caption = new Template("Caption", TemplatePosition.OutterBottom,
`<div>
- <div style="height:100%; width:100%;position:absolute;">{layout}</div>
+ <div style="height:100%; width:100%;">{layout}</div>
<div style="bottom: 0; font-size:14px; width:100%; position:absolute">
- <FormattedTextBox {...props} fieldKey={"caption"} hideOnLeave={"true"} />
+ <FormattedTextBox {...props} height="min-content" fieldKey={"caption"} hideOnLeave={"true"} />
</div>
</div>` );
- // export const Caption = new Template("Caption", TemplatePosition.OutterBottom,
- // `<div>
- // <div style="height:100%; width:100%;position:absolute;">{layout}</div>
- // <DetailedCaptionToggle {...props}/>
- // </div>` );
-
- export const TitleOverlay = new Template("TitleOverlay", TemplatePosition.InnerTop,
+ export const Title = new Template("Title", TemplatePosition.InnerTop,
`<div>
- <div style="height:100%; width:100%;position:absolute;">{layout}</div>
- <div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; ">
+ <div style="height:25px; width:100%; background-color: rgba(0, 0, 0, .4); color: white; z-index: 100">
<span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.Document.title}</span>
</div>
+ <div style="height:calc(100% - 25px);">
+ <div style="width:100%;overflow:auto">{layout}</div>
+ </div>
</div>` );
- export const Title = new Template("Title", TemplatePosition.InnerTop,
- `<div>
- <div style="height:calc(100% - 25px); margin-top: 25px; width:100%;position:absolute;">{layout}</div>
- <div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; ">
- <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.Document.title}</span>
+ export const Header = new Template("Header", TemplatePosition.InnerTop,
+ `<div style = "display:flex; flex-direction:column; height:100%;" >
+ <div style="width:100%; background-color: rgba(0, 0, 0, .4); color: white; ">
+ <FormattedTextBox {...props} height={"min-content"} color={"white"} fieldKey={"header"} />
</div>
- </div>` );
+ <div style="width:100%;height:100%;overflow:auto;">{layout}</div>
+ </div > ` );
export const Bullet = new Template("Bullet", TemplatePosition.InnerTop,
- `<div>
- <div style="height:100%; width:100%;position:absolute;">{layout}</div>
- <div id="isExpander" style="height:15px; width:15px; margin-left:-16px; pointer-events:all; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white;">
- <img id="isExpander" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAZlBMVEX///8AAABmZmb7+/tYWFhgYGBFRUVSUlL4+Pg/Pz9jY2N5eXmcnJyioqKBgYFzc3NtbW1LS0s3NzfW1taWlpaOjo6IiIgvLy9WVlampqZcXFw5OTlvb28mJiYxMTHe3t7l5eUjIyMY8kIZAAAD2UlEQVR4nO2d61biMBRGW1FBEVHxfp15/5ecOVa5lHxtArmck/Xtn1BotjtNoXQtm4YQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEFIrX6UHEA1gsmrneceRjHm7cj28attKFOf/TRyKIliH4vzbZE+xE2zbZYkxRWX5Y9JT/BW0X3G+NtlR3Ahar7jcMtlS3Ba0XXG+Y7JW3BW0XHHZM/lR7AvaVewL/ijuC1pV3Bf8VnQJ2lR0CYriq/Nxg4puwfa1aZ7dz9yUHnEgN26NZ3luWkPFd7fEtHsWVDwpO+YgTgYKCuYn6tAU7TBecaygcGpZEQie7m5luKJPQQFUvCwx5iAuvQoK4KShvSIoOHVtCz7dnOUecxBn7kG/urc2eCz6T9EOcxXDCgpAUetyAwoOCBqrGF5QMKR4mCA8L+pTBIJwkRl95eifJjPHTDYTFQ8vePyrs3BsBfXLzfFHkvKKMY4j1ctNnCmmuGKslfCQT0RZiPdFVmnFmOcy36sDWYn7DU9hxdifRkKuEGQh/pWW0K/QiUlxtUxVxTTXyhQtN6kuI6mpmO5qpxJFIBjl1yMVimmvV4PfrnIq3iYsKICTRj7F9L84gIq38fYwCCj4HnMfRY/FPL8ZFayYo6BQbLlJeZrYpVDFXAUFcMtKWkUgmOhmnwKKOQsK4NaxdIp5CwqZj8X8gv27jNecJ9nZuXtnie/SzjhRQcHkt6Fnq1imoAAUY1csVVDIUrFcQSGDIhC8jriLQZIrli0oXKdVLF1QSFqxfEEBVLyI8NYXCgoKySaqhinakajimxrBRBX1FBQSVNRyDP4SXVGbYHRFfYJN8xhTESwyj5HHHEjEihoLCqDiXfAb3aksKESqCAoqEIxUUW9BAS03E+93mOhcZDYcXVF3QeHBPcI3v4qo4EPiUQcBKr75vHaiv6AAKt6NV0SCqgoKqOKYovpFZgOo+DmsOHkyUlA4ZKKamaIdQPEJK5oqKKCKM7D9zFZBIayiuYICWm5cFWef7o3vs486CP8VdQIEVRcU7sFE7VecgSmqvKDgVxEJqi8ogIof2xVnH2YLCuMT1fAU7RirOPtrXHCsovmCwlDFCgoKWNH4IrMBTdQ/NUzRjiu3CeCq9HAPAVSspaDgX9FkQcG3ollB34qGBf0UTQv6KBoXHFc0LzimWIFg0ywGBBelBxcHXLGKggKqWElBwV2xIkF3xaoEXYqVCe4rVifYV3wpPZwULOouKLzUXVBY1F1QeKm7oLCoXVAqVi7YNM7/F0YIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCG+/ANh4i1CHdc63QAAAABJRU5ErkJggg=="
- width="15px" height="15px"/>
- </div>
- </div>`
+ `< div >
+ <div style="height:100%; width:100%;position:absolute;">{layout}</div>
+ <div id="isExpander" style="height:15px; width:15px; margin-left:-16px; pointer-events:all; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white;">
+ <img id="isExpander" src="/assets/downarrow.png" width="15px" height="15px" />
+ </div>
+ </div > `
);
export function ImageOverlay(width: number, height: number, field: string = "thumbnail") {
- return (`<div>
- <div style="height:100%; width:100%; position:absolute;">{layout}</div>
- <div style="height:auto; width:${width}px; bottom:0; right:0; background:rgba(0,0,0,0.25); position:absolute;overflow:hidden;">
- <ImageBox id="isExpander" {...props} style="width:100%; height=auto;" PanelWidth={${width}} fieldKey={"${field}"} />
+ return (`< div >
+ <div style="height:100%; width:100%; position:absolute;">{layout}</div>
+ <div style="height:auto; width:${width}px; bottom:0; right:0; background:rgba(0,0,0,0.25); position:absolute;overflow:hidden;">
+ <ImageBox id="isExpander" {...props} style="width:100%; height=auto;" PanelWidth={${width}} fieldKey={"${field}"} />
</div>
- </div>`);
+ </div > `);
}
- export const TemplateList: Template[] = [Title, TitleOverlay, Caption, Bullet];
+ export function TitleBar(datastring: string) {
+ return (`<div>
+ <div style="height:25px; width:100%; background-color: rgba(0, 0, 0, .4); color: white; z-index: 100">
+ <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">${datastring}</span>
+ </div>
+ <div style="height:calc(100% - 25px);">
+ <div style="width:100%;overflow:auto">{layout}</div>
+ </div>
+ </div>` );
+ }
+ export const TemplateList: Template[] = [Title, Header, Caption, Bullet];
export function sortTemplates(a: Template, b: Template) {
if (a.Position < b.Position) { return -1; }
diff --git a/src/client/views/_nodeModuleOverrides.scss b/src/client/views/_nodeModuleOverrides.scss
index 6f97e60f8..b8a7db034 100644
--- a/src/client/views/_nodeModuleOverrides.scss
+++ b/src/client/views/_nodeModuleOverrides.scss
@@ -2,22 +2,21 @@
// goldenlayout stuff
div .lm_header {
- background: $dark-color;
- min-height: 2em;
+ background: $dark-color;
}
.lm_tab {
- margin-top: 0.6em !important;
- padding-top: 0.5em !important;
- min-height: 1.35em;
- padding-bottom: 0px;
- border-radius: 5px;
- font-family: $sans-serif !important;
+ // margin-top: 0.6em !important;
+ // padding-top: 0.5em !important;
+ // min-height: 1.35em;
+ // padding-bottom: 0px;
+ // border-radius: 5px;
+ font-family: $sans-serif !important;
}
.lm_header .lm_controls {
- right: 1em !important;
+ right: 1em !important;
}
// @TODO the ril__navgiation buttons in the img gallery are a lil messed up but I can't figure out
-// why. Low priority for now but it's bugging me. --Julie
+// why. Low priority for now but it's bugging me. --Julie \ No newline at end of file
diff --git a/src/client/views/collections/CollectionBaseView.scss b/src/client/views/collections/CollectionBaseView.scss
new file mode 100644
index 000000000..1f5acb96a
--- /dev/null
+++ b/src/client/views/collections/CollectionBaseView.scss
@@ -0,0 +1,11 @@
+@import "../globalCssVariables";
+#collectionBaseView {
+ border-width: 0;
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
+ border-color: $light-color-secondary;
+ border-style: solid;
+ border-radius: 0 0 $border-radius $border-radius;
+ box-sizing: border-box;
+ border-radius: inherit;
+ pointer-events: all;
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index 096c65092..e9a693f55 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -1,14 +1,16 @@
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { ContextMenu } from '../ContextMenu';
-import { FieldViewProps } from '../nodes/FieldView';
-import { Cast, FieldValue, PromiseValue, NumCast } from '../../../new_fields/Types';
-import { Doc, FieldResult, Opt, DocListCast } from '../../../new_fields/Doc';
-import { listSpec } from '../../../new_fields/Schema';
+import { Doc } from '../../../new_fields/Doc';
+import { Id } from '../../../new_fields/FieldSymbols';
import { List } from '../../../new_fields/List';
+import { listSpec } from '../../../new_fields/Schema';
+import { BoolCast, Cast, NumCast, PromiseValue } from '../../../new_fields/Types';
+import { DocumentManager } from '../../util/DocumentManager';
import { SelectionManager } from '../../util/SelectionManager';
-import { Id } from '../../../new_fields/FieldSymbols';
+import { ContextMenu } from '../ContextMenu';
+import { FieldViewProps } from '../nodes/FieldView';
+import './CollectionBaseView.scss';
export enum CollectionViewType {
Invalid,
@@ -34,7 +36,6 @@ export interface CollectionViewProps extends FieldViewProps {
contentRef?: React.Ref<HTMLDivElement>;
}
-
@observer
export class CollectionBaseView extends React.Component<CollectionViewProps> {
@observable private static _safeMode = false;
@@ -58,10 +59,12 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
}
}
+ @computed get dataDoc() { return Doc.resolvedFieldDataDoc(BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc ? this.props.DataDoc : this.props.Document : this.props.Document, this.props.fieldKey, this.props.fieldExt); }
+ @computed get dataField() { return this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; }
+
active = (): boolean => {
var isSelected = this.props.isSelected();
- var topMost = this.props.isTopMost;
- return isSelected || this._isChildActive || topMost;
+ return isSelected || this._isChildActive || this.props.renderDepth === 0 || BoolCast(this.props.Document.excludeFromLibrary);
}
//TODO should this be observable?
@@ -71,85 +74,35 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
this.props.whenActiveChanged(isActive);
}
- createsCycle(documentToAdd: Doc, containerDocument: Doc): boolean {
- if (!(documentToAdd instanceof Doc)) {
- return false;
- }
- let data = DocListCast(documentToAdd.data);
- for (const doc of data) {
- if (this.createsCycle(doc, containerDocument)) {
- return true;
- }
- }
- let annots = DocListCast(documentToAdd.annotations);
- for (const annot of annots) {
- if (this.createsCycle(annot, containerDocument)) {
- return true;
- }
- }
- for (let containerProto: Opt<Doc> = containerDocument; containerProto !== undefined; containerProto = FieldValue(containerProto.proto)) {
- if (containerProto[Id] === documentToAdd[Id]) {
- return true;
- }
- }
- return false;
- }
- @computed get isAnnotationOverlay() { return this.props.fieldKey === "annotations"; }
-
@action.bound
addDocument(doc: Doc, allowDuplicates: boolean = false): boolean {
- let props = this.props;
- var curPage = NumCast(props.Document.curPage, -1);
- Doc.SetOnPrototype(doc, "page", curPage);
+ var curPage = NumCast(this.props.Document.curPage, -1);
+ Doc.GetProto(doc).page = curPage;
if (curPage >= 0) {
- Doc.SetOnPrototype(doc, "annotationOn", props.Document);
+ Doc.GetProto(doc).annotationOn = this.props.Document;
}
- if (!this.createsCycle(doc, props.Document)) {
- //TODO This won't create the field if it doesn't already exist
- const childDocs = Cast(props.Document[props.fieldKey], listSpec(Doc));
- let alreadyAdded = true;
- if (childDocs !== undefined) {
- // if this is not the first document added to the collection
- if (allowDuplicates || !childDocs.some(v => v instanceof Doc && v[Id] === doc[Id])) {
- alreadyAdded = false;
- childDocs.push(doc);
- }
- // if we're here, we've tried to add a duplicate
- } else {
- // if we are the first, set up a new list for this and all
- // future child documents stored in the associated collection document at the fieldKey (likely .data)
- // passed in via props
- alreadyAdded = false;
- Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new List([doc]));
- }
- // set the ZoomBasis only if hasn't already been set -- bcz: maybe set/resetting the ZoomBasis should be a parameter to addDocument?
- if (!alreadyAdded && (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid)) {
- let zoom = NumCast(this.props.Document.scale, 1);
- // Doc.GetProto(doc).zoomBasis = zoom;
+ allowDuplicates = true;
+ const value = Cast(this.dataDoc[this.dataField], listSpec(Doc));
+ if (value !== undefined) {
+ if (allowDuplicates || !value.some(v => v instanceof Doc && v[Id] === doc[Id])) {
+ value.push(doc);
}
+ } else {
+ Doc.GetProto(this.dataDoc)[this.dataField] = new List([doc]);
}
return true;
}
@action.bound
removeDocument(doc: Doc): boolean {
- SelectionManager.DeselectAll();
- const props = this.props;
+ let docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView);
+ docView && SelectionManager.DeselectDoc(docView);
//TODO This won't create the field if it doesn't already exist
- const value = Cast(props.Document[props.fieldKey], listSpec(Doc), []);
- let index = -1;
- for (let i = 0; i < value.length; i++) {
- let v = value[i];
- if (v instanceof Doc && v[Id] === doc[Id]) {
- index = i;
- break;
- }
- }
- PromiseValue(Cast(doc.annotationOn, Doc)).then((annotationOn) => {
- if (annotationOn === props.Document) {
- doc.annotationOn = undefined;
- }
- });
+ const value = Cast(this.dataDoc[this.dataField], listSpec(Doc), []);
+ let index = value.reduce((p, v, i) => (v instanceof Doc && v[Id] === doc[Id]) ? i : p, -1);
+ PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn =>
+ annotationOn === this.dataDoc.Document && (doc.annotationOn = undefined)
+ );
if (index !== -1) {
value.splice(index, 1);
@@ -163,11 +116,10 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
@action.bound
moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
- if (this.props.Document === targetCollection) {
+ if (Doc.AreProtosEqual(this.dataDoc, targetCollection)) {
return true;
}
if (this.removeDocument(doc)) {
- SelectionManager.DeselectAll();
return addDocument(doc);
}
return false;
@@ -183,12 +135,11 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
};
const viewtype = this.collectionViewType;
return (
- <div className={this.props.className || "collectionView-cont"}
- style={{ borderRadius: "inherit", pointerEvents: "all" }}
+ <div id="collectionBaseView" className={this.props.className || "collectionView-cont"}
onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}>
{viewtype !== undefined ? this.props.children(viewtype, props) : (null)}
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 5270a4624..f44ab50c7 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -1,122 +1,39 @@
import 'golden-layout/src/css/goldenlayout-base.css';
import 'golden-layout/src/css/goldenlayout-dark-theme.css';
-import { action, observable, reaction, Lambda, IReactionDisposer } from "mobx";
+import { action, Lambda, observable, reaction } from "mobx";
import { observer } from "mobx-react";
import * as ReactDOM from 'react-dom';
-import Measure, { ContentRect } from "react-measure";
+import Measure from "react-measure";
import * as GoldenLayout from "../../../client/goldenLayout";
-import { Doc, Field, Opt, DocListCast } from "../../../new_fields/Doc";
+import { Doc, DocListCast, Field, Opt } from "../../../new_fields/Doc";
+import { Id } from '../../../new_fields/FieldSymbols';
+import { FieldId } from "../../../new_fields/RefField";
import { listSpec } from "../../../new_fields/Schema";
-import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { emptyFunction, returnTrue, Utils } from "../../../Utils";
+import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
+import { emptyFunction, returnTrue, Utils, returnOne } from "../../../Utils";
import { DocServer } from "../../DocServer";
+import { DocumentManager } from '../../util/DocumentManager';
import { DragLinksAsDocuments, DragManager } from "../../util/DragManager";
+import { SelectionManager } from '../../util/SelectionManager';
+import { Transform } from '../../util/Transform';
import { undoBatch, UndoManager } from "../../util/UndoManager";
+import { DocumentView } from "../nodes/DocumentView";
+import { CollectionViewType } from './CollectionBaseView';
import "./CollectionDockingView.scss";
import { SubCollectionViewProps } from "./CollectionSubView";
-import React = require("react");
import { ParentDocSelector } from './ParentDocumentSelector';
-import { DocumentManager } from '../../util/DocumentManager';
-import { Id } from '../../../new_fields/FieldSymbols';
-import { DockedFrameRenderer } from './DockedFrameRenderer';
+import React = require("react");
+import { MainView } from '../MainView';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faFile } from '@fortawesome/free-solid-svg-icons';
+import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
+library.add(faFile);
@observer
export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
- public static TopLevel: CollectionDockingView;
- private _goldenLayout: any = null;
- private _containerRef = React.createRef<HTMLDivElement>();
- reactionDisposer?: IReactionDisposer;
- _removedDocs: Doc[] = [];
- private _flush: boolean = false;
- private _ignoreStateChange = "";
- private _isPointerDown = false;
- hack: boolean = false;
- undohack: any = null;
-
- constructor(props: SubCollectionViewProps) {
- super(props);
- CollectionDockingView.TopLevel = this;
- (window as any).React = React;
- (window as any).ReactDOM = ReactDOM;
- }
-
- componentDidMount: () => void = () => {
- if (this._containerRef.current) {
- this.reactionDisposer = reaction(
- () => StrCast(this.props.Document.dockingConfig),
- () => {
- if (!this._goldenLayout || this._ignoreStateChange !== this.retrieveConfiguration()) {
- // Because this is in a set timeout, if this component unmounts right after mounting,
- // we will leak a GoldenLayout, because we try to destroy it before we ever create it
- setTimeout(() => this.setupGoldenLayout(), 1);
- }
- this._ignoreStateChange = "";
- }, { fireImmediately: true });
-
- // window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window
- }
- }
-
- componentWillUnmount: () => void = () => {
- try {
- this._goldenLayout.unbind('itemDropped', this.itemDropped);
- this._goldenLayout.unbind('tabCreated', this.tabCreated);
- this._goldenLayout.unbind('stackCreated', this.stackCreated);
- this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed);
- } catch (e) {
- console.log("Unable to unbind Golden Layout event listener...", e);
- }
- if (this._goldenLayout) this._goldenLayout.destroy();
- this._goldenLayout = null;
-
- if (this.reactionDisposer) {
- this.reactionDisposer();
- }
- }
-
- setupGoldenLayout() {
- var config = StrCast(this.props.Document.dockingConfig);
- if (config) {
- if (!this._goldenLayout) {
- this.initializeConfiguration(config);
- }
- else {
- if (config === this.retrieveConfiguration()) {
- return;
- }
- try {
- this._goldenLayout.unbind('itemDropped', this.itemDropped);
- this._goldenLayout.unbind('tabCreated', this.tabCreated);
- this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed);
- this._goldenLayout.unbind('stackCreated', this.stackCreated);
- } catch (e) { }
- this._goldenLayout.destroy();
- this.initializeConfiguration(config);
- }
- this._goldenLayout.on('itemDropped', this.itemDropped);
- this._goldenLayout.on('tabCreated', this.tabCreated);
- this._goldenLayout.on('tabDestroyed', this.tabDestroyed);
- this._goldenLayout.on('stackCreated', this.stackCreated);
- this._goldenLayout.registerComponent('DocumentFrameRenderer', DockedFrameRenderer);
- this._goldenLayout.container = this._containerRef.current;
- if (this._goldenLayout.config.maximisedItemId === '__glMaximised') {
- try {
- this._goldenLayout.config.root.getItemsById(this._goldenLayout.config.maximisedItemId)[0].toggleMaximise();
- } catch (e) {
- this._goldenLayout.config.maximisedItemId = null;
- }
- }
- this._goldenLayout.init();
- }
- }
-
- private makeDocConfig = (document: Doc, width?: number) => {
- const config = CollectionDockingView.makeDocumentConfig(document, width);
- (config.props as any).parent = this;
- return config;
- }
-
- public static makeDocumentConfig(document: Doc, width?: number) {
+ public static Instance: CollectionDockingView;
+ public static makeDocumentConfig(document: Doc, dataDoc: Doc | undefined, width?: number) {
return {
type: 'react-component',
component: 'DocumentFrameRenderer',
@@ -124,83 +41,147 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
width: width,
props: {
documentId: document[Id],
+ dataDocumentId: dataDoc ? dataDoc[Id] : ""
+ //collectionDockingView: CollectionDockingView.Instance
}
};
}
- initializeConfiguration = (configText: string) => {
- let configuration: any = JSON.parse(configText);
- this.injectParentProp(configuration.content);
- this._goldenLayout = new GoldenLayout(configuration);
- }
+ private _goldenLayout: any = null;
+ private _containerRef = React.createRef<HTMLDivElement>();
+ private _flush: boolean = false;
+ private _ignoreStateChange = "";
+ private _isPointerDown = false;
+ private _maximizedSrc: Opt<DocumentView>;
- retrieveConfiguration = () => {
- let configuration: any = this._goldenLayout.toConfig();
- this.injectParentProp(configuration.content, true);
- return JSON.stringify(configuration);
+ constructor(props: SubCollectionViewProps) {
+ super(props);
+ if (props.addDocTab === emptyFunction) CollectionDockingView.Instance = this;
+ //Why is this here?
+ (window as any).React = React;
+ (window as any).ReactDOM = ReactDOM;
}
-
- injectParentProp = (contentArray: any[], reverse: boolean = false) => {
- if (!contentArray || contentArray.length === 0) return;
- contentArray.forEach(member => {
- let baseCase = Object.keys(member).includes("props");
- if (!baseCase) {
- this.injectParentProp(member.content, reverse);
- } else {
- reverse ? delete member.props.parent : member.props.parent = this;
- }
+ hack: boolean = false;
+ undohack: any = null;
+ public StartOtherDrag(e: any, dragDocs: Doc[], dragDataDocs: (Doc | undefined)[] = []) {
+ let config: any;
+ if (dragDocs.length === 1) {
+ config = CollectionDockingView.makeDocumentConfig(dragDocs[0], dragDataDocs[0]);
+ } else {
+ config = {
+ type: 'row',
+ content: dragDocs.map((doc, i) => {
+ CollectionDockingView.makeDocumentConfig(doc, dragDataDocs[i]);
+ })
+ };
+ }
+ const div = document.createElement("div");
+ const dragSource = this._goldenLayout.createDragSource(div, config);
+ dragSource._dragListener.on("dragStop", () => {
+ dragSource.destroy();
});
- }
-
- public StartOtherDrag(dragDocs: Doc[], e: any) {
- this.hack = true;
- this.undohack = UndoManager.StartBatch("goldenDrag");
- dragDocs.map(dragDoc =>
- CollectionDockingView.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener.
- onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 }));
+ dragSource._dragListener.onMouseDown(e);
+ // dragSource.destroy();
+ // this.hack = true;
+ // this.undohack = UndoManager.StartBatch("goldenDrag");
+ // dragDocs.map((dragDoc, i) =>
+ // this.AddRightSplit(dragDoc, dragDataDocs[i], true).contentItems[0].tab._dragListener.
+ // onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 }));
}
@action
- public static OpenFullScreen(document: Doc, dockingView: CollectionDockingView = CollectionDockingView.TopLevel) {
- dockingView.openFullScreen(document);
- }
-
- private openFullScreen = (document: Doc) => {
+ public OpenFullScreen(docView: DocumentView) {
+ let document = Doc.MakeAlias(docView.props.Document);
+ let dataDoc = docView.dataDoc;
let newItemStackConfig = {
type: 'stack',
- content: [this.makeDocConfig(document)]
+ content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)]
};
var docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout);
this._goldenLayout.root.contentItems[0].addChild(docconfig);
docconfig.callDownwards('_$init');
this._goldenLayout._$maximiseItem(docconfig);
- this._ignoreStateChange = this.retrieveConfiguration();
+ this._maximizedSrc = docView;
+ this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
+ this.stateChanged();
+ }
+
+ public CloseFullScreen = () => {
+ let target = this._goldenLayout._maximisedItem;
+ if (target !== null && this._maximizedSrc) {
+ this._goldenLayout._maximisedItem.remove();
+ SelectionManager.SelectDoc(this._maximizedSrc, false);
+ this._maximizedSrc = undefined;
+ this.stateChanged();
+ }
+ }
+
+ public HasFullScreen = () => {
+ return this._goldenLayout._maximisedItem !== null;
+ }
+
+ @undoBatch
+ @action
+ public CloseRightSplit = (document: Doc): boolean => {
+ let retVal = false;
+ if (this._goldenLayout.root.contentItems[0].isRow) {
+ retVal = Array.from(this._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
+ if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" &&
+ Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)) {
+ child.contentItems[0].remove();
+ this.layoutChanged(document);
+ return true;
+ } else {
+ Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => {
+ if (Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) {
+ child.contentItems[j].remove();
+ child.config.activeItemIndex = Math.max(child.contentItems.length - 1, 0);
+ let docs = Cast(this.props.Document.data, listSpec(Doc));
+ docs && docs.indexOf(document) !== -1 && docs.splice(docs.indexOf(document), 1);
+ return true;
+ }
+ return false;
+ });
+ }
+ return false;
+ });
+ }
+ if (retVal) {
+ this.stateChanged();
+ }
+ return retVal;
+ }
+
+ @action
+ layoutChanged(removed?: Doc) {
+ this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]);
+ this._goldenLayout.emit('stateChanged');
+ this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
+ if (removed) CollectionDockingView.Instance._removedDocs.push(removed);
this.stateChanged();
}
//
// Creates a vertical split on the right side of the docking view, and then adds the Document to that split
//
- public static AddRightSplit = (document: Doc, minimize: boolean = false, dockingView: CollectionDockingView = CollectionDockingView.TopLevel) => {
- return dockingView.addRightSplit(document, minimize);
- }
-
- private addRightSplit(document: Doc, minimize = false) {
+ @action
+ public AddRightSplit = (document: Doc, dataDoc: Doc | undefined, minimize: boolean = false) => {
let docs = Cast(this.props.Document.data, listSpec(Doc));
if (docs) {
docs.push(document);
}
let newItemStackConfig = {
type: 'stack',
- content: [this.makeDocConfig(document)]
+ content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)]
};
var newContentItem = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout);
- if (this._goldenLayout.root.contentItems[0].isRow) {
+ if (this._goldenLayout.root.contentItems.length === 0) {
+ this._goldenLayout.root.addChild(newContentItem);
+ } else if (this._goldenLayout.root.contentItems[0].isRow) {
this._goldenLayout.root.contentItems[0].addChild(newContentItem);
- }
- else {
+ } else {
var collayout = this._goldenLayout.root.contentItems[0];
var newRow = collayout.layoutManager.createContentItem({ type: "row" }, this._goldenLayout);
collayout.parent.replaceChild(collayout, newRow);
@@ -221,75 +202,99 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
return newContentItem;
}
-
- public static AddTab = (stack: any, document: Doc, dockingView: CollectionDockingView = CollectionDockingView.TopLevel) => {
- dockingView.addTab(stack, document);
- }
-
- private addTab = (stack: any, document: Doc) => {
+ @action
+ public AddTab = (stack: any, document: Doc, dataDocument: Doc | undefined) => {
let docs = Cast(this.props.Document.data, listSpec(Doc));
if (docs) {
docs.push(document);
}
- let docContentConfig = this.makeDocConfig(document);
+ let docContentConfig = CollectionDockingView.makeDocumentConfig(document, dataDocument);
var newContentItem = stack.layoutManager.createContentItem(docContentConfig, this._goldenLayout);
stack.addChild(newContentItem.contentItems[0], undefined);
this.layoutChanged();
}
- @undoBatch
- @action
- public static CloseRightSplit = (document: Doc, dockingView: CollectionDockingView = CollectionDockingView.TopLevel): boolean => {
- let retVal = false;
- if (dockingView._goldenLayout.root.contentItems[0].isRow) {
- retVal = Array.from(dockingView._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
- if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" &&
- Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)) {
- child.contentItems[0].remove();
- dockingView.layoutChanged(document);
- return true;
- } else {
- Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => {
- if (Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) {
- child.contentItems[j].remove();
- child.config.activeItemIndex = Math.max(child.contentItems.length - 1, 0);
- let docs = Cast(dockingView.props.Document.data, listSpec(Doc));
- docs && docs.indexOf(document) !== -1 && docs.splice(docs.indexOf(document), 1);
- return true;
- }
- return false;
- });
+ setupGoldenLayout() {
+ var config = StrCast(this.props.Document.dockingConfig);
+ if (config) {
+ if (!this._goldenLayout) {
+ this._goldenLayout = new GoldenLayout(JSON.parse(config));
+ }
+ else {
+ if (config === JSON.stringify(this._goldenLayout.toConfig())) {
+ return;
}
- return false;
- });
- }
- if (retVal) {
- dockingView.stateChanged();
+ try {
+ this._goldenLayout.unbind('itemDropped', this.itemDropped);
+ this._goldenLayout.unbind('tabCreated', this.tabCreated);
+ this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed);
+ this._goldenLayout.unbind('stackCreated', this.stackCreated);
+ } catch (e) { }
+ this._goldenLayout.destroy();
+ this._goldenLayout = new GoldenLayout(JSON.parse(config));
+ }
+ this._goldenLayout.on('itemDropped', this.itemDropped);
+ this._goldenLayout.on('tabCreated', this.tabCreated);
+ this._goldenLayout.on('tabDestroyed', this.tabDestroyed);
+ this._goldenLayout.on('stackCreated', this.stackCreated);
+ this._goldenLayout.registerComponent('DocumentFrameRenderer', DockedFrameRenderer);
+ this._goldenLayout.container = this._containerRef.current;
+ if (this._goldenLayout.config.maximisedItemId === '__glMaximised') {
+ try {
+ this._goldenLayout.config.root.getItemsById(this._goldenLayout.config.maximisedItemId)[0].toggleMaximise();
+ } catch (e) {
+ this._goldenLayout.config.maximisedItemId = null;
+ }
+ }
+ this._goldenLayout.init();
}
- return retVal;
}
+ reactionDisposer?: Lambda;
+ componentDidMount: () => void = () => {
+ if (this._containerRef.current) {
+ this.reactionDisposer = reaction(
+ () => StrCast(this.props.Document.dockingConfig),
+ () => {
+ if (!this._goldenLayout || this._ignoreStateChange !== JSON.stringify(this._goldenLayout.toConfig())) {
+ // Because this is in a set timeout, if this component unmounts right after mounting,
+ // we will leak a GoldenLayout, because we try to destroy it before we ever create it
+ setTimeout(() => this.setupGoldenLayout(), 1);
+ this.props.Document.workspaceBrush = true;
+ }
+ this._ignoreStateChange = "";
+ }, { fireImmediately: true });
- @action
- layoutChanged(removed?: Doc) {
- this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]);
- this._goldenLayout.emit('stateChanged');
- this._ignoreStateChange = this.retrieveConfiguration();
- if (removed) CollectionDockingView.TopLevel._removedDocs.push(removed);
- this.stateChanged();
+ window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window
+ }
}
+ componentWillUnmount: () => void = () => {
+ try {
+ this.props.Document.workspaceBrush = false;
+ this._goldenLayout.unbind('itemDropped', this.itemDropped);
+ this._goldenLayout.unbind('tabCreated', this.tabCreated);
+ this._goldenLayout.unbind('stackCreated', this.stackCreated);
+ this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed);
+ } catch (e) {
+ }
+ if (this._goldenLayout) this._goldenLayout.destroy();
+ this._goldenLayout = null;
+ window.removeEventListener('resize', this.onResize);
+
+ if (this.reactionDisposer) {
+ this.reactionDisposer();
+ }
+ }
@action
- onResize = (size: ContentRect) => {
+ onResize = (event: any) => {
+ var cur = this._containerRef.current;
+
// bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed
- // this._goldenLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height);
- if (this._goldenLayout) {
- this._goldenLayout.updateSize(size.offset!.width, size.offset!.height);
- }
+ this._goldenLayout && this._goldenLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height);
}
@action
onPointerUp = (e: React.PointerEvent): void => {
- this._isPointerDown = false;
if (this._flush) {
this._flush = false;
setTimeout(() => this.stateChanged(), 10);
@@ -298,6 +303,11 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
@action
onPointerDown = (e: React.PointerEvent): void => {
this._isPointerDown = true;
+ let onPointerUp = action(() => {
+ window.removeEventListener("pointerup", onPointerUp);
+ this._isPointerDown = false;
+ });
+ window.addEventListener("pointerup", onPointerUp);
var className = (e.target as any).className;
if (className === "messageCounter") {
e.stopPropagation();
@@ -306,48 +316,22 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
let y = e.clientY;
let docid = (e.target as any).DashDocId;
let tab = (e.target as any).parentElement as HTMLElement;
- DocServer.getRefField(docid).then(action(async (sourceDoc: Opt<Field>) =>
+ DocServer.GetRefField(docid).then(action(async (sourceDoc: Opt<Field>) =>
(sourceDoc instanceof Doc) && DragLinksAsDocuments(tab, x, y, sourceDoc)));
- } else
- if ((className === "lm_title" || className === "lm_tab lm_active") && !e.shiftKey) {
- e.stopPropagation();
- e.preventDefault();
- let x = e.clientX;
- let y = e.clientY;
- let docid = (e.target as any).DashDocId;
- let tab = (e.target as any).parentElement as HTMLElement;
- let glTab = (e.target as any).Tab;
- if (glTab && glTab.contentItem && glTab.contentItem.parent) {
- glTab.contentItem.parent.setActiveContentItem(glTab.contentItem);
- }
- DocServer.getRefField(docid).then(action((f: Opt<Field>) => {
- if (f instanceof Doc) {
- DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f]), x, y,
- {
- handlers: {
- dragComplete: emptyFunction,
- },
- hideSource: false
- });
- }
- }));
- }
+ }
if (className === "lm_drag_handle" || className === "lm_close" || className === "lm_maximise" || className === "lm_minimise" || className === "lm_close_tab") {
this._flush = true;
}
- if (this.props.active()) {
- e.stopPropagation();
- }
}
@undoBatch
stateChanged = () => {
- let docs = Cast(CollectionDockingView.TopLevel.props.Document.data, listSpec(Doc));
- CollectionDockingView.TopLevel._removedDocs.map(theDoc =>
+ let docs = Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc));
+ CollectionDockingView.Instance._removedDocs.map(theDoc =>
docs && docs.indexOf(theDoc) !== -1 &&
docs.splice(docs.indexOf(theDoc), 1));
- CollectionDockingView.TopLevel._removedDocs.length = 0;
- var json = this.retrieveConfiguration();
+ CollectionDockingView.Instance._removedDocs.length = 0;
+ var json = JSON.stringify(this._goldenLayout.toConfig());
this.props.Document.dockingConfig = json;
if (this.undohack && !this.hack) {
this.undohack.end();
@@ -356,52 +340,61 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
this.hack = false;
}
- private itemDropped = () => {
+ itemDropped = () => {
this.stateChanged();
}
- private htmlToElement(html: string) {
+ htmlToElement(html: string) {
var template = document.createElement('template');
html = html.trim(); // Never return a text node of whitespace as the result
template.innerHTML = html;
return template.content.firstChild;
}
- private tabCreated = async (tab: any) => {
+ tabCreated = async (tab: any) => {
if (tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") {
if (tab.contentItem.config.fixed) {
tab.contentItem.parent.config.fixed = true;
}
- DocServer.getRefField(tab.contentItem.config.props.documentId).then(async doc => {
- if (doc instanceof Doc) {
- let counter: any = this.htmlToElement(`<span class="messageCounter">0</div>`);
- tab.element.append(counter);
- let upDiv = document.createElement("span");
- const stack = tab.contentItem.parent;
- // shifts the focus to this tab when another tab is dragged over it
- tab.element[0].onmouseenter = (e: any) => {
- if (!this._isPointerDown) return;
- var activeContentItem = tab.header.parent.getActiveContentItem();
- if (tab.contentItem !== activeContentItem) {
- tab.header.parent.setActiveContentItem(tab.contentItem);
- }
- tab.setActive(true);
- };
- tab.header.element[0].ondrop = (e: any) => {
- console.log("DROPPPP THE BASS!", e);
- };
- ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, location) => CollectionDockingView.AddTab(stack, doc)} />, upDiv);
- tab.reactComponents = [upDiv];
- tab.element.append(upDiv);
- counter.DashDocId = tab.contentItem.config.props.documentId;
- tab.reactionDisposer = reaction(() => [doc.linkedFromDocs, doc.LinkedToDocs, doc.title],
- () => {
- counter.innerHTML = DocListCast(doc.linkedFromDocs).length + DocListCast(doc.linkedToDocs).length;
- tab.titleElement[0].textContent = doc.title;
- }, { fireImmediately: true });
- tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId;
- }
- });
+ let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId) as Doc;
+ let dataDoc = await DocServer.GetRefField(tab.contentItem.config.props.dataDocumentId) as Doc;
+ if (doc instanceof Doc) {
+ let dragSpan = document.createElement("span");
+ dragSpan.style.position = "relative";
+ dragSpan.style.bottom = "6px";
+ dragSpan.style.paddingLeft = "4px";
+ dragSpan.style.paddingRight = "2px";
+ let upDiv = document.createElement("span");
+ const stack = tab.contentItem.parent;
+ // shifts the focus to this tab when another tab is dragged over it
+ tab.element[0].onmouseenter = (e: any) => {
+ if (!this._isPointerDown) return;
+ var activeContentItem = tab.header.parent.getActiveContentItem();
+ if (tab.contentItem !== activeContentItem) {
+ tab.header.parent.setActiveContentItem(tab.contentItem);
+ }
+ tab.setActive(true);
+ };
+ ReactDOM.render(<span title="Drag as document" onPointerDown={
+ e => {
+ e.preventDefault();
+ e.stopPropagation();
+ DragManager.StartDocumentDrag([dragSpan], new DragManager.DocumentDragData([doc], [dataDoc]), e.clientX, e.clientY, {
+ handlers: { dragComplete: emptyFunction },
+ hideSource: false
+ });
+ }}><FontAwesomeIcon icon="file" size="lg" /></span>, dragSpan);
+ ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={doc => CollectionDockingView.Instance.AddTab(stack, doc, dataDoc)} />, upDiv);
+ tab.reactComponents = [dragSpan, upDiv];
+ tab.element.append(dragSpan);
+ tab.element.append(upDiv);
+ tab.reactionDisposer = reaction(() => [doc.title],
+ () => {
+ tab.titleElement[0].textContent = doc.title;
+ }, { fireImmediately: true });
+ //TODO why can't this just be doc instead of the id?
+ tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId;
+ }
}
tab.titleElement[0].Tab = tab;
tab.closeElement.off('click') //unbind the current click handler
@@ -409,35 +402,41 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
if (tab.reactionDisposer) {
tab.reactionDisposer();
}
- let doc = await DocServer.getRefField(tab.contentItem.config.props.documentId);
+ let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId);
if (doc instanceof Doc) {
let theDoc = doc;
- CollectionDockingView.TopLevel._removedDocs.push(theDoc);
+ CollectionDockingView.Instance._removedDocs.push(theDoc);
+ if (CurrentUserUtils.UserDocument.recentlyClosed instanceof Doc) {
+ Doc.AddDocToList(CurrentUserUtils.UserDocument.recentlyClosed, "data", doc, undefined, true, true);
+ }
+ SelectionManager.DeselectAll();
}
tab.contentItem.remove();
});
}
- private tabDestroyed = (tab: any) => {
+ tabDestroyed = (tab: any) => {
if (tab.reactComponents) {
for (const ele of tab.reactComponents) {
ReactDOM.unmountComponentAtNode(ele);
}
}
}
+ _removedDocs: Doc[] = [];
- private stackCreated = (stack: any) => {
+ stackCreated = (stack: any) => {
//stack.header.controlsContainer.find('.lm_popout').hide();
+ stack.header.element[0].style.backgroundColor = DocServer.Control.isReadOnly() ? "#228540" : undefined;
stack.header.controlsContainer.find('.lm_close') //get the close icon
.off('click') //unbind the current click handler
.click(action(function () {
//if (confirm('really close this?')) {
stack.remove();
stack.contentItems.map(async (contentItem: any) => {
- let doc = await DocServer.getRefField(contentItem.config.props.documentId);
+ let doc = await DocServer.GetRefField(contentItem.config.props.documentId);
if (doc instanceof Doc) {
let theDoc = doc;
- CollectionDockingView.TopLevel._removedDocs.push(theDoc);
+ CollectionDockingView.Instance._removedDocs.push(theDoc);
}
});
//}
@@ -452,16 +451,130 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
}
render() {
+ if (this.props.renderDepth > 0) {
+ return <div style={{ width: "100%", height: "100%" }}>Nested workspaces can't be rendered</div>;
+ }
return (
- <Measure onResize={this.onResize} offset>
- {({ measureRef }) => (
- <div ref={measureRef} style={{ width: "100%", height: "100%" }}>
+ <Measure offset onResize={this.onResize}>
+ {({ measureRef }) =>
+ <div ref={measureRef}>
<div className="collectiondockingview-container" id="menuContainer"
onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} />
</div>
- )}
+ }
</Measure>
);
}
+}
+
+interface DockedFrameProps {
+ documentId: FieldId;
+ dataDocumentId: FieldId;
+ //collectionDockingView: CollectionDockingView
+}
+@observer
+export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
+ _mainCont = React.createRef<HTMLDivElement>();
+ @observable private _panelWidth = 0;
+ @observable private _panelHeight = 0;
+ @observable private _document: Opt<Doc>;
+ @observable private _dataDoc: Opt<Doc>;
+ get _stack(): any {
+ let parent = (this.props as any).glContainer.parent.parent;
+ if (this._document && this._document.excludeFromLibrary && parent.parent && parent.parent.contentItems.length > 1) {
+ return parent.parent.contentItems[1];
+ }
+ return parent;
+ }
+ constructor(props: any) {
+ super(props);
+ DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => {
+ this._document = f as Doc;
+ if (this.props.dataDocumentId && this.props.documentId !== this.props.dataDocumentId) {
+ DocServer.GetRefField(this.props.dataDocumentId).then(action((f: Opt<Field>) => this._dataDoc = f as Doc));
+ }
+ }));
+ }
+
+ nativeWidth = () => NumCast(this._document!.nativeWidth, this._panelWidth);
+ nativeHeight = () => {
+ let nh = NumCast(this._document!.nativeHeight, this._panelHeight);
+ let res = BoolCast(this._document!.ignoreAspect) ? this._panelHeight : nh;
+ return res;
+ }
+ contentScaling = () => {
+ const nativeH = this.nativeHeight();
+ const nativeW = this.nativeWidth();
+ let wscale = this._panelWidth / nativeW;
+ return wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale;
+ }
+
+ ScreenToLocalTransform = () => {
+ if (this._mainCont.current && this._mainCont.current.children) {
+ let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current.children[0].firstChild as HTMLElement);
+ scale = Utils.GetScreenTransform(this._mainCont.current).scale;
+ return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale);
+ }
+ return Transform.Identity();
+ }
+ get scaleToFitMultiplier() {
+ let docWidth = NumCast(this._document!.width);
+ let docHeight = NumCast(this._document!.height);
+ if (NumCast(this._document!.nativeWidth) || !docWidth || !this._panelWidth || !this._panelHeight) return 1;
+ if (StrCast(this._document!.layout).indexOf("Collection") === -1 ||
+ !BoolCast(this._document!.fitToContents, false) ||
+ NumCast(this._document!.viewType) !== CollectionViewType.Freeform) return 1;
+ let scaling = Math.max(1, this._panelWidth / docWidth * docHeight > this._panelHeight ?
+ this._panelHeight / docHeight : this._panelWidth / docWidth);
+ return scaling;
+ }
+ get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; }
+
+ addDocTab = (doc: Doc, dataDoc: Doc | undefined, location: string) => {
+ if (doc.dockingConfig) {
+ MainView.Instance.openWorkspace(doc);
+ } else if (location === "onRight") {
+ CollectionDockingView.Instance.AddRightSplit(doc, dataDoc);
+ } else {
+ CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc);
+ }
+ }
+ get content() {
+ if (!this._document) {
+ return (null);
+ }
+ let resolvedDataDoc = this._document.layout instanceof Doc ? this._document : this._dataDoc;
+ return (
+ <div className="collectionDockingView-content" ref={this._mainCont}
+ style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px) scale(${this.scaleToFitMultiplier})` }}>
+ <DocumentView key={this._document[Id]}
+ Document={this._document}
+ DataDoc={resolvedDataDoc}
+ bringToFront={emptyFunction}
+ addDocument={undefined}
+ removeDocument={undefined}
+ ContentScaling={this.contentScaling}
+ PanelWidth={this.nativeWidth}
+ PanelHeight={this.nativeHeight}
+ ScreenToLocalTransform={this.ScreenToLocalTransform}
+ renderDepth={0}
+ selectOnLoad={false}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ focus={emptyFunction}
+ addDocTab={this.addDocTab}
+ ContainingCollectionView={undefined}
+ zoomToScale={emptyFunction}
+ getScale={returnOne} />
+ </div >);
+ }
+
+ render() {
+ let theContent = this.content;
+ return !this._document ? (null) :
+ <Measure offset onResize={action((r: any) => { this._panelWidth = r.offset.width; this._panelHeight = r.offset.height; })}>
+ {({ measureRef }) => <div ref={measureRef}> {theContent} </div>}
+ </Measure>;
+ }
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx
index 5e51437a4..31a73ab36 100644
--- a/src/client/views/collections/CollectionPDFView.tsx
+++ b/src/client/views/collections/CollectionPDFView.tsx
@@ -1,60 +1,74 @@
-import { action, observable } from "mobx";
+import { action, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
+import { WidthSym } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { NumCast } from "../../../new_fields/Types";
+import { emptyFunction } from "../../../Utils";
import { ContextMenu } from "../ContextMenu";
+import { FieldView, FieldViewProps } from "../nodes/FieldView";
+import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from "./CollectionBaseView";
+import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
import "./CollectionPDFView.scss";
import React = require("react");
-import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
-import { FieldView, FieldViewProps } from "../nodes/FieldView";
-import { CollectionRenderProps, CollectionBaseView, CollectionViewType } from "./CollectionBaseView";
-import { emptyFunction } from "../../../Utils";
-import { NumCast } from "../../../new_fields/Types";
-import { Id } from "../../../new_fields/FieldSymbols";
+import { PDFBox } from "../nodes/PDFBox";
@observer
export class CollectionPDFView extends React.Component<FieldViewProps> {
+ private _pdfBox?: PDFBox;
+ private _reactionDisposer?: IReactionDisposer;
+ private _buttonTray: React.RefObject<HTMLDivElement>;
+
+ constructor(props: FieldViewProps) {
+ super(props);
+
+ this._buttonTray = React.createRef();
+ }
+
+ componentDidMount() {
+ this._reactionDisposer = reaction(
+ () => NumCast(this.props.Document.scrollY),
+ () => {
+ // let transform = this.props.ScreenToLocalTransform();
+ if (this._buttonTray.current) {
+ // console.log(this._buttonTray.current.offsetHeight);
+ // console.log(NumCast(this.props.Document.scrollY));
+ let scale = this.nativeWidth() / this.props.Document[WidthSym]();
+ this.props.Document.panY = NumCast(this.props.Document.scrollY);
+ // console.log(scale);
+ }
+ // console.log(this.props.Document[HeightSym]());
+ },
+ { fireImmediately: true }
+ );
+ }
+
+ componentWillUnmount() {
+ this._reactionDisposer && this._reactionDisposer();
+ }
public static LayoutString(fieldKey: string = "data") {
return FieldView.LayoutString(CollectionPDFView, fieldKey);
}
@observable _inThumb = false;
- private set curPage(value: number) { this.props.Document.curPage = value; }
+ private set curPage(value: number) { this._pdfBox && this._pdfBox.GotoPage(value); }
private get curPage() { return NumCast(this.props.Document.curPage, -1); }
private get numPages() { return NumCast(this.props.Document.numPages); }
- @action onPageBack = () => this.curPage > 1 ? (this.props.Document.curPage = this.curPage - 1) : -1;
- @action onPageForward = () => this.curPage < this.numPages ? (this.props.Document.curPage = this.curPage + 1) : -1;
+ @action onPageBack = () => this._pdfBox && this._pdfBox.BackPage();
+ @action onPageForward = () => this._pdfBox && this._pdfBox.ForwardPage();
- @action
- onThumbDown = (e: React.PointerEvent) => {
- document.addEventListener("pointermove", this.onThumbMove, false);
- document.addEventListener("pointerup", this.onThumbUp, false);
- e.stopPropagation();
- this._inThumb = true;
- }
- @action
- onThumbMove = (e: PointerEvent) => {
- let pso = (e.clientY - (e as any).target.parentElement.getBoundingClientRect().top) / (e as any).target.parentElement.getBoundingClientRect().height;
- this.curPage = Math.trunc(Math.min(this.numPages, pso * this.numPages + 1));
- e.stopPropagation();
- }
- @action
- onThumbUp = (e: PointerEvent) => {
- this._inThumb = false;
- document.removeEventListener("pointermove", this.onThumbMove);
- document.removeEventListener("pointerup", this.onThumbUp);
- }
nativeWidth = () => NumCast(this.props.Document.nativeWidth);
nativeHeight = () => NumCast(this.props.Document.nativeHeight);
private get uIButtons() {
let ratio = (this.curPage - 1) / this.numPages * 100;
return (
- <div className="collectionPdfView-buttonTray" key="tray" style={{ height: "100%" }}>
+ <div className="collectionPdfView-buttonTray" ref={this._buttonTray} key="tray" style={{ height: "100%" }}>
<button className="collectionPdfView-backward" onClick={this.onPageBack}>{"<"}</button>
<button className="collectionPdfView-forward" onClick={this.onPageForward}>{">"}</button>
- <div className="collectionPdfView-slider" onPointerDown={this.onThumbDown} style={{ top: 60, left: -20, width: 50, height: `calc(100% - 80px)` }} >
+ {/* <div className="collectionPdfView-slider" onPointerDown={this.onThumbDown} style={{ top: 60, left: -20, width: 50, height: `calc(100% - 80px)` }} >
<div className="collectionPdfView-thumb" onPointerDown={this.onThumbDown} style={{ top: `${ratio}%`, width: 50, height: 50 }} />
- </div>
+ </div> */}
</div>
);
}
@@ -65,12 +79,15 @@ export class CollectionPDFView extends React.Component<FieldViewProps> {
}
}
+ setPdfBox = (pdfBox: PDFBox) => { this._pdfBox = pdfBox; };
+
+
private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => {
let props = { ...this.props, ...renderProps };
return (
<>
- <CollectionFreeFormView {...props} CollectionView={this} />
- {this.props.isSelected() ? this.uIButtons : (null)}
+ <CollectionFreeFormView {...props} setPdfBox={this.setPdfBox} CollectionView={this} />
+ {renderProps.active() ? this.uIButtons : (null)}
</>
);
}
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 477879b79..cf39e26a8 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -2,36 +2,35 @@ import React = require("react");
import { library } from '@fortawesome/fontawesome-svg-core';
import { faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, untracked, runInAction, trace } from "mobx";
+import { action, computed, observable, trace, untracked } from "mobx";
import { observer } from "mobx-react";
import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table";
-import { MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss';
import "react-table/react-table.css";
-import { emptyFunction, returnFalse, returnZero } from "../../../Utils";
-import { SetupDrag } from "../../util/DragManager";
+import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils";
+import { Doc, DocListCast, DocListCastAsync, Field } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { List } from "../../../new_fields/List";
+import { listSpec } from "../../../new_fields/Schema";
+import { Cast, FieldValue, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
+import { Docs } from "../../documents/Documents";
+import { Gateway } from "../../northstar/manager/Gateway";
+import { SetupDrag, DragManager } from "../../util/DragManager";
import { CompileScript } from "../../util/Scripting";
import { Transform } from "../../util/Transform";
-import { COLLECTION_BORDER_WIDTH } from "../../views/globalCssVariables.scss";
+import { COLLECTION_BORDER_WIDTH, MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss';
+import { ContextMenu } from "../ContextMenu";
import { anchorPoints, Flyout } from "../DocumentDecorations";
import '../DocumentDecorations.scss';
import { EditableView } from "../EditableView";
import { DocumentView } from "../nodes/DocumentView";
import { FieldView, FieldViewProps } from "../nodes/FieldView";
+import { CollectionPDFView } from "./CollectionPDFView";
import "./CollectionSchemaView.scss";
import { CollectionSubView } from "./CollectionSubView";
-import { Opt, Field, Doc, DocListCastAsync, DocListCast } from "../../../new_fields/Doc";
-import { Cast, FieldValue, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
-import { listSpec } from "../../../new_fields/Schema";
-import { List } from "../../../new_fields/List";
-import { Id } from "../../../new_fields/FieldSymbols";
-import { Gateway } from "../../northstar/manager/Gateway";
-import { Docs } from "../../documents/Documents";
-import { ContextMenu } from "../ContextMenu";
-import { CollectionView } from "./CollectionView";
-import { CollectionPDFView } from "./CollectionPDFView";
import { CollectionVideoView } from "./CollectionVideoView";
-import { SelectionManager } from "../../util/SelectionManager";
+import { CollectionView } from "./CollectionView";
import { undoBatch } from "../../util/UndoManager";
+import { timesSeries } from "async";
library.add(faCog);
@@ -90,8 +89,9 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
let columnDocs = DocListCast(schemaDoc.data);
if (columnDocs) {
let ddoc = columnDocs.find(doc => doc.title === columnName);
- if (ddoc)
+ if (ddoc) {
return ddoc;
+ }
}
}
return this.props.Document;
@@ -100,11 +100,13 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
renderCell = (rowProps: CellInfo) => {
let props: FieldViewProps = {
Document: rowProps.original,
+ DataDoc: rowProps.original,
fieldKey: rowProps.column.id as string,
+ fieldExt: "",
ContainingCollectionView: this.props.CollectionView,
isSelected: returnFalse,
select: emptyFunction,
- isTopMost: false,
+ renderDepth: this.props.renderDepth + 1,
selectOnLoad: false,
ScreenToLocalTransform: Transform.Identity,
focus: emptyFunction,
@@ -116,9 +118,10 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
};
let fieldContentView = <FieldView {...props} />;
let reference = React.createRef<HTMLDivElement>();
- let onItemDown = (e: React.PointerEvent) =>
- (this.props.CollectionView.props.isSelected() ?
- SetupDrag(reference, () => props.Document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e) : undefined);
+ let onItemDown = (e: React.PointerEvent) => {
+ (!this.props.CollectionView.props.isSelected() ? undefined :
+ SetupDrag(reference, () => props.Document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e));
+ };
let applyToDoc = (doc: Doc, run: (args?: { [name: string]: any }) => any) => {
const res = run({ this: doc });
if (!res.success) return false;
@@ -232,7 +235,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
onPointerDown = (e: React.PointerEvent): void => {
if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) {
if (this.props.isSelected()) e.stopPropagation();
- else e.preventDefault();
}
}
@@ -282,13 +284,13 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
@computed
get previewDocument(): Doc | undefined {
- const children = DocListCast(this.props.Document[this.props.fieldKey]);
- const selected = children.length > this._selectedIndex ? FieldValue(children[this._selectedIndex]) : undefined;
- return selected ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined;
+ const selected = this.childDocs.length > this._selectedIndex ? this.childDocs[this._selectedIndex] : undefined;
+ let pdc = selected ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined;
+ return pdc;
}
getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate(
- - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth);
+ - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth)
get documentKeysCheckList() {
@@ -331,13 +333,12 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
@computed
get reactTable() {
- trace();
let previewWidth = this.previewWidth() + 2 * this.borderWidth + this.DIVIDER_WIDTH + 1;
return <ReactTable style={{ position: "relative", float: "left", width: `calc(100% - ${previewWidth}px` }} data={this.childDocs} page={0} pageSize={this.childDocs.length} showPagination={false}
columns={this.tableColumns}
column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }}
getTrProps={this.getTrProps}
- />
+ />;
}
@computed
@@ -346,23 +347,38 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
<div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />;
}
+
@computed
get previewPanel() {
- trace();
- return <CollectionSchemaPreview
- Document={this.previewDocument}
- width={this.previewWidth}
- height={this.previewHeight}
- getTransform={this.getPreviewTransform}
- CollectionView={this.props.CollectionView}
- addDocument={this.props.addDocument}
- removeDocument={this.props.removeDocument}
- active={this.props.active}
- whenActiveChanged={this.props.whenActiveChanged}
- addDocTab={this.props.addDocTab}
- setPreviewScript={this.setPreviewScript}
- previewScript={this.previewScript}
- />
+ // let layoutDoc = this.previewDocument;
+ // let resolvedDataDoc = (layoutDoc !== this.props.DataDoc) ? this.props.DataDoc : undefined;
+ // if (layoutDoc && !(Cast(layoutDoc.layout, Doc) instanceof Doc) &&
+ // resolvedDataDoc && resolvedDataDoc !== layoutDoc) {
+ // // ... so change the layout to be an expanded view of the template layout. This allows the view override the template's properties and be referenceable as its own document.
+ // layoutDoc = Doc.expandTemplateLayout(layoutDoc, resolvedDataDoc);
+ // }
+
+ let layoutDoc = this.previewDocument ? Doc.expandTemplateLayout(this.previewDocument, this.props.DataDoc) : undefined;
+ return <div ref={this.createTarget}>
+ <CollectionSchemaPreview
+ Document={layoutDoc}
+ DataDocument={this.previewDocument !== this.props.DataDoc ? this.props.DataDoc : undefined}
+ childDocs={this.childDocs}
+ renderDepth={this.props.renderDepth}
+ width={this.previewWidth}
+ height={this.previewHeight}
+ getTransform={this.getPreviewTransform}
+ CollectionView={this.props.CollectionView}
+ moveDocument={this.props.moveDocument}
+ addDocument={this.props.addDocument}
+ removeDocument={this.props.removeDocument}
+ active={this.props.active}
+ whenActiveChanged={this.props.whenActiveChanged}
+ addDocTab={this.props.addDocTab}
+ setPreviewScript={this.setPreviewScript}
+ previewScript={this.previewScript}
+ />
+ </div>;
}
@action
setPreviewScript = (script: string) => {
@@ -370,7 +386,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
}
render() {
- trace();
return (
<div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel}
onDrop={(e: React.DragEvent) => this.onDrop(e, {})} onContextMenu={this.onContextMenu} ref={this.createTarget}>
@@ -384,21 +399,28 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
}
interface CollectionSchemaPreviewProps {
Document?: Doc;
+ DataDocument?: Doc;
+ childDocs?: Doc[];
+ fitToBox?: () => number[];
+ renderDepth: number;
width: () => number;
height: () => number;
- CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
+ CollectionView?: CollectionView | CollectionPDFView | CollectionVideoView;
getTransform: () => Transform;
addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+ moveDocument: (document: Doc, target: Doc, addDoc: ((doc: Doc) => boolean)) => boolean;
removeDocument: (document: Doc) => boolean;
active: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
- addDocTab: (document: Doc, where: string) => void;
+ addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
setPreviewScript: (script: string) => void;
previewScript?: string;
}
@observer
export class CollectionSchemaPreview extends React.Component<CollectionSchemaPreviewProps>{
+ private dropDisposer?: DragManager.DragDropDisposer;
+ _mainCont?: HTMLDivElement;
private get nativeWidth() { return NumCast(this.props.Document!.nativeWidth, this.props.width()); }
private get nativeHeight() { return NumCast(this.props.Document!.nativeHeight, this.props.height()); }
private contentScaling = () => {
@@ -408,49 +430,79 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
}
return wscale;
}
- private PanelWidth = () => this.nativeWidth * this.contentScaling();
- private PanelHeight = () => this.nativeHeight * this.contentScaling();
- private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, 0).scale(1 / this.contentScaling())
- get centeringOffset() { return (this.props.width() - this.nativeWidth * this.contentScaling()) / 2; }
- @action
- onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- this.props.setPreviewScript(e.currentTarget.value);
+ protected createDropTarget = (ele: HTMLDivElement) => {
}
+ private createTarget = (ele: HTMLDivElement) => {
+ this._mainCont = ele;
+ this.dropDisposer && this.dropDisposer();
+ if (ele) {
+ this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+ }
+ }
+
@undoBatch
@action
- public collapseToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => {
- SelectionManager.DeselectAll();
- if (expandedDocs) {
- let isMinimized: boolean | undefined;
- expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => {
- if (isMinimized === undefined) {
- isMinimized = BoolCast(maximizedDoc.isMinimized, false);
- }
- maximizedDoc.isMinimized = !isMinimized;
+ drop = (e: Event, de: DragManager.DropEvent) => {
+ if (de.data instanceof DragManager.DocumentDragData) {
+ let docDrag = de.data;
+ this.props.childDocs && this.props.childDocs.map(otherdoc => {
+ Doc.GetProto(otherdoc).layout = Doc.MakeDelegate(docDrag.draggedDocuments[0]);
});
+ e.stopPropagation();
}
+ return true;
+ }
+ private PanelWidth = () => this.nativeWidth ? this.nativeWidth * this.contentScaling() : this.props.width();
+ private PanelHeight = () => this.nativeHeight ? this.nativeHeight * this.contentScaling() : this.props.height();
+ private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, 0).scale(1 / this.contentScaling());
+ get centeringOffset() { return this.nativeWidth ? (this.props.width() - this.nativeWidth * this.contentScaling()) / 2 : 0; }
+ @action
+ onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.props.setPreviewScript(e.currentTarget.value);
+ }
+ @computed get borderRounding() {
+ let br = StrCast(this.props.Document!.borderRounding);
+ if (br.endsWith("%")) {
+ let percent = Number(br.substr(0, br.length - 1)) / 100;
+ let nativeDim = Math.min(NumCast(this.props.Document!.nativeWidth), NumCast(this.props.Document!.nativeHeight));
+ let minDim = percent * (nativeDim ? nativeDim : Math.min(this.PanelWidth(), this.PanelHeight()));
+ return minDim;
+ }
+ return undefined;
}
render() {
- trace();
- console.log(this.props.Document);
let input = this.props.previewScript === undefined ? (null) :
- <input className="collectionSchemaView-input" value={this.props.previewScript} onChange={this.onPreviewScriptChange}
- style={{ left: `calc(50% - ${Math.min(75, (this.props.Document ? this.PanelWidth() / 2 : 75))}px)` }} />;
- return (<div className="collectionSchemaView-previewRegion" style={{ width: this.props.width() }}>
+ <div ref={this.createTarget}><input className="collectionSchemaView-input" value={this.props.previewScript} onChange={this.onPreviewScriptChange}
+ style={{ left: `calc(50% - ${Math.min(75, (this.props.Document ? this.PanelWidth() / 2 : 75))}px)` }} /></div>;
+ return (<div className="collectionSchemaView-previewRegion" style={{ width: this.props.width(), height: "100%" }}>
{!this.props.Document || !this.props.width ? (null) : (
- <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.centeringOffset}px, 0px)` }}>
- <DocumentView Document={this.props.Document} isTopMost={false} selectOnLoad={false}
- addDocument={this.props.addDocument} removeDocument={this.props.removeDocument}
+ <div className="collectionSchemaView-previewDoc"
+ style={{
+ transform: `translate(${this.centeringOffset}px, 0px)`,
+ borderRadius: this.borderRounding,
+ height: "100%"
+ }}>
+ <DocumentView
+ DataDoc={this.props.Document.layout instanceof Doc ? this.props.Document : this.props.DataDocument}
+ Document={this.props.Document}
+ fitToBox={this.props.fitToBox}
+ renderDepth={this.props.renderDepth + 1}
+ selectOnLoad={false}
+ addDocument={this.props.addDocument}
+ removeDocument={this.props.removeDocument}
+ moveDocument={this.props.moveDocument}
ScreenToLocalTransform={this.getTransform}
ContentScaling={this.contentScaling}
- PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight}
+ PanelWidth={this.PanelWidth}
+ PanelHeight={this.PanelHeight}
ContainingCollectionView={this.props.CollectionView}
focus={emptyFunction}
parentActive={this.props.active}
whenActiveChanged={this.props.whenActiveChanged}
bringToFront={emptyFunction}
addDocTab={this.props.addDocTab}
- collapseToPoint={this.collapseToPoint}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}
/>
</div>)}
{input}
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 4d84aaaa9..bc733f152 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -1,19 +1,6 @@
@import "../globalCssVariables";
-
.collectionStackingView {
- top: 0;
- left: 0;
- display: flex;
- flex-direction: column;
- width: 100%;
- position: absolute;
overflow-y: auto;
- border-width: 0;
- box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
- border-color: $light-color-secondary;
- border-style: solid;
- border-radius: 0 0 $border-radius $border-radius;
- box-sizing: border-box;
.collectionStackingView-docView-container {
width: 45%;
@@ -29,13 +16,26 @@
align-items: center;
}
- .collectionStackingView-masonrySingle, .collectionStackingView-masonryGrid{
+ .collectionStackingView-masonrySingle, .collectionStackingView-masonryGrid {
width:100%;
height:100%;
position: absolute;
- }
- .collectionStackingView-masonryGrid {
display:grid;
+ top: 0;
+ left: 0;
+ width: 100%;
+ position: absolute;
+ }
+ .collectionStackingView-masonrySingle {
+ width:100%;
+ height:100%;
+ position: absolute;
+ display:flex;
+ flex-direction: column;
+ top: 0;
+ left: 0;
+ width: 100%;
+ position: absolute;
}
.collectionStackingView-description {
@@ -48,4 +48,24 @@
background: $dark-color;
color: $light-color;
}
+
+ .collectionStackingView-columnDragger {
+ width: 15;
+ height: 15;
+ position: absolute;
+ margin-left: -5;
+ }
+
+
+ .collectionStackingView-columnDoc,
+ .collectionStackingView-masonryDoc {
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ .collectionStackingView-masonryDoc {
+ transform-origin: top left;
+ grid-column-end: span 1;
+ height: 100%;
+ }
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index da7ea50c6..416c536f6 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -1,40 +1,44 @@
import React = require("react");
-import { action, computed, IReactionDisposer, reaction } from "mobx";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, computed, IReactionDisposer, reaction, untracked } from "mobx";
import { observer } from "mobx-react";
import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
-import { BoolCast, NumCast } from "../../../new_fields/Types";
-import { emptyFunction, returnOne, Utils } from "../../../Utils";
-import { SelectionManager } from "../../util/SelectionManager";
-import { undoBatch } from "../../util/UndoManager";
-import { DocumentView } from "../nodes/DocumentView";
+import { BoolCast, NumCast, Cast } from "../../../new_fields/Types";
+import { emptyFunction, Utils } from "../../../Utils";
+import { ContextMenu } from "../ContextMenu";
import { CollectionSchemaPreview } from "./CollectionSchemaView";
import "./CollectionStackingView.scss";
import { CollectionSubView } from "./CollectionSubView";
+import { undoBatch } from "../../util/UndoManager";
+import { DragManager } from "../../util/DragManager";
@observer
export class CollectionStackingView extends CollectionSubView(doc => doc) {
_masonryGridRef: HTMLDivElement | null = null;
+ _draggerRef = React.createRef<HTMLDivElement>();
_heightDisposer?: IReactionDisposer;
- get gridGap() { return 10; }
- get gridSize() { return 20; }
- get singleColumn() { return BoolCast(this.props.Document.singleColumn, true); }
- get columnWidth() { return this.singleColumn ? this.props.PanelWidth() - 4 * this.gridGap : NumCast(this.props.Document.columnWidth, 250); }
+ _gridSize = 1;
+ @computed get xMargin() { return NumCast(this.props.Document.xMargin, 2 * this.gridGap); }
+ @computed get yMargin() { return NumCast(this.props.Document.yMargin, 2 * this.gridGap); }
+ @computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); }
+ @computed get singleColumn() { return BoolCast(this.props.Document.singleColumn, true); }
+ @computed get columnWidth() { return this.singleColumn ? (this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin) : Math.min(this.props.PanelWidth() - 2 * this.xMargin, NumCast(this.props.Document.columnWidth, 250)); }
+ @computed get filteredChildren() { return this.childDocs.filter(d => !d.isMinimized); }
+ singleColDocHeight(d: Doc) {
+ let nw = NumCast(d.nativeWidth);
+ let nh = NumCast(d.nativeHeight);
+ let aspect = nw && nh ? nh / nw : 1;
+ let wid = Math.min(d[WidthSym](), this.columnWidth);
+ return (nw && nh) ? wid * aspect : d[HeightSym]();
+ }
componentDidMount() {
- this._heightDisposer = reaction(() => [this.childDocs.map(d => [d.height, d.width, d.zoomBasis, d.nativeHeight, d.nativeWidth, d.isMinimized]), this.columnWidth, this.props.PanelHeight()],
- () => {
- if (this.singleColumn) {
- this.props.Document.height = this.childDocs.filter(d => !d.isMinimized).reduce((height, d) => {
- let hgt = d[HeightSym]();
- let wid = d[WidthSym]();
- let nw = NumCast(d.nativeWidth);
- let nh = NumCast(d.nativeHeight);
- if (nw && nh) hgt = nh / nw * Math.min(this.columnWidth, wid);
- return height + hgt + 2 * this.gridGap;
- }, this.gridGap * 2);
- }
- }, { fireImmediately: true });
+ this._heightDisposer = reaction(() => [this.yMargin, this.gridGap, this.columnWidth, this.childDocs.map(d => [d.height, d.width, d.zoomBasis, d.nativeHeight, d.nativeWidth, d.isMinimized])],
+ () => this.singleColumn &&
+ (this.props.Document.height = this.filteredChildren.reduce((height, d, i) =>
+ height + this.singleColDocHeight(d) + (i === this.filteredChildren.length - 1 ? this.yMargin : this.gridGap), this.yMargin))
+ , { fireImmediately: true });
}
componentWillUnmount() {
if (this._heightDisposer) this._heightDisposer();
@@ -42,136 +46,203 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
@action
moveDocument = (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean): boolean => {
- this.props.removeDocument(doc);
- addDocument(doc);
- return true;
+ return this.props.removeDocument(doc) && addDocument(doc);
}
- getDocTransform(doc: Doc, dref: HTMLDivElement) {
- let { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
+ getSingleDocTransform(doc: Doc, ind: number, width: number) {
+ let localY = this.filteredChildren.reduce((height, d, i) =>
+ height + (i < ind ? this.singleColDocHeight(d) + this.gridGap : 0), this.yMargin);
+ let translate = this.props.ScreenToLocalTransform().inverse().transformPoint((this.props.PanelWidth() - width) / 2, localY);
let outerXf = Utils.GetScreenTransform(this._masonryGridRef!);
- let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
+ let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translate[0], outerXf.translateY - translate[1]);
return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]).scale(NumCast(doc.width, 1) / this.columnWidth);
}
createRef = (ele: HTMLDivElement | null) => {
this._masonryGridRef = ele;
this.createDropTarget(ele!);
}
- @undoBatch
- @action
- public collapseToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => {
- SelectionManager.DeselectAll();
- if (expandedDocs) {
- let isMinimized: boolean | undefined;
- expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => {
- if (isMinimized === undefined) {
- isMinimized = BoolCast(maximizedDoc.isMinimized, false);
- }
- maximizedDoc.isMinimized = !isMinimized;
- });
- }
- }
@computed
get singleColumnChildren() {
- return this.childDocs.filter(d => !d.isMinimized).map((d, i) => {
- let dref = React.createRef<HTMLDivElement>();
- let script = undefined;
- let colWidth = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth;
- let margin = colWidth() < this.columnWidth ? "auto" : undefined;
- let rowHeight = () => {
- let hgt = d[HeightSym]();
- let nw = NumCast(d.nativeWidth);
- let nh = NumCast(d.nativeHeight);
- if (nw && nh) hgt = nh / nw * colWidth();
- return hgt;
- }
- let dxf = () => this.getDocTransform(d, dref.current!).scale(this.columnWidth / d[WidthSym]());
- return <div className="collectionStackingView-masonryDoc"
+ return this.filteredChildren.map((d, i) => {
+ let layoutDoc = Doc.expandTemplateLayout(d, this.props.DataDoc);
+ let width = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth;
+ let height = () => this.singleColDocHeight(layoutDoc);
+ let dxf = () => this.getSingleDocTransform(layoutDoc, i, width()).scale(this.columnWidth / d[WidthSym]());
+ let gap = i === 0 ? 0 : this.gridGap;
+ return <div className="collectionStackingView-columnDoc"
key={d[Id]}
- ref={dref}
- style={{ marginTop: `${i ? 2 * this.gridGap : 0}px`, width: colWidth(), height: rowHeight(), marginLeft: margin, marginRight: margin }} >
+ style={{ width: width(), display: "inline-block", marginTop: gap, height: `${height() / (this.props.Document[HeightSym]() - 2 * this.yMargin) * 100}%` }} >
<CollectionSchemaPreview
- Document={d}
- width={colWidth}
- height={rowHeight}
+ Document={layoutDoc}
+ DataDocument={d !== this.props.DataDoc ? this.props.DataDoc : undefined}
+ renderDepth={this.props.renderDepth}
+ width={width}
+ height={height}
getTransform={dxf}
CollectionView={this.props.CollectionView}
addDocument={this.props.addDocument}
+ moveDocument={this.props.moveDocument}
removeDocument={this.props.removeDocument}
active={this.props.active}
whenActiveChanged={this.props.whenActiveChanged}
addDocTab={this.props.addDocTab}
setPreviewScript={emptyFunction}
- previewScript={script}>
+ previewScript={undefined}>
</CollectionSchemaPreview>
</div>;
});
}
+ getDocTransform(doc: Doc, dref: HTMLDivElement) {
+ let { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
+ let outerXf = Utils.GetScreenTransform(this._masonryGridRef!);
+ let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
+ return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]).scale(NumCast(doc.width, 1) / this.columnWidth);
+ }
+ docXfs: any[] = [];
@computed
get children() {
- return this.childDocs.filter(d => !d.isMinimized).map(d => {
+ this.docXfs.length = 0;
+ return this.filteredChildren.map((d, i) => {
+ let aspect = d.nativeHeight ? NumCast(d.nativeWidth) / NumCast(d.nativeHeight) : undefined;
let dref = React.createRef<HTMLDivElement>();
- let dxf = () => this.getDocTransform(d, dref.current!);
- let colSpan = Math.ceil(Math.min(d[WidthSym](), this.columnWidth + this.gridGap) / (this.gridSize + this.gridGap));
- let rowSpan = Math.ceil((this.columnWidth / d[WidthSym]() * d[HeightSym]() + this.gridGap) / (this.gridSize + this.gridGap));
- let childFocus = (doc: Doc) => {
- doc.libraryBrush = true;
- this.props.focus(this.props.Document); // just focus on this collection, not the underlying document because the API doesn't support adding an offset to focus on and we can't pan zoom our contents to be centered.
- }
+ let dxf = () => this.getDocTransform(d, dref.current!).scale(this.columnWidth / d[WidthSym]());
+ let width = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth;
+ let height = () => aspect ? width() / aspect : d[HeightSym]();
+ let rowSpan = Math.ceil((height() + this.gridGap) / (this._gridSize + this.gridGap));
+ this.docXfs.push({ dxf: dxf, width: width, height: height });
return (<div className="collectionStackingView-masonryDoc"
key={d[Id]}
ref={dref}
- style={{
- width: NumCast(d.nativeWidth, d[WidthSym]()),
- height: NumCast(d.nativeHeight, d[HeightSym]()),
- transformOrigin: "top left",
- gridRowEnd: `span ${rowSpan}`,
- gridColumnEnd: `span ${colSpan}`,
- transform: `scale(${this.columnWidth / NumCast(d.nativeWidth, d[WidthSym]())}, ${this.columnWidth / NumCast(d.nativeWidth, d[WidthSym]())})`
- }} >
- <DocumentView key={d[Id]} Document={d}
+ style={{ gridRowEnd: `span ${rowSpan}` }} >
+ <CollectionSchemaPreview
+ Document={d}
+ DataDocument={this.props.Document.layout instanceof Doc ? this.props.Document : this.props.DataDoc}
+ renderDepth={this.props.renderDepth}
+ CollectionView={this.props.CollectionView}
addDocument={this.props.addDocument}
+ moveDocument={this.props.moveDocument}
removeDocument={this.props.removeDocument}
- moveDocument={this.moveDocument}
- ContainingCollectionView={this.props.CollectionView}
- isTopMost={false}
- ScreenToLocalTransform={dxf}
- focus={childFocus}
- ContentScaling={returnOne}
- PanelWidth={d[WidthSym]}
- PanelHeight={d[HeightSym]}
- selectOnLoad={false}
- parentActive={this.props.active}
+ getTransform={dxf}
+ width={width}
+ height={height}
+ active={this.props.active}
addDocTab={this.props.addDocTab}
- bringToFront={emptyFunction}
whenActiveChanged={this.props.whenActiveChanged}
- collapseToPoint={this.collapseToPoint}
- />
+ setPreviewScript={emptyFunction}
+ previewScript={undefined}>
+ </CollectionSchemaPreview>
</div>);
- })
+ });
+ }
+
+ _columnStart: number = 0;
+ columnDividerDown = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ document.addEventListener("pointermove", this.onDividerMove);
+ document.addEventListener('pointerup', this.onDividerUp);
+ this._columnStart = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0];
+ }
+ @action
+ onDividerMove = (e: PointerEvent): void => {
+ let dragPos = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0];
+ let delta = dragPos - this._columnStart;
+ this._columnStart = dragPos;
+
+ this.props.Document.columnWidth = this.columnWidth + delta;
+ }
+
+ @action
+ onDividerUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onDividerMove);
+ document.removeEventListener('pointerup', this.onDividerUp);
+ }
+
+ @computed get columnDragger() {
+ return <div className="collectionStackingView-columnDragger" onPointerDown={this.columnDividerDown} ref={this._draggerRef} style={{ left: `${this.columnWidth + this.xMargin}px` }} >
+ <FontAwesomeIcon icon={"caret-down"} />
+ </div>;
+ }
+ onContextMenu = (e: React.MouseEvent): void => {
+ if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
+ ContextMenu.Instance.addItem({
+ description: "Toggle multi-column",
+ event: () => this.props.Document.singleColumn = !BoolCast(this.props.Document.singleColumn, true), icon: "file-pdf"
+ });
+ }
+ }
+
+ @undoBatch
+ @action
+ drop = (e: Event, de: DragManager.DropEvent) => {
+ let targInd = -1;
+ let where = [de.x, de.y];
+ if (de.data instanceof DragManager.DocumentDragData) {
+ this.docXfs.map((cd, i) => {
+ let pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap);
+ let pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height());
+ if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) {
+ targInd = i;
+ }
+ });
+ }
+ if (super.drop(e, de)) {
+ let newDoc = de.data.droppedDocuments[0];
+ let docs = this.childDocList;
+ if (docs) {
+ if (targInd === -1) targInd = docs.length;
+ else targInd = docs.indexOf(this.filteredChildren[targInd]);
+ let srcInd = docs.indexOf(newDoc);
+ docs.splice(srcInd, 1);
+ docs.splice(targInd > srcInd ? targInd - 1 : targInd, 0, newDoc);
+ }
+ }
+ return false;
+ }
+ @undoBatch
+ @action
+ onDrop = (e: React.DragEvent): void => {
+ let where = [e.clientX, e.clientY];
+ let targInd = -1;
+ this.docXfs.map((cd, i) => {
+ let pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap);
+ let pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height());
+ if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) {
+ targInd = i;
+ }
+ });
+ super.onDrop(e, {}, () => {
+ if (targInd !== -1) {
+ let newDoc = this.childDocs[this.childDocs.length - 1];
+ let docs = this.childDocList;
+ if (docs) {
+ docs.splice(docs.length - 1, 1);
+ docs.splice(targInd, 0, newDoc);
+ }
+ }
+ });
}
render() {
- let leftMargin = 2 * this.gridGap;
- let topMargin = 2 * this.gridGap;
- let itemCols = Math.ceil(this.columnWidth / (this.gridSize + this.gridGap));
- let cells = Math.floor((this.props.PanelWidth() - leftMargin) / (itemCols * (this.gridSize + this.gridGap)));
+ let cols = this.singleColumn ? 1 : Math.max(1, Math.min(this.filteredChildren.length,
+ Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap))));
+ let templatecols = "";
+ for (let i = 0; i < cols; i++) templatecols += `${this.columnWidth}px `;
return (
- <div className="collectionStackingView" style={{ height: "100%" }}
- ref={this.createRef} onWheel={(e: React.WheelEvent) => e.stopPropagation()}>
+ <div className="collectionStackingView" ref={this.createRef} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onWheel={(e: React.WheelEvent) => e.stopPropagation()} >
<div className={`collectionStackingView-masonry${this.singleColumn ? "Single" : "Grid"}`}
style={{
- padding: `${topMargin}px 0px 0px ${leftMargin}px`,
- width: this.singleColumn ? "100%" : `${cells * itemCols * (this.gridSize + this.gridGap) + leftMargin}`,
+ padding: this.singleColumn ? `${this.yMargin}px ${this.xMargin}px ${this.yMargin}px ${this.xMargin}px` : `${this.yMargin}px ${this.xMargin}px`,
+ margin: "auto",
+ width: this.singleColumn ? undefined : `${cols * (this.columnWidth + this.gridGap) + 2 * this.xMargin - this.gridGap}px`,
height: "100%",
- overflow: "hidden",
- marginRight: "auto",
position: "relative",
gridGap: this.gridGap,
- gridTemplateColumns: this.singleColumn ? undefined : `repeat(auto-fill, minmax(${this.gridSize}px,1fr))`,
- gridAutoRows: this.singleColumn ? undefined : `${this.gridSize}px`
+ gridTemplateColumns: this.singleColumn ? undefined : templatecols,
+ gridAutoRows: this.singleColumn ? undefined : `${this._gridSize}px`
}}
>
{this.singleColumn ? this.singleColumnChildren : this.children}
+ {this.singleColumn ? (null) : this.columnDragger}
</div>
</div>
);
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 36e276d13..a7614b605 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,23 +1,25 @@
-import { action, runInAction } from "mobx";
-import React = require("react");
-import { undoBatch, UndoManager } from "../../util/UndoManager";
-import { DragManager } from "../../util/DragManager";
-import { Docs, DocumentOptions } from "../../documents/Documents";
-import { RouteStore } from "../../../server/RouteStore";
+import { action, computed } from "mobx";
+import * as rp from 'request-promise';
+import CursorField from "../../../new_fields/CursorField";
+import { Doc, DocListCast, Opt } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { List } from "../../../new_fields/List";
+import { listSpec } from "../../../new_fields/Schema";
+import { BoolCast, Cast } from "../../../new_fields/Types";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { RouteStore } from "../../../server/RouteStore";
+import { DocServer } from "../../DocServer";
+import { Docs, DocumentOptions } from "../../documents/Documents";
+import { DragManager } from "../../util/DragManager";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
+import { DocComponent } from "../DocComponent";
import { FieldViewProps } from "../nodes/FieldView";
-import * as rp from 'request-promise';
-import { CollectionView } from "./CollectionView";
+import { FormattedTextBox } from "../nodes/FormattedTextBox";
import { CollectionPDFView } from "./CollectionPDFView";
import { CollectionVideoView } from "./CollectionVideoView";
-import { Doc, Opt, FieldResult, DocListCast } from "../../../new_fields/Doc";
-import { DocComponent } from "../DocComponent";
-import { listSpec } from "../../../new_fields/Schema";
-import { Cast, PromiseValue, FieldValue, ListSpec } from "../../../new_fields/Types";
-import { List } from "../../../new_fields/List";
-import { DocServer } from "../../DocServer";
-import CursorField from "../../../new_fields/CursorField";
-import { DocumentManager } from "../../util/DocumentManager";
+import { CollectionView } from "./CollectionView";
+import React = require("react");
+import { MainView } from "../MainView";
export interface CollectionViewProps extends FieldViewProps {
addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
@@ -35,9 +37,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) {
private dropDisposer?: DragManager.DragDropDisposer;
protected createDropTarget = (ele: HTMLDivElement) => {
- if (this.dropDisposer) {
- this.dropDisposer();
- }
+ this.dropDisposer && this.dropDisposer();
if (ele) {
this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
}
@@ -46,10 +46,18 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
this.createDropTarget(ele);
}
+ @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); }
+
get childDocs() {
+ let self = this;
+ //TODO tfs: This might not be what we want?
+ //This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue)
+ return DocListCast((BoolCast(this.props.Document.isTemplate) ? this.extensionDoc : this.props.Document)[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey]);
+ }
+ get childDocList() {
//TODO tfs: This might not be what we want?
//This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue)
- return DocListCast(this.props.Document[this.props.fieldKey]);
+ return Cast((BoolCast(this.props.Document.isTemplate) ? this.extensionDoc : this.props.Document)[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey], listSpec(Doc));
}
@action
@@ -60,10 +68,17 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
let email = CurrentUserUtils.email;
let pos = { x: position[0], y: position[1] };
if (id && email) {
- const proto = await doc.proto;
+ const proto = Doc.GetProto(doc);
if (!proto) {
return;
}
+ // The following conditional detects a recurring bug we've seen on the server
+ if (proto[Id] === "collectionProto") {
+ alert("COLLECTION PROTO CURSOR ISSUE DETECTED! Check console for more info...");
+ console.log(doc);
+ console.log(proto);
+ throw new Error(`AHA! You were trying to set a cursor on a collection's proto, which is the original collection proto! Look at the two previously printed lines for document values!`);
+ }
let cursors = Cast(proto.cursors, listSpec(CursorField));
if (!cursors) {
proto.cursors = cursors = new List<CursorField>();
@@ -81,79 +96,29 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
@action
protected drop(e: Event, de: DragManager.DropEvent): boolean {
if (de.data instanceof DragManager.DocumentDragData) {
- if (de.data.dropAction || de.data.userDropAction) {
- ["width", "height", "curPage"].map(key =>
- de.data.draggedDocuments.map((draggedDocument: Doc, i: number) =>
- PromiseValue(Cast(draggedDocument[key], "number")).then(f => f && (de.data.droppedDocuments[i][key] = f))));
- }
let added = false;
if (de.data.dropAction || de.data.userDropAction) {
- added = de.data.droppedDocuments.reduce((added: boolean, d) => {
- let moved = this.props.addDocument(d);
- return moved || added;
- }, false);
+ added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false);
} else if (de.data.moveDocument) {
- const move = de.data.moveDocument;
- added = de.data.droppedDocuments.reduce((added: boolean, d) => {
- let moved = move(d, this.props.Document, this.props.addDocument);
- return moved || added;
- }, false);
+ let movedDocs = de.data.options === this.props.Document[Id] ? de.data.draggedDocuments : de.data.droppedDocuments;
+ added = movedDocs.reduce((added: boolean, d) =>
+ de.data.moveDocument(d, this.props.Document, this.props.addDocument) || added, false);
} else {
- added = de.data.droppedDocuments.reduce((added: boolean, d) => {
- let moved = this.props.addDocument(d);
- return moved || added;
- }, false);
+ added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false);
}
e.stopPropagation();
return added;
}
- return false;
- }
-
- protected async getDocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> {
- let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined;
- if (type.indexOf("image") !== -1) {
- ctor = Docs.Create.ImageDocument;
- }
- if (type.indexOf("video") !== -1) {
- ctor = Docs.Create.VideoDocument;
- }
- if (type.indexOf("audio") !== -1) {
- ctor = Docs.Create.AudioDocument;
- }
- if (type.indexOf("pdf") !== -1) {
- ctor = Docs.Create.PdfDocument;
- options.nativeWidth = 1200;
- }
- if (type.indexOf("excel") !== -1) {
- ctor = Docs.Create.DBDocument;
- options.dropAction = "copy";
- }
- if (type.indexOf("html") !== -1) {
- if (path.includes(window.location.hostname)) {
- let s = path.split('/');
- let id = s[s.length - 1];
- DocServer.getRefField(id).then(field => {
- if (field instanceof Doc) {
- let alias = Doc.MakeAlias(field);
- alias.x = options.x || 0;
- alias.y = options.y || 0;
- alias.width = options.width || 300;
- alias.height = options.height || options.width || 300;
- this.props.addDocument(alias, false);
- }
- });
- return undefined;
- }
- ctor = Docs.Create.WebDocument;
- options = { height: options.width, ...options, title: path, nativeWidth: undefined };
+ else if (de.data instanceof DragManager.AnnotationDragData) {
+ e.stopPropagation();
+ return this.props.addDocument(de.data.dropDocument);
}
- return ctor ? ctor(path, options) : undefined;
+ return false;
}
@undoBatch
@action
- protected onDrop(e: React.DragEvent, options: DocumentOptions): void {
+ protected onDrop(e: React.DragEvent, options: DocumentOptions, completed?: () => void) {
if (e.ctrlKey) {
e.stopPropagation(); // bcz: this is a hack to stop propagation when dropping an image on a text document with shift+ctrl
return;
@@ -167,17 +132,50 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
e.stopPropagation();
e.preventDefault();
- if (html && html.indexOf(document.location.origin)) { // prosemirror text containing link to dash document
- let start = html.indexOf(window.location.origin);
- let path = html.substr(start, html.length - start);
- let docid = path.substr(0, path.indexOf("\">")).replace(DocServer.Util.prepend("/doc/"), "").split("?")[0];
- DocServer.getRefField(docid).then(f => (f instanceof Doc) && this.props.addDocument(f, false));
+ if (html && FormattedTextBox.IsFragment(html)) {
+ let href = FormattedTextBox.GetHref(html);
+ if (href) {
+ let docid = FormattedTextBox.GetDocFromUrl(href);
+ if (docid) { // prosemirror text containing link to dash document
+ DocServer.GetRefField(docid).then(f => {
+ if (f instanceof Doc) {
+ if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView
+ (f instanceof Doc) && this.props.addDocument(f, false);
+ }
+ });
+ } else {
+ this.props.addDocument && this.props.addDocument(Docs.Create.WebDocument(href, options));
+ }
+ } else if (text) {
+ this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument({ ...options, width: 100, height: 25, documentText: "@@@" + text }), false);
+ }
return;
}
- if (html && html.indexOf("<img") !== 0 && !html.startsWith("<a")) {
- let htmlDoc = Docs.Create.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text });
- this.props.addDocument(htmlDoc, false);
- return;
+ if (html && !html.startsWith("<a")) {
+ let tags = html.split("<");
+ if (tags[0] === "") tags.splice(0, 1);
+ let img = tags[0].startsWith("img") ? tags[0] : tags.length > 1 && tags[1].startsWith("img") ? tags[1] : "";
+ if (img) {
+ let split = img.split("src=\"")[1].split("\"")[0];
+ let doc = Docs.Create.ImageDocument(split, { ...options, width: 300 });
+ this.props.addDocument(doc, false);
+ return;
+ } else {
+ let path = window.location.origin + "/doc/";
+ if (text.startsWith(path)) {
+ let docid = text.replace(DocServer.prepend("/doc/"), "").split("?")[0];
+ DocServer.GetRefField(docid).then(f => {
+ if (f instanceof Doc) {
+ if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView
+ (f instanceof Doc) && this.props.addDocument(f, false);
+ }
+ });
+ } else {
+ let htmlDoc = Docs.Create.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text });
+ this.props.addDocument(htmlDoc, false);
+ }
+ return;
+ }
}
if (text && text.indexOf("www.youtube.com/watch") !== -1) {
const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/");
@@ -194,11 +192,11 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
if (item.kind === "string" && item.type.indexOf("uri") !== -1) {
let str: string;
let prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve))
- .then(action((s: string) => rp.head(DocServer.Util.prepend(RouteStore.corsProxy + "/" + (str = s)))))
+ .then(action((s: string) => rp.head(DocServer.prepend(RouteStore.corsProxy + "/" + (str = s)))))
.then(result => {
let type = result["content-type"];
if (type) {
- this.getDocumentFromType(type, str, { ...options, width: 300, nativeWidth: 300 })
+ Docs.Get.DocumentFromType(type, str, { ...options, width: 300, nativeWidth: 300 })
.then(doc => doc && this.props.addDocument(doc, false));
}
});
@@ -219,10 +217,9 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
body: formData
}).then(async (res: Response) => {
(await res.json()).map(action((file: any) => {
- let path = window.location.origin + file;
- let docPromise = this.getDocumentFromType(type, path, { ...options, nativeWidth: 300, width: 300, title: dropFileName });
-
- docPromise.then(doc => doc && this.props.addDocument(doc));
+ let full = { ...options, nativeWidth: 300, width: 300, title: dropFileName };
+ let path = DocServer.prepend(file);
+ Docs.Get.DocumentFromType(type, path, full).then(doc => doc && this.props.addDocument(doc));
}));
});
promises.push(prom);
@@ -230,7 +227,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
}
if (promises.length) {
- Promise.all(promises).finally(() => batch.end());
+ Promise.all(promises).finally(() => { completed && completed(); batch.end(); });
} else {
batch.end();
}
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index 458030b28..5205f4313 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -4,60 +4,33 @@
border-width: $COLLECTION_BORDER_WIDTH;
border-color: transparent;
border-style: solid;
- border-radius: $border-radius;
+ border-radius: inherit;
box-sizing: border-box;
height: 100%;
- padding: 20px;
+ padding-top: 20px;
padding-left: 10px;
padding-right: 0px;
background: $light-color-secondary;
font-size: 13px;
- overflow: scroll;
+ overflow: auto;
ul {
list-style: none;
padding-left: 20px;
}
- li {
- margin: 5px 0;
- }
-
.no-indent {
padding-left: 0;
}
.bullet {
- float: left;
position: relative;
width: 15px;
- display: block;
color: $intermediate-color;
- margin-top: 3px;
+ margin-top: 4px;
transform: scale(1.3, 1.3);
}
-
- .docContainer {
- margin-left: 10px;
- display: block;
- // width:100%;//width: max-content;
- }
-
- .docContainer:hover {
- .treeViewItem-openRight {
- display: inline-block;
- height:13px;
- // display: inline;
- svg {
- display:block;
- padding:0px;
- margin: 0px;
- }
- }
- }
-
-
.editableView-container {
font-weight: bold;
}
@@ -70,31 +43,72 @@
display: inline;
}
- .treeViewItem-openRight {
- margin-left: 5px;
- display: none;
- }
-
- .docContainer:hover {
- .delete-button {
- display: inline;
- // width: auto;
- }
- }
-
- .coll-title {
- width: max-content;
+ .editableView-input, .editableView-container-editing {
display: block;
+ text-overflow: ellipsis;
font-size: 24px;
+ white-space: nowrap;
}
-
- .collection-child {
- margin-top: 10px;
- margin-bottom: 10px;
- }
-
+}
+.collectionTreeView-keyHeader {
+ font-style: italic;
+ font-size: 8pt;
+ margin-left: 3px;
+ display:none;
+ background: lightgray;
+}
+
+.collectionTreeView-subtitle {
+ font-style: italic;
+ font-size: 8pt;
+ color: $intermediate-color;
+}
+
+.docContainer {
+ position: relative;
+ text-overflow: ellipsis;
+ white-space: pre-wrap;
+ overflow: hidden;
+ // width:100%;//width: max-content;
+
+}
+.treeViewItem-openRight {
+ display: none;
+}
+
+.treeViewItem-border {
+ display:inherit;
+ border-left: dashed 1px #00000042;
+}
+
+.treeViewItem-header:hover {
.collectionTreeView-keyHeader {
- font-style: italic;
- font-size: 8pt;
+ display:inherit;
+ }
+ .treeViewItem-openRight {
+ display: inline-block;
+ height:13px;
+ margin-top:2px;
+ margin-left: 5px;
+ // display: inline;
+ svg {
+ display:block;
+ padding:0px;
+ margin: 0px;
+ }
}
+}
+
+.treeViewItem-header {
+ border: transparent 1px solid;
+ display:flex;
+}
+.treeViewItem-header-above {
+ border-top: black 1px solid;
+}
+.treeViewItem-header-below {
+ border-bottom: black 1px solid;
+}
+.treeViewItem-header-inside {
+ border: black 1px solid;
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 424042fee..ef340d770 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,251 +1,558 @@
-import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
-import { faCaretDown, faCaretRight, faTrashAlt, faAngleRight } from '@fortawesome/free-solid-svg-icons';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faAngleRight, faCamera, faExpand, faTrash, faBell, faCaretDown, faCaretRight, faCaretSquareDown, faCaretSquareRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, observable, trace } from "mobx";
+import { action, computed, observable, trace } from "mobx";
import { observer } from "mobx-react";
-import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager";
-import { EditableView } from "../EditableView";
-import { CollectionSubView } from "./CollectionSubView";
-import "./CollectionTreeView.scss";
-import React = require("react");
-import { Document, listSpec } from '../../../new_fields/Schema';
-import { Cast, StrCast, BoolCast, FieldValue, NumCast } from '../../../new_fields/Types';
-import { Doc, DocListCast } from '../../../new_fields/Doc';
+import { Doc, DocListCast, HeightSym, WidthSym, Opt } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
-import { ContextMenu } from '../ContextMenu';
-import { undoBatch } from '../../util/UndoManager';
-import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
-import { CollectionDockingView } from './CollectionDockingView';
+import { List } from '../../../new_fields/List';
+import { Document, listSpec } from '../../../new_fields/Schema';
+import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types';
+import { emptyFunction, Utils } from '../../../Utils';
+import { Docs, DocUtils, DocTypes } from '../../documents/Documents';
import { DocumentManager } from '../../util/DocumentManager';
-import { Docs } from '../../documents/Documents';
+import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager";
+import { SelectionManager } from '../../util/SelectionManager';
+import { Transform } from '../../util/Transform';
+import { undoBatch } from '../../util/UndoManager';
+import { ContextMenu } from '../ContextMenu';
+import { EditableView } from "../EditableView";
import { MainView } from '../MainView';
+import { Templates } from '../Templates';
import { CollectionViewType } from './CollectionBaseView';
+import { CollectionDockingView } from './CollectionDockingView';
+import { CollectionSchemaPreview } from './CollectionSchemaView';
+import { CollectionSubView } from "./CollectionSubView";
+import "./CollectionTreeView.scss";
+import React = require("react");
+import { LinkManager } from '../../util/LinkManager';
export interface TreeViewProps {
document: Doc;
- deleteDoc: (doc: Doc) => void;
+ dataDoc?: Doc;
+ containingCollection: Doc;
+ renderDepth: number;
+ deleteDoc: (doc: Doc) => boolean;
moveDocument: DragManager.MoveFunction;
dropAction: "alias" | "copy" | undefined;
- addDocTab: (doc: Doc, where: string) => void;
-}
-
-export enum BulletType {
- Collapsed,
- Collapsible,
- List
+ addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void;
+ panelWidth: () => number;
+ panelHeight: () => number;
+ addDocument: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean;
+ indentDocument?: () => void;
+ ScreenToLocalTransform: () => Transform;
+ outerXf: () => { translateX: number, translateY: number };
+ treeViewId: string;
+ parentKey: string;
+ active: () => boolean;
}
library.add(faTrashAlt);
library.add(faAngleRight);
+library.add(faBell);
+library.add(faTrash);
+library.add(faCamera);
+library.add(faExpand);
library.add(faCaretDown);
library.add(faCaretRight);
+library.add(faCaretSquareDown);
+library.add(faCaretSquareRight);
@observer
/**
* Component that takes in a document prop and a boolean whether it's collapsed or not.
*/
class TreeView extends React.Component<TreeViewProps> {
-
+ private _header?: React.RefObject<HTMLDivElement> = React.createRef();
+ private _treedropDisposer?: DragManager.DragDropDisposer;
+ private _dref = React.createRef<HTMLDivElement>();
+ @observable __chosenKey: string = "";
+ @computed get _chosenKey() { return this.__chosenKey ? this.__chosenKey : this.fieldKey; }
+ @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.document.maxEmbedHeight, 300); }
@observable _collapsed: boolean = true;
- @undoBatch delete = () => this.props.deleteDoc(this.props.document);
-
- @undoBatch openRight = async () => {
- if (this.props.document.dockingConfig) {
- MainView.Instance.openWorkspace(this.props.document);
- } else {
- this.props.addDocTab(this.props.document, "openRight");
+ @computed get fieldKey() {
+ let keys = Array.from(Object.keys(this.resolvedDataDoc));
+ if (this.resolvedDataDoc.proto instanceof Doc) {
+ keys.push(...Array.from(Object.keys(this.resolvedDataDoc.proto)));
+ while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1);
+ }
+ let keyList: string[] = [];
+ keys.map(key => {
+ let docList = Cast(this.resolvedDataDoc[key], listSpec(Doc));
+ if (docList && docList.length > 0) {
+ keyList.push(key);
+ }
+ });
+ let layout = StrCast(this.props.document.layout);
+ if (layout.indexOf("fieldKey={\"") !== -1) {
+ return layout.split("fieldKey={\"")[1].split("\"")[0];
}
+ return keyList.length ? keyList[0] : "data";
}
- get children() {
- return Cast(this.props.document.data, listSpec(Doc), []); // bcz: needed? .filter(doc => FieldValue(doc));
+ @computed get resolvedDataDoc() { return BoolCast(this.props.document.isTemplate) && this.props.dataDoc ? this.props.dataDoc : this.props.document; }
+
+ protected createTreeDropTarget = (ele: HTMLDivElement) => {
+ this._treedropDisposer && this._treedropDisposer();
+ if (ele) {
+ this._treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.treeDrop.bind(this) } });
+ }
}
- onPointerDown = (e: React.PointerEvent) => {
+ @undoBatch delete = () => this.props.deleteDoc(this.resolvedDataDoc);
+ @undoBatch openRight = async () => this.props.addDocTab(this.props.document, undefined, "onRight");
+
+ onPointerDown = (e: React.PointerEvent) => e.stopPropagation();
+ onPointerEnter = (e: React.PointerEvent): void => {
+ this.props.active() && (this.props.document.libraryBrush = true);
+ if (e.buttons === 1 && SelectionManager.GetIsDragging()) {
+ this._header!.current!.className = "treeViewItem-header";
+ document.addEventListener("pointermove", this.onDragMove, true);
+ }
+ }
+ onPointerLeave = (e: React.PointerEvent): void => {
+ this.props.document.libraryBrush = false;
+ this._header!.current!.className = "treeViewItem-header";
+ document.removeEventListener("pointermove", this.onDragMove, true);
+ }
+ onDragMove = (e: PointerEvent): void => {
+ this.props.document.libraryBrush = false;
+ let x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY);
+ let rect = this._header!.current!.getBoundingClientRect();
+ let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
+ let before = x[1] < bounds[1];
+ let inside = x[0] > bounds[0] + 75 || (!before && !this._collapsed);
+ this._header!.current!.className = "treeViewItem-header";
+ if (inside) this._header!.current!.className += " treeViewItem-header-inside";
+ else if (before) this._header!.current!.className += " treeViewItem-header-above";
+ else if (!before) this._header!.current!.className += " treeViewItem-header-below";
e.stopPropagation();
}
@action
- remove = (document: Document, key: string) => {
- let children = Cast(this.props.document[key], listSpec(Doc), []);
- if (children) {
+ remove = (document: Document, key: string): boolean => {
+ let children = Cast(this.resolvedDataDoc[key], listSpec(Doc), []);
+ if (children.indexOf(document) !== -1) {
children.splice(children.indexOf(document), 1);
+ return true;
}
+ return false;
}
@action
- move: DragManager.MoveFunction = (document, target, addDoc) => {
- if (this.props.document === target) {
- return true;
- }
- //TODO This should check if it was removed
- this.remove(document, "data");
- return addDoc(document);
+ move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => {
+ return this.props.document !== target && this.props.deleteDoc(doc) && addDoc(doc);
}
+ @action
+ indent = () => this.props.addDocument(this.props.document) && this.delete()
- renderBullet(type: BulletType) {
- let onClicked = action(() => this._collapsed = !this._collapsed);
- let bullet: IconProp | undefined = undefined;
- switch (type) {
- case BulletType.Collapsed: bullet = "caret-right"; break;
- case BulletType.Collapsible: bullet = "caret-down"; break;
- }
- return <div className="bullet" onClick={onClicked}>{bullet ? <FontAwesomeIcon icon={bullet} /> : ""} </div>;
+ renderBullet() {
+ let docList = Cast(this.resolvedDataDoc[this.fieldKey], listSpec(Doc));
+ let doc = Cast(this.resolvedDataDoc[this.fieldKey], Doc);
+ let isDoc = doc instanceof Doc || docList;
+ return <div className="bullet" onClick={action(() => this._collapsed = !this._collapsed)}>
+ {<FontAwesomeIcon icon={this._collapsed ? (isDoc ? "caret-square-right" : "caret-right") : (isDoc ? "caret-square-down" : "caret-down")} />}
+ </div>;
}
- @action
- onMouseEnter = () => {
- this._isOver = true;
- }
- @observable _isOver: boolean = false;
- @action
- onMouseLeave = () => {
- this._isOver = false;
+ static loadId = "";
+ editableView = (key: string, style?: string) => (<EditableView
+ oneLine={true}
+ display={"inline"}
+ editing={this.resolvedDataDoc[Id] === TreeView.loadId}
+ contents={StrCast(this.props.document[key])}
+ height={36}
+ fontStyle={style}
+ fontSize={12}
+ GetValue={() => StrCast(this.props.document[key])}
+ SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc)[key] = value) ? true : true}
+ OnFillDown={(value: string) => {
+ Doc.GetProto(this.resolvedDataDoc)[key] = value;
+ let doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
+ TreeView.loadId = doc[Id];
+ return this.props.addDocument(doc);
+ }}
+ OnTab={() => this.props.indentDocument && this.props.indentDocument()}
+ />)
+
+ @computed get keyList() {
+ let keys = Array.from(Object.keys(this.resolvedDataDoc));
+ if (this.resolvedDataDoc.proto instanceof Doc) {
+ keys.push(...Array.from(Object.keys(this.resolvedDataDoc.proto)));
+ }
+ let keyList: string[] = keys.reduce((l, key) => {
+ let listspec = DocListCast(this.resolvedDataDoc[key]);
+ if (listspec && listspec.length) return [...l, key];
+ return l;
+ }, [] as string[]);
+ keys.map(key => Cast(this.resolvedDataDoc[key], Doc) instanceof Doc && keyList.push(key));
+ if (LinkManager.Instance.getAllRelatedLinks(this.props.document).length > 0) keyList.push("links");
+ if (keyList.indexOf(this.fieldKey) !== -1) {
+ keyList.splice(keyList.indexOf(this.fieldKey), 1);
+ }
+ keyList.splice(0, 0, this.fieldKey);
+ return keyList.filter((item, index) => keyList.indexOf(item) >= index);
}
/**
* Renders the EditableView title element for placement into the tree.
*/
renderTitle() {
let reference = React.createRef<HTMLDivElement>();
- let onItemDown = SetupDrag(reference, () => this.props.document, this.props.moveDocument, this.props.dropAction);
- let editableView = (titleString: string) =>
- (<EditableView
- oneLine={!this._isOver ? true : false}
- display={"inline"}
- contents={titleString}
- height={36}
- GetValue={() => StrCast(this.props.document.title)}
- SetValue={(value: string) => {
- let target = this.props.document.proto ? this.props.document.proto : this.props.document;
- target.title = value;
- return true;
- }}
- />);
- let dataDocs = CollectionDockingView.TopLevel ? Cast(CollectionDockingView.TopLevel.props.Document.data, listSpec(Doc), []) : [];
- let openRight = dataDocs && dataDocs.indexOf(this.props.document) !== -1 ? (null) : (
+ let onItemDown = SetupDrag(reference, () => this.resolvedDataDoc, this.move, this.props.dropAction, this.props.treeViewId, true);
+
+ let headerElements = (
+ <span className="collectionTreeView-keyHeader" key={this._chosenKey}
+ onPointerDown={action(() => {
+ let ind = this.keyList.indexOf(this._chosenKey);
+ ind = (ind + 1) % this.keyList.length;
+ this.__chosenKey = this.keyList[ind];
+ })} >
+ {this._chosenKey}
+ </span>);
+ let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document[this.fieldKey], listSpec(Doc), []) : [];
+ let openRight = dataDocs && dataDocs.indexOf(this.resolvedDataDoc) !== -1 ? (null) : (
<div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}>
<FontAwesomeIcon icon="angle-right" size="lg" />
- {/* <FontAwesomeIcon icon="angle-right" size="lg" /> */}
</div>);
- return (
- <div className="docContainer" ref={reference} onPointerDown={onItemDown} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}
- style={{ background: BoolCast(this.props.document.protoBrush, false) ? "#06123232" : BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0" }}
- onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
- {editableView(StrCast(this.props.document.title))}
- {openRight}
+ return <>
+ <div className="docContainer" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown}
+ style={{
+ background: BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0",
+ outline: BoolCast(this.props.document.workspaceBrush, false) ? "dashed 1px #06123232" : undefined,
+ pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? "all" : "none"
+ }}
+ >
+ {this.editableView("title")}
{/* {<div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div>} */}
- </div >);
+ </div >
+ {headerElements}
+ {openRight}
+ </>;
}
onWorkspaceContextMenu = (e: React.MouseEvent): void => {
if (!e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
- ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.props.document)) });
- ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }), "onRight"), icon: "layer-group" });
+ ContextMenu.Instance.addItem({ description: (BoolCast(this.props.document.embed) ? "Collapse" : "Expand") + " inline", event: () => this.props.document.embed = !BoolCast(this.props.document.embed), icon: "expand" });
if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking) {
- ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, "inTab"), icon: "folder" });
- ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, "onRight"), icon: "caret-square-right" });
- if (DocumentManager.Instance.getDocumentViews(this.props.document).length) {
- ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document)) });
+ ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "inTab"), icon: "folder" });
+ ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "onRight"), icon: "caret-square-right" });
+ if (DocumentManager.Instance.getDocumentViews(this.resolvedDataDoc).length) {
+ ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.resolvedDataDoc).map(view => view.props.focus(this.props.document, true)), icon: "camera" });
}
- ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)) });
+ ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)), icon: "trash-alt" });
} else {
- ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.props.document)) });
+ ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.resolvedDataDoc)), icon: "caret-square-right" });
+ ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.props.document)), icon: "trash-alt" });
}
- ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
+ ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" });
+ ContextMenu.Instance.displayMenu(e.pageX > 156 ? e.pageX - 156 : 0, e.pageY - 15);
e.stopPropagation();
+ e.preventDefault();
}
}
- onPointerEnter = (e: React.PointerEvent): void => { this.props.document.libraryBrush = true; };
- onPointerLeave = (e: React.PointerEvent): void => { this.props.document.libraryBrush = false; };
-
- render() {
- let bulletType = BulletType.List;
- let contentElement: (JSX.Element | null)[] = [];
- let keys = Array.from(Object.keys(this.props.document));
- if (this.props.document.proto instanceof Doc) {
- keys.push(...Array.from(Object.keys(this.props.document.proto)));
- while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1);
+ @undoBatch
+ treeDrop = (e: Event, de: DragManager.DropEvent) => {
+ let x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
+ let rect = this._header!.current!.getBoundingClientRect();
+ let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
+ let before = x[1] < bounds[1];
+ let inside = x[0] > bounds[0] + 75 || (!before && !this._collapsed);
+ if (de.data instanceof DragManager.LinkDragData) {
+ let sourceDoc = de.data.linkSourceDocument;
+ let destDoc = this.props.document;
+ DocUtils.MakeLink(sourceDoc, destDoc);
+ e.stopPropagation();
}
- keys.map(key => {
- let docList = DocListCast(this.props.document[key]);
- let doc = Cast(this.props.document[key], Doc);
- if (doc instanceof Doc || docList.length) {
- if (!this._collapsed) {
- bulletType = BulletType.Collapsible;
- let spacing = (key === "data") ? 0 : -10;
- contentElement.push(<ul key={key + "more"}>
- {(key === "data") ? (null) :
- <span className="collectionTreeView-keyHeader" style={{ display: "block", marginTop: "7px" }} key={key}>{key}</span>}
- <div style={{ display: "block", marginTop: `${spacing}px` }}>
- {TreeView.GetChildElements(doc instanceof Doc ? [doc] : docList, key !== "data", (doc: Doc) => this.remove(doc, key), this.move, this.props.dropAction, this.props.addDocTab)}
- </div>
- </ul >);
- } else {
- bulletType = BulletType.Collapsed;
+ if (de.data instanceof DragManager.DocumentDragData) {
+ e.stopPropagation();
+ if (de.data.draggedDocuments[0] === this.props.document) return true;
+ let addDoc = (doc: Doc) => this.props.addDocument(doc, this.resolvedDataDoc, before);
+ if (inside) {
+ let docList = Cast(this.resolvedDataDoc.data, listSpec(Doc));
+ if (docList !== undefined) {
+ addDoc = (doc: Doc) => { docList && docList.push(doc); return true; };
}
}
+ let movedDocs = (de.data.options === this.props.treeViewId ? de.data.draggedDocuments : de.data.droppedDocuments);
+ return (de.data.dropAction || de.data.userDropAction) ?
+ de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before) || added, false)
+ : (de.data.moveDocument) ?
+ movedDocs.reduce((added: boolean, d) => de.data.moveDocument(d, this.resolvedDataDoc, addDoc) || added, false)
+ : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before), false);
+ }
+ return false;
+ }
+
+ docTransform = () => {
+ let { scale, translateX, translateY } = Utils.GetScreenTransform(this._dref.current!);
+ let outerXf = this.props.outerXf();
+ let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
+ let finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1]);
+ return finalXf;
+ }
+
+ renderLinks = () => {
+ let ele: JSX.Element[] = [];
+ let remDoc = (doc: Doc) => this.remove(doc, this._chosenKey);
+ let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.props.document, this._chosenKey, doc, addBefore, before);
+ let groups = LinkManager.Instance.getRelatedGroupedLinks(this.props.document);
+ groups.forEach((groupLinkDocs, groupType) => {
+ let destLinks = groupLinkDocs.map(d => LinkManager.Instance.getOppositeAnchor(d, this.props.document));
+ ele.push(
+ <div key={"treeviewlink-" + groupType + "subtitle"}>
+ <div className="collectionTreeView-subtitle">{groupType}:</div>
+ {
+ TreeView.GetChildElements(destLinks, this.props.treeViewId, this.props.document, this.props.dataDoc, "treeviewlink-" + groupType, addDoc, remDoc, this.move,
+ this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth)
+ }
+ </div>
+ );
});
- return <div className="treeViewItem-container"
- onContextMenu={this.onWorkspaceContextMenu}>
+ return ele;
+ }
+
+ @computed get docBounds() {
+ if (StrCast(this.props.document.type).indexOf(DocTypes.COL) === -1) return undefined;
+ let layoutDoc = Doc.expandTemplateLayout(this.props.document, this.props.dataDoc);
+ return Doc.ComputeContentBounds(layoutDoc);
+ }
+ docWidth = () => {
+ let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth);
+ if (aspect) return Math.min(this.props.document[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 5));
+ return NumCast(this.props.document.nativeWidth) ? Math.min(this.props.document[WidthSym](), this.props.panelWidth() - 5) : this.props.panelWidth() - 5;
+ }
+ docHeight = () => {
+ let bounds = this.docBounds;
+ return Math.min(this.MAX_EMBED_HEIGHT, (() => {
+ let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth);
+ if (aspect) return this.docWidth() * aspect;
+ if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x);
+ return NumCast(this.props.document.height) ? NumCast(this.props.document.height) : 50;
+ })());
+ }
+ fitToBox = () => {
+ let bounds = this.docBounds!;
+ return [(bounds.x + bounds.r) / 2, (bounds.y + bounds.b) / 2, Math.min(this.docHeight() / (bounds.b - bounds.y), this.docWidth() / (bounds.r - bounds.x))];
+ }
+
+ render() {
+ let contentElement: (JSX.Element | null) = null;
+ let docList = Cast(this.resolvedDataDoc[this._chosenKey], listSpec(Doc));
+ let remDoc = (doc: Doc) => this.remove(doc, this._chosenKey);
+ let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.resolvedDataDoc, this._chosenKey, doc, addBefore, before);
+ let doc = Cast(this.resolvedDataDoc[this._chosenKey], Doc);
+
+ if (!this._collapsed) {
+ if (!this.props.document.embed) {
+ contentElement = <ul key={this._chosenKey + "more"}>
+ {this._chosenKey === "links" ? this.renderLinks() :
+ TreeView.GetChildElements(doc instanceof Doc ? [doc] : DocListCast(docList), this.props.treeViewId, this.props.document, this.props.dataDoc, this._chosenKey, addDoc, remDoc, this.move,
+ this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth)}
+ </ul >;
+ } else {
+ let layoutDoc = Doc.expandTemplateLayout(this.props.document, this.props.dataDoc);
+ contentElement = <div ref={this._dref} style={{ display: "inline-block", height: this.docHeight() }} key={this.props.document[Id]}>
+ <CollectionSchemaPreview
+ Document={layoutDoc}
+ DataDocument={this.resolvedDataDoc}
+ renderDepth={this.props.renderDepth}
+ fitToBox={this.docBounds && !NumCast(this.props.document.nativeWidth) ? this.fitToBox : undefined}
+ width={this.docWidth}
+ height={this.docHeight}
+ getTransform={this.docTransform}
+ CollectionView={undefined}
+ addDocument={emptyFunction as any}
+ moveDocument={this.props.moveDocument}
+ removeDocument={emptyFunction as any}
+ active={this.props.active}
+ whenActiveChanged={emptyFunction as any}
+ addDocTab={this.props.addDocTab}
+ setPreviewScript={emptyFunction}>
+ </CollectionSchemaPreview>
+ </div>;
+ }
+ }
+ return <div className="treeViewItem-container" ref={this.createTreeDropTarget} onContextMenu={this.onWorkspaceContextMenu}>
<li className="collection-child">
- {this.renderBullet(bulletType)}
- {this.renderTitle()}
- {contentElement}
+ <div className="treeViewItem-header" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
+ {this.renderBullet()}
+ {this.renderTitle()}
+ </div>
+ <div className="treeViewItem-border">
+ {contentElement}
+ </div>
</li>
</div>;
}
- public static GetChildElements(docs: Doc[], allowMinimized: boolean, remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType, addDocTab: (doc: Doc, where: string) => void) {
- return docs.filter(child => !child.excludeFromLibrary && (allowMinimized || !child.isMinimized)).map(child =>
- <TreeView document={child} key={child[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} addDocTab={addDocTab} />);
+ public static GetChildElements(
+ docs: Doc[],
+ treeViewId: string,
+ containingCollection: Doc,
+ dataDoc: Doc | undefined,
+ key: string,
+ add: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean,
+ remove: ((doc: Doc) => boolean),
+ move: DragManager.MoveFunction,
+ dropAction: dropActionType,
+ addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void,
+ screenToLocalXf: () => Transform,
+ outerXf: () => { translateX: number, translateY: number },
+ active: () => boolean,
+ panelWidth: () => number,
+ renderDepth: number
+ ) {
+ let docList = docs.filter(child => !child.excludeFromLibrary);
+ let rowWidth = () => panelWidth() - 20;
+ return docList.map((child, i) => {
+ let indent = i === 0 ? undefined : () => {
+ if (StrCast(docList[i - 1].layout).indexOf("CollectionView") !== -1) {
+ let fieldKeysub = StrCast(docList[i - 1].layout).split("fieldKey")[1];
+ let fieldKey = fieldKeysub.split("\"")[1];
+ Doc.AddDocToList(docList[i - 1], fieldKey, child);
+ remove(child);
+ }
+ };
+ let addDocument = (doc: Doc, relativeTo?: Doc, before?: boolean) => {
+ return add(doc, relativeTo ? relativeTo : docList[i], before !== undefined ? before : false);
+ };
+ let rowHeight = () => {
+ let aspect = NumCast(child.nativeWidth, 0) / NumCast(child.nativeHeight, 0);
+ return aspect ? Math.min(child[WidthSym](), rowWidth()) / aspect : child[HeightSym]();
+ };
+ return <TreeView
+ document={child}
+ dataDoc={dataDoc}
+ containingCollection={containingCollection}
+ treeViewId={treeViewId}
+ key={child[Id]}
+ indentDocument={indent}
+ renderDepth={renderDepth}
+ deleteDoc={remove}
+ addDocument={addDocument}
+ panelWidth={rowWidth}
+ panelHeight={rowHeight}
+ moveDocument={move}
+ dropAction={dropAction}
+ addDocTab={addDocTab}
+ ScreenToLocalTransform={screenToLocalXf}
+ outerXf={outerXf}
+ parentKey={key}
+ active={active} />;
+ });
}
}
@observer
export class CollectionTreeView extends CollectionSubView(Document) {
+ private treedropDisposer?: DragManager.DragDropDisposer;
+ private _mainEle?: HTMLDivElement;
+
+ protected createTreeDropTarget = (ele: HTMLDivElement) => {
+ this.treedropDisposer && this.treedropDisposer();
+ if (this._mainEle = ele) {
+ this.treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+ }
+ }
+
+ componentWillUnmount() {
+ this.treedropDisposer && this.treedropDisposer();
+ }
+
@action
- remove = (document: Document) => {
- let children = Cast(this.props.Document.data, listSpec(Doc), []);
- if (children) {
+ remove = (document: Document): boolean => {
+ let children = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []);
+ if (children.indexOf(document) !== -1) {
children.splice(children.indexOf(document), 1);
+ return true;
}
+ return false;
}
onContextMenu = (e: React.MouseEvent): void => {
// need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
- if (!e.isPropagationStopped() && this.props.Document.excludeFromLibrary) { // excludeFromLibrary means this is the user document
+ if (!e.isPropagationStopped() && this.props.Document.workspaceLibrary) { // excludeFromLibrary means this is the user document
ContextMenu.Instance.addItem({ description: "Create Workspace", event: undoBatch(() => MainView.Instance.createNewWorkspace()) });
ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.remove(this.props.Document)) });
+ e.stopPropagation();
+ e.preventDefault();
+ ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
}
}
- render() {
- let dropAction = StrCast(this.props.Document.dropAction, "alias") as dropActionType;
- if (!this.childDocs) {
- return (null);
+
+ @computed get resolvedDataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
+
+ outerXf = () => Utils.GetScreenTransform(this._mainEle!);
+ onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {});
+
+
+ @observable static NotifsCol: Opt<Doc>;
+
+ openNotifsCol = () => {
+ if (CollectionTreeView.NotifsCol && CollectionDockingView.Instance) {
+ CollectionDockingView.Instance.AddRightSplit(CollectionTreeView.NotifsCol, undefined);
}
- let childElements = TreeView.GetChildElements(this.childDocs, false, this.remove, this.props.moveDocument, dropAction, this.props.addDocTab);
+ }
+ @computed get notifsButton() {
+ const length = CollectionTreeView.NotifsCol ? DocListCast(CollectionTreeView.NotifsCol.data).length : 0;
+ const notifsRef = React.createRef<HTMLDivElement>();
+ const dragNotifs = action(() => CollectionTreeView.NotifsCol!);
+ return <div id="toolbar" key="toolbar">
+ <div ref={notifsRef}>
+ <button className="toolbar-button round-button" title="Notifs"
+ onClick={this.openNotifsCol} onPointerDown={CollectionTreeView.NotifsCol ? SetupDrag(notifsRef, dragNotifs) : emptyFunction}>
+ <FontAwesomeIcon icon={faBell} size="sm" />
+ </button>
+ <div className="main-notifs-badge" style={length > 0 ? { "display": "initial" } : { "display": "none" }}>
+ {length}
+ </div>
+ </div>
+ </div >;
+ }
+ @computed get clearButton() {
+ return <div id="toolbar" key="toolbar">
+ <div >
+ <button className="toolbar-button round-button" title="Notifs"
+ onClick={undoBatch(action(() => Doc.GetProto(this.props.Document)[this.props.fieldKey] = undefined))}>
+ <FontAwesomeIcon icon={faTrash} size="sm" />
+ </button>
+ </div>
+ </div >;
+ }
+
- return (
+ render() {
+ let dropAction = StrCast(this.props.Document.dropAction) as dropActionType;
+ let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before);
+ let moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc);
+
+ return !this.childDocs ? (null) : (
<div id="body" className="collectionTreeView-dropTarget"
- style={{ borderRadius: "inherit" }}
+ style={{ overflow: "auto" }}
onContextMenu={this.onContextMenu}
- onWheel={(e: React.WheelEvent) => this.props.isSelected() && e.stopPropagation()}
- onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}>
- <div className="coll-title">
- <EditableView
- contents={this.props.Document.title}
- display={"inline"}
- height={72}
- GetValue={() => StrCast(this.props.Document.title)}
- SetValue={(value: string) => {
- let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
- target.title = value;
- return true;
- }} />
- </div>
- <ul className="no-indent">
- {childElements}
+ onWheel={(e: React.WheelEvent) => (e.target as any).scrollHeight > (e.target as any).clientHeight && e.stopPropagation()}
+ onDrop={this.onTreeDrop}
+ ref={this.createTreeDropTarget}>
+ <EditableView
+ contents={this.resolvedDataDoc.title}
+ display={"block"}
+ height={72}
+ GetValue={() => StrCast(this.resolvedDataDoc.title)}
+ SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc).title = value) ? true : true}
+ OnFillDown={(value: string) => {
+ Doc.GetProto(this.props.Document).title = value;
+ let doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
+ TreeView.loadId = doc[Id];
+ Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true);
+ }} />
+ {this.props.Document.workspaceLibrary ? this.notifsButton : (null)}
+ {this.props.Document.allowClear ? this.clearButton : (null)}
+ <ul className="no-indent" style={{ width: "max-content" }} >
+ {
+ TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.Document, this.props.DataDoc, this.props.fieldKey, addDoc, this.remove,
+ moveDoc, dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.outerXf, this.props.active, this.props.PanelWidth, this.props.renderDepth)
+ }
</ul>
</div >
);
diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx
index ccbac9915..446f104d0 100644
--- a/src/client/views/collections/CollectionVideoView.tsx
+++ b/src/client/views/collections/CollectionVideoView.tsx
@@ -12,7 +12,7 @@ import { Id } from "../../../new_fields/FieldSymbols";
import { VideoBox } from "../nodes/VideoBox";
import { NumCast, Cast, StrCast } from "../../../new_fields/Types";
import { VideoField } from "../../../new_fields/URLField";
-import { SearchBox } from "../SearchBox";
+import { SearchBox } from "../search/SearchBox";
import { DocServer } from "../../DocServer";
import { Docs, DocUtils } from "../../documents/Documents";
@@ -97,7 +97,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, "");
SearchBox.convertDataUri(dataUrl, filename).then((returnedFilename) => {
if (returnedFilename) {
- let url = DocServer.Util.prepend(returnedFilename);
+ let url = DocServer.prepend(returnedFilename);
let imageSummary = Docs.Create.ImageDocument(url, {
x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y),
width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-"
@@ -122,7 +122,6 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
}
render() {
- trace();
return (
<CollectionBaseView {...this.props} className="collectionVideoView-cont" onContextMenu={this.onContextMenu}>
{this.subView}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 68eefab4c..e500e5c70 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -2,8 +2,10 @@ import { library } from '@fortawesome/fontawesome-svg-core';
import { faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons';
import { observer } from "mobx-react";
import * as React from 'react';
+import { Doc } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
+import { Docs } from '../../documents/Documents';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from '../ContextMenuItem';
@@ -29,7 +31,7 @@ export class CollectionView extends React.Component<FieldViewProps> {
private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
let props = { ...this.props, ...renderProps };
- switch (type) {
+ switch (this.isAnnotationOverlay ? CollectionViewType.Freeform : type) {
case CollectionViewType.Schema: return (<CollectionSchemaView {...props} CollectionView={this} />);
case CollectionViewType.Docking: return (<CollectionDockingView {...props} CollectionView={this} />);
case CollectionViewType.Tree: return (<CollectionTreeView {...props} CollectionView={this} />);
@@ -41,7 +43,7 @@ export class CollectionView extends React.Component<FieldViewProps> {
return (null);
}
- get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } // bcz: ? Why do we need to compare Id's?
+ get isAnnotationOverlay() { return this.props.fieldKey === "annotations" || this.props.fieldExt === "annotations"; }
onContextMenu = (e: React.MouseEvent): void => {
if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
@@ -54,6 +56,16 @@ export class CollectionView extends React.Component<FieldViewProps> {
subItems.push({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree), icon: "tree" });
subItems.push({ description: "Stacking", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Stacking), icon: "th-list" });
ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems });
+ ContextMenu.Instance.addItem({
+ description: "Apply Template", event: undoBatch(() => {
+ let otherdoc = new Doc();
+ otherdoc.width = 100;
+ otherdoc.height = 50;
+ Doc.GetProto(otherdoc).title = "applied(" + this.props.Document.title + ")";
+ Doc.GetProto(otherdoc).layout = Doc.MakeDelegate(this.props.Document);
+ this.props.addDocTab && this.props.addDocTab(otherdoc, undefined, "onRight");
+ }), icon: "project-diagram"
+ });
}
}
diff --git a/src/client/views/collections/DockedFrameRenderer.tsx b/src/client/views/collections/DockedFrameRenderer.tsx
deleted file mode 100644
index 1e7c5661b..000000000
--- a/src/client/views/collections/DockedFrameRenderer.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import 'golden-layout/src/css/goldenlayout-base.css';
-import 'golden-layout/src/css/goldenlayout-dark-theme.css';
-import { action, observable, reaction, Lambda, IReactionDisposer } from "mobx";
-import { observer } from "mobx-react";
-import Measure, { ContentRect } from "react-measure";
-import { Doc, Field, Opt, DocListCast } from "../../../new_fields/Doc";
-import { FieldId } from "../../../new_fields/RefField";
-import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { emptyFunction, returnTrue, Utils } from "../../../Utils";
-import { DocServer } from "../../DocServer";
-import { Transform } from '../../util/Transform';
-import { DocumentView } from "../nodes/DocumentView";
-import "./CollectionDockingView.scss";
-import { SubCollectionViewProps } from "./CollectionSubView";
-import React = require("react");
-import { CollectionViewType } from './CollectionBaseView';
-import { Id } from '../../../new_fields/FieldSymbols';
-import { CollectionDockingView } from './CollectionDockingView';
-
-interface DockedFrameProps {
- documentId: FieldId;
- glContainer: any;
- glEventHub: any;
- parent: CollectionDockingView;
-}
-
-@observer
-export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
- _mainCont = React.createRef<HTMLDivElement>();
- @observable private _panelWidth = 0;
- @observable private _panelHeight = 0;
- @observable private _document: Opt<Doc>;
- private get parentProps(): SubCollectionViewProps {
- return this.props.parent.props;
- }
-
- get _stack(): any {
- let parent = this.props.glContainer.parent.parent;
- if (this._document && this._document.excludeFromLibrary && parent.parent && parent.parent.contentItems.length > 1)
- return parent.parent.contentItems[1];
- return parent;
- }
- constructor(props: any) {
- super(props);
- DocServer.getRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc));
- }
-
- nativeWidth = () => NumCast(this._document!.nativeWidth, this._panelWidth);
- nativeHeight = () => NumCast(this._document!.nativeHeight, this._panelHeight);
- contentScaling = () => {
- const nativeH = this.nativeHeight();
- const nativeW = this.nativeWidth();
- let wscale = this._panelWidth / nativeW;
- return wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale;
- }
-
- ScreenToLocalTransform = () => {
- if (this._mainCont.current && this._mainCont.current.children) {
- let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current.children[0].firstChild as HTMLElement);
- scale = Utils.GetScreenTransform(this._mainCont.current).scale;
- return this.parentProps.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale);
- }
- return Transform.Identity();
- }
- get scaleToFitMultiplier() {
- let docWidth = NumCast(this._document!.width);
- let docHeight = NumCast(this._document!.height);
- if (NumCast(this._document!.nativeWidth) || !docWidth || !this._panelWidth || !this._panelHeight) return 1;
- if (StrCast(this._document!.layout).indexOf("Collection") === -1 ||
- NumCast(this._document!.viewType) !== CollectionViewType.Freeform) return 1;
- let scaling = Math.max(1, this._panelWidth / docWidth * docHeight > this._panelHeight ?
- this._panelHeight / docHeight : this._panelWidth / docWidth);
- return scaling;
- }
- get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; }
-
- addDocTab = (doc: Doc, location: string) => {
- if (location === "onRight") {
- CollectionDockingView.AddRightSplit(doc);
- } else {
- CollectionDockingView.AddTab(this._stack, doc);
- }
- }
- get content() {
- if (!this._document) {
- return (null);
- }
- return (
- <div className="collectionDockingView-content" ref={this._mainCont}
- style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px) scale(${this.scaleToFitMultiplier}, ${this.scaleToFitMultiplier})` }}>
- <DocumentView key={this._document[Id]} Document={this._document}
- bringToFront={emptyFunction}
- addDocument={undefined}
- removeDocument={undefined}
- ContentScaling={this.contentScaling}
- PanelWidth={this.nativeWidth}
- PanelHeight={this.nativeHeight}
- ScreenToLocalTransform={this.ScreenToLocalTransform}
- isTopMost={true}
- selectOnLoad={false}
- parentActive={returnTrue}
- whenActiveChanged={emptyFunction}
- focus={emptyFunction}
- addDocTab={this.addDocTab}
- ContainingCollectionView={undefined} />
- </div >);
- }
-
- render() {
- let theContent = this.content;
- return !this._document ? (null) :
- <Measure offset onResize={action((r: any) => { this._panelWidth = r.offset.width; this._panelHeight = r.offset.height; })}>
- {({ measureRef }) => <div ref={measureRef}> {theContent} </div>}
- </Measure>;
- }
-} \ No newline at end of file
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
index 95183bd2c..c0f489cd8 100644
--- a/src/client/views/collections/ParentDocumentSelector.tsx
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -9,7 +9,7 @@ import { CollectionDockingView } from "./CollectionDockingView";
import { NumCast } from "../../../new_fields/Types";
import { CollectionViewType } from "./CollectionBaseView";
-type SelectorProps = { Document: Doc, addDocTab(doc: Doc, location: string): void };
+type SelectorProps = { Document: Doc, addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void };
@observer
export class SelectorContextMenu extends React.Component<SelectorProps> {
@observable private _docs: { col: Doc, target: Doc }[] = [];
@@ -23,14 +23,14 @@ export class SelectorContextMenu extends React.Component<SelectorProps> {
async fetchDocuments() {
let aliases = (await SearchUtil.GetAliasesOfDocument(this.props.Document)).filter(doc => doc !== this.props.Document);
- const docs = await SearchUtil.Search(`data_l:"${this.props.Document[Id]}"`, true);
+ const { docs } = await SearchUtil.Search(`data_l:"${this.props.Document[Id]}"`, true);
const map: Map<Doc, Doc> = new Map;
- const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search(`data_l:"${doc[Id]}"`, true)));
+ const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search(`data_l:"${doc[Id]}"`, true).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.TopLevel.props.Document)).map(doc => ({ col: doc, target: this.props.Document }));
- this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.TopLevel.props.Document)).map(([col, target]) => ({ col, target }));
+ this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: this.props.Document }));
+ this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target }));
});
}
@@ -43,7 +43,7 @@ export class SelectorContextMenu extends React.Component<SelectorProps> {
col.panX = newPanX;
col.panY = newPanY;
}
- this.props.addDocTab(col, "inTab");
+ this.props.addDocTab(col, undefined, "inTab"); // bcz: dataDoc?
};
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index 7a0fd2b31..fc5212edd 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -9,6 +9,7 @@
opacity: 0.5;
transform: translate(10000px,10000px);
pointer-events: all;
+ cursor: pointer;
}
.collectionfreeformlinkview-linkText {
stroke: rgb(0,0,0);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 301b769af..b546d1b78 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -1,11 +1,10 @@
import { observer } from "mobx-react";
-import { Utils } from "../../../../Utils";
+import { Doc, HeightSym, WidthSym } from "../../../../new_fields/Doc";
+import { BoolCast, NumCast, StrCast } from "../../../../new_fields/Types";
+import { InkingControl } from "../../InkingControl";
import "./CollectionFreeFormLinkView.scss";
import React = require("react");
import v5 = require("uuid/v5");
-import { StrCast, NumCast, BoolCast } from "../../../../new_fields/Types";
-import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc";
-import { InkingControl } from "../../InkingControl";
export interface CollectionFreeFormLinkViewProps {
A: Doc;
@@ -26,18 +25,18 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : a[HeightSym]() / 2);
let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : b[WidthSym]() / 2);
let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : b[HeightSym]() / 2);
- this.props.LinkDocs.map(l => {
- let width = l[WidthSym]();
- l.x = (x1 + x2) / 2 - width / 2;
- l.y = (y1 + y2) / 2 + 10;
- if (!this.props.removeDocument(l)) this.props.addDocument(l, false);
- });
+ // this.props.LinkDocs.map(l => {
+ // let width = l[WidthSym]();
+ // l.x = (x1 + x2) / 2 - width / 2;
+ // l.y = (y1 + y2) / 2 + 10;
+ // if (!this.props.removeDocument(l)) this.props.addDocument(l, false);
+ // });
e.stopPropagation();
e.preventDefault();
}
}
render() {
- let l = this.props.LinkDocs;
+ // let l = this.props.LinkDocs;
let a = this.props.A;
let b = this.props.B;
let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / NumCast(a.zoomBasis, 1) / 2);
@@ -45,12 +44,13 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / NumCast(b.zoomBasis, 1) / 2);
let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / NumCast(b.zoomBasis, 1) / 2);
let text = "";
- this.props.LinkDocs.map(l => text += StrCast(l.title) + "(" + StrCast(l.linkDescription) + "), ");
- text = text.substr(0, text.length - 2);
+ // let first = this.props.LinkDocs[0];
+ // if (this.props.LinkDocs.length === 1) text += first.title + (first.linkDescription ? "(" + StrCast(first.linkDescription) + ")" : "");
+ // else text = "-multiple-";
return (
<>
<line key="linkLine" className="collectionfreeformlinkview-linkLine"
- style={{ strokeWidth: `${2 * l.length / 2}` }}
+ style={{ strokeWidth: `${2 * 1 / 2}` }}
x1={`${x1}`} y1={`${y1}`}
x2={`${x2}`} y2={`${y2}`} />
{/* <circle key="linkCircle" className="collectionfreeformlinkview-linkCircle"
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index a43c5f241..2d94f1b8e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -1,75 +1,72 @@
-import { computed, IReactionDisposer, reaction, trace } from "mobx";
+import { computed, IReactionDisposer, reaction } from "mobx";
import { observer } from "mobx-react";
-import { Utils } from "../../../../Utils";
+import { Doc, DocListCast } from "../../../../new_fields/Doc";
+import { Id } from "../../../../new_fields/FieldSymbols";
+import { List } from "../../../../new_fields/List";
+import { listSpec } from "../../../../new_fields/Schema";
+import { Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types";
import { DocumentManager } from "../../../util/DocumentManager";
import { DocumentView } from "../../nodes/DocumentView";
import { CollectionViewProps } from "../CollectionSubView";
import "./CollectionFreeFormLinksView.scss";
import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
import React = require("react");
-import { Doc, DocListCastAsync, DocListCast } from "../../../../new_fields/Doc";
-import { Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types";
-import { listSpec } from "../../../../new_fields/Schema";
-import { List } from "../../../../new_fields/List";
-import { Id } from "../../../../new_fields/FieldSymbols";
@observer
export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> {
_brushReactionDisposer?: IReactionDisposer;
componentDidMount() {
- this._brushReactionDisposer = reaction(
- () => {
- let doclist = DocListCast(this.props.Document[this.props.fieldKey]);
- return { doclist: doclist ? doclist : [], xs: doclist.map(d => d.x) };
- },
- () => {
- let doclist = DocListCast(this.props.Document[this.props.fieldKey]);
- let views = doclist ? doclist.filter(doc => StrCast(doc.backgroundLayout).indexOf("istogram") !== -1) : [];
- views.forEach((dstDoc, i) => {
- views.forEach((srcDoc, j) => {
- let dstTarg = dstDoc;
- let srcTarg = srcDoc;
- let x1 = NumCast(srcDoc.x);
- let x2 = NumCast(dstDoc.x);
- let x1w = NumCast(srcDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1);
- let x2w = NumCast(dstDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1);
- if (x1w < 0 || x2w < 0 || i === j) { }
- else {
- let findBrush = (field: (Doc | Promise<Doc>)[]) => field.findIndex(brush => {
- let bdocs = brush instanceof Doc ? Cast(brush.brushingDocs, listSpec(Doc), []) : undefined;
- return bdocs && bdocs.length && ((bdocs[0] === dstTarg && bdocs[1] === srcTarg)) ? true : false;
- });
- let brushAction = (field: (Doc | Promise<Doc>)[]) => {
- let found = findBrush(field);
- if (found !== -1) {
- console.log("REMOVE BRUSH " + srcTarg.title + " " + dstTarg.title);
- field.splice(found, 1);
- }
- };
- if (Math.abs(x1 + x1w - x2) < 20) {
- let linkDoc: Doc = new Doc();
- linkDoc.title = "Histogram Brush";
- linkDoc.linkDescription = "Brush between " + StrCast(srcTarg.title) + " and " + StrCast(dstTarg.Title);
- linkDoc.brushingDocs = new List([dstTarg, srcTarg]);
+ // this._brushReactionDisposer = reaction(
+ // () => {
+ // let doclist = DocListCast(this.props.Document[this.props.fieldKey]);
+ // return { doclist: doclist ? doclist : [], xs: doclist.map(d => d.x) };
+ // },
+ // () => {
+ // let doclist = DocListCast(this.props.Document[this.props.fieldKey]);
+ // let views = doclist ? doclist.filter(doc => StrCast(doc.backgroundLayout).indexOf("istogram") !== -1) : [];
+ // views.forEach((dstDoc, i) => {
+ // views.forEach((srcDoc, j) => {
+ // let dstTarg = dstDoc;
+ // let srcTarg = srcDoc;
+ // let x1 = NumCast(srcDoc.x);
+ // let x2 = NumCast(dstDoc.x);
+ // let x1w = NumCast(srcDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1);
+ // let x2w = NumCast(dstDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1);
+ // if (x1w < 0 || x2w < 0 || i === j) { }
+ // else {
+ // let findBrush = (field: (Doc | Promise<Doc>)[]) => field.findIndex(brush => {
+ // let bdocs = brush instanceof Doc ? Cast(brush.brushingDocs, listSpec(Doc), []) : undefined;
+ // return bdocs && bdocs.length && ((bdocs[0] === dstTarg && bdocs[1] === srcTarg)) ? true : false;
+ // });
+ // let brushAction = (field: (Doc | Promise<Doc>)[]) => {
+ // let found = findBrush(field);
+ // if (found !== -1) {
+ // field.splice(found, 1);
+ // }
+ // };
+ // if (Math.abs(x1 + x1w - x2) < 20) {
+ // let linkDoc: Doc = new Doc();
+ // linkDoc.title = "Histogram Brush";
+ // linkDoc.linkDescription = "Brush between " + StrCast(srcTarg.title) + " and " + StrCast(dstTarg.Title);
+ // linkDoc.brushingDocs = new List([dstTarg, srcTarg]);
- brushAction = (field: (Doc | Promise<Doc>)[]) => {
- if (findBrush(field) === -1) {
- console.log("ADD BRUSH " + srcTarg.title + " " + dstTarg.title);
- field.push(linkDoc);
- }
- };
- }
- if (dstTarg.brushingDocs === undefined) dstTarg.brushingDocs = new List<Doc>();
- if (srcTarg.brushingDocs === undefined) srcTarg.brushingDocs = new List<Doc>();
- let dstBrushDocs = Cast(dstTarg.brushingDocs, listSpec(Doc), []);
- let srcBrushDocs = Cast(srcTarg.brushingDocs, listSpec(Doc), []);
- brushAction(dstBrushDocs);
- brushAction(srcBrushDocs);
- }
- });
- });
- });
+ // brushAction = (field: (Doc | Promise<Doc>)[]) => {
+ // if (findBrush(field) === -1) {
+ // field.push(linkDoc);
+ // }
+ // };
+ // }
+ // if (dstTarg.brushingDocs === undefined) dstTarg.brushingDocs = new List<Doc>();
+ // if (srcTarg.brushingDocs === undefined) srcTarg.brushingDocs = new List<Doc>();
+ // let dstBrushDocs = Cast(dstTarg.brushingDocs, listSpec(Doc), []);
+ // let srcBrushDocs = Cast(srcTarg.brushingDocs, listSpec(Doc), []);
+ // brushAction(dstBrushDocs);
+ // brushAction(srcBrushDocs);
+ // }
+ // });
+ // });
+ // });
}
componentWillUnmount() {
if (this._brushReactionDisposer) {
@@ -98,6 +95,7 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
let srcViews = this.documentAnchors(connection.a);
let targetViews = this.documentAnchors(connection.b);
+
let possiblePairs: { a: Doc, b: Doc, }[] = [];
srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document })));
possiblePairs.map(possiblePair => {
@@ -110,25 +108,21 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
}
return match || found;
}, false)) {
- console.log("A" + possiblePair.a[Id] + " B" + possiblePair.b[Id] + " L" + connection.l[Id]);
- drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] })
+ drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] });
}
});
return drawnPairs;
}, [] as { a: Doc, b: Doc, l: Doc[] }[]);
- return connections.map(c => {
- let x = c.l.reduce((p, l) => p + l[Id], "");
- return <CollectionFreeFormLinkView key={x} A={c.a} B={c.b} LinkDocs={c.l}
- removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />;
- });
+ return connections.map(c => <CollectionFreeFormLinkView key={c.l.reduce((p, l) => p + l[Id], "")} A={c.a} B={c.b} LinkDocs={c.l}
+ removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />);
}
render() {
return (
<div className="collectionfreeformlinksview-container">
- <svg className="collectionfreeformlinksview-svgCanvas">
+ {/* <svg className="collectionfreeformlinksview-svgCanvas">
{this.uniqueConnections}
- </svg>
+ </svg> */}
{this.props.children}
</div>
);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
index 2838b7905..3193f5624 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
@@ -1,15 +1,13 @@
-import { computed } from "mobx";
import { observer } from "mobx-react";
+import * as mobxUtils from 'mobx-utils';
+import CursorField from "../../../../new_fields/CursorField";
+import { listSpec } from "../../../../new_fields/Schema";
+import { Cast } from "../../../../new_fields/Types";
+import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
import { CollectionViewProps } from "../CollectionSubView";
import "./CollectionFreeFormView.scss";
import React = require("react");
import v5 = require("uuid/v5");
-import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
-import CursorField from "../../../../new_fields/CursorField";
-import { List } from "../../../../new_fields/List";
-import { Cast } from "../../../../new_fields/Types";
-import { listSpec } from "../../../../new_fields/Schema";
-import * as mobxUtils from 'mobx-utils';
@observer
export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index e10ba9d7e..ccf261c95 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -1,7 +1,7 @@
@import "../../globalCssVariables";
.collectionfreeformview-ease {
- position: absolute;
+ position: inherit;
top: 0;
left: 0;
width: 100%;
@@ -25,8 +25,9 @@
height: 100%;
width: 100%;
}
+
>.jsx-parser {
- z-index:0;
+ z-index: 0;
}
//nested freeform views
@@ -40,24 +41,27 @@
border-radius: $border-radius;
box-sizing: border-box;
position: absolute;
+
.marqueeView {
overflow: hidden;
}
+
top: 0;
left: 0;
width: 100%;
height: 100%;
}
-
+
.collectionfreeformview-overlay {
.collectionfreeformview>.jsx-parser {
position: inherit;
height: 100%;
}
-
+
>.jsx-parser {
- z-index:0;
+ position: absolute;
+ z-index: 0;
}
.formattedTextBox-cont {
@@ -71,9 +75,11 @@
box-sizing: border-box;
position:absolute;
z-index: -1;
+
.marqueeView {
overflow: hidden;
}
+
top: 0;
left: 0;
width: 100%;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index cd613e6ab..1777c287c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,16 +1,25 @@
-import { action, computed, trace } from "mobx";
+import { action, computed } from "mobx";
import { observer } from "mobx-react";
-import { emptyFunction, returnFalse, returnOne } from "../../../../Utils";
+import { Doc, DocListCastAsync, HeightSym, WidthSym, DocListCast } from "../../../../new_fields/Doc";
+import { Id } from "../../../../new_fields/FieldSymbols";
+import { InkField, StrokeData } from "../../../../new_fields/InkField";
+import { createSchema, makeInterface } from "../../../../new_fields/Schema";
+import { BoolCast, Cast, FieldValue, NumCast } from "../../../../new_fields/Types";
+import { emptyFunction, returnOne } from "../../../../Utils";
import { DocumentManager } from "../../../util/DocumentManager";
import { DragManager } from "../../../util/DragManager";
+import { HistoryUtil } from "../../../util/History";
import { SelectionManager } from "../../../util/SelectionManager";
import { Transform } from "../../../util/Transform";
import { undoBatch } from "../../../util/UndoManager";
import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss";
+import { ContextMenu } from "../../ContextMenu";
import { InkingCanvas } from "../../InkingCanvas";
import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
import { DocumentContentsView } from "../../nodes/DocumentContentsView";
import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView";
+import { pageSchema } from "../../nodes/ImageBox";
+import PDFMenu from "../../pdf/PDFMenu";
import { CollectionSubView } from "../CollectionSubView";
import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView";
import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
@@ -18,19 +27,18 @@ import "./CollectionFreeFormView.scss";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
import v5 = require("uuid/v5");
-import { createSchema, makeInterface, listSpec } from "../../../../new_fields/Schema";
-import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc";
-import { FieldValue, Cast, NumCast, BoolCast } from "../../../../new_fields/Types";
-import { pageSchema } from "../../nodes/ImageBox";
-import { InkField, StrokeData } from "../../../../new_fields/InkField";
-import { HistoryUtil } from "../../../util/History";
-import { Id } from "../../../../new_fields/FieldSymbols";
-import { DocServer } from "../../../DocServer";
+import { ScriptField } from "../../../../new_fields/ScriptField";
+import { OverlayView, OverlayElementOptions } from "../../OverlayView";
+import { ScriptBox } from "../../ScriptBox";
+import { CompileScript } from "../../../util/Scripting";
+
export const panZoomSchema = createSchema({
panX: "number",
panY: "number",
- scale: "number"
+ scale: "number",
+ arrangeScript: ScriptField,
+ arrangeInit: ScriptField,
});
type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof positionSchema, typeof pageSchema]>;
@@ -46,11 +54,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@computed get nativeWidth() { return this.Document.nativeWidth || 0; }
@computed get nativeHeight() { return this.Document.nativeHeight || 0; }
- public get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; }
+ public get isAnnotationOverlay() { return this.props.fieldKey === "annotations" || this.props.fieldExt === "annotations"; }
private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; }
- private panX = () => this.Document.panX || 0;
- private panY = () => this.Document.panY || 0;
- private zoomScaling = () => this.Document.scale || 1;
+ private panX = () => this.props.fitToBox ? this.props.fitToBox()[0] : this.Document.panX || 0;
+ private panY = () => this.props.fitToBox ? this.props.fitToBox()[1] : this.Document.panY || 0;
+ private zoomScaling = () => this.props.fitToBox ? this.props.fitToBox()[2] : this.Document.scale || 1;
private centeringShiftX = () => !this.nativeWidth ? this._pwidth / 2 : 0; // shift so pan position is at center of window for non-overlay collections
private centeringShiftY = () => !this.nativeHeight ? this._pheight / 2 : 0;// shift so pan position is at center of window for non-overlay collections
private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform());
@@ -81,31 +89,45 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@undoBatch
@action
drop = (e: Event, de: DragManager.DropEvent) => {
- if (super.drop(e, de) && de.data instanceof DragManager.DocumentDragData) {
- if (de.data.droppedDocuments.length) {
- let dragDoc = de.data.droppedDocuments[0];
- let zoom = NumCast(dragDoc.zoomBasis, 1);
- let [xp, yp] = this.getTransform().transformPoint(de.x, de.y);
- let x = xp - de.data.xOffset / zoom;
- let y = yp - de.data.yOffset / zoom;
- let dropX = NumCast(de.data.droppedDocuments[0].x);
- let dropY = NumCast(de.data.droppedDocuments[0].y);
- de.data.droppedDocuments.forEach(d => {
- d.x = x + NumCast(d.x) - dropX;
- d.y = y + NumCast(d.y) - dropY;
- if (!NumCast(d.width)) {
- d.width = 300;
- }
- if (!NumCast(d.height)) {
- let nw = NumCast(d.nativeWidth);
- let nh = NumCast(d.nativeHeight);
- d.height = nw && nh ? nh / nw * NumCast(d.width) : 300;
- }
- this.bringToFront(d);
- });
- SelectionManager.ReselectAll();
+ if (super.drop(e, de)) {
+ if (de.data instanceof DragManager.DocumentDragData) {
+ if (de.data.droppedDocuments.length) {
+ let dragDoc = de.data.droppedDocuments[0];
+ let zoom = NumCast(dragDoc.zoomBasis, 1);
+ let [xp, yp] = this.getTransform().transformPoint(de.x, de.y);
+ let x = xp - de.data.xOffset / zoom;
+ let y = yp - de.data.yOffset / zoom;
+ let dropX = NumCast(de.data.droppedDocuments[0].x);
+ let dropY = NumCast(de.data.droppedDocuments[0].y);
+ de.data.droppedDocuments.forEach(d => {
+ d.x = x + NumCast(d.x) - dropX;
+ d.y = y + NumCast(d.y) - dropY;
+ if (!NumCast(d.width)) {
+ d.width = 300;
+ }
+ if (!NumCast(d.height)) {
+ let nw = NumCast(d.nativeWidth);
+ let nh = NumCast(d.nativeHeight);
+ d.height = nw && nh ? nh / nw * NumCast(d.width) : 300;
+ }
+ this.bringToFront(d);
+ });
+ }
+ }
+ else if (de.data instanceof DragManager.AnnotationDragData) {
+ if (de.data.dropDocument) {
+ let dragDoc = de.data.dropDocument;
+ let zoom = NumCast(dragDoc.zoomBasis, 1);
+ let [xp, yp] = this.getTransform().transformPoint(de.x, de.y);
+ let x = xp - de.data.xOffset / zoom;
+ let y = yp - de.data.yOffset / zoom;
+ let dropX = NumCast(de.data.dropDocument.x);
+ let dropY = NumCast(de.data.dropDocument.y);
+ dragDoc.x = x + NumCast(dragDoc.x) - dropX;
+ dragDoc.y = y + NumCast(dragDoc.y) - dropY;
+ this.bringToFront(dragDoc);
+ }
}
- return true;
}
return false;
}
@@ -135,6 +157,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
let docs = this.childDocs || [];
let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
if (!this.isAnnotationOverlay) {
+ PDFMenu.Instance.fadeOut(true);
let minx = docs.length ? NumCast(docs[0].x) : 0;
let maxx = docs.length ? NumCast(docs[0].width) / NumCast(docs[0].zoomBasis, 1) + minx : minx;
let miny = docs.length ? NumCast(docs[0].y) : 0;
@@ -147,7 +170,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]],
[range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]];
}, [[minx, maxx], [miny, maxy]]);
- let ink = Cast(this.props.Document.ink, InkField);
+ let ink = Cast(this.extensionDoc.ink, InkField);
if (ink && ink.inkData) {
ink.inkData.forEach((value: StrokeData, key: string) => {
let bounds = InkingCanvas.StrokeRect(value);
@@ -175,14 +198,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerWheel = (e: React.WheelEvent): void => {
+ if (BoolCast(this.props.Document.lockedPosition)) return;
// if (!this.props.active()) {
// return;
// }
+ if (this.props.Document.type === "pdf") {
+ return;
+ }
let childSelected = this.childDocs.some(doc => {
var dv = DocumentManager.Instance.getDocumentView(doc);
return dv && SelectionManager.IsSelected(dv) ? true : false;
});
- if (!this.props.isSelected() && !childSelected && !this.props.isTopMost) {
+ if (!this.props.isSelected() && !childSelected && this.props.renderDepth > 0) {
return;
}
e.stopPropagation();
@@ -201,7 +228,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
} else {
// if (modes[e.deltaMode] === 'pixels') coefficient = 50;
// else if (modes[e.deltaMode] === 'lines') coefficient = 1000; // This should correspond to line-height??
- let deltaScale = (1 - (e.deltaY / coefficient));
+ let deltaScale = e.deltaY > 0 ? (1 / 1.1) : 1.1;
if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) {
deltaScale = 1 / this.zoomScaling();
}
@@ -218,35 +245,24 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
setPan(panX: number, panY: number) {
+ if (BoolCast(this.props.Document.lockedPosition)) return;
this.props.Document.panTransformType = "None";
var scale = this.getLocalTransform().inverse().Scale;
const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX));
const newPanY = Math.min((1 - 1 / scale) * this.nativeHeight, Math.max(0, panY));
this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX;
this.props.Document.panY = this.isAnnotationOverlay ? newPanY : panY;
+ // this.props.Document.panX = panX;
+ // this.props.Document.panY = panY;
+ if (this.props.Document.scrollY) {
+ this.props.Document.scrollY = panY;
+ }
}
@action
onDrop = (e: React.DragEvent): void => {
var pt = this.getTransform().transformPoint(e.pageX, e.pageY);
- let html = e.dataTransfer.getData("text/html");
- if (html && html.indexOf(document.location.origin)) { // prosemirror text containing link to dash document
- e.stopPropagation();
- e.preventDefault();
- let start = html.indexOf(window.location.origin);
- let path = html.substr(start, html.length - start);
- let docid = path.substr(0, path.indexOf("\">")).replace(DocServer.Util.prepend("/doc/"), "").split("?")[0];
- DocServer.getRefField(docid).then(f => {
- if (f instanceof Doc) {
- f.x = pt[0];
- f.y = pt[1];
- (f instanceof Doc) && this.props.addDocument(f, false);
- }
- });
- return;
- } else {
- super.onDrop(e, { x: pt[0], y: pt[1] });
- }
+ super.onDrop(e, { x: pt[0], y: pt[1] });
}
onDragOver = (): void => {
@@ -262,7 +278,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
doc.zIndex = docs.length + 1;
}
- focusDocument = (doc: Doc) => {
+ focusDocument = (doc: Doc, willZoom: boolean) => {
const panX = this.Document.panX;
const panY = this.Document.panY;
const id = this.Document[Id];
@@ -289,22 +305,80 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
newState.initializers[id] = { panX: newPanX, panY: newPanY };
HistoryUtil.pushState(newState);
this.setPan(newPanX, newPanY);
+
this.props.Document.panTransformType = "Ease";
this.props.focus(this.props.Document);
+ if (willZoom) {
+ this.setScaleToZoom(doc);
+ }
+
+ }
+
+ setScaleToZoom = (doc: Doc) => {
+ let p = this.props;
+ let PanelHeight = p.PanelHeight();
+ let panelWidth = p.PanelWidth();
+
+ let docHeight = NumCast(doc.height);
+ let docWidth = NumCast(doc.width);
+ let targetHeight = 0.5 * PanelHeight;
+ let targetWidth = 0.5 * panelWidth;
+
+ let maxScaleX: number = targetWidth / docWidth;
+ let maxScaleY: number = targetHeight / docHeight;
+ let maxApplicableScale = Math.min(maxScaleX, maxScaleY);
+ this.Document.scale = maxApplicableScale;
}
+ zoomToScale = (scale: number) => {
+ this.Document.scale = scale;
+ }
- getDocumentViewProps(document: Doc): DocumentViewProps {
+ getScale = () => {
+ if (this.Document.scale) {
+ return this.Document.scale;
+ }
+ return 1;
+ }
+
+
+ getChildDocumentViewProps(childDocLayout: Doc): DocumentViewProps {
+ let resolvedDataDoc = this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined;
+ let layoutDoc = Doc.expandTemplateLayout(childDocLayout, resolvedDataDoc);
+ return {
+ DataDoc: resolvedDataDoc !== layoutDoc && resolvedDataDoc ? resolvedDataDoc : undefined,
+ Document: layoutDoc,
+ addDocument: this.props.addDocument,
+ removeDocument: this.props.removeDocument,
+ moveDocument: this.props.moveDocument,
+ ScreenToLocalTransform: this.getTransform,
+ renderDepth: this.props.renderDepth + 1,
+ selectOnLoad: layoutDoc[Id] === this._selectOnLoaded,
+ PanelWidth: layoutDoc[WidthSym],
+ PanelHeight: layoutDoc[HeightSym],
+ ContentScaling: returnOne,
+ ContainingCollectionView: this.props.CollectionView,
+ focus: this.focusDocument,
+ parentActive: this.props.active,
+ whenActiveChanged: this.props.whenActiveChanged,
+ bringToFront: this.bringToFront,
+ addDocTab: this.props.addDocTab,
+ zoomToScale: this.zoomToScale,
+ getScale: this.getScale
+ };
+ }
+ getDocumentViewProps(layoutDoc: Doc): DocumentViewProps {
return {
- Document: document,
+ DataDoc: this.props.DataDoc,
+ Document: this.props.Document,
addDocument: this.props.addDocument,
removeDocument: this.props.removeDocument,
moveDocument: this.props.moveDocument,
ScreenToLocalTransform: this.getTransform,
- isTopMost: false,
- selectOnLoad: document[Id] === this._selectOnLoaded,
- PanelWidth: document[WidthSym],
- PanelHeight: document[HeightSym],
+ renderDepth: this.props.renderDepth + 1,
+ selectOnLoad: layoutDoc[Id] === this._selectOnLoaded,
+ PanelWidth: layoutDoc[WidthSym],
+ PanelHeight: layoutDoc[HeightSym],
ContentScaling: returnOne,
ContainingCollectionView: this.props.CollectionView,
focus: this.focusDocument,
@@ -312,19 +386,41 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
whenActiveChanged: this.props.whenActiveChanged,
bringToFront: this.bringToFront,
addDocTab: this.props.addDocTab,
+ zoomToScale: this.zoomToScale,
+ getScale: this.getScale
};
}
+ getCalculatedPositions(script: ScriptField, params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): { x?: number, y?: number, width?: number, height?: number, state?: any } {
+ const result = script.script.run(params);
+ if (!result.success) {
+ return {};
+ }
+ return result.result === undefined ? {} : result.result;
+ }
+
@computed.struct
get views() {
let curPage = FieldValue(this.Document.curPage, -1);
- let docviews = this.childDocs.reduce((prev, doc) => {
+ const initScript = this.Document.arrangeInit;
+ const script = this.Document.arrangeScript;
+ let state: any = undefined;
+ const docs = this.childDocs;
+ if (initScript) {
+ const initResult = initScript.script.run({ docs, collection: this.Document });
+ if (initResult.success) {
+ state = initResult.result;
+ }
+ }
+ let docviews = docs.reduce((prev, doc) => {
if (!(doc instanceof Doc)) return prev;
var page = NumCast(doc.page, -1);
if (Math.round(page) === Math.round(curPage) || page === -1) {
let minim = BoolCast(doc.isMinimized, false);
if (minim === undefined || !minim) {
- prev.push(<CollectionFreeFormDocumentView key={doc[Id]} {...this.getDocumentViewProps(doc)} />);
+ const pos = script ? this.getCalculatedPositions(script, { doc, index: prev.length, collection: this.Document, docs, state }) : {};
+ state = pos.state === undefined ? state : pos.state;
+ prev.push(<CollectionFreeFormDocumentView key={doc[Id]} x={pos.x} y={pos.y} width={pos.width} height={pos.height} {...this.getChildDocumentViewProps(doc)} />);
}
}
return prev;
@@ -340,17 +436,76 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
}
+ onContextMenu = () => {
+ ContextMenu.Instance.addItem({
+ description: "Arrange contents in grid",
+ event: async () => {
+ const docs = await DocListCastAsync(this.Document[this.props.fieldKey]);
+ if (docs) {
+ let startX = this.Document.panX || 0;
+ let x = startX;
+ let y = this.Document.panY || 0;
+ let i = 0;
+ const width = Math.max(...docs.map(doc => NumCast(doc.width)));
+ const height = Math.max(...docs.map(doc => NumCast(doc.height)));
+ for (const doc of docs) {
+ doc.x = x;
+ doc.y = y;
+ x += width + 20;
+ if (++i === 6) {
+ i = 0;
+ x = startX;
+ y += height + 20;
+ }
+ }
+ }
+ }
+ });
+ ContextMenu.Instance.addItem({
+ description: "Add freeform arrangement",
+ event: () => {
+ let addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => {
+ let overlayDisposer: () => void = emptyFunction;
+ const script = this.Document[key];
+ let originalText: string | undefined = undefined;
+ if (script) originalText = script.script.originalScript;
+ let scriptingBox = <ScriptBox initialText={originalText} onCancel={overlayDisposer} onSave={(text, onError) => {
+ const script = CompileScript(text, {
+ params,
+ requiredType,
+ typecheck: false
+ });
+ if (!script.compiled) {
+ onError(script.errors.map(error => error.messageText).join("\n"));
+ return;
+ }
+ const docs = DocListCast(this.Document[this.props.fieldKey]);
+ docs.map(d => d.transition = "transform 1s");
+ this.Document[key] = new ScriptField(script);
+ overlayDisposer();
+ setTimeout(() => docs.map(d => d.transition = undefined), 1200);
+ }} />;
+ overlayDisposer = OverlayView.Instance.addElement(scriptingBox, options);
+ };
+ addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300 }, { collection: "Doc", docs: "Doc[]" }, undefined);
+ addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300 }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}");
+ }
+ });
+ }
+
private childViews = () => [
<CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />,
...this.views
- ];
+ ]
render() {
const containerName = `collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`;
const easing = () => this.props.Document.panTransformType === "Ease";
+
+ if (this.props.fieldExt) Doc.UpdateDocumentExtensionForField(this.extensionDoc, this.props.fieldKey);
return (
<div className={containerName} ref={this.createDropTarget} onWheel={this.onPointerWheel}
style={{ borderRadius: "inherit" }}
- onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} >
+ onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} onContextMenu={this.onContextMenu}>
<MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} isSelected={this.props.isSelected}
addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox}
getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}>
@@ -358,14 +513,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
easing={easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
<CollectionFreeFormLinksView {...this.props} key="freeformLinks">
- <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} >
+ <InkingCanvas getScreenTransform={this.getTransform} Document={this.extensionDoc} inkFieldKey={this.props.fieldExt ? "ink" : this.props.fieldKey + "_ink"} >
{this.childViews}
</InkingCanvas>
</CollectionFreeFormLinksView>
<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
</CollectionFreeFormViewPannableContents>
</MarqueeView>
- <CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} {...this.props} />
+ <CollectionFreeFormOverlayView {...this.props} {...this.getDocumentViewProps(this.props.Document)} />
</div>
);
}
@@ -375,7 +530,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
@computed get overlayView() {
return (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
- isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
+ renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />);
}
render() {
return this.overlayView;
@@ -385,8 +540,9 @@ class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps &
@observer
class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
@computed get backgroundView() {
+ let props = this.props;
return (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
- isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
+ renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />);
}
render() {
return this.props.Document.backgroundLayout ? this.backgroundView : (null);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index 6e8ec8662..9fc2e44fb 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -21,6 +21,6 @@
white-space:nowrap;
}
.marquee-legend::after {
- content: "Press: c (collection), s (summary), r (replace) or Delete"
+ content: "Press: c (collection), s (summary), or Delete"
}
} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 07a58ed64..a4a6881f8 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,27 +1,24 @@
import * as htmlToImage from "html-to-image";
-import { action, computed, observable, trace } from "mobx";
+import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
+import { Doc, FieldResult } from "../../../../new_fields/Doc";
+import { Id } from "../../../../new_fields/FieldSymbols";
+import { InkField, StrokeData } from "../../../../new_fields/InkField";
+import { List } from "../../../../new_fields/List";
+import { Cast, NumCast } from "../../../../new_fields/Types";
+import { Utils } from "../../../../Utils";
+import { DocServer } from "../../../DocServer";
import { Docs } from "../../../documents/Documents";
import { SelectionManager } from "../../../util/SelectionManager";
import { Transform } from "../../../util/Transform";
-import { undoBatch, UndoManager } from "../../../util/UndoManager";
+import { undoBatch } from "../../../util/UndoManager";
import { InkingCanvas } from "../../InkingCanvas";
import { PreviewCursor } from "../../PreviewCursor";
+import { Templates } from "../../Templates";
+import { CollectionViewType } from "../CollectionBaseView";
import { CollectionFreeFormView } from "./CollectionFreeFormView";
import "./MarqueeView.scss";
import React = require("react");
-import { Utils } from "../../../../Utils";
-import { Doc } from "../../../../new_fields/Doc";
-import { NumCast, Cast } from "../../../../new_fields/Types";
-import { InkField, StrokeData } from "../../../../new_fields/InkField";
-import { List } from "../../../../new_fields/List";
-import { ImageField } from "../../../../new_fields/URLField";
-import { Template, Templates } from "../../Templates";
-import { SearchBox } from "../../SearchBox";
-import { DocServer } from "../../../DocServer";
-import { Id } from "../../../../new_fields/FieldSymbols";
-import { CollectionView } from "../CollectionView";
-import { CollectionViewType } from "../CollectionBaseView";
interface MarqueeViewProps {
getContainerTransform: () => Transform;
@@ -94,8 +91,9 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
this.pasteTable(ns, x, y);
}
});
- } else {
+ } else if (!e.ctrlKey) {
let newBox = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" });
+ newBox.proto!.autoHeight = true;
this.props.addLiveTextDocument(newBox);
}
e.stopPropagation();
@@ -147,6 +145,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
this._downY = this._lastY = e.pageY;
this._commandExecuted = false;
PreviewCursor.Visible = false;
+ this.cleanupInteractions(true);
if (e.button === 2 || (e.button === 0 && e.altKey)) {
if (!this.props.container.props.active()) this.props.selectDocuments([this.props.container.props.Document]);
document.addEventListener("pointermove", this.onPointerMove, true);
@@ -182,14 +181,18 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
@action
onPointerUp = (e: PointerEvent): void => {
+ // console.log("pointer up!");
if (this._visible) {
+ // console.log("visible");
let mselect = this.marqueeSelect();
if (!e.shiftKey) {
SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document);
}
this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]);
}
+ //console.log("invisible");
this.cleanupInteractions(true);
+
if (e.altKey) {
e.preventDefault();
}
@@ -221,6 +224,18 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) };
}
+ get ink() {
+ let container = this.props.container.Document;
+ let containerKey = this.props.container.props.fieldKey;
+ return Cast(container[containerKey + "_ink"], InkField);
+ }
+
+ set ink(value: InkField | undefined) {
+ let container = Doc.GetProto(this.props.container.Document);
+ let containerKey = this.props.container.props.fieldKey;
+ container[containerKey + "_ink"] = value;
+ }
+
@undoBatch
@action
marqueeCommand = async (e: KeyboardEvent) => {
@@ -232,15 +247,14 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
e.stopPropagation();
(e as any).propagationIsStopped = true;
this.marqueeSelect().map(d => this.props.removeDocument(d));
- let ink = Cast(this.props.container.props.Document.ink, InkField);
- if (ink) {
- this.marqueeInkDelete(ink.inkData);
+ if (this.ink) {
+ this.marqueeInkDelete(this.ink.inkData);
}
SelectionManager.DeselectAll();
this.cleanupInteractions(false);
e.stopPropagation();
}
- if (e.key === "c" || e.key === "s" || e.key === "S" || e.key === "e" || e.key === "p") {
+ if (e.key === "c" || e.key === "s" || e.key === "S") {
this._commandExecuted = true;
e.stopPropagation();
e.preventDefault();
@@ -256,23 +270,18 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
return d;
});
}
- let ink = Cast(this.props.container.props.Document.ink, InkField);
- let inkData = ink ? ink.inkData : undefined;
- let zoomBasis = NumCast(this.props.container.props.Document.scale, 1);
+ let inkData = this.ink ? this.ink.inkData : undefined;
let newCollection = Docs.Create.FreeformDocument(selected, {
x: bounds.left,
y: bounds.top,
panX: 0,
panY: 0,
- borderRounding: e.key === "e" ? -1 : undefined,
backgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white",
- scale: zoomBasis,
- width: bounds.width * zoomBasis,
- height: bounds.height * zoomBasis,
- ink: inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined,
- title: e.key === "s" || e.key === "S" ? "-summary-" : e.key === "p" ? "-summary-" : "a nested collection",
+ width: bounds.width,
+ height: bounds.height,
+ title: e.key === "s" || e.key === "S" ? "-summary-" : "a nested collection",
});
- newCollection.zoomBasis = zoomBasis;
+ newCollection.data_ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined;
this.marqueeInkDelete(inkData);
if (e.key === "s") {
@@ -295,39 +304,25 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
this.props.addLiveTextDocument(container);
// });
} else if (e.key === "S") {
- await htmlToImage.toPng(this._mainCont.current!, { width: bounds.width * zoomBasis, height: bounds.height * zoomBasis, quality: 0.2 }).then((dataUrl) => {
- selected.map(d => {
- this.props.removeDocument(d);
- d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
- d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
- d.page = -1;
- return d;
- });
- let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
- SearchBox.convertDataUri(dataUrl, "icon" + summary[Id] + "_image").then((returnedFilename) => {
- if (returnedFilename) {
- let url = DocServer.Util.prepend(returnedFilename);
- let imageSummary = Docs.Create.ImageDocument(url, {
- x: bounds.left, y: bounds.top + 100 / zoomBasis,
- width: 150, height: bounds.height / bounds.width * 150, title: "-summary image-"
- });
- summary.imageSummary = imageSummary;
- this.props.addDocument(imageSummary, false);
- }
- })
- newCollection.proto!.summaryDoc = summary;
- selected = [newCollection];
- newCollection.x = bounds.left + bounds.width;
- //this.props.addDocument(newCollection, false);
- summary.proto!.summarizedDocs = new List<Doc>(selected);
- summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight"
-
- this.props.addLiveTextDocument(summary);
+ selected.map(d => {
+ this.props.removeDocument(d);
+ d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
+ d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
+ d.page = -1;
+ return d;
});
+ let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
+ newCollection.proto!.summaryDoc = summary;
+ selected = [newCollection];
+ newCollection.x = bounds.left + bounds.width;
+ //this.props.addDocument(newCollection, false);
+ summary.proto!.summarizedDocs = new List<Doc>(selected);
+ summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight"
+
+ this.props.addLiveTextDocument(summary);
}
else {
this.props.addDocument(newCollection, false);
- SelectionManager.DeselectAll();
this.props.selectDocuments([newCollection]);
}
this.cleanupInteractions(false);
@@ -363,7 +358,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
let idata = new Map();
ink.forEach((value: StrokeData, key: string, map: any) =>
!InkingCanvas.IntersectStrokeRect(value, this.Bounds) && idata.set(key, value));
- Doc.SetOnPrototype(this.props.container.props.Document, "ink", new InkField(idata));
+ this.ink = new InkField(idata);
}
}
diff --git a/src/client/views/document_templates/caption_toggle/DetailedCaptionToggle.tsx b/src/client/views/document_templates/caption_toggle/DetailedCaptionToggle.tsx
index 2172f2852..f8104cef3 100644
--- a/src/client/views/document_templates/caption_toggle/DetailedCaptionToggle.tsx
+++ b/src/client/views/document_templates/caption_toggle/DetailedCaptionToggle.tsx
@@ -6,15 +6,15 @@ import { FormattedTextBox, FormattedTextBoxProps } from '../../nodes/FormattedTe
import { FieldViewProps } from '../../nodes/FieldView';
interface DetailedCaptionDataProps {
- captionFieldKey?: string,
- detailsFieldKey?: string,
+ captionFieldKey?: string;
+ detailsFieldKey?: string;
}
interface DetailedCaptionStylingProps {
sharedFontColor?: ColorProperty;
- captionFontStyle?: FontStyleProperty
- detailsFontStyle?: FontStyleProperty
- toggleSize?: number
+ captionFontStyle?: FontStyleProperty;
+ detailsFontStyle?: FontStyleProperty;
+ toggleSize?: number;
}
@observer
diff --git a/src/client/views/globalCssVariables.scss b/src/client/views/globalCssVariables.scss
index 838d4d9ac..fec105516 100644
--- a/src/client/views/globalCssVariables.scss
+++ b/src/client/views/globalCssVariables.scss
@@ -9,6 +9,7 @@ $main-accent: #aaaaa3;
//$alt-accent: #59dff7;
$alt-accent: #c2c2c5;
$lighter-alt-accent: rgb(207, 220, 240);
+$darker-alt-accent: rgb(178, 206, 248);
$intermediate-color: #9c9396;
$dark-color: #121721;
// fonts
@@ -18,6 +19,7 @@ $serif: "Crimson Text", serif;
// misc values
$border-radius: 0.3em;
//
+$search-thumnail-size: 175;
// dragged items
$contextMenu-zindex: 1000; // context menu shows up over everything
@@ -32,4 +34,5 @@ $MAX_ROW_HEIGHT: 44px;
COLLECTION_BORDER_WIDTH: $COLLECTION_BORDER_WIDTH;
MINIMIZED_ICON_SIZE: $MINIMIZED_ICON_SIZE;
MAX_ROW_HEIGHT: $MAX_ROW_HEIGHT;
+ SEARCH_THUMBNAIL_SIZE: $search-thumnail-size;
} \ No newline at end of file
diff --git a/src/client/views/globalCssVariables.scss.d.ts b/src/client/views/globalCssVariables.scss.d.ts
index 9788d31f7..d95cec9d8 100644
--- a/src/client/views/globalCssVariables.scss.d.ts
+++ b/src/client/views/globalCssVariables.scss.d.ts
@@ -4,6 +4,7 @@ interface IGlobalScss {
COLLECTION_BORDER_WIDTH: string;
MINIMIZED_ICON_SIZE: string;
MAX_ROW_HEIGHT: string;
+ SEARCH_THUMBNAIL_SIZE: string;
}
declare const globalCssVariables: IGlobalScss;
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 499b83c0f..b09538d1a 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,19 +1,19 @@
-import { computed, IReactionDisposer, reaction, action } from "mobx";
+import { computed } from "mobx";
import { observer } from "mobx-react";
-import { Doc } from "../../../new_fields/Doc";
-import { List } from "../../../new_fields/List";
-import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
-import { BoolCast, Cast, FieldValue, NumCast } from "../../../new_fields/Types";
-import { OmitKeys } from "../../../Utils";
+import { createSchema, makeInterface } from "../../../new_fields/Schema";
+import { BoolCast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
import { Transform } from "../../util/Transform";
import { DocComponent } from "../DocComponent";
import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView";
import "./DocumentView.scss";
import React = require("react");
-import { UndoManager } from "../../util/UndoManager";
-import { SelectionManager } from "../../util/SelectionManager";
+import { Doc } from "../../../new_fields/Doc";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
+ x?: number;
+ y?: number;
+ width?: number;
+ height?: number;
}
const schema = createSchema({
@@ -27,20 +27,14 @@ const FreeformDocument = makeInterface(schema, positionSchema);
@observer
export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, FreeformDocument>(FreeformDocument) {
- private _mainCont = React.createRef<HTMLDivElement>();
- _bringToFrontDisposer?: IReactionDisposer;
-
- @computed get transform() {
- return `scale(${this.props.ContentScaling()}, ${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) scale(${this.zoom}, ${this.zoom}) `;
- }
-
- @computed get X() { return FieldValue(this.Document.x, 0); }
- @computed get Y() { return FieldValue(this.Document.y, 0); }
+ @computed get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) scale(${this.zoom}) `; }
+ @computed get X() { return this.props.x !== undefined ? this.props.x : this.Document.x || 0; }
+ @computed get Y() { return this.props.y !== undefined ? this.props.y : this.Document.y || 0; }
+ @computed get width(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.props.width !== undefined ? this.props.width : this.Document.width || 0; }
+ @computed get height(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.props.height !== undefined ? this.props.height : this.Document.height || 0; }
@computed get zoom(): number { return 1 / FieldValue(this.Document.zoomBasis, 1); }
@computed get nativeWidth(): number { return FieldValue(this.Document.nativeWidth, 0); }
@computed get nativeHeight(): number { return FieldValue(this.Document.nativeHeight, 0); }
- @computed get width(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : FieldValue(this.Document.width, 0); }
- @computed get height(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : FieldValue(this.Document.height, 0); }
set width(w: number) {
this.Document.width = w;
@@ -54,135 +48,60 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
this.Document.width = this.nativeWidth / this.nativeHeight * h;
}
}
+ @computed get scaleToOverridingWidth() { return this.width / NumCast(this.props.Document.width, this.width); }
contentScaling = () => this.nativeWidth > 0 ? this.width / this.nativeWidth : 1;
panelWidth = () => this.props.PanelWidth();
panelHeight = () => this.props.PanelHeight();
getTransform = (): Transform => this.props.ScreenToLocalTransform()
.translate(-this.X, -this.Y)
- .scale(1 / this.contentScaling()).scale(1 / this.zoom)
-
- @computed
- get docView() {
- return <DocumentView {...OmitKeys(this.props, ['zoomFade']).omit}
- ContentScaling={this.contentScaling}
- ScreenToLocalTransform={this.getTransform}
- PanelWidth={this.panelWidth}
- PanelHeight={this.panelHeight}
- collapseToPoint={this.collapseToPoint}
- />;
- }
-
- componentDidMount() {
- this._bringToFrontDisposer = reaction(() => this.props.Document.isIconAnimating, (values) => {
- this.props.bringToFront(this.props.Document);
- if (values instanceof List) {
- let scrpt = this.props.ScreenToLocalTransform().transformPoint(values[0], values[1]);
- this.animateBetweenIcon(true, scrpt, [this.Document.x || 0, this.Document.y || 0],
- this.Document.width || 0, this.Document.height || 0, values[2], values[3] ? true : false);
- }
- }, { fireImmediately: true });
- }
-
- componentWillUnmount() {
- if (this._bringToFrontDisposer) this._bringToFrontDisposer();
- }
-
- static _undoBatch?: UndoManager.Batch = undefined;
- @action
- public collapseToPoint = async (scrpt: number[], expandedDocs: Doc[] | undefined): Promise<void> => {
- SelectionManager.DeselectAll();
- if (expandedDocs) {
- if (!CollectionFreeFormDocumentView._undoBatch) {
- CollectionFreeFormDocumentView._undoBatch = UndoManager.StartBatch("iconAnimating");
- }
- let isMinimized: boolean | undefined;
- expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => {
- let iconAnimating = Cast(maximizedDoc.isIconAnimating, List);
- if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) {
- if (isMinimized === undefined) {
- isMinimized = BoolCast(maximizedDoc.isMinimized, false);
- }
- maximizedDoc.willMaximize = isMinimized;
- maximizedDoc.isMinimized = false;
- maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]);
- }
+ .scale(1 / this.contentScaling()).scale(1 / this.zoom / this.scaleToOverridingWidth)
+
+ animateBetweenIcon = (icon: number[], stime: number, maximizing: boolean) => {
+ this.props.bringToFront(this.props.Document);
+ let targetPos = [this.Document.x || 0, this.Document.y || 0];
+ let iconPos = this.props.ScreenToLocalTransform().transformPoint(icon[0], icon[1]);
+ DocumentView.animateBetweenIconFunc(this.props.Document,
+ this.Document.width || 0, this.Document.height || 0, stime, maximizing, (progress: number) => {
+ let pval = maximizing ?
+ [iconPos[0] + (targetPos[0] - iconPos[0]) * progress, iconPos[1] + (targetPos[1] - iconPos[1]) * progress] :
+ [targetPos[0] + (iconPos[0] - targetPos[0]) * progress, targetPos[1] + (iconPos[1] - targetPos[1]) * progress];
+ this.Document.x = progress === 1 ? targetPos[0] : pval[0];
+ this.Document.y = progress === 1 ? targetPos[1] : pval[1];
});
- setTimeout(() => {
- CollectionFreeFormDocumentView._undoBatch && CollectionFreeFormDocumentView._undoBatch.end();
- CollectionFreeFormDocumentView._undoBatch = undefined;
- }, 500);
- }
- }
-
- animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, maximizing: boolean) {
-
- setTimeout(() => {
- let now = Date.now();
- let progress = Math.min(1, (now - stime) / 200);
- let pval = maximizing ?
- [icon[0] + (targ[0] - icon[0]) * progress, icon[1] + (targ[1] - icon[1]) * progress] :
- [targ[0] + (icon[0] - targ[0]) * progress, targ[1] + (icon[1] - targ[1]) * progress];
- this.props.Document.width = maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress;
- this.props.Document.height = maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress;
- this.props.Document.x = pval[0];
- this.props.Document.y = pval[1];
- if (first) {
- this.props.Document.proto!.willMaximize = false;
- }
- if (now < stime + 200) {
- this.animateBetweenIcon(false, icon, targ, width, height, stime, maximizing);
- }
- else {
- if (!maximizing) {
- this.props.Document.proto!.isMinimized = true;
- this.props.Document.x = targ[0];
- this.props.Document.y = targ[1];
- this.props.Document.width = width;
- this.props.Document.height = height;
- }
- this.props.Document.proto!.isIconAnimating = undefined;
- }
- },
- 2);
}
borderRounding = () => {
- let br = NumCast(this.props.Document.borderRounding);
- return br >= 0 ? br :
- NumCast(this.props.Document.nativeWidth) === 0 ?
- Math.min(this.props.PanelWidth(), this.props.PanelHeight())
- : Math.min(this.Document.nativeWidth || 0, this.Document.nativeHeight || 0);
+ let br = StrCast(this.props.Document.layout instanceof Doc ? this.props.Document.layout.borderRounding : this.props.Document.borderRounding);
+ if (br.endsWith("%")) {
+ let percent = Number(br.substr(0, br.length - 1)) / 100;
+ let nativeDim = Math.min(NumCast(this.props.Document.nativeWidth), NumCast(this.props.Document.nativeHeight));
+ let minDim = percent * (nativeDim ? nativeDim : Math.min(this.props.PanelWidth(), this.props.PanelHeight()));
+ return minDim;
+ }
+ return undefined;
}
render() {
- let maximizedDoc = FieldValue(Cast(this.props.Document.maximizedDocs, listSpec(Doc)));
- let zoomFade = 1;
- //var zoom = doc.GetNumber(KeyStore.ZoomBasis, 1);
- // let transform = this.getTransform().scale(this.contentScaling()).inverse();
- // var [sptX, sptY] = transform.transformPoint(0, 0);
- // let [bptX, bptY] = transform.transformPoint(this.props.PanelWidth(), this.props.PanelHeight());
- // let w = bptX - sptX;
- //zoomFade = area < 100 || area > 800 ? Math.max(0, Math.min(1, 2 - 5 * (zoom < this.scale ? this.scale / zoom : zoom / this.scale))) : 1;
- const screenWidth = Math.min(50 * NumCast(this.props.Document.nativeWidth, 0), 1800);
- let fadeUp = .75 * screenWidth;
- let fadeDown = (maximizedDoc ? .0075 : .075) * screenWidth;
- // zoomFade = w < fadeDown /* || w > fadeUp */ ? Math.max(0.1, Math.min(1, 2 - (w < fadeDown ? Math.sqrt(Math.sqrt(fadeDown / w)) : w / fadeUp))) : 1;
-
return (
- <div className="collectionFreeFormDocumentView-container" ref={this._mainCont}
+ <div className="collectionFreeFormDocumentView-container"
style={{
- opacity: zoomFade,
- borderRadius: `${this.borderRounding()}px`,
transformOrigin: "left top",
+ position: "absolute",
+ backgroundColor: "transparent",
+ borderRadius: this.borderRounding(),
transform: this.transform,
- pointerEvents: (zoomFade < 0.09 ? "none" : "all"),
+ transition: StrCast(this.props.Document.transition),
width: this.width,
height: this.height,
- position: "absolute",
zIndex: this.Document.zIndex || 0,
- backgroundColor: "transparent"
}} >
- {this.docView}
+ <DocumentView {...this.props}
+ ContentScaling={this.contentScaling}
+ ScreenToLocalTransform={this.getTransform}
+ PanelWidth={this.panelWidth}
+ PanelHeight={this.panelHeight}
+ animateBetweenIcon={this.animateBetweenIcon}
+ />
</div>
);
}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 27843d33a..eb786d537 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -23,7 +23,9 @@ import { FieldViewProps } from "./FieldView";
import { Without, OmitKeys } from "../../../Utils";
import { Cast, StrCast, NumCast } from "../../../new_fields/Types";
import { List } from "../../../new_fields/List";
-import DetailedCaptionToggle from "../document_templates/caption_toggle/DetailedCaptionToggle";
+import { Doc } from "../../../new_fields/Doc";
+import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox";
+import { CollectionViewType } from "../collections/CollectionBaseView";
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
type BindingProps = Without<FieldViewProps, 'fieldKey'>;
@@ -48,11 +50,11 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
hideOnLeave?: boolean
}> {
@computed get layout(): string {
- const layout = Cast(this.props.Document[this.props.layoutKey], "string");
+ const layout = Cast(this.layoutDoc[this.props.layoutKey], "string");
if (layout === undefined) {
return this.props.Document.data ?
"<FieldView {...props} fieldKey='data' />" :
- KeyValueBox.LayoutString(this.props.Document.proto ? "proto" : "");
+ KeyValueBox.LayoutString(this.layoutDoc.proto ? "proto" : "");
} else if (typeof layout === "string") {
return layout;
} else {
@@ -60,8 +62,23 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
}
}
+ get dataDoc() {
+ if (this.props.DataDoc === undefined && this.props.Document.layout instanceof Doc) {
+ // if there is no dataDoc (ie, we're not rendering a temlplate layout), but this document
+ // has a template layout document, then we will render the template layout but use
+ // this document as the data document for the layout.
+ return this.props.Document;
+ }
+ return this.props.DataDoc;
+ }
+ get layoutDoc() {
+ // if this document's layout field contains a document (ie, a rendering template), then we will use that
+ // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string.
+ return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document;
+ }
+
CreateBindings(): JsxBindings {
- return { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit };
+ return { props: { ...OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit, Document: this.layoutDoc, DataDoc: this.dataDoc } };
}
@computed get templates(): List<string> {
@@ -81,7 +98,8 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
// by checking the layoutKey. This should probably be moved into
// a prop so that the overlay can explicitly turn off templates.
if ((this.props.layoutKey === "overlayLayout" && StrCast(this.props.Document.layout).indexOf("CollectionView") !== -1) ||
- (this.props.layoutKey === "layout" && StrCast(this.props.Document.layout).indexOf("CollectionView") === -1)) {
+ (this.props.layoutKey === "layout" && StrCast(this.props.Document.layout).indexOf("CollectionView") === -1) ||
+ (this.props.layoutKey === "layout" && NumCast(this.props.Document.viewType)) !== CollectionViewType.Freeform) {
this.templates.forEach(template => {
let self = this;
// this scales constants in the markup by the scaling applied to the document, but caps the constants to be smaller
@@ -102,9 +120,11 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
}
render() {
+ let self = this;
+ if (this.props.renderDepth > 7) return (null);
if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);
return <ObserverJsxParser
- components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
+ components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
bindings={this.CreateBindings()}
jsx={this.finalLayout}
showWarnings={true}
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 7c72fb6e6..3a4b46b7e 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -4,6 +4,7 @@
position: inherit;
top: 0;
left:0;
+ pointer-events: all;
// background: $light-color; //overflow: hidden;
transform-origin: left top;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index fdcb20e9a..5b5653309 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,12 +1,12 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faAlignCenter, faCaretSquareRight, faCompressArrowsAlt, faExpandArrowsAlt, faLayerGroup, faSquare, faTrash, faConciergeBell, faFolder, faMapPin, faLink, faFingerprint, faCrosshairs, faDesktop } from '@fortawesome/free-solid-svg-icons';
-import { action, computed, IReactionDisposer, reaction } from "mobx";
+import * as fa from '@fortawesome/free-solid-svg-icons';
+import { action, computed, IReactionDisposer, reaction, trace, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync } from "../../../new_fields/Doc";
import { List } from "../../../new_fields/List";
import { ObjectField } from "../../../new_fields/ObjectField";
-import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { BoolCast, Cast, FieldValue, StrCast, NumCast } from "../../../new_fields/Types";
+import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema";
+import { BoolCast, Cast, FieldValue, StrCast, NumCast, PromiseValue } from "../../../new_fields/Types";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { emptyFunction, Utils } from "../../../Utils";
import { DocServer } from "../../DocServer";
@@ -23,59 +23,74 @@ import { CollectionVideoView } from "../collections/CollectionVideoView";
import { CollectionView } from "../collections/CollectionView";
import { ContextMenu } from "../ContextMenu";
import { DocComponent } from "../DocComponent";
-import { PresentationView } from "../PresentationView";
-import { Template } from "./../Templates";
+import { PresentationView } from "../presentationview/PresentationView";
+import { Template, Templates } from "./../Templates";
import { DocumentContentsView } from "./DocumentContentsView";
+import * as rp from "request-promise";
import "./DocumentView.scss";
import React = require("react");
import { Id, Copy } from '../../../new_fields/FieldSymbols';
import { ContextMenuProps } from '../ContextMenuItem';
+import { list, object, createSimpleSchema } from 'serializr';
+import { LinkManager } from '../../util/LinkManager';
+import { RouteStore } from '../../../server/RouteStore';
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
-library.add(faTrash);
-library.add(faExpandArrowsAlt);
-library.add(faCompressArrowsAlt);
-library.add(faLayerGroup);
-library.add(faAlignCenter);
-library.add(faCaretSquareRight);
-library.add(faSquare);
-library.add(faConciergeBell);
-library.add(faFolder);
-library.add(faMapPin);
-library.add(faLink);
-library.add(faFingerprint);
-library.add(faCrosshairs);
-library.add(faDesktop);
-
-const linkSchema = createSchema({
- title: "string",
- linkDescription: "string",
- linkTags: "string",
- linkedTo: Doc,
- linkedFrom: Doc
-});
+library.add(fa.faTrash);
+library.add(fa.faShare);
+library.add(fa.faExpandArrowsAlt);
+library.add(fa.faCompressArrowsAlt);
+library.add(fa.faLayerGroup);
+library.add(fa.faExternalLinkAlt);
+library.add(fa.faAlignCenter);
+library.add(fa.faCaretSquareRight);
+library.add(fa.faSquare);
+library.add(fa.faConciergeBell);
+library.add(fa.faWindowRestore);
+library.add(fa.faFolder);
+library.add(fa.faMapPin);
+library.add(fa.faLink);
+library.add(fa.faFingerprint);
+library.add(fa.faCrosshairs);
+library.add(fa.faDesktop);
+library.add(fa.faUnlock);
+library.add(fa.faLock);
+
+
+// const linkSchema = createSchema({
+// title: "string",
+// linkDescription: "string",
+// linkTags: "string",
+// linkedTo: Doc,
+// linkedFrom: Doc
+// });
-type LinkDoc = makeInterface<[typeof linkSchema]>;
-const LinkDoc = makeInterface(linkSchema);
+// type LinkDoc = makeInterface<[typeof linkSchema]>;
+// const LinkDoc = makeInterface(linkSchema);
export interface DocumentViewProps {
ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
Document: Doc;
+ DataDoc?: Doc;
+ fitToBox?: () => number[];
addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean;
removeDocument?: (doc: Doc) => boolean;
moveDocument?: (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
- isTopMost: boolean;
+ renderDepth: number;
ContentScaling: () => number;
PanelWidth: () => number;
PanelHeight: () => number;
- focus: (doc: Doc) => void;
+ focus: (doc: Doc, willZoom: boolean) => void;
selectOnLoad: boolean;
parentActive: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
bringToFront: (doc: Doc) => void;
- addDocTab: (doc: Doc, where: string) => void;
+ addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void;
collapseToPoint?: (scrpt: number[], expandedDocs: Doc[] | undefined) => void;
+ zoomToScale: (scale: number) => void;
+ getScale: () => number;
+ animateBetweenIcon?: (iconPos: number[], startTime: number, maximizing: boolean) => void;
}
const schema = createSchema({
@@ -83,6 +98,8 @@ const schema = createSchema({
nativeWidth: "number",
nativeHeight: "number",
backgroundColor: "string",
+ opacity: "number",
+ hidden: "boolean"
});
export const positionSchema = createSchema({
@@ -112,7 +129,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
public get ContentDiv() { return this._mainCont.current; }
@computed get active(): boolean { return SelectionManager.IsSelected(this) || this.props.parentActive(); }
- @computed get topMost(): boolean { return this.props.isTopMost; }
+ @computed get topMost(): boolean { return this.props.renderDepth === 0; }
@computed get templates(): List<string> {
let field = this.props.Document.templates;
if (field && field instanceof List) {
@@ -123,6 +140,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
set templates(templates: List<string>) { this.props.Document.templates = templates; }
screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect();
+ constructor(props: DocumentViewProps) {
+ super(props);
+ }
+
+ _animateToIconDisposer?: IReactionDisposer;
_reactionDisposer?: IReactionDisposer;
@action
componentDidMount() {
@@ -144,8 +166,35 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this.props.Document.proto!.title = "-" + sumDoc.title + ".expanded";
}
}, { fireImmediately: true });
+ this._animateToIconDisposer = reaction(() => this.props.Document.isIconAnimating, (values) =>
+ (values instanceof List) && this.animateBetweenIcon(values, values[2], values[3] ? true : false)
+ , { fireImmediately: true });
DocumentManager.Instance.DocumentViews.push(this);
}
+
+ animateBetweenIcon = (iconPos: number[], startTime: number, maximizing: boolean) => {
+ this.props.animateBetweenIcon ? this.props.animateBetweenIcon(iconPos, startTime, maximizing) :
+ DocumentView.animateBetweenIconFunc(this.props.Document, this.Document[WidthSym](), this.Document[HeightSym](), startTime, maximizing);
+ }
+
+ public static animateBetweenIconFunc = (doc: Doc, width: number, height: number, stime: number, maximizing: boolean, cb?: (progress: number) => void) => {
+ setTimeout(() => {
+ let now = Date.now();
+ let progress = now < stime + 200 ? Math.min(1, (now - stime) / 200) : 1;
+ doc.width = progress === 1 ? width : maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress;
+ doc.height = progress === 1 ? height : maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress;
+ cb && cb(progress);
+ if (now < stime + 200) {
+ DocumentView.animateBetweenIconFunc(doc, width, height, stime, maximizing, cb);
+ }
+ else {
+ Doc.GetProto(doc).isMinimized = !maximizing;
+ Doc.GetProto(doc).isIconAnimating = undefined;
+ }
+ Doc.GetProto(doc).willMaximize = false;
+ },
+ 2);
+ }
@action
componentDidUpdate() {
if (this._dropDisposer) {
@@ -160,6 +209,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@action
componentWillUnmount() {
if (this._reactionDisposer) this._reactionDisposer();
+ if (this._animateToIconDisposer) this._animateToIconDisposer();
if (this._dropDisposer) this._dropDisposer();
DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
}
@@ -168,11 +218,21 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
e.stopPropagation();
}
+ get dataDoc() {
+ if (this.props.DataDoc === undefined && this.props.Document.layout instanceof Doc) {
+ // if there is no dataDoc (ie, we're not rendering a temlplate layout), but this document
+ // has a template layout document, then we will render the template layout but use
+ // this document as the data document for the layout.
+ return this.props.Document;
+ }
+ return this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined;
+ }
startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean) {
if (this._mainCont.current) {
let allConnected = [this.props.Document, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])];
+ let alldataConnected = [this.dataDoc, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])];
const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0);
- let dragData = new DragManager.DocumentDragData(allConnected);
+ let dragData = new DragManager.DocumentDragData(allConnected, alldataConnected);
const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top);
dragData.dropAction = dropAction;
dragData.xOffset = xoff;
@@ -189,9 +249,36 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
toggleMinimized = async () => {
let minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc);
if (minimizedDoc) {
- let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(
+ let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(
NumCast(minimizedDoc.x) - NumCast(this.Document.x), NumCast(minimizedDoc.y) - NumCast(this.Document.y));
- this.props.collapseToPoint && this.props.collapseToPoint(scrpt, await DocListCastAsync(minimizedDoc.maximizedDocs));
+ this.collapseTargetsToPoint(scrpt, await DocListCastAsync(minimizedDoc.maximizedDocs));
+ }
+ }
+
+ static _undoBatch?: UndoManager.Batch = undefined;
+ @action
+ public collapseTargetsToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => {
+ SelectionManager.DeselectAll();
+ if (expandedDocs) {
+ if (!DocumentView._undoBatch) {
+ DocumentView._undoBatch = UndoManager.StartBatch("iconAnimating");
+ }
+ let isMinimized: boolean | undefined;
+ expandedDocs.map(maximizedDoc => {
+ let iconAnimating = Cast(maximizedDoc.isIconAnimating, List);
+ if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) {
+ if (isMinimized === undefined) {
+ isMinimized = BoolCast(maximizedDoc.isMinimized, false);
+ }
+ maximizedDoc.willMaximize = isMinimized;
+ maximizedDoc.isMinimized = false;
+ maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]);
+ }
+ });
+ setTimeout(() => {
+ DocumentView._undoBatch && DocumentView._undoBatch.end();
+ DocumentView._undoBatch = undefined;
+ }, 500);
}
}
@@ -199,8 +286,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
e.stopPropagation();
let altKey = e.altKey;
let ctrlKey = e.ctrlKey;
- if (this._doubleTap && !this.props.isTopMost) {
- this.props.addDocTab(this.props.Document, "inTab");
+ if (this._doubleTap && this.props.renderDepth) {
+ let fullScreenAlias = Doc.MakeAlias(this.props.Document);
+ fullScreenAlias.templates = new List<string>();
+ this.props.addDocTab(fullScreenAlias, this.dataDoc, "inTab");
SelectionManager.DeselectAll();
this.props.Document.libraryBrush = false;
}
@@ -214,8 +303,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs);
let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs);
let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs);
- let linkedToDocs = await DocListCastAsync(this.props.Document.linkedToDocs, []);
- let linkedFromDocs = await DocListCastAsync(this.props.Document.linkedFromDocs, []);
+ let linkedDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document);
let expandedDocs: Doc[] = [];
expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs;
expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
@@ -225,8 +313,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
let expandedProtoDocs = expandedDocs.map(doc => Doc.GetProto(doc));
let maxLocation = StrCast(this.props.Document.maximizeLocation, "inPlace");
let getDispDoc = (target: Doc) => Object.getOwnPropertyNames(target).indexOf("isPrototype") === -1 ? target : Doc.MakeDelegate(target);
- if (altKey) {
- maxLocation = this.props.Document.maximizeLocation = (maxLocation === "inPlace" || !maxLocation ? "inTab" : "inPlace");
+ if (altKey || ctrlKey) {
+ maxLocation = this.props.Document.maximizeLocation = (ctrlKey ? maxLocation : (maxLocation === "inPlace" || !maxLocation ? "inTab" : "inPlace"));
if (!maxLocation || maxLocation === "inPlace") {
let hadView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedProtoDocs[0], this.props.ContainingCollectionView);
let wasMinimized = !hadView && expandedDocs.reduce((min, d) => !min && !BoolCast(d.IsMinimized, false), false);
@@ -238,29 +326,30 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
expandedProtoDocs.forEach(maxDoc => maxDoc.isMinimized = wasMinimized);
}
}
- if (maxLocation && maxLocation !== "inPlace" && CollectionDockingView.TopLevel) {
- let dataDocs = DocListCast(CollectionDockingView.TopLevel.props.Document.data);
+ if (maxLocation && maxLocation !== "inPlace" && CollectionDockingView.Instance) {
+ let dataDocs = DocListCast(CollectionDockingView.Instance.props.Document.data);
if (dataDocs) {
expandedDocs.forEach(maxDoc =>
- (!CollectionDockingView.CloseRightSplit(Doc.GetProto(maxDoc)) &&
- this.props.addDocTab(getDispDoc(maxDoc), maxLocation)));
+ (!CollectionDockingView.Instance.CloseRightSplit(Doc.GetProto(maxDoc)) &&
+ this.props.addDocTab(getDispDoc(maxDoc), undefined, maxLocation)));
}
} else {
- let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2);
- this.props.collapseToPoint && this.props.collapseToPoint(scrpt, expandedProtoDocs);
+ let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2);
+ this.collapseTargetsToPoint(scrpt, expandedProtoDocs);
}
}
- else if (linkedToDocs.length || linkedFromDocs.length) {
- let linkedFwdDocs = [
- linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : expandedDocs[0],
- linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : expandedDocs[0]];
-
- let linkedFwdPage = [
- linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : undefined,
- linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : undefined];
+ else if (linkedDocs.length) {
+ let first = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor1 as Doc, this.props.Document));
+ let linkedFwdDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : [expandedDocs[0], expandedDocs[0]];
+
+ let linkedFwdContextDocs = [first.length ? await (first[0].context) as Doc : undefined, undefined];
+
+ let linkedFwdPage = [first.length ? NumCast(first[0].linkedToPage, undefined) : undefined, undefined];
+
if (!linkedFwdDocs.some(l => l instanceof Promise)) {
- let maxLocation = StrCast(linkedFwdDocs[altKey ? 1 : 0].maximizeLocation, "inTab");
- DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, document => this.props.addDocTab(document, maxLocation), linkedFwdPage[altKey ? 1 : 0]);
+ let maxLocation = StrCast(linkedFwdDocs[0].maximizeLocation, "inTab");
+ let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined;
+ DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, false, document => this.props.addDocTab(document, undefined, maxLocation), linkedFwdPage[altKey ? 1 : 0], targetContext);
}
}
}
@@ -270,23 +359,23 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this._downX = e.clientX;
this._downY = e.clientY;
this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0;
- if (e.shiftKey && e.buttons === 1 && CollectionDockingView.TopLevel) {
- CollectionDockingView.TopLevel.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e);
- e.stopPropagation();
- } else {
- if (this.active) e.stopPropagation(); // events stop at the lowest document that is active.
- document.removeEventListener("pointermove", this.onPointerMove);
- document.addEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointerup", this.onPointerUp);
- }
+ // if (e.shiftKey && e.buttons === 1 && CollectionDockingView.Instance) {
+ // CollectionDockingView.Instance.StartOtherDrag(e, [Doc.MakeAlias(this.props.Document)], [this.dataDoc]);
+ // e.stopPropagation();
+ // } else {
+ if (this.active) e.stopPropagation(); // events stop at the lowest document that is active.
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointerup", this.onPointerUp);
+ // }
}
onPointerMove = (e: PointerEvent): void => {
if (!e.cancelBubble && this.active) {
- if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
+ if (!this.props.Document.excludeFromLibrary && (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3)) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
- if (!e.altKey && !this.topMost && e.buttons === 1) {
+ if (!e.altKey && !this.topMost && e.buttons === 1 && !BoolCast(this.props.Document.lockedPosition)) {
this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander);
}
}
@@ -301,22 +390,29 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this._lastTap = Date.now();
}
- deleteClicked = (): void => { this.props.removeDocument && this.props.removeDocument(this.props.Document); }
- fieldsClicked = (): void => { this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }), "onRight") };
+ @undoBatch
+ deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); }
+
+ @undoBatch
+ fieldsClicked = (): void => { let kvp = Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.dataDoc, "onRight"); }
+
+ @undoBatch
makeBtnClicked = (): void => {
let doc = Doc.GetProto(this.props.Document);
doc.isButton = !BoolCast(doc.isButton, false);
- if (StrCast(doc.layout).indexOf("Formatted") !== -1) { // only need to freeze the dimensions of text boxes since they don't have a native width and height naturally
- if (doc.isButton && !doc.nativeWidth) {
+ if (doc.isButton) {
+ if (!doc.nativeWidth) {
doc.nativeWidth = this.props.Document[WidthSym]();
doc.nativeHeight = this.props.Document[HeightSym]();
- } else {
- doc.nativeWidth = doc.nativeHeight = undefined;
}
+ } else {
+ doc.nativeWidth = doc.nativeHeight = undefined;
}
}
- fullScreenClicked = (): void => {
- CollectionDockingView.TopLevel && CollectionDockingView.OpenFullScreen(Doc.MakeCopy(this.props.Document, false));
+
+ @undoBatch
+ public fullScreenClicked = (): void => {
+ CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this);
SelectionManager.DeselectAll();
}
@@ -327,6 +423,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
let sourceDoc = de.data.linkSourceDocument;
let destDoc = this.props.Document;
+ e.stopPropagation();
if (de.mods === "AltKey") {
const protoDest = destDoc.proto;
const protoSrc = sourceDoc.proto;
@@ -337,10 +434,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
dst.nativeHeight = src.nativeHeight;
}
else {
- DocUtils.MakeLink(sourceDoc, destDoc);
+ // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true);
+ // const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView);
+ DocUtils.MakeLink(sourceDoc, destDoc, this.props.ContainingCollectionView ? this.props.ContainingCollectionView.props.Document : undefined);
de.data.droppedDocuments.push(destDoc);
}
- e.stopPropagation();
}
}
@@ -372,29 +470,33 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
this.templates = this.templates;
}
+ @action
+ clearTemplates = () => {
+ this.templates.length = 0;
+ this.templates = this.templates;
+ }
- freezeNativeDimensions = (e: React.MouseEvent): void => {
- if (NumCast(this.props.Document.nativeWidth)) {
- let proto = Doc.GetProto(this.props.Document);
- let nw = proto.nativeWidth;
- let nh = proto.nativeHeight;
- proto.nativeWidth = proto.nativeHeight = undefined;
- this.props.Document.width = this.props.Document.frozenWidth;
- this.props.Document.height = this.props.Document.frozenHeight;
- }
- else {
- let scale = this.props.ScreenToLocalTransform().Scale * NumCast(this.props.Document.zoomBasis, 1);
- let scr = this.screenRect();
- let proto = Doc.GetProto(this.props.Document);
- this.props.Document.frozenWidth = this.props.Document.width;
- this.props.Document.frozenHeight = this.props.Document.height;
- this.props.Document.height = proto.nativeHeight = scr.height * scale;
- this.props.Document.width = proto.nativeWidth = scr.width * scale;
+ @undoBatch
+ @action
+ freezeNativeDimensions = (): void => {
+ let proto = Doc.GetProto(this.props.Document);
+ if (proto.ignoreAspect === undefined && !proto.nativeWidth) {
+ proto.nativeWidth = this.props.PanelWidth();
+ proto.nativeHeight = this.props.PanelHeight();
+ proto.ignoreAspect = true;
}
+ proto.ignoreAspect = !BoolCast(proto.ignoreAspect, false);
+ }
+
+ @undoBatch
+ @action
+ toggleLockPosition = (): void => {
+ this.props.Document.lockedPosition = BoolCast(this.props.Document.lockedPosition) ? undefined : true;
}
@action
- onContextMenu = (e: React.MouseEvent): void => {
+ onContextMenu = async (e: React.MouseEvent): Promise<void> => {
+ e.persist();
e.stopPropagation();
if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3 ||
e.isDefaultPrevented()) {
@@ -406,68 +508,110 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const cm = ContextMenu.Instance;
let subitems: ContextMenuProps[] = [];
subitems.push({ description: "Open Full Screen", event: this.fullScreenClicked, icon: "desktop" });
- subitems.push({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, "inTab"), icon: "folder" });
- subitems.push({ description: "Open Tab Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), "inTab"), icon: "folder" });
- subitems.push({ description: "Open Right", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, "onRight"), icon: "caret-square-right" });
- subitems.push({ description: "Open Right Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), "onRight"), icon: "caret-square-right" });
+ subitems.push({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, this.dataDoc, "inTab"), icon: "folder" });
+ subitems.push({ description: "Open Tab Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.dataDoc, "inTab"), icon: "folder" });
+ subitems.push({ description: "Open Right", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, this.dataDoc, "onRight"), icon: "caret-square-right" });
+ subitems.push({ description: "Open Right Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.dataDoc, "onRight"), icon: "caret-square-right" });
subitems.push({ description: "Open Fields", event: this.fieldsClicked, icon: "layer-group" });
- cm.addItem({ description: "Open...", subitems: subitems });
- cm.addItem({ description: NumCast(this.props.Document.nativeWidth) ? "Unfreeze" : "Freeze", event: this.freezeNativeDimensions, icon: "edit" });
+ cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" });
+ cm.addItem({ description: BoolCast(this.props.Document.ignoreAspect, false) || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "edit" });
cm.addItem({ description: "Pin to Pres", event: () => PresentationView.Instance.PinDoc(this.props.Document), icon: "map-pin" });
+ cm.addItem({ description: BoolCast(this.props.Document.lockedPosition) ? "Unlock Pos" : "Lock Pos", event: this.toggleLockPosition, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" });
cm.addItem({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeBtnClicked, icon: "concierge-bell" });
cm.addItem({
+ description: "Make Portal", event: () => {
+ let portal = Docs.Create.FreeformDocument([], { width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".portal" });
+ Doc.GetProto(this.props.Document).subBulletDocs = new List<Doc>([portal]);
+ //summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight"
+ Doc.GetProto(this.props.Document).templates = new List<string>([Templates.Bullet.Layout]);
+ let coll = Docs.Create.StackingDocument([this.props.Document, portal], { x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y), width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".cont" });
+ this.props.addDocument && this.props.addDocument(coll);
+ this.props.removeDocument && this.props.removeDocument(this.props.Document);
+ }, icon: "window-restore"
+ });
+ cm.addItem({
description: "Find aliases", event: async () => {
const aliases = await SearchUtil.GetAliasesOfDocument(this.props.Document);
- this.props.addDocTab && this.props.addDocTab(Docs.Create.SchemaDocument(["title"], aliases, {}), "onRight");
+ this.props.addDocTab && this.props.addDocTab(Docs.Create.SchemaDocument(["title"], aliases, {}), undefined, "onRight"); // bcz: dataDoc?
}, icon: "search"
});
- cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document), icon: "crosshairs" });
- cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.Util.prepend("/doc/" + this.props.Document[Id])), icon: "link" });
+ cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" });
+ cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])), icon: "link" });
cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" });
cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" });
- if (!this.topMost) {
- // DocumentViews should stop propagation of this event
- e.stopPropagation();
- }
- ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
- if (!SelectionManager.IsSelected(this)) {
- SelectionManager.SelectDoc(this, false);
- }
+ type User = { email: string, userDocumentId: string };
+ const users: User[] = JSON.parse(await rp.get(DocServer.prepend(RouteStore.getUsers)));
+ let usersMenu: ContextMenuProps[] = users.filter(({ email }) => email !== CurrentUserUtils.email).map(({ email, userDocumentId }) => ({
+ description: email, event: async () => {
+ const userDocument = await Cast(DocServer.GetRefField(userDocumentId), Doc);
+ if (!userDocument) {
+ throw new Error(`Couldn't get user document of user ${email}`);
+ }
+ const notifDoc = await Cast(userDocument.optionalRightCollection, Doc);
+ if (notifDoc instanceof Doc) {
+ const data = await Cast(notifDoc.data, listSpec(Doc));
+ const sharedDoc = Doc.MakeAlias(this.props.Document);
+ if (data) {
+ data.push(sharedDoc);
+ } else {
+ notifDoc.data = new List([sharedDoc]);
+ }
+ }
+ }
+ }));
+ runInAction(() => {
+ cm.addItem({ description: "Share...", subitems: usersMenu, icon: "share" });
+ if (!this.topMost) {
+ // DocumentViews should stop propagation of this event
+ e.stopPropagation();
+ }
+ ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
+ if (!SelectionManager.IsSelected(this)) {
+ SelectionManager.SelectDoc(this, false);
+ }
+ });
}
onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; };
onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; };
isSelected = () => SelectionManager.IsSelected(this);
- select = (ctrlPressed: boolean) => SelectionManager.SelectDoc(this, ctrlPressed);
+ @action select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); };
@computed get nativeWidth() { return this.Document.nativeWidth || 0; }
@computed get nativeHeight() { return this.Document.nativeHeight || 0; }
- @computed get contents() { return (<DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} layoutKey={"layout"} />); }
+ @computed get contents() {
+ return (<DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} selectOnLoad={this.props.selectOnLoad} layoutKey={"layout"} DataDoc={this.dataDoc} />);
+ }
render() {
- var scaling = this.props.ContentScaling();
- var nativeHeight = this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
+ if (this.Document.hidden) {
+ return null;
+ }
+ let self = this;
+ let backgroundColor = this.props.Document.layout instanceof Doc ? StrCast(this.props.Document.layout.backgroundColor) : this.Document.backgroundColor;
+ let foregroundColor = StrCast(this.props.Document.layout instanceof Doc ? this.props.Document.layout.color : this.props.Document.color);
var nativeWidth = this.nativeWidth > 0 ? `${this.nativeWidth}px` : "100%";
-
+ var nativeHeight = BoolCast(this.props.Document.ignoreAspect) ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
return (
- <div className={`documentView-node${this.props.isTopMost ? "-topmost" : ""}`}
+ <div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
ref={this._mainCont}
style={{
+ color: foregroundColor,
outlineColor: "maroon",
outlineStyle: "dashed",
- outlineWidth: BoolCast(this.props.Document.libraryBrush, false) ||
- BoolCast(this.props.Document.protoBrush, false) ?
- `${1 * this.props.ScreenToLocalTransform().Scale}px`
- : "0px",
+ outlineWidth: BoolCast(this.props.Document.libraryBrush) && !StrCast(this.props.Document.borderRounding) ?
+ `${this.props.ScreenToLocalTransform().Scale}px` : "0px",
+ border: BoolCast(this.props.Document.libraryBrush) && StrCast(this.props.Document.borderRounding) ?
+ `dashed maroon ${this.props.ScreenToLocalTransform().Scale}px` : undefined,
borderRadius: "inherit",
- background: this.Document.backgroundColor || "",
+ background: backgroundColor,
width: nativeWidth,
height: nativeHeight,
- transform: `scale(${scaling}, ${scaling})`
+ transform: `scale(${this.props.ContentScaling()})`,
+ opacity: this.Document.opacity
}}
onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
-
onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
>
{this.contents}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 74453488a..ac9c42d05 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -1,26 +1,23 @@
import React = require("react");
+import { computed } from "mobx";
import { observer } from "mobx-react";
-import { computed, observable } from "mobx";
-import { FormattedTextBox } from "./FormattedTextBox";
-import { ImageBox } from "./ImageBox";
-import { VideoBox } from "./VideoBox";
-import { AudioBox } from "./AudioBox";
-import { DocumentContentsView } from "./DocumentContentsView";
+import { DateField } from "../../../new_fields/DateField";
+import { Doc, FieldResult, Opt } from "../../../new_fields/Doc";
+import { IconField } from "../../../new_fields/IconField";
+import { List } from "../../../new_fields/List";
+import { RichTextField } from "../../../new_fields/RichTextField";
+import { AudioField, ImageField, VideoField } from "../../../new_fields/URLField";
import { Transform } from "../../util/Transform";
-import { returnFalse, emptyFunction, returnOne } from "../../../Utils";
-import { CollectionView } from "../collections/CollectionView";
import { CollectionPDFView } from "../collections/CollectionPDFView";
import { CollectionVideoView } from "../collections/CollectionVideoView";
+import { CollectionView } from "../collections/CollectionView";
+import { AudioBox } from "./AudioBox";
+import { FormattedTextBox } from "./FormattedTextBox";
import { IconBox } from "./IconBox";
-import { Opt, Doc, FieldResult } from "../../../new_fields/Doc";
-import { List } from "../../../new_fields/List";
-import { ImageField, VideoField, AudioField } from "../../../new_fields/URLField";
-import { IconField } from "../../../new_fields/IconField";
-import { RichTextField } from "../../../new_fields/RichTextField";
-import { DateField } from "../../../new_fields/DateField";
-import { NumCast } from "../../../new_fields/Types";
-import ImageCard from "../document_templates/image_card/ImageCard";
-
+import { ImageBox } from "./ImageBox";
+import { PDFBox } from "./PDFBox";
+import { VideoBox } from "./VideoBox";
+import { Id } from "../../../new_fields/FieldSymbols";
//
// these properties get assigned through the render() method of the DocumentView when it creates this node.
@@ -29,14 +26,18 @@ import ImageCard from "../document_templates/image_card/ImageCard";
//
export interface FieldViewProps {
fieldKey: string;
+ fieldExt: string;
+ leaveNativeSize?: boolean;
+ fitToBox?: () => number[];
ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
Document: Doc;
+ DataDoc?: Doc;
isSelected: () => boolean;
select: (isCtrlPressed: boolean) => void;
- isTopMost: boolean;
+ renderDepth: number;
selectOnLoad: boolean;
addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean;
- addDocTab: (document: Doc, where: string) => void;
+ addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
removeDocument?: (document: Doc) => boolean;
moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
@@ -46,6 +47,7 @@ export interface FieldViewProps {
PanelWidth: () => number;
PanelHeight: () => number;
setVideoBox?: (player: VideoBox) => void;
+ setPdfBox?: (player: PDFBox) => void;
}
@observer
@@ -71,7 +73,7 @@ export class FieldView extends React.Component<FieldViewProps> {
return <FormattedTextBox {...this.props} />;
}
else if (field instanceof ImageField) {
- return <ImageBox {...this.props} />;
+ return <ImageBox {...this.props} leaveNativeSize={true} />;
}
else if (field instanceof IconField) {
return <IconBox {...this.props} />;
@@ -85,31 +87,32 @@ export class FieldView extends React.Component<FieldViewProps> {
return <p>{field.date.toLocaleString()}</p>;
}
else if (field instanceof Doc) {
- let returnHundred = () => 100;
- return (
- <DocumentContentsView Document={field}
- addDocument={undefined}
- addDocTab={this.props.addDocTab}
- removeDocument={undefined}
- ScreenToLocalTransform={Transform.Identity}
- ContentScaling={returnOne}
- PanelWidth={returnHundred}
- PanelHeight={returnHundred}
- isTopMost={true} //TODO Why is this top most?
- selectOnLoad={false}
- focus={emptyFunction}
- isSelected={this.props.isSelected}
- select={returnFalse}
- layoutKey={"layout"}
- ContainingCollectionView={this.props.ContainingCollectionView}
- parentActive={this.props.active}
- whenActiveChanged={this.props.whenActiveChanged}
- bringToFront={emptyFunction} />
- );
+ return <p><b>{field.title + " : id= " + field[Id]}</b></p>;
+ // let returnHundred = () => 100;
+ // return (
+ // <DocumentContentsView Document={field}
+ // addDocument={undefined}
+ // addDocTab={this.props.addDocTab}
+ // removeDocument={undefined}
+ // ScreenToLocalTransform={Transform.Identity}
+ // ContentScaling={returnOne}
+ // PanelWidth={returnHundred}
+ // PanelHeight={returnHundred}
+ // renderDepth={0} //TODO Why is renderDepth reset?
+ // selectOnLoad={false}
+ // focus={emptyFunction}
+ // isSelected={this.props.isSelected}
+ // select={returnFalse}
+ // layoutKey={"layout"}
+ // ContainingCollectionView={this.props.ContainingCollectionView}
+ // parentActive={this.props.active}
+ // whenActiveChanged={this.props.whenActiveChanged}
+ // bringToFront={emptyFunction} />
+ // );
}
else if (field instanceof List) {
return (<div>
- {field.map(f => f instanceof Doc ? f.title : f.toString()).join(", ")}
+ {field.map(f => f instanceof Doc ? f.title : (f && f.toString && f.toString())).join(", ")}
</div>);
}
// bcz: this belongs here, but it doesn't render well so taking it out for now
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 4a29c1949..d3045ae2f 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -1,7 +1,7 @@
@import "../globalCssVariables";
.ProseMirror {
width: 100%;
- height: auto;
+ height: 100%;
min-height: 100%;
font-family: $serif;
}
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 6a14a04f7..96ee6f200 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,58 +1,51 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import { faEdit, faSmile } from '@fortawesome/free-solid-svg-icons';
-import { action, IReactionDisposer, observable, reaction } from "mobx";
+import { action, IReactionDisposer, observable, reaction, runInAction, computed } from "mobx";
import { observer } from "mobx-react";
import { baseKeymap } from "prosemirror-commands";
import { history } from "prosemirror-history";
import { keymap } from "prosemirror-keymap";
+import { NodeType } from 'prosemirror-model';
import { EditorState, Plugin, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
-import { Doc, Field, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc";
+import { Doc, Opt } from "../../../new_fields/Doc";
+import { Id } from '../../../new_fields/FieldSymbols';
+import { List } from '../../../new_fields/List';
import { RichTextField } from "../../../new_fields/RichTextField";
-import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
+import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
import { DocServer } from "../../DocServer";
-import { DocumentManager } from "../../util/DocumentManager";
+import { Docs } from '../../documents/Documents';
+import { DocumentManager } from '../../util/DocumentManager';
import { DragManager } from "../../util/DragManager";
-import buildKeymap from "../../util/ProsemirrorKeymap";
+import buildKeymap from "../../util/ProsemirrorExampleTransfer";
import { inpRules } from "../../util/RichTextRules";
-import { ImageResizeView, schema } from "../../util/RichTextSchema";
+import { ImageResizeView, schema, SummarizedView } from "../../util/RichTextSchema";
import { SelectionManager } from "../../util/SelectionManager";
import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu";
import { TooltipTextMenu } from "../../util/TooltipTextMenu";
import { undoBatch, UndoManager } from "../../util/UndoManager";
import { ContextMenu } from "../../views/ContextMenu";
-import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { ContextMenuProps } from '../ContextMenuItem';
import { DocComponent } from "../DocComponent";
import { InkingControl } from "../InkingControl";
+import { Templates } from '../Templates';
import { FieldView, FieldViewProps } from "./FieldView";
import "./FormattedTextBox.scss";
import React = require("react");
-import { DocUtils } from '../../documents/Documents';
library.add(faEdit);
library.add(faSmile);
// FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document
//
-// HTML Markup: <FormattedTextBox Doc={Document's ID} FieldKey={Key's name}
-//
-// In Code, the node's HTML is specified in the document's parameterized structure as:
-// document.SetField(KeyStore.Layout, "<FormattedTextBox doc={doc} fieldKey={<KEYNAME>Key} />");
-// and the node's binding to the specified document KEYNAME as:
-// document.SetField(KeyStore.LayoutKeys, new ListField([KeyStore.<KEYNAME>]));
-// The Jsx parser at run time will bind:
-// 'fieldKey' property to the Key stored in LayoutKeys
-// and 'doc' property to the document that is being rendered
-//
-// When rendered() by React, this extracts the TextController from the Document stored at the
-// specified Key and assigns it to an HTML input node. When changes are made to this node,
-// this will edit the document and assign the new value to that field.
-//]
export interface FormattedTextBoxProps {
isOverlay?: boolean;
hideOnLeave?: boolean;
+ height?: string;
+ color?: string;
+ outer_div?: (domminus: HTMLElement) => void;
}
const richTextSchema = createSchema({
@@ -68,68 +61,135 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
return FieldView.LayoutString(FormattedTextBox, fieldStr);
}
private _ref: React.RefObject<HTMLDivElement>;
- private _proseRef: React.RefObject<HTMLDivElement>;
+ private _outerdiv?: (dominus: HTMLElement) => void;
+ private _proseRef?: HTMLDivElement;
private _editorView: Opt<EditorView>;
- private _gotDown: boolean = false;
- private _dropDisposer?: DragManager.DragDropDisposer;
+ private _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
+ private _applyingChange: boolean = false;
+ private _linkClicked = "";
private _reactionDisposer: Opt<IReactionDisposer>;
- private _inputReactionDisposer: Opt<IReactionDisposer>;
private _proxyReactionDisposer: Opt<IReactionDisposer>;
+ private dropDisposer?: DragManager.DragDropDisposer;
public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
+ @observable _entered = false;
@observable public static InputBoxOverlay?: FormattedTextBox = undefined;
public static InputBoxOverlayScroll: number = 0;
+ public static IsFragment(html: string) {
+ return html.indexOf("data-pm-slice") !== -1;
+ }
+ public static GetHref(html: string): string {
+ let parser = new DOMParser();
+ let parsedHtml = parser.parseFromString(html, 'text/html');
+ if (parsedHtml.body.childNodes.length === 1 && parsedHtml.body.childNodes[0].childNodes.length === 1 &&
+ (parsedHtml.body.childNodes[0].childNodes[0] as any).href) {
+ return (parsedHtml.body.childNodes[0].childNodes[0] as any).href;
+ }
+ return "";
+ }
+ public static GetDocFromUrl(url: string) {
+ if (url.startsWith(document.location.origin)) {
+ const split = new URL(url).pathname.split("doc/");
+ const docid = split[split.length - 1];
+ return docid;
+ }
+ return "";
+ }
+
+ @undoBatch
+ public setFontColor(color: string) {
+ let self = this;
+ if (this._editorView!.state.selection.from === this._editorView!.state.selection.to) return false;
+ if (this._editorView!.state.selection.to - this._editorView!.state.selection.from > this._editorView!.state.doc.nodeSize - 3) {
+ this.props.Document.color = color;
+ }
+ let colorMark = this._editorView!.state.schema.mark(this._editorView!.state.schema.marks.pFontColor, { color: color });
+ this._editorView!.dispatch(this._editorView!.state.tr.addMark(this._editorView!.state.selection.from,
+ this._editorView!.state.selection.to, colorMark));
+ return true;
+ }
constructor(props: FieldViewProps) {
super(props);
+ if (this.props.outer_div) {
+ this._outerdiv = this.props.outer_div;
+ }
this._ref = React.createRef();
- this._proseRef = React.createRef();
if (this.props.isOverlay) {
DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);
}
}
- _applyingChange: boolean = false;
+ @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
- _lastState: any = undefined;
dispatchTransaction = (tx: Transaction) => {
if (this._editorView) {
- const state = this._lastState = this._editorView.state.apply(tx);
+ const state = this._editorView.state.apply(tx);
this._editorView.updateState(state);
this._applyingChange = true;
- Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new RichTextField(JSON.stringify(state.toJSON())));
- Doc.SetOnPrototype(this.props.Document, "documentText", state.doc.textBetween(0, state.doc.content.size, "\n\n"));
+ Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()));
+ Doc.GetProto(this.dataDoc)[this.props.fieldKey + "_text"] = state.doc.textBetween(0, state.doc.content.size, "\n\n");
this._applyingChange = false;
- let title = StrCast(this.props.Document.title);
+ let title = StrCast(this.dataDoc.title);
if (title && title.startsWith("-") && this._editorView) {
let str = this._editorView.state.doc.textContent;
let titlestr = str.substr(0, Math.min(40, str.length));
- let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
+ let target = this.dataDoc.proto ? this.dataDoc.proto : this.dataDoc;
target.title = "-" + titlestr + (str.length > 40 ? "..." : "");
}
}
}
+ protected createDropTarget = (ele: HTMLDivElement) => {
+ this._proseRef = ele;
+ if (this.dropDisposer) {
+ this.dropDisposer();
+ }
+ if (ele) {
+ this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+ }
+ }
+
@undoBatch
@action
drop = async (e: Event, de: DragManager.DropEvent) => {
- if (de.data instanceof DragManager.LinkDragData) {
- let sourceDoc = de.data.linkSourceDocument;
- let destDoc = this.props.Document;
-
- DocUtils.MakeLink(sourceDoc, destDoc);
- de.data.droppedDocuments.push(destDoc);
+ // We're dealing with a link to a document
+ if (de.data instanceof DragManager.EmbedDragData && de.data.urlField) {
+ // We're dealing with an internal document drop
+ let url = de.data.urlField.url.href;
+ let model: NodeType = (url.includes(".mov") || url.includes(".mp4")) ? schema.nodes.video : schema.nodes.image;
+ this._editorView!.dispatch(this._editorView!.state.tr.insert(0, model.create({ src: url })));
e.stopPropagation();
+ } else {
+ if (de.data instanceof DragManager.DocumentDragData) {
+ this.props.Document.layout = de.data.draggedDocuments[0];
+ de.data.draggedDocuments[0].isTemplate = true;
+ e.stopPropagation();
+ // let ldocs = Cast(this.dataDoc.subBulletDocs, listSpec(Doc));
+ // if (!ldocs) {
+ // this.dataDoc.subBulletDocs = new List<Doc>([]);
+ // }
+ // ldocs = Cast(this.dataDoc.subBulletDocs, listSpec(Doc));
+ // if (!ldocs) return;
+ // if (!ldocs || !ldocs[0] || ldocs[0] instanceof Promise || StrCast((ldocs[0] as Doc).layout).indexOf("CollectionView") === -1) {
+ // ldocs.splice(0, 0, Docs.StackingDocument([], { title: StrCast(this.dataDoc.title) + "-subBullets", x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document.height), width: 300, height: 300 }));
+ // this.props.addDocument && this.props.addDocument(ldocs[0] as Doc);
+ // this.props.Document.templates = new List<string>([Templates.Bullet.Layout]);
+ // this.props.Document.isBullet = true;
+ // }
+ // let stackDoc = (ldocs[0] as Doc);
+ // if (de.data.moveDocument) {
+ // de.data.moveDocument(de.data.draggedDocuments[0], stackDoc, (doc) => {
+ // Cast(stackDoc.data, listSpec(Doc))!.push(doc);
+ // return true;
+ // });
+ // }
+ }
}
}
componentDidMount() {
- if (this._ref.current) {
- this._dropDisposer = DragManager.MakeDropTarget(this._ref.current, {
- handlers: { drop: this.drop.bind(this) }
- });
- }
const config = {
schema,
inpRules, //these currently don't do anything, but could eventually be helpful
@@ -151,57 +211,58 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
]
};
- if (this.props.isOverlay) {
- this._inputReactionDisposer = reaction(() => FormattedTextBox.InputBoxOverlay,
- () => {
- if (this._editorView) {
- this._editorView.destroy();
- }
- this.setupEditor(config, this.props.Document);// MainOverlayTextBox.Instance.TextDoc); // bcz: not sure why, but the order of events is such that this.props.Document hasn't updated yet, so without forcing the editor to the MainOverlayTextBox, it will display the previously focused textbox
- }
- );
- } else {
+ if (!this.props.isOverlay) {
this._proxyReactionDisposer = reaction(() => this.props.isSelected(),
() => {
if (this.props.isSelected()) {
FormattedTextBox.InputBoxOverlay = this;
FormattedTextBox.InputBoxOverlayScroll = this._ref.current!.scrollTop;
}
- });
+ }, { fireImmediately: true });
}
-
this._reactionDisposer = reaction(
() => {
- const field = this.props.Document ? Cast(this.props.Document[this.props.fieldKey], RichTextField) : undefined;
- return field ? field.Data : undefined;
+ const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined;
+ return field ? field.Data : `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`;
},
- field => field && this._editorView && !this._applyingChange &&
+ field => this._editorView && !this._applyingChange &&
this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field)))
);
- this.setupEditor(config, this.props.Document);
+ this.setupEditor(config, this.dataDoc, this.props.fieldKey);
}
- private setupEditor(config: any, doc?: Doc) {
- let field = doc ? Cast(doc[this.props.fieldKey], RichTextField) : undefined;
- if (this._proseRef.current) {
- this._editorView = new EditorView(this._proseRef.current, {
+ private setupEditor(config: any, doc: Doc, fieldKey: string) {
+ let field = doc ? Cast(doc[fieldKey], RichTextField) : undefined;
+ let startup = StrCast(doc.documentText);
+ startup = startup.startsWith("@@@") ? startup.replace("@@@", "") : "";
+ if (!field && doc) {
+ let text = StrCast(doc[fieldKey]);
+ if (text) {
+ startup = text;
+ } else if (Cast(doc[fieldKey], "number")) {
+ startup = NumCast(doc[fieldKey], 99).toString();
+ }
+ }
+ if (this._proseRef) {
+ this._editorView = new EditorView(this._proseRef, {
state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config),
dispatchTransaction: this.dispatchTransaction,
nodeViews: {
- image(node, view, getPos) { return new ImageResizeView(node, view, getPos); }
+ image(node, view, getPos) { return new ImageResizeView(node, view, getPos); },
+ star(node, view, getPos) { return new SummarizedView(node, view, getPos); }
}
});
- let text = StrCast(this.props.Document.documentText);
- if (text.startsWith("@@@")) {
- this.props.Document.proto!.documentText = undefined;
- this._editorView.dispatch(this._editorView.state.tr.insertText(text.replace("@@@", "")));
+ if (startup) {
+ Doc.GetProto(doc).documentText = undefined;
+ this._editorView.dispatch(this._editorView.state.tr.insertText(startup));
}
}
if (this.props.selectOnLoad) {
- this.props.select(false);
- this._editorView!.focus();
+ if (!this.props.isOverlay) this.props.select(false);
+ else this._editorView!.focus();
+ this.tryUpdateHeight();
}
}
@@ -212,36 +273,38 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (this._reactionDisposer) {
this._reactionDisposer();
}
- if (this._inputReactionDisposer) {
- this._inputReactionDisposer();
- }
if (this._proxyReactionDisposer) {
this._proxyReactionDisposer();
}
- if (this._dropDisposer) {
- this._dropDisposer();
- }
}
onPointerDown = (e: React.PointerEvent): void => {
if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) {
e.stopPropagation();
if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) {
- this._toolTipTextMenu.tooltip.style.opacity = "0";
+ //this._toolTipTextMenu.tooltip.style.opacity = "0";
}
}
let ctrlKey = e.ctrlKey;
if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) {
let href = (e.target as any).href;
for (let parent = (e.target as any).parentNode; !href && parent; parent = parent.parentNode) {
- href = parent.childNodes[0].href;
+ href = parent.childNodes[0].href ? parent.childNodes[0].href : parent.href;
}
if (href) {
- if (href.indexOf(DocServer.Util.prepend("/doc/")) === 0) {
- let docid = href.replace(DocServer.Util.prepend("/doc/"), "").split("?")[0];
- DocServer.getRefField(docid).then(f => {
- (f instanceof Doc) && DocumentManager.Instance.jumpToDocument(f, ctrlKey, document => this.props.addDocTab(document, "inTab"))
- });
+ if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
+ this._linkClicked = href.replace(DocServer.prepend("/doc/"), "").split("?")[0];
+ if (this._linkClicked) {
+ DocServer.GetRefField(this._linkClicked).then(f => {
+ (f instanceof Doc) && DocumentManager.Instance.jumpToDocument(f, ctrlKey, false, document => this.props.addDocTab(document, undefined, "inTab"));
+ });
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ } else {
+ let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.props.Document.x, 0) + NumCast(this.props.Document.width, 0), y: NumCast(this.props.Document.y) });
+ this.props.addDocument && this.props.addDocument(webDoc);
+ this._linkClicked = webDoc[Id];
}
e.stopPropagation();
e.preventDefault();
@@ -249,13 +312,12 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
- this._gotDown = true;
e.preventDefault();
}
}
onPointerUp = (e: React.PointerEvent): void => {
if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) {
- this._toolTipTextMenu.tooltip.style.opacity = "1";
+ //this._toolTipTextMenu.tooltip.style.opacity = "1";
}
if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
e.stopPropagation();
@@ -279,7 +341,12 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
onClick = (e: React.MouseEvent): void => {
- this._proseRef.current!.focus();
+ this._proseRef!.focus();
+ if (this._linkClicked) {
+ this._linkClicked = "";
+ e.preventDefault();
+ e.stopPropagation();
+ }
}
onMouseDown = (e: React.MouseEvent): void => {
if (!this.props.isSelected()) { // preventing default allows the onClick to be generated instead of being swallowed by the text box itself
@@ -295,9 +362,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops);
}
});
+ //this.props.Document.tooltip = self._toolTipTextMenu;
}
- _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
tooltipLinkingMenuPlugin() {
let myprops = this.props;
return new Plugin({
@@ -322,19 +389,31 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
// stop propagation doesn't seem to stop propagation of native keyboard events.
// so we set a flag on the native event that marks that the event's been handled.
(e.nativeEvent as any).DASHFormattedTextBoxHandled = true;
- if (StrCast(this.props.Document.title).startsWith("-") && this._editorView) {
+ if (StrCast(this.dataDoc.title).startsWith("-") && this._editorView) {
let str = this._editorView.state.doc.textContent;
let titlestr = str.substr(0, Math.min(40, str.length));
- let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
+ let target = this.dataDoc.proto ? this.dataDoc.proto : this.dataDoc;
target.title = "-" + titlestr + (str.length > 40 ? "..." : "");
}
if (!this._undoTyping) {
this._undoTyping = UndoManager.StartBatch("undoTyping");
}
+ this.tryUpdateHeight();
+ }
+
+ @action
+ tryUpdateHeight() {
+ if (this.props.isOverlay && this.props.Document.autoHeight) {
+ let xf = this._ref.current!.getBoundingClientRect();
+ let scrBounds = this.props.ScreenToLocalTransform().transformBounds(0, 0, xf.width, xf.height);
+ let nh = NumCast(this.dataDoc.nativeHeight, 0);
+ let dh = NumCast(this.props.Document.height, 0);
+ let sh = scrBounds.height;
+ this.props.Document.height = nh ? dh / nh * sh : sh;
+ this.dataDoc.proto!.nativeHeight = nh ? sh : undefined;
+ }
}
- @observable
- _entered = false;
@action
onPointerEnter = (e: React.PointerEvent) => {
this._entered = true;
@@ -343,22 +422,34 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
onPointerLeave = (e: React.PointerEvent) => {
this._entered = false;
}
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ let subitems: ContextMenuProps[] = [];
+ subitems.push({
+ description: BoolCast(this.props.Document.autoHeight, false) ? "Manual Height" : "Auto Height",
+ event: action(() => Doc.GetProto(this.props.Document).autoHeight = !BoolCast(this.props.Document.autoHeight, false)), icon: "expand-arrows-alt"
+ });
+ ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: subitems });
+ }
render() {
+ let self = this;
let style = this.props.isOverlay ? "scroll" : "hidden";
- let rounded = NumCast(this.props.Document.borderRounding) < 0 ? "-rounded" : "";
+ let rounded = StrCast(this.props.Document.borderRounding) === "100%" ? "-rounded" : "";
let interactive = InkingControl.Instance.selectedTool ? "" : "interactive";
return (
<div className={`formattedTextBox-cont-${style}`} ref={this._ref}
style={{
+ height: this.props.height ? this.props.height : undefined,
background: this.props.hideOnLeave ? "rgba(0,0,0,0.4)" : undefined,
opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || this.props.Document.libraryBrush ? 1 : 0.1) : 1,
- color: this.props.hideOnLeave ? "white" : "initial",
+ color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit",
pointerEvents: interactive ? "all" : "none",
+ fontSize: "13px"
}}
- // onKeyDown={this.onKeyPress}
- onKeyPress={this.onKeyPress}
+ onKeyDown={this.onKeyPress}
onFocus={this.onFocused}
onClick={this.onClick}
+ onContextMenu={this.specificContextMenu}
onBlur={this.onBlur}
onPointerUp={this.onPointerUp}
onPointerDown={this.onPointerDown}
@@ -368,7 +459,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
onPointerEnter={this.onPointerEnter}
onPointerLeave={this.onPointerLeave}
>
- <div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: this.props.Document.isButton && !this.props.isSelected() ? "none" : "all" }} ref={this._proseRef} />
+ <div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} style={{ whiteSpace: "pre-wrap", pointerEvents: this.props.Document.isButton && !this.props.isSelected() ? "none" : "all" }} />
</div>
);
}
diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx
index 00021bc78..d6ab2a34a 100644
--- a/src/client/views/nodes/IconBox.tsx
+++ b/src/client/views/nodes/IconBox.tsx
@@ -37,14 +37,14 @@ export class IconBox extends React.Component<FieldViewProps> {
return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />;
}
- setLabelField = (e: React.MouseEvent): void => {
+ setLabelField = (): void => {
this.props.Document.hideLabel = !BoolCast(this.props.Document.hideLabel);
}
- setUseOwnTitleField = (e: React.MouseEvent): void => {
+ setUseOwnTitleField = (): void => {
this.props.Document.useOwnTitle = !BoolCast(this.props.Document.useTargetTitle);
}
- specificContextMenu = (e: React.MouseEvent): void => {
+ specificContextMenu = (): void => {
ContextMenu.Instance.addItem({
description: BoolCast(this.props.Document.hideLabel) ? "Show label with icon" : "Remove label from icon",
event: this.setLabelField
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 4c2b73b70..f0363d0b8 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,25 +1,27 @@
-import { action, observable, trace } from 'mobx';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faImage } from '@fortawesome/free-solid-svg-icons';
+import { action, observable, computed } from 'mobx';
import { observer } from "mobx-react";
import Lightbox from 'react-image-lightbox';
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
+import { Doc, HeightSym, WidthSym, DocListCast } from '../../../new_fields/Doc';
+import { List } from '../../../new_fields/List';
+import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema';
+import { Cast, FieldValue, NumCast, StrCast, BoolCast } from '../../../new_fields/Types';
+import { ImageField } from '../../../new_fields/URLField';
import { Utils } from '../../../Utils';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from "../../views/ContextMenu";
+import { ContextMenuProps } from '../ContextMenuItem';
+import { DocComponent } from '../DocComponent';
+import { InkingControl } from '../InkingControl';
+import { positionSchema } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import "./ImageBox.scss";
import React = require("react");
-import { createSchema, makeInterface, listSpec } from '../../../new_fields/Schema';
-import { DocComponent } from '../DocComponent';
-import { positionSchema } from './DocumentView';
-import { FieldValue, Cast, StrCast, PromiseValue, NumCast } from '../../../new_fields/Types';
-import { ImageField } from '../../../new_fields/URLField';
-import { List } from '../../../new_fields/List';
-import { InkingControl } from '../InkingControl';
-import { Doc, WidthSym, HeightSym } from '../../../new_fields/Doc';
-import { faImage } from '@fortawesome/free-solid-svg-icons';
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { ContextMenuItemProps, ContextMenuProps } from '../ContextMenuItem';
+import { RouteStore } from '../../../server/RouteStore';
+var requestImageSize = require('../../util/request-image-size');
var path = require('path');
@@ -27,7 +29,7 @@ library.add(faImage);
export const pageSchema = createSchema({
- curPage: "number"
+ curPage: "number",
});
type ImageDocument = makeInterface<[typeof pageSchema, typeof positionSchema]>;
@@ -36,16 +38,17 @@ const ImageDocument = makeInterface(pageSchema, positionSchema);
@observer
export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageDocument) {
- public static LayoutString() { return FieldView.LayoutString(ImageBox); }
+ public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ImageBox, fieldKey); }
private _imgRef: React.RefObject<HTMLImageElement> = React.createRef();
private _downX: number = 0;
private _downY: number = 0;
private _lastTap: number = 0;
- @observable private _photoIndex: number = 0;
@observable private _isOpen: boolean = false;
private dropDisposer?: DragManager.DragDropDisposer;
+ @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
+
protected createDropTarget = (ele: HTMLDivElement) => {
if (this.dropDisposer) {
@@ -61,24 +64,28 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
console.log("IMPLEMENT ME PLEASE");
}
+ @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.dataDoc, this.props.fieldKey, "Alternates"); }
@undoBatch
drop = (e: Event, de: DragManager.DropEvent) => {
if (de.data instanceof DragManager.DocumentDragData) {
de.data.droppedDocuments.forEach(action((drop: Doc) => {
- let layout = StrCast(drop.backgroundLayout);
- if (layout.indexOf(ImageBox.name) !== -1) {
- let imgData = this.props.Document[this.props.fieldKey];
- if (imgData instanceof ImageField) {
- Doc.SetOnPrototype(this.props.Document, "data", new List([imgData]));
- }
- let imgList = Cast(this.props.Document[this.props.fieldKey], listSpec(ImageField), [] as any[]);
- if (imgList) {
- let field = drop.data;
- if (field instanceof ImageField) imgList.push(field);
- else if (field instanceof List) imgList.concat(field);
- }
+ if (de.mods === "AltKey" && /*this.dataDoc !== this.props.Document &&*/ drop.data instanceof ImageField) {
+ Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new ImageField(drop.data.url);
e.stopPropagation();
+ } else if (de.mods === "CtrlKey") {
+ if (this.extensionDoc !== this.dataDoc) {
+ let layout = StrCast(drop.backgroundLayout);
+ if (layout.indexOf(ImageBox.name) !== -1) {
+ let imgData = this.extensionDoc.Alternates;
+ if (!imgData) {
+ Doc.GetProto(this.extensionDoc).Alternates = new List([]);
+ }
+ let imgList = Cast(this.extensionDoc.Alternates, listSpec(Doc), [] as any[]);
+ imgList && imgList.push(drop);
+ e.stopPropagation();
+ }
+ }
}
}));
// de.data.removeDocument() bcz: need to implement
@@ -86,9 +93,9 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
}
onPointerDown = (e: React.PointerEvent): void => {
- if (e.shiftKey && e.ctrlKey)
-
- e.stopPropagation();
+ if (e.shiftKey && e.ctrlKey) {
+ e.stopPropagation(); // allows default system drag drop of images with shift+ctrl only
+ }
// if (Date.now() - this._lastTap < 300) {
// if (e.buttons === 1) {
// this._downX = e.clientX;
@@ -109,20 +116,21 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
e.stopPropagation();
}
+ @action
lightbox = (images: string[]) => {
if (this._isOpen) {
return (<Lightbox
- mainSrc={images[this._photoIndex]}
- nextSrc={images[(this._photoIndex + 1) % images.length]}
- prevSrc={images[(this._photoIndex + images.length - 1) % images.length]}
+ mainSrc={images[this.Document.curPage || 0]}
+ nextSrc={images[((this.Document.curPage || 0) + 1) % images.length]}
+ prevSrc={images[((this.Document.curPage || 0) + images.length - 1) % images.length]}
onCloseRequest={action(() =>
this._isOpen = false
)}
onMovePrevRequest={action(() =>
- this._photoIndex = (this._photoIndex + images.length - 1) % images.length
+ this.Document.curPage = ((this.Document.curPage || 0) + images.length - 1) % images.length
)}
onMoveNextRequest={action(() =>
- this._photoIndex = (this._photoIndex + 1) % images.length
+ this.Document.curPage = ((this.Document.curPage || 0) + 1) % images.length
)}
/>);
}
@@ -136,12 +144,15 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
subitems.push({ description: "Copy path", event: () => Utils.CopyText(url), icon: "expand-arrows-alt" });
subitems.push({
description: "Rotate", event: action(() => {
- this.props.Document.rotation = (NumCast(this.props.Document.rotation) + 90) % 360;
+ let proto = Doc.GetProto(this.props.Document);
let nw = this.props.Document.nativeWidth;
- this.props.Document.nativeWidth = this.props.Document.nativeHeight;
- this.props.Document.nativeHeight = nw;
+ let nh = this.props.Document.nativeHeight;
let w = this.props.Document.width;
- this.props.Document.width = this.props.Document.height;
+ let h = this.props.Document.height;
+ proto.rotation = (NumCast(this.props.Document.rotation) + 90) % 360;
+ proto.nativeWidth = nh;
+ proto.nativeHeight = nw;
+ this.props.Document.width = h;
this.props.Document.height = w;
}), icon: "expand-arrows-alt"
});
@@ -151,7 +162,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
@action
onDotDown(index: number) {
- this._photoIndex = index;
this.Document.curPage = index;
}
@@ -161,7 +171,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
let left = (nativeWidth - paths.length * dist) / 2;
return paths.map((p, i) =>
<div className="imageBox-placer" key={i} >
- <div className="imageBox-dot" style={{ background: (i === this._photoIndex ? "black" : "gray"), transform: `translate(${i * dist + left}px, 0px)` }} onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); this.onDotDown(i); }} />
+ <div className="imageBox-dot" style={{ background: (i === this.Document.curPage ? "black" : "gray"), transform: `translate(${i * dist + left}px, 0px)` }} onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); this.onDotDown(i); }} />
</div>
);
}
@@ -172,7 +182,8 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
return url.href;
}
let ext = path.extname(url.href);
- return url.href.replace(ext, this._curSuffix + ext);
+ const suffix = this.props.renderDepth <= 1 ? "_o" : this._curSuffix;
+ return url.href.replace(ext, suffix + ext);
}
@observable _smallRetryCount = 1;
@@ -185,10 +196,31 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
}
@action onError = () => {
let timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount;
- if (timeout < 10)
+ if (timeout < 10) {
setTimeout(this.retryPath, Math.min(10000, timeout * 5));
+ }
}
_curSuffix = "_m";
+
+ resize(srcpath: string, layoutdoc: Doc) {
+ requestImageSize(window.origin + RouteStore.corsProxy + "/" + srcpath)
+ .then((size: any) => {
+ let aspect = size.height / size.width;
+ let rotation = NumCast(this.dataDoc.rotation) % 180;
+ if (rotation === 90 || rotation === 270) aspect = 1 / aspect;
+ if (Math.abs(layoutdoc[HeightSym]() / layoutdoc[WidthSym]() - aspect) > 0.01) {
+ setTimeout(action(() => {
+ layoutdoc.height = layoutdoc[WidthSym]() * aspect;
+ layoutdoc.nativeHeight = size.height;
+ layoutdoc.nativeWidth = size.width;
+ }), 0);
+ }
+ })
+ .catch((err: any) => {
+ console.log(err);
+ });
+ }
+
render() {
// let transform = this.props.ScreenToLocalTransform().inverse();
let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
@@ -198,29 +230,37 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
let id = (this.props as any).id; // bcz: used to set id = "isExpander" in templates.tsx
let nativeWidth = FieldValue(this.Document.nativeWidth, pw);
+ let nativeHeight = FieldValue(this.Document.nativeHeight, 0);
let paths: string[] = ["http://www.cs.brown.edu/~bcz/noImage.png"];
// this._curSuffix = "";
// if (w > 20) {
- let field = this.Document[this.props.fieldKey];
+ Doc.UpdateDocumentExtensionForField(this.extensionDoc, this.props.fieldKey);
+ let alts = DocListCast(this.extensionDoc.Alternates);
+ let altpaths: string[] = alts.filter(doc => doc.data instanceof ImageField).map(doc => this.choosePath((doc.data as ImageField).url));
+ let field = this.dataDoc[this.props.fieldKey];
// if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s";
// else if (w < 600 && this._mediumRetryCount < 10) this._curSuffix = "_m";
// else if (this._largeRetryCount < 10) this._curSuffix = "_l";
if (field instanceof ImageField) paths = [this.choosePath(field.url)];
- else if (field instanceof List) paths = field.filter(val => val instanceof ImageField).map(p => this.choosePath((p as ImageField).url));
+ paths.push(...altpaths);
// }
let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive";
- let rotation = NumCast(this.props.Document.rotation, 0);
- let aspect = (rotation % 180) ? this.props.Document[HeightSym]() / this.props.Document[WidthSym]() : 1;
- let shift = (rotation % 180) ? (this.props.Document[HeightSym]() - this.props.Document[WidthSym]() / aspect) / 2 : 0;
+ let rotation = NumCast(this.dataDoc.rotation, 0);
+ let aspect = (rotation % 180) ? this.dataDoc[HeightSym]() / this.dataDoc[WidthSym]() : 1;
+ let shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0;
+ let srcpath = paths[Math.min(paths.length, this.Document.curPage || 0)];
+
+ if (!this.props.Document.ignoreAspect && !this.props.leaveNativeSize) this.resize(srcpath, this.props.Document);
+
return (
- <div id={id} className={`imageBox-cont${interactive}`}
+ <div id={id} className={`imageBox-cont${interactive}`} style={{ background: "transparent" }}
onPointerDown={this.onPointerDown}
onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
<img id={id}
key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys
- src={paths[Math.min(paths.length, this._photoIndex)]}
+ src={srcpath}
style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }}
- // style={{ objectFit: (this._photoIndex === 0 ? undefined : "contain") }}
+ // style={{ objectFit: (this.Document.curPage === 0 ? undefined : "contain") }}
width={nativeWidth}
ref={this._imgRef}
onError={this.onError} />
diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss
index 20cae03d4..87a9565e8 100644
--- a/src/client/views/nodes/KeyValueBox.scss
+++ b/src/client/views/nodes/KeyValueBox.scss
@@ -91,12 +91,12 @@ $header-height: 30px;
width: 4px;
float: left;
height: 30px;
- width: 10px;
+ width: 5px;
z-index: 20;
right: 0;
top: 0;
- border-radius: 10px;
- background: gray;
+ border-radius: 0;
+ background: black;
pointer-events: all;
}
.keyValueBox-dividerDragger{
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 849f17aa4..c40840227 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -2,17 +2,30 @@
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
-import { CompileScript } from "../../util/Scripting";
+import { CompileScript, ScriptOptions } from "../../util/Scripting";
import { FieldView, FieldViewProps } from './FieldView';
import "./KeyValueBox.scss";
import { KeyValuePair } from "./KeyValuePair";
import React = require("react");
-import { NumCast, Cast, FieldValue } from "../../../new_fields/Types";
-import { Doc, Field } from "../../../new_fields/Doc";
+import { NumCast, Cast, FieldValue, StrCast } from "../../../new_fields/Types";
+import { Doc, Field, FieldResult, DocListCastAsync } from "../../../new_fields/Doc";
+import { ComputedField, ScriptField } from "../../../new_fields/ScriptField";
+import { SetupDrag } from "../../util/DragManager";
+import { Docs } from "../../documents/Documents";
+import { RawDataOperationParameters } from "../../northstar/model/idea/idea";
+import { Templates } from "../Templates";
+import { List } from "../../../new_fields/List";
+import { TextField } from "../../util/ProsemirrorCopy/prompt";
+import { RichTextField } from "../../../new_fields/RichTextField";
+import { ImageField } from "../../../new_fields/URLField";
+import { SelectionManager } from "../../util/SelectionManager";
+import { listSpec } from "../../../new_fields/Schema";
@observer
export class KeyValueBox extends React.Component<FieldViewProps> {
private _mainCont = React.createRef<HTMLDivElement>();
+ private _keyHeader = React.createRef<HTMLTableHeaderCellElement>();
+ @observable private rows: KeyValuePair[] = [];
public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(KeyValueBox, fieldStr); }
@observable private _keyInput: string = "";
@@ -27,28 +40,42 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
@action
onEnterKey = (e: React.KeyboardEvent): void => {
if (e.key === 'Enter') {
- if (this._keyInput && this._valueInput) {
- let doc = this.fieldDocToLayout;
- if (!doc) {
- return;
+ if (this._keyInput && this._valueInput && this.fieldDocToLayout) {
+ if (KeyValueBox.SetField(this.fieldDocToLayout, this._keyInput, this._valueInput)) {
+ this._keyInput = "";
+ this._valueInput = "";
}
- let realDoc = doc;
-
- let script = CompileScript(this._valueInput, { addReturn: true });
- if (!script.compiled) {
- return;
- }
- let res = script.run();
- if (!res.success) return;
- const field = res.result;
- if (Field.IsField(field)) {
- realDoc[this._keyInput] = field;
- }
- this._keyInput = "";
- this._valueInput = "";
}
}
}
+ public static SetField(doc: Doc, key: string, value: string) {
+ let eq = value.startsWith("=");
+ let target = eq ? doc : Doc.GetProto(doc);
+ value = eq ? value.substr(1) : value;
+ let dubEq = value.startsWith(":=") ? 1 : value.startsWith(";=") ? 2 : 0;
+ value = dubEq ? value.substr(2) : value;
+ let options: ScriptOptions = { addReturn: true, params: { this: "Doc" } };
+ if (dubEq) options.typecheck = false;
+ let script = CompileScript(value, options);
+ if (!script.compiled) {
+ return false;
+ }
+ let field: Field;
+ if (dubEq === 1) {
+ field = new ComputedField(script);
+ } else if (dubEq === 2) {
+ field = new ScriptField(script);
+ } else {
+ let res = script.run({ this: target });
+ if (!res.success) return false;
+ field = res.result;
+ }
+ if (Field.IsField(field, true)) {
+ target[key] = field;
+ return true;
+ }
+ return false;
+ }
onPointerDown = (e: React.PointerEvent): void => {
if (e.buttons === 1 && this.props.isSelected()) {
@@ -78,8 +105,16 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
let rows: JSX.Element[] = [];
let i = 0;
+ const self = this;
for (let key of Object.keys(ids).sort()) {
- rows.push(<KeyValuePair doc={realDoc} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />);
+ rows.push(<KeyValuePair doc={realDoc} ref={(function () {
+ let oldEl: KeyValuePair | undefined;
+ return (el: KeyValuePair) => {
+ if (oldEl) self.rows.splice(self.rows.indexOf(oldEl), 1);
+ oldEl = el;
+ if (el) self.rows.push(el);
+ };
+ })()} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />);
}
return rows;
}
@@ -123,6 +158,75 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
document.addEventListener('pointerup', this.onDividerUp);
}
+ getTemplate = async () => {
+ let parent = Docs.Create.StackingDocument([], { width: 800, height: 800, title: "Template" });
+ parent.singleColumn = false;
+ parent.columnWidth = 100;
+ for (let row of this.rows.filter(row => row.isChecked)) {
+ await this.createTemplateField(parent, row);
+ row.uncheck();
+ }
+ return parent;
+ }
+
+ createTemplateField = async (parentStackingDoc: Doc, row: KeyValuePair) => {
+ let metaKey = row.props.keyName;
+ let sourceDoc = await Cast(this.props.Document.data, Doc);
+ if (!sourceDoc) {
+ return;
+ }
+
+ let fieldTemplate = await this.inferType(sourceDoc[metaKey], metaKey);
+ let previousViewType = fieldTemplate.viewType;
+
+ // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)
+ let backgroundLayout = StrCast(fieldTemplate.backgroundLayout);
+ let layout = StrCast(fieldTemplate.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
+ if (backgroundLayout) {
+ layout = StrCast(fieldTemplate.layout).replace(/fieldKey={"annotations"}/, `fieldKey={"${metaKey}"} fieldExt={"annotations"}`);
+ backgroundLayout = backgroundLayout.replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
+ }
+ let nw = NumCast(fieldTemplate.nativeWidth);
+ let nh = NumCast(fieldTemplate.nativeHeight);
+
+ fieldTemplate.title = metaKey;
+ fieldTemplate.layout = layout;
+ fieldTemplate.backgroundLayout = backgroundLayout;
+ fieldTemplate.nativeWidth = nw;
+ fieldTemplate.nativeHeight = nh;
+ fieldTemplate.embed = true;
+ fieldTemplate.isTemplate = true;
+ fieldTemplate.templates = new List<string>([Templates.TitleBar(metaKey)]);
+ fieldTemplate.proto = Doc.GetProto(parentStackingDoc);
+ previousViewType && (fieldTemplate.viewType = previousViewType);
+
+ Cast(parentStackingDoc.data, listSpec(Doc))!.push(fieldTemplate);
+ }
+
+ inferType = async (data: FieldResult, metaKey: string) => {
+ let options = { width: 300, height: 300, title: metaKey };
+ if (data instanceof RichTextField || typeof data === "string" || typeof data === "number") {
+ return Docs.Create.TextDocument(options);
+ } else if (data instanceof List) {
+ if (data.length === 0) {
+ return Docs.Create.StackingDocument([], options);
+ }
+ let first = await Cast(data[0], Doc);
+ if (!first) {
+ return Docs.Create.StackingDocument([], options);
+ }
+ switch (first.type) {
+ case "image":
+ return Docs.Create.StackingDocument([], options);
+ case "text":
+ return Docs.Create.TreeDocument([], options);
+ }
+ } else if (data instanceof ImageField) {
+ return Docs.Create.ImageDocument("https://image.flaticon.com/icons/png/512/23/23765.png", options);
+ }
+ return new Doc;
+ }
+
render() {
let dividerDragger = this.splitPercentage === 0 ? (null) :
<div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}>
@@ -133,7 +237,9 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
<table className="keyValueBox-table">
<tbody className="keyValueBox-tbody">
<tr className="keyValueBox-header">
- <th className="keyValueBox-key" style={{ width: `${100 - this.splitPercentage}%` }}>Key</th>
+ <th className="keyValueBox-key" style={{ width: `${100 - this.splitPercentage}%` }} ref={this._keyHeader}
+ onPointerDown={SetupDrag(this._keyHeader, this.getTemplate)}
+ >Key</th>
<th className="keyValueBox-fields" style={{ width: `${this.splitPercentage}%` }}>Fields</th>
</tr>
{this.createTable()}
@@ -143,4 +249,4 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
{dividerDragger}
</div>);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss
index a1c5d5537..f78767234 100644
--- a/src/client/views/nodes/KeyValuePair.scss
+++ b/src/client/views/nodes/KeyValuePair.scss
@@ -3,6 +3,7 @@
.keyValuePair-td-key {
display:inline-block;
+
.keyValuePair-td-key-container{
width:100%;
height:100%;
@@ -10,14 +11,23 @@
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
+ align-items: center;
.keyValuePair-td-key-delete{
position: relative;
background-color: transparent;
color:red;
}
+ .keyValuePair-td-key-check {
+ position: relative;
+ margin: 0;
+ }
.keyValuePair-keyField {
width:100%;
- text-align: center;
+ margin-left: 20px;
+ margin-top: -1px;
+ font-family: monospace;
+ // text-align: center;
+ align-self: center;
position: relative;
overflow: auto;
}
@@ -26,12 +36,25 @@
.keyValuePair-td-value {
display:inline-block;
overflow: scroll;
- img {
- max-height: 36px;
- width: auto;
- }
- .videoBox-cont{
- width: auto;
- max-height: 36px;
+ font-family: monospace;
+ height: 30px;
+ .keyValuePair-td-value-container {
+ display: flex;
+ align-items: center;
+ align-content: center;
+ flex-direction: row;
+ justify-content: space-between;
+ flex-wrap: nowrap;
+ width: 100%;
+ height: 100%;
+
+ img {
+ max-height: 36px;
+ width: auto;
+ }
+ .videoBox-cont{
+ width: auto;
+ max-height: 36px;
+ }
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 228d07018..b5db52ac7 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -2,7 +2,7 @@ import { action, observable } from 'mobx';
import { observer } from "mobx-react";
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
import { emptyFunction, returnFalse, returnZero, returnTrue } from '../../../Utils';
-import { CompileScript } from "../../util/Scripting";
+import { CompileScript, CompiledScript, ScriptOptions } from "../../util/Scripting";
import { Transform } from '../../util/Transform';
import { EditableView } from "../EditableView";
import { FieldView, FieldViewProps } from './FieldView';
@@ -11,6 +11,8 @@ import "./KeyValuePair.scss";
import React = require("react");
import { Doc, Opt, Field } from '../../../new_fields/Doc';
import { FieldValue } from '../../../new_fields/Types';
+import { KeyValueBox } from './KeyValueBox';
+import { DragManager, SetupDrag } from '../../util/DragManager';
// Represents one row in a key value plane
@@ -22,15 +24,31 @@ export interface KeyValuePairProps {
}
@observer
export class KeyValuePair extends React.Component<KeyValuePairProps> {
+ @observable private isPointerOver = false;
+ @observable public isChecked = false;
+ private checkbox = React.createRef<HTMLInputElement>();
+
+ @action
+ handleCheck = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.isChecked = e.currentTarget.checked;
+ }
+
+ @action
+ uncheck = () => {
+ this.checkbox.current!.checked = false;
+ this.isChecked = false;
+ }
render() {
let props: FieldViewProps = {
Document: this.props.doc,
+ DataDoc: this.props.doc,
ContainingCollectionView: undefined,
fieldKey: this.props.keyName,
+ fieldExt: "",
isSelected: returnFalse,
select: emptyFunction,
- isTopMost: false,
+ renderDepth: 1,
selectOnLoad: false,
active: returnFalse,
whenActiveChanged: emptyFunction,
@@ -38,15 +56,19 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
focus: emptyFunction,
PanelWidth: returnZero,
PanelHeight: returnZero,
- addDocTab: emptyFunction
+ addDocTab: returnZero,
};
let contents = <FieldView {...props} />;
- let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")";
+ // let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")";
+ let keyStyle = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? "black" : "blue";
+
+ let hover = { transition: "0.3s ease opacity", opacity: this.isPointerOver || this.isChecked ? 1 : 0 };
+
return (
- <tr className={this.props.rowStyle}>
+ <tr className={this.props.rowStyle} onPointerEnter={action(() => this.isPointerOver = true)} onPointerLeave={action(() => this.isPointerOver = false)}>
<td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}>
<div className="keyValuePair-td-key-container">
- <button className="keyValuePair-td-key-delete" onClick={() => {
+ <button style={hover} className="keyValuePair-td-key-delete" onClick={() => {
if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) {
props.Document[props.fieldKey] = undefined;
}
@@ -54,33 +76,35 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
}}>
X
</button>
- <div className="keyValuePair-keyField">{fieldKey}</div>
+ <input
+ className={"keyValuePair-td-key-check"}
+ type="checkbox"
+ style={hover}
+ onChange={this.handleCheck}
+ ref={this.checkbox}
+ />
+ <div className="keyValuePair-keyField" style={{ color: keyStyle }}>{props.fieldKey}</div>
</div>
</td>
<td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }}>
- <EditableView contents={contents} height={36} GetValue={() => {
+ <div className="keyValuePair-td-value-container">
+ <EditableView
+ contents={contents}
+ height={36}
+ GetValue={() => {
+ const onDelegate = Object.keys(props.Document).includes(props.fieldKey);
- let field = FieldValue(props.Document[props.fieldKey]);
- if (Field.IsField(field)) {
- return Field.toScriptString(field);
- }
- return "";
- }}
- SetValue={(value: string) => {
- let script = CompileScript(value, { addReturn: true });
- if (!script.compiled) {
- return false;
- }
- let res = script.run();
- if (!res.success) return false;
- const field = res.result;
- if (Field.IsField(field, true)) {
- props.Document[props.fieldKey] = field;
- return true;
- }
- return false;
- }}>
- </EditableView></td>
+ let field = FieldValue(props.Document[props.fieldKey]);
+ if (Field.IsField(field)) {
+ return (onDelegate ? "=" : "") + Field.toScriptString(field);
+ }
+ return "";
+ }}
+ SetValue={(value: string) =>
+ KeyValueBox.SetField(props.Document, props.fieldKey, value)}>
+ </EditableView>
+ </div>
+ </td>
</tr>
);
}
diff --git a/src/client/views/nodes/LinkBox.scss b/src/client/views/nodes/LinkBox.scss
deleted file mode 100644
index 639f83b38..000000000
--- a/src/client/views/nodes/LinkBox.scss
+++ /dev/null
@@ -1,66 +0,0 @@
-@import "../globalCssVariables";
-.link-container {
- width: 100%;
- height: 50px;
- display: flex;
- flex-direction: row;
- border-top: 0.5px solid #bababa;
-}
-
-.info-container {
- width: 65%;
- padding-top: 5px;
- padding-left: 5px;
- display: flex;
- flex-direction: column
-}
-
-.link-name {
- font-size: 11px;
-}
-
-.doc-name {
- font-size: 8px;
-}
-
-.button-container {
- width: 35%;
- padding-top: 8px;
- display: flex;
- flex-direction: row;
-}
-
-.button {
- height: 20px;
- width: 20px;
- margin: 8px 4px;
- border-radius: 50%;
- opacity: 0.9;
- pointer-events: auto;
- background-color: $dark-color;
- color: $light-color;
- text-transform: uppercase;
- letter-spacing: 2px;
- font-size: 60%;
- transition: transform 0.2s;
-}
-
-.button:hover {
- background: $main-accent;
- cursor: pointer;
-}
-
-// .fa-icon-view {
-// margin-left: 3px;
-// margin-top: 5px;
-// }
-
-.fa-icon-edit {
- margin-left: 6px;
- margin-top: 6px;
-}
-
-.fa-icon-delete {
- margin-left: 7px;
- margin-top: 6px;
-} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
deleted file mode 100644
index 68b692aad..000000000
--- a/src/client/views/nodes/LinkBox.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEdit, faEye, faTimes } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { observer } from "mobx-react";
-import { DocumentManager } from "../../util/DocumentManager";
-import { undoBatch } from "../../util/UndoManager";
-import { CollectionDockingView } from "../collections/CollectionDockingView";
-import './LinkBox.scss';
-import React = require("react");
-import { Doc } from '../../../new_fields/Doc';
-import { Cast, NumCast } from '../../../new_fields/Types';
-import { listSpec } from '../../../new_fields/Schema';
-import { action } from 'mobx';
-
-
-library.add(faEye);
-library.add(faEdit);
-library.add(faTimes);
-
-interface Props {
- linkDoc: Doc;
- linkName: String;
- pairedDoc: Doc;
- type: String;
- showEditor: () => void;
-}
-
-@observer
-export class LinkBox extends React.Component<Props> {
-
- @undoBatch
- onViewButtonPressed = async (e: React.PointerEvent): Promise<void> => {
- e.stopPropagation();
- DocumentManager.Instance.jumpToDocument(this.props.pairedDoc, e.altKey);
- }
-
- onEditButtonPressed = (e: React.PointerEvent): void => {
- e.stopPropagation();
-
- this.props.showEditor();
- }
-
- @action
- onDeleteButtonPressed = async (e: React.PointerEvent): Promise<void> => {
- e.stopPropagation();
- const [linkedFrom, linkedTo] = await Promise.all([Cast(this.props.linkDoc.linkedFrom, Doc), Cast(this.props.linkDoc.linkedTo, Doc)]);
- if (linkedFrom) {
- const linkedToDocs = Cast(linkedFrom.linkedToDocs, listSpec(Doc));
- if (linkedToDocs) {
- linkedToDocs.splice(linkedToDocs.indexOf(this.props.linkDoc), 1);
- }
- }
- if (linkedTo) {
- const linkedFromDocs = Cast(linkedTo.linkedFromDocs, listSpec(Doc));
- if (linkedFromDocs) {
- linkedFromDocs.splice(linkedFromDocs.indexOf(this.props.linkDoc), 1);
- }
- }
- }
-
- render() {
-
- return (
- //<LinkEditor linkBox={this} linkDoc={this.props.linkDoc} />
- <div className="link-container">
- <div className="info-container" onPointerDown={this.onViewButtonPressed}>
- <div className="link-name">
- <p>{this.props.linkName}</p>
- </div>
- <div className="doc-name">
- <p>{this.props.type}{this.props.pairedDoc.Title}</p>
- </div>
- </div>
-
- <div className="button-container">
- {/* <div title="Follow Link" className="button" onPointerDown={this.onViewButtonPressed}>
- <FontAwesomeIcon className="fa-icon-view" icon="eye" size="sm" /></div> */}
- <div title="Edit Link" className="button" onPointerDown={this.onEditButtonPressed}>
- <FontAwesomeIcon className="fa-icon-edit" icon="edit" size="sm" /></div>
- <div title="Delete Link" className="button" onPointerDown={this.onDeleteButtonPressed}>
- <FontAwesomeIcon className="fa-icon-delete" icon="times" size="sm" /></div>
- </div>
- </div>
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss
index 9629585d7..3c49c2212 100644
--- a/src/client/views/nodes/LinkEditor.scss
+++ b/src/client/views/nodes/LinkEditor.scss
@@ -1,42 +1,133 @@
@import "../globalCssVariables";
-.edit-container {
+
+.linkEditor {
width: 100%;
height: auto;
+ font-size: 12px; // TODO
+}
+
+.linkEditor-back {
+ margin-bottom: 6px;
+}
+
+.linkEditor-info {
+ border-bottom: 0.5px solid $light-color-secondary;
+ padding-bottom: 6px;
+ margin-bottom: 6px;
display: flex;
- flex-direction: column;
+ justify-content: space-between;
+
+ .linkEditor-linkedTo {
+ width: calc(100% - 26px);
+ }
}
-.name-input {
- margin-bottom: 10px;
- padding: 5px;
- font-size: 12px;
- border: 1px solid #bababa;
+.linkEditor-button {
+ width: 20px;
+ height: 20px;
+ margin-left: 6px;
+ padding: 0;
+ // font-size: 12px;
+ border-radius: 10px;
+
+ &:disabled {
+ background-color: gray;
+ }
}
-.description-input {
- font-size: 11px;
- padding: 5px;
- margin-bottom: 10px;
- border: 1px solid #bababa;
+.linkEditor-groupsLabel {
+ display: flex;
+ justify-content: space-between;
+}
+
+.linkEditor-group {
+ background-color: $light-color-secondary;
+ padding: 6px;
+ margin: 3px 0;
+ border-radius: 3px;
+
+ .linkEditor-group-row {
+ // display: flex;
+ margin-bottom: 3px;
+
+ .linkEditor-group-row-label {
+ margin-right: 6px;
+ }
+ }
+
+ .linkEditor-metadata-row {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 6px;
+
+ .linkEditor-error {
+ border-color: red;
+ }
+
+ input {
+ width: calc(50% - 16px);
+ height: 20px;
+ }
+
+ button {
+ width: 20px;
+ height: 20px;
+ margin-left: 3px;
+ padding: 0;
+ font-size: 10px;
+ }
+ }
}
-.save-button {
- width: 50px;
- height: 22px;
- pointer-events: auto;
- background-color: $dark-color;
- color: $light-color;
- text-transform: uppercase;
- letter-spacing: 2px;
- padding: 2px;
- font-size: 10px;
- margin: 0 auto;
- transition: transform 0.2s;
- text-align: center;
- line-height: 20px;
+
+.linkEditor-dropdown {
+ width: 100%;
+ position: relative;
+ z-index: 999;
+
+ input {
+ width: 100%;
+ }
+
+ .linkEditor-options-wrapper {
+ width: 100%;
+ position: absolute;
+ top: 19px;
+ left: 0;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .linkEditor-option {
+ background-color: $light-color-secondary;
+ border: 1px solid $intermediate-color;
+ border-top: 0;
+ padding: 3px;
+ cursor: pointer;
+
+ &:hover {
+ background-color: lightgray;
+ }
+ }
}
-.save-button:hover {
- background: $main-accent;
- cursor: pointer;
+.linkEditor-group-buttons {
+ height: 20px;
+ display: flex;
+ justify-content: flex-end;
+
+ .linkEditor-button {
+ margin-left: 6px;
+ }
+
+ // .linkEditor-groupOpts {
+ // width: calc(20% - 3px);
+ // height: 20px;
+ // padding: 0;
+ // font-size: 10px;
+
+ // &:disabled {
+ // background-color: gray;
+ // }
+ // }
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 71a423338..ec8cb33ab 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -1,57 +1,345 @@
-import { observable, computed, action } from "mobx";
+import { observable, computed, action, trace } from "mobx";
import React = require("react");
-import { SelectionManager } from "../../util/SelectionManager";
import { observer } from "mobx-react";
import './LinkEditor.scss';
-import { props } from "bluebird";
-import { DocumentView } from "./DocumentView";
-import { link } from "fs";
-import { StrCast } from "../../../new_fields/Types";
+import { StrCast, Cast, FieldValue } from "../../../new_fields/Types";
import { Doc } from "../../../new_fields/Doc";
+import { LinkManager } from "../../util/LinkManager";
+import { Docs } from "../../documents/Documents";
+import { Utils } from "../../../Utils";
+import { faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus } from '@fortawesome/free-solid-svg-icons';
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { SetupDrag } from "../../util/DragManager";
-interface Props {
- linkDoc: Doc;
- showLinks: () => void;
+library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus);
+
+
+interface GroupTypesDropdownProps {
+ groupType: string;
+ setGroupType: (group: string) => void;
+}
+// this dropdown could be generalized
+@observer
+class GroupTypesDropdown extends React.Component<GroupTypesDropdownProps> {
+ @observable private _searchTerm: string = "";
+ @observable private _groupType: string = this.props.groupType;
+
+ @action setSearchTerm = (value: string): void => { this._searchTerm = value; };
+ @action setGroupType = (value: string): void => { this._groupType = value; };
+
+ @action
+ createGroup = (groupType: string): void => {
+ this.props.setGroupType(groupType);
+ LinkManager.Instance.addGroupType(groupType);
+ }
+
+ onChange = (val: string): void => {
+ this.setSearchTerm(val);
+ this.setGroupType(val);
+ }
+
+ renderOptions = (): JSX.Element[] | JSX.Element => {
+ if (this._searchTerm === "") return <></>;
+
+ let allGroupTypes = Array.from(LinkManager.Instance.getAllGroupTypes());
+ let groupOptions = allGroupTypes.filter(groupType => groupType.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
+ let exactFound = groupOptions.findIndex(groupType => groupType.toUpperCase() === this._searchTerm.toUpperCase()) > -1;
+
+ let options = groupOptions.map(groupType => {
+ return <div key={groupType} className="linkEditor-option"
+ onClick={() => { this.props.setGroupType(groupType); this.setGroupType(groupType); this.setSearchTerm(""); }}>{groupType}</div>;
+ });
+
+ // if search term does not already exist as a group type, give option to create new group type
+ if (!exactFound && this._searchTerm !== "") {
+ options.push(<div key={""} className="linkEditor-option"
+ onClick={() => { this.createGroup(this._searchTerm); this.setGroupType(this._searchTerm); this.setSearchTerm(""); }}>Define new "{this._searchTerm}" relationship</div>);
+ }
+
+ return options;
+ }
+
+ render() {
+ return (
+ <div className="linkEditor-dropdown">
+ <input type="text" value={this._groupType} placeholder="Search for or create a new group"
+ onChange={e => this.onChange(e.target.value)}></input>
+ <div className="linkEditor-options-wrapper">
+ {this.renderOptions()}
+ </div>
+ </div >
+ );
+ }
}
+
+interface LinkMetadataEditorProps {
+ id: string;
+ groupType: string;
+ mdDoc: Doc;
+ mdKey: string;
+ mdValue: string;
+ changeMdIdKey: (id: string, newKey: string) => void;
+}
@observer
-export class LinkEditor extends React.Component<Props> {
+class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
+ @observable private _key: string = this.props.mdKey;
+ @observable private _value: string = this.props.mdValue;
+ @observable private _keyError: boolean = false;
- @observable private _nameInput: string = StrCast(this.props.linkDoc.title);
- @observable private _descriptionInput: string = StrCast(this.props.linkDoc.linkDescription);
+ @action
+ setMetadataKey = (value: string): void => {
+ let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);
+ // don't allow user to create existing key
+ let newIndex = groupMdKeys.findIndex(key => key.toUpperCase() === value.toUpperCase());
+ if (newIndex > -1) {
+ this._keyError = true;
+ this._key = value;
+ return;
+ } else {
+ this._keyError = false;
+ }
- onSaveButtonPressed = (e: React.PointerEvent): void => {
- e.stopPropagation();
+ // set new value for key
+ let currIndex = groupMdKeys.findIndex(key => {
+ return StrCast(key).toUpperCase() === this._key.toUpperCase();
+ });
+ if (currIndex === -1) console.error("LinkMetadataEditor: key was not found");
+ groupMdKeys[currIndex] = value;
- let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
- linkDoc.title = this._nameInput;
- linkDoc.linkDescription = this._descriptionInput;
+ this.props.changeMdIdKey(this.props.id, value);
+ this._key = value;
+ LinkManager.Instance.setMetadataKeysForGroup(this.props.groupType, [...groupMdKeys]);
+ }
- this.props.showLinks();
+ @action
+ setMetadataValue = (value: string): void => {
+ if (!this._keyError) {
+ this._value = value;
+ this.props.mdDoc[this._key] = value;
+ }
}
+ @action
+ removeMetadata = (): void => {
+ let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);
+ let index = groupMdKeys.findIndex(key => key.toUpperCase() === this._key.toUpperCase());
+ if (index === -1) console.error("LinkMetadataEditor: key was not found");
+ groupMdKeys.splice(index, 1);
- render() {
+ LinkManager.Instance.setMetadataKeysForGroup(this.props.groupType, groupMdKeys);
+ this._key = "";
+ }
+ render() {
return (
- <div className="edit-container">
- <input onChange={this.onNameChanged} className="name-input" type="text" value={this._nameInput} placeholder="Name . . ."></input>
- <textarea onChange={this.onDescriptionChanged} className="description-input" value={this._descriptionInput} placeholder="Description . . ."></textarea>
- <div className="save-button" onPointerDown={this.onSaveButtonPressed}>SAVE</div>
+ <div className="linkEditor-metadata-row">
+ <input className={this._keyError ? "linkEditor-error" : ""} type="text" value={this._key === "new key" ? "" : this._key} placeholder="key" onChange={e => this.setMetadataKey(e.target.value)}></input>:
+ <input type="text" value={this._value} placeholder="value" onChange={e => this.setMetadataValue(e.target.value)}></input>
+ <button onClick={() => this.removeMetadata()}><FontAwesomeIcon icon="times" size="sm" /></button>
</div>
+ );
+ }
+}
+
+interface LinkGroupEditorProps {
+ sourceDoc: Doc;
+ linkDoc: Doc;
+ groupDoc: Doc;
+}
+@observer
+export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
+ private _metadataIds: Map<string, string> = new Map();
+
+ constructor(props: LinkGroupEditorProps) {
+ super(props);
+
+ let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(StrCast(props.groupDoc.type));
+ groupMdKeys.forEach(key => {
+ this._metadataIds.set(key, Utils.GenerateGuid());
+ });
+ }
+
+ @action
+ setGroupType = (groupType: string): void => {
+ this.props.groupDoc.type = groupType;
+ }
+
+ removeGroupFromLink = (groupType: string): void => {
+ LinkManager.Instance.removeGroupFromAnchor(this.props.linkDoc, this.props.sourceDoc, groupType);
+ }
+
+ deleteGroup = (groupType: string): void => {
+ LinkManager.Instance.deleteGroupType(groupType);
+ }
+
+ copyGroup = async (groupType: string): Promise<void> => {
+ let sourceGroupDoc = this.props.groupDoc;
+ const sourceMdDoc = await Cast(sourceGroupDoc.metadata, Doc);
+ if (!sourceMdDoc) return;
+
+ let destDoc = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
+ // let destGroupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, destDoc);
+ let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+
+ // create new metadata doc with copied kvp
+ let destMdDoc = new Doc();
+ destMdDoc.anchor1 = StrCast(sourceMdDoc.anchor2);
+ destMdDoc.anchor2 = StrCast(sourceMdDoc.anchor1);
+ keys.forEach(key => {
+ let val = sourceMdDoc[key] === undefined ? "" : StrCast(sourceMdDoc[key]);
+ destMdDoc[key] = val;
+ });
+
+ // create new group doc with new metadata doc
+ let destGroupDoc = new Doc();
+ destGroupDoc.type = groupType;
+ destGroupDoc.metadata = destMdDoc;
+
+ LinkManager.Instance.addGroupToAnchor(this.props.linkDoc, destDoc, destGroupDoc, true);
+ }
+
+ @action
+ addMetadata = (groupType: string): void => {
+ this._metadataIds.set("new key", Utils.GenerateGuid());
+ let mdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+ // only add "new key" if there is no other key with value "new key"; prevents spamming
+ if (mdKeys.indexOf("new key") === -1) mdKeys.push("new key");
+ LinkManager.Instance.setMetadataKeysForGroup(groupType, mdKeys);
+ }
+
+ // for key rendering purposes
+ changeMdIdKey = (id: string, newKey: string) => {
+ this._metadataIds.set(newKey, id);
+ }
+
+ renderMetadata = (): JSX.Element[] => {
+ let metadata: Array<JSX.Element> = [];
+ let groupDoc = this.props.groupDoc;
+ const mdDoc = FieldValue(Cast(groupDoc.metadata, Doc));
+ if (!mdDoc) {
+ return [];
+ }
+ let groupType = StrCast(groupDoc.type);
+ let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+
+ groupMdKeys.forEach((key) => {
+ let val = StrCast(mdDoc[key]);
+ metadata.push(
+ <LinkMetadataEditor key={"mded-" + this._metadataIds.get(key)} id={this._metadataIds.get(key)!} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={val} changeMdIdKey={this.changeMdIdKey} />
+ );
+ });
+ return metadata;
+ }
+
+ viewGroupAsTable = (groupType: string): JSX.Element => {
+ let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+ let index = keys.indexOf("");
+ if (index > -1) keys.splice(index, 1);
+ let cols = ["anchor1", "anchor2", ...[...keys]];
+ let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
+ let createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
+ let ref = React.createRef<HTMLDivElement>();
+ return <div ref={ref}><button className="linkEditor-button" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>;
+ }
+
+ render() {
+ let groupType = StrCast(this.props.groupDoc.type);
+ // if ((groupType && LinkManager.Instance.getMetadataKeysInGroup(groupType).length > 0) || groupType === "") {
+ let buttons;
+ if (groupType === "") {
+ buttons = (
+ <>
+ <button className="linkEditor-button" disabled={true} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button>
+ <button className="linkEditor-button" disabled title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button>
+ <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupType)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button>
+ <button className="linkEditor-button" disabled title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button>
+ <button className="linkEditor-button" disabled title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button>
+ </>
+ );
+ } else {
+ buttons = (
+ <>
+ <button className="linkEditor-button" onClick={() => this.addMetadata(groupType)} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button>
+ <button className="linkEditor-button" onClick={() => this.copyGroup(groupType)} title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button>
+ <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupType)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button>
+ <button className="linkEditor-button" onClick={() => this.deleteGroup(groupType)} title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button>
+ {this.viewGroupAsTable(groupType)}
+ </>
+ );
+ }
+ return (
+ <div className="linkEditor-group">
+ <div className="linkEditor-group-row">
+ <p className="linkEditor-group-row-label">type:</p>
+ <GroupTypesDropdown groupType={groupType} setGroupType={this.setGroupType} />
+ </div>
+ {this.renderMetadata().length > 0 ? <p className="linkEditor-group-row-label">metadata:</p> : <></>}
+ {this.renderMetadata()}
+ <div className="linkEditor-group-buttons">
+ {buttons}
+ </div>
+ </div>
);
}
+}
+
+
+interface LinkEditorProps {
+ sourceDoc: Doc;
+ linkDoc: Doc;
+ showLinks: () => void;
+}
+@observer
+export class LinkEditor extends React.Component<LinkEditorProps> {
@action
- onNameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
- this._nameInput = e.target.value;
+ deleteLink = (): void => {
+ LinkManager.Instance.deleteLink(this.props.linkDoc);
+ this.props.showLinks();
}
@action
- onDescriptionChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
- this._descriptionInput = e.target.value;
+ addGroup = (): void => {
+ // create new metadata document for group
+ let mdDoc = new Doc();
+ mdDoc.anchor1 = this.props.sourceDoc.title;
+ mdDoc.anchor2 = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc).title;
+
+ // create new group document
+ let groupDoc = new Doc();
+ groupDoc.type = "";
+ groupDoc.metadata = mdDoc;
+
+ LinkManager.Instance.addGroupToAnchor(this.props.linkDoc, this.props.sourceDoc, groupDoc);
+ }
+
+ render() {
+ let destination = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
+
+ let groupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc);
+ let groups = groupList.map(groupDoc => {
+ return <LinkGroupEditor key={"gred-" + StrCast(groupDoc.type)} linkDoc={this.props.linkDoc} sourceDoc={this.props.sourceDoc} groupDoc={groupDoc} />;
+ });
+
+ return (
+ <div className="linkEditor">
+ <button className="linkEditor-back" onPointerDown={() => this.props.showLinks()}><FontAwesomeIcon icon="arrow-left" size="sm" /></button>
+ <div className="linkEditor-info">
+ <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto!.title}</b></p>
+ <button className="linkEditor-button" onPointerDown={() => this.deleteLink()} title="Delete link"><FontAwesomeIcon icon="trash" size="sm" /></button>
+ </div>
+ <div className="linkEditor-groupsLabel">
+ <b>Relationships:</b>
+ <button className="linkEditor-button" onClick={() => this.addGroup()} title=" Add Group"><FontAwesomeIcon icon="plus" size="sm" /></button>
+ </div>
+ {groups.length > 0 ? groups : <div className="linkEditor-group">There are currently no relationships associated with this link.</div>}
+ </div>
+
+ );
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/nodes/LinkMenu.scss
index dedcce6ef..a4018bd2d 100644
--- a/src/client/views/nodes/LinkMenu.scss
+++ b/src/client/views/nodes/LinkMenu.scss
@@ -1,21 +1,137 @@
-#linkMenu-container {
+@import "../globalCssVariables";
+
+.linkMenu {
width: 100%;
height: auto;
- display: flex;
- flex-direction: column;
}
-#linkMenu-searchBar {
- width: 100%;
- padding: 5px;
- margin-bottom: 10px;
+.linkMenu-list {
+ max-height: 200px;
+ overflow-y: scroll;
+}
+
+.linkMenu-group {
+ border-bottom: 0.5px solid lightgray;
+ padding: 5px 0;
+
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .linkMenu-group-name {
+ display: flex;
+
+ &:hover {
+ p {
+ background-color: lightgray;
+ }
+ p.expand-one {
+ width: calc(100% - 26px);
+ }
+ .linkEditor-tableButton {
+ display: block;
+ }
+ }
+
+ p {
+ width: 100%;
+ padding: 4px 6px;
+ line-height: 12px;
+ border-radius: 5px;
+ font-weight: bold;
+ }
+
+ .linkEditor-tableButton {
+ display: none;
+ }
+ }
+}
+
+.linkMenu-item {
+ // border-top: 0.5px solid $main-accent;
+ position: relative;
+ display: flex;
font-size: 12px;
- border: 1px solid #bababa;
+
+
+ .link-name {
+ position: relative;
+
+ p {
+ padding: 4px 6px;
+ line-height: 12px;
+ border-radius: 5px;
+ overflow-wrap: break-word;
+ }
+ }
+
+ .linkMenu-item-content {
+ width: 100%;
+ }
+
+ .link-metadata {
+ padding: 0 10px 0 16px;
+ margin-bottom: 4px;
+ color: $main-accent;
+ font-style: italic;
+ font-size: 10.5px;
+ }
+
+ &:hover {
+ .linkMenu-item-buttons {
+ display: flex;
+ }
+ .linkMenu-item-content {
+ &.expand-two p {
+ width: calc(100% - 52px);
+ background-color: lightgray;
+ }
+ &.expand-three p {
+ width: calc(100% - 84px);
+ background-color: lightgray;
+ }
+ }
+ }
}
-#linkMenu-list {
- margin-top: 5px;
- width: 100%;
- height: 100px;
- overflow-y: scroll;
-} \ No newline at end of file
+.linkMenu-item-buttons {
+ display: none;
+ position: absolute;
+ top: 50%;
+ right: 0;
+ transform: translateY(-50%);
+
+ .button {
+ width: 20px;
+ height: 20px;
+ margin: 0;
+ margin-right: 6px;
+ border-radius: 50%;
+ cursor: pointer;
+ pointer-events: auto;
+ background-color: $dark-color;
+ color: $light-color;
+ font-size: 65%;
+ transition: transform 0.2s;
+ text-align: center;
+ position: relative;
+
+ .fa-icon {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+
+ &:last-child {
+ margin-right: 0;
+ }
+ &:hover {
+ background: $main-accent;
+ }
+ }
+}
+
+
+
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index 3f09d6214..cccf3c329 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -1,13 +1,20 @@
import { action, observable } from "mobx";
import { observer } from "mobx-react";
import { DocumentView } from "./DocumentView";
-import { LinkBox } from "./LinkBox";
import { LinkEditor } from "./LinkEditor";
import './LinkMenu.scss';
import React = require("react");
-import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Doc } from "../../../new_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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+library.add(faTrash);
import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
import { Id } from "../../../new_fields/FieldSymbols";
+import { DocTypes } from "../../documents/Documents";
interface Props {
docView: DocumentView;
@@ -19,34 +26,46 @@ export class LinkMenu extends React.Component<Props> {
@observable private _editingLink?: Doc;
- renderLinkItems(links: Doc[], key: string, type: string) {
- return links.map(link => {
- let doc = FieldValue(Cast(link[key], Doc));
- if (doc) {
- return <LinkBox key={doc[Id]} linkDoc={link} linkName={StrCast(link.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />;
- }
+ @action
+ componentWillReceiveProps() {
+ this._editingLink = undefined;
+ }
+
+ clearAllLinks = () => {
+ LinkManager.Instance.deleteAllLinksOnAnchor(this.props.docView.props.Document);
+ }
+
+ renderAllGroups = (groups: Map<string, Array<Doc>>): Array<JSX.Element> => {
+ let linkItems: Array<JSX.Element> = [];
+ groups.forEach((group, groupType) => {
+ linkItems.push(
+ <LinkMenuGroup key={groupType} sourceDoc={this.props.docView.props.Document} group={group} groupType={groupType} showEditor={action((linkDoc: Doc) => this._editingLink = linkDoc)} />
+ );
});
+
+ // if source doc has no links push message
+ if (linkItems.length === 0) linkItems.push(<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>);
+
+ return linkItems;
}
render() {
- //get list of links from document
- let linkFrom = DocListCast(this.props.docView.props.Document.linkedFromDocs);
- let linkTo = DocListCast(this.props.docView.props.Document.linkedToDocs);
+ let sourceDoc = this.props.docView.props.Document;
+ let groups: Map<string, Doc[]> = LinkManager.Instance.getRelatedGroupedLinks(sourceDoc);
if (this._editingLink === undefined) {
return (
- <div id="linkMenu-container">
+ <div className="linkMenu">
+ <button className="linkEditor-button linkEditor-clearButton" onClick={() => this.clearAllLinks()} title="Clear all links"><FontAwesomeIcon icon="trash" size="sm" /></button>
{/* <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input> */}
- <div id="linkMenu-list">
- {this.renderLinkItems(linkTo, "linkedTo", "Destination: ")}
- {this.renderLinkItems(linkFrom, "linkedFrom", "Source: ")}
+ <div className="linkMenu-list">
+ {this.renderAllGroups(groups)}
</div>
</div>
);
} else {
return (
- <LinkEditor linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor>
+ <LinkEditor sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor>
);
}
-
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenuGroup.tsx b/src/client/views/nodes/LinkMenuGroup.tsx
new file mode 100644
index 000000000..be45c3e6e
--- /dev/null
+++ b/src/client/views/nodes/LinkMenuGroup.tsx
@@ -0,0 +1,92 @@
+import { action, observable } from "mobx";
+import { observer } from "mobx-react";
+import { DocumentView } from "./DocumentView";
+import { LinkMenuItem } from "./LinkMenuItem";
+import { LinkEditor } from "./LinkEditor";
+import './LinkMenu.scss';
+import React = require("react");
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { LinkManager } from "../../util/LinkManager";
+import { DragLinksAsDocuments, DragManager, SetupDrag } from "../../util/DragManager";
+import { emptyFunction } from "../../../Utils";
+import { Docs } from "../../documents/Documents";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+interface LinkMenuGroupProps {
+ sourceDoc: Doc;
+ group: Doc[];
+ groupType: string;
+ showEditor: (linkDoc: Doc) => void;
+}
+
+@observer
+export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
+
+ private _drag = React.createRef<HTMLDivElement>();
+ private _table = React.createRef<HTMLDivElement>();
+
+ onLinkButtonDown = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ document.removeEventListener("pointermove", this.onLinkButtonMoved);
+ document.addEventListener("pointermove", this.onLinkButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkButtonUp);
+ document.addEventListener("pointerup", this.onLinkButtonUp);
+ }
+
+ onLinkButtonUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onLinkButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkButtonUp);
+ e.stopPropagation();
+ }
+
+ onLinkButtonMoved = async (e: PointerEvent) => {
+ if (this._drag.current !== null && (e.movementX > 1 || e.movementY > 1)) {
+ document.removeEventListener("pointermove", this.onLinkButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkButtonUp);
+
+ let draggedDocs = this.props.group.map(linkDoc => LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc));
+ let dragData = new DragManager.DocumentDragData(draggedDocs, draggedDocs.map(d => undefined));
+
+ DragManager.StartLinkedDocumentDrag([this._drag.current], this.props.sourceDoc, dragData, e.x, e.y, {
+ handlers: {
+ dragComplete: action(emptyFunction),
+ },
+ hideSource: false
+ });
+ }
+ e.stopPropagation();
+ }
+
+ viewGroupAsTable = (groupType: string): JSX.Element => {
+ let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+ let index = keys.indexOf("");
+ if (index > -1) keys.splice(index, 1);
+ let cols = ["anchor1", "anchor2", ...[...keys]];
+ let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
+ let createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
+ let ref = React.createRef<HTMLDivElement>();
+ return <div ref={ref}><button className="linkEditor-button linkEditor-tableButton" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>;
+ }
+
+ render() {
+ let groupItems = this.props.group.map(linkDoc => {
+ let destination = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc);
+ return <LinkMenuItem key={destination[Id] + this.props.sourceDoc[Id]} groupType={this.props.groupType}
+ linkDoc={linkDoc} sourceDoc={this.props.sourceDoc} destinationDoc={destination} showEditor={this.props.showEditor} />;
+ });
+
+ return (
+ <div className="linkMenu-group">
+ <div className="linkMenu-group-name">
+ <p ref={this._drag} onPointerDown={this.onLinkButtonDown}
+ className={this.props.groupType === "*" || this.props.groupType === "" ? "" : "expand-one"} > {this.props.groupType}:</p>
+ {this.props.groupType === "*" || this.props.groupType === "" ? <></> : this.viewGroupAsTable(this.props.groupType)}
+ </div>
+ <div className="linkMenu-group-wrapper">
+ {groupItems}
+ </div>
+ </div >
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/nodes/LinkMenuItem.tsx
new file mode 100644
index 000000000..9728671c0
--- /dev/null
+++ b/src/client/views/nodes/LinkMenuItem.tsx
@@ -0,0 +1,118 @@
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faEdit, faEye, faTimes, faArrowRight, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { observer } from "mobx-react";
+import { DocumentManager } from "../../util/DocumentManager";
+import { undoBatch } from "../../util/UndoManager";
+import './LinkMenu.scss';
+import React = require("react");
+import { Doc } from '../../../new_fields/Doc';
+import { StrCast, Cast, BoolCast, FieldValue } from '../../../new_fields/Types';
+import { observable, action } from 'mobx';
+import { LinkManager } from '../../util/LinkManager';
+import { DragLinkAsDocument } from '../../util/DragManager';
+import { CollectionDockingView } from '../collections/CollectionDockingView';
+library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp);
+
+
+interface LinkMenuItemProps {
+ groupType: string;
+ linkDoc: Doc;
+ sourceDoc: Doc;
+ destinationDoc: Doc;
+ showEditor: (linkDoc: Doc) => void;
+}
+
+@observer
+export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
+ private _drag = React.createRef<HTMLDivElement>();
+ @observable private _showMore: boolean = false;
+ @action toggleShowMore() { this._showMore = !this._showMore; }
+
+ @undoBatch
+ onFollowLink = async (e: React.PointerEvent): Promise<void> => {
+ e.stopPropagation();
+ let jumpToDoc = this.props.destinationDoc;
+ let pdfDoc = FieldValue(Cast(this.props.destinationDoc, Doc));
+ if (pdfDoc) {
+ jumpToDoc = pdfDoc;
+ }
+ if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
+ DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey);
+ } else {
+ CollectionDockingView.Instance.AddRightSplit(jumpToDoc, undefined);
+ }
+ }
+
+ onEdit = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ this.props.showEditor(this.props.linkDoc);
+ }
+
+ renderMetadata = (): JSX.Element => {
+ let groups = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc);
+ let index = groups.findIndex(groupDoc => StrCast(groupDoc.type).toUpperCase() === this.props.groupType.toUpperCase());
+ let groupDoc = index > -1 ? groups[index] : undefined;
+
+ let mdRows: Array<JSX.Element> = [];
+ if (groupDoc) {
+ let mdDoc = Cast(groupDoc.metadata, Doc, null);
+ if (mdDoc) {
+ let keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType);
+ mdRows = keys.map(key => {
+ return (<div key={key} className="link-metadata-row"><b>{key}</b>: {StrCast(mdDoc[key])}</div>);
+ });
+ }
+ }
+
+ return (<div className="link-metadata">{mdRows}</div>);
+ }
+
+ onLinkButtonDown = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ document.removeEventListener("pointermove", this.onLinkButtonMoved);
+ document.addEventListener("pointermove", this.onLinkButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkButtonUp);
+ document.addEventListener("pointerup", this.onLinkButtonUp);
+ }
+
+ onLinkButtonUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onLinkButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkButtonUp);
+ e.stopPropagation();
+ }
+
+ onLinkButtonMoved = async (e: PointerEvent) => {
+ if (this._drag.current !== null && (e.movementX > 1 || e.movementY > 1)) {
+ document.removeEventListener("pointermove", this.onLinkButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkButtonUp);
+
+ DragLinkAsDocument(this._drag.current, e.x, e.y, this.props.linkDoc, this.props.sourceDoc);
+ }
+ e.stopPropagation();
+ }
+
+ render() {
+
+ let keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType);
+ let canExpand = keys ? keys.length > 0 : false;
+
+ return (
+ <div className="linkMenu-item">
+ <div className={canExpand ? "linkMenu-item-content expand-three" : "linkMenu-item-content expand-two"}>
+ <div className="link-name">
+ <p ref={this._drag} onPointerDown={this.onLinkButtonDown}>{StrCast(this.props.destinationDoc.title)}</p>
+ <div className="linkMenu-item-buttons">
+ {canExpand ? <div title="Show more" className="button" onPointerDown={() => this.toggleShowMore()}>
+ <FontAwesomeIcon className="fa-icon" icon={this._showMore ? "chevron-up" : "chevron-down"} size="sm" /></div> : <></>}
+ <div title="Edit link" className="button" onPointerDown={this.onEdit}><FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /></div>
+ <div title="Follow link" className="button" onPointerDown={this.onFollowLink}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
+ </div>
+ </div>
+ {this._showMore ? this.renderMetadata() : <></>}
+ </div>
+
+ </div >
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index 449408a61..e7655d598 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -2,40 +2,139 @@
transform-origin: left top;
position: absolute;
top: 0;
- left:0;
+ left: 0;
}
+
.react-pdf__Page__textContent span {
user-select: text;
}
+
.react-pdf__Document {
position: absolute;
}
+
.pdfBox-buttonTray {
- position:absolute;
+ position: absolute;
top: 0;
- left:0;
+ left: 0;
z-index: 25;
pointer-events: all;
}
+
.pdfBox-thumbnail {
position: absolute;
width: 100%;
}
+
.pdfButton {
pointer-events: all;
width: 100px;
- height:100px;
+ height: 100px;
+}
+
+.pdfBox-cont,
+.pdfBox-cont-interactive {
+ display: flex;
+ flex-direction: row;
+ height: 100%;
+ overflow-y: scroll;
+ overflow-x: hidden;
}
+
.pdfBox-cont {
- pointer-events: none ;
- span {
- pointer-events: none !important;
+ pointer-events: none;
+
+ .textlayer {
+ pointer-events: none;
+
+ span {
+ pointer-events: none !important;
+ }
+ }
+
+ .page-cont {
+ pointer-events: none;
}
}
+
.pdfBox-cont-interactive {
pointer-events: all;
+ display: flex;
+ flex-direction: row;
+
+ .textlayer {
+ span {
+ pointer-events: all !important;
+ user-select: text;
+ }
+ }
}
+
.pdfBox-contentContainer {
position: absolute;
transform-origin: left top;
+}
+
+.pdfBox-settingsCont {
+ position: absolute;
+ right: 0;
+ top: 0;
+
+ .pdfBox-settingsButton {
+ border-bottom-left-radius: 50%;
+ display: flex;
+ justify-content: space-evenly;
+ align-items: center;
+ height: 70px;
+ background: none;
+ padding: 0;
+
+ .pdfBox-settingsButton-arrow {
+ width: 0;
+ height: 0;
+ border-top: 25px solid transparent;
+ border-bottom: 25px solid transparent;
+ border-right: 25px solid #121721;
+ transition: all 0.5s;
+ }
+
+ .pdfBox-settingsButton-iconCont {
+ background: #121721;
+ height: 50px;
+ width: 70px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-left: -2px;
+ border-radius: 3px;
+ }
+ }
+
+ .pdfBox-settingsButton:hover {
+ background: none;
+ }
+
+ .pdfBox-settingsFlyout {
+ width: 600px;
+ position: absolute;
+ background: #323232;
+ box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
+ left: -400px;
+ border-radius: 7px;
+ padding: 20px;
+ display: flex;
+ flex-direction: column;
+ font-size: 30px;
+ transition: all 0.5s;
+
+ .pdfBox-settingsFlyout-title {
+ color: white;
+ }
+
+ .pdfBox-settingsFlyout-kvpInput {
+ margin-top: 20px;
+ display: grid;
+ grid-template-columns: 47.5% 5% 47.5%;
+ }
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index df9e49b64..cc02bb282 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,52 +1,28 @@
-import * as htmlToImage from "html-to-image";
-import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx';
+import { action, IReactionDisposer, observable, reaction, trace, untracked, computed } from 'mobx';
import { observer } from "mobx-react";
import 'react-image-lightbox/style.css';
-import Measure from "react-measure";
-//@ts-ignore
-import { Document, Page } from "react-pdf";
-import 'react-pdf/dist/Page/AnnotationLayer.css';
-import { Id } from "../../../new_fields/FieldSymbols";
+import { WidthSym, Doc } from "../../../new_fields/Doc";
import { makeInterface } from "../../../new_fields/Schema";
-import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
-import { ImageField, PdfField } from "../../../new_fields/URLField";
+import { Cast, NumCast, BoolCast } from "../../../new_fields/Types";
+import { PdfField } from "../../../new_fields/URLField";
+//@ts-ignore
+// import { Document, Page } from "react-pdf";
+// import 'react-pdf/dist/Page/AnnotationLayer.css';
import { RouteStore } from "../../../server/RouteStore";
-import { Utils } from '../../../Utils';
-import { DocServer } from "../../DocServer";
import { DocComponent } from "../DocComponent";
import { InkingControl } from "../InkingControl";
-import { SearchBox } from "../SearchBox";
+import { FilterBox } from "../search/FilterBox";
import { Annotation } from './Annotation';
+import { PDFViewer } from "../pdf/PDFViewer";
import { positionSchema } from "./DocumentView";
import { FieldView, FieldViewProps } from './FieldView';
import { pageSchema } from "./ImageBox";
import "./PDFBox.scss";
-var path = require('path');
import React = require("react");
-import { ContextMenu } from "../ContextMenu";
-
-/** ALSO LOOK AT: Annotation.tsx, Sticky.tsx
- * This method renders PDF and puts all kinds of functionalities such as annotation, highlighting,
- * area selection (I call it stickies), embedded ink node for directly annotating using a pen or
- * mouse, and pagination.
- *
- *
- * HOW TO USE:
- * AREA selection:
- * 1) Click on Area button.
- * 2) click on any part of the PDF, and drag to get desired sized area shape
- * 3) You can write on the area (hence the reason why it's called sticky)
- * 4) to make another area, you need to click on area button AGAIN.
- *
- * HIGHLIGHT: (Buggy. No multiline/multidiv text highlighting for now...)
- * 1) just click and drag on a text
- * 2) click highlight
- * 3) for annotation, just pull your cursor over to that text
- * 4) another method: click on highlight first and then drag on your desired text
- * 5) To make another highlight, you need to reclick on the button
- *
- * written by: Andrew Kim
- */
+import { CompileScript } from '../../util/Scripting';
+import { Flyout, anchorPoints } from '../DocumentDecorations';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { ScriptField } from '../../../new_fields/ScriptField';
type PdfDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
const PdfDocument = makeInterface(positionSchema, pageSchema);
@@ -55,349 +31,222 @@ const PdfDocument = makeInterface(positionSchema, pageSchema);
export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocument) {
public static LayoutString() { return FieldView.LayoutString(PDFBox); }
- private _mainDiv = React.createRef<HTMLDivElement>();
- private renderHeight = 2400;
-
- @observable private _renderAsSvg = true;
@observable private _alt = false;
+ @observable private _scrollY: number = 0;
+ @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
+ @observable private _flyout: boolean = false;
+ private _mainCont: React.RefObject<HTMLDivElement>;
private _reactionDisposer?: IReactionDisposer;
+ private _keyValue: string = "";
+ private _valueValue: string = "";
+ private _scriptValue: string = "";
+ private _keyRef: React.RefObject<HTMLInputElement>;
+ private _valueRef: React.RefObject<HTMLInputElement>;
+ private _scriptRef: React.RefObject<HTMLInputElement>;
- @observable private _perPageInfo: Object[] = []; //stores pageInfo
- @observable private _pageInfo: any = { area: [], divs: [], anno: [] }; //divs is array of objects linked to anno
+ constructor(props: FieldViewProps) {
+ super(props);
- @observable private _currAnno: any = [];
- @observable private _interactive: boolean = false;
- @observable private _loaded: boolean = false;
-
- @computed private get curPage() { return NumCast(this.Document.curPage, 1); }
- @computed private get thumbnailPage() { return NumCast(this.props.Document.thumbnailPage, -1); }
-
- componentDidMount() {
- let wasSelected = this.props.isSelected();
+ this._mainCont = React.createRef();
this._reactionDisposer = reaction(
- () => [this.props.isSelected(), this.curPage],
+ () => this.props.Document.scrollY,
() => {
- if (this.curPage > 0 && !this.props.isTopMost && this.curPage !== this.thumbnailPage && wasSelected && !this.props.isSelected()) {
- this.saveThumbnail();
+ if (this._mainCont.current) {
+ this._mainCont.current && this._mainCont.current.scrollTo({ top: NumCast(this.Document.scrollY), behavior: "auto" });
}
- wasSelected = this._interactive = this.props.isSelected();
- },
- { fireImmediately: true });
+ }
+ );
+
+ this._keyRef = React.createRef();
+ this._valueRef = React.createRef();
+ this._scriptRef = React.createRef();
+ }
+ componentDidMount() {
+ if (this.props.setPdfBox) this.props.setPdfBox(this);
}
componentWillUnmount() {
- if (this._reactionDisposer) this._reactionDisposer();
+ this._reactionDisposer && this._reactionDisposer();
}
- /**
- * highlighting helper function
- */
- makeEditableAndHighlight = (colour: string) => {
- var range, sel = window.getSelection();
- if (sel && sel.rangeCount && sel.getRangeAt) {
- range = sel.getRangeAt(0);
+ public GetPage() {
+ return Math.floor(NumCast(this.props.Document.scrollY) / NumCast(this.dataDoc.pdfHeight)) + 1;
+ }
+ public BackPage() {
+ let cp = Math.ceil(NumCast(this.props.Document.scrollY) / NumCast(this.dataDoc.pdfHeight)) + 1;
+ cp = cp - 1;
+ if (cp > 0) {
+ this.props.Document.curPage = cp;
+ this.props.Document.scrollY = (cp - 1) * NumCast(this.dataDoc.pdfHeight);
}
- document.designMode = "on";
- if (!document.execCommand("HiliteColor", false, colour)) {
- document.execCommand("HiliteColor", false, colour);
+ }
+ public GotoPage(p: number) {
+ if (p > 0 && p <= NumCast(this.props.Document.numPages)) {
+ this.props.Document.curPage = p;
+ this.props.Document.scrollY = (p - 1) * NumCast(this.dataDoc.pdfHeight);
}
+ }
- if (range && sel) {
- sel.removeAllRanges();
- sel.addRange(range);
-
- let obj: Object = { parentDivs: [], spans: [] };
- //@ts-ignore
- if (range.commonAncestorContainer.className === 'react-pdf__Page__textContent') { //multiline highlighting case
- obj = this.highlightNodes(range.commonAncestorContainer.childNodes);
- } else { //single line highlighting case
- let parentDiv = range.commonAncestorContainer.parentElement;
- if (parentDiv) {
- if (parentDiv.className === 'react-pdf__Page__textContent') { //when highlight is overwritten
- obj = this.highlightNodes(parentDiv.childNodes);
- } else {
- parentDiv.childNodes.forEach((child) => {
- if (child.nodeName === 'SPAN') {
- //@ts-ignore
- obj.parentDivs.push(parentDiv);
- //@ts-ignore
- child.id = "highlighted";
- //@ts-ignore
- obj.spans.push(child);
- // child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler
- }
- });
- }
- }
- }
- this._pageInfo.divs.push(obj);
-
+ public ForwardPage() {
+ let cp = this.GetPage() + 1;
+ if (cp <= NumCast(this.props.Document.numPages)) {
+ this.props.Document.curPage = cp;
+ this.props.Document.scrollY = (cp - 1) * NumCast(this.dataDoc.pdfHeight);
}
- document.designMode = "off";
}
- highlightNodes = (nodes: NodeListOf<ChildNode>) => {
- let temp = { parentDivs: [], spans: [] };
- nodes.forEach((div) => {
- div.childNodes.forEach((child) => {
- if (child.nodeName === 'SPAN') {
- //@ts-ignore
- temp.parentDivs.push(div);
- //@ts-ignore
- child.id = "highlighted";
- //@ts-ignore
- temp.spans.push(child);
- // child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler
- }
- });
-
- });
- return temp;
+ private newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._keyValue = e.currentTarget.value;
}
- /**
- * when the cursor enters the highlight, it pops out annotation. ONLY WORKS FOR SINGLE DIV LINES
- */
- @action
- onEnter = (e: any) => {
- let span: HTMLSpanElement = e.toElement;
- let index: any;
- this._pageInfo.divs.forEach((obj: any) => {
- obj.spans.forEach((element: any) => {
- if (element === span && !index) {
- index = this._pageInfo.divs.indexOf(obj);
- }
- });
- });
-
- if (this._pageInfo.anno.length >= index + 1) {
- if (this._currAnno.length === 0) {
- this._currAnno.push(this._pageInfo.anno[index]);
- }
- } else {
- if (this._currAnno.length === 0) { //if there are no current annotation
- let div = span.offsetParent;
- //@ts-ignore
- let divX = div.style.left;
- //@ts-ignore
- let divY = div.style.top;
- //slicing "px" from the end
- divX = divX.slice(0, divX.length - 2); //gets X of the DIV element (parent of Span)
- divY = divY.slice(0, divY.length - 2); //gets Y of the DIV element (parent of Span)
- let annotation = <Annotation key={Utils.GenerateGuid()} Span={span} X={divX} Y={divY - 300} Highlights={this._pageInfo.divs} Annotations={this._pageInfo.anno} CurrAnno={this._currAnno} />;
- this._pageInfo.anno.push(annotation);
- this._currAnno.push(annotation);
- }
- }
-
+ private newValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._valueValue = e.currentTarget.value;
}
- /**
- * highlight function for highlighting actual text. This works fine.
- */
- highlight = (color: string) => {
- if (window.getSelection()) {
- try {
- if (!document.execCommand("hiliteColor", false, color)) {
- this.makeEditableAndHighlight(color);
- }
- } catch (ex) {
- this.makeEditableAndHighlight(color);
- }
- }
+ @action
+ private newScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._scriptValue = e.currentTarget.value;
}
- /**
- * controls the area highlighting (stickies) Kinda temporary
- */
- onPointerDown = (e: React.PointerEvent) => {
- if (this.props.isSelected() && !InkingControl.Instance.selectedTool && e.buttons === 1) {
- if (e.altKey) {
- this._alt = true;
- } else {
- if (e.metaKey) {
- e.stopPropagation();
- }
- }
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointerup", this.onPointerUp);
+ private applyFilter = () => {
+ let scriptText = "";
+ if (this._scriptValue.length > 0) {
+ scriptText = this._scriptValue;
+ } else if (this._keyValue.length > 0 && this._valueValue.length > 0) {
+ scriptText = `return this.${this._keyValue} === ${this._valueValue}`;
}
- if (this.props.isSelected() && e.buttons === 2) {
- runInAction(() => this._alt = true);
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointerup", this.onPointerUp);
+ else {
+ scriptText = "return true";
}
- }
-
- /**
- * controls area highlighting and partially highlighting. Kinda temporary
- */
- @action
- onPointerUp = (e: PointerEvent) => {
- this._alt = false;
- document.removeEventListener("pointerup", this.onPointerUp);
- if (this.props.isSelected()) {
- this.highlight("rgba(76, 175, 80, 0.3)"); //highlights to this default color.
+ let script = CompileScript(scriptText, { params: { this: Doc.name } });
+ if (script.compiled) {
+ this.props.Document.filterScript = new ScriptField(script);
}
- this._interactive = true;
}
-
@action
- saveThumbnail = () => {
- this.props.Document.thumbnailPage = FieldValue(this.Document.curPage, -1);
- this._renderAsSvg = false;
- setTimeout(() => {
- runInAction(() => this._smallRetryCount = this._mediumRetryCount = this._largeRetryCount = 0);
- let nwidth = FieldValue(this.Document.nativeWidth, 0);
- let nheight = FieldValue(this.Document.nativeHeight, 0);
- htmlToImage.toPng(this._mainDiv.current!, { width: nwidth, height: nheight, quality: 0.8 })
- .then(action((dataUrl: string) => {
- SearchBox.convertDataUri(dataUrl, "icon" + this.Document[Id] + "_" + this.curPage).then((returnedFilename) => {
- if (returnedFilename) {
- let url = DocServer.Util.prepend(returnedFilename);
- this.props.Document.thumbnail = new ImageField(new URL(url));
- }
- runInAction(() => this._renderAsSvg = true);
- })
- }))
- .catch(function (error: any) {
- console.error('oops, something went wrong!', error);
- });
- }, 1250);
+ private toggleFlyout = () => {
+ this._flyout = !this._flyout;
}
@action
- onLoaded = (page: any) => {
- // bcz: the number of pages should really be set when the document is imported.
- this.props.Document.numPages = page._transport.numPages;
- if (this._perPageInfo.length === 0) { //Makes sure it only runs once
- this._perPageInfo = [...Array(page._transport.numPages)];
+ private resetFilters = () => {
+ this._keyValue = this._valueValue = "";
+ this._scriptValue = "return true";
+ if (this._keyRef.current) {
+ this._keyRef.current.value = "";
}
- this._loaded = true;
- }
-
- @action
- setScaling = (r: any) => {
- // bcz: the nativeHeight should really be set when the document is imported.
- // also, the native dimensions could be different for different pages of the canvas
- // so this design is flawed.
- var nativeWidth = FieldValue(this.Document.nativeWidth, 0);
- if (!FieldValue(this.Document.nativeHeight, 0)) {
- var nativeHeight = nativeWidth * r.offset.height / r.offset.width;
- this.props.Document.height = nativeHeight / nativeWidth * FieldValue(this.Document.width, 0);
- this.props.Document.nativeHeight = nativeHeight;
+ if (this._valueRef.current) {
+ this._valueRef.current.value = "";
}
- }
- @computed
- get pdfPage() {
- return <Page height={this.renderHeight} renderTextLayer={false} pageNumber={this.curPage} onLoadSuccess={this.onLoaded} />;
- }
- @computed
- get pdfContent() {
- let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField);
- if (!pdfUrl) {
- return <p>No pdf url to render</p>;
+ if (this._scriptRef.current) {
+ this._scriptRef.current.value = "";
}
- let pdfpage = this.pdfPage;
- let body = this.Document.nativeHeight ?
- pdfpage :
- <Measure offset onResize={this.setScaling}>
- {({ measureRef }) =>
- <div className="pdfBox-page" ref={measureRef}>
- {pdfpage}
- </div>
- }
- </Measure>;
- let xf = (this.Document.nativeHeight || 0) / this.renderHeight;
- return <div className="pdfBox-contentContainer" key="container" style={{ transform: `scale(${xf}, ${xf})` }}>
- <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl.url}`} renderMode={this._renderAsSvg || this.props.isTopMost ? "svg" : "canvas"}>
- {body}
- </Document>
- </div >;
+ this.applyFilter();
}
- @computed
- get pdfRenderer() {
- let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField);
- let proxy = this.imageProxyRenderer;
- if ((!this._interactive && proxy && (!this.props.ContainingCollectionView || !this.props.ContainingCollectionView.props.isTopMost)) || !pdfUrl) {
- return proxy;
+ scrollTo(y: number) {
+ if (this._mainCont.current) {
+ this._mainCont.current.scrollTo({ top: y, behavior: "auto" });
}
- return [
- proxy,
- this._pageInfo.area.filter(() => this._pageInfo.area).map((element: any) => element),
- this._currAnno.map((element: any) => element),
- this.pdfContent
- ];
- }
-
- choosePath(url: URL) {
- if (url.protocol === "data" || url.href.indexOf(window.location.origin) === -1)
- return url.href;
- let ext = path.extname(url.href);
- return url.href.replace(ext, this._curSuffix + ext);
- }
- @observable _smallRetryCount = 1;
- @observable _mediumRetryCount = 1;
- @observable _largeRetryCount = 1;
- @action retryPath = () => {
- if (this._curSuffix === "_s") this._smallRetryCount++;
- if (this._curSuffix === "_m") this._mediumRetryCount++;
- if (this._curSuffix === "_l") this._largeRetryCount++;
}
- @action onError = () => {
- let timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount;
- if (timeout < 10)
- setTimeout(this.retryPath, Math.min(10000, timeout * 5));
- }
- _curSuffix = "_m";
-
- @computed
- get imageProxyRenderer() {
- let thumbField = this.props.Document.thumbnail;
- if (thumbField && this._renderAsSvg && NumCast(this.props.Document.thumbnailPage, 0) === this.Document.curPage) {
-
- // let transform = this.props.ScreenToLocalTransform().inverse();
- let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
- // var [sptX, sptY] = transform.transformPoint(0, 0);
- // let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight());
- // let w = bptX - sptX;
- let path = thumbField instanceof ImageField ? thumbField.url.href : "http://cs.brown.edu/people/bcz/prairie.jpg";
- // this._curSuffix = "";
- // if (w > 20) {
- let field = thumbField;
- // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s";
- // else if (w < 400 && this._mediumRetryCount < 10) this._curSuffix = "_m";
- // else if (this._largeRetryCount < 10) this._curSuffix = "_l";
- if (field instanceof ImageField) path = this.choosePath(field.url);
- // }
- return <img className="pdfBox-thumbnail" key={path + (this._mediumRetryCount).toString()} src={path} onError={this.onError} />;
+ settingsPanel() {
+ return !this.props.active() ? (null) :
+ (
+ <div className="pdfBox-settingsCont" onPointerDown={(e) => e.stopPropagation()}>
+ <button className="pdfBox-settingsButton" onClick={this.toggleFlyout} title="Open Annotation Settings"
+ style={{ marginTop: `${NumCast(this.props.ContainingCollectionView!.props.Document.panY)}px` }}>
+ <div className="pdfBox-settingsButton-arrow"
+ style={{
+ borderTop: `25px solid ${this._flyout ? "#121721" : "transparent"}`,
+ borderBottom: `25px solid ${this._flyout ? "#121721" : "transparent"}`,
+ borderRight: `25px solid ${this._flyout ? "transparent" : "#121721"}`,
+ transform: `scaleX(${this._flyout ? -1 : 1})`
+ }}></div>
+ <div className="pdfBox-settingsButton-iconCont">
+ <FontAwesomeIcon style={{ color: "white" }} icon="cog" size="3x" />
+ </div>
+ </button>
+ <div className="pdfBox-settingsFlyout" style={{ left: `${this._flyout ? -600 : 100}px` }} >
+ <div className="pdfBox-settingsFlyout-title">
+ Annotation View Settings
+ </div>
+ <div className="pdfBox-settingsFlyout-kvpInput">
+ <input placeholder="Key" className="pdfBox-settingsFlyout-input" onChange={this.newKeyChange}
+ style={{ gridColumn: 1 }} ref={this._keyRef} />
+ <input placeholder="Value" className="pdfBox-settingsFlyout-input" onChange={this.newValueChange}
+ style={{ gridColumn: 3 }} ref={this._valueRef} />
+ </div>
+ <div className="pdfBox-settingsFlyout-kvpInput">
+ <input placeholder="Custom Script" onChange={this.newScriptChange} style={{ gridColumn: "1 / 4" }} ref={this._scriptRef} />
+ </div>
+ <div className="pdfBox-settingsFlyout-kvpInput">
+ <button style={{ gridColumn: 1 }} onClick={this.resetFilters}>
+ <FontAwesomeIcon style={{ color: "white" }} icon="trash" size="lg" />
+ &nbsp; Reset Filters
+ </button>
+ <button style={{ gridColumn: 3 }} onClick={this.applyFilter}>
+ <FontAwesomeIcon style={{ color: "white" }} icon="check" size="lg" />
+ &nbsp; Apply
+ </button>
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ loaded = (nw: number, nh: number, np: number) => {
+ if (this.props.Document) {
+ let doc = this.dataDoc;
+ doc.numPages = np;
+ if (doc.nativeWidth && doc.nativeHeight) return;
+ let oldaspect = NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth, 1);
+ doc.nativeWidth = nw;
+ if (doc.nativeHeight) doc.nativeHeight = nw * oldaspect;
+ else doc.nativeHeight = nh;
+ let ccv = this.props.ContainingCollectionView;
+ if (ccv) {
+ ccv.props.Document.pdfHeight = nh;
+ }
+ doc.height = nh * (doc[WidthSym]() / nw);
}
- return (null);
}
- @action onKeyDown = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = true);
- @action onKeyUp = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = false);
- onContextMenu = (e: React.MouseEvent): void => {
- let field = Cast(this.Document[this.props.fieldKey], PdfField);
- if (field) {
- let url = field.url.href;
- ContextMenu.Instance.addItem({
- description: "Copy path", event: () => {
- Utils.CopyText(url);
- }, icon: "expand-arrows-alt"
- });
+
+ @action
+ onScroll = (e: React.UIEvent<HTMLDivElement>) => {
+
+ if (e.currentTarget) {
+ this._scrollY = e.currentTarget.scrollTop;
+ let ccv = this.props.ContainingCollectionView;
+ if (ccv) {
+ ccv.props.Document.panTransformType = "None";
+ ccv.props.Document.scrollY = this._scrollY;
+ }
}
}
+
render() {
- let classname = "pdfBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : "");
+ // uses mozilla pdf as default
+ const pdfUrl = Cast(this.props.Document.data, PdfField);
+ if (!(pdfUrl instanceof PdfField)) return <div>{`pdf, ${this.props.Document.data}, not found`}</div>;
+ let classname = "pdfBox-cont" + (this.props.active() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : "");
return (
- <div className={classname} tabIndex={0} ref={this._mainDiv} onPointerDown={this.onPointerDown} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} onContextMenu={this.onContextMenu} >
- {this.pdfRenderer}
- </div >
+ <div className={classname}
+ onScroll={this.onScroll}
+ style={{
+ marginTop: `${NumCast(this.props.ContainingCollectionView!.props.Document.panY)}px`
+ }}
+ ref={this._mainCont}
+ onWheel={(e: React.WheelEvent) => {
+ e.stopPropagation();
+ }}>
+ <PDFViewer url={pdfUrl.url.pathname} loaded={this.loaded} scrollY={this._scrollY} parent={this} />
+ {/* <div style={{ width: "100px", height: "300px" }}></div> */}
+ {this.settingsPanel()}
+ </div>
);
}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 9ab607e91..264d3c1f7 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,19 +1,17 @@
import React = require("react");
+import { action, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
-import { FieldView, FieldViewProps } from './FieldView';
import * as rp from "request-promise";
-import "./VideoBox.scss";
-import { action, IReactionDisposer, reaction, observable } from "mobx";
-import { DocComponent } from "../DocComponent";
-import { positionSchema } from "./DocumentView";
import { makeInterface } from "../../../new_fields/Schema";
-import { pageSchema } from "./ImageBox";
-import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { Cast, FieldValue } from "../../../new_fields/Types";
import { VideoField } from "../../../new_fields/URLField";
-import "./VideoBox.scss";
import { RouteStore } from "../../../server/RouteStore";
import { DocServer } from "../../DocServer";
-import { actionFieldDecorator } from "mobx/lib/internal";
+import { DocComponent } from "../DocComponent";
+import { positionSchema } from "./DocumentView";
+import { FieldView, FieldViewProps } from './FieldView';
+import { pageSchema } from "./ImageBox";
+import "./VideoBox.scss";
type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
const VideoDocument = makeInterface(positionSchema, pageSchema);
@@ -97,7 +95,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
};
try {
let responseSchema: any = {};
- const videoInfoResponse = await rp.get(DocServer.Util.prepend(RouteStore.corsProxy + "/" + `https://www.youtube.com/watch?v=${videoId}`), videoInfoRequestConfig);
+ const videoInfoResponse = await rp.get(DocServer.prepend(RouteStore.corsProxy + "/" + `https://www.youtube.com/watch?v=${videoId}`), videoInfoRequestConfig);
const dataHtml = videoInfoResponse;
const start = dataHtml.indexOf('ytplayer.config = ') + 18;
const end = dataHtml.indexOf(';ytplayer.load');
@@ -121,7 +119,6 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
});
}
onPointerDown = (e: React.PointerEvent) => {
- e.preventDefault();
e.stopPropagation();
}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 2239a8e38..98c57fc75 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,12 +1,11 @@
-import "./WebBox.scss";
-import React = require("react");
-import { FieldViewProps, FieldView } from './FieldView';
+import { observer } from "mobx-react";
import { HtmlField } from "../../../new_fields/HtmlField";
import { WebField } from "../../../new_fields/URLField";
-import { observer } from "mobx-react";
-import { computed, reaction, IReactionDisposer } from 'mobx';
import { DocumentDecorations } from "../DocumentDecorations";
import { InkingControl } from "../InkingControl";
+import { FieldView, FieldViewProps } from './FieldView';
+import "./WebBox.scss";
+import React = require("react");
@observer
export class WebBox extends React.Component<FieldViewProps> {
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
new file mode 100644
index 000000000..0a1661a1a
--- /dev/null
+++ b/src/client/views/pdf/Annotation.tsx
@@ -0,0 +1,153 @@
+import React = require("react");
+import { Doc, DocListCast, WidthSym, HeightSym } from "../../../new_fields/Doc";
+import { AnnotationTypes, Viewer, scale } from "./PDFViewer";
+import { observer } from "mobx-react";
+import { observable, IReactionDisposer, reaction, action } from "mobx";
+import { BoolCast, NumCast, FieldValue, Cast, StrCast } from "../../../new_fields/Types";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { List } from "../../../new_fields/List";
+import PDFMenu from "./PDFMenu";
+import { DocumentManager } from "../../util/DocumentManager";
+import { PresentationView } from "../presentationview/PresentationView";
+
+interface IAnnotationProps {
+ anno: Doc;
+ index: number;
+ parent: Viewer;
+}
+
+export default class Annotation extends React.Component<IAnnotationProps> {
+ render() {
+ let annotationDocs = DocListCast(this.props.anno.annotations);
+ let res = annotationDocs.map(a => {
+ let type = NumCast(a.type);
+ switch (type) {
+ // case AnnotationTypes.Pin:
+ // return <PinAnnotation parent={this} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />;
+ case AnnotationTypes.Region:
+ return <RegionAnnotation parent={this.props.parent} document={a} index={this.props.index} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />;
+ default:
+ return <div></div>;
+ }
+ });
+ return res;
+ }
+}
+
+interface IRegionAnnotationProps {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+ index: number;
+ parent: Viewer;
+ document: Doc;
+}
+
+@observer
+class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
+ @observable private _backgroundColor: string = "red";
+
+ private _reactionDisposer?: IReactionDisposer;
+ private _scrollDisposer?: IReactionDisposer;
+ private _mainCont: React.RefObject<HTMLDivElement>;
+
+ constructor(props: IRegionAnnotationProps) {
+ super(props);
+
+ this._mainCont = React.createRef();
+ }
+
+ componentDidMount() {
+ this._reactionDisposer = reaction(
+ () => BoolCast(this.props.document.delete),
+ () => {
+ if (BoolCast(this.props.document.delete)) {
+ if (this._mainCont.current) {
+ this._mainCont.current.style.display = "none";
+ }
+ }
+ },
+ { fireImmediately: true }
+ );
+
+ this._scrollDisposer = reaction(
+ () => this.props.parent.Index,
+ () => {
+ if (this.props.parent.Index === this.props.index) {
+ this.props.parent.scrollTo(this.props.y * scale - (NumCast(this.props.parent.props.parent.Document.pdfHeight) / 2));
+ }
+ }
+ );
+ }
+
+ componentWillUnmount() {
+ this._reactionDisposer && this._reactionDisposer();
+ this._scrollDisposer && this._scrollDisposer();
+ }
+
+ deleteAnnotation = () => {
+ let annotation = DocListCast(this.props.parent.props.parent.Document.annotations);
+ let group = FieldValue(Cast(this.props.document.group, Doc));
+ if (group && annotation.indexOf(group) !== -1) {
+ let newAnnotations = annotation.filter(a => a !== FieldValue(Cast(this.props.document.group, Doc)));
+ this.props.parent.props.parent.Document.annotations = new List<Doc>(newAnnotations);
+ }
+
+ if (group) {
+ let groupAnnotations = DocListCast(group.annotations);
+ groupAnnotations.forEach(anno => anno.delete = true);
+ }
+
+ PDFMenu.Instance.fadeOut(true);
+ }
+
+ pinToPres = () => {
+ let group = FieldValue(Cast(this.props.document.group, Doc));
+ if (group) {
+ PresentationView.Instance.PinDoc(group);
+ }
+ }
+
+ @action
+ onPointerDown = (e: React.PointerEvent) => {
+ if (e.button === 0) {
+ let targetDoc = Cast(this.props.document.target, Doc, null);
+ if (targetDoc) {
+ DocumentManager.Instance.jumpToDocument(targetDoc, false);
+ }
+ }
+ if (e.button === 2) {
+ PDFMenu.Instance.Status = "annotation";
+ PDFMenu.Instance.Delete = this.deleteAnnotation.bind(this);
+ PDFMenu.Instance.Pinned = false;
+ PDFMenu.Instance.AddTag = this.addTag.bind(this);
+ PDFMenu.Instance.PinToPres = this.pinToPres;
+ PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true);
+ }
+ }
+
+ addTag = (key: string, value: string): boolean => {
+ let group = FieldValue(Cast(this.props.document.group, Doc));
+ if (group) {
+ let valNum = parseInt(value);
+ group[key] = isNaN(valNum) ? value : valNum;
+ return true;
+ }
+ return false;
+ }
+
+ render() {
+ return (
+ <div className="pdfViewer-annotationBox" onPointerDown={this.onPointerDown} ref={this._mainCont}
+ style={{
+ top: this.props.y * scale,
+ left: this.props.x * scale,
+ width: this.props.width * scale,
+ height: this.props.height * scale,
+ pointerEvents: "all",
+ backgroundColor: this.props.parent.Index === this.props.index ? "green" : StrCast(this.props.document.color)
+ }}></div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/pdf/PDFAnnotationLayer.tsx b/src/client/views/pdf/PDFAnnotationLayer.tsx
new file mode 100644
index 000000000..1f49e0d2f
--- /dev/null
+++ b/src/client/views/pdf/PDFAnnotationLayer.tsx
@@ -0,0 +1,24 @@
+import React = require("react");
+import { observer } from "mobx-react";
+
+interface IAnnotationProps {
+
+}
+
+@observer
+export class PDFAnnotationLayer extends React.Component {
+ onPointerDown = (e: React.PointerEvent) => {
+ if (e.ctrlKey) {
+ console.log("annotating");
+ e.stopPropagation();
+ }
+ }
+
+ render() {
+ return (
+ <div className="pdfAnnotationLayer-cont" style={{ width: "100%", height: "100%", position: "relative", top: "-200%" }} onPointerDown={this.onPointerDown}>
+
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/pdf/PDFMenu.scss b/src/client/views/pdf/PDFMenu.scss
new file mode 100644
index 000000000..a4624b1f6
--- /dev/null
+++ b/src/client/views/pdf/PDFMenu.scss
@@ -0,0 +1,32 @@
+.pdfMenu-cont {
+ position: absolute;
+ z-index: 10000;
+ height: 35px;
+ background: #323232;
+ box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
+ border-radius: 0px 6px 6px 6px;
+ overflow: hidden;
+ display: flex;
+
+ .pdfMenu-button {
+ background-color: transparent;
+ width: 35px;
+ height: 35px;
+ }
+
+ .pdfMenu-button:hover {
+ background-color: #121212;
+ }
+
+ .pdfMenu-dragger {
+ height: 100%;
+ transition: width .2s;
+ }
+
+ .pdfMenu-addTag {
+ display: grid;
+ width: 200px;
+ padding: 5px;
+ grid-template-columns: 90px 20px 90px;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx
new file mode 100644
index 000000000..d6970e7f4
--- /dev/null
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -0,0 +1,268 @@
+import React = require("react");
+import "./PDFMenu.scss";
+import { observable, action, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { emptyFunction, returnFalse } from "../../../Utils";
+import { Doc } from "../../../new_fields/Doc";
+
+@observer
+export default class PDFMenu extends React.Component {
+ static Instance: PDFMenu;
+
+ @observable private _top: number = -300;
+ @observable private _left: number = -300;
+ @observable private _opacity: number = 1;
+ @observable private _transition: string = "opacity 0.5s";
+ @observable private _transitionDelay: string = "";
+
+
+ StartDrag: (e: PointerEvent, ele: HTMLDivElement) => void = emptyFunction;
+ Highlight: (d: Doc | undefined, color: string | undefined) => void = emptyFunction;
+ Delete: () => void = emptyFunction;
+ Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = emptyFunction;
+ AddTag: (key: string, value: string) => boolean = returnFalse;
+ PinToPres: () => void = emptyFunction;
+
+ @observable public Highlighting: boolean = false;
+ @observable public Status: "pdf" | "annotation" | "snippet" | "" = "";
+ @observable public Pinned: boolean = false;
+
+ public Marquee: { left: number; top: number; width: number; height: number; } | undefined;
+
+ private _offsetY: number = 0;
+ private _offsetX: number = 0;
+ private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
+ private _commentCont: React.RefObject<HTMLDivElement> = React.createRef();
+ private _snippetButton: React.RefObject<HTMLButtonElement> = React.createRef();
+ private _dragging: boolean = false;
+ @observable private _keyValue: string = "";
+ @observable private _valueValue: string = "";
+ @observable private _added: boolean = false;
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+
+ PDFMenu.Instance = this;
+ }
+
+ pointerDown = (e: React.PointerEvent) => {
+ document.removeEventListener("pointermove", this.pointerMove);
+ document.addEventListener("pointermove", this.pointerMove);
+ document.removeEventListener("pointerup", this.pointerUp);
+ document.addEventListener("pointerup", this.pointerUp);
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ pointerMove = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ if (this._dragging) {
+ return;
+ }
+
+ this.StartDrag(e, this._commentCont.current!);
+ this._dragging = true;
+ }
+
+ pointerUp = (e: PointerEvent) => {
+ this._dragging = false;
+ document.removeEventListener("pointermove", this.pointerMove);
+ document.removeEventListener("pointerup", this.pointerUp);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ @action
+ jumpTo = (x: number, y: number, forceJump: boolean = false) => {
+ if (!this.Pinned || forceJump) {
+ this._transition = this._transitionDelay = "";
+ this._opacity = 1;
+ this._left = x;
+ this._top = y;
+ }
+ }
+
+ @action
+ fadeOut = (forceOut: boolean) => {
+ if (!this.Pinned) {
+ if (this._opacity === 0.2) {
+ this._transition = "opacity 0.1s";
+ this._transitionDelay = "";
+ this._opacity = 0;
+ this._left = this._top = -300;
+ }
+
+ if (forceOut) {
+ this._transition = "";
+ this._transitionDelay = "";
+ this._opacity = 0;
+ this._left = this._top = -300;
+ }
+ }
+ }
+
+ @action
+ pointerLeave = (e: React.PointerEvent) => {
+ if (!this.Pinned) {
+ this._transition = "opacity 0.5s";
+ this._transitionDelay = "1s";
+ this._opacity = 0.2;
+ setTimeout(() => this.fadeOut(false), 3000);
+ }
+ }
+
+ @action
+ pointerEntered = (e: React.PointerEvent) => {
+ this._transition = "opacity 0.1s";
+ this._transitionDelay = "";
+ this._opacity = 1;
+ }
+
+ @action
+ togglePin = (e: React.MouseEvent) => {
+ this.Pinned = !this.Pinned;
+ if (!this.Pinned) {
+ this.Highlighting = false;
+ }
+ }
+
+ @action
+ dragging = (e: PointerEvent) => {
+ this._left = e.pageX - this._offsetX;
+ this._top = e.pageY - this._offsetY;
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ dragEnd = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.dragging);
+ document.removeEventListener("pointerup", this.dragEnd);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ dragStart = (e: React.PointerEvent) => {
+ document.removeEventListener("pointermove", this.dragging);
+ document.addEventListener("pointermove", this.dragging);
+ document.removeEventListener("pointerup", this.dragEnd);
+ document.addEventListener("pointerup", this.dragEnd);
+
+ this._offsetX = this._mainCont.current!.getBoundingClientRect().width - e.nativeEvent.offsetX;
+ this._offsetY = e.nativeEvent.offsetY;
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ @action
+ highlightClicked = (e: React.MouseEvent) => {
+ if (!this.Pinned) {
+ this.Highlight(undefined, "#f4f442");
+ }
+ else {
+ this.Highlighting = !this.Highlighting;
+ this.Highlight(undefined, "#f4f442");
+ }
+ }
+
+ deleteClicked = (e: React.PointerEvent) => {
+ this.Delete();
+ }
+
+ handleContextMenu = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ snippetStart = (e: React.PointerEvent) => {
+ document.removeEventListener("pointermove", this.snippetDrag);
+ document.addEventListener("pointermove", this.snippetDrag);
+ document.removeEventListener("pointerup", this.snippetEnd);
+ document.addEventListener("pointerup", this.snippetEnd);
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ snippetDrag = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ if (this._dragging) {
+ return;
+ }
+ this._dragging = true;
+
+ if (this.Marquee) {
+ this.Snippet(this.Marquee);
+ }
+ }
+
+ snippetEnd = (e: PointerEvent) => {
+ this._dragging = false;
+ document.removeEventListener("pointermove", this.snippetDrag);
+ document.removeEventListener("pointerup", this.snippetEnd);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ @action
+ keyChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._keyValue = e.currentTarget.value;
+ }
+
+ @action
+ valueChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._valueValue = e.currentTarget.value;
+ }
+
+ @action
+ addTag = (e: React.PointerEvent) => {
+ if (this._keyValue.length > 0 && this._valueValue.length > 0) {
+ this._added = this.AddTag(this._keyValue, this._valueValue);
+
+ setTimeout(
+ () => {
+ runInAction(() => {
+ this._added = false;
+ });
+ }, 1000
+ );
+ }
+ }
+
+ render() {
+ let buttons = this.Status === "pdf" || this.Status === "snippet" ? [
+ <button className="pdfMenu-button" title="Click to Highlight" onClick={this.highlightClicked} key="1"
+ style={this.Highlighting ? { backgroundColor: "#121212" } : {}}>
+ <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} />
+ </button>,
+ <button className="pdfMenu-button" title="Drag to Annotate" ref={this._commentCont} onPointerDown={this.pointerDown}><FontAwesomeIcon icon="comment-alt" size="lg" key="2" /></button>,
+ this.Status === "snippet" ? <button className="pdfMenu-button" title="Drag to Snippetize Selection" onPointerDown={this.snippetStart} ref={this._snippetButton}><FontAwesomeIcon icon="cut" size="lg" /></button> : undefined,
+ <button className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin} key="3"
+ style={this.Pinned ? { backgroundColor: "#121212" } : {}}>
+ <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} />
+ </button>
+ ] : [
+ <button className="pdfMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}><FontAwesomeIcon icon="trash-alt" size="lg" key="1" /></button>,
+ <button className="pdfMenu-button" title="Pin to Presentation" onPointerDown={this.PinToPres}><FontAwesomeIcon icon="map-pin" size="lg" key="2" /></button>,
+ <div className="pdfMenu-addTag" key="3">
+ <input onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} />
+ <input onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} />
+ </div>,
+ <button className="pdfMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}><FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" key="4" /></button>,
+ ];
+
+ return (
+ <div className="pdfMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu}
+ style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay }}>
+ {buttons}
+ <div className="pdfMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} />
+ </div >
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
new file mode 100644
index 000000000..0fde764d0
--- /dev/null
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -0,0 +1,131 @@
+
+.textLayer {
+ div {
+ user-select: text;
+ }
+}
+.viewer-button-cont {
+ position: absolute;
+ display: flex;
+ justify-content: space-evenly;
+ align-items: center;
+}
+
+.viewer-previousPage,
+.viewer-nextPage {
+ background: grey;
+ font-weight: bold;
+ opacity: 0.5;
+ padding: 0 10px;
+ border-radius: 5px;
+}
+
+.textLayer {
+ user-select: auto;
+}
+.viewer {
+ // position: absolute;
+ // top: 0;
+}
+
+.pdfViewer-text {
+
+ .page {
+ .canvasWrapper {
+ display: none;
+ }
+
+ .textLayer {
+ position: relative;
+ user-select: none;
+ }
+ }
+}
+.pdfViewer-viewerCont {
+ width:100%;
+}
+
+.page-cont {
+ .textLayer {
+ user-select: auto;
+
+ div {
+ user-select: text;
+ }
+ }
+}
+
+.pdfViewer-overlayCont {
+ position: absolute;
+ width: 100%;
+ height: 100px;
+ background: #121721;
+ bottom: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 20px;
+ overflow: hidden;
+ transition: left .5s;
+}
+
+.pdfViewer-overlaySearchBar {
+ width: 20%;
+ height: 100%;
+ font-size: 30px;
+ padding: 5px;
+}
+
+.pdfViewer-overlayButton {
+ border-bottom-left-radius: 50%;
+ display: flex;
+ justify-content: space-evenly;
+ align-items: center;
+ height: 70px;
+ background: none;
+ padding: 0;
+ position: absolute;
+
+ .pdfViewer-overlayButton-arrow {
+ width: 0;
+ height: 0;
+ border-top: 25px solid transparent;
+ border-bottom: 25px solid transparent;
+ border-right: 25px solid #121721;
+ transition: all 0.5s;
+ }
+
+ .pdfViewer-overlayButton-iconCont {
+ background: #121721;
+ height: 50px;
+ width: 70px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-left: -2px;
+ border-radius: 3px;
+ }
+}
+
+.pdfViewer-overlayButton:hover {
+ background: none;
+}
+
+.pdfViewer-annotationBox {
+ position: absolute;
+ background-color: red;
+ opacity: 0.1;
+}
+
+.pdfViewer-annotationLayer {
+ position: absolute;
+ top: 0;
+}
+
+
+
+.pdfViewer-pinAnnotation {
+ background-color: red;
+ position: absolute;
+ border-radius: 100%;
+} \ No newline at end of file
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
new file mode 100644
index 000000000..e49611a5e
--- /dev/null
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -0,0 +1,715 @@
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import * as Pdfjs from "pdfjs-dist";
+import "pdfjs-dist/web/pdf_viewer.css";
+import * as rp from "request-promise";
+import { Dictionary } from "typescript-collections";
+import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { List } from "../../../new_fields/List";
+import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../../new_fields/Types";
+import { emptyFunction } from "../../../Utils";
+import { DocServer } from "../../DocServer";
+import { Docs, DocUtils, DocumentOptions } from "../../documents/Documents";
+import { DocumentManager } from "../../util/DocumentManager";
+import { DragManager } from "../../util/DragManager";
+import { DocumentView } from "../nodes/DocumentView";
+import { PDFBox } from "../nodes/PDFBox";
+import Page from "./Page";
+import "./PDFViewer.scss";
+import React = require("react");
+import PDFMenu from "./PDFMenu";
+import { UndoManager } from "../../util/UndoManager";
+import { CompileScript, CompiledScript, CompileResult } from "../../util/Scripting";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import Annotation from "./Annotation";
+const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
+
+export const scale = 2;
+interface IPDFViewerProps {
+ url: string;
+ loaded: (nw: number, nh: number, np: number) => void;
+ scrollY: number;
+ parent: PDFBox;
+}
+
+/**
+ * Wrapper that loads the PDF and cascades the pdf down
+ */
+@observer
+export class PDFViewer extends React.Component<IPDFViewerProps> {
+ @observable _pdf: Opt<Pdfjs.PDFDocumentProxy>;
+ private _mainDiv = React.createRef<HTMLDivElement>();
+
+ @action
+ componentDidMount() {
+ Pdfjs.getDocument(this.props.url).promise.then(pdf => runInAction(() => this._pdf = pdf));
+ }
+
+ render() {
+ return (
+ <div className="pdfViewer-viewerCont" ref={this._mainDiv}>
+ {!this._pdf ? (null) :
+ <Viewer pdf={this._pdf} loaded={this.props.loaded} scrollY={this.props.scrollY} parent={this.props.parent} mainCont={this._mainDiv} url={this.props.url} />}
+ </div>
+ );
+ }
+}
+
+interface IViewerProps {
+ pdf: Pdfjs.PDFDocumentProxy;
+ loaded: (nw: number, nh: number, np: number) => void;
+ scrollY: number;
+ parent: PDFBox;
+ mainCont: React.RefObject<HTMLDivElement>;
+ url: string;
+}
+
+/**
+ * Handles rendering and virtualization of the pdf
+ */
+@observer
+export class Viewer extends React.Component<IViewerProps> {
+ // _visibleElements is the array of JSX elements that gets rendered
+ @observable.shallow private _visibleElements: JSX.Element[] = [];
+ // _isPage is an array that tells us whether or not an index is rendered as a page or as a placeholder
+ @observable private _isPage: string[] = [];
+ @observable private _pageSizes: { width: number, height: number }[] = [];
+ @observable private _annotations: Doc[] = [];
+ @observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>();
+ @observable private _script: CompileResult | undefined;
+ @observable private _searching: boolean = false;
+
+ @observable public Index: number = -1;
+
+ private _pageBuffer: number = 1;
+ private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
+ private _reactionDisposer?: IReactionDisposer;
+ private _annotationReactionDisposer?: IReactionDisposer;
+ private _dropDisposer?: DragManager.DragDropDisposer;
+ private _filterReactionDisposer?: IReactionDisposer;
+ private _activeReactionDisposer?: IReactionDisposer;
+ private _viewer: React.RefObject<HTMLDivElement>;
+ private _mainCont: React.RefObject<HTMLDivElement>;
+ // private _textContent: Pdfjs.TextContent[] = [];
+ private _pdfFindController: any;
+ private _searchString: string = "";
+ private _rendered: boolean = false;
+ private _pageIndex: number = -1;
+ private _matchIndex: number = 0;
+
+ constructor(props: IViewerProps) {
+ super(props);
+
+ let scriptfield = Cast(this.props.parent.Document.filterScript, ScriptField);
+ this._script = scriptfield ? scriptfield.script : CompileScript("return true");
+ this._viewer = React.createRef();
+ this._mainCont = React.createRef();
+ }
+
+ componentDidUpdate = (prevProps: IViewerProps) => {
+ if (this.scrollY !== prevProps.scrollY) {
+ this.renderPages();
+ }
+ }
+
+ @action
+ componentDidMount = () => {
+ this._reactionDisposer = reaction(
+
+ () => [this.props.parent.props.active(), this.startIndex, this._pageSizes.length ? this.endIndex : 0],
+ async () => {
+ await this.initialLoad();
+ this.renderPages();
+ }, { fireImmediately: true });
+
+ this._annotationReactionDisposer = reaction(
+ () => this.props.parent.Document && DocListCast(this.props.parent.Document.annotations),
+ (annotations: Doc[]) =>
+ annotations && annotations.length && this.renderAnnotations(annotations, true),
+ { fireImmediately: true });
+
+ this._activeReactionDisposer = reaction(
+ () => this.props.parent.props.active(),
+ () => {
+ runInAction(() => {
+ if (!this.props.parent.props.active()) {
+ this._searching = false;
+ this._pdfFindController = null;
+ if (this._viewer.current) {
+ let cns = this._viewer.current.childNodes;
+ for (let i = cns.length - 1; i >= 0; i--) {
+ cns.item(i).remove();
+ }
+ }
+ }
+ });
+ }
+ );
+
+ if (this.props.parent.props.ContainingCollectionView) {
+ this._filterReactionDisposer = reaction(
+ () => this.props.parent.Document.filterScript,
+ () => {
+ runInAction(() => {
+ let scriptfield = Cast(this.props.parent.Document.filterScript, ScriptField);
+ this._script = scriptfield ? scriptfield.script : CompileScript("return true");
+ if (this.props.parent.props.ContainingCollectionView) {
+ let ccvAnnos = DocListCast(this.props.parent.props.ContainingCollectionView.props.Document.annotations);
+ ccvAnnos.forEach(d => {
+ if (this._script && this._script.compiled) {
+ let run = this._script.run(d);
+ if (run.success) {
+ d.opacity = run.result ? 1 : 0;
+ }
+ }
+ });
+ }
+ this.Index = -1;
+ });
+ }
+ );
+ }
+
+ if (this._mainCont.current) {
+ this._dropDisposer = this._mainCont.current && DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } });
+ }
+ }
+
+ componentWillUnmount = () => {
+ this._reactionDisposer && this._reactionDisposer();
+ this._annotationReactionDisposer && this._annotationReactionDisposer();
+ this._filterReactionDisposer && this._filterReactionDisposer();
+ this._dropDisposer && this._dropDisposer();
+ }
+
+ scrollTo(y: number) {
+ this.props.parent.scrollTo(y);
+ }
+
+ @action
+ initialLoad = async () => {
+ if (this._pageSizes.length === 0) {
+ let pageSizes = Array<{ width: number, height: number }>(this.props.pdf.numPages);
+ this._isPage = Array<string>(this.props.pdf.numPages);
+ // this._textContent = Array<Pdfjs.TextContent>(this.props.pdf.numPages);
+ const proms: Pdfjs.PDFPromise<any>[] = [];
+ for (let i = 0; i < this.props.pdf.numPages; i++) {
+ proms.push(this.props.pdf.getPage(i + 1).then(page => runInAction(() => {
+ pageSizes[i] = {
+ width: (page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]) * scale,
+ height: (page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0]) * scale
+ };
+ // let x = page.getViewport(scale);
+ // page.getTextContent().then((text: Pdfjs.TextContent) => {
+ // // let tc = new Pdfjs.TextContentItem()
+ // // let tc = {str: }
+ // this._textContent[i] = text;
+ // // text.items.forEach(t => {
+ // // tcStr += t.str;
+ // // })
+ // });
+ // pageSizes[i] = { width: x.width, height: x.height };
+ })));
+ }
+ await Promise.all(proms);
+ runInAction(() =>
+ Array.from(Array((this._pageSizes = pageSizes).length).keys()).map(this.getPlaceholderPage));
+ this.props.loaded(Math.max(...pageSizes.map(i => i.width)), pageSizes[0].height, this.props.pdf.numPages);
+ // this.props.loaded(Math.max(...pageSizes.map(i => i.width)), pageSizes[0].height, this.props.pdf.numPages);
+
+ let startY = NumCast(this.props.parent.Document.startY);
+ let ccv = this.props.parent.props.ContainingCollectionView;
+ if (ccv) {
+ ccv.props.Document.panY = startY;
+ }
+ this.props.parent.Document.scrollY = 0;
+ this.props.parent.Document.scrollY = startY + 1;
+ }
+ }
+
+ makeAnnotationDocument = (sourceDoc: Doc | undefined, s: number, color: string): Doc => {
+ let annoDocs: Doc[] = [];
+ let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {});
+
+ mainAnnoDoc.title = "Annotation on " + StrCast(this.props.parent.Document.title);
+ mainAnnoDoc.pdfDoc = this.props.parent.Document;
+ let minY = Number.MAX_VALUE;
+ this._savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => {
+ for (let anno of value) {
+ let annoDoc = new Doc();
+ if (anno.style.left) annoDoc.x = parseInt(anno.style.left) / scale;
+ if (anno.style.top) {
+ annoDoc.y = parseInt(anno.style.top) / scale;
+ minY = Math.min(parseInt(anno.style.top), minY);
+ }
+ if (anno.style.height) annoDoc.height = parseInt(anno.style.height) / scale;
+ if (anno.style.width) annoDoc.width = parseInt(anno.style.width) / scale;
+ annoDoc.page = key;
+ annoDoc.target = sourceDoc;
+ annoDoc.group = mainAnnoDoc;
+ annoDoc.color = color;
+ annoDoc.type = AnnotationTypes.Region;
+ annoDocs.push(annoDoc);
+ anno.remove();
+ }
+ });
+
+ mainAnnoDoc.y = Math.max(minY, 0);
+ mainAnnoDoc.annotations = new List<Doc>(annoDocs);
+ if (sourceDoc) {
+ DocUtils.MakeLink(sourceDoc, mainAnnoDoc, undefined, `Annotation from ${StrCast(this.props.parent.Document.title)}`, "", StrCast(this.props.parent.Document.title));
+ }
+ this._savedAnnotations.clear();
+ this.Index = -1;
+ return mainAnnoDoc;
+ }
+
+ drop = async (e: Event, de: DragManager.DropEvent) => {
+ if (de.data instanceof DragManager.LinkDragData) {
+ let sourceDoc = de.data.linkSourceDocument;
+ let destDoc = this.makeAnnotationDocument(sourceDoc, 1, "red");
+ let targetAnnotations = DocListCast(this.props.parent.Document.annotations);
+ if (targetAnnotations) {
+ targetAnnotations.push(destDoc);
+ this.props.parent.Document.annotations = new List<Doc>(targetAnnotations);
+ }
+ else {
+ this.props.parent.Document.annotations = new List<Doc>([destDoc]);
+ }
+ e.stopPropagation();
+ }
+ }
+ /**
+ * Called by the Page class when it gets rendered, initializes the lists and
+ * puts a placeholder with all of the correct page sizes when all of the pages have been loaded.
+ */
+ @action
+ pageLoaded = (index: number, page: Pdfjs.PDFPageViewport): void => {
+ this.props.loaded(page.width, page.height, this.props.pdf.numPages);
+ }
+
+ @action
+ getPlaceholderPage = (page: number) => {
+ if (this._isPage[page] !== "none") {
+ this._isPage[page] = "none";
+ this._visibleElements[page] = (
+ <div key={`${this.props.url}-placeholder-${page + 1}`} className="pdfviewer-placeholder"
+ style={{ width: this._pageSizes[page].width, height: this._pageSizes[page].height }} />
+ );
+ }
+ }
+
+ @action
+ getRenderedPage = (page: number) => {
+ if (this._isPage[page] !== "page") {
+ this._isPage[page] = "page";
+ this._visibleElements[page] = (
+ <Page
+ size={this._pageSizes[page]}
+ pdf={this.props.pdf}
+ page={page}
+ numPages={this.props.pdf.numPages}
+ key={`${this.props.url}-rendered-${page + 1}`}
+ name={`${this.props.pdf.fingerprint + `-page${page + 1}`}`}
+ pageLoaded={this.pageLoaded}
+ parent={this.props.parent}
+ makePin={emptyFunction}
+ renderAnnotations={this.renderAnnotations}
+ createAnnotation={this.createAnnotation}
+ sendAnnotations={this.receiveAnnotations}
+ makeAnnotationDocuments={this.makeAnnotationDocument}
+ getScrollFromPage={this.getScrollFromPage}
+ {...this.props} />
+ );
+ }
+ }
+
+ // change the address to be the file address of the PNG version of each page
+ // file address of the pdf
+ @action
+ getPageImage = async (page: number) => {
+ let handleError = () => this.getRenderedPage(page);
+ if (this._isPage[page] !== "image") {
+ this._isPage[page] = "image";
+ const address = this.props.url;
+ try {
+ let res = JSON.parse(await rp.get(DocServer.prepend(`/thumbnail${address.substring("files/".length, address.length - ".pdf".length)}-${page + 1}.PNG`)));
+ runInAction(() => this._visibleElements[page] =
+ <img key={res.path} src={res.path} onError={handleError}
+ style={{ width: `${parseInt(res.width) * scale}px`, height: `${parseInt(res.height) * scale}px` }} />);
+ } catch (e) {
+
+ }
+ }
+ }
+
+ @computed get scrollY(): number { return this.props.scrollY; }
+
+ // startIndex: where to start rendering pages
+ @computed get startIndex(): number { return Math.max(0, this.getPageFromScroll(this.scrollY) - this._pageBuffer); }
+
+ // endIndex: where to end rendering pages
+ @computed get endIndex(): number {
+ return Math.min(this.props.pdf.numPages - 1, this.getPageFromScroll(this.scrollY + this._pageSizes[0].height) + this._pageBuffer);
+ }
+
+ @action
+ renderPages = () => {
+ for (let i = 0; i < this.props.pdf.numPages; i++) {
+ if (i < this.startIndex || i > this.endIndex) {
+ this.getPlaceholderPage(i); // pages outside of the pdf use empty stand-in divs
+ } else {
+ if (this.props.parent.props.active()) {
+ this.getRenderedPage(i);
+ } else {
+ this.getPageImage(i);
+ }
+ }
+ }
+ }
+
+ @action
+ private renderAnnotations = (annotations: Doc[], removeOldAnnotations: boolean): void => {
+ if (removeOldAnnotations) {
+ this._annotations = annotations;
+ }
+ else {
+ this._annotations.push(...annotations);
+ this._annotations = new Array<Doc>(...this._annotations);
+ }
+ }
+
+ @action
+ receiveAnnotations = (annotations: HTMLDivElement[], page: number) => {
+ if (page === -1) {
+ this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove()));
+ this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, annotations));
+ }
+ else {
+ this._savedAnnotations.setValue(page, annotations);
+ }
+ }
+
+ sendAnnotations = (page: number): HTMLDivElement[] | undefined => {
+ return this._savedAnnotations.getValue(page);
+ }
+
+ // get the page index that the vertical offset passed in is on
+ getPageFromScroll = (vOffset: number) => {
+ let index = 0;
+ let currOffset = vOffset;
+ while (index < this._pageSizes.length && currOffset - this._pageSizes[index].height > 0) {
+ currOffset -= this._pageSizes[index++].height;
+ }
+ return index;
+ }
+
+ getScrollFromPage = (index: number): number => {
+ let counter = 0;
+ for (let i = 0; i < Math.min(this.props.pdf.numPages, index); i++) {
+ counter += this._pageSizes[i].height;
+ }
+ return counter;
+ }
+
+ createAnnotation = (div: HTMLDivElement, page: number) => {
+ if (this._annotationLayer.current) {
+ if (div.style.top) {
+ div.style.top = (parseInt(div.style.top) + this.getScrollFromPage(page)).toString();
+ }
+ this._annotationLayer.current.append(div);
+ let savedPage = this._savedAnnotations.getValue(page);
+ if (savedPage) {
+ savedPage.push(div);
+ this._savedAnnotations.setValue(page, savedPage);
+ }
+ else {
+ this._savedAnnotations.setValue(page, [div]);
+ }
+ }
+ }
+
+ renderAnnotation = (anno: Doc, index: number): JSX.Element => {
+ return <Annotation anno={anno} index={index} parent={this} key={`${anno[Id]}-annotation`} />;
+ }
+
+ @action
+ pointerDown = () => {
+ // this._searching = false;
+ }
+
+ @action
+ search = (searchString: string) => {
+ if (searchString.length === 0) {
+ return;
+ }
+
+ if (this._rendered) {
+ this._pdfFindController.executeCommand('find',
+ {
+ caseSensitive: false,
+ findPrevious: undefined,
+ highlightAll: true,
+ phraseSearch: true,
+ query: searchString
+ });
+ }
+ else {
+ let container = this._mainCont.current;
+ if (container) {
+ container.addEventListener("pagerendered", () => {
+ console.log("rendered");
+ this._pdfFindController.executeCommand('find',
+ {
+ caseSensitive: false,
+ findPrevious: undefined,
+ highlightAll: true,
+ phraseSearch: true,
+ query: searchString
+ });
+ this._rendered = true;
+ });
+ }
+ }
+
+ // let viewer = this._viewer.current;
+
+ // if (!this._pdfFindController) {
+ // if (container && viewer) {
+ // let simpleLinkService = new SimpleLinkService();
+ // let pdfViewer = new PDFJSViewer.PDFViewer({
+ // container: container,
+ // viewer: viewer,
+ // linkService: simpleLinkService
+ // });
+ // simpleLinkService.setPdf(this.props.pdf);
+ // container.addEventListener("pagesinit", () => {
+ // pdfViewer.currentScaleValue = 1;
+ // });
+ // container.addEventListener("pagerendered", () => {
+ // console.log("rendered");
+ // this._pdfFindController.executeCommand('find',
+ // {
+ // caseSensitive: false,
+ // findPrevious: undefined,
+ // highlightAll: true,
+ // phraseSearch: true,
+ // query: searchString
+ // });
+ // });
+ // pdfViewer.setDocument(this.props.pdf);
+ // this._pdfFindController = new PDFJSViewer.PDFFindController(pdfViewer);
+ // // this._pdfFindController._linkService = pdfLinkService;
+ // pdfViewer.findController = this._pdfFindController;
+ // }
+ // }
+ // else {
+ // this._pdfFindController.executeCommand('find',
+ // {
+ // caseSensitive: false,
+ // findPrevious: undefined,
+ // highlightAll: true,
+ // phraseSearch: true,
+ // query: searchString
+ // });
+ // }
+ }
+
+ searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._searchString = e.currentTarget.value;
+ }
+
+ @action
+ toggleSearch = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ this._searching = !this._searching;
+
+ if (this._searching) {
+ let container = this._mainCont.current;
+ let viewer = this._viewer.current;
+
+ if (!this._pdfFindController) {
+ if (container && viewer) {
+ let simpleLinkService = new SimpleLinkService();
+ let pdfViewer = new PDFJSViewer.PDFViewer({
+ container: container,
+ viewer: viewer,
+ linkService: simpleLinkService
+ });
+ simpleLinkService.setPdf(this.props.pdf);
+ container.addEventListener("pagesinit", () => {
+ pdfViewer.currentScaleValue = 1;
+ });
+ container.addEventListener("pagerendered", () => {
+ console.log("rendered");
+ this._rendered = true;
+ });
+ pdfViewer.setDocument(this.props.pdf);
+ this._pdfFindController = new PDFJSViewer.PDFFindController(pdfViewer);
+ // this._pdfFindController._linkService = pdfLinkService;
+ pdfViewer.findController = this._pdfFindController;
+ }
+ }
+ }
+ else {
+ this._pdfFindController = null;
+ if (this._viewer.current) {
+ let cns = this._viewer.current.childNodes;
+ for (let i = cns.length - 1; i >= 0; i--) {
+ cns.item(i).remove();
+ }
+ }
+ }
+ }
+
+ @action
+ prevAnnotation = (e: React.MouseEvent) => {
+ e.stopPropagation();
+
+ // if (this.Index > 0) {
+ // this.Index--;
+ // }
+ this.Index = Math.max(this.Index - 1, 0);
+ }
+
+ @action
+ nextAnnotation = (e: React.MouseEvent) => {
+ e.stopPropagation();
+
+ let compiled = this._script;
+ let filtered = this._annotations.filter(anno => {
+ if (compiled && compiled.compiled) {
+ let run = compiled.run({ this: anno });
+ if (run.success) {
+ return run.result;
+ }
+ }
+ return true;
+ });
+ this.Index = Math.min(this.Index + 1, filtered.length - 1);
+ }
+
+ nextResult = () => {
+ // if (this._viewer.current) {
+ // let results = this._pdfFindController.pageMatches;
+ // if (results && results.length) {
+ // if (this._pageIndex === this.props.pdf.numPages && this._matchIndex === results[this._pageIndex].length - 1) {
+ // return;
+ // }
+ // if (this._pageIndex === -1 || this._matchIndex === results[this._pageIndex].length - 1) {
+ // this._matchIndex = 0;
+ // this._pageIndex++;
+ // }
+ // else {
+ // this._matchIndex++;
+ // }
+ // this._pdfFindController._nextMatch()
+ // let nextMatch = this._viewer.current.children[this._pageIndex].children[1].children[results[this._pageIndex][this._matchIndex]];
+ // rconsole.log(nextMatch);
+ // this.props.parent.scrollTo(nextMatch.getBoundingClientRect().top);
+ // nextMatch.setAttribute("style", nextMatch.getAttribute("style") ? nextMatch.getAttribute("style") + ", background-color: green" : "background-color: green");
+ // }
+ // }
+ }
+
+ render() {
+ let compiled = this._script;
+ return (
+ <div ref={this._mainCont} style={{ pointerEvents: "all" }} onPointerDown={this.pointerDown}>
+ <div className="viewer" style={this._searching ? { position: "absolute", top: 0 } : {}}>
+ {this._visibleElements}
+ </div>
+ <div className="pdfViewer-text" ref={this._viewer} style={{ transform: "scale(1.5)", transformOrigin: "top left" }} />
+ <div className="pdfViewer-annotationLayer"
+ style={{
+ height: this.props.parent.Document.nativeHeight, width: `100%`,
+ pointerEvents: this.props.parent.props.active() ? "none" : "all"
+ }}>
+ <div className="pdfViewer-annotationLayer-subCont" ref={this._annotationLayer}>
+ {this._annotations.filter(anno => {
+ if (compiled && compiled.compiled) {
+ let run = compiled.run({ this: anno });
+ if (run.success) {
+ return run.result;
+ }
+ }
+ return true;
+ }).sort((a: Doc, b: Doc) => NumCast(a.y) - NumCast(b.y))
+ .map((anno: Doc, index: number) => this.renderAnnotation(anno, index))}
+ </div>
+ </div>
+ <div className="pdfViewer-overlayCont" onPointerDown={(e) => e.stopPropagation()}
+ style={{
+ bottom: -this.props.scrollY,
+ left: `${this._searching ? 0 : 100}%`
+ }}>
+ <button className="pdfViewer-overlayButton" title="Open Search Bar"></button>
+ {/* <button title="Previous Result" onClick={() => this.search(this._searchString)}><FontAwesomeIcon icon="arrow-up" size="3x" color="white" /></button>
+ <button title="Next Result" onClick={this.nextResult}><FontAwesomeIcon icon="arrow-down" size="3x" color="white" /></button> */}
+ <input placeholder="Search" className="pdfViewer-overlaySearchBar" onChange={this.searchStringChanged} />
+ <button title="Search" onClick={() => this.search(this._searchString)}><FontAwesomeIcon icon="search" size="3x" color="white" /></button>
+ </div>
+ <button className="pdfViewer-overlayButton" onClick={this.prevAnnotation} title="Previous Annotation"
+ style={{ bottom: -this.props.scrollY + 280, right: 10, display: this.props.parent.props.active() ? "flex" : "none" }}>
+ <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}>
+ <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-up"} size="3x" />
+ </div>
+ </button>
+ <button className="pdfViewer-overlayButton" onClick={this.nextAnnotation} title="Next Annotation"
+ style={{ bottom: -this.props.scrollY + 200, right: 10, display: this.props.parent.props.active() ? "flex" : "none" }}>
+ <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}>
+ <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-down"} size="3x" />
+ </div>
+ </button>
+ <button className="pdfViewer-overlayButton" onClick={this.toggleSearch} title="Open Search Bar"
+ style={{ bottom: -this.props.scrollY + 10, right: 0, display: this.props.parent.props.active() ? "flex" : "none" }}>
+ <div className="pdfViewer-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()}></div>
+ <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}>
+ <FontAwesomeIcon style={{ color: "white" }} icon={this._searching ? "times" : "search"} size="3x" />
+ </div>
+ </button>
+ </div >
+ );
+ }
+}
+
+export enum AnnotationTypes {
+ Region
+}
+
+class SimpleLinkService {
+ externalLinkTarget: any = null;
+ externalLinkRel: any = null;
+ pdf: any = null;
+
+ navigateTo(dest: any) { }
+
+ getDestinationHash(dest: any) { return "#"; }
+
+ getAnchorUrl(hash: any) { return "#"; }
+
+ setHash(hash: any) { }
+
+ executeNamedAction(action: any) { }
+
+ cachePageRef(pageNum: any, pageRef: any) { }
+
+ get pagesCount() {
+ return this.pdf ? this.pdf.numPages : 0;
+ }
+
+ get page() {
+ return 0;
+ }
+
+ setPdf(pdf: any) {
+ this.pdf = pdf;
+ }
+
+ get rotation() {
+ return 0;
+ }
+ set rotation(value: any) { }
+} \ No newline at end of file
diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx
new file mode 100644
index 000000000..021841541
--- /dev/null
+++ b/src/client/views/pdf/Page.tsx
@@ -0,0 +1,416 @@
+import { observer } from "mobx-react";
+import React = require("react");
+import { observable, action, runInAction, IReactionDisposer, reaction } from "mobx";
+import * as Pdfjs from "pdfjs-dist";
+import { Opt, Doc, FieldResult, Field, DocListCast, WidthSym, HeightSym } from "../../../new_fields/Doc";
+import "./PDFViewer.scss";
+import "pdfjs-dist/web/pdf_viewer.css";
+import { PDFBox } from "../nodes/PDFBox";
+import { DragManager } from "../../util/DragManager";
+import { Docs, DocUtils } from "../../documents/Documents";
+import { List } from "../../../new_fields/List";
+import { emptyFunction } from "../../../Utils";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { listSpec } from "../../../new_fields/Schema";
+import { menuBar } from "prosemirror-menu";
+import { AnnotationTypes, PDFViewer, scale } from "./PDFViewer";
+import PDFMenu from "./PDFMenu";
+import { UndoManager } from "../../util/UndoManager";
+import { copy } from "typescript-collections/dist/lib/arrays";
+
+
+interface IPageProps {
+ size: { width: number, height: number };
+ pdf: Opt<Pdfjs.PDFDocumentProxy>;
+ name: string;
+ numPages: number;
+ page: number;
+ pageLoaded: (index: number, page: Pdfjs.PDFPageViewport) => void;
+ parent: PDFBox;
+ renderAnnotations: (annotations: Doc[], removeOld: boolean) => void;
+ makePin: (x: number, y: number, page: number) => void;
+ sendAnnotations: (annotations: HTMLDivElement[], page: number) => void;
+ createAnnotation: (div: HTMLDivElement, page: number) => void;
+ makeAnnotationDocuments: (doc: Doc | undefined, scale: number, color: string, linkTo: boolean) => Doc;
+ getScrollFromPage: (page: number) => number;
+}
+
+@observer
+export default class Page extends React.Component<IPageProps> {
+ @observable private _state: string = "N/A";
+ @observable private _width: number = this.props.size.width;
+ @observable private _height: number = this.props.size.height;
+ @observable private _page: Opt<Pdfjs.PDFPageProxy>;
+ @observable private _currPage: number = this.props.page + 1;
+ @observable private _marqueeX: number = 0;
+ @observable private _marqueeY: number = 0;
+ @observable private _marqueeWidth: number = 0;
+ @observable private _marqueeHeight: number = 0;
+ @observable private _rotate: string = "";
+
+ private _canvas: React.RefObject<HTMLCanvasElement>;
+ private _textLayer: React.RefObject<HTMLDivElement>;
+ private _annotationLayer: React.RefObject<HTMLDivElement>;
+ private _marquee: React.RefObject<HTMLDivElement>;
+ // private _curly: React.RefObject<HTMLImageElement>;
+ private _marqueeing: boolean = false;
+ private _reactionDisposer?: IReactionDisposer;
+
+ constructor(props: IPageProps) {
+ super(props);
+ this._canvas = React.createRef();
+ this._textLayer = React.createRef();
+ this._annotationLayer = React.createRef();
+ this._marquee = React.createRef();
+ // this._curly = React.createRef();
+ }
+
+ componentDidMount = (): void => {
+ if (this.props.pdf) {
+ this.update(this.props.pdf);
+ }
+ }
+
+ componentWillUnmount = (): void => {
+ if (this._reactionDisposer) {
+ this._reactionDisposer();
+ }
+ }
+
+ componentDidUpdate = (): void => {
+ if (this.props.pdf) {
+ this.update(this.props.pdf);
+ }
+ }
+
+ private update = (pdf: Pdfjs.PDFDocumentProxy): void => {
+ if (pdf) {
+ this.loadPage(pdf);
+ }
+ else {
+ this._state = "loading";
+ }
+ }
+
+ private loadPage = (pdf: Pdfjs.PDFDocumentProxy): void => {
+ if (this._state === "rendering" || this._page) return;
+
+ pdf.getPage(this._currPage).then(
+ (page: Pdfjs.PDFPageProxy): void => {
+ this._state = "rendering";
+ this.renderPage(page);
+ }
+ );
+ }
+
+ @action
+ private renderPage = (page: Pdfjs.PDFPageProxy): void => {
+ // lower scale = easier to read at small sizes, higher scale = easier to read at large sizes
+ let viewport = page.getViewport(scale);
+ let canvas = this._canvas.current;
+ let textLayer = this._textLayer.current;
+ if (canvas && textLayer) {
+ let ctx = canvas.getContext("2d");
+ canvas.width = viewport.width;
+ this._width = viewport.width;
+ canvas.height = viewport.height;
+ this._height = viewport.height;
+ this.props.pageLoaded(this._currPage, viewport);
+ if (ctx) {
+ // renders the page onto the canvas context
+ page.render({ canvasContext: ctx, viewport: viewport });
+ // renders text onto the text container
+ page.getTextContent().then((res: Pdfjs.TextContent): void => {
+ //@ts-ignore
+ Pdfjs.renderTextLayer({
+ textContent: res,
+ container: textLayer,
+ viewport: viewport
+ });
+ });
+
+ this._page = page;
+ }
+ }
+ }
+
+ @action
+ highlight = (targetDoc?: Doc, color: string = "red") => {
+ // creates annotation documents for current highlights
+ let annotationDoc = this.props.makeAnnotationDocuments(targetDoc, scale, color, false);
+ let targetAnnotations = Cast(this.props.parent.Document.annotations, listSpec(Doc));
+ if (targetAnnotations === undefined) {
+ Doc.GetProto(this.props.parent.Document).annotations = new List([annotationDoc]);
+ } else {
+ targetAnnotations.push(annotationDoc);
+ }
+ return annotationDoc;
+ }
+
+ /**
+ * This is temporary for creating annotations from highlights. It will
+ * start a drag event and create or put the necessary info into the drag event.
+ */
+ @action
+ startDrag = (e: PointerEvent, ele: HTMLDivElement): void => {
+ e.preventDefault();
+ e.stopPropagation();
+ let thisDoc = this.props.parent.Document;
+ // document that this annotation is linked to
+ let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "New Annotation" });
+ targetDoc.targetPage = this.props.page;
+ let annotationDoc = this.highlight(targetDoc, "red");
+ // create dragData and star tdrag
+ let dragData = new DragManager.AnnotationDragData(thisDoc, annotationDoc, targetDoc);
+ if (this._textLayer.current) {
+ DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, {
+ handlers: {
+ dragComplete: emptyFunction,
+ },
+ hideSource: false
+ });
+ }
+ }
+
+ // cleans up events and boolean
+ endDrag = (e: PointerEvent): void => {
+ // document.removeEventListener("pointermove", this.startDrag);
+ // document.removeEventListener("pointerup", this.endDrag);
+ e.stopPropagation();
+ }
+
+ createSnippet = (marquee: { left: number, top: number, width: number, height: number }): void => {
+ let doc = this.props.parent.Document;
+ let view = Doc.MakeAlias(doc);
+ let data = Doc.MakeDelegate(doc.proto!);
+ data.title = StrCast(data.title) + "_snippet";
+ view.proto = data;
+ view.nativeHeight = marquee.height;
+ view.height = (doc[WidthSym]() / NumCast(doc.nativeWidth)) * marquee.height;
+ view.nativeWidth = doc.nativeWidth;
+ view.startY = marquee.top + this.props.getScrollFromPage(this.props.page);
+ view.width = doc[WidthSym]();
+ let dragData = new DragManager.DocumentDragData([view], [undefined]);
+ DragManager.StartDocumentDrag([], dragData, 0, 0);
+ }
+
+ @action
+ onPointerDown = (e: React.PointerEvent): void => {
+ // if alt+left click, drag and annotate
+ if (e.altKey && e.button === 0) {
+ e.stopPropagation();
+
+ // document.removeEventListener("pointermove", this.startDrag);
+ // document.addEventListener("pointermove", this.startDrag);
+ // document.removeEventListener("pointerup", this.endDrag);
+ // document.addEventListener("pointerup", this.endDrag);
+ }
+ else if (e.button === 0) {
+ PDFMenu.Instance.StartDrag = this.startDrag;
+ PDFMenu.Instance.Highlight = this.highlight;
+ PDFMenu.Instance.Snippet = this.createSnippet;
+ PDFMenu.Instance.Status = "pdf";
+ PDFMenu.Instance.fadeOut(true);
+ let target: any = e.target;
+ if (target && target.parentElement === this._textLayer.current) {
+ e.stopPropagation();
+ }
+ else {
+ // set marquee x and y positions to the spatially transformed position
+ let current = this._textLayer.current;
+ if (current) {
+ let boundingRect = current.getBoundingClientRect();
+ this._marqueeX = (e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width);
+ this._marqueeY = (e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height);
+ }
+ this._marqueeing = true;
+ if (this._marquee.current) this._marquee.current.style.opacity = "0.2";
+ }
+ document.removeEventListener("pointermove", this.onSelectStart);
+ document.addEventListener("pointermove", this.onSelectStart);
+ document.removeEventListener("pointerup", this.onSelectEnd);
+ document.addEventListener("pointerup", this.onSelectEnd);
+ if (!e.ctrlKey) {
+ this.props.sendAnnotations([], -1);
+ }
+ }
+ }
+
+ @action
+ onSelectStart = (e: PointerEvent): void => {
+ let target: any = e.target;
+ if (this._marqueeing) {
+ let current = this._textLayer.current;
+ if (current) {
+ // transform positions and find the width and height to set the marquee to
+ let boundingRect = current.getBoundingClientRect();
+ this._marqueeWidth = (e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width) - this._marqueeX;
+ this._marqueeHeight = (e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height) - this._marqueeY;
+ let { background, opacity, transform: transform } = this.getCurlyTransform();
+ if (this._marquee.current /*&& this._curly.current*/) {
+ this._marquee.current.style.background = background;
+ // this._curly.current.style.opacity = opacity;
+ this._rotate = transform;
+ }
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ else if (target && target.parentElement === this._textLayer.current) {
+ e.stopPropagation();
+ }
+ }
+
+ getCurlyTransform = (): { background: string, opacity: string, transform: string } => {
+ // let background = "", opacity = "", transform = "";
+ // if (this._marquee.current && this._curly.current) {
+ // if (this._marqueeWidth > 100 && this._marqueeHeight > 100) {
+ // background = "red";
+ // opacity = "0";
+ // }
+ // else {
+ // background = "transparent";
+ // opacity = "1";
+ // }
+
+ // // split up for simplicity, could be done in a nested ternary. please do not. -syip2
+ // let ratio = this._marqueeWidth / this._marqueeHeight;
+ // if (ratio > 1.5) {
+ // // vertical
+ // transform = "rotate(90deg) scale(1, 5)";
+ // }
+ // else if (ratio < 0.5) {
+ // // horizontal
+ // transform = "scale(2, 1)";
+ // }
+ // else {
+ // // diagonal
+ // transform = "rotate(45deg) scale(1.5, 1.5)";
+ // }
+ // }
+ return { background: "red", opacity: "0.5", transform: "" };
+ }
+
+ @action
+ onSelectEnd = (e: PointerEvent): void => {
+ if (this._marqueeing) {
+ this._marqueeing = false;
+ if (this._marquee.current) {
+ let copy = document.createElement("div");
+ // make a copy of the marquee
+ let style = this._marquee.current.style;
+ copy.style.left = style.left;
+ copy.style.top = style.top;
+ copy.style.width = style.width;
+ copy.style.height = style.height;
+
+ // apply the appropriate background, opacity, and transform
+ let { background, opacity, transform } = this.getCurlyTransform();
+ copy.style.background = background;
+ // if curly bracing, add a curly brace
+ // if (opacity === "1" && this._curly.current) {
+ // copy.style.opacity = opacity;
+ // let img = this._curly.current.cloneNode();
+ // (img as any).style.opacity = opacity;
+ // (img as any).style.transform = transform;
+ // copy.appendChild(img);
+ // }
+ // else {
+ copy.style.border = style.border;
+ copy.style.opacity = style.opacity;
+ // }
+ copy.className = this._marquee.current.className;
+ this.props.createAnnotation(copy, this.props.page);
+ this._marquee.current.style.opacity = "0";
+ }
+
+ if (this._marqueeWidth > 10 || this._marqueeHeight > 10) {
+ if (!e.ctrlKey) {
+ PDFMenu.Instance.Status = "snippet";
+ PDFMenu.Instance.Marquee = { left: this._marqueeX, top: this._marqueeY, width: this._marqueeWidth, height: this._marqueeHeight };
+ }
+ PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
+ }
+
+ this._marqueeHeight = this._marqueeWidth = 0;
+ }
+ else {
+ let sel = window.getSelection();
+ if (sel && sel.type === "Range") {
+ this.createTextAnnotation(sel);
+ PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
+ }
+ }
+
+
+ if (PDFMenu.Instance.Highlighting) {
+ this.highlight(undefined, "goldenrod");
+ }
+ else {
+ PDFMenu.Instance.StartDrag = this.startDrag;
+ PDFMenu.Instance.Highlight = this.highlight;
+ }
+ document.removeEventListener("pointermove", this.onSelectStart);
+ document.removeEventListener("pointerup", this.onSelectEnd);
+ }
+
+ @action
+ createTextAnnotation = (sel: Selection) => {
+ let clientRects = sel.getRangeAt(0).getClientRects();
+ if (this._textLayer.current) {
+ let boundingRect = this._textLayer.current.getBoundingClientRect();
+ for (let i = 0; i < clientRects.length; i++) {
+ let rect = clientRects.item(i);
+ if (rect && rect.width !== this._textLayer.current.getBoundingClientRect().width && rect.height !== this._textLayer.current.getBoundingClientRect().height) {
+ let annoBox = document.createElement("div");
+ annoBox.className = "pdfViewer-annotationBox";
+ // transforms the positions from screen onto the pdf div
+ annoBox.style.top = ((rect.top - boundingRect.top) * (this._textLayer.current.offsetHeight / boundingRect.height)).toString();
+ annoBox.style.left = ((rect.left - boundingRect.left) * (this._textLayer.current.offsetWidth / boundingRect.width)).toString();
+ annoBox.style.width = (rect.width * this._textLayer.current.offsetWidth / boundingRect.width).toString();
+ annoBox.style.height = (rect.height * this._textLayer.current.offsetHeight / boundingRect.height).toString();
+ this.props.createAnnotation(annoBox, this.props.page);
+ }
+ }
+ }
+ // clear selection
+ if (sel.empty) { // Chrome
+ sel.empty();
+ } else if (sel.removeAllRanges) { // Firefox
+ sel.removeAllRanges();
+ }
+ }
+
+ doubleClick = (e: React.MouseEvent) => {
+ let target: any = e.target;
+ // if double clicking text
+ if (target && target.parentElement === this._textLayer.current) {
+ // do something to select the paragraph ideally
+ }
+
+ let current = this._textLayer.current;
+ if (current) {
+ let boundingRect = current.getBoundingClientRect();
+ let x = (e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width);
+ let y = (e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height);
+ this.props.makePin(x, y, this.props.page);
+ }
+ }
+
+ render() {
+ return (
+ <div onPointerDown={this.onPointerDown} onDoubleClick={this.doubleClick} className={"page-cont"} style={{ "width": this._width, "height": this._height }}>
+ <div className="canvasContainer">
+ <canvas ref={this._canvas} />
+ </div>
+ <div className="pdfInkingLayer-cont" ref={this._annotationLayer} style={{ width: "100%", height: "100%", position: "relative", top: "-100%" }}>
+ <div className="pdfViewer-annotationBox" ref={this._marquee}
+ style={{ left: `${this._marqueeX}px`, top: `${this._marqueeY}px`, width: `${this._marqueeWidth}px`, height: `${this._marqueeHeight}px`, background: "red", border: `${this._marqueeWidth === 0 ? "" : "10px dashed black"}` }}>
+ {/* <img ref={this._curly} src="https://static.thenounproject.com/png/331760-200.png" style={{ width: "100%", height: "100%", transform: `${this._rotate}` }} /> */}
+ </div>
+ </div>
+ <div className="textlayer" ref={this._textLayer} style={{ "position": "relative", "top": `-${2 * this._height}px`, "height": `${this._height}px` }} />
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx
new file mode 100644
index 000000000..6896ee452
--- /dev/null
+++ b/src/client/views/presentationview/PresentationElement.tsx
@@ -0,0 +1,402 @@
+import { observer } from "mobx-react";
+import React = require("react");
+import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
+import { NumCast, BoolCast, StrCast, Cast } from "../../../new_fields/Types";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { observable, action, computed, runInAction } from "mobx";
+import "./PresentationView.scss";
+import { Utils } from "../../../Utils";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faFile as fileSolid, faFileDownload, faLocationArrow, faArrowUp, faSearch } from '@fortawesome/free-solid-svg-icons';
+import { faFile as fileRegular } from '@fortawesome/free-regular-svg-icons';
+import { List } from "../../../new_fields/List";
+import { listSpec } from "../../../new_fields/Schema";
+
+library.add(faArrowUp);
+library.add(fileSolid);
+library.add(faLocationArrow);
+library.add(fileRegular as any);
+library.add(faSearch);
+
+interface PresentationElementProps {
+ mainDocument: Doc;
+ document: Doc;
+ index: number;
+ deleteDocument(index: number): void;
+ gotoDocument(index: number, fromDoc: number): Promise<void>;
+ allListElements: Doc[];
+ groupMappings: Map<String, Doc[]>;
+ presStatus: boolean;
+ presButtonBackUp: Doc;
+ presGroupBackUp: Doc;
+
+
+}
+
+//enum for the all kinds of buttons a doc in presentation can have
+export enum buttonIndex {
+ Show = 0,
+ Navigate = 1,
+ HideTillPressed = 2,
+ FadeAfter = 3,
+ HideAfter = 4,
+ Group = 5,
+
+}
+
+/**
+ * This class models the view a document added to presentation will have in the presentation.
+ * It involves some functionality for its buttons and options.
+ */
+@observer
+export default class PresentationElement extends React.Component<PresentationElementProps> {
+
+ @observable private selectedButtons: boolean[];
+
+
+ constructor(props: PresentationElementProps) {
+ super(props);
+ this.selectedButtons = new Array(6);
+ }
+
+ /**
+ * Getter to get the status of the buttons.
+ */
+ @computed
+ get selected() {
+ return this.selectedButtons;
+ }
+
+ //Lifecycle function that makes sure that button BackUp is received when mounted.
+ async componentDidMount() {
+ this.receiveButtonBackUp();
+
+ }
+
+ //Lifecycle function that makes sure button BackUp is received when not re-mounted bu re-rendered.
+ async componentDidUpdate() {
+ this.receiveButtonBackUp();
+ }
+
+ receiveButtonBackUp = async () => {
+
+ //get the list that stores docs that keep track of buttons
+ let castedList = Cast(this.props.presButtonBackUp.selectedButtonDocs, listSpec(Doc));
+ if (!castedList) {
+ this.props.presButtonBackUp.selectedButtonDocs = castedList = new List<Doc>();
+ }
+ //if this is the first time this doc mounts, push a doc for it to store
+ if (castedList.length <= this.props.index) {
+ let newDoc = new Doc();
+ let defaultBooleanArray: boolean[] = new Array(6);
+ newDoc.selectedButtons = new List(defaultBooleanArray);
+ castedList.push(newDoc);
+ //otherwise update the selected buttons depending on storage.
+ } else {
+ let curDoc: Doc = await castedList[this.props.index];
+ let selectedButtonOfDoc = Cast(curDoc.selectedButtons, listSpec("boolean"), null);
+ if (selectedButtonOfDoc !== undefined) {
+ runInAction(() => this.selectedButtons = selectedButtonOfDoc);
+ }
+ }
+
+ }
+
+ /**
+ * The function that is called to group docs together. It tries to group a doc
+ * that turned grouping option with the above document. If that doc is grouped with
+ * other documents. Those other documents will be grouped with doc's above document as well.
+ */
+ @action
+ onGroupClick = (document: Doc, index: number, buttonStatus: boolean) => {
+ let p = this.props;
+ if (index >= 1) {
+ //checking if options was turned true
+ if (buttonStatus) {
+ //getting the id of the above-doc and the doc
+ let aboveGuid = StrCast(p.allListElements[index - 1].presentId, null);
+ let docGuid = StrCast(document.presentId, null);
+ //the case where above-doc is already in group
+ if (p.groupMappings.has(aboveGuid)) {
+ let aboveArray = p.groupMappings.get(aboveGuid)!;
+ //case where doc is already in group
+ if (p.groupMappings.has(docGuid)) {
+ let docsArray = p.groupMappings.get(docGuid)!;
+ docsArray.forEach((doc: Doc) => {
+ if (!aboveArray.includes(doc)) {
+ aboveArray.push(doc);
+ }
+ doc.presentId = aboveGuid;
+ });
+ p.groupMappings.delete(docGuid);
+ //the case where doc was not in group
+ } else {
+ if (!aboveArray.includes(document)) {
+ aboveArray.push(document);
+
+ }
+
+ }
+ //the case where above-doc was not in group
+ } else {
+ let newAboveArray: Doc[] = [];
+ newAboveArray.push(p.allListElements[index - 1]);
+
+ //the case where doc is in group
+ if (p.groupMappings.has(docGuid)) {
+ let docsArray = p.groupMappings.get(docGuid)!;
+ docsArray.forEach((doc: Doc) => {
+ newAboveArray.push(doc);
+ doc.presentId = aboveGuid;
+ });
+ p.groupMappings.delete(docGuid);
+
+ //the case where doc is not in a group
+ } else {
+ newAboveArray.push(document);
+
+ }
+ p.groupMappings.set(aboveGuid, newAboveArray);
+
+ }
+ document.presentId = aboveGuid;
+
+ //when grouping is turned off
+ } else {
+ let curArray = p.groupMappings.get(StrCast(document.presentId, Utils.GenerateGuid()))!;
+ let targetIndex = curArray.indexOf(document);
+ let firstPart = curArray.slice(0, targetIndex);
+ let firstPartNewGuid = Utils.GenerateGuid();
+ firstPart.forEach((doc: Doc) => doc.presentId = firstPartNewGuid);
+ let secondPart = curArray.slice(targetIndex);
+ p.groupMappings.set(StrCast(p.allListElements[index - 1].presentId, Utils.GenerateGuid()), firstPart);
+ p.groupMappings.set(StrCast(document.presentId, Utils.GenerateGuid()), secondPart);
+
+
+ }
+
+ }
+ this.autoSaveGroupChanges();
+
+ }
+
+
+ /**
+ * This function is called at the end of each group update to update the group updates.
+ */
+ @action
+ autoSaveGroupChanges = () => {
+ let castedList: List<Doc> = new List<Doc>();
+ this.props.presGroupBackUp.groupDocs = castedList;
+ this.props.groupMappings.forEach((docArray: Doc[], id: String) => {
+ //create a new doc for each group
+ let newGroupDoc = new Doc();
+ castedList.push(newGroupDoc);
+ //store the id of the group in the doc
+ newGroupDoc.presentIdStore = id.toString();
+ //store the doc array which represents the group in the doc
+ newGroupDoc.grouping = new List(docArray);
+ });
+
+ }
+
+ /**
+ * Function that is called on click to change the group status of a docus, by turning the option on/off.
+ */
+ @action
+ changeGroupStatus = () => {
+ if (this.selectedButtons[buttonIndex.Group]) {
+ this.selectedButtons[buttonIndex.Group] = false;
+ } else {
+ this.selectedButtons[buttonIndex.Group] = true;
+ }
+ this.autoSaveButtonChange(buttonIndex.Group);
+
+ }
+
+ /**
+ * The function that is called on click to turn Hiding document till press option on/off.
+ * It also sets the beginning and end opacitys.
+ */
+ @action
+ onHideDocumentUntilPressClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ const current = NumCast(this.props.mainDocument.selectedDoc);
+ if (this.selectedButtons[buttonIndex.HideTillPressed]) {
+ this.selectedButtons[buttonIndex.HideTillPressed] = false;
+ if (this.props.index >= current) {
+ this.props.document.opacity = 1;
+ }
+ } else {
+ this.selectedButtons[buttonIndex.HideTillPressed] = true;
+ if (this.props.presStatus) {
+ if (this.props.index > current) {
+ this.props.document.opacity = 0;
+ }
+ }
+ }
+ this.autoSaveButtonChange(buttonIndex.HideTillPressed);
+ }
+
+ /**
+ * This function is called to get the updates for the changed buttons.
+ */
+ @action
+ autoSaveButtonChange = async (index: buttonIndex) => {
+ let castedList = (await DocListCastAsync(this.props.presButtonBackUp.selectedButtonDocs))!;
+ castedList[this.props.index].selectedButtons = new List(this.selectedButtons);
+
+ }
+
+ /**
+ * The function that is called on click to turn Hiding document after presented option on/off.
+ * It also makes sure that the option swithches from fade-after to this one, since both
+ * can't coexist.
+ */
+ @action
+ onHideDocumentAfterPresentedClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ const current = NumCast(this.props.mainDocument.selectedDoc);
+ if (this.selectedButtons[buttonIndex.HideAfter]) {
+ this.selectedButtons[buttonIndex.HideAfter] = false;
+ if (this.props.index <= current) {
+ this.props.document.opacity = 1;
+ }
+ } else {
+ if (this.selectedButtons[buttonIndex.FadeAfter]) {
+ this.selectedButtons[buttonIndex.FadeAfter] = false;
+ }
+ this.selectedButtons[buttonIndex.HideAfter] = true;
+ if (this.props.presStatus) {
+ if (this.props.index < current) {
+ this.props.document.opacity = 0;
+ }
+ }
+ }
+ this.autoSaveButtonChange(buttonIndex.HideAfter);
+
+ }
+
+ /**
+ * 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 current = NumCast(this.props.mainDocument.selectedDoc);
+ if (this.selectedButtons[buttonIndex.FadeAfter]) {
+ this.selectedButtons[buttonIndex.FadeAfter] = false;
+ if (this.props.index <= current) {
+ this.props.document.opacity = 1;
+ }
+ } else {
+ if (this.selectedButtons[buttonIndex.HideAfter]) {
+ this.selectedButtons[buttonIndex.HideAfter] = false;
+ }
+ this.selectedButtons[buttonIndex.FadeAfter] = true;
+ if (this.props.presStatus) {
+ if (this.props.index < current) {
+ this.props.document.opacity = 0.5;
+ }
+ }
+ }
+ this.autoSaveButtonChange(buttonIndex.FadeAfter);
+
+ }
+
+ /**
+ * The function that is called on click to turn navigation option of docs on/off.
+ */
+ @action
+ onNavigateDocumentClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ if (this.selectedButtons[buttonIndex.Navigate]) {
+ this.selectedButtons[buttonIndex.Navigate] = false;
+
+ } else {
+ if (this.selectedButtons[buttonIndex.Show]) {
+ this.selectedButtons[buttonIndex.Show] = false;
+ }
+ this.selectedButtons[buttonIndex.Navigate] = true;
+ const current = NumCast(this.props.mainDocument.selectedDoc);
+ if (current === this.props.index) {
+ this.props.gotoDocument(this.props.index, this.props.index);
+ }
+ }
+
+ this.autoSaveButtonChange(buttonIndex.Navigate);
+
+ }
+
+ /**
+ * The function that is called on click to turn zoom option of docs on/off.
+ */
+ @action
+ onZoomDocumentClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ if (this.selectedButtons[buttonIndex.Show]) {
+ this.selectedButtons[buttonIndex.Show] = false;
+ this.props.document.viewScale = 1;
+
+ } else {
+ if (this.selectedButtons[buttonIndex.Navigate]) {
+ this.selectedButtons[buttonIndex.Navigate] = false;
+ }
+ this.selectedButtons[buttonIndex.Show] = true;
+ const current = NumCast(this.props.mainDocument.selectedDoc);
+ if (current === this.props.index) {
+ this.props.gotoDocument(this.props.index, this.props.index);
+ }
+ }
+
+ this.autoSaveButtonChange(buttonIndex.Show);
+
+ }
+
+
+ render() {
+ let p = this.props;
+ let title = p.document.title;
+
+ //to get currently selected presentation doc
+ let selected = NumCast(p.mainDocument.selectedDoc, 0);
+
+ let className = "presentationView-item";
+ if (selected === p.index) {
+ //this doc is selected
+ className += " presentationView-selected";
+ }
+ let onEnter = (e: React.PointerEvent) => { p.document.libraryBrush = true; };
+ let onLeave = (e: React.PointerEvent) => { p.document.libraryBrush = false; };
+ return (
+ <div className={className} key={p.document[Id] + p.index}
+ onPointerEnter={onEnter} onPointerLeave={onLeave}
+ style={{
+ outlineColor: "maroon",
+ outlineStyle: "dashed",
+ outlineWidth: BoolCast(p.document.libraryBrush, false) ? `1px` : "0px",
+ }}
+ onClick={e => { p.gotoDocument(p.index, NumCast(this.props.mainDocument.selectedDoc)); e.stopPropagation(); }}>
+ <strong className="presentationView-name">
+ {`${p.index + 1}. ${title}`}
+ </strong>
+ <button className="presentation-icon" onClick={e => { this.props.deleteDocument(p.index); e.stopPropagation(); }}>X</button>
+ <br></br>
+ <button title="Zoom" className={this.selectedButtons[buttonIndex.Show] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
+ <button title="Navigate" className={this.selectedButtons[buttonIndex.Navigate] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
+ <button title="Hide Document Till Presented" className={this.selectedButtons[buttonIndex.HideTillPressed] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
+ <button title="Fade Document After Presented" className={this.selectedButtons[buttonIndex.FadeAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} color={"gray"} /></button>
+ <button title="Hide Document After Presented" className={this.selectedButtons[buttonIndex.HideAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
+ <button title="Group With Up" className={this.selectedButtons[buttonIndex.Group] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={(e) => {
+ e.stopPropagation();
+ this.changeGroupStatus();
+ this.onGroupClick(p.document, p.index, this.selectedButtons[buttonIndex.Group]);
+ }}> <FontAwesomeIcon icon={"arrow-up"} /> </button>
+
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/presentationview/PresentationList.tsx b/src/client/views/presentationview/PresentationList.tsx
new file mode 100644
index 000000000..7abd3e366
--- /dev/null
+++ b/src/client/views/presentationview/PresentationList.tsx
@@ -0,0 +1,104 @@
+import { observer } from "mobx-react";
+import React = require("react");
+import { action } from "mobx";
+import "./PresentationView.scss";
+import { Utils } from "../../../Utils";
+import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
+import { NumCast, StrCast } from "../../../new_fields/Types";
+import { Id } from "../../../new_fields/FieldSymbols";
+import PresentationElement, { buttonIndex } from "./PresentationElement";
+
+
+
+
+interface PresListProps {
+ mainDocument: Doc;
+ deleteDocument(index: number): void;
+ gotoDocument(index: number, fromDoc: number): Promise<void>;
+ groupMappings: Map<String, Doc[]>;
+ presElementsMappings: Map<Doc, PresentationElement>;
+ setChildrenDocs: (docList: Doc[]) => void;
+ presStatus: boolean;
+ presButtonBackUp: Doc;
+ presGroupBackUp: Doc;
+}
+
+
+@observer
+/**
+ * Component that takes in a document prop and a boolean whether it's collapsed or not.
+ */
+export default class PresentationViewList extends React.Component<PresListProps> {
+
+ /**
+ * Method that initializes presentation ids for the
+ * docs that is in the presentation, when presentation list
+ * gets re-rendered. It makes sure to not assign ids to the
+ * docs that are in the group, so that mapping won't be disrupted.
+ */
+
+ @action
+ initializeGroupIds = async (docList: Doc[]) => {
+ docList.forEach(async (doc: Doc, index: number) => {
+ let docGuid = StrCast(doc.presentId, null);
+ //checking if part of group
+ let storedGuids: string[] = [];
+ let castedGroupDocs = await DocListCastAsync(this.props.presGroupBackUp.groupDocs);
+ //making sure the docs that were in groups, which were stored, to not get new guids.
+ if (castedGroupDocs !== undefined) {
+ castedGroupDocs.forEach((doc: Doc) => {
+ let storedGuid = StrCast(doc.presentIdStore, null);
+ if (storedGuid) {
+ storedGuids.push(storedGuid);
+ }
+
+ });
+ }
+ if (!this.props.groupMappings.has(docGuid) && !storedGuids.includes(docGuid)) {
+ doc.presentId = Utils.GenerateGuid();
+ }
+ });
+ }
+
+ /**
+ * Initially every document starts with a viewScale 1, which means
+ * that they will be displayed in a canvas with scale 1.
+ */
+ @action
+ initializeScaleViews = (docList: Doc[]) => {
+ docList.forEach((doc: Doc) => {
+ let curScale = NumCast(doc.viewScale, null);
+ if (curScale === undefined) {
+ doc.viewScale = 1;
+ }
+ });
+ }
+
+ render() {
+ const children = DocListCast(this.props.mainDocument.data);
+ this.initializeGroupIds(children);
+ this.initializeScaleViews(children);
+ this.props.setChildrenDocs(children);
+ return (
+
+ <div className="presentationView-listCont">
+ {children.map((doc: Doc, index: number) =>
+ <PresentationElement
+ ref={(e) => { if (e) { this.props.presElementsMappings.set(doc, e); } }}
+ key={doc[Id]}
+ mainDocument={this.props.mainDocument}
+ document={doc}
+ index={index}
+ deleteDocument={this.props.deleteDocument}
+ gotoDocument={this.props.gotoDocument}
+ groupMappings={this.props.groupMappings}
+ allListElements={children}
+ presStatus={this.props.presStatus}
+ presButtonBackUp={this.props.presButtonBackUp}
+ presGroupBackUp={this.props.presGroupBackUp}
+ />
+ )}
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/PresentationView.scss b/src/client/views/presentationview/PresentationView.scss
index fb4a851c4..a35a5849b 100644
--- a/src/client/views/PresentationView.scss
+++ b/src/client/views/presentationview/PresentationView.scss
@@ -47,20 +47,30 @@
padding-bottom: 3px;
font-size: 25px;
display: inline-block;
+ width: calc(100% - 160px);
}
.presentation-icon {
float: right;
}
+.presentation-interaction {
+ float: left;
+}
+
+.presentation-interaction-selected {
+ background: #505050;
+ float: left;
+}
+
.presentationView-name {
font-size: 15px;
display: inline-block;
}
.presentation-button {
- margin-right: 12.5%;
- margin-left: 12.5%;
+ margin-right: 2.5%;
+ margin-left: 2.5%;
width: 25%;
}
diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx
new file mode 100644
index 000000000..edbbeb8f9
--- /dev/null
+++ b/src/client/views/presentationview/PresentationView.tsx
@@ -0,0 +1,794 @@
+import { observer } from "mobx-react";
+import React = require("react");
+import { observable, action, runInAction, reaction } from "mobx";
+import "./PresentationView.scss";
+import { DocumentManager } from "../../util/DocumentManager";
+import { Utils } from "../../../Utils";
+import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
+import { listSpec } from "../../../new_fields/Schema";
+import { Cast, NumCast, FieldValue, PromiseValue, StrCast, BoolCast } from "../../../new_fields/Types";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { List } from "../../../new_fields/List";
+import PresentationElement, { buttonIndex } from "./PresentationElement";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faArrowRight, faArrowLeft, faPlay, faStop, faPlus, faTimes, faMinus, faEdit } from '@fortawesome/free-solid-svg-icons';
+import { Docs } from "../../documents/Documents";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
+import PresentationViewList from "./PresentationList";
+
+library.add(faArrowLeft);
+library.add(faArrowRight);
+library.add(faPlay);
+library.add(faStop);
+library.add(faPlus);
+library.add(faTimes);
+library.add(faMinus);
+library.add(faEdit);
+
+
+export interface PresViewProps {
+ Documents: List<Doc>;
+}
+
+@observer
+export class PresentationView extends React.Component<PresViewProps> {
+ public static Instance: PresentationView;
+
+ //Mapping from presentation ids to a list of doc that represent a group
+ @observable groupMappings: Map<String, Doc[]> = new Map();
+ //mapping from docs to their rendered component
+ @observable presElementsMappings: Map<Doc, PresentationElement> = new Map();
+ //variable that holds all the docs in the presentation
+ @observable childrenDocs: Doc[] = [];
+ //variable to hold if presentation is started
+ @observable presStatus: boolean = false;
+ //back-up so that presentation stays the way it's when refreshed
+ @observable presGroupBackUp: Doc = new Doc();
+ @observable presButtonBackUp: Doc = new Doc();
+
+ //Keeping track of the doc for the current presentation
+ @observable curPresentation: Doc = new Doc();
+ //Mapping of guids to presentations.
+ @observable presentationsMapping: Map<String, Doc> = new Map();
+ //Mapping of presentations to guid, so that select option values can be given.
+ @observable presentationsKeyMapping: Map<Doc, String> = new Map();
+ //Variable to keep track of guid of the current presentation
+ @observable currentSelectedPresValue: string | undefined;
+ //A flag to keep track if title input is open, which is used in rendering.
+ @observable PresTitleInputOpen: boolean = false;
+ //Variable that holds reference to title input, so that new presentations get titles assigned.
+ @observable titleInputElement: HTMLInputElement | undefined;
+ @observable PresTitleChangeOpen: boolean = false;
+
+ //initilize class variables
+ constructor(props: PresViewProps) {
+ super(props);
+ PresentationView.Instance = this;
+ }
+
+ //The first lifecycle function that gets called to set up the current presentation.
+ async componentWillMount() {
+ this.props.Documents.forEach(async (doc, index: number) => {
+
+ //For each presentation received from mainContainer, a mapping is created.
+ let curDoc: Doc = await doc;
+ let newGuid = Utils.GenerateGuid();
+ this.presentationsKeyMapping.set(curDoc, newGuid);
+ this.presentationsMapping.set(newGuid, curDoc);
+
+ //The Presentation at first index gets set as default start presentation
+ if (index === 0) {
+ runInAction(() => this.currentSelectedPresValue = newGuid);
+ runInAction(() => this.curPresentation = curDoc);
+ }
+ });
+ }
+
+ //Second lifecycle function that gets called when component mounts. It makes sure to
+ //get the back-up information from previous session for the current presentation.
+ async componentDidMount() {
+ let docAtZero = await this.props.Documents[0];
+ runInAction(() => this.curPresentation = docAtZero);
+
+ this.setPresentationBackUps();
+
+ }
+
+
+ /**
+ * The function that retrieves the backUps for the current Presentation if present,
+ * otherwise initializes.
+ */
+ setPresentationBackUps = async () => {
+ //getting both backUp documents
+
+ let castedGroupBackUp = Cast(this.curPresentation.presGroupBackUp, Doc);
+ let castedButtonBackUp = Cast(this.curPresentation.presButtonBackUp, Doc);
+ //if instantiated before
+ if (castedGroupBackUp instanceof Promise) {
+ castedGroupBackUp.then(doc => {
+ let toAssign = doc ? doc : new Doc();
+ this.curPresentation.presGroupBackUp = toAssign;
+ runInAction(() => this.presGroupBackUp = toAssign);
+ if (doc) {
+ if (toAssign[Id] === doc[Id]) {
+ this.retrieveGroupMappings();
+ }
+ }
+ });
+
+ //if never instantiated a store doc yet
+ } else if (castedGroupBackUp instanceof Doc) {
+ let castedDoc: Doc = await castedGroupBackUp;
+ runInAction(() => this.presGroupBackUp = castedDoc);
+ this.retrieveGroupMappings();
+ } else {
+ runInAction(() => {
+ let toAssign = new Doc();
+ this.presGroupBackUp = toAssign;
+ this.curPresentation.presGroupBackUp = toAssign;
+
+ });
+
+ }
+ //if instantiated before
+ if (castedButtonBackUp instanceof Promise) {
+ castedButtonBackUp.then(doc => {
+ let toAssign = doc ? doc : new Doc();
+ this.curPresentation.presButtonBackUp = toAssign;
+ runInAction(() => this.presButtonBackUp = toAssign);
+ });
+
+ //if never instantiated a store doc yet
+ } else if (castedButtonBackUp instanceof Doc) {
+ let castedDoc: Doc = await castedButtonBackUp;
+ runInAction(() => this.presButtonBackUp = castedDoc);
+
+ } else {
+ runInAction(() => {
+ let toAssign = new Doc();
+ this.presButtonBackUp = toAssign;
+ this.curPresentation.presButtonBackUp = toAssign;
+ });
+
+ }
+
+
+ //storing the presentation status,ie. whether it was stopped or playing
+ let presStatusBackUp = BoolCast(this.curPresentation.presStatus, null);
+ runInAction(() => this.presStatus = presStatusBackUp);
+ }
+
+ /**
+ * This is the function that is called to retrieve the groups that have been stored and
+ * push them to the groupMappings.
+ */
+ retrieveGroupMappings = async () => {
+ let castedGroupDocs = await DocListCastAsync(this.presGroupBackUp.groupDocs);
+ if (castedGroupDocs !== undefined) {
+ castedGroupDocs.forEach(async (groupDoc: Doc, index: number) => {
+ let castedGrouping = await DocListCastAsync(groupDoc.grouping);
+ let castedKey = StrCast(groupDoc.presentIdStore, null);
+ if (castedGrouping) {
+ castedGrouping.forEach((doc: Doc) => {
+ doc.presentId = castedKey;
+ });
+ }
+ if (castedGrouping !== undefined && castedKey !== undefined) {
+ this.groupMappings.set(castedKey, castedGrouping);
+ }
+ });
+ }
+ }
+
+ //observable means render is re-called every time variable is changed
+ @observable
+ collapsed: boolean = false;
+ closePresentation = action(() => this.curPresentation.width = 0);
+ next = async () => {
+ const current = NumCast(this.curPresentation.selectedDoc);
+ //asking to get document at current index
+ let docAtCurrentNext = await this.getDocAtIndex(current + 1);
+ if (docAtCurrentNext === undefined) {
+ return;
+ }
+ //asking for it's presentation id
+ let curNextPresId = StrCast(docAtCurrentNext.presentId);
+ let nextSelected = current + 1;
+
+ //if curDoc is in a group, selection slides until last one, if not it's next one
+ if (this.groupMappings.has(curNextPresId)) {
+ let currentsArray = this.groupMappings.get(StrCast(docAtCurrentNext.presentId))!;
+ nextSelected = current + currentsArray.length - currentsArray.indexOf(docAtCurrentNext);
+
+ //end of grup so go beyond
+ if (nextSelected === current) nextSelected = current + 1;
+ }
+
+ this.gotoDocument(nextSelected, current);
+
+ }
+ back = async () => {
+ const current = NumCast(this.curPresentation.selectedDoc);
+ //requesting for the doc at current index
+ let docAtCurrent = await this.getDocAtIndex(current);
+ if (docAtCurrent === undefined) {
+ return;
+ }
+
+ //asking for its presentation id.
+ let curPresId = StrCast(docAtCurrent.presentId);
+ let prevSelected = current - 1;
+ let zoomOut: boolean = false;
+
+ //checking if this presentation id is mapped to a group, if so chosing the first element in group
+ if (this.groupMappings.has(curPresId)) {
+ let currentsArray = this.groupMappings.get(StrCast(docAtCurrent.presentId))!;
+ prevSelected = current - currentsArray.length + (currentsArray.length - currentsArray.indexOf(docAtCurrent)) - 1;
+ //end of grup so go beyond
+ if (prevSelected === current) prevSelected = current - 1;
+
+ //checking if any of the group members had used zooming in
+ currentsArray.forEach((doc: Doc) => {
+ if (this.presElementsMappings.get(doc)!.selected[buttonIndex.Show]) {
+ zoomOut = true;
+ return;
+ }
+ });
+
+ }
+
+ // if a group set that flag to zero or a single element
+ //If so making sure to zoom out, which goes back to state before zooming action
+ if (current > 0) {
+ if (zoomOut || this.presElementsMappings.get(docAtCurrent)!.selected[buttonIndex.Show]) {
+ let prevScale = NumCast(this.childrenDocs[prevSelected].viewScale, null);
+ let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[current]);
+ if (prevScale !== undefined) {
+ if (prevScale !== curScale) {
+ DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale);
+ }
+ }
+ }
+ }
+ this.gotoDocument(prevSelected, current);
+
+ }
+
+ /**
+ * 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.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => {
+ let selectedButtons: boolean[] = presElem.selected;
+ //the order of cases is aligned based on priority
+ if (selectedButtons[buttonIndex.HideTillPressed]) {
+ if (this.childrenDocs.indexOf(key) <= index) {
+ key.opacity = 1;
+ }
+ }
+ if (selectedButtons[buttonIndex.HideAfter]) {
+ if (this.childrenDocs.indexOf(key) < index) {
+ key.opacity = 0;
+ }
+ }
+ if (selectedButtons[buttonIndex.FadeAfter]) {
+ if (this.childrenDocs.indexOf(key) < index) {
+ key.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) => {
+ this.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => {
+ let selectedButtons: boolean[] = presElem.selected;
+
+ //the order of cases is aligned based on priority
+
+ if (selectedButtons[buttonIndex.HideAfter]) {
+ if (this.childrenDocs.indexOf(key) >= index) {
+ key.opacity = 1;
+ }
+ }
+ if (selectedButtons[buttonIndex.FadeAfter]) {
+ if (this.childrenDocs.indexOf(key) >= index) {
+ key.opacity = 1;
+ }
+ }
+ if (selectedButtons[buttonIndex.HideTillPressed]) {
+ if (this.childrenDocs.indexOf(key) > index) {
+ key.opacity = 0;
+ }
+ }
+ });
+ }
+
+ /**
+ * 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.
+ */
+ navigateToElement = async (curDoc: Doc, fromDoc: number) => {
+ let docToJump: Doc = curDoc;
+ let curDocPresId = StrCast(curDoc.presentId, null);
+ let willZoom: boolean = false;
+
+ //checking if in group
+ if (curDocPresId !== undefined) {
+ if (this.groupMappings.has(curDocPresId)) {
+ let currentDocGroup = this.groupMappings.get(curDocPresId)!;
+ currentDocGroup.forEach((doc: Doc, index: number) => {
+ let selectedButtons: boolean[] = this.presElementsMappings.get(doc)!.selected;
+ if (selectedButtons[buttonIndex.Navigate]) {
+ docToJump = doc;
+ willZoom = false;
+ }
+ if (selectedButtons[buttonIndex.Show]) {
+ docToJump = doc;
+ willZoom = true;
+ }
+ });
+ }
+
+ }
+ //docToJump stayed same meaning, it was not in the group or was the last element in the group
+ if (docToJump === curDoc) {
+ //checking if curDoc has navigation open
+ let curDocButtons = this.presElementsMappings.get(curDoc)!.selected;
+ if (curDocButtons[buttonIndex.Navigate]) {
+ DocumentManager.Instance.jumpToDocument(curDoc, false);
+ } else if (curDocButtons[buttonIndex.Show]) {
+ let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]);
+ //awaiting jump so that new scale can be found, since jumping is async
+ await DocumentManager.Instance.jumpToDocument(curDoc, true);
+ let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc);
+ curDoc.viewScale = newScale;
+
+ //saving the scale user was on before zooming in
+ if (curScale !== 1) {
+ this.childrenDocs[fromDoc].viewScale = curScale;
+ }
+
+ }
+ return;
+ }
+ let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]);
+
+ //awaiting jump so that new scale can be found, since jumping is async
+ await DocumentManager.Instance.jumpToDocument(docToJump, willZoom);
+ let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc);
+ curDoc.viewScale = newScale;
+ //saving the scale that user was on
+ if (curScale !== 1) {
+ this.childrenDocs[fromDoc].viewScale = curScale;
+ }
+
+ }
+
+ /**
+ * Async function that supposedly return the doc that is located at given index.
+ */
+ getDocAtIndex = async (index: number) => {
+ const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+ if (!list) {
+ return undefined;
+ }
+ if (index < 0 || index >= list.length) {
+ return undefined;
+ }
+
+ this.curPresentation.selectedDoc = index;
+ //awaiting async call to finish to get Doc instance
+ const doc = await list[index];
+ return doc;
+ }
+
+ /**
+ * The function that removes a doc from a presentation. It also makes sure to
+ * do necessary updates to backUps and mappings stored locally.
+ */
+ @action
+ public RemoveDoc = async (index: number) => {
+ const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+ if (value) {
+ let removedDoc = await value.splice(index, 1)[0];
+
+ //removing the Presentation Element stored for it
+ this.presElementsMappings.delete(removedDoc);
+
+ let removedDocPresentId = StrCast(removedDoc.presentId);
+
+ //Removing it from local mapping of the groups
+ if (this.groupMappings.has(removedDocPresentId)) {
+ let removedDocsGroup = this.groupMappings.get(removedDocPresentId);
+ if (removedDocsGroup) {
+ removedDocsGroup.splice(removedDocsGroup.indexOf(removedDoc), 1);
+ if (removedDocsGroup.length === 0) {
+ this.groupMappings.delete(removedDocPresentId);
+ }
+ }
+ }
+
+ //removing it from the backUp of selected Buttons
+ let castedList = Cast(this.presButtonBackUp.selectedButtonDocs, listSpec(Doc));
+ if (castedList) {
+ castedList.splice(index, 1);
+ }
+
+ //removing it from the backup of groups
+ let castedGroupDocs = await DocListCastAsync(this.presGroupBackUp.groupDocs);
+ if (castedGroupDocs) {
+ castedGroupDocs.forEach(async (groupDoc: Doc, index: number) => {
+ let castedKey = StrCast(groupDoc.presentIdStore, null);
+ if (castedKey === removedDocPresentId) {
+ let castedGrouping = await DocListCastAsync(groupDoc.grouping);
+ if (castedGrouping) {
+ castedGrouping.splice(castedGrouping.indexOf(removedDoc), 1);
+ if (castedGrouping.length === 0) {
+ castedGroupDocs!.splice(castedGroupDocs!.indexOf(groupDoc), 1);
+ }
+ }
+ }
+
+ });
+
+ }
+
+
+ }
+ }
+
+ //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.
+ @action
+ public gotoDocument = async (index: number, fromDoc: number) => {
+ const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+ if (!list) {
+ return;
+ }
+ if (index < 0 || index >= list.length) {
+ return;
+ }
+ this.curPresentation.selectedDoc = index;
+
+ if (!this.presStatus) {
+ this.presStatus = true;
+ this.startPresentation(index);
+ }
+
+ const doc = await list[index];
+ if (this.presStatus) {
+ this.navigateToElement(doc, fromDoc);
+ this.hideIfNotPresented(index);
+ this.showAfterPresented(index);
+ }
+
+ }
+
+ //Function that is called to resetGroupIds, so that documents get new groupIds at
+ //first load, when presentation is changed.
+ resetGroupIds = async () => {
+ let castedGroupDocs = await DocListCastAsync(this.presGroupBackUp.groupDocs);
+ if (castedGroupDocs !== undefined) {
+ castedGroupDocs.forEach(async (groupDoc: Doc, index: number) => {
+ let castedGrouping = await DocListCastAsync(groupDoc.grouping);
+ if (castedGrouping) {
+ castedGrouping.forEach((doc: Doc) => {
+ doc.presentId = Utils.GenerateGuid();
+ });
+ }
+ });
+ }
+ runInAction(() => this.groupMappings = new Map());
+ }
+
+ /**
+ * Adds a document to the presentation view
+ **/
+ @undoBatch
+ @action
+ public PinDoc(doc: Doc) {
+ //add this new doc to props.Document
+ const data = Cast(this.curPresentation.data, listSpec(Doc));
+ if (data) {
+ data.push(doc);
+ } else {
+ this.curPresentation.data = new List([doc]);
+ }
+
+ this.curPresentation.width = 400;
+ }
+
+ //Function that sets the store of the children docs.
+ @action
+ setChildrenDocs = (docList: Doc[]) => {
+ this.childrenDocs = docList;
+ }
+
+ //The function that is called to render the play or pause button depending on
+ //if presentation is running or not.
+ renderPlayPauseButton = () => {
+ if (this.presStatus) {
+ return <button title="Reset Presentation" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="stop" /></button>;
+ } else {
+ return <button title="Start Presentation From Start" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="play" /></button>;
+ }
+ }
+
+ //The function that starts or resets presentaton functionally, depending on status flag.
+ @action
+ startOrResetPres = () => {
+ if (this.presStatus) {
+ this.resetPresentation();
+ } else {
+ this.presStatus = true;
+ this.startPresentation(0);
+ const current = NumCast(this.curPresentation.selectedDoc);
+ this.gotoDocument(0, current);
+ }
+ this.curPresentation.presStatus = this.presStatus;
+ }
+
+ //The function that resets the presentation by removing every action done by it. It also
+ //stops the presentaton.
+ @action
+ resetPresentation = () => {
+ this.childrenDocs.forEach((doc: Doc) => {
+ doc.opacity = 1;
+ doc.viewScale = 1;
+ });
+ this.curPresentation.selectedDoc = 0;
+ this.presStatus = false;
+ this.curPresentation.presStatus = this.presStatus;
+ if (this.childrenDocs.length === 0) {
+ return;
+ }
+ DocumentManager.Instance.zoomIntoScale(this.childrenDocs[0], 1);
+ }
+
+
+ //The function that starts the presentation, also checking if actions should be applied
+ //directly at start.
+ startPresentation = (startIndex: number) => {
+ let selectedButtons: boolean[];
+ this.presElementsMappings.forEach((component: PresentationElement, doc: Doc) => {
+ selectedButtons = component.selected;
+ if (selectedButtons[buttonIndex.HideTillPressed]) {
+ if (this.childrenDocs.indexOf(doc) > startIndex) {
+ doc.opacity = 0;
+ }
+
+ }
+ if (selectedButtons[buttonIndex.HideAfter]) {
+ if (this.childrenDocs.indexOf(doc) < startIndex) {
+ doc.opacity = 0;
+ }
+ }
+ if (selectedButtons[buttonIndex.FadeAfter]) {
+ if (this.childrenDocs.indexOf(doc) < startIndex) {
+ doc.opacity = 0.5;
+ }
+ }
+
+ });
+
+ }
+
+ /**
+ * The function that is called to add a new presentation to the presentationView.
+ * It sets up te mappings and local copies of it. Resets the groupings and presentation.
+ * Makes the new presentation current selected, and retrieve the back-Ups if present.
+ */
+ @action
+ addNewPresentation = (presTitle: string) => {
+ //creating a new presentation doc
+ let newPresentationDoc = Docs.Create.TreeDocument([], { title: presTitle });
+ this.props.Documents.push(newPresentationDoc);
+
+ //setting that new doc as current
+ this.curPresentation = newPresentationDoc;
+
+ //storing the doc in local copies for easier access
+ let newGuid = Utils.GenerateGuid();
+ this.presentationsMapping.set(newGuid, newPresentationDoc);
+ this.presentationsKeyMapping.set(newPresentationDoc, newGuid);
+
+ //resetting the previous presentation's actions so that new presentation can be loaded.
+ this.resetGroupIds();
+ this.resetPresentation();
+ this.presElementsMappings = new Map();
+ this.currentSelectedPresValue = newGuid;
+ this.setPresentationBackUps();
+
+ }
+
+ /**
+ * The function that is called to change the current selected presentation.
+ * Changes the presentation, also resetting groupings and presentation in process.
+ * Plus retrieving the backUps for the newly selected presentation.
+ */
+ @action
+ getSelectedPresentation = (e: React.ChangeEvent<HTMLSelectElement>) => {
+ //get the guid of the selected presentation
+ let selectedGuid = e.target.value;
+ //set that as current presentation
+ this.curPresentation = this.presentationsMapping.get(selectedGuid)!;
+
+ //reset current Presentations local things so that new one can be loaded
+ this.resetGroupIds();
+ this.resetPresentation();
+ this.presElementsMappings = new Map();
+ this.currentSelectedPresValue = selectedGuid;
+ this.setPresentationBackUps();
+
+
+ }
+
+ /**
+ * The function that is called to render either select for presentations, or title inputting.
+ */
+ renderSelectOrPresSelection = () => {
+ let presentationList = DocListCast(this.props.Documents);
+ if (this.PresTitleInputOpen || this.PresTitleChangeOpen) {
+ return <input ref={(e) => this.titleInputElement = e!} type="text" className="presentationView-title" placeholder="Enter Name!" onKeyDown={this.submitPresentationTitle} />;
+ } else {
+ return <select value={this.currentSelectedPresValue} id="pres_selector" className="presentationView-title" onChange={this.getSelectedPresentation}>
+ {presentationList.map((doc: Doc, index: number) => {
+ let mappedGuid = this.presentationsKeyMapping.get(doc);
+ let docGuid: string = mappedGuid ? mappedGuid.toString() : "";
+ return <option key={docGuid} value={docGuid}>{StrCast(doc.title)}</option>;
+ })}
+ </select>;
+ }
+ }
+
+ /**
+ * The function that is called on enter press of title input. It gives the
+ * new presentation the title user entered. If nothing is entered, gives a default title.
+ */
+ @action
+ submitPresentationTitle = (e: React.KeyboardEvent) => {
+ if (e.keyCode === 13) {
+ let presTitle = this.titleInputElement!.value;
+ this.titleInputElement!.value = "";
+ if (this.PresTitleInputOpen) {
+ if (presTitle === "") {
+ presTitle = "Presentation";
+ }
+ this.PresTitleInputOpen = false;
+ this.addNewPresentation(presTitle);
+ } else if (this.PresTitleChangeOpen) {
+ this.PresTitleChangeOpen = false;
+ this.changePresentationTitle(presTitle);
+ }
+ }
+ }
+
+ /**
+ * The function that is called to remove a presentation from all its copies, and the main Container's
+ * list. Sets up the next presentation as current.
+ */
+ @action
+ removePresentation = async () => {
+ if (this.presentationsMapping.size !== 1) {
+ let presentationList = Cast(this.props.Documents, listSpec(Doc));
+ let batch = UndoManager.StartBatch("presRemoval");
+
+ //getting the presentation that will be removed
+ let removedDoc = this.presentationsMapping.get(this.currentSelectedPresValue!);
+ //that presentation is removed
+ presentationList!.splice(presentationList!.indexOf(removedDoc!), 1);
+
+ //its mappings are removed from local copies
+ this.presentationsKeyMapping.delete(removedDoc!);
+ this.presentationsMapping.delete(this.currentSelectedPresValue!);
+
+ //the next presentation is set as current
+ let remainingPresentations = this.presentationsMapping.values();
+ let nextDoc = remainingPresentations.next().value;
+ this.curPresentation = nextDoc;
+
+
+ //Storing these for being able to undo changes
+ let curGuid = this.currentSelectedPresValue!;
+ let curPresStatus = this.presStatus;
+
+ //resetting the groups and presentation actions so that next presentation gets loaded
+ this.resetGroupIds();
+ this.resetPresentation();
+ this.currentSelectedPresValue = this.presentationsKeyMapping.get(nextDoc)!.toString();
+ this.setPresentationBackUps();
+
+ //Storing for undo
+ let currentGroups = this.groupMappings;
+ let curPresElemMapping = this.presElementsMappings;
+
+ //Event to undo actions that are not related to doc directly, aka. local things
+ UndoManager.AddEvent({
+ undo: action(() => {
+ this.curPresentation = removedDoc!;
+ this.presentationsMapping.set(curGuid, removedDoc!);
+ this.presentationsKeyMapping.set(removedDoc!, curGuid);
+ this.currentSelectedPresValue = curGuid;
+
+ this.presStatus = curPresStatus;
+ this.groupMappings = currentGroups;
+ this.presElementsMappings = curPresElemMapping;
+ this.setPresentationBackUps();
+
+ }),
+ redo: action(() => {
+ this.curPresentation = nextDoc;
+ this.presStatus = false;
+ this.presentationsKeyMapping.delete(removedDoc!);
+ this.presentationsMapping.delete(curGuid);
+ this.currentSelectedPresValue = this.presentationsKeyMapping.get(nextDoc)!.toString();
+ this.setPresentationBackUps();
+
+ }),
+ });
+
+ batch.end();
+ }
+ }
+
+ /**
+ * The function that is called to change title of presentation to what user entered.
+ */
+ @undoBatch
+ changePresentationTitle = (newTitle: string) => {
+ if (newTitle === "") {
+ return;
+ }
+ this.curPresentation.title = newTitle;
+ }
+
+
+ render() {
+
+ let width = NumCast(this.curPresentation.width);
+
+ return (
+ <div className="presentationView-cont" style={{ width: width, overflow: "hidden" }}>
+ <div className="presentationView-heading">
+ {this.renderSelectOrPresSelection()}
+ <button title="Close Presentation" className='presentation-icon' onClick={this.closePresentation}><FontAwesomeIcon icon={"times"} /></button>
+ <button title="Add Presentation" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => {
+ runInAction(() => { if (this.PresTitleChangeOpen) { this.PresTitleChangeOpen = false; } });
+ runInAction(() => this.PresTitleInputOpen ? this.PresTitleInputOpen = false : this.PresTitleInputOpen = true);
+ }}><FontAwesomeIcon icon={"plus"} /></button>
+ <button title="Remove Presentation" className='presentation-icon' style={{ marginRight: 10 }} onClick={this.removePresentation}><FontAwesomeIcon icon={"minus"} /></button>
+ <button title="Change Presentation Title" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => {
+ runInAction(() => { if (this.PresTitleInputOpen) { this.PresTitleInputOpen = false; } });
+ runInAction(() => this.PresTitleChangeOpen ? this.PresTitleChangeOpen = false : this.PresTitleChangeOpen = true);
+ }}><FontAwesomeIcon icon={"edit"} /></button>
+ </div>
+ <div className="presentation-buttons">
+ <button title="Back" className="presentation-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
+ {this.renderPlayPauseButton()}
+ <button title="Next" className="presentation-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
+ </div>
+ <PresentationViewList
+ mainDocument={this.curPresentation}
+ deleteDocument={this.RemoveDoc}
+ gotoDocument={this.gotoDocument}
+ groupMappings={this.groupMappings}
+ presElementsMappings={this.presElementsMappings}
+ setChildrenDocs={this.setChildrenDocs}
+ presStatus={this.presStatus}
+ presButtonBackUp={this.presButtonBackUp}
+ presGroupBackUp={this.presGroupBackUp}
+ />
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/search/CheckBox.scss b/src/client/views/search/CheckBox.scss
new file mode 100644
index 000000000..af59d5fbf
--- /dev/null
+++ b/src/client/views/search/CheckBox.scss
@@ -0,0 +1,59 @@
+@import "../globalCssVariables";
+
+.checkboxfilter {
+ display: flex;
+ margin-top: 0px;
+ padding: 3px;
+
+ .outer {
+ display: flex;
+ position: relative;
+ justify-content: center;
+ align-items: center;
+ margin-top: 0px;
+
+ .check-container:hover~.check-box {
+ background-color: $intermediate-color;
+ }
+
+ .check-container {
+ width: 40px;
+ height: 40px;
+ position: absolute;
+ z-index: 1000;
+
+ .checkmark {
+ z-index: 1000;
+ position: absolute;
+ fill-opacity: 0;
+ stroke-width: 4px;
+ stroke: white;
+ }
+ }
+
+ .check-box {
+ z-index: 900;
+ position: relative;
+ height: 20px;
+ width: 20px;
+ overflow: visible;
+ background-color: transparent;
+ border-style: solid;
+ border-color: $alt-accent;
+ border-width: 2px;
+ -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;
+ }
+ }
+
+ .checkbox-title {
+ display: flex;
+ align-items: center;
+ text-transform: capitalize;
+ margin-left: 15px;
+ }
+
+}
+
diff --git a/src/client/views/search/CheckBox.tsx b/src/client/views/search/CheckBox.tsx
new file mode 100644
index 000000000..a9d48219a
--- /dev/null
+++ b/src/client/views/search/CheckBox.tsx
@@ -0,0 +1,131 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action, runInAction, IReactionDisposer, reaction } from 'mobx';
+import "./CheckBox.scss";
+import * as anime from 'animejs';
+
+interface CheckBoxProps {
+ originalStatus: boolean;
+ updateStatus(newStatus: boolean): void;
+ title: string;
+ parent: any;
+ numCount: number;
+ default: boolean;
+}
+
+@observer
+export class CheckBox extends React.Component<CheckBoxProps>{
+ // true = checked, false = unchecked
+ @observable private _status: boolean;
+ @observable private uncheckTimeline: anime.AnimeTimelineInstance;
+ @observable private checkTimeline: anime.AnimeTimelineInstance;
+ @observable private checkRef: any;
+ @observable private _resetReaction?: IReactionDisposer;
+
+
+ constructor(props: CheckBoxProps) {
+ super(props);
+ this._status = this.props.originalStatus;
+ this.checkRef = React.createRef();
+
+ this.checkTimeline = anime.timeline({
+ loop: false,
+ autoplay: false,
+ direction: "normal",
+ }); this.uncheckTimeline = anime.timeline({
+ loop: false,
+ autoplay: false,
+ direction: "normal",
+ });
+ }
+
+ componentDidMount = () => {
+ this.uncheckTimeline.add({
+ targets: this.checkRef.current,
+ easing: "easeInOutQuad",
+ duration: 500,
+ opacity: 0,
+ });
+ this.checkTimeline.add({
+ targets: this.checkRef.current,
+ easing: "easeInOutQuad",
+ duration: 500,
+ strokeDashoffset: [anime.setDashoffset, 0],
+ opacity: 1
+ });
+
+ if (this.props.originalStatus) {
+ this.checkTimeline.play();
+ this.checkTimeline.restart();
+ }
+
+ this._resetReaction = reaction(
+ () => this.props.parent._resetBoolean,
+ () => {
+ if (this.props.parent._resetBoolean) {
+ runInAction(() => {
+ this.reset();
+ this.props.parent._resetCounter++;
+ if (this.props.parent._resetCounter === this.props.numCount) {
+ this.props.parent._resetCounter = 0;
+ this.props.parent._resetBoolean = false;
+ }
+ });
+ }
+ },
+ );
+ }
+
+ @action.bound
+ reset() {
+ if (this.props.default) {
+ if (!this._status) {
+ this._status = true;
+ this.checkTimeline.play();
+ this.checkTimeline.restart();
+ }
+ }
+ else {
+ if (this._status) {
+ this._status = false;
+ this.uncheckTimeline.play();
+ this.uncheckTimeline.restart();
+ }
+ }
+
+ this.props.updateStatus(this.props.default);
+ }
+
+ @action.bound
+ onClick = () => {
+ if (this._status) {
+ this.uncheckTimeline.play();
+ this.uncheckTimeline.restart();
+ }
+ else {
+ this.checkTimeline.play();
+ this.checkTimeline.restart();
+
+ }
+ this._status = !this._status;
+ this.props.updateStatus(this._status);
+
+ }
+
+ render() {
+ return (
+ <div className="checkboxfilter" onClick={this.onClick}>
+ <div className="outer">
+ <div className="check-container">
+ <svg viewBox="0 12 40 40">
+ <path ref={this.checkRef} className="checkmark" d="M14.1 27.2l7.1 7.2 16.7-18" />
+ </svg>
+ </div>
+ <div className="check-box" />
+ </div>
+ <div className="checkbox-title">{this.props.title}</div>
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/search/CollectionFilters.scss b/src/client/views/search/CollectionFilters.scss
new file mode 100644
index 000000000..b54cdcbd1
--- /dev/null
+++ b/src/client/views/search/CollectionFilters.scss
@@ -0,0 +1,20 @@
+@import "../globalCssVariables";
+
+.collection-filters {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+
+ &.main {
+ width: 47%;
+ float: left;
+ }
+
+ &.optional {
+ width: 60%;
+ display: grid;
+ grid-template-columns: 50% 50%;
+ float: right;
+ opacity: 0;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/CollectionFilters.tsx b/src/client/views/search/CollectionFilters.tsx
new file mode 100644
index 000000000..48d0b2ddb
--- /dev/null
+++ b/src/client/views/search/CollectionFilters.tsx
@@ -0,0 +1,83 @@
+import * as React from 'react';
+import { observable, action } from 'mobx';
+import { CheckBox } from './CheckBox';
+import "./CollectionFilters.scss";
+import * as anime from 'animejs';
+
+interface CollectionFilterProps {
+ collectionStatus: boolean;
+ updateCollectionStatus(val: boolean): void;
+ collectionSelfStatus: boolean;
+ updateSelfCollectionStatus(val: boolean): void;
+ collectionParentStatus: boolean;
+ updateParentCollectionStatus(val: boolean): void;
+}
+
+export class CollectionFilters extends React.Component<CollectionFilterProps> {
+
+ static Instance: CollectionFilters;
+
+ @observable public _resetBoolean = false;
+ @observable public _resetCounter: number = 0;
+
+ @observable private _collectionsSelected = this.props.collectionStatus;
+ @observable private _timeline: anime.AnimeTimelineInstance;
+ @observable private _ref: any;
+
+ constructor(props: CollectionFilterProps) {
+ super(props);
+ CollectionFilters.Instance = this;
+ this._ref = React.createRef();
+
+ this._timeline = anime.timeline({
+ loop: false,
+ autoplay: false,
+ direction: "reverse",
+ });
+ }
+
+ componentDidMount = () => {
+ this._timeline.add({
+ targets: this._ref.current,
+ easing: "easeInOutQuad",
+ duration: 500,
+ opacity: 1,
+ });
+
+ if (this._collectionsSelected) {
+ this._timeline.play();
+ this._timeline.reverse();
+ }
+ }
+
+ @action.bound
+ resetCollectionFilters() { this._resetBoolean = true; }
+
+ @action.bound
+ updateColStat(val: boolean) {
+ this.props.updateCollectionStatus(val);
+
+ if (this._collectionsSelected !== val) {
+ this._timeline.play();
+ this._timeline.reverse();
+ }
+
+ this._collectionsSelected = val;
+ }
+
+ render() {
+ return (
+ <div>
+ <div className="collection-filters">
+ <div className="collection-filters main">
+ <CheckBox default={false} title={"limit to current collection"} parent={this} numCount={3} updateStatus={this.updateColStat} originalStatus={this.props.collectionStatus} />
+ </div>
+ <div className="collection-filters optional" ref={this._ref}>
+ <CheckBox default={true} title={"Search in self"} parent={this} numCount={3} updateStatus={this.props.updateSelfCollectionStatus} originalStatus={this.props.collectionSelfStatus} />
+ <CheckBox default={true} title={"Search in parent"} parent={this} numCount={3} updateStatus={this.props.updateParentCollectionStatus} originalStatus={this.props.collectionParentStatus} />
+ </div>
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/FieldFilters.scss b/src/client/views/search/FieldFilters.scss
new file mode 100644
index 000000000..ba0926140
--- /dev/null
+++ b/src/client/views/search/FieldFilters.scss
@@ -0,0 +1,5 @@
+.field-filters {
+ width: 100%;
+ display: grid;
+ grid-template-columns: 18% 20% 60%;
+} \ No newline at end of file
diff --git a/src/client/views/search/FieldFilters.tsx b/src/client/views/search/FieldFilters.tsx
new file mode 100644
index 000000000..648aac20a
--- /dev/null
+++ b/src/client/views/search/FieldFilters.tsx
@@ -0,0 +1,41 @@
+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={true} numCount={3} parent={this} originalStatus={this.props.dataFieldStatus} updateStatus={this.props.updateDataStatus} title={Keys.DATA} />
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/FilterBox.scss b/src/client/views/search/FilterBox.scss
new file mode 100644
index 000000000..1eb8963d7
--- /dev/null
+++ b/src/client/views/search/FilterBox.scss
@@ -0,0 +1,108 @@
+@import "../globalCssVariables";
+@import "./NaviconButton.scss";
+
+.filter-form {
+ padding: 25px;
+ width: 600px;
+ background: $dark-color;
+ position: relative;
+ right: 1px;
+ color: $light-color;
+ flex-direction: column;
+ display: inline-block;
+ transform-origin: top;
+ overflow: auto;
+
+ .top-filter-header {
+
+ #header {
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 25;
+ width: 80%;
+ }
+
+ .close-icon {
+ width: 20%;
+ opacity: .6;
+ position: relative;
+ display: inline-block;
+
+ .line {
+ display: block;
+ background: $alt-accent;
+ width: $width-line;
+ height: $height-line;
+ 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;
+
+ .filter-title {
+ font-size: 18;
+ 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;
+ }
+ }
+ }
+
+ .filter-buttons {
+ border-color: rgba(178, 206, 248, .2); // $darker-alt-accent
+ border-top-style: solid;
+ padding-top: 10px;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx
new file mode 100644
index 000000000..23a1b31d8
--- /dev/null
+++ b/src/client/views/search/FilterBox.tsx
@@ -0,0 +1,384 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action } from 'mobx';
+import "./SearchBox.scss";
+import { faTimes } from '@fortawesome/free-solid-svg-icons';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { Doc } from '../../../new_fields/Doc';
+import { Id } from '../../../new_fields/FieldSymbols';
+import { DocTypes } from '../../documents/Documents';
+import { Cast, StrCast } from '../../../new_fields/Types';
+import * as _ from "lodash";
+import { ToggleBar } from './ToggleBar';
+import { IconBar } from './IconBar';
+import { FieldFilters } from './FieldFilters';
+import { SelectionManager } from '../../util/SelectionManager';
+import { DocumentView } from '../nodes/DocumentView';
+import { CollectionFilters } from './CollectionFilters';
+import { NaviconButton } from './NaviconButton';
+import * as $ from 'jquery';
+import "./FilterBox.scss";
+import { SearchBox } from './SearchBox';
+
+library.add(faTimes);
+
+export enum Keys {
+ TITLE = "title",
+ AUTHOR = "author",
+ DATA = "data"
+}
+
+@observer
+export class FilterBox extends React.Component {
+
+ static Instance: FilterBox;
+ public _allIcons: string[] = [DocTypes.AUDIO, DocTypes.COL, DocTypes.HIST, DocTypes.IMG, DocTypes.LINK, DocTypes.PDF, DocTypes.TEXT, DocTypes.VID, DocTypes.WEB];
+
+ //if true, any keywords can be used. if false, all keywords are required.
+ @observable private _basicWordStatus: boolean = true;
+ @observable private _filterOpen: boolean = false;
+ @observable private _icons: string[] = this._allIcons;
+ @observable private _titleFieldStatus: boolean = true;
+ @observable private _authorFieldStatus: boolean = true;
+ @observable private _dataFieldStatus: boolean = true;
+ @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;
+ }
+
+ componentDidMount = () => {
+ document.addEventListener("pointerdown", (e) => {
+ if (!e.defaultPrevented && e.timeStamp !== this._pointerTime) {
+ SearchBox.Instance.closeSearch();
+ }
+ });
+ }
+
+ 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");
+
+ var panel = this.nextElementSibling as HTMLElement;
+ if (panel.style.maxHeight) {
+ panel.style.overflow = "hidden";
+ panel.style.maxHeight = null;
+ panel.style.opacity = "0";
+ } else {
+ setTimeout(() => {
+ panel.style.overflow = "visible";
+ }, 200);
+ setTimeout(() => {
+ panel.style.opacity = "1";
+ }, 50);
+ panel.style.maxHeight = panel.scrollHeight + "px";
+
+ }
+ });
+ }
+ });
+ }
+
+ @action.bound
+ minimizeAll() {
+ $('document').ready(function () {
+ var acc = document.getElementsByClassName('filter-header');
+
+ // tslint:disable-next-line: prefer-for-of
+ for (var i = 0; i < acc.length; i++) {
+ let classList = acc[i].classList;
+ if (classList.contains("active")) {
+ acc[i].classList.toggle("active");
+ var panel = acc[i].nextElementSibling as HTMLElement;
+ panel.style.overflow = "hidden";
+ panel.style.maxHeight = null;
+ }
+ }
+ });
+ }
+
+ @action.bound
+ resetFilters = () => {
+ ToggleBar.Instance.resetToggle();
+ IconBar.Instance.selectAll();
+ FieldFilters.Instance.resetFieldFilters();
+ CollectionFilters.Instance.resetCollectionFilters();
+ }
+
+ basicRequireWords(query: string): string {
+ let oldWords = query.split(" ");
+ let newWords: string[] = [];
+ oldWords.forEach(word => {
+ let newWrd = "+" + word;
+ newWords.push(newWrd);
+ });
+ query = newWords.join(" ");
+
+ return query;
+ }
+
+ basicFieldFilters(query: string, type: string): string {
+ let oldWords = query.split(" ");
+ let mod = "";
+
+ if (type === Keys.AUTHOR) {
+ mod = " author_t:";
+ } if (type === Keys.DATA) {
+ //TODO
+ } if (type === Keys.TITLE) {
+ mod = " title_t:";
+ }
+
+ let newWords: string[] = [];
+ oldWords.forEach(word => {
+ let 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._dataFieldStatus) {
+ finalQuery = finalQuery + this.basicFieldFilters(query, Keys.DATA);
+ }
+ return finalQuery;
+ }
+
+ get fieldFiltersApplied() { return !(this._dataFieldStatus && 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[] {
+ let selectedDocs: DocumentView[] = SelectionManager.SelectedDocuments();
+ let collections: Doc[] = [];
+
+ selectedDocs.forEach(async element => {
+ let layout: string = StrCast(element.props.Document.baseLayout);
+ //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);
+ }
+ }
+ //gets the selected doc's containing view
+ let containingView = element.props.ContainingCollectionView;
+ //makes sure collections aren't added more than once
+ if (containingView && !collections.includes(containingView.props.Document)) {
+ collections.push(containingView.props.Document);
+ }
+ });
+
+ 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 {
+ let collections: Doc[] = this.getCurCollections();
+ let oldWords = query.split(" ");
+
+ let collectionString: string[] = [];
+ collections.forEach(doc => {
+ let proto = doc.proto;
+ let protoId = (proto || doc)[Id];
+ let colString: string = "{!join from=data_l to=id}id:" + protoId + " ";
+ collectionString.push(colString);
+ });
+
+ let finalColString = collectionString.join(" ");
+ finalColString = finalColString.trim();
+ return "+(" + finalColString + ")" + query;
+ }
+
+ @action
+ filterDocsByType(docs: Doc[]) {
+ let finalDocs: Doc[] = [];
+ docs.forEach(doc => {
+ let layoutresult = Cast(doc.type, "string");
+ if (!layoutresult || this._icons.includes(layoutresult)) {
+ finalDocs.push(doc);
+ }
+ });
+ return finalDocs;
+ }
+
+ @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
+ getBasicWordStatus() { return 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
+ toggleFieldOpen() { this._fieldOpen = !this._fieldOpen; }
+
+ @action.bound
+ toggleColOpen() { this._colOpen = !this._colOpen; }
+
+ @action.bound
+ toggleTypeOpen() { this._typeOpen = !this._typeOpen; }
+
+ @action.bound
+ toggleWordStatusOpen() { this._wordStatusOpen = !this._wordStatusOpen; }
+
+ @action.bound
+ updateTitleStatus(newStat: boolean) { this._titleFieldStatus = newStat; }
+
+ @action.bound
+ updateAuthorStatus(newStat: boolean) { this._authorFieldStatus = newStat; }
+
+ @action.bound
+ updateDataStatus(newStat: boolean) { this._dataFieldStatus = 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; }
+
+ getCollectionStatus() { return this._collectionStatus; }
+ getSelfCollectionStatus() { return this._collectionSelfStatus; }
+ getParentCollectionStatus() { return this._collectionParentStatus; }
+ getTitleStatus() { return this._titleFieldStatus; }
+ getAuthorStatus() { return this._authorFieldStatus; }
+ getDataStatus() { return this._dataFieldStatus; }
+
+ // 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 />
+ </div>
+ {this._filterOpen ? (
+ <div className="filter-form" onPointerDown={this.stopProp} id="filter-form" style={this._filterOpen ? { display: "flex" } : { display: "none" }}>
+ <div className="top-filter-header" style={{ display: "flex", width: "100%" }}>
+ <div id="header">Filter Search Results</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 style={{ marginLeft: "auto" }}><NaviconButton onClick={this.toggleWordStatusOpen} /></div>
+ </div>
+ <div className="filter-panel" >
+ <ToggleBar handleChange={this.handleWordQueryChange} getStatus={this.getBasicWordStatus}
+ originalStatus={this._basicWordStatus} optionOne={"Include Any Keywords"} optionTwo={"Include All Keywords"} />
+ </div>
+ </div>
+ <div className="filter-div">
+ <div className="filter-header">
+ <div className="filter-title icon">Filter by type of node</div>
+ <div style={{ marginLeft: "auto" }}><NaviconButton onClick={this.toggleTypeOpen} /></div>
+ </div>
+ <div className="filter-panel"><IconBar /></div>
+ </div>
+ <div className="filter-div">
+ <div className="filter-header">
+ <div className='filter-title collection'>Search in current collections</div>
+ <div style={{ marginLeft: "auto" }}><NaviconButton onClick={this.toggleColOpen} /></div>
+ </div>
+ <div className="filter-panel"><CollectionFilters
+ updateCollectionStatus={this.updateCollectionStatus} updateParentCollectionStatus={this.updateParentCollectionStatus} updateSelfCollectionStatus={this.updateSelfCollectionStatus}
+ collectionStatus={this._collectionStatus} collectionParentStatus={this._collectionParentStatus} collectionSelfStatus={this._collectionSelfStatus} /></div>
+ </div>
+ <div className="filter-div">
+ <div className="filter-header">
+ <div className="filter-title field">Filter by Basic Keys</div>
+ <div style={{ marginLeft: "auto" }}><NaviconButton onClick={this.toggleFieldOpen} /></div>
+ </div>
+ <div className="filter-panel"><FieldFilters
+ titleFieldStatus={this._titleFieldStatus} dataFieldStatus={this._dataFieldStatus} 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="minimize-filter" onClick={this.minimizeAll}>Minimize All</button>
+ <button className="advanced-filter" >Advanced Filters</button>
+ <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/IconBar.scss b/src/client/views/search/IconBar.scss
new file mode 100644
index 000000000..e384722ce
--- /dev/null
+++ b/src/client/views/search/IconBar.scss
@@ -0,0 +1,12 @@
+@import "../globalCssVariables";
+
+.icon-bar {
+ display: flex;
+ justify-content: space-evenly;
+ align-items: center;
+ height: 40px;
+ width: 100%;
+ flex-wrap: wrap;
+ margin-bottom: 10px;
+}
+
diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx
new file mode 100644
index 000000000..744dd898a
--- /dev/null
+++ b/src/client/views/search/IconBar.tsx
@@ -0,0 +1,83 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action } from 'mobx';
+// import "./SearchBox.scss";
+import "./IconBar.scss";
+import "./IconButton.scss";
+import { DocTypes } from '../../documents/Documents';
+import { faSearch, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia, faBan, faTimesCircle, faCheckCircle } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import * as _ from "lodash";
+import { IconButton } from './IconButton';
+import { FilterBox } from './FilterBox';
+
+library.add(faSearch);
+library.add(faObjectGroup);
+library.add(faImage);
+library.add(faStickyNote);
+library.add(faFilePdf);
+library.add(faFilm);
+library.add(faMusic);
+library.add(faLink);
+library.add(faChartBar);
+library.add(faGlobeAsia);
+library.add(faBan);
+
+@observer
+export class IconBar extends React.Component {
+
+ static Instance: IconBar;
+
+ @observable public _resetClicked: boolean = false;
+ @observable public _selectAllClicked: boolean = false;
+ @observable public _reset: number = 0;
+ @observable public _select: number = 0;
+
+ constructor(props: any) {
+ super(props);
+ IconBar.Instance = this;
+ }
+
+ @action.bound
+ getList(): string[] { return FilterBox.Instance.getIcons(); }
+
+ @action.bound
+ updateList(newList: string[]) { FilterBox.Instance.updateIcon(newList); }
+
+ @action.bound
+ resetSelf = () => {
+ this._resetClicked = true;
+ this.updateList([]);
+ }
+
+ @action.bound
+ selectAll = () => {
+ this._selectAllClicked = true;
+ this.updateList(FilterBox.Instance._allIcons);
+ }
+
+ render() {
+ return (
+ <div className="icon-bar">
+ <div className="type-outer">
+ <div className={"type-icon all"}
+ onClick={this.selectAll}>
+ <FontAwesomeIcon className="fontawesome-icon" icon={faCheckCircle} />
+ </div>
+ <div className="filter-description">Select All</div>
+ </div>
+ {FilterBox.Instance._allIcons.map((type: string) =>
+ <IconButton type={type} />
+ )}
+ <div className="type-outer">
+ <div className={"type-icon none"}
+ onClick={this.resetSelf}>
+ <FontAwesomeIcon className="fontawesome-icon" icon={faTimesCircle} />
+ </div>
+ <div className="filter-description">Clear</div>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/search/IconButton.scss b/src/client/views/search/IconButton.scss
new file mode 100644
index 000000000..94b294ba5
--- /dev/null
+++ b/src/client/views/search/IconButton.scss
@@ -0,0 +1,52 @@
+@import "../globalCssVariables";
+
+.type-outer {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 45px;
+ height: 60px;
+
+ .type-icon {
+ height: 45px;
+ width: 45px;
+ color: $light-color;
+ border-radius: 50%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ -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;
+ font-size: 2em;
+
+ .fontawesome-icon {
+ height: 24px;
+ width: 24px;
+ }
+ }
+
+ .filter-description {
+ text-transform: capitalize;
+ font-size: 10;
+ text-align: center;
+ height: 15px;
+ margin-top: 5px;
+ 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;
+ }
+
+ .type-icon:hover {
+ transform: scale(1.1);
+ background-color: $darker-alt-accent;
+ opacity: 1;
+
+ +.filter-description {
+ opacity: 1;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/IconButton.tsx b/src/client/views/search/IconButton.tsx
new file mode 100644
index 000000000..23ab42de0
--- /dev/null
+++ b/src/client/views/search/IconButton.tsx
@@ -0,0 +1,192 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action, runInAction, IReactionDisposer, reaction } from 'mobx';
+import "./SearchBox.scss";
+import "./IconButton.scss";
+import { faSearch, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia, faBan, faVideo, faCaretDown } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { library, icon } from '@fortawesome/fontawesome-svg-core';
+import { DocTypes } from '../../documents/Documents';
+import '../globalCssVariables.scss';
+import * as _ from "lodash";
+import { IconBar } from './IconBar';
+import { props } from 'bluebird';
+import { FilterBox } from './FilterBox';
+import { Search } from '../../../server/Search';
+
+library.add(faSearch);
+library.add(faObjectGroup);
+library.add(faImage);
+library.add(faStickyNote);
+library.add(faFilePdf);
+library.add(faFilm);
+library.add(faMusic);
+library.add(faLink);
+library.add(faChartBar);
+library.add(faGlobeAsia);
+library.add(faBan);
+
+interface IconButtonProps {
+ type: string;
+}
+
+@observer
+export class IconButton extends React.Component<IconButtonProps>{
+
+ @observable private _isSelected: boolean = FilterBox.Instance.getIcons().indexOf(this.props.type) !== -1;
+ @observable private _hover = false;
+ private _resetReaction?: IReactionDisposer;
+ private _selectAllReaction?: IReactionDisposer;
+
+ static Instance: IconButton;
+ constructor(props: IconButtonProps) {
+ super(props);
+ IconButton.Instance = this;
+ }
+
+ componentDidMount = () => {
+ this._resetReaction = reaction(
+ () => IconBar.Instance._resetClicked,
+ () => {
+ if (IconBar.Instance._resetClicked) {
+ runInAction(() => {
+ this.reset();
+ IconBar.Instance._reset++;
+ if (IconBar.Instance._reset === 9) {
+ IconBar.Instance._reset = 0;
+ IconBar.Instance._resetClicked = false;
+ }
+ });
+ }
+ },
+ );
+ this._selectAllReaction = reaction(
+ () => IconBar.Instance._selectAllClicked,
+ () => {
+ if (IconBar.Instance._selectAllClicked) {
+ runInAction(() => {
+ this.select();
+ IconBar.Instance._select++;
+ if (IconBar.Instance._select === 9) {
+ IconBar.Instance._select = 0;
+ IconBar.Instance._selectAllClicked = false;
+ }
+ });
+ }
+ },
+ );
+ }
+
+ @action.bound
+ getIcon() {
+ switch (this.props.type) {
+ case (DocTypes.NONE):
+ return faBan;
+ case (DocTypes.AUDIO):
+ return faMusic;
+ case (DocTypes.COL):
+ return faObjectGroup;
+ case (DocTypes.HIST):
+ return faChartBar;
+ case (DocTypes.IMG):
+ return faImage;
+ case (DocTypes.LINK):
+ return faLink;
+ case (DocTypes.PDF):
+ return faFilePdf;
+ case (DocTypes.TEXT):
+ return faStickyNote;
+ case (DocTypes.VID):
+ return faVideo;
+ case (DocTypes.WEB):
+ return faGlobeAsia;
+ default:
+ return faCaretDown;
+ }
+ }
+
+ @action.bound
+ onClick = () => {
+ let newList: string[] = FilterBox.Instance.getIcons();
+
+ if (!this._isSelected) {
+ this._isSelected = true;
+ newList.push(this.props.type);
+ }
+ else {
+ this._isSelected = false;
+ _.pull(newList, this.props.type);
+ }
+
+ FilterBox.Instance.updateIcon(newList);
+ }
+
+ selected = {
+ opacity: 1,
+ backgroundColor: "#c2c2c5" //$alt-accent
+ };
+
+ notSelected = {
+ opacity: 0.6,
+ };
+
+ hoverStyle = {
+ opacity: 1,
+ backgroundColor: "rgb(178, 206, 248)" //$darker-alt-accent
+ };
+
+ @action.bound
+ public reset() { this._isSelected = false; }
+
+ @action.bound
+ public select() { this._isSelected = true; }
+
+ @action
+ onMouseLeave = () => { this._hover = false; }
+
+ @action
+ onMouseEnter = () => { this._hover = true; }
+
+ getFA = () => {
+ switch (this.props.type) {
+ case (DocTypes.NONE):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faBan} />);
+ case (DocTypes.AUDIO):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faMusic} />);
+ case (DocTypes.COL):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faObjectGroup} />);
+ case (DocTypes.HIST):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faChartBar} />);
+ case (DocTypes.IMG):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faImage} />);
+ case (DocTypes.LINK):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faLink} />);
+ case (DocTypes.PDF):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faFilePdf} />);
+ case (DocTypes.TEXT):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faStickyNote} />);
+ case (DocTypes.VID):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faVideo} />);
+ case (DocTypes.WEB):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faGlobeAsia} />);
+ default:
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faCaretDown} />);
+ }
+ }
+
+ render() {
+ return (
+ <div className="type-outer" id={this.props.type + "-filter"}
+ onMouseEnter={this.onMouseEnter}
+ onMouseLeave={this.onMouseLeave}
+ onClick={this.onClick}>
+ <div className="type-icon" id={this.props.type + "-icon"}
+ style={this._hover ? this.hoverStyle : this._isSelected ? this.selected : this.notSelected}
+ >
+ {this.getFA()}
+ </div>
+ <div className="filter-description">{this.props.type}</div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/NaviconButton.scss b/src/client/views/search/NaviconButton.scss
new file mode 100644
index 000000000..c23bab461
--- /dev/null
+++ b/src/client/views/search/NaviconButton.scss
@@ -0,0 +1,69 @@
+@import "../globalCssVariables";
+
+$height-icon: 15px;
+$width-line: 30px;
+$height-line: 4px;
+
+$transition-time: 0.4s;
+$rotation: 45deg;
+$translateY: ($height-icon / 2);
+$translateX: 0;
+
+#hamburger-icon {
+ width: $width-line;
+ height: $height-icon;
+ position: relative;
+ display: block;
+ transition: all $transition-time;
+ -webkit-transition: all $transition-time;
+ -moz-transition: all $transition-time;
+
+ .line {
+ display: block;
+ background: $alt-accent;
+ width: $width-line;
+ height: $height-line;
+ position: absolute;
+ left: 0;
+ border-radius: ($height-line / 2);
+ transition: all $transition-time;
+ -webkit-transition: all $transition-time;
+ -moz-transition: all $transition-time;
+
+ &.line-1 {
+ top: 0;
+ }
+
+ &.line-2 {
+ top: 50%;
+ }
+
+ &.line-3 {
+ top: 100%;
+ }
+ }
+}
+
+.filter-header.active {
+ .line-1 {
+ transform: translateY($translateY) translateX($translateX) rotate($rotation);
+ -webkit-transform: translateY($translateY) translateX($translateX) rotate($rotation);
+ -moz-transform: translateY($translateY) translateX($translateX) rotate($rotation);
+ }
+
+ .line-2 {
+ opacity: 0;
+ }
+
+ .line-3 {
+ transform: translateY($translateY * -1) translateX($translateX) rotate($rotation * -1);
+ -webkit-transform: translateY($translateY * -1) translateX($translateX) rotate($rotation * -1);
+ -moz-transform: translateY($translateY * -1) translateX($translateX) rotate($rotation * -1);
+ }
+}
+
+.filter-header:hover #hamburger-icon {
+ transform: scale(1.1);
+ -webkit-transform: scale(1.1);
+ -moz-transform: scale(1.1);
+} \ No newline at end of file
diff --git a/src/client/views/search/NaviconButton.tsx b/src/client/views/search/NaviconButton.tsx
new file mode 100644
index 000000000..3fa36b163
--- /dev/null
+++ b/src/client/views/search/NaviconButton.tsx
@@ -0,0 +1,35 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import "./NaviconButton.scss";
+import * as $ from 'jquery';
+import { observable } from 'mobx';
+
+export interface NaviconProps{
+ onClick(): void;
+}
+
+export class NaviconButton extends React.Component<NaviconProps> {
+
+ @observable private _ref: React.RefObject<HTMLAnchorElement> = React.createRef();
+
+ componentDidMount = () => {
+ let that = this;
+ if(this._ref.current){this._ref.current.addEventListener("click", function(e) {
+ e.preventDefault();
+ if(that._ref.current){
+ that._ref.current.classList.toggle('active');
+ return false;
+ }
+ });}
+ }
+
+ render() {
+ return (
+ <a id="hamburger-icon" href="#" ref = {this._ref} title="Menu">
+ <span className="line line-1"></span>
+ <span className="line line-2"></span>
+ <span className="line line-3"></span>
+ </a>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/Pager.scss b/src/client/views/search/Pager.scss
new file mode 100644
index 000000000..2b9c81b93
--- /dev/null
+++ b/src/client/views/search/Pager.scss
@@ -0,0 +1,47 @@
+@import "../globalCssVariables";
+
+.search-pager {
+ background-color: $dark-color;
+ border-radius: 10px;
+ width: 500px;
+ display: flex;
+ justify-content: center;
+ // margin-left: 27px;
+ float: right;
+ margin-right: 74px;
+ margin-left: auto;
+
+ // flex-direction: column;
+
+ .search-arrows {
+ display: flex;
+ justify-content: center;
+ margin: 10px;
+ width: 50%;
+
+ .arrow {
+ -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;
+
+ .fontawesome-icon {
+ color: $light-color;
+ width: 20px;
+ height: 20px;
+ margin-right: 2px;
+ margin-left: 2px;
+ // opacity: .7;
+ }
+ }
+
+ .pager-title {
+ text-align: center;
+ // font-size: 8px;
+ // margin-bottom: 10px;
+ color: $light-color;
+ // padding: 2px;
+ width: 40%;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/Pager.tsx b/src/client/views/search/Pager.tsx
new file mode 100644
index 000000000..1c62773b1
--- /dev/null
+++ b/src/client/views/search/Pager.tsx
@@ -0,0 +1,78 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { faArrowCircleRight, faArrowCircleLeft } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import "./Pager.scss";
+import { SearchBox } from './SearchBox';
+import { observable, action } from 'mobx';
+import { FilterBox } from './FilterBox';
+
+library.add(faArrowCircleRight);
+library.add(faArrowCircleLeft);
+
+@observer
+export class Pager extends React.Component {
+
+ @observable _leftHover: boolean = false;
+ @observable _rightHover: boolean = false;
+
+ @action
+ onLeftClick(e: React.PointerEvent) {
+ FilterBox.Instance._pointerTime = e.timeStamp;
+ if (SearchBox.Instance._pageNum > 0) {
+ SearchBox.Instance._pageNum -= 1;
+ }
+ }
+
+ @action
+ onRightClick(e: React.PointerEvent) {
+ FilterBox.Instance._pointerTime = e.timeStamp;
+ if (SearchBox.Instance._pageNum + 1 < SearchBox.Instance._maxNum) {
+ SearchBox.Instance._pageNum += 1;
+ }
+ }
+
+ @action.bound
+ mouseInLeft() {
+ this._leftHover = true;
+ }
+
+ @action.bound
+ mouseOutLeft() {
+ this._leftHover = false;
+ }
+
+ @action.bound
+ mouseInRight() {
+ this._rightHover = true;
+ }
+
+ @action.bound
+ mouseOutRight() {
+ this._rightHover = false;
+ }
+
+ render() {
+ return (
+ <div className="search-pager">
+ <div className="search-arrows">
+ <div className="arrow"
+ onPointerDown={this.onLeftClick} style={SearchBox.Instance._pageNum === 0 ? { opacity: .2 } : this._leftHover ? { opacity: 1 } : { opacity: .7 }}
+ onMouseEnter={this.mouseInLeft} onMouseOut={this.mouseOutLeft}>
+ <FontAwesomeIcon className="fontawesome-icon" icon={faArrowCircleLeft} />
+ </div>
+ <div className="pager-title">
+ page {SearchBox.Instance._pageNum + 1} of {SearchBox.Instance._maxNum}
+ </div>
+ <div className="arrow"
+ onPointerDown={this.onRightClick} style={SearchBox.Instance._pageNum === SearchBox.Instance._maxNum - 1 ? { opacity: .2 } : this._rightHover ? { opacity: 1 } : { opacity: .7 }}
+ onMouseEnter={this.mouseInRight} onMouseOut={this.mouseOutRight}>
+ <FontAwesomeIcon className="fontawesome-icon" icon={faArrowCircleRight} />
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss
new file mode 100644
index 000000000..2a27bbe62
--- /dev/null
+++ b/src/client/views/search/SearchBox.scss
@@ -0,0 +1,64 @@
+@import "../globalCssVariables";
+@import "./NaviconButton.scss";
+
+.searchBox-bar {
+ height: 32px;
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ padding-left: 2px;
+ padding-right: 2px;
+
+ .searchBox-barChild {
+
+ &.searchBox-collection {
+ flex: 0 1 auto;
+ margin-left: 2px;
+ margin-right: 2px
+ }
+
+ &.searchBox-input {
+ display: block;
+ width: 130px;
+ -webkit-transition: width 0.4s;
+ transition: width 0.4s;
+ align-self: stretch;
+ margin-left: 2px;
+ margin-right: 2px
+ }
+
+ .searchBox-input:focus {
+ width: 500px;
+ outline: 3px solid lightblue;
+ }
+
+ &.searchBox-filter {
+ align-self: stretch;
+ margin-left: 2px;
+ margin-right: 2px
+ }
+ }
+}
+
+.searchBox-results {
+ margin-left: 27px;
+ top: 300px;
+ display: flex;
+ flex-direction: column;
+ margin-right: 72px;
+ height: 560px;
+ overflow: hidden;
+ overflow-y: auto;
+
+ .no-result {
+ width: 500px;
+ background: $light-color-secondary;
+ border-color: $intermediate-color;
+ border-bottom-style: solid;
+ padding: 10px;
+ height: 50px;
+ text-transform: uppercase;
+ text-align: left;
+ font-weight: bold;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
new file mode 100644
index 000000000..7dcfbe1ef
--- /dev/null
+++ b/src/client/views/search/SearchBox.tsx
@@ -0,0 +1,194 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action, runInAction } from 'mobx';
+import "./SearchBox.scss";
+import "./FilterBox.scss";
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { SetupDrag } from '../../util/DragManager';
+import { Docs } from '../../documents/Documents';
+import { NumCast } from '../../../new_fields/Types';
+import { Doc } from '../../../new_fields/Doc';
+import { SearchItem } from './SearchItem';
+import { DocServer } from '../../DocServer';
+import * as rp from 'request-promise';
+import { Id } from '../../../new_fields/FieldSymbols';
+import { SearchUtil } from '../../util/SearchUtil';
+import { RouteStore } from '../../../server/RouteStore';
+import { FilterBox } from './FilterBox';
+
+@observer
+export class SearchBox extends React.Component {
+
+ @observable private _searchString: string = "";
+ @observable private _resultsOpen: boolean = false;
+ @observable private _results: Doc[] = [];
+ @observable private _openNoResults: boolean = false;
+ @observable public _pageNum: number = 0;
+ //temp
+ @observable public _maxNum: number = 10;
+
+ static Instance: SearchBox;
+
+ constructor(props: any) {
+ super(props);
+
+ SearchBox.Instance = this;
+ }
+
+ @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;
+
+ if (this._searchString === "") {
+ this._results = [];
+ this._openNoResults = false;
+ }
+ }
+
+ enter = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") { this.submitSearch(); }
+ }
+
+ public static async convertDataUri(imageUri: string, returnedFilename: string) {
+ try {
+ let posting = DocServer.prepend(RouteStore.dataUriToImage);
+ const returnedUri = await rp.post(posting, {
+ body: {
+ uri: imageUri,
+ name: returnedFilename
+ },
+ json: true,
+ });
+ return returnedUri;
+
+ } catch (e) {
+ console.log(e);
+ }
+ }
+
+ @action
+ submitSearch = async () => {
+ let query = this._searchString; // searchbox gets query
+ let results: Doc[];
+
+ query = FilterBox.Instance.getFinalQuery(query);
+
+ //if there is no query there should be no result
+ if (query === "") {
+ results = [];
+ }
+ else {
+ //gets json result into a list of documents that can be used
+ //these are filtered by type
+ results = await this.getResults(query);
+ }
+
+ runInAction(() => {
+ this._resultsOpen = true;
+ this._results = results;
+ this._openNoResults = true;
+ });
+ }
+
+ @action
+ getResults = async (query: string) => {
+ const { docs } = await SearchUtil.Search(query, true);
+ return FilterBox.Instance.filterDocsByType(docs);
+ }
+
+ collectionRef = React.createRef<HTMLSpanElement>();
+ startDragCollection = async () => {
+ const results = await this.getResults(FilterBox.Instance.getFinalQuery(this._searchString));
+ const docs = results.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) {
+ 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;
+ }
+ doc.zoomBasis = 1;
+ x += 250;
+ if (x > 1000) {
+ x = 0;
+ y += 300;
+ }
+ }
+ return Docs.Create.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` });
+ }
+
+ @action.bound
+ openSearch(e: React.PointerEvent) {
+ e.stopPropagation();
+ this._openNoResults = false;
+ FilterBox.Instance.closeFilter();
+ this._resultsOpen = true;
+ FilterBox.Instance._pointerTime = e.timeStamp;
+ }
+
+ @action.bound
+ closeSearch = () => {
+ console.log("closing search");
+ FilterBox.Instance.closeFilter();
+ this.closeResults();
+ }
+
+ @action.bound
+ closeResults() {
+ this._resultsOpen = false;
+ this._results = [];
+ }
+
+ render() {
+ return (
+ <div className="searchBox-container">
+ <div className="searchBox-bar">
+ <span className="searchBox-barChild searchBox-collection" onPointerDown={SetupDrag(this.collectionRef, this.startDragCollection)} ref={this.collectionRef}>
+ <FontAwesomeIcon icon="object-group" size="lg" />
+ </span>
+ <input value={this._searchString} onChange={this.onChange} type="text" placeholder="Search..."
+ className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter}
+ style={{ width: this._resultsOpen ? "500px" : "100px" }} />
+ <button className="searchBox-barChild searchBox-filter" onClick={FilterBox.Instance.openFilter} onPointerDown={FilterBox.Instance.stopProp}>Filter</button>
+ </div>
+ <div className="searchBox-results" style={this._resultsOpen ? { display: "flex" } : { display: "none" }}>
+ {(this._results.length !== 0) ? (
+ this._results.map(result => <SearchItem doc={result} key={result[Id]} />)
+ ) :
+ this._openNoResults ? (<div className="no-result">No Search Results</div>) : null}
+ </div>
+ {/* <div style={this._results.length !== 0 ? { display: "flex" } : { display: "none" }}>
+ <Pager />
+ </div> */}
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/search/SearchItem.scss b/src/client/views/search/SearchItem.scss
new file mode 100644
index 000000000..fa4c9cb38
--- /dev/null
+++ b/src/client/views/search/SearchItem.scss
@@ -0,0 +1,187 @@
+@import "../globalCssVariables";
+
+.search-overview {
+ display: flex;
+ flex-direction: row-reverse;
+ justify-content: flex-end;
+ height: 70px;
+ z-index: 0;
+
+ .search-item {
+ width: 500px;
+ background: $light-color-secondary;
+ border-color: $intermediate-color;
+ border-bottom-style: solid;
+ padding: 10px;
+ height: 70px;
+ z-index: 0;
+ display: inline-block;
+
+ .main-search-info {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+
+ .search-title {
+ text-transform: uppercase;
+ text-align: left;
+ width: 100%;
+ font-weight: bold;
+ }
+
+ .search-info {
+ display: flex;
+ justify-content: flex-end;
+
+ .link-container.item {
+ margin-left: auto;
+ margin-right: auto;
+ height: 26px;
+ width: 26px;
+ border-radius: 13px;
+ background: $dark-color;
+ color: $light-color-secondary;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ -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;
+ transform-origin: top right;
+ overflow: hidden;
+ position: relative;
+
+ .link-count {
+ opacity: 1;
+ position: absolute;
+ z-index: 1000;
+ text-align: center;
+ -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;
+ }
+
+ .link-extended {
+ opacity: 0;
+ position: relative;
+ z-index: 500;
+ overflow: hidden;
+ -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;
+ }
+ }
+
+ .link-container.item:hover {
+ width: 70px;
+ }
+
+ .link-container.item:hover .link-count {
+ opacity: 0;
+ }
+
+ .link-container.item:hover .link-extended {
+ opacity: 1;
+ }
+
+ .icon-icons {
+ width:50px
+ }
+ .icon-live {
+ width:175px;
+ }
+
+ .icon-icons, .icon-live {
+ height:50px;
+ margin:auto;
+ overflow: hidden;
+ .search-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;
+ }
+ }
+ .search-type:hover+.search-label {
+ opacity: 1;
+ }
+
+ .search-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 {
+ height:175px;
+ .pdfBox-cont {
+ img {
+ width:100% !important;
+ }
+ }
+ }
+ }
+ .search-info:hover {
+ width:60%;
+ }
+ }
+ }
+
+ .search-item:hover~.searchBox-instances,
+ .searchBox-instances:hover,
+ .searchBox-instances:active {
+ opacity: 1;
+ background: $lighter-alt-accent;
+ -webkit-transform: scale(1);
+ -ms-transform: scale(1);
+ transform: scale(1);
+ }
+
+ .search-item:hover {
+ transition: all 0.2s;
+ background: $lighter-alt-accent;
+ }
+
+ .searchBox-instances {
+ float: left;
+ opacity: 1;
+ width: 150px;
+ transition: all 0.2s ease;
+ color: black;
+ transform-origin: top right;
+ -webkit-transform: scale(0);
+ -ms-transform: scale(0);
+ transform: scale(0);
+ }
+
+}
+.search-overview:hover {
+ z-index: 1;
+}
+.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
new file mode 100644
index 000000000..804ffa7f5
--- /dev/null
+++ b/src/client/views/search/SearchItem.tsx
@@ -0,0 +1,267 @@
+import React = require("react");
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faCaretUp, faChartBar, faFilePdf, faFilm, 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, DocListCast, HeightSym, WidthSym } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { emptyFunction, returnFalse, returnOne, Utils } from "../../../Utils";
+import { DocTypes } from "../../documents/Documents";
+import { DocumentManager } from "../../util/DocumentManager";
+import { SetupDrag, DragManager } from "../../util/DragManager";
+import { LinkManager } from "../../util/LinkManager";
+import { SearchUtil } from "../../util/SearchUtil";
+import { Transform } from "../../util/Transform";
+import { SEARCH_THUMBNAIL_SIZE } from "../../views/globalCssVariables.scss";
+import { CollectionViewType } from "../collections/CollectionBaseView";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { DocumentView } from "../nodes/DocumentView";
+import { SearchBox } from "./SearchBox";
+import "./SearchItem.scss";
+import "./SelectorContextMenu.scss";
+import { ContextMenu } from "../ContextMenu";
+import { faFile } from '@fortawesome/free-solid-svg-icons';
+
+export interface SearchItemProps {
+ doc: Doc;
+}
+
+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);
+
+@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() {
+ let aliases = (await SearchUtil.GetViewsOfDocument(this.props.doc)).filter(doc => doc !== this.props.doc);
+ const { docs } = await SearchUtil.Search(`data_l:"${this.props.doc[Id]}"`, true);
+ const map: Map<Doc, Doc> = new Map;
+ const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search(`data_l:"${doc[Id]}"`, true).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 (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+ const newPanX = NumCast(target.x) + NumCast(target.width) / NumCast(target.zoomBasis, 1) / 2;
+ const newPanY = NumCast(target.y) + NumCast(target.height) / NumCast(target.zoomBasis, 1) / 2;
+ col.panX = newPanX;
+ col.panY = newPanY;
+ }
+ CollectionDockingView.Instance.AddRightSplit(col, undefined);
+ };
+ }
+ render() {
+ return (
+ <div className="parents">
+ <p className="contexts">Contexts:</p>
+ {[...this._docs, ...this._otherDocs].map(doc => {
+ let 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, undefined, undefined, () => SearchBox.Instance.closeSearch())}>
+ <FontAwesomeIcon icon={faStickyNote} />
+ </div>
+ <a className="title" onClick={this.getOnClick(doc)}>{doc.col.title}</a>
+ </div>;
+ })}
+ </div>
+ );
+ }
+}
+
+@observer
+export class SearchItem extends React.Component<SearchItemProps> {
+
+ @observable _selected: boolean = false;
+
+ onClick = () => {
+ DocumentManager.Instance.jumpToDocument(this.props.doc, false);
+ }
+ @observable _useIcons = true;
+ @observable _displayDim = 50;
+
+ fitToBox = () => {
+ let bounds = Doc.ComputeContentBounds(this.props.doc);
+ return [(bounds.x + bounds.r) / 2, (bounds.y + bounds.b) / 2, Number(SEARCH_THUMBNAIL_SIZE) / Math.max((bounds.b - bounds.y), (bounds.r - bounds.x)), this._displayDim];
+ }
+ @computed
+ public get DocumentIcon() {
+ if (!this._useIcons) {
+ let returnXDimension = () => this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE);
+ let returnYDimension = () => this._displayDim;
+ let scale = () => returnXDimension() / NumCast(this.props.doc.nativeWidth, returnXDimension());
+ return <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))}
+ onPointerLeave={action(() => this._displayDim = 50)} >
+ <DocumentView
+ fitToBox={StrCast(this.props.doc.type).indexOf(DocTypes.COL) !== -1 ? this.fitToBox : undefined}
+ Document={this.props.doc}
+ addDocument={returnFalse}
+ removeDocument={returnFalse}
+ ScreenToLocalTransform={Transform.Identity}
+ addDocTab={returnFalse}
+ renderDepth={1}
+ PanelWidth={returnXDimension}
+ PanelHeight={returnYDimension}
+ focus={emptyFunction}
+ selectOnLoad={false}
+ parentActive={returnFalse}
+ whenActiveChanged={returnFalse}
+ bringToFront={emptyFunction}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}
+ ContainingCollectionView={undefined}
+ ContentScaling={scale}
+ />
+ </div>;
+ }
+
+ let layoutresult = StrCast(this.props.doc.type);
+ let button = layoutresult.indexOf(DocTypes.PDF) !== -1 ? faFilePdf :
+ layoutresult.indexOf(DocTypes.IMG) !== -1 ? faImage :
+ layoutresult.indexOf(DocTypes.TEXT) !== -1 ? faStickyNote :
+ layoutresult.indexOf(DocTypes.VID) !== -1 ? faFilm :
+ layoutresult.indexOf(DocTypes.COL) !== -1 ? faObjectGroup :
+ layoutresult.indexOf(DocTypes.AUDIO) !== -1 ? faMusic :
+ layoutresult.indexOf(DocTypes.LINK) !== -1 ? faLink :
+ layoutresult.indexOf(DocTypes.HIST) !== -1 ? faChartBar :
+ layoutresult.indexOf(DocTypes.WEB) !== -1 ? faGlobeAsia :
+ faCaretUp;
+ return <div onPointerDown={action(() => { this._useIcons = false; this._displayDim = Number(SEARCH_THUMBNAIL_SIZE); })} >
+ <FontAwesomeIcon icon={button} size="2x" />
+ </div>;
+ }
+
+ collectionRef = React.createRef<HTMLDivElement>();
+ startDocDrag = () => {
+ let doc = this.props.doc;
+ const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
+ if (isProto) {
+ return Doc.MakeDelegate(doc);
+ } else {
+ return Doc.MakeAlias(doc);
+ }
+ }
+
+ @computed
+ get linkCount() { return LinkManager.Instance.getAllRelatedLinks(this.props.doc).length; }
+
+ @computed
+ get linkString(): string {
+ let num = this.linkCount;
+ if (num === 1) {
+ return num.toString() + " link";
+ }
+ return num.toString() + " links";
+ }
+
+ @action
+ pointerDown = (e: React.PointerEvent) => { e.preventDefault(); e.button === 0 && SearchBox.Instance.openSearch(e); }
+
+ highlightDoc = (e: React.PointerEvent) => {
+ if (this.props.doc.type === DocTypes.LINK) {
+ if (this.props.doc.anchor1 && this.props.doc.anchor2) {
+
+ let doc1 = Cast(this.props.doc.anchor1, Doc, null);
+ let doc2 = Cast(this.props.doc.anchor2, Doc, null);
+ doc1 && (doc1.libraryBrush = true);
+ doc2 && (doc2.libraryBrush = true);
+ }
+ } else {
+ let docViews: DocumentView[] = DocumentManager.Instance.getAllDocumentViews(this.props.doc);
+ docViews.forEach(element => {
+ element.props.Document.libraryBrush = true;
+ });
+ }
+ }
+
+ unHighlightDoc = (e: React.PointerEvent) => {
+ if (this.props.doc.type === DocTypes.LINK) {
+ if (this.props.doc.anchor1 && this.props.doc.anchor2) {
+
+ let doc1 = Cast(this.props.doc.anchor1, Doc, null);
+ let doc2 = Cast(this.props.doc.anchor2, Doc, null);
+ doc1 && (doc1.libraryBrush = false);
+ doc2 && (doc2.libraryBrush = false);
+ }
+ } else {
+ let docViews: DocumentView[] = DocumentManager.Instance.getAllDocumentViews(this.props.doc);
+ docViews.forEach(element => {
+ element.props.Document.libraryBrush = false;
+ });
+ }
+ }
+
+ onContextMenu = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ ContextMenu.Instance.clearItems();
+ ContextMenu.Instance.addItem({
+ description: "Copy ID", event: () => {
+ Utils.CopyText(this.props.doc[Id]);
+ }
+ });
+ ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
+ }
+
+ onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
+ e.stopPropagation();
+ const doc = Doc.IsPrototype(this.props.doc) ? Doc.MakeDelegate(this.props.doc) : this.props.doc;
+ DragManager.StartDocumentDrag([e.currentTarget], new DragManager.DocumentDragData([doc], []), e.clientX, e.clientY, {
+ handlers: { dragComplete: emptyFunction },
+ hideSource: false,
+ });
+ }
+
+ render() {
+ return (
+ <div className="search-overview" onPointerDown={this.pointerDown} onContextMenu={this.onContextMenu}>
+ <div className="search-item" onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc} id="result"
+ onClick={this.onClick} onPointerDown={this.pointerDown} >
+ <div className="main-search-info">
+ <div title="Drag as document" onPointerDown={this.onPointerDown}> <FontAwesomeIcon icon="file" size="lg" /> </div>
+ <div className="search-title" id="result" >{this.props.doc.title}</div>
+ <div className="search-info" style={{ width: this._useIcons ? "15%" : "400px" }}>
+ <div className={`icon-${this._useIcons ? "icons" : "live"}`}>
+ <div className="search-type" >{this.DocumentIcon}</div>
+ <div className="search-label">{this.props.doc.type}</div>
+ </div>
+ <div className="link-container item">
+ <div className="link-count">{this.linkCount}</div>
+ <div className="link-extended">{this.linkString}</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div className="searchBox-instances">
+ <SelectorContextMenu {...this.props} />
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/SelectorContextMenu.scss b/src/client/views/search/SelectorContextMenu.scss
new file mode 100644
index 000000000..49f77b9bf
--- /dev/null
+++ b/src/client/views/search/SelectorContextMenu.scss
@@ -0,0 +1,15 @@
+@import "../globalCssVariables";
+
+.parents {
+ background: $lighter-alt-accent;
+ padding: 10px;
+
+ .contexts {
+ text-transform: uppercase;
+ }
+
+ .collection {
+ border-color: $darker-alt-accent;
+ border-bottom-style: solid;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/ToggleBar.scss b/src/client/views/search/ToggleBar.scss
new file mode 100644
index 000000000..633a194fe
--- /dev/null
+++ b/src/client/views/search/ToggleBar.scss
@@ -0,0 +1,36 @@
+@import "../globalCssVariables";
+
+.toggle-title {
+ display: flex;
+ align-items: center;
+ color: $light-color;
+ text-transform: uppercase;
+ flex-direction: row;
+ justify-content: space-around;
+ padding: 5px;
+
+ .toggle-option {
+ width: 50%;
+ text-align: center;
+ -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;
+ }
+}
+
+.toggle-bar {
+ height: 50px;
+ background-color: $alt-accent;
+ border-radius: 10px;
+ padding: 5px;
+ display: flex;
+ align-items: center;
+
+ .toggle-button {
+ width: 275px;
+ height: 100%;
+ border-radius: 10px;
+ background-color: $light-color;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/ToggleBar.tsx b/src/client/views/search/ToggleBar.tsx
new file mode 100644
index 000000000..178578c5c
--- /dev/null
+++ b/src/client/views/search/ToggleBar.tsx
@@ -0,0 +1,86 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action, runInAction, computed } from 'mobx';
+import "./SearchBox.scss";
+import "./ToggleBar.scss";
+import * as anime from 'animejs';
+
+export interface ToggleBarProps {
+ originalStatus: boolean;
+ optionOne: string;
+ optionTwo: string;
+ handleChange(): void;
+ getStatus(): boolean;
+}
+
+@observer
+export class ToggleBar extends React.Component<ToggleBarProps>{
+ static Instance: ToggleBar;
+
+ @observable private _forwardTimeline: anime.AnimeTimelineInstance;
+ @observable private _toggleButton: React.RefObject<HTMLDivElement>;
+ @observable private _originalStatus: boolean = this.props.originalStatus;
+
+ constructor(props: ToggleBarProps) {
+ super(props);
+ ToggleBar.Instance = this;
+ this._toggleButton = React.createRef();
+ this._forwardTimeline = anime.timeline({
+ loop: false,
+ autoplay: false,
+ direction: "reverse",
+ });
+ }
+
+ componentDidMount = () => {
+
+ let totalWidth = 265;
+
+ if (this._originalStatus) {
+ this._forwardTimeline.add({
+ targets: this._toggleButton.current,
+ translateX: totalWidth,
+ easing: "easeInOutQuad",
+ duration: 500
+ });
+ }
+ else {
+ this._forwardTimeline.add({
+ targets: this._toggleButton.current,
+ translateX: -totalWidth,
+ easing: "easeInOutQuad",
+ duration: 500
+ });
+ }
+ }
+
+ @action.bound
+ onclick() {
+ this._forwardTimeline.play();
+ this._forwardTimeline.reverse();
+ this.props.handleChange();
+ }
+
+ @action.bound
+ public resetToggle = () => {
+ if (!this.props.getStatus()) {
+ this._forwardTimeline.play();
+ this._forwardTimeline.reverse();
+ this.props.handleChange();
+ }
+ }
+
+ render() {
+ return (
+ <div>
+ <div className="toggle-title">
+ <div className="toggle-option" style={{ opacity: (this.props.getStatus() ? 1 : .4) }}>{this.props.optionOne}</div>
+ <div className="toggle-option" style={{ opacity: (this.props.getStatus() ? .4 : 1) }}>{this.props.optionTwo}</div>
+ </div>
+ <div className="toggle-bar" id="toggle-bar" onClick={this.onclick} style={{ flexDirection: (this._originalStatus ? "row" : "row-reverse") }}>
+ <div className="toggle-button" id="toggle-button" ref={this._toggleButton} />
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/debug/Repl.tsx b/src/debug/Repl.tsx
index c2db3bdcb..91b711c79 100644
--- a/src/debug/Repl.tsx
+++ b/src/debug/Repl.tsx
@@ -4,6 +4,8 @@ import { observer } from 'mobx-react';
import { observable, computed } from 'mobx';
import { CompileScript } from '../client/util/Scripting';
import { makeInterface } from '../new_fields/Schema';
+import { ObjectField } from '../new_fields/ObjectField';
+import { RefField } from '../new_fields/RefField';
@observer
class Repl extends React.Component {
@@ -42,7 +44,8 @@ class Repl extends React.Component {
return (
<div style={{ marginTop: "5px" }}>
<p>{command.command}</p>
- <pre>{JSON.stringify(command.result, null, 2)}</pre>
+ {/* <pre>{JSON.stringify(command.result, null, 2)}</pre> */}
+ <pre>{command.result instanceof RefField || command.result instanceof ObjectField ? "object" : String(command.result)}</pre>
</div>
);
});
diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx
index 57221aa39..0dca4b4b1 100644
--- a/src/debug/Test.tsx
+++ b/src/debug/Test.tsx
@@ -6,7 +6,6 @@ import { ImageField } from '../new_fields/URLField';
import { Doc } from '../new_fields/Doc';
import { List } from '../new_fields/List';
-
const schema1 = createSchema({
hello: "number",
test: "string",
diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx
index 753149756..f48eb696c 100644
--- a/src/debug/Viewer.tsx
+++ b/src/debug/Viewer.tsx
@@ -10,6 +10,15 @@ import { List } from '../new_fields/List';
import { URLField } from '../new_fields/URLField';
import { EditableView } from '../client/views/EditableView';
import { CompileScript } from '../client/util/Scripting';
+import { DateField } from '../new_fields/DateField';
+import { ScriptField } from '../new_fields/ScriptField';
+import CursorField from '../new_fields/CursorField';
+
+DateField;
+URLField;
+ScriptField;
+CursorField;
+
function applyToDoc(doc: { [index: string]: FieldResult }, key: string, scriptString: string): boolean;
function applyToDoc(doc: { [index: number]: FieldResult }, key: number, scriptString: string): boolean;
@@ -146,7 +155,7 @@ class Viewer extends React.Component {
@action
onKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Enter") {
- DocServer.getRefField(this.idToAdd).then(action((field: any) => {
+ DocServer.GetRefField(this.idToAdd).then(action((field: any) => {
if (field !== undefined) {
this.fields.push(field);
}
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
deleted file mode 100644
index ae532c9e2..000000000
--- a/src/fields/ScriptField.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-// import { Field, FieldId } from "./Field";
-// import { Types } from "../server/Message";
-// import { CompileScript, ScriptOptions, CompiledScript } from "../client/util/Scripting";
-// import { Server } from "../client/Server";
-// import { Without } from "../Utils";
-
-// export interface SerializableOptions extends Without<ScriptOptions, "capturedVariables"> {
-// capturedIds: { [id: string]: string };
-// }
-
-// export interface ScriptData {
-// script: string;
-// options: SerializableOptions;
-// }
-
-// export class ScriptField extends Field {
-// private _script?: CompiledScript;
-// get script(): CompiledScript {
-// return this._script!;
-// }
-// private options?: ScriptData;
-
-// constructor(script?: CompiledScript, id?: FieldId, save: boolean = true) {
-// super(id);
-
-// this._script = script;
-
-// if (save) {
-// Server.UpdateField(this);
-// }
-// }
-
-// ToScriptString() {
-// return "new ScriptField(...)";
-// }
-
-// GetValue() {
-// return this.script;
-// }
-
-// TrySetValue(): boolean {
-// throw new Error("Script fields currently can't be modified");
-// }
-
-// UpdateFromServer() {
-// throw new Error("Script fields currently can't be updated");
-// }
-
-// static FromJson(id: string, data: ScriptData): ScriptField {
-// let field = new ScriptField(undefined, id, false);
-// field.options = data;
-// return field;
-// }
-
-// init(callback: (res: Field) => any) {
-// const options = this.options!;
-// const keys = Object.keys(options.options.capturedIds);
-// Server.GetFields(keys).then(fields => {
-// let captured: { [name: string]: Field } = {};
-// keys.forEach(key => captured[options.options.capturedIds[key]] = fields[key]);
-// const opts: ScriptOptions = {
-// addReturn: options.options.addReturn,
-// params: options.options.params,
-// requiredType: options.options.requiredType,
-// capturedVariables: captured
-// };
-// const script = CompileScript(options.script, opts);
-// if (!script.compiled) {
-// throw new Error("Can't compile script");
-// }
-// this._script = script;
-// callback(this);
-// });
-// }
-
-// ToJson() {
-// const { options, originalScript } = this.script;
-// let capturedIds: { [id: string]: string } = {};
-// for (const capt in options.capturedVariables) {
-// capturedIds[options.capturedVariables[capt].Id] = capt;
-// }
-// const opts: SerializableOptions = {
-// ...options,
-// capturedIds
-// };
-// delete (opts as any).capturedVariables;
-// return {
-// id: this.Id,
-// type: Types.Script,
-// data: {
-// script: originalScript,
-// options: opts,
-// },
-// };
-// }
-
-// Copy(): Field {
-// //Script fields are currently immutable, so we can fake copy them
-// return this;
-// }
-// } \ No newline at end of file
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index df597e0a9..a8f94b746 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -57,11 +57,11 @@ class Uploader extends React.Component {
this.status = "getting user document";
- const res = await rp.get(DocServer.Util.prepend(RouteStore.getUserDocumentId));
+ const res = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId));
if (!res) {
throw new Error("No user id returned");
}
- const field = await DocServer.getRefField(res);
+ const field = await DocServer.GetRefField(res);
let pending: Opt<Doc>;
if (field instanceof Doc) {
pending = await Cast(field.optionalRightCollection, Doc);
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 9da8912fe..0fe3f76e3 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -2,12 +2,14 @@ import { observable, action } from "mobx";
import { serializable, primitive, map, alias, list } from "serializr";
import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper";
import { DocServer } from "../client/DocServer";
-import { setter, getter, getField, updateFunction, deleteProperty } from "./util";
+import { setter, getter, getField, updateFunction, deleteProperty, makeEditable, makeReadOnly } from "./util";
import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast } from "./Types";
import { listSpec } from "./Schema";
import { ObjectField } from "./ObjectField";
import { RefField, FieldId } from "./RefField";
import { ToScriptString, SelfProxy, Parent, OnUpdate, Self, HandleUpdate, Update, Id } from "./FieldSymbols";
+import { scriptingGlobal } from "../client/util/Scripting";
+import { List } from "./List";
export namespace Field {
export function toScriptString(field: Field): string {
@@ -55,6 +57,7 @@ export function DocListCast(field: FieldResult): Doc[] {
export const WidthSym = Symbol("Width");
export const HeightSym = Symbol("Height");
+@scriptingGlobal
@Deserializable("doc").withFields(["id"])
export class Doc extends RefField {
constructor(id?: FieldId, forceSave?: boolean) {
@@ -80,7 +83,7 @@ export class Doc extends RefField {
});
this[SelfProxy] = doc;
if (!id || forceSave) {
- DocServer.createField(doc);
+ DocServer.CreateField(doc);
}
return doc;
}
@@ -108,7 +111,7 @@ export class Doc extends RefField {
private ___fields: any = {};
private [Update] = (diff: any) => {
- DocServer.emitFieldUpdate(this[Id], diff);
+ DocServer.UpdateField(this[Id], diff);
}
private [Self] = this;
@@ -132,6 +135,16 @@ export class Doc extends RefField {
this[fKey] = value;
}
}
+ const unset = diff.$unset;
+ if (unset) {
+ for (const key in unset) {
+ if (!key.startsWith("fields.")) {
+ continue;
+ }
+ const fKey = key.substring(7);
+ delete this[fKey];
+ }
+ }
}
}
@@ -146,6 +159,15 @@ export namespace Doc {
// return Cast(field, ctor);
// });
// }
+ export function MakeReadOnly(): { end(): void } {
+ makeReadOnly();
+ return {
+ end() {
+ makeEditable();
+ }
+ };
+ }
+
export function Get(doc: Doc, key: string, ignoreProto: boolean = false): FieldResult {
const self = doc[Self];
return getField(self, key, ignoreProto);
@@ -156,6 +178,14 @@ export namespace Doc {
export function IsPrototype(doc: Doc) {
return GetT(doc, "isPrototype", "boolean", true);
}
+ export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) {
+ let hasProto = doc.proto instanceof Doc;
+ let onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1;
+ let onProto = hasProto && Object.getOwnPropertyNames(doc.proto).indexOf(key) !== -1;
+ if (onDeleg || !hasProto || (!onProto && !defaultProto)) {
+ doc[key] = value;
+ } else doc.proto![key] = value;
+ }
export async function SetOnPrototype(doc: Doc, key: string, value: Field) {
const proto = Object.getOwnPropertyNames(doc).indexOf("isPrototype") === -1 ? doc.proto : doc;
@@ -198,7 +228,8 @@ export namespace Doc {
}
// compare whether documents or their protos match
- export function AreProtosEqual(doc: Doc, other: Doc) {
+ export function AreProtosEqual(doc?: Doc, other?: Doc) {
+ if (!doc || !other) return false;
let r = (doc === other);
let r2 = (doc.proto === other);
let r3 = (other.proto === doc);
@@ -208,7 +239,7 @@ export namespace Doc {
// gets the document's prototype or returns the document if it is a prototype
export function GetProto(doc: Doc) {
- return Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : doc.proto!;
+ return Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc);
}
export function allKeys(doc: Doc): string[] {
@@ -223,13 +254,91 @@ export namespace Doc {
return Array.from(results);
}
+ export function AddDocToList(target: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean) {
+ if (target[key] === undefined) {
+ Doc.GetProto(target)[key] = new List<Doc>();
+ }
+ let list = Cast(target[key], listSpec(Doc));
+ if (list) {
+ if (allowDuplicates !== true) {
+ let pind = list.reduce((l, d, i) => d instanceof Doc && Doc.AreProtosEqual(d, doc) ? i : l, -1);
+ if (pind !== -1) {
+ list.splice(pind, 1);
+ }
+ }
+ if (first) list.splice(0, 0, doc);
+ else {
+ let ind = relativeTo ? list.indexOf(relativeTo) : -1;
+ if (ind === -1) list.push(doc);
+ else list.splice(before ? ind : ind + 1, 0, doc);
+ }
+ }
+ return true;
+ }
+
+ export function ComputeContentBounds(doc: Doc) {
+ let bounds = DocListCast(doc.data).reduce((bounds, doc) => {
+ var [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)];
+ let [bptX, bptY] = [sptX + doc[WidthSym](), sptY + doc[HeightSym]()];
+ return {
+ x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
+ r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
+ };
+ }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE });
+ return bounds;
+ }
+
+ //
+ // Resolves a reference to a field by returning 'doc' if field extension is specified,
+ // otherwise, it returns the extension document stored in doc.<fieldKey>_ext.
+ // This mechanism allows any fields to be extended with an extension document that can
+ // be used to capture field-specific metadata. For example, an image field can be extended
+ // to store annotations, ink, and other data.
+ //
+ export function resolvedFieldDataDoc(doc: Doc, fieldKey: string, fieldExt: string) {
+ return fieldExt && doc[fieldKey + "_ext"] instanceof Doc ? doc[fieldKey + "_ext"] as Doc : doc;
+ }
+ export function UpdateDocumentExtensionForField(doc: Doc, fieldKey: string) {
+ if (doc[fieldKey + "_ext"] === undefined) {
+ setTimeout(() => {
+ let docExtensionForField = new Doc(doc[Id] + fieldKey, true);
+ docExtensionForField.title = "Extension of " + doc.title + "'s field:" + fieldKey;
+ let proto: Doc | undefined = doc;
+ while (proto && !Doc.IsPrototype(proto)) {
+ proto = proto.proto;
+ }
+ (proto ? proto : doc)[fieldKey + "_ext"] = docExtensionForField;
+ }, 0);
+ }
+ }
export function MakeAlias(doc: Doc) {
- const alias = new Doc;
if (!GetT(doc, "isPrototype", "boolean", true)) {
- alias.proto = doc.proto;
+ return Doc.MakeCopy(doc);
}
- return alias;
+ return Doc.MakeDelegate(doc); // bcz?
+ }
+
+ export function expandTemplateLayout(templateLayoutDoc: Doc, dataDoc?: Doc) {
+ let resolvedDataDoc = (templateLayoutDoc !== dataDoc) ? dataDoc : undefined;
+ if (!dataDoc || !(templateLayoutDoc && !(Cast(templateLayoutDoc.layout, Doc) instanceof Doc) && resolvedDataDoc && resolvedDataDoc !== templateLayoutDoc)) {
+ return templateLayoutDoc;
+ }
+ // if we have a data doc that doesn't match the layout, then we're rendering a template.
+ // ... which means we change the layout to be an expanded view of the template layout.
+ // This allows the view override the template's properties and be referenceable as its own document.
+
+ let expandedTemplateLayout = templateLayoutDoc["_expanded_" + dataDoc[Id]];
+ if (expandedTemplateLayout instanceof Doc) {
+ return expandedTemplateLayout;
+ }
+ if (expandedTemplateLayout === undefined) {
+ setTimeout(() => {
+ templateLayoutDoc["_expanded_" + dataDoc[Id]] = Doc.MakeDelegate(templateLayoutDoc);
+ (templateLayoutDoc["_expanded_" + dataDoc[Id]] as Doc).title = templateLayoutDoc.title + " applied to " + dataDoc.title;
+ }, 0);
+ }
+ return templateLayoutDoc;
}
export function MakeCopy(doc: Doc, copyProto: boolean = false): Doc {
@@ -250,6 +359,7 @@ export namespace Doc {
}
}
});
+
return copy;
}
diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts
index f1e4c4721..a2133a990 100644
--- a/src/new_fields/List.ts
+++ b/src/new_fields/List.ts
@@ -7,6 +7,7 @@ import { ObjectField } from "./ObjectField";
import { RefField } from "./RefField";
import { ProxyField } from "./Proxy";
import { Self, Update, Parent, OnUpdate, SelfProxy, ToScriptString, Copy } from "./FieldSymbols";
+import { Scripting } from "../client/util/Scripting";
const listHandlers: any = {
/// Mutator methods
@@ -294,4 +295,6 @@ class ListImpl<T extends Field> extends ObjectField {
}
}
export type List<T extends Field> = ListImpl<T> & (T | (T extends RefField ? Promise<T> : never))[];
-export const List: { new <T extends Field>(fields?: T[]): List<T> } = ListImpl as any; \ No newline at end of file
+export const List: { new <T extends Field>(fields?: T[]): List<T> } = ListImpl as any;
+
+Scripting.addGlobal("List", List); \ No newline at end of file
diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts
index 230e4ab8b..38d874a68 100644
--- a/src/new_fields/Proxy.ts
+++ b/src/new_fields/Proxy.ts
@@ -48,23 +48,21 @@ export class ProxyField<T extends RefField> extends ObjectField {
private failed = false;
private promise?: Promise<any>;
- value(callback?: ((field: T | undefined) => void)): T | undefined | FieldWaiting {
+ value(): T | undefined | FieldWaiting {
if (this.cache) {
- callback && callback(this.cache);
return this.cache;
}
if (this.failed) {
return undefined;
}
if (!this.promise) {
- this.promise = DocServer.getRefField(this.fieldId).then(action((field: any) => {
+ this.promise = DocServer.GetRefField(this.fieldId).then(action((field: any) => {
this.promise = undefined;
this.cache = field;
if (field === undefined) this.failed = true;
return field;
}));
}
- callback && this.promise.then(callback);
return this.promise;
}
}
diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts
index 89d077a47..78a3a4067 100644
--- a/src/new_fields/RichTextField.ts
+++ b/src/new_fields/RichTextField.ts
@@ -2,7 +2,9 @@ import { ObjectField } from "./ObjectField";
import { serializable } from "serializr";
import { Deserializable } from "../client/util/SerializationHelper";
import { Copy, ToScriptString } from "./FieldSymbols";
+import { scriptingGlobal } from "../client/util/Scripting";
+@scriptingGlobal
@Deserializable("RichTextField")
export class RichTextField extends ObjectField {
@serializable(true)
diff --git a/src/new_fields/Schema.ts b/src/new_fields/Schema.ts
index 250f3c975..2355304d5 100644
--- a/src/new_fields/Schema.ts
+++ b/src/new_fields/Schema.ts
@@ -1,5 +1,8 @@
import { Interface, ToInterface, Cast, ToConstructor, HasTail, Head, Tail, ListSpec, ToType, DefaultFieldConstructor } from "./Types";
import { Doc, Field } from "./Doc";
+import { ObjectField } from "./ObjectField";
+import { RefField } from "./RefField";
+import { SelfProxy } from "./FieldSymbols";
type AllToInterface<T extends Interface[]> = {
1: ToInterface<Head<T>> & AllToInterface<Tail<T>>,
@@ -10,10 +13,16 @@ export const emptySchema = createSchema({});
export const Document = makeInterface(emptySchema);
export type Document = makeInterface<[typeof emptySchema]>;
+export interface InterfaceFunc<T extends Interface[]> {
+ (docs: Doc[]): makeInterface<T>[];
+ (): makeInterface<T>;
+ (doc: Doc): makeInterface<T>;
+}
+
export type makeInterface<T extends Interface[]> = AllToInterface<T> & Doc & { proto: Doc | undefined };
// export function makeInterface<T extends Interface[], U extends Doc>(schemas: T): (doc: U) => All<T, U>;
// export function makeInterface<T extends Interface, U extends Doc>(schema: T): (doc: U) => makeInterface<T, U>;
-export function makeInterface<T extends Interface[]>(...schemas: T): (doc?: Doc) => makeInterface<T> {
+export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFunc<T> {
let schema: Interface = {};
for (const s of schemas) {
for (const key in s) {
@@ -25,10 +34,19 @@ export function makeInterface<T extends Interface[]>(...schemas: T): (doc?: Doc)
const field = receiver.doc[prop];
if (prop in schema) {
const desc = (schema as any)[prop];
- if (typeof desc === "object" && "defaultVal" in desc && "type" in desc) {
+ if (typeof desc === "object" && "defaultVal" in desc && "type" in desc) {//defaultSpec
return Cast(field, desc.type, desc.defaultVal);
+ } else if (typeof desc === "function" && !ObjectField.isPrototypeOf(desc) && !RefField.isPrototypeOf(desc)) {
+ const doc = Cast(field, Doc);
+ if (doc === undefined) {
+ return undefined;
+ } else if (doc instanceof Doc) {
+ return desc(doc);
+ } else {
+ return doc.then(doc => doc && desc(doc));
+ }
} else {
- return Cast(field, (schema as any)[prop]);
+ return Cast(field, desc);
}
}
return field;
@@ -38,14 +56,22 @@ export function makeInterface<T extends Interface[]>(...schemas: T): (doc?: Doc)
return true;
}
});
- return function (doc?: Doc) {
- doc = doc || new Doc;
- if (!(doc instanceof Doc)) {
- throw new Error("Currently wrapping a schema in another schema isn't supported");
- }
+ const fn = (doc: Doc) => {
+ doc = doc[SelfProxy];
+ // if (!(doc instanceof Doc)) {
+ // throw new Error("Currently wrapping a schema in another schema isn't supported");
+ // }
const obj = Object.create(proto, { doc: { value: doc, writable: false } });
return obj;
};
+ return function (doc?: Doc | Doc[]) {
+ doc = doc || new Doc;
+ if (doc instanceof Doc) {
+ return fn(doc);
+ } else {
+ return doc.map(fn);
+ }
+ };
}
export type makeStrictInterface<T extends Interface> = Partial<ToInterface<T>>;
diff --git a/src/new_fields/ScriptField.ts b/src/new_fields/ScriptField.ts
new file mode 100644
index 000000000..e2994ed70
--- /dev/null
+++ b/src/new_fields/ScriptField.ts
@@ -0,0 +1,96 @@
+import { ObjectField } from "./ObjectField";
+import { CompiledScript, CompileScript, scriptingGlobal } from "../client/util/Scripting";
+import { Copy, ToScriptString, Parent, SelfProxy } from "./FieldSymbols";
+import { serializable, createSimpleSchema, map, primitive, object, deserialize, PropSchema, custom, SKIP } from "serializr";
+import { Deserializable } from "../client/util/SerializationHelper";
+import { Doc } from "../new_fields/Doc";
+
+function optional(propSchema: PropSchema) {
+ return custom(value => {
+ if (value !== undefined) {
+ return propSchema.serializer(value);
+ }
+ return SKIP;
+ }, (jsonValue: any, context: any, oldValue: any, callback: (err: any, result: any) => void) => {
+ if (jsonValue !== undefined) {
+ return propSchema.deserializer(jsonValue, callback, context, oldValue);
+ }
+ return SKIP;
+ });
+}
+
+const optionsSchema = createSimpleSchema({
+ requiredType: true,
+ addReturn: true,
+ typecheck: true,
+ readonly: true,
+ params: optional(map(primitive()))
+});
+
+const scriptSchema = createSimpleSchema({
+ options: object(optionsSchema),
+ originalScript: true
+});
+
+function deserializeScript(script: ScriptField) {
+ const comp = CompileScript(script.script.originalScript, script.script.options);
+ if (!comp.compiled) {
+ throw new Error("Couldn't compile loaded script");
+ }
+ (script as any).script = comp;
+}
+
+@scriptingGlobal
+@Deserializable("script", deserializeScript)
+export class ScriptField extends ObjectField {
+ @serializable(object(scriptSchema))
+ readonly script: CompiledScript;
+
+ constructor(script: CompiledScript) {
+ super();
+
+ this.script = script;
+ }
+
+ // init(callback: (res: Field) => any) {
+ // const options = this.options!;
+ // const keys = Object.keys(options.options.capturedIds);
+ // Server.GetFields(keys).then(fields => {
+ // let captured: { [name: string]: Field } = {};
+ // keys.forEach(key => captured[options.options.capturedIds[key]] = fields[key]);
+ // const opts: ScriptOptions = {
+ // addReturn: options.options.addReturn,
+ // params: options.options.params,
+ // requiredType: options.options.requiredType,
+ // capturedVariables: captured
+ // };
+ // const script = CompileScript(options.script, opts);
+ // if (!script.compiled) {
+ // throw new Error("Can't compile script");
+ // }
+ // this._script = script;
+ // callback(this);
+ // });
+ // }
+
+ [Copy](): ObjectField {
+ return new ScriptField(this.script);
+ }
+
+ [ToScriptString]() {
+ return "script field";
+ }
+}
+
+@scriptingGlobal
+@Deserializable("computed", deserializeScript)
+export class ComputedField extends ScriptField {
+ //TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc
+ value(doc: Doc) {
+ const val = this.script.run({ this: doc });
+ if (val.success) {
+ return val.result;
+ }
+ return undefined;
+ }
+} \ No newline at end of file
diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts
index c04dd5e6d..39d384d64 100644
--- a/src/new_fields/Types.ts
+++ b/src/new_fields/Types.ts
@@ -2,7 +2,7 @@ import { Field, Opt, FieldResult, Doc } from "./Doc";
import { List } from "./List";
import { RefField } from "./RefField";
-export type ToType<T extends ToConstructor<Field> | ListSpec<Field> | DefaultFieldConstructor<Field>> =
+export type ToType<T extends InterfaceValue> =
T extends "string" ? string :
T extends "number" ? number :
T extends "boolean" ? boolean :
@@ -10,7 +10,8 @@ export type ToType<T extends ToConstructor<Field> | ListSpec<Field> | DefaultFie
// T extends { new(...args: any[]): infer R } ? (R | Promise<R>) : never;
T extends DefaultFieldConstructor<infer _U> ? never :
T extends { new(...args: any[]): List<Field> } ? never :
- T extends { new(...args: any[]): infer R } ? R : never;
+ T extends { new(...args: any[]): infer R } ? R :
+ T extends (doc?: Doc) => infer R ? R : never;
export type ToConstructor<T extends Field> =
T extends string ? "string" :
@@ -38,14 +39,16 @@ export type Tail<T extends any[]> =
((...t: T) => any) extends ((_: any, ...tail: infer TT) => any) ? TT : [];
export type HasTail<T extends any[]> = T extends ([] | [any]) ? false : true;
+export type InterfaceValue = ToConstructor<Field> | ListSpec<Field> | DefaultFieldConstructor<Field> | ((doc?: Doc) => any);
//TODO Allow you to optionally specify default values for schemas, which should then make that field not be partial
export interface Interface {
- [key: string]: ToConstructor<Field> | ListSpec<Field> | DefaultFieldConstructor<Field>;
+ [key: string]: InterfaceValue;
// [key: string]: ToConstructor<Field> | ListSpec<Field[]>;
}
+export type WithoutRefField<T extends Field> = T extends RefField ? never : T;
export function Cast<T extends ToConstructor<Field> | ListSpec<Field>>(field: FieldResult, ctor: T): FieldResult<ToType<T>>;
-export function Cast<T extends ToConstructor<Field> | ListSpec<Field>>(field: FieldResult, ctor: T, defaultVal: WithoutList<ToType<T>> | null): WithoutList<ToType<T>>;
+export function Cast<T extends ToConstructor<Field> | ListSpec<Field>>(field: FieldResult, ctor: T, defaultVal: WithoutList<WithoutRefField<ToType<T>>> | null): WithoutList<ToType<T>>;
export function Cast<T extends ToConstructor<Field> | ListSpec<Field>>(field: FieldResult, ctor: T, defaultVal?: ToType<T> | null): FieldResult<ToType<T>> | undefined {
if (field instanceof Promise) {
return defaultVal === undefined ? field.then(f => Cast(f, ctor) as any) as any : defaultVal === null ? undefined : defaultVal;
diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts
index 4a2841fb6..d935a61af 100644
--- a/src/new_fields/URLField.ts
+++ b/src/new_fields/URLField.ts
@@ -2,6 +2,7 @@ import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, custom } from "serializr";
import { ObjectField } from "./ObjectField";
import { ToScriptString, Copy } from "./FieldSymbols";
+import { Scripting, scriptingGlobal } from "../client/util/Scripting";
function url() {
return custom(
@@ -37,8 +38,8 @@ export abstract class URLField extends ObjectField {
}
}
-@Deserializable("audio") export class AudioField extends URLField { }
-@Deserializable("image") export class ImageField extends URLField { }
-@Deserializable("video") export class VideoField extends URLField { }
-@Deserializable("pdf") export class PdfField extends URLField { }
-@Deserializable("web") export class WebField extends URLField { } \ No newline at end of file
+@scriptingGlobal @Deserializable("audio") export class AudioField extends URLField { }
+@scriptingGlobal @Deserializable("image") export class ImageField extends URLField { }
+@scriptingGlobal @Deserializable("video") export class VideoField extends URLField { }
+@scriptingGlobal @Deserializable("pdf") export class PdfField extends URLField { }
+@scriptingGlobal @Deserializable("web") export class WebField extends URLField { } \ No newline at end of file
diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts
index 8caceb063..b5e50d501 100644
--- a/src/new_fields/util.ts
+++ b/src/new_fields/util.ts
@@ -2,13 +2,17 @@ import { UndoManager } from "../client/util/UndoManager";
import { Doc, Field } from "./Doc";
import { SerializationHelper } from "../client/util/SerializationHelper";
import { ProxyField } from "./Proxy";
-import { FieldValue } from "./Types";
import { RefField } from "./RefField";
import { ObjectField } from "./ObjectField";
import { action } from "mobx";
-import { Parent, OnUpdate, Update, Id } from "./FieldSymbols";
+import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols";
+import { ComputedField } from "./ScriptField";
-export const setter = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
+function _readOnlySetter(): never {
+ throw new Error("Documents can't be modified in read-only mode");
+}
+const _setterImpl = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
+ //console.log("-set " + target[SelfProxy].title + "(" + target[SelfProxy][prop] + ")." + prop.toString() + " = " + value);
if (SerializationHelper.IsSerializing()) {
target[prop] = value;
return true;
@@ -17,6 +21,9 @@ export const setter = action(function (target: any, prop: string | symbol | numb
target[prop] = value;
return true;
}
+ if (value !== undefined) {
+ value = value[SelfProxy] || value;
+ }
const curValue = target.__fields[prop];
if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) {
// TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically
@@ -27,11 +34,10 @@ export const setter = action(function (target: any, prop: string | symbol | numb
value = new ProxyField(value);
}
if (value instanceof ObjectField) {
- //TODO Instead of target, maybe use target[Self]
- if (value[Parent] && value[Parent] !== target) {
+ if (value[Parent] && value[Parent] !== receiver) {
throw new Error("Can't put the same object in multiple documents at the same time");
}
- value[Parent] = target;
+ value[Parent] = receiver;
value[OnUpdate] = updateFunction(target, prop, value, receiver);
}
if (curValue instanceof ObjectField) {
@@ -43,7 +49,8 @@ export const setter = action(function (target: any, prop: string | symbol | numb
} else {
target.__fields[prop] = value;
}
- target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } });
+ if (value === undefined) target[Update]({ '$unset': { ["fields." + prop]: "" } });
+ else target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } });
UndoManager.AddEvent({
redo: () => receiver[prop] = value,
undo: () => receiver[prop] = curValue
@@ -51,6 +58,20 @@ export const setter = action(function (target: any, prop: string | symbol | numb
return true;
});
+let _setter: (target: any, prop: string | symbol | number, value: any, receiver: any) => boolean = _setterImpl;
+
+export function makeReadOnly() {
+ _setter = _readOnlySetter;
+}
+
+export function makeEditable() {
+ _setter = _setterImpl;
+}
+
+export function setter(target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
+ return _setter(target, prop, value, receiver);
+}
+
export function getter(target: any, prop: string | symbol | number, receiver: any): any {
if (typeof prop === "symbol") {
return target.__fields[prop] || target[prop];
@@ -58,37 +79,30 @@ export function getter(target: any, prop: string | symbol | number, receiver: an
if (SerializationHelper.IsSerializing()) {
return target[prop];
}
- return getField(target, prop);
+ return getFieldImpl(target, prop, receiver);
}
-function getProtoField(protoField: Doc | undefined, prop: string | number, cb?: (field: Field | undefined) => void) {
- if (!protoField) return undefined;
- let field = protoField[prop];
- if (field instanceof Promise) {
- cb && field.then(cb);
- return field;
- } else {
- cb && cb(field);
- return field;
- }
-}
-
-//TODO The callback parameter is never being passed in currently, so we should be able to get rid of it.
-export function getField(target: any, prop: string | number, ignoreProto: boolean = false, callback?: (field: Field | undefined) => void): any {
+function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreProto: boolean = false): any {
+ receiver = receiver || target[SelfProxy];
const field = target.__fields[prop];
if (field instanceof ProxyField) {
- return field.value(callback);
+ return field.value();
+ }
+ if (field instanceof ComputedField) {
+ return field.value(receiver);
}
if (field === undefined && !ignoreProto && prop !== "proto") {
- const proto = getField(target, "proto", true);
+ const proto = getFieldImpl(target, "proto", receiver, true);//TODO tfs: instead of receiver we could use target[SelfProxy]... I don't which semantics we want or if it really matters
if (proto instanceof Doc) {
- return getProtoField(proto, prop, callback);
- } else if (proto instanceof Promise) {
- return proto.then(async proto => getProtoField(proto, prop, callback));
+ return getFieldImpl(proto[Self], prop, receiver, ignoreProto);
}
+ return undefined;
}
- callback && callback(field);
return field;
+
+}
+export function getField(target: any, prop: string | number, ignoreProto: boolean = false): any {
+ return getFieldImpl(target, prop, undefined, ignoreProto);
}
export function deleteProperty(target: any, prop: string | number | symbol) {
@@ -96,7 +110,8 @@ export function deleteProperty(target: any, prop: string | number | symbol) {
delete target[prop];
return true;
}
- throw new Error("Currently properties can't be deleted from documents, assign to undefined instead");
+ target[SelfProxy][prop] = undefined;
+ return true;
}
export function updateFunction(target: any, prop: any, value: any, receiver: any) {
diff --git a/src/scraping/acm/.gitignore b/src/scraping/acm/.gitignore
new file mode 100644
index 000000000..caca8b99c
--- /dev/null
+++ b/src/scraping/acm/.gitignore
@@ -0,0 +1,2 @@
+./citations.txt
+./results.txt \ No newline at end of file
diff --git a/src/scraping/acm/chromedriver b/src/scraping/acm/chromedriver
new file mode 100755
index 000000000..9e9b16717
--- /dev/null
+++ b/src/scraping/acm/chromedriver
Binary files differ
diff --git a/src/scraping/acm/index.js b/src/scraping/acm/index.js
index 81f775617..b71d55226 100644
--- a/src/scraping/acm/index.js
+++ b/src/scraping/acm/index.js
@@ -1,88 +1,279 @@
-const { Builder, By, Key, until } = require('selenium-webdriver');
-const fs = require("fs");
+const {
+ Builder,
+ By
+} = require('selenium-webdriver');
+const {
+ readFile,
+ writeFile
+} = require('fs');
-let driver;
-fs.readFile("./citations.txt", { encoding: "utf8" }, scrapeTargets);
-results = []
+const target_source = './citations.txt';
+const target_browser = 'chrome';
+const target_dist = './results.txt';
-async function scrapeTargets(error, data) {
- if (error) {
- console.log("\nUnable to collect target citations from a citations.txt file stored in this directory.\nPlease make sure one is provided.")
- return;
+const driver_pause = 500; // milliseconds
+const sample_line_char_max = 100; // characters
+
+const tab_map = {
+ abstract: "11",
+ authors: "14",
+ references: "15",
+ cited_by: "16",
+ index_terms: "17",
+ publication: "18",
+ reviews: "19",
+ comments: "20",
+ table_of_contents: "21"
+};
+
+String.prototype.removeAll = function (replacements, trim = true) {
+ let result = this;
+ for (let expression of replacements) {
+ result = result.replace(expression, "");
}
+ return trim ? result.trim() : result;
+};
- driver = await new Builder().forBrowser('chrome').build();
+String.prototype.remove = function (replacement, trim = true) {
+ let result = this.replace(replacement, "");
+ return trim ? result.trim() : result;
+};
- let references = data.split("\n").map(entry => entry.replace("\r", "")).filter(line => line.match(/\d+/g));
+Object.prototype.first = function () {
+ return this[Object.keys(this)[0]];
+};
- let results = []
- let pdfs = []
- for (let id of references) {
- let result = {}
- let lines = []
- try {
- let url = `https://dl.acm.org/citation.cfm?id=${id}`;
- await driver.get(url);
- await driver.sleep(500)
- let candidates = await driver.findElements(By.xpath('.//a[contains(@href, "ft_gateway.cfm?id=")]'));
- if (candidates.length > 0) {
- pdfs.push(candidates[0])
- }
- let webElements = await driver.findElements(By.id("abstract-body"))
- for (let el of webElements) {
- let text = await el.getText()
- lines.push(text)
- }
- result.url = url
- result.abstract = lines.join(" ");
- await driver.findElement(By.xpath(`//*[@id="tab-1014-btnInnerEl"]/span`)).click()
- await driver.sleep(500)
- let authors = await driver.findElement(By.xpath('//*[@id="tabpanel-1009-body"]')).getText()
- let sanitize = line => line.length > 0 && !(line.startsWith("No contact information") || line.startsWith("View colleagues of") || line.startsWith("Bibliometrics:"))
- authorLines = authors.split("\n").map(line => line.trim()).filter(sanitize)
-
- let i = 0;
- let allAuthors = []
- while (i < authorLines.length) {
- let individual = [];
- while (!authorLines[i].startsWith("Average citations")) {
- individual.push(authorLines[i])
- i++
+// GENERAL UTILITY FUNCTIONS
+
+function log_read(content) {
+ process.stdout.write("reading " + content + "...");
+}
+
+function log_snippet(result, quotes = true) {
+ let snippet = "failed to create snippet";
+ switch (typeof result) {
+ case "string":
+ let ellipse = result.length > sample_line_char_max;
+ let i = sample_line_char_max;
+ if (ellipse) {
+ while (result[i] != " " && i < -1) {
+ i--;
}
- individual.push(authorLines[i])
- allAuthors.push(individual);
- i++
}
- result.authors = allAuthors.map(metadata => {
- let publicationYears = metadata[1].substring(18).split("-");
- author = {
- name: metadata[0],
- publication_start: parseInt(publicationYears[0]),
- publication_end: parseInt(publicationYears[1])
- };
- for (let count = 2; count < metadata.length; count++) {
- let attr = metadata[count]
- let char = attr.length - 1;
- while (attr[char] != " ") {
- char--
+ snippet = `${result.substring(0, i + 1).trim()}${ellipse ? "..." : ""}`;
+ snippet = quotes ? `"${snippet}"` : snippet;
+ break;
+ case "object":
+ if (Array.isArray(result)) {
+ snippet = result.map(res => {
+ switch (typeof res) {
+ case "string":
+ return res.substring(0, sample_line_char_max / result.length);
+ case "object":
+ return res.first();
}
- let key = attr.substring(0, char).toLowerCase().replace(/ /g, "_").replace(/[\(\)]/g, "");
- let value = parseFloat(attr.substring(char + 1).replace(/,/g, ""));
- author[key] = value
- }
- return author
- })
- } catch (e) {
- console.log(e)
- await driver.quit();
+ }).join(', ');
+ } else {
+ snippet = result.first();
+ }
+ }
+ console.log(snippet);
+ return result;
+}
+
+// DRIVER UTILITY FUNCTIONS
+
+async function navigate_to(url) {
+ await driver.get(url);
+ await driver.sleep(driver_pause);
+}
+
+async function click_on(ref) {
+ await (await locate(ref)).click();
+ await driver.sleep(driver_pause);
+}
+
+async function click_on_acm_tab(target) {
+ await click_on(`//*[@id="tab-10${tab_map[target]}-btnInnerEl"]/span`);
+}
+
+async function locate(ref, multiple = false) {
+ let locator = ref.startsWith("//") ? By.xpath(ref) : By.id(ref);
+ return await multiple ? driver.findElements(locator) : driver.findElement(locator);
+}
+
+async function text_of(ref) {
+ let element = await locate(ref);
+ return await element.getText();
+}
+
+async function text_of_all(ref, delimiter = undefined) {
+ let elements = await locate(ref, true);
+ let results = [];
+ for (let element of elements) {
+ results.push(await element.getText());
+ }
+ return delimiter ? results.join(delimiter) : results;
+}
+
+async function logged_assign(key, value) {
+ log_read(key);
+ result[key] = log_snippet(value);
+}
+
+// TEXT SCRAPING
+
+async function read_authors() {
+ let authors = await text_of('//*[@id="tabpanel-1009-body"]');
+ let sanitize = line => line.length > 0 && !(line.startsWith("No contact information") || line.startsWith("View colleagues of") || line.startsWith("Bibliometrics:"));
+ let author_lines = authors.split("\n").map(line => line.trim()).filter(sanitize);
+
+ let all_authors = [];
+ let i = 0;
+ while (i < author_lines.length) {
+ let individual = [];
+ while (!author_lines[i].startsWith("Average citations")) {
+ individual.push(author_lines[i]);
+ i++;
+ }
+ individual.push(author_lines[i]);
+ all_authors.push(individual);
+ i++;
+ }
+
+ return all_authors.map(parse_author);
+}
+
+async function read_publication() {
+ let publciation_elements = (await text_of("source-body")).split("\n");
+ let publication_module = {};
+
+ let extract = (regex, target, index = 1) => regex.exec(target)[index];
+
+ for (let element of publciation_elements) {
+
+ let location = /Volume (\d+) Issue (\d+), ([\w.\d]+)/g;
+ let pages = /(\d+)-(\d+)/g;
+ let publication_date = /(\d{4}-\d{2}-\d{2})/g;
+ let publisher = /Publisher (.*)/g;
+ let issn = /ISSN: (\d{4}-\d{4})/g;
+ let eissn = /EISSN: ([\dA-Z]{4}-[\dA-Z]{4})/g;
+ let doi = /doi>([\.\d\/A-Z]+)/g;
+
+ if (element.startsWith("Title")) {
+ publication_module.name = element.substring(6).removeAll(["table of contents", "archive", /\w+ Homepage/]);
+ } else if (element.startsWith("Volume ")) {
+ let match = location.exec(element);
+ publication_module.volume = parseInt(match[1]);
+ publication_module.issue = parseInt(match[2]);
+ publication_module.month = match[3];
+ } else if (element.startsWith("Pages ")) {
+ let match = pages.exec(element);
+ publication_module.page_start = parseInt(match[1]);
+ publication_module.page_end = parseInt(match[2]);
+ } else if (element.startsWith("Publication Date ")) {
+ publication_module.publication_date = extract(publication_date, element);
+ } else if (element.startsWith("Publisher ")) {
+ publication_module.publisher = extract(publisher, element);
+ } else if (element.startsWith("ISSN: ")) {
+ publication_module.issn = extract(issn, element);
+ if (element.includes("EISSN: ")) {
+ publication_module.eissn = extract(eissn, element);
+ }
+ publication_module.doi = extract(doi, element);
}
- results.push(result)
}
+ return publication_module;
+}
+
+// JSON / DASH CONVERSION AND EXPORT
+function parse_author(metadata) {
+ let publicationYears = metadata[1].substring(18).split("-");
+ author = {
+ name: metadata[0],
+ publication_start: parseInt(publicationYears[0]),
+ publication_end: parseInt(publicationYears[1])
+ };
+ for (let count = 2; count < metadata.length; count++) {
+ let attr = metadata[count];
+ let char = attr.length - 1;
+ while (attr[char] != " ") {
+ char--;
+ }
+ let key = attr.substring(0, char).toLowerCase().replace(/ /g, "_").remove(/[\(\)]/g);
+ let value = parseFloat(attr.substring(char + 1).remove(/,/g));
+ author[key] = value;
+ }
+ return author;
+}
+
+function write_results() {
+ console.log();
let output = "";
results.forEach(res => output += (JSON.stringify(res, null, 4) + "\n"));
- fs.writeFile("./results.txt", output, function errorHandler(exception) { console.log(exception || "results successfully written") })
+ writeFile(target_dist, output, function errorHandler(exception) {
+ console.log(exception || "scraped references successfully written as JSON to ./results.txt");
+ });
+}
+
+async function scrape_targets(error, data) {
+ if (error) {
+ console.log("\nUnable to collect target citations from a citations.txt file stored in this directory.\nPlease make sure one is provided.");
+ return;
+ }
+
+ let references = data.split("\n").map(entry => entry.removeAll(["\r"])).filter(line => line.match(/\d+/g));
+ let quota = references.length;
+ log_snippet(`found ${quota} references to scrape`, false);
+
+ driver = await new Builder().forBrowser(target_browser).build();
+
+ for (let i = 0; i < quota; i++) {
+ try {
+ result = {};
+ let target;
+
+ let id = references[i];
+ let url = `https://dl.acm.org/citation.cfm?id=${id}`;
+ console.log(`\nscraping ${i + 1}/${quota} (${id})`);
+
+ await navigate_to(url);
+
+ logged_assign("url", url);
+ logged_assign("title", await text_of('//*[@id="divmain"]/div/h1'));
+
+ target = "abstract";
+ await click_on_acm_tab(target);
+ logged_assign(target, await text_of_all("abstract-body", " "));
+
+ target = "authors";
+ await click_on_acm_tab(target);
+ logged_assign(target, await read_authors());
+
+ target = "publication";
+ await click_on_acm_tab(target);
+ logged_assign(target, await read_publication());
+ } catch (e) {
+ console.log(e);
+ await driver.quit();
+ }
+
+ results.push(result);
+ }
+
+ write_results();
await driver.quit();
-} \ No newline at end of file
+}
+
+let driver;
+let results = [];
+let result = {};
+
+log_read("target references");
+
+readFile(target_source, {
+ encoding: "utf8"
+}, scrape_targets);
diff --git a/src/scraping/acm/results.txt b/src/scraping/acm/results.txt
index 05bb2be8b..a15da8b10 100644
--- a/src/scraping/acm/results.txt
+++ b/src/scraping/acm/results.txt
@@ -1,5 +1,6 @@
{
"url": "https://dl.acm.org/citation.cfm?id=321046",
+ "title": "Integer Programming Formulation of Traveling Salesman Problems",
"abstract": "It has been observed by many people that a striking number of quite diverse mathematical problems can be formulated as problems in integer programming, that is, linear programming problems in which some or all of the variables are required to assume integral values. This fact is rendered quite interesting by recent research on such problems, notably by R. E. Gomory [2, 3], which gives promise of yielding efficient computational techniques for their solution. The present paper provides yet another example of the versatility of integer programming as a mathematical modeling device by representing a generalization of the well-known “Travelling Salesman Problem” in integer programming terms. The authors have developed several such models, of which the one presented here is the most efficient in terms of generality, number of variables, and number of constraints. This model is due to the second author [4] and was presented briefly at the Symposium on Combinatorial Problems held at Princeton University, April 1960, sponsored by SIAM and IBM. The problem treated is: (1) A salesman is required to visit each of n cities, indexed by 1, … , n. He leaves from a “base city” indexed by 0, visits each of the n other cities exactly once, and returns to city 0. During his travels he must return to 0 exactly t times, including his final return (here t may be allowed to vary), and he must visit no more than p cities in one tour. (By a tour we mean a succession of visits to cities without stopping at city 0.) It is required to find such an itinerary which minimizes the total distance traveled by the salesman. Note that if t is fixed, then for the problem to have a solution we must have tp ≧ n. For t = 1, p ≧ n, we have the standard traveling salesman problem. Let dij (i ≠ j = 0, 1, … , n) be the distance covered in traveling from city i to city j. The following integer programming problem will be shown to be equivalent to (1): (2) Minimize the linear form ∑0≦i≠j≦n∑ dijxij over the set determined by the relations ∑ni=0i≠j xij = 1 (j = 1, … , n) ∑nj=0j≠i xij = 1 (i = 1, … , n) ui - uj + pxij ≦ p - 1 (1 ≦ i ≠ j ≦ n) where the xij are non-negative integers and the ui (i = 1, …, n) are arbitrary real numbers. (We shall see that it is permissible to restrict the ui to be non-negative integers as well.) If t is fixed it is necessary to add the additional relation: ∑nu=1 xi0 = t Note that the constraints require that xij = 0 or 1, so that a natural correspondence between these two problems exists if the xij are interpreted as follows: The salesman proceeds from city i to city j if and only if xij = 1. Under this correspondence the form to be minimized in (2) is the total distance to be traveled by the salesman in (1), so the burden of proof is to show that the two feasible sets correspond; i.e., a feasible solution to (2) has xij which do define a legitimate itinerary in (1), and, conversely a legitimate itinerary in (1) defines xij, which, together with appropriate ui, satisfy the constraints of (2). Consider a feasible solution to (2). The number of returns to city 0 is given by ∑ni=1 xi0. The constraints of the form ∑ xij = 1, all xij non-negative integers, represent the conditions that each city (other than zero) is visited exactly once. The ui play a role similar to node potentials in a network and the inequalities involving them serve to eliminate tours that do not begin and end at city 0 and tours that visit more than p cities. Consider any xr0r1 = 1 (r1 ≠ 0). There exists a unique r2 such that xr1r2 = 1. Unless r2 = 0, there is a unique r3 with xr2r3 = 1. We proceed in this fashion until some rj = 0. This must happen since the alternative is that at some point we reach an rk = rj, j + 1 < k. Since none of the r's are zero we have uri - uri + 1 + pxriri + 1 ≦ p - 1 or uri - uri + 1 ≦ - 1. Summing from i = j to k - 1, we have urj - urk = 0 ≦ j + 1 - k, which is a contradiction. Thus all tours include city 0. It remains to observe that no tours is of length greater than p. Suppose such a tour exists, x0r1 , xr1r2 , … , xrprp+1 = 1 with all ri ≠ 0. Then, as before, ur1 - urp+1 ≦ - p or urp+1 - ur1 ≧ p. But we have urp+1 - ur1 + pxrp+1r1 ≦ p - 1 or urp+1 - ur1 ≦ p (1 - xrp+1r1) - 1 ≦ p - 1, which is a contradiction. Conversely, if the xij correspond to a legitimate itinerary, it is clear that the ui can be adjusted so that ui = j if city i is the jth city visited in the tour which includes city i, for we then have ui - uj = - 1 if xij = 1, and always ui - uj ≦ p - 1. The above integer program involves n2 + n constraints (if t is not fixed) in n2 + 2n variables. Since the inequality form of constraint is fundamental for integer programming calculations, one may eliminate 2n variables, say the xi0 and x0j, by means of the equation constraints and produce an equivalent problem with n2 + n inequalities and n2 variables. The currently known integer programming procedures are sufficiently regular in their behavior to cast doubt on the heuristic value of machine experiments with our model. However, it seems appropriate to report the results of the five machine experiments we have conducted so far. The solution procedure used was the all-integer algorithm of R. E. Gomory [3] without the ranking procedure he describes. The first three experiments were simple model verification tests on a four-city standard traveling salesman problem with distance matrix [ 20 23 4 30 7 27 25 5 25 3 21 26 ] The first experiment was with a model, now obsolete, using roughly twice as many constraints and variables as the current model (for this problem, 28 constraints in 21 variables). The machine was halted after 4000 pivot steps had failed to produce a solution. The second experiment used the earlier model with the xi0 and x0j eliminated, resulting in a 28-constraint, 15-variable problem. Here the machine produced the optimal solution in 41 pivot steps. The third experiment used the current formulation with the xi0 and x0j eliminated, yielding 13 constraints and 9 variables. The optimal solution was reached in 7 pivot steps. The fourth and fifth experiments were used on a standard ten-city problem, due to Barachet, solved by Dantzig, Johnson and Fulkerson [1]. The current formulation was used, yielding 91 constraints in 81 variables. The fifth problem differed from the fourth only in that the ordering of the rows was altered to attempt to introduce more favorable pivot choices. In each case the machine was stopped after over 250 pivot steps had failed to produce the solution. In each case the last 100 pivot steps had failed to change the value of the objective function. It seems hopeful that more efficient integer programming procedures now under development will yield a satisfactory algorithmic solution to the traveling salesman problem, when applied to this model. In any case, the model serves to illustrate how problems of this sort may be succinctly formulated in integer programming terms.",
"authors": [
{
@@ -9,10 +10,10 @@
"publication_count": 1,
"citation_count": 179,
"available_for_download": 1,
- "downloads_6_weeks": 132,
- "downloads_12_months": 993,
- "downloads_cumulative": 9781,
- "average_downloads_per_article": 9781,
+ "downloads_6_weeks": 124,
+ "downloads_12_months": 923,
+ "downloads_cumulative": 9794,
+ "average_downloads_per_article": 9794,
"average_citations_per_article": 179
},
{
@@ -22,10 +23,10 @@
"publication_count": 5,
"citation_count": 196,
"available_for_download": 1,
- "downloads_6_weeks": 132,
- "downloads_12_months": 993,
- "downloads_cumulative": 9781,
- "average_downloads_per_article": 9781,
+ "downloads_6_weeks": 124,
+ "downloads_12_months": 923,
+ "downloads_cumulative": 9794,
+ "average_downloads_per_article": 9794,
"average_citations_per_article": 39.2
},
{
@@ -35,16 +36,30 @@
"publication_count": 2,
"citation_count": 188,
"available_for_download": 2,
- "downloads_6_weeks": 132,
- "downloads_12_months": 998,
- "downloads_cumulative": 10012,
- "average_downloads_per_article": 5006,
+ "downloads_6_weeks": 124,
+ "downloads_12_months": 928,
+ "downloads_cumulative": 10025,
+ "average_downloads_per_article": 5012.5,
"average_citations_per_article": 94
}
- ]
+ ],
+ "publication": {
+ "name": "Journal of the ACM (JACM)",
+ "volume": 7,
+ "issue": 4,
+ "month": "Oct.",
+ "page_start": 326,
+ "page_end": 329,
+ "publication_date": "1960-10-01",
+ "publisher": "ACM New York, NY, USA",
+ "issn": "0004-5411",
+ "eissn": "1557-735X",
+ "doi": "10.1145/321043.321046"
+ }
}
{
"url": "https://dl.acm.org/citation.cfm?id=2412979",
+ "title": "STRUCT programming analysis system",
"abstract": "The STRUCT system utilizes the flexibility of a powerful graphics display system to provide a set of tools for program analysis. These tools allow the analysis of the static prograin structure and the dynamic execution behavior. of programs within the entire operating system/user program environment of the Brown University Graphics System (BUGS). Information is collected and presented in a manner which fully exploits two aspects of this environment. First, the operating system has been developed in a well-structured hierarcal manner following principles laid down by other researchers (2), (3). Second the programs under analysis have been written in a structured programming language following coding conventions which make available, at the source code level, valuable program control information. A new set of pictorial constructs is introduced for presenting a. program structure (static or dynamic) for inspection. These constructs combine the best features of an indented structured source code listing and the box odented nature of traditional flow charts. The graphical tools available are USed to provide for swift changes in. the desired level of detail displayed within a program structure, for traveling linearly through a program structure, for traveling through a complex program structure (following subroutine or system calls), for concurrently viewing multiple related program structures, and for presenting dynamic program behavior data using three-dimensional projections, The volume of a three-dimensional box representing a program block is proportional to the block's resource utilization. The scope of this paper is limited to a description of the STRUCT system. This system is currently being used to predict and analyze the performance advantages available through the migration of function (program modules) between levels of software and between software and firmware within BUGS. The results of this research on migration will be included in a doctoral dissertation currently being written.",
"authors": [
{
@@ -60,5 +75,17 @@
"average_downloads_per_article": 0,
"average_citations_per_article": 0
}
- ]
+ ],
+ "publication": {
+ "name": "IEEE Transactions on Software Engineering",
+ "volume": 1,
+ "issue": 1,
+ "month": "March",
+ "page_start": 384,
+ "page_end": 389,
+ "publication_date": "1975-03-01",
+ "publisher": "IEEE Press Piscataway, NJ, USA",
+ "issn": "0098-5589",
+ "doi": "10.1109/TSE.1975.6312869"
+ }
}
diff --git a/src/scraping/buxton/scraper.py b/src/scraping/buxton/scraper.py
index 97af10519..700269727 100644
--- a/src/scraping/buxton/scraper.py
+++ b/src/scraping/buxton/scraper.py
@@ -26,7 +26,7 @@ def extract_links(fileName):
item = rels[rel]
if item.reltype == RT.HYPERLINK and ".aspx" not in item._target:
links.append(item._target)
- return listify(links)
+ return text_doc_map(links)
def extract_value(kv_string):
@@ -60,7 +60,13 @@ def protofy(fieldId):
}
-def write_schema(parse_results, display_fields):
+def text_doc_map(string_list):
+ def guid_map(caption):
+ return write_text_doc(caption)
+ return listify(proxify_guids(list(map(guid_map, string_list))))
+
+
+def write_schema(parse_results, display_fields, storage_key):
view_guids = parse_results["child_guids"]
data_doc = parse_results["schema"]
@@ -87,7 +93,7 @@ def write_schema(parse_results, display_fields):
}
fields["proto"] = protofy("collectionProto")
- fields["data"] = listify(proxify_guids(view_guids))
+ fields[storage_key] = listify(proxify_guids(view_guids))
fields["schemaColumns"] = listify(display_fields)
fields["backgroundColor"] = "white"
fields["scale"] = 0.5
@@ -110,9 +116,7 @@ def write_schema(parse_results, display_fields):
return view_doc_guid
-def write_image(folder, name):
- path = f"http://localhost:1050/files/{folder}/{name}"
-
+def write_text_doc(content):
data_doc_guid = guid()
view_doc_guid = guid()
@@ -122,16 +126,66 @@ def write_image(folder, name):
"proto": protofy(data_doc_guid),
"x": 10,
"y": 10,
- "width": 300,
+ "width": 400,
"zIndex": 2,
"libraryBrush": False
},
"__type": "Doc"
}
+ data_doc = {
+ "_id": data_doc_guid,
+ "fields": {
+ "proto": protofy("textProto"),
+ "data": {
+ "Data": '{"doc":{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"' + content + '"}]}]},"selection":{"type":"text","anchor":1,"head":1}' + '}',
+ "__type": "RichTextField"
+ },
+ "title": content,
+ "nativeWidth": 200,
+ "author": "Bill Buxton",
+ "creationDate": {
+ "date": datetime.datetime.utcnow().microsecond,
+ "__type": "date"
+ },
+ "isPrototype": True,
+ "autoHeight": True,
+ "page": -1,
+ "nativeHeight": 200,
+ "height": 200,
+ "data_text": content
+ },
+ "__type": "Doc"
+ }
+
+ db.newDocuments.insert_one(view_doc)
+ db.newDocuments.insert_one(data_doc)
+
+ return view_doc_guid
+
+
+def write_image(folder, name):
+ path = f"http://localhost:1050/files/{folder}/{name}"
+
+ data_doc_guid = guid()
+ view_doc_guid = guid()
+
image = Image.open(f"{dist}/{folder}/{name}")
native_width, native_height = image.size
+ view_doc = {
+ "_id": view_doc_guid,
+ "fields": {
+ "proto": protofy(data_doc_guid),
+ "x": 10,
+ "y": 10,
+ "width": min(800, native_width),
+ "zIndex": 2,
+ "libraryBrush": False
+ },
+ "__type": "Doc"
+ }
+
data_doc = {
"_id": data_doc_guid,
"fields": {
@@ -253,7 +307,7 @@ def parse_document(file_name: str):
while lines[cur] != "Image":
link_descriptions.append(lines[cur].strip())
cur += 1
- result["link_descriptions"] = listify(link_descriptions)
+ result["link_descriptions"] = text_doc_map(link_descriptions)
result["hyperlinks"] = extract_links(source + "/" + file_name)
@@ -265,7 +319,8 @@ def parse_document(file_name: str):
captions.append(lines[cur + 1])
cur += 2
result["images"] = listify(images)
- result["captions"] = listify(captions)
+
+ result["captions"] = text_doc_map(captions)
notes = []
if (cur < len(lines) and lines[cur] == "NOTES:"):
@@ -304,7 +359,7 @@ for file_name in os.listdir(source):
if file_name.endswith('.docx'):
candidates += 1
schema_guids.append(write_schema(
- parse_document(file_name), ["title", "data"]))
+ parse_document(file_name), ["title", "data"], "image_data"))
print("writing parent schema...")
parent_guid = write_schema({
@@ -314,7 +369,7 @@ parent_guid = write_schema({
"__type": "Doc"
},
"child_guids": schema_guids
-}, ["title", "short_description", "original_price"])
+}, ["title", "short_description", "original_price"], "data")
print("appending parent schema to main workspace...\n")
db.newDocuments.update_one(
diff --git a/src/server/GarbageCollector.ts b/src/server/GarbageCollector.ts
new file mode 100644
index 000000000..268239481
--- /dev/null
+++ b/src/server/GarbageCollector.ts
@@ -0,0 +1,124 @@
+import { Database } from './database';
+
+import * as path from 'path';
+import * as fs from 'fs';
+import { Search } from './Search';
+
+function addDoc(doc: any, ids: string[], files: { [name: string]: string[] }) {
+ for (const key in doc) {
+ if (!doc.hasOwnProperty(key)) {
+ continue;
+ }
+ const field = doc[key];
+ if (field === undefined || field === null) {
+ continue;
+ }
+ if (field.__type === "proxy") {
+ ids.push(field.fieldId);
+ } else if (field.__type === "list") {
+ addDoc(field.fields, ids, files);
+ } else if (typeof field === "string") {
+ const re = /"(?:dataD|d)ocumentId"\s*:\s*"([\w\-]*)"/g;
+ let match: string[] | null;
+ while ((match = re.exec(field)) !== null) {
+ ids.push(match[1]);
+ }
+ } else if (field.__type === "RichTextField") {
+ const re = /"href"\s*:\s*"(.*?)"/g;
+ let match: string[] | null;
+ while ((match = re.exec(field.Data)) !== null) {
+ const urlString = match[1];
+ const split = new URL(urlString).pathname.split("doc/");
+ if (split.length > 1) {
+ ids.push(split[split.length - 1]);
+ }
+ }
+ const re2 = /"src"\s*:\s*"(.*?)"/g;
+ while ((match = re2.exec(field.Data)) !== null) {
+ const urlString = match[1];
+ const pathname = new URL(urlString).pathname;
+ const ext = path.extname(pathname);
+ const fileName = path.basename(pathname, ext);
+ let exts = files[fileName];
+ if (!exts) {
+ files[fileName] = exts = [];
+ }
+ exts.push(ext);
+ }
+ } else if (["audio", "image", "video", "pdf", "web"].includes(field.__type)) {
+ const url = new URL(field.url);
+ const pathname = url.pathname;
+ const ext = path.extname(pathname);
+ const fileName = path.basename(pathname, ext);
+ let exts = files[fileName];
+ if (!exts) {
+ files[fileName] = exts = [];
+ }
+ exts.push(ext);
+ }
+ }
+}
+
+async function GarbageCollect() {
+ // await new Promise(res => setTimeout(res, 3000));
+ const cursor = await Database.Instance.query({}, { userDocumentId: 1 }, 'users');
+ const users = await cursor.toArray();
+ const ids: string[] = users.map(user => user.userDocumentId);
+ const visited = new Set<string>();
+ const files: { [name: string]: string[] } = {};
+
+ while (ids.length) {
+ const count = Math.min(ids.length, 100);
+ const index = ids.length - count;
+ const fetchIds = ids.splice(index, count).filter(id => !visited.has(id));
+ if (!fetchIds.length) {
+ continue;
+ }
+ const docs = await new Promise<{ [key: string]: any }[]>(res => Database.Instance.getDocuments(fetchIds, res, "newDocuments"));
+ for (const doc of docs) {
+ const id = doc.id;
+ if (doc === undefined) {
+ console.log(`Couldn't find field with Id ${id}`);
+ continue;
+ }
+ visited.add(id);
+ addDoc(doc.fields, ids, files);
+ }
+ console.log(`To Go: ${ids.length}, visited: ${visited.size}`);
+ }
+
+ console.log(`Done: ${visited.size}`);
+
+ cursor.close();
+
+ const toDeleteCursor = await Database.Instance.query({ _id: { $nin: Array.from(visited) } }, { _id: 1 });
+ const toDelete: string[] = (await toDeleteCursor.toArray()).map(doc => doc._id);
+ toDeleteCursor.close();
+ const result = await Database.Instance.delete({ _id: { $in: toDelete } }, "newDocuments");
+ console.log(`${result.deletedCount} documents deleted`);
+
+ await Search.Instance.deleteDocuments(toDelete);
+ console.log("Cleared search documents");
+
+ const folder = "./src/server/public/files/";
+ fs.readdir(folder, (_, fileList) => {
+ const filesToDelete = fileList.filter(file => {
+ const ext = path.extname(file);
+ let base = path.basename(file, ext);
+ const existsInDb = (base in files || (base = base.substring(0, base.length - 2)) in files) && files[base].includes(ext);
+ return file !== ".gitignore" && !existsInDb;
+ });
+ console.log(`Deleting ${filesToDelete.length} files`);
+ filesToDelete.forEach(file => {
+ console.log(`Deleting file ${file}`);
+ try {
+ fs.unlinkSync(folder + file);
+ } catch {
+ console.warn(`Couldn't delete file ${file}`);
+ }
+ });
+ console.log(`Deleted ${filesToDelete.length} files`);
+ });
+}
+
+GarbageCollect();
diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts
index c4af5cdaa..5c13495ff 100644
--- a/src/server/RouteStore.ts
+++ b/src/server/RouteStore.ts
@@ -16,6 +16,7 @@ export enum RouteStore {
// USER AND WORKSPACES
getCurrUser = "/getCurrentUser",
+ getUsers = "/getUsers",
getUserDocumentId = "/getUserDocumentId",
updateCursor = "/updateCursor",
diff --git a/src/server/Search.ts b/src/server/Search.ts
index 5ca5578a7..8591f8857 100644
--- a/src/server/Search.ts
+++ b/src/server/Search.ts
@@ -14,23 +14,36 @@ export class Search {
});
return res;
} catch (e) {
- console.warn("Search error: " + e + document);
+ // console.warn("Search error: " + e + document);
}
}
- public async search(query: string) {
+ public async updateDocuments(documents: any[]) {
+ try {
+ const res = await rp.post(this.url + "dash/update", {
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify(documents)
+ });
+ return res;
+ } catch (e) {
+ // console.warn("Search error: " + e + document);
+ }
+ }
+
+ public async search(query: string, start: number = 0) {
try {
const searchResults = JSON.parse(await rp.get(this.url + "dash/select", {
qs: {
q: query,
- fl: "id"
+ fl: "id",
+ start: start
}
}));
- const fields = searchResults.response.docs;
- const ids = fields.map((field: any) => field.id);
- return ids;
+ const { docs, numFound } = searchResults.response;
+ const ids = docs.map((field: any) => field.id);
+ return { ids, numFound };
} catch {
- return [];
+ return { ids: [], numFound: -1 };
}
}
@@ -46,4 +59,25 @@ export class Search {
});
} catch { }
}
+
+ public deleteDocuments(docs: string[]) {
+ const promises: rp.RequestPromise[] = [];
+ const nToDelete = 1000;
+ let index = 0;
+ while (index < docs.length) {
+ const count = Math.min(docs.length - index, nToDelete);
+ const deleteIds = docs.slice(index, index + count);
+ index += count;
+ promises.push(rp.post(this.url + "dash/update", {
+ body: {
+ delete: {
+ query: deleteIds.map(id => `id:"${id}"`).join(" ")
+ }
+ },
+ json: true
+ }));
+ }
+
+ return Promise.all(promises);
+ }
} \ No newline at end of file
diff --git a/src/server/authentication/controllers/user_controller.ts b/src/server/authentication/controllers/user_controller.ts
index 1dacdf3fa..ca4fc171c 100644
--- a/src/server/authentication/controllers/user_controller.ts
+++ b/src/server/authentication/controllers/user_controller.ts
@@ -42,10 +42,6 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => {
const errors = req.validationErrors();
if (errors) {
- res.render("signup.pug", {
- title: "Sign Up",
- user: req.user,
- });
return res.redirect(RouteStore.signup);
}
@@ -66,16 +62,23 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => {
user.save((err) => {
if (err) { return next(err); }
req.logIn(user, (err) => {
- if (err) {
- return next(err);
- }
- res.redirect(RouteStore.home);
+ if (err) { return next(err); }
+ tryRedirectToTarget(req, res);
});
});
});
};
+let tryRedirectToTarget = (req: Request, res: Response) => {
+ if (req.session && req.session.target) {
+ res.redirect(req.session.target);
+ req.session.target = undefined;
+ } else {
+ res.redirect(RouteStore.home);
+ }
+};
+
/**
* GET /login
@@ -83,6 +86,7 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => {
*/
export let getLogin = (req: Request, res: Response) => {
if (req.user) {
+ req.session!.target = undefined;
return res.redirect(RouteStore.home);
}
res.render("login.pug", {
@@ -115,7 +119,7 @@ export let postLogin = (req: Request, res: Response, next: NextFunction) => {
}
req.logIn(user, (err) => {
if (err) { next(err); return; }
- res.redirect(RouteStore.home);
+ tryRedirectToTarget(req, res);
});
})(req, res, next);
};
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 95c20d2db..763693dd6 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -10,7 +10,7 @@ import { CollectionView } from "../../../client/views/collections/CollectionView
import { Doc } from "../../../new_fields/Doc";
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
-import { Cast } from "../../../new_fields/Types";
+import { Cast, FieldValue } from "../../../new_fields/Types";
import { RouteStore } from "../../RouteStore";
export class CurrentUserUtils {
@@ -29,9 +29,14 @@ export class CurrentUserUtils {
private static createUserDocument(id: string): Doc {
let doc = new Doc(id, true);
doc.viewType = CollectionViewType.Tree;
+ doc.dropAction = "alias";
doc.layout = CollectionView.LayoutString();
doc.title = this.email;
+ this.updateUserDocument(doc);
doc.data = new List<Doc>();
+ doc.gridGap = 5;
+ doc.xMargin = 5;
+ doc.yMargin = 5;
doc.excludeFromLibrary = true;
doc.optionalRightCollection = Docs.Create.StackingDocument([], { title: "New mobile uploads" });
// doc.library = Docs.Create.TreeDocument([doc], { title: `Library: ${CurrentUserUtils.email}` });
@@ -39,8 +44,31 @@ export class CurrentUserUtils {
return doc;
}
+ static updateUserDocument(doc: Doc) {
+ if (doc.workspaces === undefined) {
+ const workspaces = Docs.Create.TreeDocument([], { title: "Workspaces", height: 100 });
+ workspaces.excludeFromLibrary = true;
+ workspaces.workspaceLibrary = true;
+ doc.workspaces = workspaces;
+ }
+ if (doc.recentlyClosed === undefined) {
+ const recentlyClosed = Docs.Create.TreeDocument([], { title: "Recently Closed", height: 75 });
+ recentlyClosed.excludeFromLibrary = true;
+ doc.recentlyClosed = recentlyClosed;
+ }
+ if (doc.sidebar === undefined) {
+ const sidebar = Docs.Create.StackingDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { title: "Sidebar" });
+ sidebar.excludeFromLibrary = true;
+ sidebar.gridGap = 5;
+ sidebar.xMargin = 5;
+ sidebar.yMargin = 5;
+ doc.sidebar = sidebar;
+ }
+
+ }
+
public static async loadCurrentUser(): Promise<any> {
- let userPromise = rp.get(DocServer.Util.prepend(RouteStore.getCurrUser)).then(response => {
+ let userPromise = rp.get(DocServer.prepend(RouteStore.getCurrUser)).then(response => {
if (response) {
let obj = JSON.parse(response);
CurrentUserUtils.curr_id = obj.id as string;
@@ -49,10 +77,16 @@ export class CurrentUserUtils {
throw new Error("There should be a user! Why does Dash think there isn't one?");
}
});
- let userDocPromise = await rp.get(DocServer.Util.prepend(RouteStore.getUserDocumentId)).then(id => {
+ let userDocPromise = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId)).then(id => {
if (id) {
- return DocServer.getRefField(id).then(field =>
- runInAction(() => this.user_document = field instanceof Doc ? field : this.createUserDocument(id)));
+ return DocServer.GetRefField(id).then(async field => {
+ if (field instanceof Doc) {
+ await this.updateUserDocument(field);
+ runInAction(() => this.user_document = field);
+ } else {
+ runInAction(() => this.user_document = this.createUserDocument(id));
+ }
+ });
} else {
throw new Error("There should be a user id! Why does Dash think there isn't one?");
}
diff --git a/src/server/database.ts b/src/server/database.ts
index 70b3efced..29a8ffafa 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -41,11 +41,17 @@ export class Database {
}
}
- public delete(id: string, collectionName = Database.DocumentsCollection) {
+ public delete(query: any, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>;
+ public delete(id: string, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>;
+ public delete(id: any, collectionName = Database.DocumentsCollection) {
+ if (typeof id === "string") {
+ id = { _id: id };
+ }
if (this.db) {
- this.db.collection(collectionName).remove({ id: id });
+ const db = this.db;
+ return new Promise(res => db.collection(collectionName).deleteMany(id, (err, result) => res(result)));
} else {
- this.onConnect.push(() => this.delete(id, collectionName));
+ return new Promise(res => this.onConnect.push(() => res(this.delete(id, collectionName))));
}
}
@@ -120,12 +126,16 @@ export class Database {
}
}
- public query(query: any): Promise<mongodb.Cursor> {
+ public query(query: { [key: string]: any }, projection?: { [key: string]: 0 | 1 }, collectionName = "newDocuments"): Promise<mongodb.Cursor> {
if (this.db) {
- return Promise.resolve<mongodb.Cursor>(this.db.collection('newDocuments').find(query));
+ let cursor = this.db.collection(collectionName).find(query);
+ if (projection) {
+ cursor = cursor.project(projection);
+ }
+ return Promise.resolve<mongodb.Cursor>(cursor);
} else {
return new Promise<mongodb.Cursor>(res => {
- this.onConnect.push(() => res(this.query(query)));
+ this.onConnect.push(() => res(this.query(query, projection, collectionName)));
});
}
}
diff --git a/src/server/index.ts b/src/server/index.ts
index d9fe9d93d..287ba6058 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -1,3 +1,4 @@
+require('dotenv').config();
import * as bodyParser from 'body-parser';
import { exec } from 'child_process';
import * as cookieParser from 'cookie-parser';
@@ -7,9 +8,9 @@ import * as expressValidator from 'express-validator';
import * as formidable from 'formidable';
import * as fs from 'fs';
import * as sharp from 'sharp';
+import * as Pdfjs from 'pdfjs-dist';
const imageDataUri = require('image-data-uri');
import * as mobileDetect from 'mobile-detect';
-import { ObservableMap } from 'mobx';
import * as passport from 'passport';
import * as path from 'path';
import * as request from 'request';
@@ -28,6 +29,7 @@ import { MessageStore, Transferable, Types, Diff } from "./Message";
import { RouteStore } from './RouteStore';
const app = express();
const config = require('../../webpack.config');
+import { createCanvas, loadImage, Canvas } from "canvas";
const compiler = webpack(config);
const port = 1050; // default port to listen
const serverPort = 4321;
@@ -37,11 +39,24 @@ import c = require("crypto");
import { Search } from './Search';
import { debug } from 'util';
import _ = require('lodash');
+import { Response } from 'express-serve-static-core';
const MongoStore = require('connect-mongo')(session);
const mongoose = require('mongoose');
+const probe = require("probe-image-size");
const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest));
+const release = process.env.RELEASE === "true";
+if (process.env.RELEASE === "true") {
+ console.log("Running server in release mode");
+} else {
+ console.log("Running server in debug mode");
+}
+console.log(process.env.PWD);
+let clientUtils = fs.readFileSync("./src/client/util/ClientUtils.ts.temp", "utf8");
+clientUtils = `//AUTO-GENERATED FILE: DO NOT EDIT\n${clientUtils.replace('"mode"', String(release))}`;
+fs.writeFileSync("./src/client/util/ClientUtils.ts", clientUtils, "utf8");
+
const mongoUrl = 'mongodb://localhost:27017/Dash';
mongoose.connect(mongoUrl);
mongoose.connection.on('connected', () => console.log("connected"));
@@ -88,14 +103,15 @@ enum Method {
*/
function addSecureRoute(method: Method,
handler: (user: DashUserModel, res: express.Response, req: express.Request) => void,
- onRejection: (res: express.Response) => any = (res) => res.redirect(RouteStore.logout),
+ onRejection: (res: express.Response, req: express.Request) => any = res => res.redirect(RouteStore.login),
...subscribers: string[]
) {
let abstracted = (req: express.Request, res: express.Response) => {
if (req.user) {
handler(req.user, res, req);
} else {
- onRejection(res);
+ req.session!.target = `http://localhost:${port}${req.originalUrl}`;
+ onRejection(res, req);
}
};
subscribers.forEach(route => {
@@ -133,6 +149,64 @@ app.get("/search", async (req, res) => {
res.send(results);
});
+app.get("/thumbnail/:filename", (req, res) => {
+ let filename = req.params.filename;
+ let noExt = filename.substring(0, filename.length - ".png".length);
+ let pagenumber = parseInt(noExt[noExt.length - 1]);
+ fs.exists(uploadDir + filename, (exists: boolean) => {
+ console.log(`${uploadDir + filename} ${exists ? "exists" : "does not exist"}`);
+ if (exists) {
+ let input = fs.createReadStream(uploadDir + filename);
+ probe(input, (err: any, result: any) => {
+ if (err) {
+ console.log(err);
+ return;
+ }
+ res.send({ path: "/files/" + filename, width: result.width, height: result.height });
+ });
+ }
+ else {
+ LoadPage(uploadDir + filename.substring(0, filename.length - "-n.png".length) + ".pdf", pagenumber, res);
+ }
+ });
+});
+
+function LoadPage(file: string, pageNumber: number, res: Response) {
+ console.log(file);
+ Pdfjs.getDocument(file).promise
+ .then((pdf: Pdfjs.PDFDocumentProxy) => {
+ let factory = new NodeCanvasFactory();
+ console.log(pageNumber);
+ pdf.getPage(pageNumber).then((page: Pdfjs.PDFPageProxy) => {
+ console.log("reading " + page);
+ let viewport = page.getViewport(1);
+ let canvasAndContext = factory.create(viewport.width, viewport.height);
+ let renderContext = {
+ canvasContext: canvasAndContext.context,
+ viewport: viewport,
+ canvasFactory: factory
+ };
+ console.log("read " + pageNumber);
+
+ page.render(renderContext).promise
+ .then(() => {
+ console.log("saving " + pageNumber);
+ let stream = canvasAndContext.canvas.createPNGStream();
+ let pngFile = `${file.substring(0, file.length - ".pdf".length)}-${pageNumber}.PNG`;
+ let out = fs.createWriteStream(pngFile);
+ stream.pipe(out);
+ out.on("finish", () => {
+ console.log(`Success! Saved to ${pngFile}`);
+ let name = path.basename(pngFile);
+ res.send({ path: "/files/" + name, width: viewport.width, height: viewport.height });
+ });
+ }, (reason: string) => {
+ console.error(reason + ` ${pageNumber}`);
+ });
+ });
+ });
+}
+
// anyone attempting to navigate to localhost at this port will
// first have to login
addSecureRoute(
@@ -144,6 +218,17 @@ addSecureRoute(
addSecureRoute(
Method.GET,
+ async (_, res) => {
+ const cursor = await Database.Instance.query({}, { email: 1, userDocumentId: 1 }, "users");
+ const results = await cursor.toArray();
+ res.send(results.map(user => ({ email: user.email, userDocumentId: user.userDocumentId })));
+ },
+ undefined,
+ RouteStore.getUsers
+);
+
+addSecureRoute(
+ Method.GET,
(user, res, req) => {
let detector = new mobileDetect(req.headers['user-agent'] || "");
let filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html';
@@ -168,7 +253,31 @@ addSecureRoute(
RouteStore.getCurrUser
);
+class NodeCanvasFactory {
+ create = (width: number, height: number) => {
+ var canvas = createCanvas(width, height);
+ var context = canvas.getContext('2d');
+ return {
+ canvas: canvas,
+ context: context,
+ };
+ }
+
+ reset = (canvasAndContext: any, width: number, height: number) => {
+ canvasAndContext.canvas.width = width;
+ canvasAndContext.canvas.height = height;
+ }
+
+ destroy = (canvasAndContext: any) => {
+ canvasAndContext.canvas.width = 0;
+ canvasAndContext.canvas.height = 0;
+ canvasAndContext.canvas = null;
+ canvasAndContext.context = null;
+ }
+}
+
const pngTypes = [".png", ".PNG"];
+const pdfTypes = [".pdf", ".PDF"];
const jpgTypes = [".jpg", ".JPG", ".jpeg", ".JPEG"];
const uploadDir = __dirname + "/public/files/";
// SETTERS
@@ -187,9 +296,10 @@ app.post(
const file = path.basename(files[name].path);
const ext = path.extname(file);
let resizers = [
- { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
- { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
- { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" },
+ { resizer: sharp().rotate(), suffix: "_o" },
+ { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }).rotate(), suffix: "_s" },
+ { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }).rotate(), suffix: "_m" },
+ { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }).rotate(), suffix: "_l" },
];
let isImage = false;
if (pngTypes.includes(ext)) {
@@ -203,6 +313,38 @@ app.post(
});
isImage = true;
}
+ else if (pdfTypes.includes(ext)) {
+ // Pdfjs.getDocument(uploadDir + file).promise
+ // .then((pdf: Pdfjs.PDFDocumentProxy) => {
+ // let numPages = pdf.numPages;
+ // let factory = new NodeCanvasFactory();
+ // for (let pageNum = 0; pageNum < numPages; pageNum++) {
+ // console.log(pageNum);
+ // pdf.getPage(pageNum + 1).then((page: Pdfjs.PDFPageProxy) => {
+ // console.log("reading " + pageNum);
+ // let viewport = page.getViewport(1);
+ // let canvasAndContext = factory.create(viewport.width, viewport.height);
+ // let renderContext = {
+ // canvasContext: canvasAndContext.context,
+ // viewport: viewport,
+ // canvasFactory: factory
+ // }
+ // console.log("read " + pageNum);
+
+ // page.render(renderContext).promise
+ // .then(() => {
+ // console.log("saving " + pageNum);
+ // let stream = canvasAndContext.canvas.createPNGStream();
+ // let out = fs.createWriteStream(uploadDir + file.substring(0, file.length - ext.length) + `-${pageNum + 1}.PNG`);
+ // stream.pipe(out);
+ // out.on("finish", () => console.log(`Success! Saved to ${uploadDir + file.substring(0, file.length - ext.length) + `-${pageNum + 1}.PNG`}`));
+ // }, (reason: string) => {
+ // console.error(reason + ` ${pageNum}`);
+ // });
+ // });
+ // }
+ // });
+ }
if (isImage) {
resizers.forEach(resizer => {
fs.createReadStream(uploadDir + file).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDir + file.substring(0, file.length - ext.length) + resizer.suffix + ext));
@@ -278,11 +420,21 @@ app.post(RouteStore.reset, postReset);
app.use(RouteStore.corsProxy, (req, res) =>
req.pipe(request(req.url.substring(1))).pipe(res));
-app.get(RouteStore.delete, (req, res) =>
- deleteFields().then(() => res.redirect(RouteStore.home)));
+app.get(RouteStore.delete, (req, res) => {
+ if (release) {
+ res.send("no");
+ return;
+ }
+ deleteFields().then(() => res.redirect(RouteStore.home));
+});
-app.get(RouteStore.deleteAll, (req, res) =>
- deleteAll().then(() => res.redirect(RouteStore.home)));
+app.get(RouteStore.deleteAll, (req, res) => {
+ if (release) {
+ res.send("no");
+ return;
+ }
+ deleteAll().then(() => res.redirect(RouteStore.home));
+});
app.use(wdm(compiler, { publicPath: config.output.publicPath }));
@@ -303,14 +455,16 @@ server.on("connection", function (socket: Socket) {
Utils.emit(socket, MessageStore.Foo, "handshooken");
- Utils.addServerHandler(socket, MessageStore.Bar, barReceived);
- Utils.addServerHandler(socket, MessageStore.SetField, (args) => setField(socket, args));
+ Utils.AddServerHandler(socket, MessageStore.Bar, barReceived);
+ Utils.AddServerHandler(socket, MessageStore.SetField, (args) => setField(socket, args));
Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField);
Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields);
- Utils.addServerHandler(socket, MessageStore.DeleteAll, deleteFields);
+ if (!release) {
+ Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteFields);
+ }
- Utils.addServerHandler(socket, MessageStore.CreateField, CreateField);
- Utils.addServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff));
+ Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField);
+ Utils.AddServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff));
Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField);
Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields);
});
@@ -364,8 +518,8 @@ function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => v
const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
"number": "_n",
"string": "_t",
- // "boolean": "_b",
- // "image": ["_t", "url"],
+ "boolean": "_b",
+ "image": ["_t", "url"],
"video": ["_t", "url"],
"pdf": ["_t", "url"],
"audio": ["_t", "url"],
diff --git a/src/server/updateSearch.ts b/src/server/updateSearch.ts
index de1fd25e1..906b795f1 100644
--- a/src/server/updateSearch.ts
+++ b/src/server/updateSearch.ts
@@ -6,7 +6,7 @@ import pLimit from 'p-limit';
const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
"number": "_n",
"string": "_t",
- // "boolean": "_b",
+ "boolean": "_b",
// "image": ["_t", "url"],
"video": ["_t", "url"],
"pdf": ["_t", "url"],
@@ -67,7 +67,7 @@ async function update() {
if ((numDocs % 50) === 0) {
console.log("updateDoc " + numDocs);
}
- console.log("doc " + numDocs);
+ // console.log("doc " + numDocs);
if (doc.__type !== "Doc") {
return;
}
@@ -88,13 +88,35 @@ async function update() {
}
if (dynfield) {
updates.push(update);
- console.log(updates.length);
+ // console.log(updates.length);
}
}
await cursor.forEach(updateDoc);
- await Promise.all(updates.map(update => {
- return limit(() => Search.Instance.updateDocument(update));
- }));
+ console.log(`Updating ${updates.length} documents`);
+ const result = await Search.Instance.updateDocuments(updates);
+ try {
+ console.log(JSON.parse(result).responseHeader.status);
+ } catch {
+ console.log("Error:");
+ // console.log(updates[i]);
+ console.log(result);
+ console.log("\n");
+ }
+ // for (let i = 0; i < updates.length; i++) {
+ // console.log(i);
+ // const result = await Search.Instance.updateDocument(updates[i]);
+ // try {
+ // console.log(JSON.parse(result).responseHeader.status);
+ // } catch {
+ // console.log("Error:");
+ // console.log(updates[i]);
+ // console.log(result);
+ // console.log("\n");
+ // }
+ // }
+ // await Promise.all(updates.map(update => {
+ // return limit(() => Search.Instance.updateDocument(update));
+ // }));
cursor.close();
}
diff --git a/test/test.ts b/test/test.ts
index 91dc43379..f1cf75bd4 100644
--- a/test/test.ts
+++ b/test/test.ts
@@ -1,9 +1,17 @@
import { expect } from 'chai';
import 'mocha';
+const { JSDOM } = require('jsdom');
+const dom = new JSDOM("", {
+ url: "http://localhost:1050"
+});
+(global as any).window = dom.window;
+
+
import { autorun, reaction } from "mobx";
import { Doc } from '../src/new_fields/Doc';
import { Cast } from '../src/new_fields/Types';
-
+import { createSchema, makeInterface, defaultSpec } from '../src/new_fields/Schema';
+import { ImageField } from '../src/new_fields/URLField';
describe("Document", () => {
it('should hold fields', () => {
let key = "Test";
@@ -33,3 +41,120 @@ describe("Document", () => {
expect(ran).to.equal(true);
});
});
+
+const testSchema1 = createSchema({
+ a: "number",
+ b: "string",
+ c: "boolean",
+ d: ImageField,
+ e: Doc
+});
+
+type TestDoc = makeInterface<[typeof testSchema1]>;
+const TestDoc = makeInterface(testSchema1);
+
+const testSchema2 = createSchema({
+ a: defaultSpec("boolean", true),
+ b: defaultSpec("number", 5),
+ c: defaultSpec("string", "hello world")
+});
+
+type TestDoc2 = makeInterface<[typeof testSchema2]>;
+const TestDoc2 = makeInterface(testSchema2);
+
+const testSchema3 = createSchema({
+ a: TestDoc2
+});
+
+type TestDoc3 = makeInterface<[typeof testSchema3]>;
+const TestDoc3 = makeInterface(testSchema3);
+
+describe("Schema", () => {
+ it("should do the right thing 1", () => {
+ const test1 = new Doc;
+ const test2 = new Doc;
+ const ifield = new ImageField(new URL("http://google.com"));
+ test1.a = 5;
+ test1.b = "hello";
+ test1.c = true;
+ test1.d = ifield;
+ test1.e = test2;
+ const doc = TestDoc(test1);
+ expect(doc.a).to.equal(5);
+ expect(doc.b).to.equal("hello");
+ expect(doc.c).to.equal(true);
+ expect(doc.d).to.equal(ifield);
+ expect(doc.e).to.equal(test2);
+ });
+
+ it("should do the right thing 2", () => {
+ const test1 = new Doc;
+ const test2 = new Doc;
+ const ifield = new ImageField(new URL("http://google.com"));
+ test1.a = "hello";
+ test1.b = 5;
+ test1.c = test2;
+ test1.d = true;
+ test1.e = ifield;
+ const doc = TestDoc(test1);
+ expect(doc.a).to.equal(undefined);
+ expect(doc.b).to.equal(undefined);
+ expect(doc.c).to.equal(undefined);
+ expect(doc.d).to.equal(undefined);
+ expect(doc.e).to.equal(undefined);
+ });
+
+ it("should do the right thing 3", () => {
+ const test1 = new Doc;
+ const test2 = new Doc;
+ const ifield = new ImageField(new URL("http://google.com"));
+ test1.a = "hello";
+ test1.b = 5;
+ test1.c = test2;
+ test1.d = true;
+ test1.e = ifield;
+ const doc = TestDoc(test1);
+ expect(doc.a).to.equal(undefined);
+ expect(doc.b).to.equal(undefined);
+ expect(doc.c).to.equal(undefined);
+ expect(doc.d).to.equal(undefined);
+ expect(doc.e).to.equal(undefined);
+ });
+
+ it("should do the right thing 4", () => {
+ const doc = TestDoc2();
+ expect(doc.a).to.equal(true);
+ expect(doc.b).to.equal(5);
+ expect(doc.c).to.equal("hello world");
+
+ const d2 = new Doc;
+ d2.a = false;
+ d2.b = 4;
+ d2.c = "goodbye";
+ const doc2 = TestDoc2(d2);
+ expect(doc2.a).to.equal(false);
+ expect(doc2.b).to.equal(4);
+ expect(doc2.c).to.equal("goodbye");
+
+ const d3 = new Doc;
+ d3.a = "hello";
+ d3.b = false;
+ d3.c = 5;
+ const doc3 = TestDoc2(d3);
+ expect(doc3.a).to.equal(true);
+ expect(doc3.b).to.equal(5);
+ expect(doc3.c).to.equal("hello world");
+ });
+
+ it("should do the right thing 5", async () => {
+ const test1 = new Doc;
+ const test2 = new Doc;
+ const doc = TestDoc3(test1);
+ expect(doc.a).to.equal(undefined);
+ test1.a = test2;
+ const doc2 = (await doc.a)!;
+ expect(doc2.a).to.equal(true);
+ expect(doc2.b).to.equal(5);
+ expect(doc2.c).to.equal("hello world");
+ });
+});
diff --git a/tsconfig.json b/tsconfig.json
index 0d4d77002..9ea91ec49 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,10 +1,13 @@
{
"compilerOptions": {
"target": "es5",
+ // "module": "system",
"removeComments": true,
"experimentalDecorators": true,
+ "allowSyntheticDefaultImports": true,
"strict": true,
"jsx": "react",
+ "allowJs": true,
"sourceMap": true,
"outDir": "dist",
"lib": [