diff options
author | Sam Wilkins <abdullah_ahmed@brown.edu> | 2019-03-02 17:43:55 -0500 |
---|---|---|
committer | Sam Wilkins <abdullah_ahmed@brown.edu> | 2019-03-02 17:43:55 -0500 |
commit | 950c656d06d7c6b25b82030feaa7c5c70102fe6a (patch) | |
tree | f5ff6112a6d0c34c225479e94a85450559b3f769 | |
parent | bf26137803c09807b6e7e3d9d9ad086a7ead32ac (diff) | |
parent | bde8aabad7e5745b4797e73b564e4efb19faeca9 (diff) |
Merge with master and initial pass at converting main to a component
49 files changed, 1393 insertions, 676 deletions
diff --git a/.DS_Store b/.DS_Store Binary files differnew file mode 100644 index 000000000..8493b4a74 --- /dev/null +++ b/.DS_Store diff --git a/.vscode/launch.json b/.vscode/launch.json index 81d901b39..5f320d3fe 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,10 +12,6 @@ "breakOnLoad": true, "url": "http://localhost:1050/signup", "webRoot": "${workspaceFolder}", - "skipFiles": [ - "${workspaceRoot}/node_modules/**/*.js", - "${workspaceRoot}/node_modules/**/*.ts", - ] }, { "type": "node", diff --git a/build/index.html b/build/index.html new file mode 100644 index 000000000..fda212af4 --- /dev/null +++ b/build/index.html @@ -0,0 +1,12 @@ +<html> + +<head> + <title>Dash Web</title> +</head> + +<body> + <div id="root"></div> + <script src="./bundle.js"></script> +</body> + +</html>
\ No newline at end of file diff --git a/deploy/debug/test.html b/deploy/debug/test.html new file mode 100644 index 000000000..58febbc81 --- /dev/null +++ b/deploy/debug/test.html @@ -0,0 +1,13 @@ +<html> + +<head> + <title>Test view</title> + <link href="https://fonts.googleapis.com/css?family=Fjalla+One|Hind+Siliguri:300" rel="stylesheet"> +</head> + +<body> + <div id="root"></div> + <script src="../test.js"></script> +</body> + +</html>
\ No newline at end of file diff --git a/package-lock.json b/package-lock.json index da5ad8385..be73fb141 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1022,12 +1022,10 @@ "integrity": "sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=" }, "async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", - "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", - "requires": { - "lodash": "^4.17.11" - } + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true }, "async-each": { "version": "1.0.1", @@ -2199,6 +2197,11 @@ "sha.js": "^2.4.8" } }, + "crel": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/crel/-/crel-3.1.0.tgz", + "integrity": "sha512-VIGY44ERxx8lXVkOEfcB0A49OkjxkQNK+j+fHvoLy7GsGX1KKgAaQ+p9N0YgvQXu+X+ryUWGDeLx/fSI+w7+eg==" + }, "cross-spawn": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", @@ -3428,7 +3431,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -3446,11 +3450,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3463,15 +3469,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3574,7 +3583,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3584,6 +3594,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3596,17 +3607,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3623,6 +3637,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3695,7 +3710,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3705,6 +3721,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3780,7 +3797,8 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -3810,6 +3828,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3827,6 +3846,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3865,11 +3885,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true + "bundled": true, + "optional": true } } }, @@ -10065,6 +10087,43 @@ "prosemirror-transform": "^1.0.0" } }, + "prosemirror-dropcursor": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.1.1.tgz", + "integrity": "sha512-GeUyMO/tOEf8MXrP7Xb7UIMrfK86OGh0fnyBrHfhav4VjY9cw65mNoqHy87CklE5711AhCP5Qzfp8RL/hVKusg==", + "requires": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "prosemirror-example-setup": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prosemirror-example-setup/-/prosemirror-example-setup-1.0.1.tgz", + "integrity": "sha512-4NKWpdmm75Zzgq/dIrypRnkBNPx+ONKyoGF42a9g3VIVv0TWglf1CBNxt5kzCgli9xdfut/xE5B42F9DR6BLHw==", + "requires": { + "prosemirror-commands": "^1.0.0", + "prosemirror-dropcursor": "^1.0.0", + "prosemirror-gapcursor": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-inputrules": "^1.0.0", + "prosemirror-keymap": "^1.0.0", + "prosemirror-menu": "^1.0.0", + "prosemirror-schema-list": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, + "prosemirror-gapcursor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.0.3.tgz", + "integrity": "sha512-X+hJhr42PcHWiSWL+lI5f/UeOhXCxlBFb8M6O8aG1hssmaRrW7sS2/Fjg5jFV+pTdS1REFkmm1occh01FMdDIQ==", + "requires": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, "prosemirror-history": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.0.3.tgz", @@ -10075,6 +10134,15 @@ "rope-sequence": "^1.2.0" } }, + "prosemirror-inputrules": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.0.1.tgz", + "integrity": "sha512-UHy22NmwxS5WIMQYkzraDttQAF8mpP82FfbJsmKFfx6jwkR/SZa+ZhbkLY0zKQ5fBdJN7euj36JG/B5iAlrpxA==", + "requires": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, "prosemirror-keymap": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.0.1.tgz", @@ -10084,6 +10152,17 @@ "w3c-keyname": "^1.1.8" } }, + "prosemirror-menu": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.0.5.tgz", + "integrity": "sha512-9Vrn7CC191v7FA4QrAkL8W1SrR73V3CRIYCDuk94R8oFVk4VxSFdoKVLHuvGzxZ8b5LCu3DMJfh86YW9uL4RkQ==", + "requires": { + "crel": "^3.0.0", + "prosemirror-commands": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, "prosemirror-model": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.7.0.tgz", @@ -10100,6 +10179,15 @@ "prosemirror-model": "^1.0.0" } }, + "prosemirror-schema-list": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.0.2.tgz", + "integrity": "sha512-IJ4DEpUEymfO+NNA4DAgCMF39XiQqpmCoPYY3SXa1jYcVgObGpGfJlSjZYVFEpimoLI7/mLoOLDhCtpGCRhTfg==", + "requires": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, "prosemirror-state": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.2.2.tgz", @@ -13409,4 +13497,4 @@ "dev": true } } -} +}
\ No newline at end of file diff --git a/package.json b/package.json index 4d74e229e..50d5f8802 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "passport": "^0.4.0", "passport-local": "^1.0.0", "prosemirror-commands": "^1.0.7", + "prosemirror-example-setup": "^1.0.1", "prosemirror-history": "^1.0.3", "prosemirror-keymap": "^1.0.1", "prosemirror-model": "^1.7.0", diff --git a/src/.DS_Store b/src/.DS_Store Binary files differnew file mode 100644 index 000000000..4d6acb95a --- /dev/null +++ b/src/.DS_Store diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 2dfff6235..1d24ff7d2 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -7,10 +7,14 @@ import { ListField } from "../../fields/ListField"; import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; import { ImageField } from "../../fields/ImageField"; import { ImageBox } from "../views/nodes/ImageBox"; +import { WebField } from "../../fields/WebField"; +import { WebBox } from "../views/nodes/WebBox"; import { CollectionView, CollectionViewType } from "../views/collections/CollectionView"; -import { FieldView } from "../views/nodes/FieldView"; import { HtmlField } from "../../fields/HtmlField"; -import { WebView } from "../views/nodes/WebView"; +import { Key } from "../../fields/Key" +import { Field } from "../../fields/Field"; +import { KeyValueBox } from "../views/nodes/KeyValueBox" +import { KVPField } from "../../fields/KVPField"; export interface DocumentOptions { x?: number; @@ -20,111 +24,124 @@ export interface DocumentOptions { nativeWidth?: number; nativeHeight?: number; title?: string; + panx?: number; + pany?: number; + scale?: number; + layout?: string; + layoutKeys?: Key[]; + viewType?: number; } export namespace Documents { - export function initProtos(callback: () => void) { - Server.GetFields([collectionProtoId, textProtoId, imageProtoId], (fields) => { - collectionProto = fields[collectionProtoId] as Document; + let textProto: Document; + let imageProto: Document; + let webProto: Document; + let collProto: Document; + let kvpProto: Document; + const textProtoId = "textProto"; + const imageProtoId = "imageProto"; + const webProtoId = "webProto"; + const collProtoId = "collectionProto"; + const kvpProtoId = "kvpProto"; + + export function initProtos(mainDocId: string, callback: (mainDoc?: Document) => void) { + Server.GetFields([collProtoId, textProtoId, imageProtoId, mainDocId], (fields) => { + collProto = fields[collProtoId] as Document; imageProto = fields[imageProtoId] as Document; textProto = fields[textProtoId] as Document; - callback() + webProto = fields[webProtoId] as Document; + kvpProto = fields[kvpProtoId] as Document; + callback(fields[mainDocId] as Document) }); } + function assignOptions(doc: Document, options: DocumentOptions): Document { + if (options.x !== undefined) { doc.SetNumber(KeyStore.X, options.x); } + if (options.y !== undefined) { doc.SetNumber(KeyStore.Y, options.y); } + if (options.width !== undefined) { doc.SetNumber(KeyStore.Width, options.width); } + if (options.height !== undefined) { doc.SetNumber(KeyStore.Height, options.height); } + if (options.nativeWidth !== undefined) { doc.SetNumber(KeyStore.NativeWidth, options.nativeWidth); } + if (options.nativeHeight !== undefined) { doc.SetNumber(KeyStore.NativeHeight, options.nativeHeight); } + if (options.title !== undefined) { doc.SetText(KeyStore.Title, options.title); } + if (options.panx !== undefined) { doc.SetNumber(KeyStore.PanX, options.panx); } + if (options.pany !== undefined) { doc.SetNumber(KeyStore.PanY, options.pany); } + if (options.scale !== undefined) { doc.SetNumber(KeyStore.Scale, options.scale); } + if (options.viewType !== undefined) { doc.SetNumber(KeyStore.ViewType, options.viewType); } + if (options.layout !== undefined) { doc.SetText(KeyStore.Layout, options.layout); } + if (options.layoutKeys !== undefined) { doc.Set(KeyStore.LayoutKeys, new ListField(options.layoutKeys)); } + return doc; + } + function setupPrototypeOptions(protoId: string, title: string, layout: string, options: DocumentOptions): Document { + return assignOptions(new Document(protoId), { ...options, title: title, layout: layout }); + } + function SetInstanceOptions<T, U extends Field & { Data: T }>(doc: Document, options: DocumentOptions, value: T, ctor: { new(): U }, id?: string) { + var deleg = doc.MakeDelegate(id); + deleg.SetData(KeyStore.Data, value, ctor); + return assignOptions(deleg, options); + } - function setupOptions(doc: Document, options: DocumentOptions): void { - if (options.x !== undefined) { - doc.SetData(KeyStore.X, options.x, NumberField); - } - if (options.y !== undefined) { - doc.SetData(KeyStore.Y, options.y, NumberField); - } - if (options.width !== undefined) { - doc.SetData(KeyStore.Width, options.width, NumberField); - } - if (options.height !== undefined) { - doc.SetData(KeyStore.Height, options.height, NumberField); - } - if (options.nativeWidth !== undefined) { - doc.SetData(KeyStore.NativeWidth, options.nativeWidth, NumberField); - } - if (options.nativeHeight !== undefined) { - doc.SetData(KeyStore.NativeHeight, options.nativeHeight, NumberField); - } - if (options.title !== undefined) { - doc.SetData(KeyStore.Title, options.title, TextField); + function GetImagePrototype(): Document { + if (!imageProto) { + imageProto = setupPrototypeOptions(imageProtoId, "IMAGE_PROTO", CollectionView.LayoutString("AnnotationsKey"), + { x: 0, y: 0, nativeWidth: 300, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations] }); + imageProto.SetText(KeyStore.BackgroundLayout, ImageBox.LayoutString()); } - doc.SetData(KeyStore.Scale, 1, NumberField); - doc.SetData(KeyStore.PanX, 0, NumberField); - doc.SetData(KeyStore.PanY, 0, NumberField); + return imageProto; } - - let textProto: Document; - const textProtoId = "textProto"; function GetTextPrototype(): Document { - if (!textProto) { - textProto = new Document(textProtoId); - textProto.Set(KeyStore.X, new NumberField(0)); - textProto.Set(KeyStore.Y, new NumberField(0)); - textProto.Set(KeyStore.Width, new NumberField(300)); - textProto.Set(KeyStore.Height, new NumberField(150)); - textProto.Set(KeyStore.Layout, new TextField(FormattedTextBox.LayoutString())); - textProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data])); - } - return textProto; + return textProto ? textProto : + textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(), + { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] }); } - - export function TextDocument(options: DocumentOptions = {}): Document { - let doc = GetTextPrototype().MakeDelegate(); - setupOptions(doc, options); - // doc.SetField(KeyStore.Data, new RichTextField()); - return doc; + function GetWebPrototype(): Document { + return webProto ? webProto : + webProto = setupPrototypeOptions(webProtoId, "WEB_PROTO", WebBox.LayoutString(), + { x: 0, y: 0, width: 300, height: 300, layoutKeys: [KeyStore.Data] }); + } + function GetCollectionPrototype(): Document { + return collProto ? collProto : + collProto = setupPrototypeOptions(collProtoId, "COLLECTION_PROTO", CollectionView.LayoutString("DataKey"), + { panx: 0, pany: 0, scale: 1, layoutKeys: [KeyStore.Data] }); } - let htmlProto: Document; - const htmlProtoId = "htmlProto"; - function GetHtmlPrototype(): Document { - if (!htmlProto) { - htmlProto = new Document(htmlProtoId); - htmlProto.Set(KeyStore.X, new NumberField(0)); - htmlProto.Set(KeyStore.Y, new NumberField(0)); - htmlProto.Set(KeyStore.Width, new NumberField(300)); - htmlProto.Set(KeyStore.Height, new NumberField(150)); - htmlProto.Set(KeyStore.Layout, new TextField(WebView.LayoutString())); - htmlProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data])); - } - return htmlProto; + function GetKVPPrototype(): Document { + return kvpProto ? kvpProto : + kvpProto = setupPrototypeOptions(kvpProtoId, "KVP_PROTO", KeyValueBox.LayoutString(), + { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] }) } - export function HtmlDocument(html: string, options: DocumentOptions = {}): Document { - let doc = GetHtmlPrototype().MakeDelegate(); - setupOptions(doc, options); - doc.Set(KeyStore.Data, new HtmlField(html)); + export function ImageDocument(url: string, options: DocumentOptions = {}) { + 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 TextDocument(options: DocumentOptions = {}) { + return SetInstanceOptions(GetTextPrototype(), options, "", TextField); + } + export function WebDocument(url: string, options: DocumentOptions = {}) { + return SetInstanceOptions(GetWebPrototype(), options, new URL(url), WebField); + } + export function HtmlDocument(html: string, options: DocumentOptions = {}) { + return SetInstanceOptions(GetWebPrototype(), options, html, HtmlField); + } + export function FreeformDocument(documents: Array<Document>, options: DocumentOptions, id?: string) { + return SetInstanceOptions(GetCollectionPrototype(), { ...options, viewType: CollectionViewType.Freeform }, documents, ListField, id) + } + export function SchemaDocument(documents: Array<Document>, options: DocumentOptions, id?: string) { + return SetInstanceOptions(GetCollectionPrototype(), { ...options, viewType: CollectionViewType.Schema }, documents, ListField, id) + } + export function DockDocument(config: string, options: DocumentOptions, id?: string) { + return SetInstanceOptions(GetCollectionPrototype(), { ...options, viewType: CollectionViewType.Docking }, config, TextField, id) + } + export function KVPDocument(document: Document, options: DocumentOptions = {}, id?: string) { + var deleg = GetKVPPrototype().MakeDelegate(id); + deleg.Set(KeyStore.Data, document); + return assignOptions(deleg, options); + } - let imageProto: Document; - const imageProtoId = "imageProto"; - function GetImagePrototype(): Document { - if (!imageProto) { - imageProto = new Document(imageProtoId); - imageProto.Set(KeyStore.Title, new TextField("IMAGE PROTO")); - imageProto.Set(KeyStore.X, new NumberField(0)); - imageProto.Set(KeyStore.Y, new NumberField(0)); - imageProto.Set(KeyStore.NativeWidth, new NumberField(300)); - imageProto.Set(KeyStore.NativeHeight, new NumberField(300)); - imageProto.Set(KeyStore.Width, new NumberField(300)); - imageProto.Set(KeyStore.Height, new NumberField(300)); - imageProto.Set(KeyStore.Layout, new TextField(CollectionView.LayoutString("AnnotationsKey"))); - imageProto.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform) - imageProto.Set(KeyStore.BackgroundLayout, new TextField(ImageBox.LayoutString())); - // imageProto.SetField(KeyStore.Layout, new TextField('<div style={"background-image: " + {Data}} />')); - imageProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data, KeyStore.Annotations])); - return imageProto; - } - return imageProto; - } // example of custom display string for an image that shows a caption. function EmbeddedCaption() { @@ -132,66 +149,14 @@ export namespace Documents { <div style="position:relative; margin:auto; height:85%;" >` + ImageBox.LayoutString() + `</div> - <div style="position:relative; overflow:auto; height:15%; text-align:center; ">` + <div style="position:relative; height:15%; text-align:center; ">` + FormattedTextBox.LayoutString("CaptionKey") + `</div> </div>` }; function FixedCaption() { return `<div style="position:absolute; height:30px; bottom:0; width:100%"> - <div style="position:absolute; width:100%; height:100%; overflow:auto;text-align:center;bottom:0;">` + <div style="position:absolute; width:100%; height:100%; text-align:center;bottom:0;">` + FormattedTextBox.LayoutString("CaptionKey") + `</div> </div>` }; - - export function ImageDocument(url: string, options: DocumentOptions = {}): Document { - let doc = GetImagePrototype().MakeDelegate(); - setupOptions(doc, options); - doc.Set(KeyStore.Data, new ImageField(new URL(url))); - doc.Set(KeyStore.Caption, new TextField("my caption...")); - doc.Set(KeyStore.BackgroundLayout, new TextField(EmbeddedCaption())); - doc.Set(KeyStore.OverlayLayout, new TextField(FixedCaption())); - doc.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data, KeyStore.Annotations, KeyStore.Caption])); - - let annotation = Documents.TextDocument({ title: "hello" }); - doc.Set(KeyStore.Annotations, new ListField([annotation])); - return doc; - } - - let collectionProto: Document; - const collectionProtoId = "collectionProto"; - function GetCollectionPrototype(): Document { - if (!collectionProto) { - collectionProto = new Document(collectionProtoId); - collectionProto.Set(KeyStore.Scale, new NumberField(1)); - collectionProto.Set(KeyStore.PanX, new NumberField(0)); - collectionProto.Set(KeyStore.PanY, new NumberField(0)); - collectionProto.Set(KeyStore.Layout, new TextField(CollectionView.LayoutString("DataKey"))); - collectionProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data])); - } - return collectionProto; - } - - export function CollectionDocument(data: Array<Document> | string, viewType: CollectionViewType, options: DocumentOptions = {}, id?: string): Document { - let doc = GetCollectionPrototype().MakeDelegate(id); - setupOptions(doc, options); - if (typeof data === "string") { - doc.SetText(KeyStore.Data, data); - } else { - doc.SetData(KeyStore.Data, data, ListField); - } - doc.SetNumber(KeyStore.ViewType, viewType); - return doc; - } - - export function FreeformDocument(documents: Array<Document>, options: DocumentOptions, id?: string) { - return CollectionDocument(documents, CollectionViewType.Freeform, options, id) - } - - export function SchemaDocument(documents: Array<Document>, options: DocumentOptions, id?: string) { - return CollectionDocument(documents, CollectionViewType.Schema, options, id) - } - - export function DockDocument(config: string, options: DocumentOptions, id?: string) { - return CollectionDocument(config, CollectionViewType.Docking, options, id) - } }
\ No newline at end of file diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index eb4b3aeaa..60910a40b 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,4 +1,37 @@ import { DocumentDecorations } from "../views/DocumentDecorations"; +import { CollectionDockingView } from "../views/collections/CollectionDockingView"; +import { Document } from "../../fields/Document" +import { action } from "mobx"; +import { DocumentView } from "../views/nodes/DocumentView"; + +export function setupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc: () => Document) { + let onRowMove = action((e: PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + + document.removeEventListener("pointermove", onRowMove); + document.removeEventListener('pointerup', onRowUp); + DragManager.StartDrag(_reference.current!, { document: docFunc() }); + }); + let onRowUp = action((e: PointerEvent): void => { + document.removeEventListener("pointermove", onRowMove); + document.removeEventListener('pointerup', onRowUp); + }); + let onItemDown = (e: React.PointerEvent) => { + // if (this.props.isSelected() || this.props.isTopMost) { + if (e.button == 0) { + e.stopPropagation(); + if (e.shiftKey) { + CollectionDockingView.Instance.StartOtherDrag(docFunc(), e); + } else { + document.addEventListener("pointermove", onRowMove); + document.addEventListener('pointerup', onRowUp); + } + } + //} + } + return onItemDown; +} export namespace DragManager { export function Root() { @@ -61,7 +94,7 @@ export namespace DragManager { }; } - export function StartDrag(ele: HTMLElement, dragData: { [id: string]: any }, options: DragOptions) { + export function StartDrag(ele: HTMLElement, dragData: { [id: string]: any }, options?: DragOptions) { DocumentDecorations.Instance.Hidden = true; if (!dragDiv) { dragDiv = document.createElement("div"); @@ -75,6 +108,8 @@ export namespace DragManager { let dragElement = ele.cloneNode(true) as HTMLElement; dragElement.style.opacity = "0.7"; dragElement.style.position = "absolute"; + dragElement.style.bottom = ""; + dragElement.style.left = ""; dragElement.style.transformOrigin = "0 0"; dragElement.style.zIndex = "1000"; dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`; @@ -87,38 +122,54 @@ export namespace DragManager { dragDiv.appendChild(dragElement); let hideSource = false; - if (typeof options.hideSource === "boolean") { - hideSource = options.hideSource; - } else { - hideSource = options.hideSource(); + if (options) { + if (typeof options.hideSource === "boolean") { + hideSource = options.hideSource; + } else { + hideSource = options.hideSource(); + } } const wasHidden = ele.hidden; if (hideSource) { ele.hidden = true; } - const moveHandler = (e: PointerEvent) => { e.stopPropagation(); e.preventDefault(); x += e.movementX; y += e.movementY; + if (e.shiftKey) { + abortDrag(); + const docView: DocumentView = dragData["documentView"]; + const doc: Document = docView ? docView.props.Document : dragData["document"]; + CollectionDockingView.Instance.StartOtherDrag(doc, { pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 }); + } dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`; }; - const upHandler = (e: PointerEvent) => { + + const abortDrag = () => { document.removeEventListener("pointermove", moveHandler, true); document.removeEventListener("pointerup", upHandler); - FinishDrag(dragElement, e, options, dragData); + dragDiv.removeChild(dragElement); if (hideSource && !wasHidden) { ele.hidden = false; } + } + const upHandler = (e: PointerEvent) => { + abortDrag(); + FinishDrag(ele, e, dragData, options); }; document.addEventListener("pointermove", moveHandler, true); document.addEventListener("pointerup", upHandler); } - function FinishDrag(dragEle: HTMLElement, e: PointerEvent, options: DragOptions, dragData: { [index: string]: any }) { - dragDiv.removeChild(dragEle); + function FinishDrag(dragEle: HTMLElement, e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions) { + let parent = dragEle.parentElement; + if (parent) + parent.removeChild(dragEle); const target = document.elementFromPoint(e.x, e.y); + if (parent) + parent.appendChild(dragEle); if (!target) { return; } @@ -130,7 +181,9 @@ export namespace DragManager { data: dragData } })); - options.handlers.dragComplete({}); + if (options) { + options.handlers.dragComplete({}); + } DocumentDecorations.Instance.Hidden = false; } }
\ No newline at end of file diff --git a/src/client/util/Transform.ts b/src/client/util/Transform.ts index 9fd4f7bef..3e1039166 100644 --- a/src/client/util/Transform.ts +++ b/src/client/util/Transform.ts @@ -102,6 +102,12 @@ export class Transform { return [x * this._scale, y * this._scale]; } + transformBounds(x: number, y: number, width: number, height: number): { x: number, y: number, width: number, height: number } { + [x, y] = this.transformPoint(x, y); + [width, height] = this.transformDirection(width, height); + return { x, y, width, height }; + } + inverse = () => { return new Transform(-this._translateX / this._scale, -this._translateY / this._scale, 1 / this._scale) } diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index 4f26a75d2..9459d45f8 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -1,6 +1,6 @@ import React = require("react"); import { ContextMenuItem, ContextMenuProps } from "./ContextMenuItem"; -import { observable } from "mobx"; +import { observable, action } from "mobx"; import { observer } from "mobx-react"; import "./ContextMenu.scss" @@ -23,11 +23,13 @@ export class ContextMenu extends React.Component { ContextMenu.Instance = this; } + @action clearItems() { this._items = [] this._display = "none" } + @action addItem(item: ContextMenuProps) { if (this._items.indexOf(item) === -1) { this._items.push(item); @@ -38,6 +40,7 @@ export class ContextMenu extends React.Component { return this._items; } + @action displayMenu(x: number, y: number) { this._pageX = x this._pageY = y diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 2f012913d..975a125f7 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -104,25 +104,26 @@ export class DocumentDecorations extends React.Component { const rect = element.screenRect(); if (rect.width !== 0) { let doc = element.props.Document; - let width = doc.GetOrCreate(KeyStore.Width, NumberField); - let height = doc.GetOrCreate(KeyStore.Height, NumberField); + let width = doc.GetNumber(KeyStore.Width, 0); + let nwidth = doc.GetNumber(KeyStore.NativeWidth, 0); + let nheight = doc.GetNumber(KeyStore.NativeHeight, 0); + let height = doc.GetNumber(KeyStore.Height, nwidth ? nheight / nwidth * width : 0); let x = doc.GetOrCreate(KeyStore.X, NumberField); let y = doc.GetOrCreate(KeyStore.Y, NumberField); - let scale = width.Data / rect.width; - let actualdW = Math.max(width.Data + (dW * scale), 20); - let actualdH = Math.max(height.Data + (dH * scale), 20); - x.Data += dX * (actualdW - width.Data); - y.Data += dY * (actualdH - height.Data); + let scale = width / rect.width; + let actualdW = Math.max(width + (dW * scale), 20); + let actualdH = Math.max(height + (dH * scale), 20); + x.Data += dX * (actualdW - width); + y.Data += dY * (actualdH - height); var nativeWidth = doc.GetNumber(KeyStore.NativeWidth, 0); var nativeHeight = doc.GetNumber(KeyStore.NativeHeight, 0); if (nativeWidth > 0 && nativeHeight > 0) { if (Math.abs(dW) > Math.abs(dH)) actualdH = nativeHeight / nativeWidth * actualdW; - else - actualdW = nativeWidth / nativeHeight * actualdH; + else actualdW = nativeWidth / nativeHeight * actualdH; } - width.Data = actualdW; - height.Data = actualdH; + doc.SetNumber(KeyStore.Width, actualdW); + doc.SetNumber(KeyStore.Height, actualdH); } }) } diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index e73f62904..4334ed299 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -28,4 +28,24 @@ h1 { p { margin: 0px; padding: 0px; -}
\ No newline at end of file +} +::-webkit-scrollbar { + -webkit-appearance: none; + height:5px; + width:5px; +} +::-webkit-scrollbar-thumb { + border-radius: 2px; + background-color: rgba(0,0,0,.5); +} + +.main-buttonDiv { + position: absolute; + width: 150px; + left: 0px; +} +.main-undoButtons { + position: absolute; + width: 150px; + right: 0px; +} diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index db7138965..88bf0934c 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -1,216 +1,143 @@ -import { action, configure, reaction, computed, observable } from 'mobx'; +import { action, configure, observable } from 'mobx'; import "normalize.css"; import * as React from 'react'; import * as ReactDOM from 'react-dom'; -import { DocumentDecorations } from './DocumentDecorations'; -import { Documents } from '../documents/Documents'; import { Document } from '../../fields/Document'; import { KeyStore } from '../../fields/KeyStore'; -import "./Main.scss"; -import { ContextMenu } from './ContextMenu'; -import { DocumentView } from './nodes/DocumentView'; -import { Server } from '../Server'; import { Utils } from '../../Utils'; import { ServerUtils } from '../../server/ServerUtil'; import { MessageStore, DocumentTransfer } from '../../server/Message'; import { Database } from '../../server/database'; import * as request from 'request' +import { Documents } from '../documents/Documents'; +import { Server } from '../Server'; +import { setupDrag } from '../util/DragManager'; import { Transform } from '../util/Transform'; -import { CollectionDockingView } from './collections/CollectionDockingView'; -import { FieldWaiting } from '../../fields/Field'; import { UndoManager } from '../util/UndoManager'; import { WorkspacesMenu } from '../../server/authentication/controllers/WorkspacesMenu'; +import { CollectionDockingView } from './collections/CollectionDockingView'; +import { ContextMenu } from './ContextMenu'; +import { DocumentDecorations } from './DocumentDecorations'; +import { DocumentView } from './nodes/DocumentView'; +import "./Main.scss"; +import { observer } from 'mobx-react'; +@observer +export class Main extends React.Component { + @observable private mainDocId = "mainDoc"; + // dummy document initializations keep the compiler happy + @observable private mainContainer: Document = new Document; + @observable private mainfreeform: Document = new Document; -configure({ - enforceActions: "observed" -}); -window.addEventListener("drop", function (e) { - e.preventDefault(); -}, false) -window.addEventListener("dragover", function (e) { - e.preventDefault(); -}, false) -document.addEventListener("pointerdown", action(function (e: PointerEvent) { - if (!ContextMenu.Instance.intersects(e.pageX, e.pageY)) { - ContextMenu.Instance.clearItems() + constructor(props: Readonly<{}>) { + super(props); + // causes errors to be generated when modifying an observable outside of an action + configure({ enforceActions: "observed" }); + this.initEventListeners(); + this.initAuthenticationRouters(); } -}), true) -// Load the user's active workspace, or create a new one if initial session after signup -request.get(window.location.origin + "/getActiveWorkspaceId", (error, response, body) => { - init(body ? body : getNewWorkspace()); -}); + initEventListeners = () => { + window.addEventListener("drop", (e) => e.preventDefault(), false) // drop event handler + window.addEventListener("dragover", (e) => e.preventDefault(), false) // drag event handler + // click interactions for the context menu + document.addEventListener("pointerdown", action(function (e: PointerEvent) { + if (!ContextMenu.Instance.intersects(e.pageX, e.pageY)) { + ContextMenu.Instance.clearItems(); + } + }), true); + } -function getNewWorkspace(): string { - let newId = Utils.GenerateGuid(); - const here = window.location.origin; - request.post(here + "/addWorkspaceId", { - body: { - target: newId - }, - json: true - }) - request.post(here + "/setActiveWorkspaceId", { - body: { - target: newId - }, - json: true - }) - return newId; -} + initAuthenticationRouters = () => { + // Load the user's active workspace, or create a new one if initial session after signup + request.get(window.location.origin + "/getActiveWorkspaceId", (error, response, body) => { + this.initRender(body ? body : this.getNewWorkspace()); + }); + } -//runInAction(() => -// let doc1 = Documents.TextDocument({ title: "hello" }); -// let doc2 = doc1.MakeDelegate(); -// doc2.Set(KS.X, new NumberField(150)); -// doc2.Set(KS.Y, new NumberField(20)); -// let doc3 = Documents.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { -// x: 450, y: 100, title: "cat 1" -// }); -// doc3.Set(KeyStore.Data, new ImageField); -// const schemaDocs = Array.from(Array(5).keys()).map(v => Documents.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { -// x: 50 + 100 * v, y: 50, width: 100, height: 100, title: "cat" + v -// })); -// schemaDocs[0].SetData(KS.Author, "Tyler", TextField); -// schemaDocs[4].SetData(KS.Author, "Bob", TextField); -// schemaDocs.push(doc2); -// const doc7 = Documents.SchemaDocument(schemaDocs) -function init(mainDocId: string) { - Documents.initProtos(() => { - Utils.EmitCallback(Server.Socket, MessageStore.GetField, mainDocId, (res: any) => { - console.log("HELLO WORLD") - console.log("RESPONSE: " + res) - let mainContainer: Document; - let mainfreeform: Document; - if (res) { - mainContainer = ServerUtils.FromJson(res) as Document; - mainContainer.GetAsync(KeyStore.ActiveFrame, field => mainfreeform = field as Document); + getNewWorkspace = (): string => { + let newId = Utils.GenerateGuid(); + const here = window.location.origin; + request.post(here + "/addWorkspaceId", { + body: { target: newId }, + json: true + }) + request.post(here + "/setActiveWorkspaceId", { + body: { target: newId }, + json: true + }) + return newId; + } + + initRender = (activeWorkspaceId: string) => { + Documents.initProtos(activeWorkspaceId, (res?: Document) => { + if (res instanceof Document) { + this.mainContainer = res; + this.mainContainer.GetAsync(KeyStore.ActiveFrame, field => this.mainfreeform = field as Document); } else { - mainContainer = Documents.DockDocument(JSON.stringify({ content: [{ type: 'row', content: [] }] }), { title: "main container" }, mainDocId); - Utils.Emit(Server.Socket, MessageStore.AddDocument, new DocumentTransfer(mainContainer.ToJson())) + this.mainContainer = Documents.DockDocument(JSON.stringify({ content: [{ type: 'row', content: [] }] }), { title: "main container" }, this.mainDocId); - mainfreeform = Documents.FreeformDocument([], { x: 0, y: 400, title: "mini collection" }); - Utils.Emit(Server.Socket, MessageStore.AddDocument, new DocumentTransfer(mainfreeform.ToJson())); + // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) + setTimeout(() => { + this.mainfreeform = Documents.FreeformDocument([], { x: 0, y: 400, title: "mini collection" }); - var docs = [mainfreeform].map(doc => CollectionDockingView.makeDocumentConfig(doc)); - mainContainer.SetText(KeyStore.Data, JSON.stringify({ content: [{ type: 'row', content: docs }] })); - mainContainer.Set(KeyStore.ActiveFrame, mainfreeform); + var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(this.mainfreeform)] }] }; + this.mainContainer.SetText(KeyStore.Data, JSON.stringify(dockingLayout)); + this.mainContainer.Set(KeyStore.ActiveFrame, this.mainfreeform); + }, 0); } + }); + } - let addImageNode = action(() => { - mainfreeform.GetList<Document>(KeyStore.Data, []).push(Documents.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { - x: 0, y: 300, width: 200, height: 200, title: "added note" - })); - }) - let addTextNode = action(() => { - mainfreeform.GetList<Document>(KeyStore.Data, []).push(Documents.TextDocument({ - x: 0, y: 300, width: 200, height: 200, title: "added note" - })); - }) - let addColNode = action(() => { - mainfreeform.GetList<Document>(KeyStore.Data, []).push(Documents.FreeformDocument([], { - x: 0, y: 300, width: 200, height: 200, title: "added note" - })); - }) - let addSchemaNode = action(() => { - mainfreeform.GetList<Document>(KeyStore.Data, []).push(Documents.SchemaDocument([Documents.TextDocument()], { - x: 0, y: 300, width: 200, height: 200, title: "added note" - })); - }) + render() { + let imgRef = React.createRef<HTMLDivElement>(); + let webRef = React.createRef<HTMLDivElement>(); + let textRef = React.createRef<HTMLDivElement>(); + let schemaRef = React.createRef<HTMLDivElement>(); + let colRef = React.createRef<HTMLDivElement>(); - let clearDatabase = action(() => { - Utils.Emit(Server.Socket, MessageStore.DeleteAll, {}); - }) + let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; + let weburl = "https://cs.brown.edu/courses/cs166/"; + let clearDatabase = action(() => Utils.Emit(Server.Socket, MessageStore.DeleteAll, {})) + let addTextNode = action(() => Documents.TextDocument({ width: 200, height: 200, title: "a text note" })) + let addColNode = action(() => Documents.FreeformDocument([], { width: 200, height: 200, title: "a feeform collection" })); + let addSchemaNode = action(() => Documents.SchemaDocument([Documents.TextDocument()], { width: 200, height: 200, title: "a schema collection" })); + let addImageNode = action(() => Documents.ImageDocument(imgurl, { width: 200, height: 200, title: "an image of a cat" })); + let addWebNode = action(() => Documents.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" })); - ReactDOM.render(( - <div style={{ position: "absolute", width: "100%", height: "100%" }}> - <DocumentView Document={mainContainer} - AddDocument={undefined} RemoveDocument={undefined} ScreenToLocalTransform={() => Transform.Identity} - ContentScaling={() => 1} - PanelWidth={() => 0} - PanelHeight={() => 0} - isTopMost={true} - ContainingCollectionView={undefined} /> - <DocumentDecorations /> - <ContextMenu /> - <button style={{ - position: 'absolute', - bottom: '0px', - left: '0px', - width: '150px' - }} onClick={addImageNode}>Add Image</button> - <button style={{ - position: 'absolute', - bottom: '25px', - left: '0px', - width: '150px' - }} onClick={addTextNode}>Add Text</button> - <button style={{ - position: 'absolute', - bottom: '50px', - left: '0px', - width: '150px' - }} onClick={addColNode}>Add Collection</button> - <button style={{ - position: 'absolute', - bottom: '100', - left: '0px', - width: '150px' - }} onClick={addSchemaNode}>Add Schema</button> - <button style={{ - position: 'absolute', - bottom: '75px', - left: '0px', - width: '150px' - }} onClick={clearDatabase}>Clear Database</button> - <button style={{ - position: 'absolute', - bottom: '25', - right: '0px', - width: '150px' - }} onClick={() => UndoManager.Undo()}>Undo</button> - <button style={{ - position: 'absolute', - bottom: '0', - right: '0px', - width: '150px' - }} onClick={() => UndoManager.Redo()}>Redo</button> - <button style={{ - position: 'absolute', - top: '24px', - right: '0px', - width: '150px' - }} onClick={() => window.location.pathname = "/logout"}>Logout</button> - <button style={{ - position: 'absolute', - top: '24px', - left: '4px', - width: '150px' - }} onClick={() => WorkspacesMenu.Instance.toggle()}>Workspaces</button> - <WorkspacesMenu load={init} new={getNewWorkspace} active={mainDocId} /> - </div>), - document.getElementById('root')); - }) - }); + let addClick = (creator: () => Document) => action(() => this.mainfreeform.GetList<Document>(KeyStore.Data, []).push(creator())); + + return ( + <div style={{ position: "absolute", width: "100%", height: "100%" }}> + <DocumentView Document={this.mainContainer} + AddDocument={undefined} RemoveDocument={undefined} ScreenToLocalTransform={() => Transform.Identity} + ContentScaling={() => 1} + PanelWidth={() => 0} + PanelHeight={() => 0} + isTopMost={true} + SelectOnLoad={false} + ContainingCollectionView={undefined} /> + <DocumentDecorations /> + <ContextMenu /> + <div className="main-buttonDiv" style={{ bottom: '0px' }} ref={imgRef} > + <button onPointerDown={setupDrag(imgRef, addImageNode)} onClick={addClick(addImageNode)}>Add Image</button></div> + <div className="main-buttonDiv" style={{ bottom: '25px' }} ref={webRef} > + <button onPointerDown={setupDrag(webRef, addWebNode)} onClick={addClick(addWebNode)}>Add Web</button></div> + <div className="main-buttonDiv" style={{ bottom: '50px' }} ref={textRef}> + <button onPointerDown={setupDrag(textRef, addTextNode)} onClick={addClick(addTextNode)}>Add Text</button></div> + <div className="main-buttonDiv" style={{ bottom: '75px' }} ref={colRef}> + <button onPointerDown={setupDrag(colRef, addColNode)} onClick={addClick(addColNode)}>Add Collection</button></div> + <div className="main-buttonDiv" style={{ bottom: '100px' }} ref={schemaRef}> + <button onPointerDown={setupDrag(schemaRef, addSchemaNode)} onClick={addClick(addSchemaNode)}>Add Schema</button></div> + <div className="main-buttonDiv" style={{ bottom: '125px' }} > + <button onClick={clearDatabase}>Clear Database</button></div> + <button className="main-undoButtons" style={{ bottom: '25px' }} onClick={() => UndoManager.Undo()}>Undo</button> + <button className="main-undoButtons" style={{ bottom: '0px' }} onClick={() => UndoManager.Redo()}>Redo</button> + <WorkspacesMenu active={this.mainDocId} load={this.initRender} new={this.getNewWorkspace} /> + </div> + ); + } } -// let doc5 = Documents.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { -// x: 650, y: 500, width: 600, height: 600, title: "cat 2" -// }); -// let docset2 = new Array<Document>(doc4);//, doc1, doc3); -// let doc6 = Documents.CollectionDocument(docset2, { -// x: 350, y: 100, width: 600, height: 600, title: "docking collection" -// }); -// let mainNodes = mainContainer.GetOrCreate(KeyStore.Data, ListField); -// mainNodes.Data.push(doc6); -// mainNodes.Data.push(doc2); -// mainNodes.Data.push(doc4); -// mainNodes.Data.push(doc3); -// mainNodes.Data.push(doc5); -// mainNodes.Data.push(doc1); -//mainNodes.Data.push(doc2); -//mainNodes.Data.push(doc6); -// mainContainer.Set(KeyStore.Data, mainNodes); -//} -//); + +ReactDOM.render(<Main />, document.getElementById('root'));
\ No newline at end of file diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index 7c0b512a7..2706c3272 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -1,3 +1,7 @@ +.collectiondockingview-content { + height: 100%; +} + .collectiondockingview-container { position: relative; top: 0; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index ac8bac22e..40a6213dd 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -10,7 +10,6 @@ import { FieldId, Opt, Field } from "../../../fields/Field"; import { KeyStore } from "../../../fields/KeyStore"; import { Utils } from "../../../Utils"; import { Server } from "../../Server"; -import { DragManager } from "../../util/DragManager"; import { undoBatch } from "../../util/UndoManager"; import { DocumentView } from "../nodes/DocumentView"; import "./CollectionDockingView.scss"; @@ -34,10 +33,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } private _goldenLayout: any = null; - private _dragDiv: any = null; - private _dragParent: HTMLElement | null = null; - private _dragElement: HTMLDivElement | undefined; - private _dragFakeElement: HTMLDivElement | undefined; private _containerRef = React.createRef<HTMLDivElement>(); private _fullScreen: any = null; @@ -47,28 +42,8 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp (window as any).React = React; (window as any).ReactDOM = ReactDOM; } - - public StartOtherDrag(dragElement: HTMLDivElement, dragDoc: Document) { - this._dragElement = dragElement; - this._dragParent = dragElement.parentElement; - // bcz: we want to copy this document into the header, not move it there. - // However, GoldenLayout is setup to move things, so we have to do some kludgy stuff: - - // - create a temporary invisible div and register that as a DragSource with GoldenLayout - this._dragDiv = document.createElement("div"); - this._dragDiv.style.opacity = 0; - DragManager.Root().appendChild(this._dragDiv); - this._goldenLayout.createDragSource(this._dragDiv, CollectionDockingView.makeDocumentConfig(dragDoc)); - - // - add our document to that div so that GoldenLayout will get the move events its listening for - this._dragDiv.appendChild(this._dragElement); - - // - add a duplicate of our document to the original document's container - // (GoldenLayout will be removing our original one) - this._dragFakeElement = dragElement.cloneNode(true) as HTMLDivElement; - this._dragParent!.appendChild(this._dragFakeElement); - - // all of this must be undone when the document has been dropped (see tabCreated) + public StartOtherDrag(dragDoc: Document, e: any) { + this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener.onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: e.button }) } @action @@ -98,7 +73,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp // Creates a vertical split on the right side of the docking view, and then adds the Document to that split // @action - public AddRightSplit(document: Document) { + public AddRightSplit(document: Document, minimize: boolean = false) { this._goldenLayout.emit('stateChanged'); let newItemStackConfig = { type: 'stack', @@ -121,10 +96,15 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp collayout.config["width"] = 50; newContentItem.config["width"] = 50; } + if (minimize) { + newContentItem.config["width"] = 10; + newContentItem.config["height"] = 10; + } newContentItem.callDownwards('_$init'); this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]); this._goldenLayout.emit('stateChanged'); this.stateChanged(); + return newContentItem; } setupGoldenLayout() { @@ -218,13 +198,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp this.stateChanged(); } tabCreated = (tab: any) => { - if (this._dragDiv) { - this._dragDiv.removeChild(this._dragElement); - this._dragParent!.removeChild(this._dragFakeElement!); - this._dragParent!.appendChild(this._dragElement!); - DragManager.Root().removeChild(this._dragDiv); - this._dragDiv = null; - } tab.closeElement.off('click') //unbind the current click handler .click(function () { tab.contentItem.remove(); @@ -235,17 +208,17 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp //stack.header.controlsContainer.find('.lm_popout').hide(); stack.header.controlsContainer.find('.lm_close') //get the close icon .off('click') //unbind the current click handler - .click(function () { + .click(action(function () { //if (confirm('really close this?')) { stack.remove(); //} - }); + })); } render() { return ( <div className="collectiondockingview-container" id="menuContainer" - onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} onContextMenu={(e) => e.preventDefault()} ref={this._containerRef} + onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} style={{ width: "100%", height: "100%", @@ -263,8 +236,9 @@ interface DockedFrameProps { @observer export class DockedFrameRenderer extends React.Component<DockedFrameProps> { - @observable private _mainCont = React.createRef<HTMLDivElement>(); + private _mainCont = React.createRef<HTMLDivElement>(); @observable private _panelWidth = 0; + @observable private _panelHeight = 0; @observable private _document: Opt<Document>; constructor(props: any) { @@ -272,8 +246,8 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { Server.GetField(this.props.documentId, action((f: Opt<Field>) => this._document = f as Document)); } - private _nativeWidth = () => { return this._document!.GetNumber(KeyStore.NativeWidth, 0); } - private _nativeHeight = () => { return this._document!.GetNumber(KeyStore.NativeHeight, 0); } + private _nativeWidth = () => { return this._document!.GetNumber(KeyStore.NativeWidth, this._panelWidth); } + private _nativeHeight = () => { return this._document!.GetNumber(KeyStore.NativeHeight, this._panelHeight); } private _contentScaling = () => { return this._panelWidth / (this._nativeWidth() ? this._nativeWidth() : this._panelWidth); } ScreenToLocalTransform = () => { @@ -294,10 +268,11 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { PanelHeight={this._nativeHeight} ScreenToLocalTransform={this.ScreenToLocalTransform} isTopMost={true} + SelectOnLoad={false} ContainingCollectionView={undefined} /> </div> - return <Measure onResize={action((r: any) => this._panelWidth = r.entry.width)}> + return <Measure onResize={action((r: any) => { this._panelWidth = r.entry.width; this._panelHeight = r.entry.height; })}> {({ measureRef }) => <div ref={measureRef}> {content} </div>} </Measure> } diff --git a/src/client/views/collections/CollectionFreeFormView.scss b/src/client/views/collections/CollectionFreeFormView.scss index 5f930d135..2ec22367f 100644 --- a/src/client/views/collections/CollectionFreeFormView.scss +++ b/src/client/views/collections/CollectionFreeFormView.scss @@ -1,14 +1,7 @@ .collectionfreeformview-container { - ::-webkit-scrollbar { - -webkit-appearance: none; - width: 10px; - } - ::-webkit-scrollbar-thumb { - border-radius: 5px; - background-color: rgba(0, 0, 0, .5); - } - .collectionfreeformview>.jsx-parser { - position: absolute; + + .collectionfreeformview > .jsx-parser{ + position:absolute; height: 100%; } border-style: solid; @@ -26,4 +19,22 @@ width: 100%; height: 100% } +} + +.border { + border-style: solid; + box-sizing: border-box; + width: 100%; + height: 100%; +} + +//this is an animation for the blinking cursor! +@keyframes blink { + 0% {opacity: 0} + 49%{opacity: 0} + 50% {opacity: 1} +} + +#prevCursor { + animation: blink 1s infinite; }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx index c40da6eaa..5e9dcd5d5 100644 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -1,4 +1,4 @@ -import { action, computed } from "mobx"; +import { observable, action, computed } from "mobx"; import { observer } from "mobx-react"; import { Document } from "../../../fields/Document"; import { FieldWaiting } from "../../../fields/Field"; @@ -13,14 +13,15 @@ import { CollectionSchemaView } from "../collections/CollectionSchemaView"; import { CollectionView } from "../collections/CollectionView"; import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; import { DocumentView } from "../nodes/DocumentView"; -import { WebView } from "../nodes/WebView"; import { FormattedTextBox } from "../nodes/FormattedTextBox"; import { ImageBox } from "../nodes/ImageBox"; +import { WebBox } from "../nodes/WebBox"; +import { KeyValueBox } from "../nodes/KeyValueBox" import "./CollectionFreeFormView.scss"; import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; import { CollectionViewBase } from "./CollectionViewBase"; -import React = require("react"); import { Documents } from "../../documents/Documents"; +import React = require("react"); const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this? @observer @@ -28,9 +29,16 @@ export class CollectionFreeFormView extends CollectionViewBase { private _canvasRef = React.createRef<HTMLDivElement>(); private _lastX: number = 0; private _lastY: number = 0; + private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type) + + @observable private _downX: number = 0; + @observable private _downY: number = 0; + //determines whether the blinking cursor for indicating whether a text will be made on key down is visible + @observable + private _previewCursorVisible: boolean = false; @computed get panX(): number { return this.props.Document.GetNumber(KeyStore.PanX, 0) } @computed get panY(): number { return this.props.Document.GetNumber(KeyStore.PanY, 0) } @@ -39,21 +47,20 @@ export class CollectionFreeFormView extends CollectionViewBase { @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } @computed get zoomScaling() { return this.props.Document.GetNumber(KeyStore.Scale, 1); } + @computed get centeringShiftX() { return !this.props.Document.GetNumber(KeyStore.NativeWidth, 0) ? this.props.panelWidth() / 2 : 0; } // shift so pan position is at center of window for non-overlay collections + @computed get centeringShiftY() { return !this.props.Document.GetNumber(KeyStore.NativeHeight, 0) ? this.props.panelHeight() / 2 : 0; }// shift so pan position is at center of window for non-overlay collections @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { super.drop(e, de); - const doc: DocumentView = de.data["document"]; - const xOffset = de.data["xOffset"] as number || 0; - const yOffset = de.data["yOffset"] as number || 0; - //this should be able to use translate and scale methods on an Identity transform, no? - const transform = this.getTransform(); - const screenX = de.x - xOffset; - const screenY = de.y - yOffset; - const [x, y] = transform.transformPoint(screenX, screenY); - doc.props.Document.SetNumber(KeyStore.X, x); - doc.props.Document.SetNumber(KeyStore.Y, y); + const docView: DocumentView = de.data["documentView"]; + let doc: Document = docView ? docView.props.Document : de.data["document"]; + let screenX = de.x - (de.data["xOffset"] as number || 0); + let screenY = de.y - (de.data["yOffset"] as number || 0); + const [x, y] = this.getTransform().transformPoint(screenX, screenY); + doc.SetNumber(KeyStore.X, x); + doc.SetNumber(KeyStore.Y, y); this.bringToFront(doc); } @@ -65,8 +72,10 @@ export class CollectionFreeFormView extends CollectionViewBase { document.addEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointerup", this.onPointerUp); - this._downX = this._lastX = e.pageX; - this._downY = this._lastY = e.pageY; + this._lastX = e.pageX; + this._lastY = e.pageY; + this._downX = e.pageX; + this._downY = e.pageY; } } @@ -76,22 +85,25 @@ export class CollectionFreeFormView extends CollectionViewBase { document.removeEventListener("pointerup", this.onPointerUp); e.stopPropagation(); if (Math.abs(this._downX - e.clientX) < 3 && Math.abs(this._downY - e.clientY) < 3) { + //show preview text cursor on tap + this._previewCursorVisible = true; + //select is not already selected if (!this.props.isSelected()) { this.props.select(false); } } + } @action onPointerMove = (e: PointerEvent): void => { if (!e.cancelBubble && this.props.active()) { - e.preventDefault(); e.stopPropagation(); let x = this.props.Document.GetNumber(KeyStore.PanX, 0); let y = this.props.Document.GetNumber(KeyStore.PanY, 0); - let [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); - - this.SetPan(x + dx, y + dy); + let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); + this._previewCursorVisible = false; + this.SetPan(x - dx, y - dy); } this._lastX = e.pageX; this._lastY = e.pageY; @@ -102,35 +114,46 @@ export class CollectionFreeFormView extends CollectionViewBase { e.stopPropagation(); e.preventDefault(); let coefficient = 1000; - // if (modes[e.deltaMode] == 'pixels') coefficient = 50; - // else if (modes[e.deltaMode] == 'lines') coefficient = 1000; // This should correspond to line-height?? - let transform = this.getTransform(); - let deltaScale = (1 - (e.deltaY / coefficient)); - let [x, y] = transform.transformPoint(e.clientX, e.clientY); + if (e.ctrlKey) { + var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); + var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0); + const coefficient = 1000; + let deltaScale = (1 - (e.deltaY / coefficient)); + this.props.Document.SetNumber(KeyStore.NativeWidth, nativeWidth * deltaScale); + this.props.Document.SetNumber(KeyStore.NativeHeight, nativeHeight * deltaScale); + e.stopPropagation(); + e.preventDefault(); + } else { + // if (modes[e.deltaMode] == 'pixels') coefficient = 50; + // else if (modes[e.deltaMode] == 'lines') coefficient = 1000; // This should correspond to line-height?? + let transform = this.getTransform(); + + let deltaScale = (1 - (e.deltaY / coefficient)); + if (deltaScale * this.zoomScaling < 1 && this.isAnnotationOverlay) + deltaScale = 1 / this.zoomScaling; + let [x, y] = transform.transformPoint(e.clientX, e.clientY); - let localTransform = this.getLocalTransform(); - localTransform = localTransform.inverse().scaleAbout(deltaScale, x, y) + let localTransform = this.getLocalTransform() + localTransform = localTransform.inverse().scaleAbout(deltaScale, x, y) + console.log(localTransform) - this.props.Document.SetNumber(KeyStore.Scale, localTransform.Scale); - this.SetPan(localTransform.TranslateX, localTransform.TranslateY); + this.props.Document.SetNumber(KeyStore.Scale, localTransform.Scale); + this.SetPan(-localTransform.TranslateX / localTransform.Scale, -localTransform.TranslateY / localTransform.Scale); + } } @action private SetPan(panX: number, panY: number) { const newPanX = Math.max((1 - this.zoomScaling) * this.nativeWidth, Math.min(0, panX)); const newPanY = Math.max((1 - this.zoomScaling) * this.nativeHeight, Math.min(0, panY)); - this.props.Document.SetNumber(KeyStore.PanX, false && this.isAnnotationOverlay ? newPanX : panX); - this.props.Document.SetNumber(KeyStore.PanY, false && this.isAnnotationOverlay ? newPanY : panY); + this.props.Document.SetNumber(KeyStore.PanX, this.isAnnotationOverlay ? newPanX : panX); + this.props.Document.SetNumber(KeyStore.PanY, this.isAnnotationOverlay ? newPanY : panY); } @action onDrop = (e: React.DragEvent): void => { - const panx: number = this.props.Document.GetNumber(KeyStore.PanX, 0); - const pany: number = this.props.Document.GetNumber(KeyStore.PanY, 0); - let transform = this.getTransform(); - - var pt = transform.transformPoint(e.pageX, e.pageY); + var pt = this.getTransform().transformPoint(e.pageX, e.pageY); super.onDrop(e, { x: pt[0], y: pt[1] }); } @@ -138,15 +161,33 @@ export class CollectionFreeFormView extends CollectionViewBase { } @action - bringToFront(doc: DocumentView) { + onKeyDown = (e: React.KeyboardEvent<Element>) => { + //if not these keys, make a textbox if preview cursor is active! + if (!e.ctrlKey && !e.altKey && !e.shiftKey) { + if (this._previewCursorVisible) { + //make textbox and add it to this collection + let [x, y] = this.getTransform().transformPoint(this._downX, this._downY); (this._downX, this._downY); + let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "new" }); + // mark this collection so that when the text box is created we can send it the SelectOnLoad prop to focus itself + this._selectOnLoaded = newBox.Id; + //set text to be the typed key and get focus on text box + this.props.CollectionView.addDocument(newBox); + //remove cursor from screen + this._previewCursorVisible = false; + } + } + } + + @action + bringToFront(doc: Document) { const { fieldKey: fieldKey, Document: Document } = this.props; const value: Document[] = Document.GetList<Document>(fieldKey, []).slice(); value.sort((doc1, doc2) => { - if (doc1 === doc.props.Document) { + if (doc1 === doc) { return 1; } - if (doc2 === doc.props.Document) { + if (doc2 === doc) { return -1; } return doc1.GetNumber(KeyStore.ZIndex, 0) - doc2.GetNumber(KeyStore.ZIndex, 0); @@ -155,7 +196,6 @@ export class CollectionFreeFormView extends CollectionViewBase { }); } - @computed get backgroundLayout(): string | undefined { let field = this.props.Document.GetT(KeyStore.BackgroundLayout, TextField); if (field && field !== "<Waiting>") { @@ -170,15 +210,15 @@ export class CollectionFreeFormView extends CollectionViewBase { } @computed get views() { - const { fieldKey, Document } = this.props; - const lvalue = Document.GetT<ListField<Document>>(fieldKey, ListField); + const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField); if (lvalue && lvalue != FieldWaiting) { return lvalue.Data.map(doc => { - return (<CollectionFreeFormDocumentView key={doc.Id} Document={doc} + return (<CollectionFreeFormDocumentView key={doc.Id} Document={doc} ref={focus} AddDocument={this.props.addDocument} RemoveDocument={this.props.removeDocument} ScreenToLocalTransform={this.getTransform} isTopMost={false} + SelectOnLoad={doc.Id === this._selectOnLoaded} ContentScaling={this.noScaling} PanelWidth={doc.Width} PanelHeight={doc.Height} @@ -192,7 +232,7 @@ export class CollectionFreeFormView extends CollectionViewBase { get backgroundView() { return !this.backgroundLayout ? (null) : (<JsxParser - components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebView }} + components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox }} bindings={this.props.bindings} jsx={this.backgroundLayout} showWarnings={true} @@ -203,33 +243,58 @@ export class CollectionFreeFormView extends CollectionViewBase { get overlayView() { return !this.overlayLayout ? (null) : (<JsxParser - components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView }} + components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox }} bindings={this.props.bindings} jsx={this.overlayLayout} showWarnings={true} onError={(test: any) => console.log(test)} />); } - getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH).transform(this.getLocalTransform()) - getLocalTransform = (): Transform => Transform.Identity.translate(-this.panX, -this.panY).scale(1 / this.scale); + + getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH).translate(-this.centeringShiftX, -this.centeringShiftY).transform(this.getLocalTransform()) + getLocalTransform = (): Transform => Transform.Identity.scale(1 / this.scale).translate(this.panX, this.panY); noScaling = () => 1; + //when focus is lost, this will remove the preview cursor + @action + onBlur = (e: React.FocusEvent<HTMLInputElement>): void => { + this._previewCursorVisible = false; + } + render() { - const panx: number = this.props.Document.GetNumber(KeyStore.PanX, 0); - const pany: number = this.props.Document.GetNumber(KeyStore.PanY, 0); + + //determines whether preview text cursor should be visible (ie when user taps this collection it should) + let cursor = null; + if (this._previewCursorVisible) { + //get local position and place cursor there! + let [x, y] = this.getTransform().transformPoint(this._downX, this._downY); + cursor = <div id="prevCursor" onKeyPress={this.onKeyDown} style={{ color: "black", position: "absolute", transformOrigin: "left top", transform: `translate(${x}px, ${y}px)` }}>I</div> + } + + let [dx, dy] = [this.centeringShiftX, this.centeringShiftY]; + + const panx: number = -this.props.Document.GetNumber(KeyStore.PanX, 0); + const pany: number = -this.props.Document.GetNumber(KeyStore.PanY, 0); + // const panx: number = this.props.Document.GetNumber(KeyStore.PanX, 0) + this.centeringShiftX; + // const pany: number = this.props.Document.GetNumber(KeyStore.PanY, 0) + this.centeringShiftY; + console.log("center:", this.getLocalTransform().transformPoint(this.centeringShiftX, this.centeringShiftY)); + return ( <div className="collectionfreeformview-container" onPointerDown={this.onPointerDown} + onKeyPress={this.onKeyDown} onWheel={this.onPointerWheel} - onContextMenu={(e) => e.preventDefault()} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} + onBlur={this.onBlur} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px`, }} + tabIndex={0} ref={this.createDropTarget}> <div className="collectionfreeformview" - style={{ transformOrigin: "left top", transform: ` translate(${panx}px, ${pany}px) scale(${this.zoomScaling}, ${this.zoomScaling})` }} + style={{ transformOrigin: "left top", transform: `translate(${dx}px, ${dy}px) scale(${this.zoomScaling}, ${this.zoomScaling}) translate(${panx}px, ${pany}px)` }} ref={this._canvasRef}> {this.backgroundView} + {cursor} {this.views} </div> {this.overlayView} diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index b02d528a0..88a3b73d4 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -4,13 +4,35 @@ position: absolute; width: 100%; height: 100%; - ::-webkit-scrollbar { - -webkit-appearance: none; - width: 10px; + .collectionSchemaView-previewRegion { + position: relative; + background: black; + float: left; + height: 100%; + } + .collectionSchemaView-previewHandle { + position: absolute; + height: 37px; + width: 20px; + z-index: 20; + right: 0; + top: 0; + background: Black ; + } + .collectionSchemaView-dividerDragger{ + position: relative; + background: black; + float: left; + height: 100%; } ::-webkit-scrollbar-thumb { border-radius: 5px; background-color: rgba(0, 0, 0, .5); + } + .collectionSchemaView-tableContainer { + position: relative; + float: left; + height: 100%; } .ReactTable { position: absolute; // display: inline-block; @@ -33,6 +55,7 @@ } .rt-tr-group { direction: ltr; + max-height: 44px; } .rt-td { border-width: 1; @@ -68,11 +91,11 @@ direction: ltr; } .ReactTable .rt-thead.-header { - background: grey; + background:grey; } - .ReactTable .rt-th, - .ReactTable .rt-td { - max-height: 75px; + .ReactTable .rt-th, .ReactTable .rt-td { + max-height: 44; + padding: 3px 7px; } .ReactTable .rt-tbody .rt-tr-group:last-child { border-bottom: grey; diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index dc952ef82..5bcd501cc 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -15,9 +15,11 @@ import { FieldView, FieldViewProps } from "../nodes/FieldView"; import "./CollectionSchemaView.scss"; import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; import { CollectionViewBase } from "./CollectionViewBase"; +import { setupDrag } from "../../util/DragManager"; // bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657 + @observer export class CollectionSchemaView extends CollectionViewBase { private _mainCont = React.createRef<HTMLDivElement>(); @@ -37,36 +39,44 @@ export class CollectionSchemaView extends CollectionViewBase { isSelected: () => false, select: () => { }, isTopMost: false, - bindings: {} + bindings: {}, + selectOnLoad: false, } let contents = ( <FieldView {...props} /> ) + let reference = React.createRef<HTMLDivElement>(); + let onItemDown = setupDrag(reference, () => props.doc); return ( - <EditableView contents={contents} height={36} GetValue={() => { - let field = props.doc.Get(props.fieldKey); - if (field && field instanceof Field) { - return field.ToScriptString(); - } - return field || ""; - }} SetValue={(value: string) => { - let script = CompileScript(value); - if (!script.compiled) { - return false; - } - let field = script(); - if (field instanceof Field) { - props.doc.Set(props.fieldKey, field); - return true; - } else { - let dataField = ToField(field); - if (dataField) { - props.doc.Set(props.fieldKey, dataField); - return true; - } - } - return false; - }}></EditableView> + <div onPointerDown={onItemDown} key={props.doc.Id} ref={reference}> + <EditableView contents={contents} + height={36} GetValue={() => { + let field = props.doc.Get(props.fieldKey); + if (field && field instanceof Field) { + return field.ToScriptString(); + } + return field || ""; + }} + SetValue={(value: string) => { + let script = CompileScript(value); + if (!script.compiled) { + return false; + } + let field = script(); + if (field instanceof Field) { + props.doc.Set(props.fieldKey, field); + return true; + } else { + let dataField = ToField(field); + if (dataField) { + props.doc.Set(props.fieldKey, dataField); + return true; + } + } + return false; + }}> + </EditableView> + </div> ) } @@ -85,27 +95,55 @@ export class CollectionSchemaView extends CollectionViewBase { } }), style: { - background: rowInfo.index == this._selectedIndex ? "#00afec" : "white", - color: rowInfo.index == this._selectedIndex ? "white" : "black" + background: rowInfo.index == this._selectedIndex ? "lightGray" : "white", + //color: rowInfo.index == this._selectedIndex ? "white" : "black" } }; } + _startSplitPercent = 0; @action onDividerMove = (e: PointerEvent): void => { let nativeWidth = this._mainCont.current!.getBoundingClientRect(); this._splitPercentage = Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100); } + @action onDividerUp = (e: PointerEvent): void => { document.removeEventListener("pointermove", this.onDividerMove); document.removeEventListener('pointerup', this.onDividerUp); + if (this._startSplitPercent == this._splitPercentage) { + this._splitPercentage = this._splitPercentage == 1 ? 66 : 100; + } } onDividerDown = (e: React.PointerEvent) => { + this._startSplitPercent = this._splitPercentage; e.stopPropagation(); e.preventDefault(); document.addEventListener("pointermove", this.onDividerMove); document.addEventListener('pointerup', this.onDividerUp); } + @action + onExpanderMove = (e: PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + } + @action + onExpanderUp = (e: PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + document.removeEventListener("pointermove", this.onExpanderMove); + document.removeEventListener('pointerup', this.onExpanderUp); + if (this._startSplitPercent == this._splitPercentage) { + this._splitPercentage = this._splitPercentage == 100 ? 66 : 100; + } + } + onExpanderDown = (e: React.PointerEvent) => { + this._startSplitPercent = this._splitPercentage; + e.stopPropagation(); + e.preventDefault(); + document.addEventListener("pointermove", this.onExpanderMove); + document.addEventListener('pointerup', this.onExpanderUp); + } onPointerDown = (e: React.PointerEvent) => { // if (e.button === 2 && this.active) { @@ -113,8 +151,10 @@ export class CollectionSchemaView extends CollectionViewBase { // e.preventDefault(); // } else { - if (e.buttons === 1 && this.props.active()) { - e.stopPropagation(); + if (e.buttons === 1) { + if (this.props.isSelected()) { + e.stopPropagation(); + } } } } @@ -146,6 +186,7 @@ export class CollectionSchemaView extends CollectionViewBase { <DocumentView Document={selected} AddDocument={this.props.addDocument} RemoveDocument={this.props.removeDocument} isTopMost={false} + SelectOnLoad={false} ScreenToLocalTransform={this.getTransform} ContentScaling={this.getContentScaling} PanelWidth={this.getPanelWidth} @@ -155,40 +196,42 @@ export class CollectionSchemaView extends CollectionViewBase { } </Measure> ) + let previewHandle = !this.props.active() ? (null) : ( + <div className="collectionSchemaView-previewHandle" onPointerDown={this.onExpanderDown} />); return ( - <div onPointerDown={this.onPointerDown} ref={this._mainCont} className="collectionSchemaView-container" style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} > - <Measure onResize={action((r: any) => { - this._dividerX = r.entry.width; - this._panelHeight = r.entry.height; - })}> - {({ measureRef }) => - <div ref={measureRef} className="collectionSchemaView-tableContainer" style={{ position: "relative", float: "left", width: `${this._splitPercentage}%`, height: "100%" }}> - <ReactTable - data={children} - pageSize={children.length} - page={0} - showPagination={false} - columns={columns.map(col => ({ - Header: col.Name, - accessor: (doc: Document) => [doc, col], - id: col.Id - }))} - column={{ - ...ReactTableDefaults.column, - Cell: this.renderCell, + <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} ref={this._mainCont} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} > + <div className="collectionSchemaView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}> + <Measure onResize={action((r: any) => { + this._dividerX = r.entry.width; + this._panelHeight = r.entry.height; + })}> + {({ measureRef }) => + <div ref={measureRef} className="collectionSchemaView-tableContainer" style={{ width: `${this._splitPercentage}%` }}> + <ReactTable + data={children} + pageSize={children.length} + page={0} + showPagination={false} + columns={columns.map(col => ({ + Header: col.Name, + accessor: (doc: Document) => [doc, col], + id: col.Id + }))} + column={{ + ...ReactTableDefaults.column, + Cell: this.renderCell, - }} - getTrProps={this.getTrProps} - /> - </div> - } - </Measure> - <div className="collectionSchemaView-dividerDragger" style={{ position: "relative", background: "black", float: "left", width: `${this.DIVIDER_WIDTH}px`, height: "100%" }} onPointerDown={this.onDividerDown} /> - <div className="collectionSchemaView-previewRegion" - onDrop={(e: React.DragEvent) => this.onDrop(e, {})} - ref={this.createDropTarget} - style={{ position: "relative", float: "left", width: `calc(${100 - this._splitPercentage}% - ${this.DIVIDER_WIDTH}px)`, height: "100%" }}> - {content} + }} + getTrProps={this.getTrProps} + /> + </div> + } + </Measure> + <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} /> + <div className="collectionSchemaView-previewRegion" style={{ width: `calc(${100 - this._splitPercentage}% - ${this.DIVIDER_WIDTH}px)` }}> + {content} + </div> + {previewHandle} </div> </div > ) diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss new file mode 100644 index 000000000..c488e2894 --- /dev/null +++ b/src/client/views/collections/CollectionTreeView.scss @@ -0,0 +1,34 @@ +ul { + list-style: none; +} + +li { + margin: 5px 0; +} + +.no-indent { + padding-left: 0; +} + +/* ALL THESE SPACINGS ARE SUPER HACKY RIGHT NOW HANNAH PLS HELP */ + +li:before { + content: '\2014'; + margin-right: 0.7em; +} + +.collapsed:before { + content: '\25b6'; + margin-right: 0.65em; +} + +.uncollapsed:before { + content: '\25bc'; + margin-right: 0.5em; +} + +.collectionTreeView-dropTarget { + border-style: solid; + box-sizing: border-box; + height:100%; +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx new file mode 100644 index 000000000..55c804337 --- /dev/null +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -0,0 +1,86 @@ +import { observer } from "mobx-react"; +import { CollectionViewBase } from "./CollectionViewBase"; +import { Document } from "../../../fields/Document"; +import { KeyStore } from "../../../fields/KeyStore"; +import { ListField } from "../../../fields/ListField"; +import React = require("react") +import { TextField } from "../../../fields/TextField"; +import { observable, action } from "mobx"; +import "./CollectionTreeView.scss"; +import { setupDrag } from "../../util/DragManager"; +import { FieldWaiting } from "../../../fields/Field"; +import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; + +export interface TreeViewProps { + document: Document; +} + +@observer +/** + * Component that takes in a document prop and a boolean whether it's collapsed or not. + */ +class TreeView extends React.Component<TreeViewProps> { + + @observable + collapsed: boolean = false; + + /** + * Renders a single child document. If this child is a collection, it will call renderTreeView again. Otherwise, it will just append a list element. + * @param childDocument The document to render. + */ + renderChild(childDocument: Document) { + let reference = React.createRef<HTMLDivElement>(); + + var children = childDocument.GetT<ListField<Document>>(KeyStore.Data, ListField); + let title = childDocument.GetT<TextField>(KeyStore.Title, TextField); + let onItemDown = setupDrag(reference, () => childDocument); + + if (title && title != FieldWaiting) { + let subView = !children || this.collapsed || children === FieldWaiting ? (null) : + <ul> + <TreeView document={childDocument} /> + </ul>; + return <div className="treeViewItem-container" onPointerDown={onItemDown} ref={reference}> + <li className={!children ? "leaf" : this.collapsed ? "collapsed" : "uncollapsed"} + onClick={action(() => this.collapsed = !this.collapsed)} > + {title.Data} + {subView} + </li> + </div> + } + return (null); + } + + render() { + var children = this.props.document.GetT<ListField<Document>>(KeyStore.Data, ListField); + return !children || children === FieldWaiting ? (null) : + (children.Data.map(value => + <div key={value.Id}> + {this.renderChild(value)} + </div>) + ) + } +} + + +@observer +export class CollectionTreeView extends CollectionViewBase { + + render() { + let titleStr = ""; + let title = this.props.Document.GetT<TextField>(KeyStore.Title, TextField); + if (title && title !== FieldWaiting) { + titleStr = title.Data; + } + return ( + <div className="collectionTreeView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} > + <h3>{titleStr}</h3> + <ul className="no-indent"> + <TreeView + document={this.props.Document} + /> + </ul> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 90080ab43..f938d2237 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,4 +1,4 @@ -import { action, computed } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import { Document } from "../../../fields/Document"; import { ListField } from "../../../fields/ListField"; @@ -11,14 +11,15 @@ import { CollectionFreeFormView } from "./CollectionFreeFormView"; import { CollectionDockingView } from "./CollectionDockingView"; import { CollectionSchemaView } from "./CollectionSchemaView"; import { CollectionViewProps } from "./CollectionViewBase"; - - +import { CollectionTreeView } from "./CollectionTreeView"; +import { Field } from "../../../fields/Field"; export enum CollectionViewType { Invalid, Freeform, Schema, Docking, + Tree } export const COLLECTION_BORDER_WIDTH = 2; @@ -28,8 +29,8 @@ export class CollectionView extends React.Component<CollectionViewProps> { public static LayoutString(fieldKey: string = "DataKey") { return `<CollectionView Document={Document} - ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} isSelected={isSelected} select={select} bindings={bindings} - isTopMost={isTopMost} BackgroundView={BackgroundView} />`; + ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} panelWidth={PanelWidth} panelHeight={PanelHeight} isSelected={isSelected} select={select} bindings={bindings} + isTopMost={isTopMost} SelectOnLoad={selectOnLoad} BackgroundView={BackgroundView} />`; } public active = () => { var isSelected = this.props.isSelected(); @@ -39,16 +40,28 @@ export class CollectionView extends React.Component<CollectionViewProps> { } @action addDocument = (doc: Document): void => { - //TODO This won't create the field if it doesn't already exist - const value = this.props.Document.GetData(this.props.fieldKey, ListField, new Array<Document>()) - value.push(doc); + if (this.props.Document.Get(this.props.fieldKey) instanceof Field) { + //TODO This won't create the field if it doesn't already exist + const value = this.props.Document.GetData(this.props.fieldKey, ListField, new Array<Document>()) + value.push(doc); + } else { + this.props.Document.SetData(this.props.fieldKey, [doc], ListField); + } } + @action removeDocument = (doc: Document): boolean => { //TODO This won't create the field if it doesn't already exist const value = this.props.Document.GetData(this.props.fieldKey, ListField, new Array<Document>()) - let index = value.indexOf(doc); + let index = -1; + for (let i = 0; i < value.length; i++) { + if (value[i].Id == doc.Id) { + index = i; + break; + } + } + if (index !== -1) { value.splice(index, 1) @@ -76,13 +89,15 @@ export class CollectionView extends React.Component<CollectionViewProps> { Document.SetData(KeyStore.ViewType, type, NumberField); } + render() { let viewType = this.collectionViewType; + switch (viewType) { case CollectionViewType.Freeform: return (<CollectionFreeFormView {...this.props} addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active} - CollectionView={this} />) + CollectionView={this} />); case CollectionViewType.Schema: return (<CollectionSchemaView {...this.props} addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active} @@ -91,6 +106,10 @@ export class CollectionView extends React.Component<CollectionViewProps> { return (<CollectionDockingView {...this.props} addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active} CollectionView={this} />) + case CollectionViewType.Tree: + return (<CollectionTreeView {...this.props} + addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active} + CollectionView={this} />) default: return <div></div> } diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx index 7e269caf1..7067724c8 100644 --- a/src/client/views/collections/CollectionViewBase.tsx +++ b/src/client/views/collections/CollectionViewBase.tsx @@ -20,6 +20,8 @@ export interface CollectionViewProps { isTopMost: boolean; select: (ctrlPressed: boolean) => void; bindings: any; + panelWidth: () => number; + panelHeight: () => number; } export interface SubCollectionViewProps extends CollectionViewProps { active: () => boolean; @@ -42,12 +44,16 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps> @undoBatch @action protected drop(e: Event, de: DragManager.DropEvent) { - const doc: DocumentView = de.data["document"]; - if (doc.props.ContainingCollectionView && doc.props.ContainingCollectionView !== this.props.CollectionView) { - if (doc.props.RemoveDocument) { - doc.props.RemoveDocument(doc.props.Document); + const docView: DocumentView = de.data["documentView"]; + const doc: Document = de.data["document"]; + if (docView && docView.props.ContainingCollectionView && docView.props.ContainingCollectionView !== this.props.CollectionView) { + if (docView.props.RemoveDocument) { + docView.props.RemoveDocument(docView.props.Document); } - this.props.addDocument(doc.props.Document); + this.props.addDocument(docView.props.Document); + } else if (doc) { + this.props.removeDocument(doc); + this.props.addDocument(doc); } e.stopPropagation(); } @@ -60,8 +66,8 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps> let html = e.dataTransfer.getData("text/html"); let text = e.dataTransfer.getData("text/plain"); - if (html) { - let htmlDoc = Documents.HtmlDocument(html, { ...options }); + if (html && html.indexOf("<img") != 0) { + let htmlDoc = Documents.HtmlDocument(html, { ...options, width: 300, height: 300 }); htmlDoc.SetText(KeyStore.DocumentText, text); this.props.addDocument(htmlDoc); return; @@ -72,7 +78,7 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps> if (item.kind === "string" && item.type.indexOf("uri") != -1) { e.dataTransfer.items[i].getAsString(function (s) { action(() => { - var img = Documents.ImageDocument(s, { ...options, nativeWidth: 300, nativeHeight: 300, width: 300, height: 300 }) + var img = Documents.ImageDocument(s, { ...options, nativeWidth: 300, width: 300, }) let docs = that.props.Document.GetT(KeyStore.Data, ListField); if (docs != FieldWaiting) { diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index d7243421a..9edad1f64 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -27,44 +27,26 @@ export class CollectionFreeFormDocumentView extends React.Component<DocumentView return `scale(${this.props.ContentScaling()}, ${this.props.ContentScaling()}) translate(${this.props.Document.GetNumber(KeyStore.X, 0)}px, ${this.props.Document.GetNumber(KeyStore.Y, 0)}px)`; } - @computed - get width(): number { - return this.props.Document.GetNumber(KeyStore.Width, 0); - } - - @computed - get nativeWidth(): number { - return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); - } + @computed get zIndex(): number { return this.props.Document.GetNumber(KeyStore.ZIndex, 0); } + @computed get width(): number { return this.props.Document.Width(); } + @computed get height(): number { return this.props.Document.Height(); } + @computed get nativeWidth(): number { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } + @computed get nativeHeight(): number { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } set width(w: number) { this.props.Document.SetData(KeyStore.Width, w, NumberField) - if (this.nativeWidth > 0 && this.nativeHeight > 0) { + if (this.nativeWidth && this.nativeHeight) { this.props.Document.SetNumber(KeyStore.Height, this.nativeHeight / this.nativeWidth * w) } } - @computed - get height(): number { - return this.props.Document.GetNumber(KeyStore.Height, 0); - } - @computed - get nativeHeight(): number { - return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); - } - set height(h: number) { this.props.Document.SetData(KeyStore.Height, h, NumberField); - if (this.nativeWidth > 0 && this.nativeHeight > 0) { + if (this.nativeWidth && this.nativeHeight) { this.props.Document.SetNumber(KeyStore.Width, this.nativeWidth / this.nativeHeight * h) } } - @computed - get zIndex(): number { - return this.props.Document.GetNumber(KeyStore.ZIndex, 0); - } - set zIndex(h: number) { this.props.Document.SetData(KeyStore.ZIndex, h, NumberField) } @@ -87,6 +69,7 @@ export class CollectionFreeFormDocumentView extends React.Component<DocumentView } render() { + console.log(this.transform); return ( <div className="collectionFreeFormDocumentView-container" ref={this._mainCont} style={{ transformOrigin: "left top", diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index ad1328e5d..19a455b74 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -12,14 +12,18 @@ import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionFreeFormView } from "../collections/CollectionFreeFormView"; import { CollectionSchemaView } from "../collections/CollectionSchemaView"; import { CollectionView, CollectionViewType } from "../collections/CollectionView"; -import { WebView } from "./WebView"; import { ContextMenu } from "../ContextMenu"; import { FormattedTextBox } from "../nodes/FormattedTextBox"; import { ImageBox } from "../nodes/ImageBox"; +import { Documents } from "../../documents/Documents" +import { KeyValueBox } from "./KeyValueBox" +import { WebBox } from "../nodes/WebBox"; import "./DocumentView.scss"; import React = require("react"); +import { CollectionViewProps } from "../collections/CollectionViewBase"; const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this? + export interface DocumentViewProps { ContainingCollectionView: Opt<CollectionView>; @@ -32,6 +36,7 @@ export interface DocumentViewProps { ContentScaling: () => number; PanelWidth: () => number; PanelHeight: () => number; + SelectOnLoad: boolean; } export interface JsxArgs extends DocumentViewProps { Keys: { [name: string]: Key } @@ -80,7 +85,6 @@ export class DocumentView extends React.Component<DocumentViewProps> { private _mainCont = React.createRef<HTMLDivElement>(); private _documentBindings: any = null; - private _contextMenuCanOpen = false; private _downX: number = 0; private _downY: number = 0; @@ -96,10 +100,9 @@ export class DocumentView extends React.Component<DocumentViewProps> { this._downX = e.clientX; this._downY = e.clientY; if (e.shiftKey && e.buttons === 1) { - CollectionDockingView.Instance.StartOtherDrag(this._mainCont.current!, this.props.Document); + CollectionDockingView.Instance.StartOtherDrag(this.props.Document, e); e.stopPropagation(); } else { - this._contextMenuCanOpen = true; if (this.active && !e.isDefaultPrevented()) { e.stopPropagation(); if (e.buttons === 2) { @@ -115,21 +118,20 @@ export class DocumentView extends React.Component<DocumentViewProps> { onPointerMove = (e: PointerEvent): void => { if (e.cancelBubble) { - this._contextMenuCanOpen = false; return; } if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { - this._contextMenuCanOpen = false; + document.removeEventListener("pointermove", this.onPointerMove) + document.removeEventListener("pointerup", this.onPointerUp) if (this._mainCont.current != null && !this.topMost) { - this._contextMenuCanOpen = false; const [left, top] = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); let dragData: { [id: string]: any } = {}; - dragData["document"] = this; + dragData["documentView"] = this; dragData["xOffset"] = e.x - left; dragData["yOffset"] = e.y - top; DragManager.StartDrag(this._mainCont.current, dragData, { handlers: { - dragComplete: action((e: DragManager.DragCompleteEvent) => { }), + dragComplete: action(() => { }), }, hideSource: true }) @@ -148,17 +150,24 @@ export class DocumentView extends React.Component<DocumentViewProps> { } } - deleteClicked = (e: React.MouseEvent): void => { + deleteClicked = (): void => { if (this.props.RemoveDocument) { this.props.RemoveDocument(this.props.Document); } } + + fieldsClicked = (e: React.MouseEvent): void => { + if (this.props.AddDocument) { + this.props.AddDocument(Documents.KVPDocument(this.props.Document)); + } + } fullScreenClicked = (e: React.MouseEvent): void => { CollectionDockingView.Instance.OpenFullScreen(this.props.Document); ContextMenu.Instance.clearItems(); ContextMenu.Instance.addItem({ description: "Close Full Screen", event: this.closeFullScreenClicked }); ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) } + closeFullScreenClicked = (e: React.MouseEvent): void => { CollectionDockingView.Instance.CloseFullScreen(); ContextMenu.Instance.clearItems(); @@ -168,42 +177,51 @@ export class DocumentView extends React.Component<DocumentViewProps> { @action onContextMenu = (e: React.MouseEvent): void => { - e.preventDefault() e.stopPropagation(); - if (!SelectionManager.IsSelected(this) || !this._contextMenuCanOpen) { + let moved = Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3; + if (moved || e.isDefaultPrevented()) { + e.preventDefault() return; } + e.preventDefault() - if (this.topMost) { - ContextMenu.Instance.clearItems() - ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }) - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) - } - else { + ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }) + ContextMenu.Instance.addItem({ description: "Fields", event: this.fieldsClicked }) + ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document) }) + ContextMenu.Instance.addItem({ description: "Freeform", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform) }) + ContextMenu.Instance.addItem({ description: "Schema", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Schema) }) + ContextMenu.Instance.addItem({ description: "Treeview", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Tree) }) + ContextMenu.Instance.addItem({ + description: "center", event: () => { + if (this.props.ContainingCollectionView) { + let doc = this.props.ContainingCollectionView.props.Document; + doc.SetNumber(KeyStore.PanX, this.props.Document.GetNumber(KeyStore.X, 0) + (this.props.Document.GetNumber(KeyStore.Width, 0) / 2)) + doc.SetNumber(KeyStore.PanY, this.props.Document.GetNumber(KeyStore.Y, 0) + (this.props.Document.GetNumber(KeyStore.Height, 0) / 2)) + } + } + }) + //ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) }) + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) + if (!this.topMost) { // DocumentViews should stop propagation of this event e.stopPropagation(); - - ContextMenu.Instance.clearItems(); - ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }) - ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document) }) - ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked }) - ContextMenu.Instance.addItem({ description: "Freeform", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform) }) - ContextMenu.Instance.addItem({ description: "Schema", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Schema) }) - ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) }) - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) - SelectionManager.SelectDoc(this, e.ctrlKey); } + + ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked }) + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) + SelectionManager.SelectDoc(this, e.ctrlKey); } @computed get mainContent() { return <JsxParser - components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebView }} + components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox }} bindings={this._documentBindings} jsx={this.layout} showWarnings={true} onError={(test: any) => { console.log(test) }} /> } + render() { if (!this.props.Document) return <div></div> @@ -237,7 +255,8 @@ export class DocumentView extends React.Component<DocumentViewProps> { transform: `scale(${scaling},${scaling})` }} onContextMenu={this.onContextMenu} - onPointerDown={this.onPointerDown} > + onPointerDown={this.onPointerDown} + > {this.mainContent} </div> ) diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 1a9d325db..f372258f8 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -7,11 +7,11 @@ import { TextField } from "../../../fields/TextField"; import { NumberField } from "../../../fields/NumberField"; import { RichTextField } from "../../../fields/RichTextField"; import { ImageField } from "../../../fields/ImageField"; +import { WebField } from "../../../fields/WebField"; import { Key } from "../../../fields/Key"; import { FormattedTextBox } from "./FormattedTextBox"; import { ImageBox } from "./ImageBox"; -import { HtmlField } from "../../../fields/HtmlField"; -import { WebView } from "./WebView"; +import { WebBox } from "./WebBox"; // // these properties get assigned through the render() method of the DocumentView when it creates this node. @@ -24,12 +24,15 @@ export interface FieldViewProps { isSelected: () => boolean; select: () => void; isTopMost: boolean; + selectOnLoad: boolean; bindings: any; } @observer export class FieldView extends React.Component<FieldViewProps> { - public static LayoutString(fieldType: { name: string }, fieldStr: string = "DataKey") { return `<${fieldType.name} doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={${fieldStr}} isSelected={isSelected} select={select} isTopMost={isTopMost} />`; } + public static LayoutString(fieldType: { name: string }, fieldStr: string = "DataKey") { + return `<${fieldType.name} doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={${fieldStr}} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost} />`; + } @computed get field(): FieldValue<Field> { @@ -50,13 +53,20 @@ export class FieldView extends React.Component<FieldViewProps> { else if (field instanceof ImageField) { return <ImageBox {...this.props} /> } + else if (field instanceof WebField) { + return <WebBox {...this.props} /> + } + // bcz: this belongs here, but it doesn't render well so taking it out for now + // else if (field instanceof HtmlField) { + // return <WebBox {...this.props} /> + // } else if (field instanceof NumberField) { return <p>{field.Data}</p> - } else if (field instanceof HtmlField) { - return <WebView {...this.props} /> - } else if (field != FieldWaiting) { - return <p>{field.GetValue}</p> - } else + } + else if (field != FieldWaiting) { + return <p>{JSON.stringify(field.GetValue())}</p> + } + else return <p> {"Waiting for server..."} </p> } diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 0389a3f85..21bd43b6e 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -9,8 +9,10 @@ } .formattedTextBox-cont { - background: beige; - padding: 0; + background: white; + padding: 1; + border: black; + border-width: 10; overflow-y: scroll; overflow-x: hidden; color: initial; diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 60ee0b5e1..e65615af4 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -3,15 +3,17 @@ import { baseKeymap } from "prosemirror-commands"; import { history, redo, undo } from "prosemirror-history"; import { keymap } from "prosemirror-keymap"; import { schema } from "prosemirror-schema-basic"; -import { EditorState, Transaction } from "prosemirror-state"; +import { EditorState, Transaction, } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; -import { Opt, FieldWaiting, FieldValue } from "../../../fields/Field"; +import { Opt, FieldWaiting } from "../../../fields/Field"; import "./FormattedTextBox.scss"; import React = require("react") import { RichTextField } from "../../../fields/RichTextField"; import { FieldViewProps, FieldView } from "./FieldView"; + + // 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 + "Key"} @@ -39,7 +41,6 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { super(props); this._ref = React.createRef(); - this.onChange = this.onChange.bind(this); } @@ -47,25 +48,23 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { if (this._editorView) { const state = this._editorView.state.apply(tx); this._editorView.updateState(state); - const { doc, fieldKey } = this.props; - doc.SetData(fieldKey, JSON.stringify(state.toJSON()), RichTextField); + this.props.doc.SetData(this.props.fieldKey, JSON.stringify(state.toJSON()), RichTextField); } } componentDidMount() { let state: EditorState; - const { doc, fieldKey } = this.props; const config = { schema, plugins: [ history(), keymap({ "Mod-z": undo, "Mod-y": redo }), - keymap(baseKeymap) + keymap(baseKeymap), ] }; - let field = doc.GetT(fieldKey, RichTextField); - if (field && field != FieldWaiting) { // bcz: don't think this works + let field = this.props.doc.GetT(this.props.fieldKey, RichTextField); + if (field && field != FieldWaiting) { state = EditorState.fromJSON(config, JSON.parse(field.Data)); } else { state = EditorState.create(config); @@ -85,6 +84,10 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field))); } }) + if (this.props.selectOnLoad) { + this.props.select(); + this._editorView!.focus(); + } } componentWillUnmount() { @@ -102,19 +105,20 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { @action onChange(e: React.ChangeEvent<HTMLInputElement>) { - const { fieldKey, doc } = this.props; - doc.SetData(fieldKey, e.target.value, RichTextField); + this.props.doc.SetData(this.props.fieldKey, e.target.value, RichTextField); } onPointerDown = (e: React.PointerEvent): void => { - let me = this; if (e.buttons === 1 && this.props.isSelected()) { e.stopPropagation(); } } + onPointerWheel = (e: React.WheelEvent): void => { + e.stopPropagation(); + } render() { - var val = this.props.doc.Get(this.props.fieldKey); return (<div className="formattedTextBox-cont" onPointerDown={this.onPointerDown} + onWheel={this.onPointerWheel} ref={this._ref} />) } }
\ No newline at end of file diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index b5ce8b28c..e206bf8d5 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -15,6 +15,7 @@ export class ImageBox extends React.Component<FieldViewProps> { public static LayoutString() { return FieldView.LayoutString(ImageBox) } private _ref: React.RefObject<HTMLDivElement>; + private _imgRef: React.RefObject<HTMLImageElement>; private _downX: number = 0; private _downY: number = 0; private _lastTap: number = 0; @@ -25,12 +26,20 @@ export class ImageBox extends React.Component<FieldViewProps> { super(props); this._ref = React.createRef(); + this._imgRef = React.createRef(); this.state = { photoIndex: 0, isOpen: false, }; } + @action + onLoad = (target: any) => { + var h = this._imgRef.current!.naturalHeight; + var w = this._imgRef.current!.naturalWidth; + this.props.doc.SetNumber(KeyStore.NativeHeight, this.props.doc.GetNumber(KeyStore.NativeWidth, 0) * h / w) + } + componentDidMount() { } @@ -84,10 +93,9 @@ export class ImageBox extends React.Component<FieldViewProps> { let path = field == FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" : field instanceof ImageField ? field.Data.href : "http://www.cs.brown.edu/~bcz/face.gif"; let nativeWidth = this.props.doc.GetNumber(KeyStore.NativeWidth, 1); - return ( <div className="imageBox-cont" onPointerDown={this.onPointerDown} ref={this._ref} > - <img src={path} width={nativeWidth} alt="Image not found" /> + <img src={path} width={nativeWidth} alt="Image not found" ref={this._imgRef} onLoad={this.onLoad} /> {this.lightbox(path)} </div>) } diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss new file mode 100644 index 000000000..1295266e5 --- /dev/null +++ b/src/client/views/nodes/KeyValueBox.scss @@ -0,0 +1,31 @@ +.keyValueBox-cont { + overflow-y:scroll; + height: 100%; + border: black; + border-width: 1px; + border-style: solid; + box-sizing: border-box; + display: inline-block; + .imageBox-cont img { + max-height:45px; + height: auto; + } +} +.keyValueBox-table { + position: relative; +} +.keyValueBox-header { + background:gray; +} +.keyValueBox-evenRow { + background: white; + .formattedTextBox-cont { + background: white; + } +} +.keyValueBox-oddRow { + background: lightGray; + .formattedTextBox-cont { + background: lightgray; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx new file mode 100644 index 000000000..e8ebd50be --- /dev/null +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -0,0 +1,85 @@ + +import { IReactionDisposer } from 'mobx'; +import { observer } from "mobx-react"; +import { EditorView } from 'prosemirror-view'; +import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app +import { Document } from '../../../fields/Document'; +import { Opt, FieldWaiting } from '../../../fields/Field'; +import { KeyStore } from '../../../fields/KeyStore'; +import { FieldView, FieldViewProps } from './FieldView'; +import { KeyValuePair } from "./KeyValuePair"; +import "./KeyValueBox.scss"; +import React = require("react") + +@observer +export class KeyValueBox extends React.Component<FieldViewProps> { + + public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(KeyValueBox, fieldStr) } + private _ref: React.RefObject<HTMLDivElement>; + private _editorView: Opt<EditorView>; + private _reactionDisposer: Opt<IReactionDisposer>; + + + constructor(props: FieldViewProps) { + super(props); + + this._ref = React.createRef(); + } + + + + shouldComponentUpdate() { + return false; + } + + + onPointerDown = (e: React.PointerEvent): void => { + if (e.buttons === 1 && this.props.isSelected()) { + e.stopPropagation(); + } + } + onPointerWheel = (e: React.WheelEvent): void => { + e.stopPropagation(); + } + + createTable = () => { + let doc = this.props.doc.GetT(KeyStore.Data, Document); + if (!doc || doc == FieldWaiting) { + return <tr><td>Loading...</td></tr> + } + let realDoc = doc; + + let ids: { [key: string]: string } = {}; + let protos = doc.GetAllPrototypes(); + for (const proto of protos) { + proto._proxies.forEach((val, key) => { + if (!(key in ids)) { + ids[key] = key; + } + }) + } + + let rows: JSX.Element[] = []; + let i = 0; + for (let key in ids) { + rows.push(<KeyValuePair doc={realDoc} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} fieldId={key} key={key} />) + } + return rows; + } + + + render() { + + return (<div className="keyValueBox-cont" onWheel={this.onPointerWheel}> + <table className="keyValueBox-table"> + <tbody> + <tr className="keyValueBox-header"> + <th>Key</th> + <th>Fields</th> + </tr> + {this.createTable()} + </tbody> + </table> + </div>) + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx new file mode 100644 index 000000000..a97e98313 --- /dev/null +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -0,0 +1,58 @@ +import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app +import "./KeyValueBox.scss"; +import React = require("react") +import { FieldViewProps, FieldView } from './FieldView'; +import { Opt, Field } from '../../../fields/Field'; +import { observer } from "mobx-react" +import { observable, action } from 'mobx'; +import { Document } from '../../../fields/Document'; +import { Key } from '../../../fields/Key'; +import { Server } from "../../Server" + +// Represents one row in a key value plane + +export interface KeyValuePairProps { + rowStyle: string; + fieldId: string; + doc: Document; +} +@observer +export class KeyValuePair extends React.Component<KeyValuePairProps> { + + @observable + private key: Opt<Key> + + constructor(props: KeyValuePairProps) { + super(props); + Server.GetField(this.props.fieldId, + action((field: Opt<Field>) => { + if (field) { + this.key = field as Key; + } + })); + + } + + + render() { + if (!this.key) { + return <tr><td>error</td><td></td></tr> + + } + let props: FieldViewProps = { + doc: this.props.doc, + fieldKey: this.key, + isSelected: () => false, + select: () => { }, + isTopMost: false, + bindings: {}, + selectOnLoad: false, + } + return ( + <tr className={this.props.rowStyle}> + <td>{this.key.Name}</td> + <td><FieldView {...props} /></td> + </tr> + ) + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss new file mode 100644 index 000000000..e72b3c4da --- /dev/null +++ b/src/client/views/nodes/WebBox.scss @@ -0,0 +1,14 @@ + +.webBox-cont { + padding: 0vw; + position: absolute; + width: 100%; + height: 100%; +} + +.webBox-button { + padding : 0vw; + border: none; + width : 100%; + height: 100%; +}
\ No newline at end of file diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx new file mode 100644 index 000000000..2ca8d49ce --- /dev/null +++ b/src/client/views/nodes/WebBox.tsx @@ -0,0 +1,38 @@ +import "./WebBox.scss"; +import React = require("react") +import { WebField } from '../../../fields/WebField'; +import { FieldViewProps, FieldView } from './FieldView'; +import { FieldWaiting } from '../../../fields/Field'; +import { observer } from "mobx-react" +import { computed } from 'mobx'; +import { KeyStore } from '../../../fields/KeyStore'; + +@observer +export class WebBox extends React.Component<FieldViewProps> { + + public static LayoutString() { return FieldView.LayoutString(WebBox); } + + constructor(props: FieldViewProps) { + super(props); + } + + @computed get html(): string { return this.props.doc.GetHtml(KeyStore.Data, ""); } + + render() { + let field = this.props.doc.Get(this.props.fieldKey); + let path = field == FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" : + field instanceof WebField ? field.Data.href : "https://crossorigin.me/" + "https://cs.brown.edu"; + + let content = this.html ? + <span dangerouslySetInnerHTML={{ __html: this.html }}></span> : + <div style={{ width: "100%", height: "100%", position: "absolute" }}> + <iframe src={path} style={{ position: "absolute", width: "100%", height: "100%" }}></iframe> + {this.props.isSelected() ? (null) : <div style={{ width: "100%", height: "100%", position: "absolute" }} />} + </div>; + + return ( + <div className="webBox-cont" > + {content} + </div>) + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/WebView.tsx b/src/client/views/nodes/WebView.tsx deleted file mode 100644 index 717aa8bf5..000000000 --- a/src/client/views/nodes/WebView.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { FieldViewProps, FieldView } from "./FieldView"; -import { computed } from "mobx"; -import { observer } from "mobx-react"; -import { KeyStore } from "../../../fields/KeyStore"; -import React = require('react') -import { TextField } from "../../../fields/TextField"; -import { HtmlField } from "../../../fields/HtmlField"; -import { RichTextField } from "../../../fields/RichTextField"; - -@observer -export class WebView extends React.Component<FieldViewProps> { - public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(WebView, fieldStr) } - - @computed - get html(): string { - return this.props.doc.GetData(KeyStore.Data, HtmlField, "" as string); - } - - render() { - return <span dangerouslySetInnerHTML={{ __html: this.html }}></span> - } -}
\ No newline at end of file diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx new file mode 100644 index 000000000..7bc70615f --- /dev/null +++ b/src/debug/Test.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +class TestInternal extends React.Component { + onContextMenu = (e: React.MouseEvent) => { + console.log("Internal"); + e.stopPropagation(); + } + + onPointerDown = (e: React.MouseEvent) => { + console.log("pointer down") + e.preventDefault(); + } + + render() { + return <div onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} + onPointerUp={this.onPointerDown}>Hello world</div> + } +} + +class TestChild extends React.Component { + onContextMenu = () => { + console.log("Child"); + } + + render() { + return <div onContextMenu={this.onContextMenu}><TestInternal /></div> + } +} + +class TestParent extends React.Component { + onContextMenu = () => { + console.log("Parent"); + } + + render() { + return <div onContextMenu={this.onContextMenu}><TestChild /></div> + } +} + +ReactDOM.render(( + <div style={{ position: "absolute", width: "100%", height: "100%" }}> + <TestParent /> + </div>), + document.getElementById('root') +);
\ No newline at end of file diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx index aff77fca3..780e9f8f2 100644 --- a/src/debug/Viewer.tsx +++ b/src/debug/Viewer.tsx @@ -17,7 +17,7 @@ configure({ @observer class FieldViewer extends React.Component<{ field: BasicField<any> }> { render() { - return <span>{this.props.field.Data} ({this.props.field.Id})</span>; + return <span>{JSON.stringify(this.props.field.Data)} ({this.props.field.Id})</span>; } } diff --git a/src/fields/Document.ts b/src/fields/Document.ts index 0d7d357a0..0c156b282 100644 --- a/src/fields/Document.ts +++ b/src/fields/Document.ts @@ -8,6 +8,7 @@ import { ListField } from "./ListField"; import { Server } from "../client/Server"; import { Types } from "../server/Message"; import { UndoManager } from "../client/util/UndoManager"; +import { HtmlField } from "./HtmlField"; export class Document extends Field { public fields: ObservableMap<string, { key: Key, field: Field }> = new ObservableMap(); @@ -17,8 +18,6 @@ export class Document extends Field { super(id) if (save) { - var title = (this._proxies.has(KeyStore.Title.Id) ? "???" : this.Title) + "(" + this.Id + ")"; - console.log("Save " + title); Server.UpdateField(this) } } @@ -31,7 +30,7 @@ export class Document extends Field { } public Width = () => { return this.GetNumber(KeyStore.Width, 0) } - public Height = () => { return this.GetNumber(KeyStore.Height, 0) } + public Height = () => { return this.GetNumber(KeyStore.Height, this.GetNumber(KeyStore.NativeWidth, 0) ? this.GetNumber(KeyStore.NativeHeight, 0) / this.GetNumber(KeyStore.NativeWidth, 0) * this.GetNumber(KeyStore.Width, 0) : 0) } public Scale = () => { return this.GetNumber(KeyStore.Scale, 1) } @computed @@ -103,6 +102,25 @@ export class Document extends Field { return false; } + GetOrCreateAsync<T extends Field>(key: Key, ctor: { new(): T }, callback: (field: T) => void): void { + //This currently doesn't deal with prototypes + if (this._proxies.has(key.Id)) { + Server.GetDocumentField(this, key, (field) => { + if (field && field instanceof ctor) { + callback(field); + } else { + let newField = new ctor(); + this.Set(key, newField); + callback(newField); + } + }); + } else { + let newField = new ctor(); + this.Set(key, newField); + callback(newField); + } + } + GetT<T extends Field = Field>(key: Key, ctor: { new(...args: any[]): T }, ignoreProto: boolean = false): FieldValue<T> { var getfield = this.Get(key, ignoreProto); if (getfield != FieldWaiting) { @@ -127,6 +145,10 @@ export class Document extends Field { return vval; } + GetHtml(key: Key, defaultVal: string): string { + return this.GetData(key, HtmlField, defaultVal); + } + GetNumber(key: Key, defaultVal: number): number { return this.GetData(key, NumberField, defaultVal); } diff --git a/src/fields/Field.ts b/src/fields/Field.ts index c7e0232af..d48509a47 100644 --- a/src/fields/Field.ts +++ b/src/fields/Field.ts @@ -1,6 +1,7 @@ import { Utils } from "../Utils"; import { Types } from "../server/Message"; +import { computed } from "mobx"; export function Cast<T extends Field>(field: FieldValue<Field>, ctor: { new(): T }): Opt<T> { if (field) { @@ -25,6 +26,8 @@ export abstract class Field { } private id: FieldId; + + @computed get Id(): FieldId { return this.id; } diff --git a/src/fields/KVPField b/src/fields/KVPField new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/fields/KVPField diff --git a/src/fields/KVPField.ts b/src/fields/KVPField.ts new file mode 100644 index 000000000..a7ecc0768 --- /dev/null +++ b/src/fields/KVPField.ts @@ -0,0 +1,30 @@ +import { BasicField } from "./BasicField" +import { FieldId } from "./Field"; +import { Types } from "../server/Message"; +import { Document } from "./Document" + +export class KVPField extends BasicField<Document> { + constructor(data: Document | undefined = undefined, id?: FieldId, save: boolean = true) { + super(data == undefined ? new Document() : data, save, id); + } + + toString(): string { + return this.Data.Title; + } + + ToScriptString(): string { + return `new KVPField("${this.Data}")`; + } + + Copy() { + return new KVPField(this.Data); + } + + ToJson(): { type: Types, data: Document, _id: string } { + return { + type: Types.Text, + data: this.Data, + _id: this.Id + } + } +}
\ No newline at end of file diff --git a/src/fields/Key.ts b/src/fields/Key.ts index c16a00878..00d78d516 100644 --- a/src/fields/Key.ts +++ b/src/fields/Key.ts @@ -2,7 +2,6 @@ import { Field, FieldId } from "./Field" import { Utils } from "../Utils"; import { observable } from "mobx"; import { Types } from "../server/Message"; -import { ObjectID } from "bson"; import { Server } from "../client/Server"; export class Key extends Field { diff --git a/src/fields/WebField.ts b/src/fields/WebField.ts new file mode 100644 index 000000000..8f945d686 --- /dev/null +++ b/src/fields/WebField.ts @@ -0,0 +1,30 @@ +import { BasicField } from "./BasicField"; +import { Field, FieldId } from "./Field"; +import { Types } from "../server/Message"; + +export class WebField extends BasicField<URL> { + constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) { + super(data == undefined ? new URL("https://crossorigin.me/" + "https://cs.brown.edu/") : data, save, id); + } + + toString(): string { + return this.Data.href; + } + + ToScriptString(): string { + return `new WebField("${this.Data}")`; + } + + Copy(): Field { + return new WebField(this.Data); + } + + ToJson(): { type: Types, data: URL, _id: string } { + return { + type: Types.Web, + data: this.Data, + _id: this.Id + } + } + +}
\ No newline at end of file diff --git a/src/server/Message.ts b/src/server/Message.ts index 80fc9a80d..148e6e723 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -45,7 +45,7 @@ export class GetFieldArgs { } export enum Types { - Number, List, Key, Image, Document, Text, RichText, DocumentReference, Html + Number, List, Key, Image, Web, Document, Text, RichText, DocumentReference, Html } export class DocumentTransfer implements Transferable { diff --git a/src/server/ServerUtil.ts b/src/server/ServerUtil.ts index 08e72fdae..a53fb5d2b 100644 --- a/src/server/ServerUtil.ts +++ b/src/server/ServerUtil.ts @@ -10,6 +10,7 @@ import { Server } from './../client/Server'; import { Types } from './Message'; import { Utils } from '../Utils'; import { HtmlField } from '../fields/HtmlField'; +import { WebField } from '../fields/WebField'; export class ServerUtils { public static FromJson(json: any): Field { @@ -30,6 +31,8 @@ export class ServerUtils { return new TextField(data, id, false) case Types.Html: return new HtmlField(data, id, false) + case Types.Web: + return new WebField(new URL(data), id, false) case Types.RichText: return new RichTextField(data, id, false) case Types.Key: diff --git a/src/server/index.ts b/src/server/index.ts index 5115142b8..e6f08bc29 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -35,6 +35,12 @@ const mongoose = require('mongoose'); import { performance } from 'perf_hooks' import * as path from 'path' import User, { DashUserModel } from './authentication/models/user_model'; +import * as fs from 'fs'; +import * as request from 'request' + +const download = (url: string, dest: fs.PathLike) => { + request.get(url).pipe(fs.createWriteStream(dest)); +} const mongoUrl = 'mongodb://localhost:27017/Dash'; mongoose.connect(mongoUrl) diff --git a/tsconfig.json b/tsconfig.json index 823fc2732..e2538bc39 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,9 +11,5 @@ "dom", "es2015" ] - }, - "exclude": [ - "node_modules", - "static" - ] + } }
\ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 2a29844e8..900802d7d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -7,6 +7,7 @@ module.exports = { entry: { bundle: ["./src/client/views/Main.tsx", 'webpack-hot-middleware/client?reload=true'], viewer: ["./src/debug/Viewer.tsx", 'webpack-hot-middleware/client?reload=true'], + test: ["./src/debug/Test.tsx", 'webpack-hot-middleware/client?reload=true'], }, devtool: "source-map", node: { |