aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Utils.ts37
-rw-r--r--src/client/Server.ts112
-rw-r--r--src/client/SocketStub.ts77
-rw-r--r--src/client/documents/Documents.ts169
-rw-r--r--src/client/util/DragManager.ts41
-rw-r--r--src/client/util/Scripting.ts2
-rw-r--r--src/client/util/SelectionManager.ts12
-rw-r--r--src/client/util/Transform.ts79
-rw-r--r--src/client/util/UndoManager.ts133
-rw-r--r--src/client/views/DocumentDecorations.tsx72
-rw-r--r--src/client/views/EditableView.tsx6
-rw-r--r--src/client/views/Main.tsx200
-rw-r--r--src/client/views/collections/CollectionDockingView.scss4
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx452
-rw-r--r--src/client/views/collections/CollectionFreeFormView.scss17
-rw-r--r--src/client/views/collections/CollectionFreeFormView.tsx308
-rw-r--r--src/client/views/collections/CollectionSchemaView.scss80
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx300
-rw-r--r--src/client/views/collections/CollectionTreeView.scss28
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx104
-rw-r--r--src/client/views/collections/CollectionView.tsx113
-rw-r--r--src/client/views/collections/CollectionViewBase.tsx152
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx225
-rw-r--r--src/client/views/nodes/DocumentView.scss (renamed from src/client/views/nodes/NodeView.scss)2
-rw-r--r--src/client/views/nodes/DocumentView.tsx323
-rw-r--r--src/client/views/nodes/FieldView.tsx13
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss12
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx18
-rw-r--r--src/client/views/nodes/ImageBox.scss13
-rw-r--r--src/client/views/nodes/ImageBox.tsx25
-rw-r--r--src/client/views/nodes/WebView.tsx22
-rw-r--r--src/debug/Viewer.tsx192
-rw-r--r--src/fields/BasicField.ts29
-rw-r--r--src/fields/Document.ts129
-rw-r--r--src/fields/DocumentReference.ts15
-rw-r--r--src/fields/Field.ts11
-rw-r--r--src/fields/HtmlField.ts25
-rw-r--r--src/fields/ImageField.ts15
-rw-r--r--src/fields/Key.ts46
-rw-r--r--src/fields/KeyStore.ts29
-rw-r--r--src/fields/ListField.ts93
-rw-r--r--src/fields/NumberField.ts14
-rw-r--r--src/fields/RichTextField.ts14
-rw-r--r--src/fields/TextField.ts16
-rw-r--r--src/server/Client.ts15
-rw-r--r--src/server/Message.ts125
-rw-r--r--src/server/ServerUtil.ts52
-rw-r--r--src/server/authentication/config/passport.ts49
-rw-r--r--src/server/authentication/controllers/user.ts107
-rw-r--r--src/server/authentication/models/User.ts89
-rw-r--r--src/server/database.ts81
-rw-r--r--src/server/index.js13
-rw-r--r--src/server/index.ts133
53 files changed, 3214 insertions, 1229 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index cc1d8f6c6..d4b7da52c 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -1,17 +1,22 @@
import v4 = require('uuid/v4');
import v5 = require("uuid/v5");
+import { Socket } from 'socket.io';
+import { Message, Types } from './server/Message';
export class Utils {
public static GenerateGuid(): string {
- return v4();
+ return v4()
}
public static GenerateDeterministicGuid(seed: string): string {
- return v5(seed, v5.URL);
+ return v5(seed, v5.URL)
}
public static GetScreenTransform(ele: HTMLElement): { scale: number, translateX: number, translateY: number } {
+ if (!ele) {
+ return { scale: 1, translateX: 1, translateY: 1 }
+ }
const rect = ele.getBoundingClientRect();
const scale = ele.offsetWidth == 0 && rect.width == 0 ? 1 : rect.width / ele.offsetWidth;
const translateX = rect.left;
@@ -19,4 +24,32 @@ export class Utils {
return { scale, translateX, translateY };
}
+
+ public static CopyText(text: string) {
+ var textArea = document.createElement("textarea");
+ textArea.value = text;
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+
+ try { document.execCommand('copy'); } catch (err) { }
+
+ document.body.removeChild(textArea);
+ }
+
+ public static Emit<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T) {
+ socket.emit(message.Message, args);
+ }
+
+ public static EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T, fn: (args: any) => any) {
+ socket.emit(message.Message, args, fn);
+ }
+
+ public static AddServerHandler<T>(socket: Socket, message: Message<T>, handler: (args: T) => any) {
+ socket.on(message.Message, handler);
+ }
+
+ public static AddServerHandlerCallback<T>(socket: Socket, message: Message<T>, handler: (args: [T, (res: any) => any]) => any) {
+ socket.on(message.Message, (arg: T, fn: (res: any) => any) => handler([arg, fn]));
+ }
} \ No newline at end of file
diff --git a/src/client/Server.ts b/src/client/Server.ts
index 0cb6e17c2..2d162b93a 100644
--- a/src/client/Server.ts
+++ b/src/client/Server.ts
@@ -1,40 +1,77 @@
-import { Field, FieldWaiting, FieldId, FIELD_WAITING, FieldValue, Opt } from "../fields/Field"
-import { Key, KeyStore } from "../fields/Key"
-import { ObservableMap, action } from "mobx";
+import { Key } from "../fields/Key"
+import { ObservableMap, action, reaction } from "mobx";
+import { Field, FieldWaiting, FIELD_WAITING, Opt, FieldId } from "../fields/Field"
import { Document } from "../fields/Document"
import { SocketStub } from "./SocketStub";
+import * as OpenSocket from 'socket.io-client';
+import { Utils } from "./../Utils";
+import { MessageStore, Types } from "./../server/Message";
export class Server {
- private static ClientFieldsCached: ObservableMap<FieldId, Field | FIELD_WAITING> = new ObservableMap();
+ public static ClientFieldsCached: ObservableMap<FieldId, Field | FIELD_WAITING> = new ObservableMap();
+ static Socket: SocketIOClient.Socket = OpenSocket("http://localhost:1234");
+ static GUID: string = Utils.GenerateGuid()
+
// Retrieves the cached value of the field and sends a request to the server for the real value (if it's not cached).
// Call this is from within a reaction and test whether the return value is FieldWaiting.
// 'hackTimeout' is here temporarily for simplicity when debugging things.
- public static GetField(fieldid: FieldId, callback: (field: Field) => void = (f) => { }, hackTimeout: number = -1) {
- if (!this.ClientFieldsCached.get(fieldid)) {
+ public static GetField(fieldid: FieldId, callback: (field: Opt<Field>) => void): Opt<Field> | FIELD_WAITING {
+ let cached = this.ClientFieldsCached.get(fieldid);
+ if (!cached) {
this.ClientFieldsCached.set(fieldid, FieldWaiting);
- //simulating a server call with a registered callback action
- SocketStub.SEND_FIELD_REQUEST(fieldid,
- action((field: Field) => callback(Server.cacheField(field))),
- hackTimeout);
- } else if (this.ClientFieldsCached.get(fieldid) != FieldWaiting) {
- callback(this.ClientFieldsCached.get(fieldid) as Field);
+ SocketStub.SEND_FIELD_REQUEST(fieldid, action((field: Field | undefined) => {
+ let cached = this.ClientFieldsCached.get(fieldid);
+ if (cached != FieldWaiting)
+ callback(cached);
+ else {
+ if (field) {
+ this.ClientFieldsCached.set(fieldid, field);
+ } else {
+ this.ClientFieldsCached.delete(fieldid)
+ }
+ callback(field)
+ }
+ }));
+ } else if (cached != FieldWaiting) {
+ setTimeout(() => callback(cached as Field), 0);
+ } else {
+ reaction(() => {
+ return this.ClientFieldsCached.get(fieldid);
+ }, (field, reaction) => {
+ if (field !== "<Waiting>") {
+ callback(field)
+ reaction.dispose()
+ }
+ })
}
- return this.ClientFieldsCached.get(fieldid);
+ return cached;
}
- static times = 0; // hack for testing
- public static GetDocumentField(doc: Document, key: Key): FieldValue<Field> {
- var hackTimeout: number = key == KeyStore.Data ? (this.times++ == 0 ? 5000 : 1000) : key == KeyStore.X ? 2500 : 500;
+ public static GetFields(fieldIds: FieldId[], callback: (fields: { [id: string]: Field }) => any) {
+ SocketStub.SEND_FIELDS_REQUEST(fieldIds, (fields) => {
+ for (let key in fields) {
+ let field = fields[key];
+ if (!this.ClientFieldsCached.has(field.Id)) {
+ this.ClientFieldsCached.set(field.Id, field)
+ }
+ }
+ callback(fields)
+ });
+ }
- let fieldId = doc._proxies.get(key);
- if (fieldId) {
- return this.GetField(fieldId,
- action((fieldfromserver: Field) => {
- doc._proxies.delete(key);
- doc.fields.set(key, fieldfromserver);
- })
- , hackTimeout);
+ public static GetDocumentField(doc: Document, key: Key, callback?: (field: Field) => void) {
+ let field = doc._proxies.get(key.Id);
+ if (field) {
+ this.GetField(field,
+ action((fieldfromserver: Opt<Field>) => {
+ if (fieldfromserver) {
+ doc.fields.set(key.Id, { key, field: fieldfromserver });
+ if (callback) {
+ callback(fieldfromserver);
+ }
+ }
+ }));
}
}
@@ -42,13 +79,22 @@ export class Server {
SocketStub.SEND_ADD_DOCUMENT(document);
}
public static AddDocumentField(doc: Document, key: Key, value: Field) {
+ console.log("Add doc field " + doc.Title + " " + key.Name + " fid " + value.Id + " " + value);
SocketStub.SEND_ADD_DOCUMENT_FIELD(doc, key, value);
}
public static DeleteDocumentField(doc: Document, key: Key) {
SocketStub.SEND_DELETE_DOCUMENT_FIELD(doc, key);
}
- public static SetFieldValue(field: Field, value: any) {
- SocketStub.SEND_SET_FIELD(field, value);
+
+ public static UpdateField(field: Field) {
+ if (!this.ClientFieldsCached.has(field.Id)) {
+ this.ClientFieldsCached.set(field.Id, field)
+ }
+ SocketStub.SEND_SET_FIELD(field);
+ }
+
+ static connected(message: string) {
+ Server.Socket.emit(MessageStore.Bar.Message, Server.GUID);
}
@action
@@ -61,4 +107,18 @@ export class Server {
}
return this.ClientFieldsCached.get(clientField.Id) as Field;
}
+
+ @action
+ static updateField(field: { _id: string, data: any, type: Types }) {
+ if (Server.ClientFieldsCached.has(field._id)) {
+ var f = Server.ClientFieldsCached.get(field._id);
+ if (f && f != FieldWaiting) {
+ f.UpdateFromServer(field.data);
+ f.init(() => { });
+ }
+ }
+ }
}
+
+Server.Socket.on(MessageStore.Foo.Message, Server.connected);
+Server.Socket.on(MessageStore.SetField.Message, Server.updateField); \ No newline at end of file
diff --git a/src/client/SocketStub.ts b/src/client/SocketStub.ts
index cea30cb8b..18df4ca0a 100644
--- a/src/client/SocketStub.ts
+++ b/src/client/SocketStub.ts
@@ -1,7 +1,11 @@
-import { Field, FieldId } from "../fields/Field"
-import { Key, KeyStore } from "../fields/Key"
-import { ObservableMap, action } from "mobx";
+import { Key } from "../fields/Key"
+import { Field, FieldId, Opt } from "../fields/Field"
+import { ObservableMap } from "mobx";
import { Document } from "../fields/Document"
+import { MessageStore, DocumentTransfer } from "../server/Message";
+import { Utils } from "../Utils";
+import { Server } from "./Server";
+import { ServerUtils } from "../server/ServerUtil";
export class SocketStub {
@@ -12,33 +16,46 @@ export class SocketStub {
// ...SOCKET(ADD_DOCUMENT, serialied document)
// server stores each document field in its repository of stored fields
- document.fields.forEach((f, key) => this.FieldStore.set((f as Field).Id, f as Field));
-
- // server stores stripped down document (w/ only field id proxies) in the field store
- this.FieldStore.set(document.Id, new Document(document.Id));
- document.fields.forEach((f, key) => (this.FieldStore.get(document.Id) as Document)._proxies.set(key, (f as Field).Id));
+ // document.fields.forEach((f, key) => this.FieldStore.set((f as Field).Id, f as Field));
+ // let strippedDoc = new Document(document.Id);
+ // document.fields.forEach((f, key) => {
+ // if (f) {
+ // // let args: SetFieldArgs = new SetFieldArgs(f.Id, f.GetValue())
+ // let args: Transferable = f.ToJson()
+ // Utils.Emit(Server.Socket, MessageStore.SetField, args)
+ // }
+ // })
+
+ // // server stores stripped down document (w/ only field id proxies) in the field store
+ // this.FieldStore.set(document.Id, new Document(document.Id));
+ // document.fields.forEach((f, key) => (this.FieldStore.get(document.Id) as Document)._proxies.set(key.Id, (f as Field).Id));
+
+ console.log("sending " + document.Title);
+ Utils.Emit(Server.Socket, MessageStore.AddDocument, new DocumentTransfer(document.ToJson()))
}
- public static SEND_FIELD_REQUEST(fieldid: FieldId, callback: (field: Field) => void, timeout: number) {
-
- if (timeout < 0)// this is a hack to make things easier to setup until we have a server... won't be neededa fter that.
- callback(this.FieldStore.get(fieldid) as Field);
- else { // actual logic here...
-
- // Send a request for fieldid to the server
- // ...SOCKET(RETRIEVE_FIELD, fieldid)
-
- // server responds (simulated with a timeout) and the callback is invoked
- setTimeout(() =>
-
- // when the field data comes back, call the callback() function
- callback(this.FieldStore.get(fieldid) as Field),
-
-
- timeout);
+ public static SEND_FIELD_REQUEST(fieldid: FieldId, callback: (field: Opt<Field>) => void) {
+ if (fieldid) {
+ Utils.EmitCallback(Server.Socket, MessageStore.GetField, fieldid, (field: any) => {
+ if (field) {
+ ServerUtils.FromJson(field).init(callback);
+ } else {
+ callback(undefined);
+ }
+ })
}
}
+ public static SEND_FIELDS_REQUEST(fieldIds: FieldId[], callback: (fields: { [key: string]: Field }) => any) {
+ Utils.EmitCallback(Server.Socket, MessageStore.GetFields, fieldIds, (fields: any[]) => {
+ let fieldMap: any = {};
+ for (let field of fields) {
+ fieldMap[field._id] = ServerUtils.FromJson(field);
+ }
+ callback(fieldMap)
+ });
+ }
+
public static SEND_ADD_DOCUMENT_FIELD(doc: Document, key: Key, value: Field) {
// Send a serialized version of the field to the server along with the
@@ -49,10 +66,11 @@ export class SocketStub {
// server updates its document to hold a proxy mapping from key => fieldId
var document = this.FieldStore.get(doc.Id) as Document;
if (document)
- document._proxies.set(key, value.Id);
+ document._proxies.set(key.Id, value.Id);
// server adds the field to its repository of fields
this.FieldStore.set(value.Id, value);
+ // Utils.Emit(Server.Socket, MessageStore.AddDocument, new DocumentTransfer(doc.ToJson()))
}
public static SEND_DELETE_DOCUMENT_FIELD(doc: Document, key: Key) {
@@ -63,16 +81,15 @@ export class SocketStub {
// Server removes the field id from the document's list of field proxies
var document = this.FieldStore.get(doc.Id) as Document;
if (document)
- document._proxies.delete(key);
+ document._proxies.delete(key.Id);
}
- public static SEND_SET_FIELD(field: Field, value: any) {
+ public static SEND_SET_FIELD(field: Field) {
// Send a request to set the value of a field
// ...SOCKET(SET_FIELD, field id, serialized field value)
// Server updates the value of the field in its fieldstore
- if (this.FieldStore.get(field.Id))
- this.FieldStore.get(field.Id)!.TrySetValue(value);
+ Utils.Emit(Server.Socket, MessageStore.SetField, field.ToJson())
}
}
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 7512d25cb..15ecfbfe6 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,18 +1,18 @@
import { Document } from "../../fields/Document";
import { Server } from "../Server";
-import { KeyStore } from "../../fields/Key";
+import { KeyStore } from "../../fields/KeyStore";
import { TextField } from "../../fields/TextField";
import { NumberField } from "../../fields/NumberField";
import { ListField } from "../../fields/ListField";
import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
-import { CollectionDockingView } from "../views/collections/CollectionDockingView";
-import { CollectionSchemaView } from "../views/collections/CollectionSchemaView";
import { ImageField } from "../../fields/ImageField";
import { ImageBox } from "../views/nodes/ImageBox";
-import { CollectionFreeFormView } from "../views/collections/CollectionFreeFormView";
-import { FieldId } from "../../fields/Field";
+import { CollectionView, CollectionViewType } from "../views/collections/CollectionView";
+import { FieldView } from "../views/nodes/FieldView";
+import { HtmlField } from "../../fields/HtmlField";
+import { WebView } from "../views/nodes/WebView";
-interface DocumentOptions {
+export interface DocumentOptions {
x?: number;
y?: number;
width?: number;
@@ -23,26 +23,35 @@ interface DocumentOptions {
}
export namespace Documents {
+ export function initProtos(callback: () => void) {
+ Server.GetFields([collectionProtoId, textProtoId, imageProtoId], (fields) => {
+ collectionProto = fields[collectionProtoId] as Document;
+ imageProto = fields[imageProtoId] as Document;
+ textProto = fields[textProtoId] as Document;
+ callback()
+ });
+ }
+
function setupOptions(doc: Document, options: DocumentOptions): void {
- if (options.x) {
+ if (options.x !== undefined) {
doc.SetData(KeyStore.X, options.x, NumberField);
}
- if (options.y) {
+ if (options.y !== undefined) {
doc.SetData(KeyStore.Y, options.y, NumberField);
}
- if (options.width) {
+ if (options.width !== undefined) {
doc.SetData(KeyStore.Width, options.width, NumberField);
}
- if (options.height) {
+ if (options.height !== undefined) {
doc.SetData(KeyStore.Height, options.height, NumberField);
}
- if (options.nativeWidth) {
+ if (options.nativeWidth !== undefined) {
doc.SetData(KeyStore.NativeWidth, options.nativeWidth, NumberField);
}
- if (options.nativeHeight) {
+ if (options.nativeHeight !== undefined) {
doc.SetData(KeyStore.NativeHeight, options.nativeHeight, NumberField);
}
- if (options.title) {
+ if (options.title !== undefined) {
doc.SetData(KeyStore.Title, options.title, TextField);
}
doc.SetData(KeyStore.Scale, 1, NumberField);
@@ -51,9 +60,10 @@ export namespace Documents {
}
let textProto: Document;
+ const textProtoId = "textProto";
function GetTextPrototype(): Document {
if (!textProto) {
- textProto = new Document();
+ 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));
@@ -71,106 +81,113 @@ export namespace Documents {
return doc;
}
- let schemaProto: Document;
- function GetSchemaPrototype(): Document {
- if (!schemaProto) {
- schemaProto = new Document();
- schemaProto.Set(KeyStore.X, new NumberField(0));
- schemaProto.Set(KeyStore.Y, new NumberField(0));
- schemaProto.Set(KeyStore.Width, new NumberField(300));
- schemaProto.Set(KeyStore.Height, new NumberField(150));
- schemaProto.Set(KeyStore.Layout, new TextField(CollectionSchemaView.LayoutString()));
- schemaProto.Set(KeyStore.LayoutKeys, new ListField([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 schemaProto;
+ return htmlProto;
}
- export function SchemaDocument(documents: Array<Document>, options: DocumentOptions = {}): Document {
- let doc = GetSchemaPrototype().MakeDelegate();
+ export function HtmlDocument(html: string, options: DocumentOptions = {}): Document {
+ let doc = GetHtmlPrototype().MakeDelegate();
setupOptions(doc, options);
- doc.Set(KeyStore.Data, new ListField(documents));
+ doc.Set(KeyStore.Data, new HtmlField(html));
return doc;
}
-
- let dockProto: Document;
- function GetDockPrototype(): Document {
- if (!dockProto) {
- dockProto = new Document();
- dockProto.Set(KeyStore.X, new NumberField(0));
- dockProto.Set(KeyStore.Y, new NumberField(0));
- dockProto.Set(KeyStore.Width, new NumberField(300));
- dockProto.Set(KeyStore.Height, new NumberField(150));
- dockProto.Set(KeyStore.Layout, new TextField(CollectionDockingView.LayoutString()));
- dockProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data]));
- }
- return dockProto;
- }
-
- export function DockDocument(documents: Array<Document>, options: DocumentOptions = {}): Document {
- let doc = GetDockPrototype().MakeDelegate();
- setupOptions(doc, options);
- doc.Set(KeyStore.Data, new ListField(documents));
- return doc;
- }
-
-
- let imageProtoId: FieldId;
+ let imageProto: Document;
+ const imageProtoId = "imageProto";
function GetImagePrototype(): Document {
- if (imageProtoId === undefined) {
- let imageProto = new Document();
- imageProtoId = imageProto.Id;
+ 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("<CollectionFreeFormView DocumentForCollection={Document} CollectionFieldKey={AnnotationsKey} BackgroundView={BackgroundView} ContainingDocumentView={DocumentView} />"));
+ 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]));
- Server.AddDocument(imageProto);
return imageProto;
}
- return Server.GetField(imageProtoId) as Document;
+ return imageProto;
+
}
+ // example of custom display string for an image that shows a caption.
+ function EmbeddedCaption() {
+ return `<div style="height:100%">
+ <div style="position:relative; margin:auto; height:85%;" >`
+ + ImageBox.LayoutString() +
+ `</div>
+ <div style="position:relative; overflow:auto; 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;">`
+ + 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)));
-
- let annotation = Documents.TextDocument({ title: "hello" });
- Server.AddDocument(annotation);
- doc.Set(KeyStore.Annotations, new ListField([annotation]));
- Server.AddDocument(doc);
- var sdoc = Server.GetField(doc.Id) as Document;
- return sdoc;
+ 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]));
+ console.log("" + doc.GetNumber(KeyStore.Height, 311));
+ return doc;
}
let collectionProto: Document;
+ const collectionProtoId = "collectionProto";
function GetCollectionPrototype(): Document {
if (!collectionProto) {
- collectionProto = new Document();
- collectionProto.Set(KeyStore.X, new NumberField(0));
- collectionProto.Set(KeyStore.Y, new NumberField(0));
+ 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.Width, new NumberField(300));
- collectionProto.Set(KeyStore.Height, new NumberField(300));
- collectionProto.Set(KeyStore.Layout, new TextField(CollectionFreeFormView.LayoutString()));
+ collectionProto.Set(KeyStore.Layout, new TextField(CollectionView.LayoutString("DataKey")));
collectionProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data]));
}
return collectionProto;
}
- export function CollectionDocument(documents: Array<Document>, options: DocumentOptions = {}): Document {
- let doc = GetCollectionPrototype().MakeDelegate();
+ export function CollectionDocument(data: Array<Document> | string, viewType: CollectionViewType, options: DocumentOptions = {}, id?: string): Document {
+ let doc = GetCollectionPrototype().MakeDelegate(id);
setupOptions(doc, options);
- doc.Set(KeyStore.Data, new ListField(documents));
+ 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 f4dcce7c8..aab23f91c 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,3 +1,4 @@
+import { DocumentDecorations } from "../views/DocumentDecorations";
export namespace DragManager {
export function Root() {
@@ -43,6 +44,7 @@ export namespace DragManager {
drop: (e: Event, de: DropEvent) => void;
}
+
export function MakeDropTarget(element: HTMLElement, options: DropOptions): DragDropDisposer {
if ("canDrop" in element.dataset) {
throw new Error("Element is already droppable, can't make it droppable again");
@@ -59,10 +61,8 @@ export namespace DragManager {
};
}
-
- let _lastPointerX: number = 0;
- let _lastPointerY: number = 0;
- 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");
DragManager.Root().appendChild(dragDiv);
@@ -75,18 +75,26 @@ 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})`;
+ dragElement.style.width = `${rect.width / scaleX}px`;
+ dragElement.style.height = `${rect.height / scaleY}px`;
+ // It seems like the above code should be able to just be this:
+ // dragElement.style.transform = `translate(${x}px, ${y}px)`;
+ // dragElement.style.width = `${rect.width}px`;
+ // dragElement.style.height = `${rect.height}px`;
dragDiv.appendChild(dragElement);
- _lastPointerX = dragData["xOffset"] + rect.left;
- _lastPointerY = dragData["yOffset"] + rect.top;
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) {
@@ -96,14 +104,14 @@ export namespace DragManager {
const moveHandler = (e: PointerEvent) => {
e.stopPropagation();
e.preventDefault();
- x += e.clientX - _lastPointerX; _lastPointerX = e.clientX;
- y += e.clientY - _lastPointerY; _lastPointerY = e.clientY;
+ x += e.movementX;
+ y += e.movementY;
dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`;
};
const upHandler = (e: PointerEvent) => {
document.removeEventListener("pointermove", moveHandler, true);
document.removeEventListener("pointerup", upHandler);
- FinishDrag(dragElement, e, options, dragData);
+ FinishDrag(dragElement, e, dragData, options);
if (hideSource && !wasHidden) {
ele.hidden = false;
}
@@ -112,7 +120,7 @@ export namespace DragManager {
document.addEventListener("pointerup", upHandler);
}
- function FinishDrag(dragEle: HTMLElement, e: PointerEvent, options: DragOptions, dragData: { [index: string]: any }) {
+ function FinishDrag(dragEle: HTMLElement, e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions) {
dragDiv.removeChild(dragEle);
const target = document.elementFromPoint(e.x, e.y);
if (!target) {
@@ -126,6 +134,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/Scripting.ts b/src/client/util/Scripting.ts
index 6bc5fa412..befb9df4c 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -6,7 +6,7 @@ import { NumberField as NumberFieldImport, NumberField } from "../../fields/Numb
import { ImageField as ImageFieldImport } from "../../fields/ImageField";
import { TextField as TextFieldImport, TextField } from "../../fields/TextField";
import { RichTextField as RichTextFieldImport } from "../../fields/RichTextField";
-import { KeyStore as KeyStoreImport } from "../../fields/Key";
+import { KeyStore as KeyStoreImport } from "../../fields/KeyStore";
export interface ExecutableScript {
(): any;
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 0759ae110..1a711ae64 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,13 +1,13 @@
-import { CollectionFreeFormDocumentView } from "../views/nodes/CollectionFreeFormDocumentView";
import { observable, action } from "mobx";
+import { DocumentView } from "../views/nodes/DocumentView";
export namespace SelectionManager {
class Manager {
@observable
- SelectedDocuments: Array<CollectionFreeFormDocumentView> = [];
+ SelectedDocuments: Array<DocumentView> = [];
@action
- SelectDoc(doc: CollectionFreeFormDocumentView, ctrlPressed: boolean): void {
+ SelectDoc(doc: DocumentView, ctrlPressed: boolean): void {
// if doc is not in SelectedDocuments, add it
if (!ctrlPressed) {
manager.SelectedDocuments = [];
@@ -21,11 +21,11 @@ export namespace SelectionManager {
const manager = new Manager;
- export function SelectDoc(doc: CollectionFreeFormDocumentView, ctrlPressed: boolean): void {
+ export function SelectDoc(doc: DocumentView, ctrlPressed: boolean): void {
manager.SelectDoc(doc, ctrlPressed)
}
- export function IsSelected(doc: CollectionFreeFormDocumentView): boolean {
+ export function IsSelected(doc: DocumentView): boolean {
return manager.SelectedDocuments.indexOf(doc) !== -1;
}
@@ -33,7 +33,7 @@ export namespace SelectionManager {
manager.SelectedDocuments = []
}
- export function SelectedDocuments(): Array<CollectionFreeFormDocumentView> {
+ export function SelectedDocuments(): Array<DocumentView> {
return manager.SelectedDocuments;
}
} \ No newline at end of file
diff --git a/src/client/util/Transform.ts b/src/client/util/Transform.ts
index 7861ed308..3e1039166 100644
--- a/src/client/util/Transform.ts
+++ b/src/client/util/Transform.ts
@@ -7,6 +7,10 @@ export class Transform {
return new Transform(0, 0, 1);
}
+ get TranslateX(): number { return this._translateX; }
+ get TranslateY(): number { return this._translateY; }
+ get Scale(): number { return this._scale; }
+
constructor(x: number, y: number, scale: number) {
this._translateX = x;
this._translateY = y;
@@ -19,56 +23,67 @@ export class Transform {
return this;
}
- translated = (x: number, y: number): Transform => {
- return this.copy().translate(x, y);
- }
-
- preTranslate = (x: number, y: number): Transform => {
- this._translateX += x * this._scale;
- this._translateY += y * this._scale;
+ scale = (scale: number): Transform => {
+ this._scale *= scale;
+ this._translateX *= scale;
+ this._translateY *= scale;
return this;
}
- preTranslated = (x: number, y: number): Transform => {
- return this.copy().preTranslate(x, y);
+ scaleAbout = (scale: number, x: number, y: number): Transform => {
+ this._translateX += x * this._scale - x * this._scale * scale;
+ this._translateY += y * this._scale - y * this._scale * scale;
+ this._scale *= scale;
+ return this;
}
- scale = (scale: number): Transform => {
- this._scale *= scale;
+ transform = (transform: Transform): Transform => {
+ this._translateX = transform._translateX + transform._scale * this._translateX;
+ this._translateY = transform._translateY + transform._scale * this._translateY;
+ this._scale *= transform._scale;
return this;
}
- scaled = (scale: number): Transform => {
- return this.copy().scale(scale);
+ preTranslate = (x: number, y: number): Transform => {
+ this._translateX += this._scale * x;
+ this._translateY += this._scale * y;
+ return this;
}
preScale = (scale: number): Transform => {
this._scale *= scale;
- this._translateX *= scale;
- this._translateY *= scale;
return this;
}
- preScaled = (scale: number): Transform => {
- return this.copy().preScale(scale);
- }
-
- transform = (transform: Transform): Transform => {
+ preTransform = (transform: Transform): Transform => {
this._translateX += transform._translateX * this._scale;
this._translateY += transform._translateY * this._scale;
this._scale *= transform._scale;
return this;
}
- transformed = (transform: Transform): Transform => {
- return this.copy().transform(transform);
+ translated = (x: number, y: number): Transform => {
+ return this.copy().translate(x, y);
}
- preTransform = (transform: Transform): Transform => {
- this._translateX = transform._translateX + this._translateX * transform._scale;
- this._translateY = transform._translateY + this._translateY * transform._scale;
- this._scale *= transform._scale;
- return this;
+ preTranslated = (x: number, y: number): Transform => {
+ return this.copy().preTranslate(x, y);
+ }
+
+ scaled = (scale: number): Transform => {
+ return this.copy().scale(scale);
+ }
+
+ scaledAbout = (scale: number, x: number, y: number): Transform => {
+ return this.copy().scaleAbout(scale, x, y);
+ }
+
+ preScaled = (scale: number): Transform => {
+ return this.copy().preScale(scale);
+ }
+
+ transformed = (transform: Transform): Transform => {
+ return this.copy().transform(transform);
}
preTransformed = (transform: Transform): Transform => {
@@ -83,6 +98,16 @@ export class Transform {
return [x, y];
}
+ transformDirection = (x: number, y: number): [number, number] => {
+ 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/util/UndoManager.ts b/src/client/util/UndoManager.ts
new file mode 100644
index 000000000..46ad558f3
--- /dev/null
+++ b/src/client/util/UndoManager.ts
@@ -0,0 +1,133 @@
+import { observable, action } from "mobx";
+
+function propertyDecorator(target: any, key: string | symbol) {
+ Object.defineProperty(target, key, {
+ configurable: true,
+ enumerable: false,
+ get: function () {
+ return 5;
+ },
+ set: function (value: any) {
+ Object.defineProperty(this, key, {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: function (...args: any[]) {
+ try {
+ UndoManager.StartBatch();
+ return value.apply(this, args);
+ } finally {
+ UndoManager.EndBatch();
+ }
+ }
+ })
+ }
+ })
+}
+export function undoBatch(target: any, key: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any {
+ if (!descriptor) {
+ propertyDecorator(target, key);
+ return;
+ }
+ const oldFunction = descriptor.value;
+
+ descriptor.value = function (...args: any[]) {
+ try {
+ UndoManager.StartBatch()
+ return oldFunction.apply(this, args)
+ } finally {
+ UndoManager.EndBatch()
+ }
+ }
+
+ return descriptor;
+}
+
+export namespace UndoManager {
+ export interface UndoEvent {
+ undo: () => void;
+ redo: () => void;
+ }
+ type UndoBatch = UndoEvent[];
+
+ let undoStack: UndoBatch[] = observable([]);
+ let redoStack: UndoBatch[] = observable([]);
+ let currentBatch: UndoBatch | undefined;
+ let batchCounter = 0;
+ let undoing = false;
+
+ export function AddEvent(event: UndoEvent): void {
+ if (currentBatch && batchCounter && !undoing) {
+ currentBatch.push(event);
+ }
+ }
+
+ export function CanUndo(): boolean {
+ return undoStack.length > 0;
+ }
+
+ export function CanRedo(): boolean {
+ return redoStack.length > 0;
+ }
+
+ export function StartBatch(): void {
+ batchCounter++;
+ if (batchCounter > 0) {
+ currentBatch = [];
+ }
+ }
+
+ export const EndBatch = action(() => {
+ batchCounter--;
+ if (batchCounter === 0 && currentBatch && currentBatch.length) {
+ undoStack.push(currentBatch);
+ redoStack.length = 0;
+ currentBatch = undefined;
+ }
+ })
+
+ export function RunInBatch(fn: () => void) {
+ StartBatch();
+ fn();
+ EndBatch();
+ }
+
+ export const Undo = action(() => {
+ if (undoStack.length === 0) {
+ return;
+ }
+
+ let commands = undoStack.pop();
+ if (!commands) {
+ return;
+ }
+
+ undoing = true;
+ for (let i = commands.length - 1; i >= 0; i--) {
+ commands[i].undo();
+ }
+ undoing = false;
+
+ redoStack.push(commands);
+ })
+
+ export const Redo = action(() => {
+ if (redoStack.length === 0) {
+ return;
+ }
+
+ let commands = redoStack.pop();
+ if (!commands) {
+ return;
+ }
+
+ undoing = true;
+ for (let i = 0; i < commands.length; i++) {
+ commands[i].redo();
+ }
+ undoing = false;
+
+ undoStack.push(commands);
+ })
+
+} \ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 7efaa5533..975a125f7 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,16 +1,17 @@
-import { observable, computed } from "mobx";
+import { observable, computed, action } from "mobx";
import React = require("react");
import { SelectionManager } from "../util/SelectionManager";
import { observer } from "mobx-react";
import './DocumentDecorations.scss'
-import { CollectionFreeFormView } from "./collections/CollectionFreeFormView";
+import { KeyStore } from '../../fields/KeyStore'
+import { NumberField } from "../../fields/NumberField";
@observer
export class DocumentDecorations extends React.Component {
static Instance: DocumentDecorations
private _resizer = ""
private _isPointerDown = false;
- @observable private _opacity = 1;
+ @observable private _hidden = false;
constructor(props: Readonly<{}>) {
super(props)
@@ -20,28 +21,24 @@ export class DocumentDecorations extends React.Component {
@computed
get Bounds(): { x: number, y: number, b: number, r: number } {
- return SelectionManager.SelectedDocuments().reduce((bounds, element) => {
- if (element.props.ContainingCollectionView != undefined &&
- !(element.props.ContainingCollectionView instanceof CollectionFreeFormView)) {
+ return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => {
+ if (documentView.props.isTopMost) {
return bounds;
}
- var spt = element.TransformToScreenPoint(0, 0);
- var bpt = element.TransformToScreenPoint(element.width, element.height);
+ let transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse();
+ var [sptX, sptY] = transform.transformPoint(0, 0);
+ let [bptX, bptY] = transform.transformPoint(documentView.props.PanelWidth(), documentView.props.PanelHeight());
return {
- x: Math.min(spt.ScreenX, bounds.x), y: Math.min(spt.ScreenY, bounds.y),
- r: Math.max(bpt.ScreenX, bounds.r), b: Math.max(bpt.ScreenY, bounds.b)
+ x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
+ r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
}
}, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE });
}
- @computed
- get opacity(): number {
- return this._opacity
- }
- set opacity(o: number) {
- this._opacity = Math.min(Math.max(0, o), 1)
- }
+ @computed
+ public get Hidden() { return this._hidden; }
+ public set Hidden(value: boolean) { this._hidden = value; }
onPointerDown = (e: React.PointerEvent): void => {
e.stopPropagation();
@@ -104,17 +101,29 @@ export class DocumentDecorations extends React.Component {
}
SelectionManager.SelectedDocuments().forEach(element => {
- const rect = element.screenRect;
+ const rect = element.screenRect();
if (rect.width !== 0) {
- let scale = element.width / rect.width;
- let actualdW = Math.max(element.width + (dW * scale), 20);
- let actualdH = Math.max(element.height + (dH * scale), 20);
- element.x += dX * (actualdW - element.width);
- element.y += dY * (actualdH - element.height);
- if (Math.abs(dW) > Math.abs(dH))
- element.width = actualdW;
- else
- element.height = actualdH;
+ let doc = element.props.Document;
+ 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 / 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;
+ }
+ doc.SetNumber(KeyStore.Width, actualdW);
+ doc.SetNumber(KeyStore.Height, actualdH);
}
})
}
@@ -131,13 +140,19 @@ export class DocumentDecorations extends React.Component {
render() {
var bounds = this.Bounds;
+ if (this.Hidden) {
+ return (null);
+ }
+ if (isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
+ console.log("DocumentDecorations: Bounds Error")
+ return (null);
+ }
return (
<div id="documentDecorations-container" style={{
width: (bounds.r - bounds.x + 40) + "px",
height: (bounds.b - bounds.y + 40) + "px",
left: bounds.x - 20,
top: bounds.y - 20,
- opacity: this.opacity
}}>
<div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-topResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
@@ -148,7 +163,6 @@ export class DocumentDecorations extends React.Component {
<div id="documentDecorations-bottomLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-bottomResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-
</div>
)
}
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 2e784d3f9..8d9a47eaa 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -6,6 +6,7 @@ export interface EditableProps {
GetValue(): string;
SetValue(value: string): boolean;
contents: any;
+ height: number
}
@observer
@@ -29,9 +30,10 @@ export class EditableView extends React.Component<EditableProps> {
style={{ width: "100%" }}></input>
} else {
return (
- <div>
+ <div className="editableView-container-editing" style={{ display: "flex", height: "100%", maxHeight: `${this.props.height}` }}
+ onClick={action(() => this.editing = true)}
+ >
{this.props.contents}
- <button onClick={action(() => this.editing = true)}>Edit</button>
</div>
)
}
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index b58704264..17dda899d 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -1,30 +1,28 @@
-import { action, configure } from 'mobx';
+import { action, configure, reaction, computed } 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, KeyStore as KS } from '../../fields/Key';
-import { ListField } from '../../fields/ListField';
-import { NumberField } from '../../fields/NumberField';
-import { TextField } from '../../fields/TextField';
+import { KeyStore } from '../../fields/KeyStore';
import "./Main.scss";
import { ContextMenu } from './ContextMenu';
import { DocumentView } from './nodes/DocumentView';
-import { ImageField } from '../../fields/ImageField';
+import { Server } from '../Server';
+import { Utils } from '../../Utils';
+import { ServerUtils } from '../../server/ServerUtil';
+import { MessageStore, DocumentTransfer } from '../../server/Message';
import { Transform } from '../util/Transform';
+import { CollectionDockingView } from './collections/CollectionDockingView';
+import { FieldWaiting } from '../../fields/Field';
+import { UndoManager } from '../util/UndoManager';
+import { DragManager } from '../util/DragManager';
configure({
enforceActions: "observed"
});
-
-const mainNodeCollection = new Array<Document>();
-let mainContainer = Documents.DockDocument(mainNodeCollection, {
- x: 0, y: 0, title: "main container"
-})
-
window.addEventListener("drop", function (e) {
e.preventDefault();
}, false)
@@ -32,6 +30,7 @@ window.addEventListener("dragover", function (e) {
e.preventDefault();
}, false)
document.addEventListener("pointerdown", action(function (e: PointerEvent) {
+ console.log(ContextMenu);
if (!ContextMenu.Instance.intersects(e.pageX, e.pageY)) {
ContextMenu.Instance.clearItems()
}
@@ -39,57 +38,130 @@ document.addEventListener("pointerdown", action(function (e: PointerEvent) {
//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://psmag.com/.image/t_share/MTMyNzc2NzM1MDY1MjgzMDM4/shutterstock_151341212jpg.jpg", {
- x: 450, y: 100, title: "cat 1", width: 606, height: 386, nativeWidth: 606, nativeHeight: 386
- });
- //doc3.Set(KeyStore.Data, new ImageField);
- const schemaDocs = Array.from(Array(5).keys()).map(v => Documents.ImageDocument("https://psmag.com/.image/t_share/MTMyNzc2NzM1MDY1MjgzMDM4/shutterstock_151341212jpg.jpg", {
- x: 50 + 100 * v, y: 50, width: 100, height: 100, title: "cat" + v, nativeWidth: 606, nativeHeight: 386
- }));
- schemaDocs[0].SetData(KS.Author, "Tyler", TextField);
- schemaDocs[4].SetData(KS.Author, "Bob", TextField);
- schemaDocs.push(doc2);
- const doc7 = Documents.SchemaDocument(schemaDocs)
- const docset = [doc1, doc2, doc3, doc7];
- let doc4 = Documents.CollectionDocument(docset, {
- x: 0, y: 400, title: "mini collection"
- });
- // 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 = [doc4, doc1, doc3];
- let doc6 = Documents.CollectionDocument(docset2, {
- x: 350, y: 100, width: 600, height: 600, title: "docking collection"
- });
- let mainNodes = null;// mainContainer.GetFieldT(KeyStore.Data, ListField);
- if (!mainNodes) {
- mainNodes = new ListField<Document>();
- }
- // 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);
-}
+// 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)
+
+const mainDocId = "mainDoc";
+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);
+ }
+ else {
+ mainContainer = Documents.DockDocument(JSON.stringify({ content: [{ type: 'row', content: [] }] }), { title: "main container" }, mainDocId);
+ Utils.Emit(Server.Socket, MessageStore.AddDocument, new DocumentTransfer(mainContainer.ToJson()))
+
+ setTimeout(() => {
+ mainfreeform = Documents.FreeformDocument([], { x: 0, y: 400, title: "mini collection" });
+ Utils.Emit(Server.Socket, MessageStore.AddDocument, new DocumentTransfer(mainfreeform.ToJson()));
+
+ var docs = [mainfreeform].map(doc => CollectionDockingView.makeDocumentConfig(doc));
+ mainContainer.SetText(KeyStore.Data, JSON.stringify({ content: [{ type: 'row', content: docs }] }));
+ mainContainer.Set(KeyStore.ActiveFrame, mainfreeform);
+ }, 0);
+ }
+
+ 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("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", {
+ width: 200, height: 200, title: "an image of a cat"
+ }));
+
+ let addClick = (creator: any) => action(() => {
+ var img = creator();
+ img.SetNumber(KeyStore.X, 0);
+ img.SetNumber(KeyStore.Y, 0);
+ mainfreeform.GetList<Document>(KeyStore.Data, []).push(img);
+ });
+
+ let imgRef = React.createRef<HTMLDivElement>();
+ let textRef = React.createRef<HTMLDivElement>();
+ let schemaRef = React.createRef<HTMLDivElement>();
+ let colRef = React.createRef<HTMLDivElement>();
+ let curMoveListener: any = null
+ let onRowMove = (creator: any, dragRef: any) => action((e: PointerEvent): void => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ document.removeEventListener("pointermove", curMoveListener);
+ document.removeEventListener('pointerup', onRowUp);
+ DragManager.StartDrag(dragRef.current!, { document: creator() });
+ });
+ let onRowUp = action((e: PointerEvent): void => {
+ document.removeEventListener("pointermove", curMoveListener);
+ document.removeEventListener('pointerup', onRowUp);
+ });
+ let onRowDown = (creator: any, dragRef: any) => (e: React.PointerEvent) => {
+ if (e.shiftKey) {
+ CollectionDockingView.Instance.StartOtherDrag(dragRef.current!, creator());
+ e.stopPropagation();
+ } else {
+ document.addEventListener("pointermove", curMoveListener = onRowMove(creator, dragRef));
+ document.addEventListener('pointerup', onRowUp);
+ }
+ }
+ 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 />
+ <div style={{ position: 'absolute', bottom: '0px', left: '0px', width: '150px' }} ref={imgRef} >
+ <button onPointerDown={onRowDown(addImageNode, imgRef)} onClick={addClick(addImageNode)}>Add Image</button></div>
+ <div style={{ position: 'absolute', bottom: '25px', left: '0px', width: '150px' }} ref={textRef}>
+ <button onPointerDown={onRowDown(addTextNode, textRef)} onClick={addClick(addTextNode)}>Add Text</button></div>
+ <div style={{ position: 'absolute', bottom: '50px', left: '0px', width: '150px' }} ref={colRef}>
+ <button onPointerDown={onRowDown(addColNode, colRef)} onClick={addClick(addColNode)}>Add Collection</button></div>
+ <div style={{ position: 'absolute', bottom: '75px', left: '0px', width: '150px' }} ref={schemaRef}>
+ <button onPointerDown={onRowDown(addSchemaNode, schemaRef)} onClick={addClick(addSchemaNode)}>Add Schema</button></div>
+ <button style={{ position: 'absolute', bottom: '100px', 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>
+ </div>),
+ document.getElementById('root'));
+ })
+});
+// 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((
- <div style={{ position: "absolute", width: "100%", height: "100%" }}>
- <DocumentView Document={mainContainer}
- AddDocument={undefined} RemoveDocument={undefined} GetTransform={() => Transform.Identity}
- Scaling={1}
- ContainingCollectionView={undefined} DocumentView={undefined} />
- <DocumentDecorations />
- <ContextMenu />
- </div>),
- 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 e3d50eb80..5fb632469 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -1,126 +1,54 @@
-import FlexLayout from "flexlayout-react";
import * as GoldenLayout from "golden-layout";
import 'golden-layout/src/css/goldenlayout-base.css';
import 'golden-layout/src/css/goldenlayout-dark-theme.css';
-import { action, computed } from "mobx";
+import { action, computed, observable, reaction } from "mobx";
import { observer } from "mobx-react";
import * as ReactDOM from 'react-dom';
+import Measure from "react-measure";
import { Document } from "../../../fields/Document";
-import { KeyStore } from "../../../fields/Key";
-import { ListField } from "../../../fields/ListField";
-import { NumberField } from "../../../fields/NumberField";
+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 { Transform } from "../../util/Transform";
+import { undoBatch } from "../../util/UndoManager";
import { DocumentView } from "../nodes/DocumentView";
import "./CollectionDockingView.scss";
-import { CollectionViewBase, CollectionViewProps, COLLECTION_BORDER_WIDTH } from "./CollectionViewBase";
+import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
import React = require("react");
-import { changeDependenciesStateTo0 } from "mobx/lib/internal";
+import { SubCollectionViewProps } from "./CollectionViewBase";
@observer
-export class CollectionDockingView extends CollectionViewBase {
-
- private static UseGoldenLayout = true;
- public static LayoutString() { return CollectionViewBase.LayoutString("CollectionDockingView"); }
- private _containerRef = React.createRef<HTMLDivElement>();
- @computed
- private get modelForFlexLayout() {
- const { CollectionFieldKey: fieldKey, DocumentForCollection: Document } = this.props;
- const value: Document[] = Document.GetData(fieldKey, ListField, []);
- var docs = value.map(doc => {
- return { type: 'tabset', weight: 50, selected: 0, children: [{ type: "tab", name: doc.Title, component: doc.Id }] };
- });
- return FlexLayout.Model.fromJson({
- global: {}, borders: [],
- layout: {
- "type": "row",
- "weight": 100,
- "children": docs
+export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
+ public static Instance: CollectionDockingView;
+ public static makeDocumentConfig(document: Document) {
+ return {
+ type: 'react-component',
+ component: 'DocumentFrameRenderer',
+ title: document.Title,
+ props: {
+ documentId: document.Id,
+ //collectionDockingView: CollectionDockingView.Instance
}
- });
- }
- @computed
- private get modelForGoldenLayout(): any {
- const { CollectionFieldKey: fieldKey, DocumentForCollection: Document } = this.props;
- const value: Document[] = Document.GetData(fieldKey, ListField, []);
- var docs = value.map(doc => {
- return { type: 'component', componentName: 'documentViewComponent', componentState: { doc: doc } };
- });
- return new GoldenLayout({
- settings: {
- selectionEnabled: true
- }, content: [{ type: 'row', content: docs }]
- });
- }
- constructor(props: CollectionViewProps) {
- super(props);
- }
-
- componentDidMount: () => void = () => {
- if (this._containerRef.current && CollectionDockingView.UseGoldenLayout) {
- this.goldenLayoutFactory();
- window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window
}
}
- componentWillUnmount: () => void = () => {
- window.removeEventListener('resize', this.onResize);
- }
- private nextId = (function () { var _next_id = 0; return function () { return _next_id++; } })();
+ private _goldenLayout: any = null;
+ private _dragDiv: any = null;
+ private _dragParent: HTMLElement | null = null;
+ private _dragElement: HTMLElement | undefined;
+ private _dragFakeElement: HTMLElement | undefined;
+ private _containerRef = React.createRef<HTMLDivElement>();
+ private _fullScreen: any = null;
- @action
- onResize = (event: any) => {
- var cur = this.props.ContainingDocumentView!.MainContent.current;
-
- // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed
- CollectionDockingView.myLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height);
- }
-
- @action
- onPointerDown = (e: React.PointerEvent): void => {
- if (e.button === 2 && this.active) {
- e.stopPropagation();
- e.preventDefault();
- } else {
- if (e.buttons === 1 && this.active) {
- e.stopPropagation();
- }
- }
- }
-
- flexLayoutFactory = (node: any): any => {
- var component = node.getComponent();
- if (component === "button") {
- return <button>{node.getName()}</button>;
- }
- const { CollectionFieldKey: fieldKey, DocumentForCollection: Document } = this.props;
- const value: Document[] = Document.GetData(fieldKey, ListField, []);
- for (var i: number = 0; i < value.length; i++) {
- if (value[i].Id === component) {
- return (<DocumentView key={value[i].Id} Document={value[i]}
- AddDocument={this.addDocument} RemoveDocument={this.removeDocument}
- GetTransform={() => Transform.Identity}
- Scaling={1}
- ContainingCollectionView={this} DocumentView={undefined} />);
- }
- }
- if (component === "text") {
- return (<div className="panel">Panel {node.getName()}</div>);
- }
+ constructor(props: SubCollectionViewProps) {
+ super(props);
+ CollectionDockingView.Instance = this;
+ (window as any).React = React;
+ (window as any).ReactDOM = ReactDOM;
}
- public static myLayout: any = null;
-
- private static _dragDiv: any = null;
- private static _dragParent: HTMLElement | null = null;
- private static _dragElement: HTMLDivElement;
- private static _dragFakeElement: HTMLDivElement;
- public static StartOtherDrag(dragElement: HTMLDivElement, dragDoc: Document) {
- var newItemConfig = {
- type: 'component',
- componentName: 'documentViewComponent',
- componentState: { doc: dragDoc }
- };
+ public StartOtherDrag(dragElement: HTMLElement, dragDoc: Document) {
this._dragElement = dragElement;
this._dragParent = dragElement.parentElement;
// bcz: we want to copy this document into the header, not move it there.
@@ -130,64 +58,61 @@ export class CollectionDockingView extends CollectionViewBase {
this._dragDiv = document.createElement("div");
this._dragDiv.style.opacity = 0;
DragManager.Root().appendChild(this._dragDiv);
- CollectionDockingView.myLayout.createDragSource(this._dragDiv, newItemConfig);
+ 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._dragFakeElement = dragElement.cloneNode(true) as HTMLElement;
this._dragParent!.appendChild(this._dragFakeElement);
// all of this must be undone when the document has been dropped (see tabCreated)
}
- _makeFullScreen: boolean = false;
- _maximizedStack: any = null;
- public static OpenFullScreen(document: Document) {
- var newItemConfig = {
- type: 'component',
- componentName: 'documentViewComponent',
- componentState: { doc: document }
- };
- CollectionDockingView.myLayout._makeFullScreen = true;
- CollectionDockingView.myLayout.root.contentItems[0].addChild(newItemConfig);
+ @action
+ public OpenFullScreen(document: Document) {
+ let newItemStackConfig = {
+ type: 'stack',
+ content: [CollectionDockingView.makeDocumentConfig(document)]
+ }
+ var docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout);
+ this._goldenLayout.root.contentItems[0].addChild(docconfig);
+ docconfig.callDownwards('_$init');
+ this._goldenLayout._$maximiseItem(docconfig);
+ this._fullScreen = docconfig;
+ this.stateChanged();
}
- public static CloseFullScreen() {
- if (CollectionDockingView.myLayout._maximizedStack != null) {
- CollectionDockingView.myLayout._maximizedStack.header.controlsContainer.find('.lm_close').click();
- CollectionDockingView.myLayout._maximizedStack = null;
+ @action
+ public CloseFullScreen() {
+ if (this._fullScreen) {
+ this._goldenLayout._$minimiseItem(this._fullScreen);
+ this._goldenLayout.root.contentItems[0].removeChild(this._fullScreen);
+ this._fullScreen = null;
+ this.stateChanged();
}
}
//
// Creates a vertical split on the right side of the docking view, and then adds the Document to that split
//
- public static AddRightSplit(document: Document) {
- var newItemConfig = {
- type: 'component',
- componentName: 'documentViewComponent',
- componentState: { doc: document }
- }
+ @action
+ public AddRightSplit(document: Document) {
+ this._goldenLayout.emit('stateChanged');
let newItemStackConfig = {
type: 'stack',
- content: [newItemConfig]
- };
- var newContentItem = new CollectionDockingView.myLayout._typeToItem[newItemStackConfig.type](CollectionDockingView.myLayout, newItemStackConfig, parent);
+ content: [CollectionDockingView.makeDocumentConfig(document)]
+ }
- if (CollectionDockingView.myLayout.root.contentItems[0].isRow) {
- var rowlayout = CollectionDockingView.myLayout.root.contentItems[0];
- var lastRowItem = rowlayout.contentItems[rowlayout.contentItems.length - 1];
+ var newContentItem = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout);
- lastRowItem.config["width"] *= 0.5;
- newContentItem.config["width"] = lastRowItem.config["width"];
- rowlayout.addChild(newContentItem, rowlayout.contentItems.length, true);
- rowlayout.callDownwards('setSize');
+ if (this._goldenLayout.root.contentItems[0].isRow) {
+ this._goldenLayout.root.contentItems[0].addChild(newContentItem);
}
else {
- var collayout = CollectionDockingView.myLayout.root.contentItems[0];
- var newRow = collayout.layoutManager.createContentItem({ type: "row" }, CollectionDockingView.myLayout);
+ var collayout = this._goldenLayout.root.contentItems[0];
+ var newRow = collayout.layoutManager.createContentItem({ type: "row" }, this._goldenLayout);
collayout.parent.replaceChild(collayout, newRow);
newRow.addChild(newContentItem, undefined, true);
@@ -195,103 +120,186 @@ export class CollectionDockingView extends CollectionViewBase {
collayout.config["width"] = 50;
newContentItem.config["width"] = 50;
- collayout.parent.callDownwards('setSize');
}
+ newContentItem.callDownwards('_$init');
+ this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]);
+ this._goldenLayout.emit('stateChanged');
+ this.stateChanged();
}
- goldenLayoutFactory() {
- CollectionDockingView.myLayout = this.modelForGoldenLayout;
- var layout = CollectionDockingView.myLayout;
- CollectionDockingView.myLayout.on('tabCreated', function (tab: any) {
- if (CollectionDockingView._dragDiv) {
- CollectionDockingView._dragDiv.removeChild(CollectionDockingView._dragElement);
- CollectionDockingView._dragParent!.removeChild(CollectionDockingView._dragFakeElement);
- CollectionDockingView._dragParent!.appendChild(CollectionDockingView._dragElement);
- DragManager.Root().removeChild(CollectionDockingView._dragDiv);
- CollectionDockingView._dragDiv = null;
+ setupGoldenLayout() {
+ var config = this.props.Document.GetText(KeyStore.Data, "");
+ if (config) {
+ if (!this._goldenLayout) {
+ this._goldenLayout = new GoldenLayout(JSON.parse(config));
}
- tab.setTitle(tab.contentItem.config.componentState.doc.Title);
- tab.closeElement.off('click') //unbind the current click handler
- .click(function () {
- tab.contentItem.remove();
- });
- });
-
- CollectionDockingView.myLayout.on('stackCreated', function (stack: any) {
- if (CollectionDockingView.myLayout._makeFullScreen) {
- CollectionDockingView.myLayout._maximizedStack = stack;
- CollectionDockingView.myLayout._maxstack = stack.header.controlsContainer.find('.lm_maximise');
+ else {
+ if (config == JSON.stringify(this._goldenLayout.toConfig()))
+ return;
+ try {
+ this._goldenLayout.unbind('itemDropped', this.itemDropped);
+ this._goldenLayout.unbind('tabCreated', this.tabCreated);
+ this._goldenLayout.unbind('stackCreated', this.stackCreated);
+ } catch (e) { }
+ this._goldenLayout.destroy();
+ this._goldenLayout = new GoldenLayout(JSON.parse(config));
}
- //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 () {
- //if (confirm('really close this?')) {
- stack.remove();
- //}
- });
- });
-
- var me = this;
- CollectionDockingView.myLayout.registerComponent('documentViewComponent', function (container: any, state: any) {
- // bcz: this is crufty
- // calling html() causes a div tag to be added in the DOM with id 'containingDiv'.
- // Apparently, we need to wait to allow a live html div element to actually be instantiated.
- // After a timeout, we lookup the live html div element and add our React DocumentView to it.
- var containingDiv = "component_" + me.nextId();
- container.getElement().html("<div id='" + containingDiv + "'></div>");
- setTimeout(function () {
- var htmlElement = document.getElementById(containingDiv);
- container.on('resize', (e: any) => {
- // state.doc.SetNumber(KeyStore.Width, htmlElement!.clientWidth);
- // if (htmlElement!.clientHeight > 0)
- // state.doc.SetNumber(KeyStore.Height, htmlElement!.clientHeight);
- })
- ReactDOM.render((
- <DocumentView key={state.doc.Id} Document={state.doc}
- AddDocument={me.addDocument} RemoveDocument={me.removeDocument}
- GetTransform={() => Transform.Identity}
- Scaling={1}
- ContainingCollectionView={me} DocumentView={undefined} />
- ),
- htmlElement
- );
- if (CollectionDockingView.myLayout._maxstack != null) {
- CollectionDockingView.myLayout._maxstack.click();
+ this._goldenLayout.on('itemDropped', this.itemDropped);
+ this._goldenLayout.on('tabCreated', this.tabCreated);
+ this._goldenLayout.on('stackCreated', this.stackCreated);
+ this._goldenLayout.registerComponent('DocumentFrameRenderer', DockedFrameRenderer);
+ this._goldenLayout.container = this._containerRef.current;
+ if (this._goldenLayout.config.maximisedItemId === '__glMaximised') {
+ try {
+ this._goldenLayout.config.root.getItemsById(this._goldenLayout.config.maximisedItemId)[0].toggleMaximise();
+ } catch (e) {
+ this._goldenLayout.config.maximisedItemId = null;
}
- }, 0);
- });
- CollectionDockingView.myLayout.container = this._containerRef.current;
- CollectionDockingView.myLayout.init();
+ }
+ this._goldenLayout.init();
+ }
}
+ componentDidMount: () => void = () => {
+ if (this._containerRef.current) {
+ reaction(
+ () => this.props.Document.GetText(KeyStore.Data, ""),
+ () => this.setupGoldenLayout(), { fireImmediately: true });
+ window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window
+ }
+ }
+ componentWillUnmount: () => void = () => {
+ this._goldenLayout.unbind('itemDropped', this.itemDropped);
+ this._goldenLayout.unbind('tabCreated', this.tabCreated);
+ this._goldenLayout.unbind('stackCreated', this.stackCreated);
+ this._goldenLayout.destroy();
+ this._goldenLayout = null;
+ window.removeEventListener('resize', this.onResize);
+ }
+ @action
+ onResize = (event: any) => {
+ var cur = this._containerRef.current;
- render() {
- const { CollectionFieldKey: fieldKey, DocumentForCollection: Document } = this.props;
- const value: Document[] = Document.GetData(fieldKey, ListField, []);
- // bcz: not sure why, but I need these to force the flexlayout to update when the collection size changes.
- var s = this.props.ContainingDocumentView != undefined ? this.props.ContainingDocumentView!.ScalingToScreenSpace : 1;
- var w = Document.GetData(KeyStore.Width, NumberField, Number(0)) / s;
- var h = Document.GetData(KeyStore.Height, NumberField, Number(0)) / s;
+ // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed
+ this._goldenLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height);
+ }
- var chooseLayout = () => {
- if (!CollectionDockingView.UseGoldenLayout)
- return <FlexLayout.Layout model={this.modelForFlexLayout} factory={this.flexLayoutFactory} />;
+ _flush: boolean = false;
+ @action
+ onPointerUp = (e: React.PointerEvent): void => {
+ if (this._flush) {
+ this._flush = false;
+ setTimeout(() => this.stateChanged(), 10);
}
+ }
+ @action
+ onPointerDown = (e: React.PointerEvent): void => {
+ if (e.button === 2 && this.props.active()) {
+ e.stopPropagation();
+ e.preventDefault();
+ } else {
+ var className = (e.target as any).className;
+ if (className == "lm_drag_handle" || className == "lm_close" || className == "lm_maximise" || className == "lm_minimise" || className == "lm_close_tab") {
+ this._flush = true;
+ }
+ if (e.buttons === 1 && this.props.active()) {
+ e.stopPropagation();
+ }
+ }
+ }
+ @undoBatch
+ stateChanged = () => {
+ var json = JSON.stringify(this._goldenLayout.toConfig());
+ this.props.Document.SetText(KeyStore.Data, json)
+ }
+
+ itemDropped = () => {
+ 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();
+ });
+ }
+
+ stackCreated = (stack: any) => {
+ //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(action(function () {
+ //if (confirm('really close this?')) {
+ stack.remove();
+ //}
+ }));
+ }
+
+ render() {
return (
- <div className="border" style={{
- borderStyle: "solid",
- borderWidth: `${COLLECTION_BORDER_WIDTH}px`,
- }}>
- <div className="collectiondockingview-container" id="menuContainer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} ref={this._containerRef}
- style={{
- width: CollectionDockingView.UseGoldenLayout || s > 1 ? "100%" : w - 2 * COLLECTION_BORDER_WIDTH,
- height: CollectionDockingView.UseGoldenLayout || s > 1 ? "100%" : h - 2 * COLLECTION_BORDER_WIDTH
- }} >
- {chooseLayout()}
- </div>
- </div>
+ <div className="collectiondockingview-container" id="menuContainer"
+ onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} onContextMenu={(e) => e.preventDefault()} ref={this._containerRef}
+ style={{
+ width: "100%",
+ height: "100%",
+ borderStyle: "solid",
+ borderWidth: `${COLLECTION_BORDER_WIDTH}px`,
+ }} />
);
}
+}
+
+interface DockedFrameProps {
+ documentId: FieldId,
+ //collectionDockingView: CollectionDockingView
+}
+@observer
+export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
+
+ @observable private _mainCont = React.createRef<HTMLDivElement>();
+ @observable private _panelWidth = 0;
+ @observable private _panelHeight = 0;
+ @observable private _document: Opt<Document>;
+
+ constructor(props: any) {
+ super(props);
+ Server.GetField(this.props.documentId, action((f: Opt<Field>) => this._document = f as Document));
+ }
+
+ 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 = () => {
+ let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current!);
+ return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(scale / this._contentScaling())
+ }
+
+ render() {
+ if (!this._document)
+ return (null);
+ var content =
+ <div className="collectionDockingView-content" ref={this._mainCont}>
+ <DocumentView key={this._document.Id} Document={this._document}
+ AddDocument={undefined}
+ RemoveDocument={undefined}
+ ContentScaling={this._contentScaling}
+ PanelWidth={this._nativeWidth}
+ PanelHeight={this._nativeHeight}
+ ScreenToLocalTransform={this.ScreenToLocalTransform}
+ isTopMost={true}
+ ContainingCollectionView={undefined} />
+ </div>
+
+ return <Measure onResize={action((r: any) => { this._panelWidth = r.entry.width; this._panelHeight = r.entry.height; })}>
+ {({ measureRef }) => <div ref={measureRef}> {content} </div>}
+ </Measure>
+ }
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionFreeFormView.scss b/src/client/views/collections/CollectionFreeFormView.scss
index 4cf474f77..d583a8218 100644
--- a/src/client/views/collections/CollectionFreeFormView.scss
+++ b/src/client/views/collections/CollectionFreeFormView.scss
@@ -1,4 +1,19 @@
.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;
+ height: 100%;
+ }
+
border-style: solid;
box-sizing: border-box;
position: relative;
@@ -11,5 +26,7 @@
position: absolute;
top: 0;
left: 0;
+ width:100%;
+ height: 100%
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx
index 977a03f58..12909c151 100644
--- a/src/client/views/collections/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/CollectionFreeFormView.tsx
@@ -1,83 +1,63 @@
+import { action, computed } from "mobx";
import { observer } from "mobx-react";
-import React = require("react");
-import { action, observable, computed } from "mobx";
-import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
-import { DragManager } from "../../util/DragManager";
-import "./CollectionFreeFormView.scss";
-import { Utils } from "../../../Utils";
-import { CollectionViewBase, CollectionViewProps, COLLECTION_BORDER_WIDTH } from "./CollectionViewBase";
-import { SelectionManager } from "../../util/SelectionManager";
-import { Key, KeyStore } from "../../../fields/Key";
import { Document } from "../../../fields/Document";
-import { ListField } from "../../../fields/ListField";
-import { NumberField } from "../../../fields/NumberField";
-import { Documents } from "../../documents/Documents";
import { FieldWaiting } from "../../../fields/Field";
+import { KeyStore } from "../../../fields/KeyStore";
+import { ListField } from "../../../fields/ListField";
+import { TextField } from "../../../fields/TextField";
+import { DragManager } from "../../util/DragManager";
import { Transform } from "../../util/Transform";
+import { undoBatch } from "../../util/UndoManager";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { CollectionSchemaView } from "../collections/CollectionSchemaView";
+import { CollectionTreeView } from "../collections/CollectionTreeView";
+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 "./CollectionFreeFormView.scss";
+import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
+import { CollectionViewBase } from "./CollectionViewBase";
+import React = require("react");
+const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this?
@observer
export class CollectionFreeFormView extends CollectionViewBase {
- public static LayoutString() { return CollectionViewBase.LayoutString("CollectionFreeFormView"); }
- private _containerRef = React.createRef<HTMLDivElement>();
private _canvasRef = React.createRef<HTMLDivElement>();
private _lastX: number = 0;
private _lastY: number = 0;
private _downX: number = 0;
private _downY: number = 0;
- constructor(props: CollectionViewProps) {
- super(props);
- }
-
- @computed
- get isAnnotationOverlay() { return this.props.CollectionFieldKey == KeyStore.Annotations; }
-
- @computed
- get nativeWidth() { return this.props.DocumentForCollection.GetNumber(KeyStore.NativeWidth, 0); }
- @computed
- get nativeHeight() { return this.props.DocumentForCollection.GetNumber(KeyStore.NativeHeight, 0); }
-
- @computed
- get zoomScaling() { return this.props.DocumentForCollection.GetNumber(KeyStore.Scale, 1); }
-
- @computed
- get resizeScaling() { return this.isAnnotationOverlay ? this.props.DocumentForCollection.GetNumber(KeyStore.Width, 0) / this.nativeWidth : 1; }
-
+ @computed get panX(): number { return this.props.Document.GetNumber(KeyStore.PanX, 0) }
+ @computed get panY(): number { return this.props.Document.GetNumber(KeyStore.PanY, 0) }
+ @computed get scale(): number { return this.props.Document.GetNumber(KeyStore.Scale, 1); }
+ @computed get isAnnotationOverlay() { return this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's?
+ @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) => {
- const doc = de.data["document"];
- var me = this;
- if (doc instanceof CollectionFreeFormDocumentView) {
- if (doc.props.ContainingCollectionView && doc.props.ContainingCollectionView !== this) {
- doc.props.ContainingCollectionView.removeDocument(doc.props.Document);
- this.addDocument(doc.props.Document);
- }
- const xOffset = de.data["xOffset"] as number || 0;
- const yOffset = de.data["yOffset"] as number || 0;
- const { translateX, translateY } = Utils.GetScreenTransform(this._canvasRef.current!);
- const currScale = this.resizeScaling * this.zoomScaling * this.props.ContainingDocumentView!.ScalingToScreenSpace;
- const screenX = de.x - xOffset;
- const screenY = de.y - yOffset;
- doc.x = (screenX - translateX) / currScale;
- doc.y = (screenY - translateY) / currScale;
- this.bringToFront(doc);
- }
- e.stopPropagation();
- }
-
- componentDidMount() {
- if (this._containerRef.current) {
- DragManager.MakeDropTarget(this._containerRef.current, {
- handlers: {
- drop: this.drop
- }
- });
- }
+ super.drop(e, de);
+ 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);
}
@action
onPointerDown = (e: React.PointerEvent): void => {
- if ((e.button === 2 && this.active) ||
+ if ((e.button === 2 && this.props.active()) ||
!e.defaultPrevented) {
document.removeEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointermove", this.onPointerMove);
@@ -94,22 +74,22 @@ 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) {
- if (!SelectionManager.IsSelected(this.props.ContainingDocumentView as CollectionFreeFormDocumentView)) {
- SelectionManager.SelectDoc(this.props.ContainingDocumentView as CollectionFreeFormDocumentView, false);
+ if (!this.props.isSelected()) {
+ this.props.select(false);
}
}
}
@action
onPointerMove = (e: PointerEvent): void => {
- if (!e.cancelBubble && this.active) {
+ if (!e.cancelBubble && this.props.active()) {
e.preventDefault();
e.stopPropagation();
- let currScale: number = this.props.ContainingDocumentView!.ScalingToScreenSpace;
- let x = this.props.DocumentForCollection.GetNumber(KeyStore.PanX, 0);
- let y = this.props.DocumentForCollection.GetNumber(KeyStore.PanY, 0);
+ 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 + (e.pageX - this._lastX) / currScale, y + (e.pageY - this._lastY) / currScale);
+ this.SetPan(x + dx, y + dy);
}
this._lastX = e.pageX;
this._lastY = e.pageY;
@@ -119,131 +99,149 @@ export class CollectionFreeFormView extends CollectionViewBase {
onPointerWheel = (e: React.WheelEvent): void => {
e.stopPropagation();
e.preventDefault();
- let modes = ['pixels', 'lines', 'page'];
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 { LocalX, Ss, Panxx, Xx, LocalY, Panyy, Yy, ContainerX, ContainerY } = this.props.ContainingDocumentView!.TransformToLocalPoint(e.pageX, e.pageY);
+ 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);
- var deltaScale = (1 - (e.deltaY / coefficient)) * Ss;
- var newDeltaScale = this.isAnnotationOverlay ? Math.max(1, deltaScale) : deltaScale;
+ let localTransform = this.getLocalTransform();
+ localTransform = localTransform.inverse().scaleAbout(deltaScale, x, y)
- this.props.DocumentForCollection.SetNumber(KeyStore.Scale, newDeltaScale);
- this.SetPan(ContainerX - (LocalX * newDeltaScale + Xx), ContainerY - (LocalY * newDeltaScale + Yy));
+ this.props.Document.SetNumber(KeyStore.Scale, localTransform.Scale);
+ this.SetPan(localTransform.TranslateX, localTransform.TranslateY);
+ }
}
@action
private SetPan(panX: number, panY: number) {
- const newPanX = Math.max(-(this.resizeScaling * this.zoomScaling - this.resizeScaling) * this.nativeWidth, Math.min(0, panX));
- const newPanY = Math.max(-(this.resizeScaling * this.zoomScaling - this.resizeScaling) * this.nativeHeight, Math.min(0, panY));
- this.props.DocumentForCollection.SetNumber(KeyStore.PanX, this.isAnnotationOverlay ? newPanX : panX);
- this.props.DocumentForCollection.SetNumber(KeyStore.PanY, this.isAnnotationOverlay ? newPanY : panY);
+ 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, this.isAnnotationOverlay ? newPanX : panX);
+ this.props.Document.SetNumber(KeyStore.PanY, this.isAnnotationOverlay ? newPanY : panY);
}
@action
onDrop = (e: React.DragEvent): void => {
- e.stopPropagation()
- e.preventDefault()
- let fReader = new FileReader()
- let file = e.dataTransfer.items[0].getAsFile();
- let that = this;
- const panx: number = this.props.DocumentForCollection.GetData(KeyStore.PanX, NumberField, Number(0));
- const pany: number = this.props.DocumentForCollection.GetData(KeyStore.PanY, NumberField, Number(0));
- let x = e.pageX - panx
- let y = e.pageY - pany
-
- fReader.addEventListener("load", action("drop", (event) => {
- if (fReader.result) {
- let url = "" + fReader.result;
- let doc = Documents.ImageDocument(url, {
- x: x, y: y
- })
- let docs = that.props.DocumentForCollection.GetT(KeyStore.Data, ListField);
- if (docs != FieldWaiting) {
- if (!docs) {
- docs = new ListField<Document>();
- that.props.DocumentForCollection.Set(KeyStore.Data, docs)
- }
- docs.Data.push(doc);
- }
- }
- }), false)
-
- if (file) {
- fReader.readAsDataURL(file)
- }
+ var pt = this.getTransform().transformPoint(e.pageX, e.pageY);
+ super.onDrop(e, { x: pt[0], y: pt[1] });
}
- onDragOver = (e: React.DragEvent): void => {
+ onDragOver = (): void => {
}
@action
- bringToFront(doc: CollectionFreeFormDocumentView) {
- const { CollectionFieldKey: fieldKey, DocumentForCollection: Document } = this.props;
-
- const value: Document[] = Document.GetList<Document>(fieldKey, []);
- var topmost = value.reduce((topmost, d) => Math.max(d.GetNumber(KeyStore.ZIndex, 0), topmost), -1000);
- value.map(d => {
- var zind = d.GetNumber(KeyStore.ZIndex, 0);
- if (zind != topmost - 1 - (topmost - zind) && d != doc.props.Document) {
- d.SetData(KeyStore.ZIndex, topmost - 1 - (topmost - zind), NumberField);
+ 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) {
+ return 1;
+ }
+ if (doc2 === doc) {
+ return -1;
}
- })
+ return doc1.GetNumber(KeyStore.ZIndex, 0) - doc2.GetNumber(KeyStore.ZIndex, 0);
+ }).map((doc, index) => {
+ doc.SetNumber(KeyStore.ZIndex, index + 1)
+ });
+ }
+
- if (doc.props.Document.GetNumber(KeyStore.ZIndex, 0) != 0) {
- doc.props.Document.SetData(KeyStore.ZIndex, 0, NumberField);
+ @computed get backgroundLayout(): string | undefined {
+ let field = this.props.Document.GetT(KeyStore.BackgroundLayout, TextField);
+ if (field && field !== "<Waiting>") {
+ return field.Data;
+ }
+ }
+ @computed get overlayLayout(): string | undefined {
+ let field = this.props.Document.GetT(KeyStore.OverlayLayout, TextField);
+ if (field && field !== "<Waiting>") {
+ return field.Data;
}
}
-
@computed
- get translate(): [number, number] {
- const x = this.props.DocumentForCollection.GetNumber(KeyStore.PanX, 0);
- const y = this.props.DocumentForCollection.GetNumber(KeyStore.PanY, 0);
- return [x, y];
+ get views() {
+ const { fieldKey, Document } = this.props;
+ const lvalue = Document.GetT<ListField<Document>>(fieldKey, ListField);
+ if (lvalue && lvalue != FieldWaiting) {
+ return lvalue.Data.map(doc => {
+ return (<CollectionFreeFormDocumentView key={doc.Id} Document={doc}
+ AddDocument={this.props.addDocument}
+ RemoveDocument={this.props.removeDocument}
+ ScreenToLocalTransform={this.getTransform}
+ isTopMost={false}
+ ContentScaling={this.noScaling}
+ PanelWidth={doc.Width}
+ PanelHeight={doc.Height}
+ ContainingCollectionView={this.props.CollectionView} />);
+ })
+ }
+ return null;
}
@computed
- get scale(): number {
- return this.props.DocumentForCollection.GetNumber(KeyStore.Scale, 1);
+ get backgroundView() {
+ return !this.backgroundLayout ? (null) :
+ (<JsxParser
+ components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebView }}
+ bindings={this.props.bindings}
+ jsx={this.backgroundLayout}
+ showWarnings={true}
+ onError={(test: any) => console.log(test)}
+ />);
}
-
- getTransform = (): Transform => {
- const [x, y] = this.translate;
- return this.props.GetTransform().scaled(this.scale).translate(x, y);
+ @computed
+ get overlayView() {
+ return !this.overlayLayout ? (null) :
+ (<JsxParser
+ components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView }}
+ bindings={this.props.bindings}
+ jsx={this.overlayLayout}
+ showWarnings={true}
+ onError={(test: any) => console.log(test)}
+ />);
}
- render() {
- const Document: Document = this.props.DocumentForCollection;
- const value: Document[] = Document.GetList<Document>(this.props.CollectionFieldKey, []);
- const panx: number = Document.GetNumber(KeyStore.PanX, 0);
- const pany: number = Document.GetNumber(KeyStore.PanY, 0);
+ getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH - this.centeringShiftX, -COLLECTION_BORDER_WIDTH - this.centeringShiftY).transform(this.getLocalTransform())
+ getLocalTransform = (): Transform => Transform.Identity.translate(-this.panX, -this.panY).scale(1 / this.scale);
+ noScaling = () => 1;
+ render() {
+ const panx: number = this.props.Document.GetNumber(KeyStore.PanX, 0) + this.centeringShiftX;
+ const pany: number = this.props.Document.GetNumber(KeyStore.PanY, 0) + this.centeringShiftY;
return (
<div className="collectionfreeformview-container"
onPointerDown={this.onPointerDown}
onWheel={this.onPointerWheel}
onContextMenu={(e) => e.preventDefault()}
- onDrop={this.onDrop}
+ onDrop={this.onDrop.bind(this)}
onDragOver={this.onDragOver}
- style={{
- borderWidth: `${COLLECTION_BORDER_WIDTH}px`,
- }}
- ref={this._containerRef}>
+ style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px`, }}
+ ref={this.createDropTarget}>
<div className="collectionfreeformview"
- style={{ width: "100%", transform: `translate(${panx}px, ${pany}px) scale(${this.zoomScaling}, ${this.zoomScaling})`, transformOrigin: `left, top` }}
+ style={{ transformOrigin: "left top", transform: ` translate(${panx}px, ${pany}px) scale(${this.zoomScaling}, ${this.zoomScaling})` }}
ref={this._canvasRef}>
-
- {this.props.BackgroundView}
- {value.map(doc => {
- return (<CollectionFreeFormDocumentView key={doc.Id} Document={doc}
- AddDocument={this.addDocument}
- RemoveDocument={this.removeDocument}
- GetTransform={this.getTransform}
- Scaling={this.resizeScaling}
- ContainingCollectionView={this} DocumentView={undefined} />);
- })}
+ {this.backgroundView}
+ {this.views}
</div>
+ {this.overlayView}
</div>
);
}
diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss
index 707b44db6..0bd5a2ed3 100644
--- a/src/client/views/collections/CollectionSchemaView.scss
+++ b/src/client/views/collections/CollectionSchemaView.scss
@@ -1,3 +1,83 @@
+
+.collectionSchemaView-container {
+ border-style: solid;
+ box-sizing: border-box;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ ::-webkit-scrollbar {
+ -webkit-appearance: none;
+ width: 10px;
+ }
+ ::-webkit-scrollbar-thumb {
+ border-radius: 5px;
+ background-color: rgba(0,0,0,.5);
+ }
+
+ .ReactTable {
+ position: absolute;
+ // display: inline-block;
+ // overflow: auto;
+ width: 100%;
+ height: 100%;
+ background: white;
+ box-sizing: border-box;
+ .rt-table {
+ overflow-y: auto;
+ overflow-x: auto;
+ height: 100%;
+
+ display: -webkit-inline-box;
+ direction: ltr;
+ // direction:rtl;
+ // display:block;
+ }
+ .rt-tbody {
+ //direction: ltr;
+ direction: rtl;
+ }
+ .rt-tr-group {
+ direction: ltr;
+ max-height: 44px;
+ }
+ .rt-td {
+ border-width: 1;
+ border-right-color: #aaa;
+ .imageBox-cont {
+ position:relative;
+ max-height:100%;
+ }
+ .imageBox-cont img {
+ object-fit: contain;
+ max-width: 100%;
+ height: 100%
+ }
+ }
+ .rt-tr-group {
+ border-width: 1;
+ border-bottom-color: #aaa
+ }
+ }
+ .ReactTable .rt-thead.-header {
+ background:grey;
+ }
+ .ReactTable .rt-th, .ReactTable .rt-td {
+ max-height: 44;
+ padding: 3px 7px;
+ }
+ .ReactTable .rt-tbody .rt-tr-group:last-child {
+ border-bottom: grey;
+ border-bottom-style: solid;
+ border-bottom-width: 1;
+ }
+ .documentView-node:first-child {
+ background: grey;
+ .imageBox-cont img {
+ object-fit: contain;
+ }
+ }
+}
+
.Resizer {
box-sizing: border-box;
background: #000;
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index d924cb163..22b5989a0 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -1,63 +1,104 @@
import React = require("react")
-import ReactTable, { ReactTableDefaults, CellInfo, ComponentPropsGetterRC, ComponentPropsGetterR } from "react-table";
+import { action, observable, trace } from "mobx";
import { observer } from "mobx-react";
-import { FieldView, FieldViewProps } from "../nodes/FieldView";
-import "react-table/react-table.css"
-import { observable, action, computed } from "mobx";
-import SplitPane from "react-split-pane"
-import "./CollectionSchemaView.scss"
-import { ScrollBox } from "../../util/ScrollBox";
-import { CollectionViewBase } from "./CollectionViewBase";
-import { DocumentView } from "../nodes/DocumentView";
-import { EditableView } from "../EditableView";
-import { CompileScript, ToField } from "../../util/Scripting";
-import { KeyStore as KS, Key } from "../../../fields/Key";
+import Measure from "react-measure";
+import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table";
+import "react-table/react-table.css";
import { Document } from "../../../fields/Document";
-import { Field } from "../../../fields/Field";
+import { Field, FieldWaiting } from "../../../fields/Field";
+import { KeyStore } from "../../../fields/KeyStore";
+import { CompileScript, ToField } from "../../util/Scripting";
import { Transform } from "../../util/Transform";
import { ContextMenu } from "../ContextMenu";
+import { EditableView } from "../EditableView";
+import { DocumentView } from "../nodes/DocumentView";
+import { FieldView, FieldViewProps } from "../nodes/FieldView";
+import "./CollectionSchemaView.scss";
+import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
+import { CollectionViewBase } from "./CollectionViewBase";
+import { DragManager } from "../../util/DragManager";
+import { CollectionDockingView } from "./CollectionDockingView";
+
+// 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 {
- public static LayoutString() { return CollectionViewBase.LayoutString("CollectionSchemaView"); }
+ private _mainCont = React.createRef<HTMLDivElement>();
+ private DIVIDER_WIDTH = 5;
+
+ @observable _contentScaling = 1; // used to transfer the dimensions of the content pane in the DOM to the ContentScaling prop of the DocumentView
+ @observable _dividerX = 0;
+ @observable _panelWidth = 0;
+ @observable _panelHeight = 0;
+ @observable _selectedIndex = 0;
+ @observable _splitPercentage: number = 50;
+
+
- @observable
- selectedIndex = 0;
renderCell = (rowProps: CellInfo) => {
let props: FieldViewProps = {
doc: rowProps.value[0],
fieldKey: rowProps.value[1],
- DocumentViewForField: undefined
+ isSelected: () => false,
+ select: () => { },
+ isTopMost: false,
+ bindings: {}
}
let contents = (
<FieldView {...props} />
)
- return (
- <EditableView contents={contents} 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;
+ let reference = React.createRef<HTMLDivElement>();
+ let onRowMove = action((e: PointerEvent): void => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ document.removeEventListener("pointermove", onRowMove);
+ document.removeEventListener('pointerup', onRowUp);
+ DragManager.StartDrag(reference.current!, { document: props.doc });
+ });
+ let onRowUp = action((e: PointerEvent): void => {
+ document.removeEventListener("pointermove", onRowMove);
+ document.removeEventListener('pointerup', onRowUp);
+ });
+ let onRowDown = (e: React.PointerEvent) => {
+ if (this.props.isSelected() || this.props.isTopMost) {
+ if (e.shiftKey) {
+ CollectionDockingView.Instance.StartOtherDrag(reference.current!, props.doc);
+ e.stopPropagation();
} else {
- let dataField = ToField(field);
- if (dataField) {
- props.doc.Set(props.fieldKey, dataField);
- return true;
- }
+ document.addEventListener("pointermove", onRowMove);
+ document.addEventListener('pointerup', onRowUp);
}
- return false;
- }}></EditableView>
+ }
+ }
+ return (
+ <div onPointerDown={onRowDown} 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>
)
}
@@ -68,33 +109,74 @@ export class CollectionSchemaView extends CollectionViewBase {
}
return {
onClick: action((e: React.MouseEvent, handleOriginal: Function) => {
- that.selectedIndex = rowInfo.index;
- const doc: Document = rowInfo.original;
- console.log("Row clicked: ", doc.Title)
+ that._selectedIndex = rowInfo.index;
+ this._splitPercentage += 0.05; // bcz - ugh - needed to force Measure to do its thing and call onResize
if (handleOriginal) {
handleOriginal()
}
}),
style: {
- background: rowInfo.index == this.selectedIndex ? "#00afec" : "white",
- color: rowInfo.index == this.selectedIndex ? "white" : "black"
+ background: rowInfo.index == this._selectedIndex ? "#00afec" : "white",
+ color: rowInfo.index == this._selectedIndex ? "white" : "black"
}
};
}
- onPointerDown = (e: React.PointerEvent) => {
- let target = e.target as HTMLElement;
- if (target.tagName == "SPAN" && target.className.includes("Resizer")) {
- e.stopPropagation();
+ _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) {
// e.stopPropagation();
// e.preventDefault();
// } else
{
- if (e.buttons === 1 && this.active) {
- e.stopPropagation();
+ if (e.buttons === 1) {
+ if (this.props.isSelected()) {
+ e.stopPropagation();
+ }
}
}
}
@@ -106,55 +188,81 @@ export class CollectionSchemaView extends CollectionViewBase {
specificContextMenu = (e: React.MouseEvent): void => {
ContextMenu.Instance.addItem({ description: "Collection Capability", event: this.collectionCapability });
}
+ @action
+ setScaling = (r: any) => {
+ const children = this.props.Document.GetList<Document>(this.props.fieldKey, []);
+ const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined;
+ this._panelWidth = r.entry.width;
+ this._panelHeight = r.entry.height ? r.entry.height : this._panelHeight;
+ this._contentScaling = r.entry.width / selected!.GetNumber(KeyStore.NativeWidth, r.entry.width);
+ }
+
+ getContentScaling = (): number => this._contentScaling;
+ getPanelWidth = (): number => this._panelWidth;
+ getPanelHeight = (): number => this._panelHeight;
+ getTransform = (): Transform => {
+ return this.props.ScreenToLocalTransform().translate(- COLLECTION_BORDER_WIDTH - this.DIVIDER_WIDTH - this._dividerX, - COLLECTION_BORDER_WIDTH).scale(1 / this._contentScaling);
+ }
render() {
- const { DocumentForCollection: Document, CollectionFieldKey: fieldKey } = this.props;
- const children = Document.GetList<Document>(fieldKey, []);
- const columns = Document.GetList(KS.ColumnsKey,
- [KS.Title, KS.Data, KS.Author])
- let content;
- if (this.selectedIndex != -1) {
- content = (
- <DocumentView Document={children[this.selectedIndex]}
- AddDocument={this.addDocument} RemoveDocument={this.removeDocument}
- GetTransform={() => Transform.Identity}//TODO This should probably be an actual transform
- Scaling={1}
- DocumentView={undefined} ContainingCollectionView={this} />
- )
- } else {
- content = <div />
- }
+ const columns = this.props.Document.GetList(KeyStore.ColumnsKey, [KeyStore.Title, KeyStore.Data, KeyStore.Author])
+ const children = this.props.Document.GetList<Document>(this.props.fieldKey, []);
+ const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined;
+ let content = this._selectedIndex == -1 || !selected ? (null) : (
+ <Measure onResize={this.setScaling}>
+ {({ measureRef }) =>
+ <div className="collectionSchemaView-content" ref={measureRef}>
+ <DocumentView Document={selected}
+ AddDocument={this.props.addDocument} RemoveDocument={this.props.removeDocument}
+ isTopMost={false}
+ ScreenToLocalTransform={this.getTransform}
+ ContentScaling={this.getContentScaling}
+ PanelWidth={this.getPanelWidth}
+ PanelHeight={this.getPanelHeight}
+ ContainingCollectionView={this.props.CollectionView} />
+ </div>
+ }
+ </Measure>
+ )
+ let handle = !this.props.active() ? (null) : (
+ <div style={{ position: "absolute", height: "37px", width: "20px", zIndex: 20, right: 0, top: 0, background: "Black" }} onPointerDown={this.onExpanderDown} />);
return (
- <div onPointerDown={this.onPointerDown} onContextMenu={this.specificContextMenu}>
- <SplitPane split={"vertical"} defaultSize="60%">
- <ScrollBox>
- <ReactTable
- data={children}
- pageSize={children.length}
- page={0}
- showPagination={false}
- style={{
- display: "inline-block",
- width: "100%"
- }}
- columns={columns.map(col => {
- return (
- {
- Header: col.Name,
- accessor: (doc: Document) => [doc, col],
- id: col.Id
- })
- })}
- column={{
- ...ReactTableDefaults.column,
- Cell: this.renderCell
- }}
- getTrProps={this.getTrProps}
- />
- </ScrollBox>
+ <div onPointerDown={this.onPointerDown} onContextMenu={this.specificContextMenu} 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,
+
+ }}
+ 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}
- </SplitPane>
- </div>
+ </div>
+ {handle}
+ </div >
)
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
new file mode 100644
index 000000000..675fc6c53
--- /dev/null
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -0,0 +1,28 @@
+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;
+} \ 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..52e853bf7
--- /dev/null
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -0,0 +1,104 @@
+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";
+
+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 document The document to render.
+ */
+ renderChild(document: Document) {
+ var children = document.GetT<ListField<Document>>(KeyStore.Data, ListField);
+ let title = document.GetT<TextField>(KeyStore.Title, TextField);
+
+ // if the title hasn't loaded, immediately return the div
+ if (!title || title === "<Waiting>") {
+ return <div key={document.Id}></div>;
+ }
+
+ // otherwise, check if it's a collection.
+ else if (children && children !== "<Waiting>") {
+ // if it's not collapsed, then render the full TreeView.
+ if (!this.collapsed) {
+ return (
+ <li className="uncollapsed" key={document.Id} onClick={action(() => this.collapsed = true)} >
+ {title.Data}
+ <ul key={document.Id}>
+ <TreeView
+ document={document}
+ />
+ </ul>
+ </li>
+ );
+ } else {
+ return <li className="collapsed" key={document.Id} onClick={action(() => this.collapsed = false)}>{title.Data}</li>
+ }
+ }
+
+ // finally, if it's a normal document, then render it as such.
+ else {
+ return <li key={document.Id}>{title.Data}</li>;
+ }
+ }
+
+ render() {
+ var children = this.props.document.GetT<ListField<Document>>(KeyStore.Data, ListField);
+
+ if (children && children !== "<Waiting>") {
+ return (<div>
+ {children.Data.map(value => this.renderChild(value))}
+ </div>)
+ // let results: JSX.Element[] = [];
+
+ // // append a list item for each child in the collection
+ // children.Data.forEach((value) => {
+ // results.push(this.renderChild(value));
+ // })
+
+ // return results;
+ } else {
+ return <div></div>;
+ }
+ }
+}
+
+
+@observer
+export class CollectionTreeView extends CollectionViewBase {
+
+ render() {
+ let titleStr = "";
+ let title = this.props.Document.GetT<TextField>(KeyStore.Title, TextField);
+ if (title && title !== "<Waiting>") {
+ titleStr = title.Data;
+ }
+ return (
+ <div>
+ <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
new file mode 100644
index 000000000..03e1f1fa4
--- /dev/null
+++ b/src/client/views/collections/CollectionView.tsx
@@ -0,0 +1,113 @@
+import { action, computed } from "mobx";
+import { observer } from "mobx-react";
+import { Document } from "../../../fields/Document";
+import { ListField } from "../../../fields/ListField";
+import { SelectionManager } from "../../util/SelectionManager";
+import { ContextMenu } from "../ContextMenu";
+import React = require("react");
+import { KeyStore } from "../../../fields/KeyStore";
+import { NumberField } from "../../../fields/NumberField";
+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;
+
+@observer
+export class CollectionView extends React.Component<CollectionViewProps> {
+
+ public static LayoutString(fieldKey: string = "DataKey") {
+ return `<CollectionView Document={Document}
+ ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} panelWidth={PanelWidth} panelHeight={PanelHeight} isSelected={isSelected} select={select} bindings={bindings}
+ isTopMost={isTopMost} BackgroundView={BackgroundView} />`;
+ }
+ public active = () => {
+ var isSelected = this.props.isSelected();
+ var childSelected = SelectionManager.SelectedDocuments().some(view => view.props.ContainingCollectionView == this);
+ var topMost = this.props.isTopMost;
+ return isSelected || childSelected || topMost;
+ }
+ @action
+ addDocument = (doc: Document): void => {
+ 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 = -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)
+
+ SelectionManager.DeselectAll()
+ ContextMenu.Instance.clearItems()
+ return true;
+ }
+ return false
+ }
+
+ get collectionViewType(): CollectionViewType {
+ let Document = this.props.Document;
+ let viewField = Document.GetT(KeyStore.ViewType, NumberField);
+ if (viewField === "<Waiting>") {
+ return CollectionViewType.Invalid;
+ } else if (viewField) {
+ return viewField.Data;
+ } else {
+ return CollectionViewType.Freeform;
+ }
+ }
+
+ set collectionViewType(type: CollectionViewType) {
+ let Document = this.props.Document;
+ 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} />)
+ case CollectionViewType.Schema:
+ return (<CollectionSchemaView {...this.props}
+ addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active}
+ CollectionView={this} />)
+ case CollectionViewType.Docking:
+ 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>
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
index 7ee010ec6..217536e2b 100644
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -1,72 +1,120 @@
import { action, computed } from "mobx";
-import { observer } from "mobx-react";
import { Document } from "../../../fields/Document";
-import { Opt } from "../../../fields/Field";
-import { Key, KeyStore } from "../../../fields/Key";
import { ListField } from "../../../fields/ListField";
-import { SelectionManager } from "../../util/SelectionManager";
-import { ContextMenu } from "../ContextMenu";
import React = require("react");
+import { KeyStore } from "../../../fields/KeyStore";
+import { Opt, FieldWaiting } from "../../../fields/Field";
+import { undoBatch } from "../../util/UndoManager";
+import { DragManager } from "../../util/DragManager";
import { DocumentView } from "../nodes/DocumentView";
-import { CollectionDockingView } from "./CollectionDockingView";
-import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
+import { Documents, DocumentOptions } from "../../documents/Documents";
+import { Key } from "../../../fields/Key";
import { Transform } from "../../util/Transform";
export interface CollectionViewProps {
- CollectionFieldKey: Key;
- DocumentForCollection: Document;
- ContainingDocumentView: Opt<DocumentView>;
- GetTransform: () => Transform;
- BackgroundView: Opt<DocumentView>;
+ fieldKey: Key;
+ Document: Document;
+ ScreenToLocalTransform: () => Transform;
+ isSelected: () => boolean;
+ isTopMost: boolean;
+ select: (ctrlPressed: boolean) => void;
+ bindings: any;
+ panelWidth: () => number;
+ panelHeight: () => number;
+}
+export interface SubCollectionViewProps extends CollectionViewProps {
+ active: () => boolean;
+ addDocument: (doc: Document) => void;
+ removeDocument: (doc: Document) => boolean;
+ CollectionView: any;
}
-export const COLLECTION_BORDER_WIDTH = 2;
-
-@observer
-export class CollectionViewBase extends React.Component<CollectionViewProps> {
-
- public static LayoutString(collectionType: string) {
- return `<${collectionType} onContextMenu={this.specificContextMenu} DocumentForCollection={Document} CollectionFieldKey={DataKey} ContainingDocumentView={DocumentView}/>`;
- }
-
- //REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE
- collectionCapability = (e: React.MouseEvent): void => {
- }
-
- specificContextMenu = (e: React.MouseEvent): void => {
- ContextMenu.Instance.addItem({ description: "Collection Capability", event: this.collectionCapability });
+export class CollectionViewBase extends React.Component<SubCollectionViewProps> {
+ private dropDisposer?: DragManager.DragDropDisposer;
+ protected createDropTarget = (ele: HTMLDivElement) => {
+ if (this.dropDisposer) {
+ this.dropDisposer();
+ }
+ if (ele) {
+ this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+ }
}
- @computed
- public get active(): boolean {
- var isSelected = (this.props.ContainingDocumentView instanceof CollectionFreeFormDocumentView && SelectionManager.IsSelected(this.props.ContainingDocumentView));
- var childSelected = SelectionManager.SelectedDocuments().some(view => view.props.ContainingCollectionView == this);
- var topMost = this.props.ContainingDocumentView != undefined && (
- this.props.ContainingDocumentView.props.ContainingCollectionView == undefined ||
- this.props.ContainingDocumentView.props.ContainingCollectionView instanceof CollectionDockingView);
- return isSelected || childSelected || topMost;
- }
+ @undoBatch
@action
- addDocument = (doc: Document): void => {
- //TODO This won't create the field if it doesn't already exist
- const value = this.props.DocumentForCollection.GetData(this.props.CollectionFieldKey, ListField, new Array<Document>())
- value.push(doc);
+ protected drop(e: Event, de: DragManager.DropEvent) {
+ 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(docView.props.Document);
+ } else if (doc) {
+ this.props.removeDocument(doc);
+ this.props.addDocument(doc);
+ }
+ e.stopPropagation();
}
@action
- removeDocument = (doc: Document): boolean => {
- //TODO This won't create the field if it doesn't already exist
- const value = this.props.DocumentForCollection.GetData(this.props.CollectionFieldKey, ListField, new Array<Document>())
- let index = value.indexOf(doc);
- if (index !== -1) {
- value.splice(index, 1)
+ protected onDrop(e: React.DragEvent, options: DocumentOptions): void {
+ e.stopPropagation()
+ e.preventDefault()
+ let that = this;
- SelectionManager.DeselectAll()
- ContextMenu.Instance.clearItems()
- return true;
+ let html = e.dataTransfer.getData("text/html");
+ let text = e.dataTransfer.getData("text/plain");
+ if (html && html.indexOf("<img") != 0) {
+ let htmlDoc = Documents.HtmlDocument(html, { ...options });
+ htmlDoc.SetText(KeyStore.DocumentText, text);
+ this.props.addDocument(htmlDoc);
+ return;
}
- return false
- }
-} \ No newline at end of file
+ for (let i = 0; i < e.dataTransfer.items.length; i++) {
+ let item = e.dataTransfer.items[i];
+ 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, width: 300, })
+
+ let docs = that.props.Document.GetT(KeyStore.Data, ListField);
+ if (docs != FieldWaiting) {
+ if (!docs) {
+ docs = new ListField<Document>();
+ that.props.Document.Set(KeyStore.Data, docs)
+ }
+ docs.Data.push(img);
+ }
+ })()
+
+ })
+ }
+ if (item.kind == "file" && item.type.indexOf("image")) {
+ let fReader = new FileReader()
+ let file = item.getAsFile();
+
+ fReader.addEventListener("load", action("drop", () => {
+ if (fReader.result) {
+ let url = "" + fReader.result;
+ let doc = Documents.ImageDocument(url, options)
+ let docs = that.props.Document.GetT(KeyStore.Data, ListField);
+ if (docs != FieldWaiting) {
+ if (!docs) {
+ docs = new ListField<Document>();
+ that.props.Document.Set(KeyStore.Data, docs)
+ }
+ docs.Data.push(doc);
+ }
+ }
+ }), false)
+
+ if (file) {
+ fReader.readAsDataURL(file)
+ }
+ }
+ }
+ }
+}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index f37232c08..50dc5a619 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,24 +1,16 @@
-import { action, computed } from "mobx";
+import { computed, trace } from "mobx";
import { observer } from "mobx-react";
-import { Key, KeyStore } from "../../../fields/Key";
+import { KeyStore } from "../../../fields/KeyStore";
import { NumberField } from "../../../fields/NumberField";
-import { DragManager } from "../../util/DragManager";
-import { SelectionManager } from "../../util/SelectionManager";
-import { CollectionDockingView } from "../collections/CollectionDockingView";
-import { CollectionFreeFormView } from "../collections/CollectionFreeFormView";
-import { ContextMenu } from "../ContextMenu";
-import "./NodeView.scss";
-import React = require("react");
-import { DocumentView, DocumentViewProps } from "./DocumentView";
import { Transform } from "../../util/Transform";
+import { DocumentView, DocumentViewProps } from "./DocumentView";
+import "./DocumentView.scss";
+import React = require("react");
@observer
-export class CollectionFreeFormDocumentView extends DocumentView {
- private _contextMenuCanOpen = false;
- private _downX: number = 0;
- private _downY: number = 0;
- // private _mainCont = React.createRef<HTMLDivElement>();
+export class CollectionFreeFormDocumentView extends React.Component<DocumentViewProps> {
+ private _mainCont = React.createRef<HTMLDivElement>();
constructor(props: DocumentViewProps) {
super(props);
@@ -31,212 +23,63 @@ export class CollectionFreeFormDocumentView extends DocumentView {
}
@computed
- get x(): number {
- return this.props.Document.GetData(KeyStore.X, NumberField, Number(0));
- }
-
- @computed
- get y(): number {
- return this.props.Document.GetData(KeyStore.Y, NumberField, Number(0));
- }
-
- set x(x: number) {
- this.props.Document.SetData(KeyStore.X, x, NumberField)
- }
-
- set y(y: number) {
- this.props.Document.SetData(KeyStore.Y, y, NumberField)
- }
-
- @computed
get transform(): string {
- return `scale(${this.props.Scaling}, ${this.props.Scaling}) translate(${this.x}px, ${this.y}px)`;
- }
-
- @computed
- get width(): number {
- return this.props.Document.GetNumber(KeyStore.Width, 0);
+ 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 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.GetData(KeyStore.ZIndex, NumberField, Number(0));
- }
-
set zIndex(h: number) {
this.props.Document.SetData(KeyStore.ZIndex, h, NumberField)
}
- @action
- dragComplete = (e: DragManager.DragCompleteEvent) => {
+ contentScaling = () => {
+ return this.nativeWidth > 0 ? this.width / this.nativeWidth : 1;
}
- @computed
- get active(): boolean {
- return SelectionManager.IsSelected(this) || this.props.ContainingCollectionView === undefined ||
- this.props.ContainingCollectionView.active;
+ getTransform = (): Transform => {
+ return this.props.ScreenToLocalTransform().
+ translate(-this.props.Document.GetNumber(KeyStore.X, 0), -this.props.Document.GetNumber(KeyStore.Y, 0)).scale(1 / this.contentScaling());
}
@computed
- get topMost(): boolean {
- return this.props.ContainingCollectionView == undefined || this.props.ContainingCollectionView instanceof CollectionDockingView;
- }
-
- onPointerDown = (e: React.PointerEvent): void => {
- this._downX = e.clientX;
- this._downY = e.clientY;
- var me = this;
- if (e.shiftKey && e.buttons === 1) {
- CollectionDockingView.StartOtherDrag(this._mainCont.current!, this.props.Document);
- e.stopPropagation();
- return;
- }
- this._contextMenuCanOpen = e.button == 2;
- if (this.active && !e.isDefaultPrevented()) {
- e.stopPropagation();
- if (e.buttons === 2) {
- e.preventDefault();
- }
- document.removeEventListener("pointermove", this.onPointerMove)
- document.addEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp)
- document.addEventListener("pointerup", this.onPointerUp);
- }
- }
-
- onPointerMove = (e: PointerEvent): void => {
- if (e.cancelBubble) {
- this._contextMenuCanOpen = false;
- return;
- }
- if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
- this._contextMenuCanOpen = false;
- if (this._mainCont.current != null && !this.topMost) {
- this._contextMenuCanOpen = false;
- const rect = this.screenRect;
- let dragData: { [id: string]: any } = {};
- dragData["document"] = this;
- dragData["xOffset"] = e.x - rect.left;
- dragData["yOffset"] = e.y - rect.top;
- DragManager.StartDrag(this._mainCont.current, dragData, {
- handlers: {
- dragComplete: this.dragComplete,
- },
- hideSource: true
- })
- }
- }
- e.stopPropagation();
- e.preventDefault();
- }
-
- onPointerUp = (e: PointerEvent): void => {
- document.removeEventListener("pointermove", this.onPointerMove)
- document.removeEventListener("pointerup", this.onPointerUp)
- e.stopPropagation();
- if (Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientY - this._downY) < 4) {
- SelectionManager.SelectDoc(this, e.ctrlKey);
- }
- }
-
- openRight = (e: React.MouseEvent): void => {
- CollectionDockingView.AddRightSplit(this.props.Document);
- }
-
- deleteClicked = (e: React.MouseEvent): void => {
- if (this.props.ContainingCollectionView instanceof CollectionFreeFormView) {
- this.props.ContainingCollectionView.removeDocument(this.props.Document)
- }
- }
- @action
- fullScreenClicked = (e: React.MouseEvent): void => {
- CollectionDockingView.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)
- }
- @action
- closeFullScreenClicked = (e: React.MouseEvent): void => {
- CollectionDockingView.CloseFullScreen();
- ContextMenu.Instance.clearItems();
- ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked })
- ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15)
- }
-
- @action
- onContextMenu = (e: React.MouseEvent): void => {
- if (!SelectionManager.IsSelected(this)) {
- return;
- }
- e.preventDefault()
-
- if (!this._contextMenuCanOpen) {
- return;
- }
-
- if (this.topMost) {
- ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked })
- ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15)
- }
- else {
- // DocumentViews should stop propogation of this event
- e.stopPropagation();
-
- ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked })
- ContextMenu.Instance.addItem({ description: "Open Right", event: this.openRight })
- ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked })
- ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15)
- SelectionManager.SelectDoc(this, e.ctrlKey);
- }
- }
-
- getTransform = (): Transform => {
- return this.props.GetTransform().translated(this.x, this.y);
+ get docView() {
+ return <DocumentView {...this.props}
+ ContentScaling={this.contentScaling}
+ ScreenToLocalTransform={this.getTransform}
+ />
}
render() {
- var freestyling = this.props.ContainingCollectionView instanceof CollectionFreeFormView;
return (
- <div className="node" ref={this._mainCont} style={{
+ <div className="collectionFreeFormDocumentView-container" ref={this._mainCont} style={{
transformOrigin: "left top",
- transform: freestyling ? this.transform : "",
- width: freestyling ? this.width : "100%",
- height: freestyling ? this.height : "100%",
- position: freestyling ? "absolute" : "relative",
- zIndex: freestyling ? this.zIndex : 0,
- }}
- onContextMenu={this.onContextMenu}
- onPointerDown={this.onPointerDown}>
-
- <DocumentView {...this.props} GetTransform={this.getTransform} DocumentView={this} />
+ transform: this.transform,
+ width: this.width,
+ height: this.height,
+ position: "absolute",
+ zIndex: this.zIndex,
+ backgroundColor: "transparent"
+ }} >
+ {this.docView}
</div>
);
}
diff --git a/src/client/views/nodes/NodeView.scss b/src/client/views/nodes/DocumentView.scss
index dac1c0a8e..8e2ebd690 100644
--- a/src/client/views/nodes/NodeView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -1,4 +1,4 @@
-.node {
+.documentView-node {
position: absolute;
background: #cdcdcd;
overflow: hidden;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 691ac6311..87f2e205b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,172 +1,247 @@
import { action, computed } from "mobx";
import { observer } from "mobx-react";
import { Document } from "../../../fields/Document";
-import { Opt, FieldWaiting } from "../../../fields/Field";
-import { Key, KeyStore } from "../../../fields/Key";
+import { Field, FieldWaiting, Opt } from "../../../fields/Field";
+import { Key } from "../../../fields/Key";
+import { KeyStore } from "../../../fields/KeyStore";
import { ListField } from "../../../fields/ListField";
-import { NumberField } from "../../../fields/NumberField";
-import { TextField } from "../../../fields/TextField";
-import { Utils } from "../../../Utils";
+import { DragManager } from "../../util/DragManager";
+import { SelectionManager } from "../../util/SelectionManager";
+import { Transform } from "../../util/Transform";
import { CollectionDockingView } from "../collections/CollectionDockingView";
import { CollectionFreeFormView } from "../collections/CollectionFreeFormView";
import { CollectionSchemaView } from "../collections/CollectionSchemaView";
-import { CollectionViewBase, COLLECTION_BORDER_WIDTH } from "../collections/CollectionViewBase";
+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 "./NodeView.scss";
+import "./DocumentView.scss";
import React = require("react");
-import { Transform } from "../../util/Transform";
const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this?
export interface DocumentViewProps {
- DocumentView: Opt<DocumentView> // needed only to set ContainingDocumentView on CollectionViewProps when invoked from JsxParser -- is there a better way?
- ContainingCollectionView: Opt<CollectionViewBase>;
+ ContainingCollectionView: Opt<CollectionView>;
Document: Document;
AddDocument?: (doc: Document) => void;
RemoveDocument?: (doc: Document) => boolean;
- GetTransform: () => Transform;
- Scaling: number;
+ ScreenToLocalTransform: () => Transform;
+ isTopMost: boolean;
+ //tfs: This shouldn't be necessary I don't think
+ ContentScaling: () => number;
+ PanelWidth: () => number;
+ PanelHeight: () => number;
+}
+export interface JsxArgs extends DocumentViewProps {
+ Keys: { [name: string]: Key }
+ Fields: { [name: string]: Field }
}
-@observer
-export class DocumentView extends React.Component<DocumentViewProps> {
-
- protected _mainCont = React.createRef<any>();
- get MainContent() {
- return this._mainCont;
+/*
+This function is pretty much a hack that lets us fill out the fields in JsxArgs with something that
+jsx-to-string can recover the jsx from
+Example usage of this function:
+ public static LayoutString() {
+ let args = FakeJsxArgs(["Data"]);
+ return jsxToString(
+ <CollectionFreeFormView
+ doc={args.Document}
+ fieldKey={args.Keys.Data}
+ DocumentViewForField={args.DocumentView} />,
+ { useFunctionCode: true, functionNameOnly: true }
+ )
}
- @computed
- get layout(): string {
- return this.props.Document.GetData(KeyStore.Layout, TextField, String("<p>Error loading layout data</p>"));
+*/
+export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs {
+ let Keys: { [name: string]: any } = {}
+ let Fields: { [name: string]: any } = {}
+ for (const key of keys) {
+ let fn = () => { }
+ Object.defineProperty(fn, "name", { value: key + "Key" })
+ Keys[key] = fn;
}
-
- @computed
- get backgroundLayout(): string {
- return this.props.Document.GetData(KeyStore.BackgroundLayout, TextField, String("<p>Error loading layout data</p>"));
+ for (const field of fields) {
+ let fn = () => { }
+ Object.defineProperty(fn, "name", { value: field })
+ Fields[field] = fn;
}
+ let args: JsxArgs = {
+ Document: function Document() { },
+ DocumentView: function DocumentView() { },
+ Keys,
+ Fields
+ } as any;
+ return args;
+}
- @computed
- get layoutKeys(): Key[] {
- return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array<Key>());
- }
+@observer
+export class DocumentView extends React.Component<DocumentViewProps> {
- @computed
- get layoutFields(): Key[] {
- return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array<Key>());
+ private _mainCont = React.createRef<HTMLDivElement>();
+ private _documentBindings: any = null;
+ private _contextMenuCanOpen = false;
+ private _downX: number = 0;
+ private _downY: number = 0;
+
+ @computed get active(): boolean { return SelectionManager.IsSelected(this) || !this.props.ContainingCollectionView || this.props.ContainingCollectionView.active(); }
+ @computed get topMost(): boolean { return !this.props.ContainingCollectionView || this.props.ContainingCollectionView.collectionViewType == CollectionViewType.Docking; }
+ @computed get layout(): string { return this.props.Document.GetText(KeyStore.Layout, "<p>Error loading layout data</p>"); }
+ @computed get layoutKeys(): Key[] { return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array<Key>()); }
+ @computed get layoutFields(): Key[] { return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array<Key>()); }
+
+ screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect();
+
+ onPointerDown = (e: React.PointerEvent): void => {
+ this._downX = e.clientX;
+ this._downY = e.clientY;
+ if (e.shiftKey && e.buttons === 1) {
+ CollectionDockingView.Instance.StartOtherDrag(this._mainCont.current!, this.props.Document);
+ e.stopPropagation();
+ } else {
+ this._contextMenuCanOpen = true;
+ if (this.active && !e.isDefaultPrevented()) {
+ e.stopPropagation();
+ if (e.buttons === 2) {
+ e.preventDefault();
+ }
+ document.removeEventListener("pointermove", this.onPointerMove)
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp)
+ document.addEventListener("pointerup", this.onPointerUp);
+ }
+ }
}
- //
- // returns the cumulative scaling between the document and the screen
- //
- @computed
- public get ScalingToScreenSpace(): number {
- if (this.props.ContainingCollectionView != undefined &&
- this.props.ContainingCollectionView.props.ContainingDocumentView != undefined) {
- let ss = this.props.ContainingCollectionView.props.DocumentForCollection.GetNumber(KeyStore.Scale, 1);
- return this.props.ContainingCollectionView.props.ContainingDocumentView.ScalingToScreenSpace * ss;
+ 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;
+ 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["documentView"] = this;
+ dragData["xOffset"] = e.x - left;
+ dragData["yOffset"] = e.y - top;
+ DragManager.StartDrag(this._mainCont.current, dragData, {
+ handlers: {
+ dragComplete: action((e: DragManager.DragCompleteEvent) => { }),
+ },
+ hideSource: true
+ })
+ }
}
- return 1;
+ e.stopPropagation();
+ e.preventDefault();
}
- //
- // Converts a coordinate in the screen space of the app into a local document coordinate.
- //
- public TransformToLocalPoint(screenX: number, screenY: number) {
- // if this collection view is nested within another collection view, then
- // first transform the screen point into the parent collection's coordinate space.
- let { LocalX: parentX, LocalY: parentY } = this.props.ContainingCollectionView != undefined &&
- this.props.ContainingCollectionView.props.ContainingDocumentView != undefined ?
- this.props.ContainingCollectionView.props.ContainingDocumentView.TransformToLocalPoint(screenX, screenY) :
- { LocalX: screenX, LocalY: screenY };
- let ContainerX: number = parentX - COLLECTION_BORDER_WIDTH;
- let ContainerY: number = parentY - COLLECTION_BORDER_WIDTH;
-
- var Xx = this.props.Document.GetData(KeyStore.X, NumberField, Number(0));
- var Yy = this.props.Document.GetData(KeyStore.Y, NumberField, Number(0));
- // CollectionDockingViews change the location of their children frames without using a Dash transformation.
- // They also ignore any transformation that may have been applied to their content document.
- // NOTE: this currently assumes CollectionDockingViews aren't nested.
- if (this.props.ContainingCollectionView instanceof CollectionDockingView) {
- var { translateX: rx, translateY: ry } = Utils.GetScreenTransform(this.MainContent.current!);
- Xx = rx - COLLECTION_BORDER_WIDTH;
- Yy = ry - COLLECTION_BORDER_WIDTH;
+ onPointerUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onPointerMove)
+ document.removeEventListener("pointerup", this.onPointerUp)
+ e.stopPropagation();
+ if (Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientY - this._downY) < 4) {
+ SelectionManager.SelectDoc(this, e.ctrlKey);
}
+ }
- let Ss = this.props.Document.GetNumber(KeyStore.Scale, 1);
- let Panxx = this.props.Document.GetData(KeyStore.PanX, NumberField, Number(0));
- let Panyy = this.props.Document.GetData(KeyStore.PanY, NumberField, Number(0));
- let LocalX = (ContainerX - (Xx + Panxx)) / Ss;
- let LocalY = (ContainerY - (Yy + Panyy)) / Ss;
-
- return { LocalX, Ss, Panxx, Xx, LocalY, Panyy, Yy, ContainerX, ContainerY };
+ deleteClicked = (e: React.MouseEvent): void => {
+ if (this.props.RemoveDocument) {
+ this.props.RemoveDocument(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();
+ ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked })
+ ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15)
}
- //
- // Converts a point in the coordinate space of a document to a screen space coordinate.
- //
- public TransformToScreenPoint(localX: number, localY: number, Ss: number = 1, Panxx: number = 0, Panyy: number = 0): { ScreenX: number, ScreenY: number } {
-
- var Xx = this.props.Document.GetData(KeyStore.X, NumberField, Number(0));
- var Yy = this.props.Document.GetData(KeyStore.Y, NumberField, Number(0));
- // CollectionDockingViews change the location of their children frames without using a Dash transformation.
- // They also ignore any transformation that may have been applied to their content document.
- // NOTE: this currently assumes CollectionDockingViews aren't nested.
- if (this.props.ContainingCollectionView instanceof CollectionDockingView) {
- var { translateX: rx, translateY: ry } = Utils.GetScreenTransform(this.MainContent.current!);
- Xx = rx - COLLECTION_BORDER_WIDTH;
- Yy = ry - COLLECTION_BORDER_WIDTH;
+ @action
+ onContextMenu = (e: React.MouseEvent): void => {
+ e.preventDefault()
+ e.stopPropagation();
+ if (!SelectionManager.IsSelected(this) || !this._contextMenuCanOpen) {
+ return;
}
- let W = COLLECTION_BORDER_WIDTH;
- let H = COLLECTION_BORDER_WIDTH;
- let parentX = (localX - W) * Ss + (Xx + Panxx) + W;
- let parentY = (localY - H) * Ss + (Yy + Panyy) + H;
-
- // if this collection view is nested within another collection view, then
- // first transform the local point into the parent collection's coordinate space.
- let containingDocView = this.props.ContainingCollectionView != undefined ? this.props.ContainingCollectionView.props.ContainingDocumentView : undefined;
- if (containingDocView != undefined) {
- let ss = containingDocView.props.Document.GetNumber(KeyStore.Scale, 1) * this.props.Scaling;
- let panxx = containingDocView.props.Document.GetData(KeyStore.PanX, NumberField, Number(0)) + COLLECTION_BORDER_WIDTH * ss;
- let panyy = containingDocView.props.Document.GetData(KeyStore.PanY, NumberField, Number(0)) + COLLECTION_BORDER_WIDTH * ss;
- let { ScreenX, ScreenY } = containingDocView.TransformToScreenPoint(parentX, parentY, ss, panxx, panyy);
- parentX = ScreenX;
- parentY = ScreenY;
+ 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 {
+ // 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: "Treeview", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Tree) })
+ 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);
}
- return { ScreenX: parentX, ScreenY: parentY };
}
-
+ @computed get mainContent() {
+ var val = this.props.Document.Id;
+ return <JsxParser
+ components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebView }}
+ bindings={this._documentBindings}
+ jsx={this.layout}
+ showWarnings={true}
+ onError={(test: any) => { console.log(test) }}
+ />
+ }
render() {
- let bindings = { ...this.props } as any;
+ if (!this.props.Document)
+ return <div></div>
+ let lkeys = this.props.Document.GetT(KeyStore.LayoutKeys, ListField);
+ if (!lkeys || lkeys === "<Waiting>") {
+ return <p>Error loading layout keys</p>;
+ }
+ this._documentBindings = {
+ ...this.props,
+ isSelected: () => SelectionManager.IsSelected(this),
+ select: (ctrlPressed: boolean) => SelectionManager.SelectDoc(this, ctrlPressed)
+ };
for (const key of this.layoutKeys) {
- bindings[key.Name + "Key"] = key; // this maps string values of the form <keyname>Key to an actual key Kestore.keyname e.g, "DataKey" => KeyStore.Data
+ this._documentBindings[key.Name + "Key"] = key; // this maps string values of the form <keyname>Key to an actual key Kestore.keyname e.g, "DataKey" => KeyStore.Data
}
for (const key of this.layoutFields) {
let field = this.props.Document.Get(key);
- bindings[key.Name] = field && field != FieldWaiting ? field.GetValue() : field;
- }
- if (bindings.DocumentView === undefined) {
- bindings.DocumentView = this; // set the DocumentView to this if it hasn't already been set by a sub-class during its render method.
+ this._documentBindings[key.Name] = field && field != FieldWaiting ? field.GetValue() : field;
}
- var annotated = <JsxParser
- components={{ FormattedTextBox: FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView }}
- bindings={bindings}
- jsx={this.backgroundLayout}
- showWarnings={true}
- onError={(test: any) => { console.log(test) }}
- />;
- bindings["BackgroundView"] = this.backgroundLayout ? annotated : null;
+ this._documentBindings.bindings = this._documentBindings;
+
+ var scaling = this.props.ContentScaling();
+ var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0);
+ var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0);
return (
- <div className="node" ref={this._mainCont} style={{ width: "100%", height: "100%", }}>
- <JsxParser
- components={{ FormattedTextBox: FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView }}
- bindings={bindings}
- jsx={this.layout}
- showWarnings={true}
- onError={(test: any) => { console.log(test) }}
- />
+ <div className="documentView-node" ref={this._mainCont}
+ style={{
+ width: nativeWidth > 0 ? nativeWidth.toString() + "px" : "100%",
+ height: nativeHeight > 0 ? nativeHeight.toString() + "px" : "100%",
+ transformOrigin: "left top",
+ transform: `scale(${scaling},${scaling})`
+ }}
+ onContextMenu={this.onContextMenu}
+ onPointerDown={this.onPointerDown}
+ >
+ {this.mainContent}
</div>
)
}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 12371eb2e..1a9d325db 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -10,7 +10,8 @@ import { ImageField } from "../../../fields/ImageField";
import { Key } from "../../../fields/Key";
import { FormattedTextBox } from "./FormattedTextBox";
import { ImageBox } from "./ImageBox";
-import { DocumentView } from "./DocumentView";
+import { HtmlField } from "../../../fields/HtmlField";
+import { WebView } from "./WebView";
//
// these properties get assigned through the render() method of the DocumentView when it creates this node.
@@ -20,12 +21,16 @@ import { DocumentView } from "./DocumentView";
export interface FieldViewProps {
fieldKey: Key;
doc: Document;
- DocumentViewForField: Opt<DocumentView>
+ isSelected: () => boolean;
+ select: () => void;
+ isTopMost: boolean;
+ bindings: any;
}
@observer
export class FieldView extends React.Component<FieldViewProps> {
- public static LayoutString(fieldType: string) { return `<${fieldType} doc={Document} DocumentViewForField={DocumentView} fieldKey={DataKey} />`; }
+ 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} />`; }
+
@computed
get field(): FieldValue<Field> {
const { doc, fieldKey } = this.props;
@@ -47,6 +52,8 @@ export class FieldView extends React.Component<FieldViewProps> {
}
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
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 492367fce..0389a3f85 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -1,7 +1,7 @@
.ProseMirror {
- margin-top: -1em;
width: 100%;
- height: 100%;
+ height: auto;
+ min-height: 100%
}
.ProseMirror:focus {
@@ -9,6 +9,10 @@
}
.formattedTextBox-cont {
- background: white;
- padding: 1vw;
+ background: beige;
+ padding: 0;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ color: initial;
+ height: 100%;
} \ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 23eca4e24..d0dd9e91f 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -6,13 +6,10 @@ import { schema } from "prosemirror-schema-basic";
import { EditorState, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { Opt, FieldWaiting, FieldValue } from "../../../fields/Field";
-import { SelectionManager } from "../../util/SelectionManager";
import "./FormattedTextBox.scss";
import React = require("react")
import { RichTextField } from "../../../fields/RichTextField";
import { FieldViewProps, FieldView } from "./FieldView";
-import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView";
-import { observer } from "mobx-react";
import { ContextMenu } from "../../views/ContextMenu";
@@ -34,7 +31,7 @@ import { ContextMenu } from "../../views/ContextMenu";
//]
export class FormattedTextBox extends React.Component<FieldViewProps> {
- public static LayoutString() { return FieldView.LayoutString("FormattedTextBox"); }
+ public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(FormattedTextBox, fieldStr) }
private _ref: React.RefObject<HTMLDivElement>;
private _editorView: Opt<EditorView>;
private _reactionDisposer: Opt<IReactionDisposer>;
@@ -111,7 +108,7 @@ export class FormattedTextBox extends React.Component<FieldViewProps> {
}
onPointerDown = (e: React.PointerEvent): void => {
let me = this;
- if (e.buttons === 1 && me.props.DocumentViewForField instanceof CollectionFreeFormDocumentView && SelectionManager.IsSelected(me.props.DocumentViewForField)) {
+ if (e.buttons === 1 && this.props.isSelected()) {
e.stopPropagation();
}
}
@@ -124,8 +121,15 @@ export class FormattedTextBox extends React.Component<FieldViewProps> {
ContextMenu.Instance.addItem({ description: "Text Capability", event: this.textCapability });
}
+ onPointerWheel = (e: React.WheelEvent): void => {
+ e.stopPropagation();
+ }
+
render() {
- return (<div className="formattedTextBox-cont" style={{ color: "initial", whiteSpace: "initial" }}
- onPointerDown={this.onPointerDown} ref={this._ref} onContextMenu={this.specificContextMenu} />)
+ return (<div className="formattedTextBox-cont"
+ onPointerDown={this.onPointerDown}
+ onContextMenu={this.specificContextMenu}
+ onWheel={this.onPointerWheel}
+ ref={this._ref} />)
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 2bd8b1d3c..ea459b911 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -1,8 +1,17 @@
.imageBox-cont {
padding: 0vw;
- position: absolute;
- width: 100%
+ position: relative;
+ text-align: center;
+ width: 100%;
+ height: auto;
+ max-width: 100%;
+ max-height: 100%
+}
+
+.imageBox-cont img {
+ object-fit: contain;
+ height: 100%;
}
.imageBox-button {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 0f10ef0ef..8c44395f4 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,23 +1,22 @@
import Lightbox from 'react-image-lightbox';
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
-import { SelectionManager } from "../../util/SelectionManager";
import "./ImageBox.scss";
import React = require("react")
import { ImageField } from '../../../fields/ImageField';
import { FieldViewProps, FieldView } from './FieldView';
-import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView';
import { FieldWaiting } from '../../../fields/Field';
import { observer } from "mobx-react"
-import { observable, action, spy } from 'mobx';
-import { KeyStore } from '../../../fields/Key';
import { ContextMenu } from "../../views/ContextMenu";
+import { observable, action } from 'mobx';
+import { KeyStore } from '../../../fields/KeyStore';
@observer
export class ImageBox extends React.Component<FieldViewProps> {
- public static LayoutString() { return FieldView.LayoutString("ImageBox"); }
+ 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;
@@ -28,12 +27,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() {
}
@@ -42,7 +49,7 @@ export class ImageBox extends React.Component<FieldViewProps> {
onPointerDown = (e: React.PointerEvent): void => {
if (Date.now() - this._lastTap < 300) {
- if (e.buttons === 1 && this.props.DocumentViewForField instanceof CollectionFreeFormDocumentView && SelectionManager.IsSelected(this.props.DocumentViewForField)) {
+ if (e.buttons === 1 && this.props.isSelected()) {
e.stopPropagation();
this._downX = e.clientX;
this._downY = e.clientY;
@@ -64,7 +71,7 @@ export class ImageBox extends React.Component<FieldViewProps> {
lightbox = (path: string) => {
const images = [path, "http://www.cs.brown.edu/~bcz/face.gif"];
- if (this._isOpen && this.props.DocumentViewForField instanceof CollectionFreeFormDocumentView && SelectionManager.IsSelected(this.props.DocumentViewForField)) {
+ if (this._isOpen && this.props.isSelected()) {
return (<Lightbox
mainSrc={images[this._photoIndex]}
nextSrc={images[(this._photoIndex + 1) % images.length]}
@@ -94,10 +101,10 @@ export class ImageBox extends React.Component<FieldViewProps> {
let field = this.props.doc.Get(this.props.fieldKey);
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} onContextMenu={this.specificContextMenu}>
- <img src={path} width="100%" 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/WebView.tsx b/src/client/views/nodes/WebView.tsx
new file mode 100644
index 000000000..717aa8bf5
--- /dev/null
+++ b/src/client/views/nodes/WebView.tsx
@@ -0,0 +1,22 @@
+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/Viewer.tsx b/src/debug/Viewer.tsx
new file mode 100644
index 000000000..780e9f8f2
--- /dev/null
+++ b/src/debug/Viewer.tsx
@@ -0,0 +1,192 @@
+import { action, configure, observable, ObservableMap, Lambda } from 'mobx';
+import "normalize.css";
+import * as React from 'react';
+import * as ReactDOM from 'react-dom';
+import { observer } from 'mobx-react';
+import { Document } from '../fields/Document';
+import { BasicField } from '../fields/BasicField';
+import { ListField } from '../fields/ListField';
+import { Key } from '../fields/Key';
+import { Opt, Field } from '../fields/Field';
+import { Server } from '../client/Server';
+
+configure({
+ enforceActions: "observed"
+});
+
+@observer
+class FieldViewer extends React.Component<{ field: BasicField<any> }> {
+ render() {
+ return <span>{JSON.stringify(this.props.field.Data)} ({this.props.field.Id})</span>;
+ }
+}
+
+@observer
+class KeyViewer extends React.Component<{ field: Key }> {
+ render() {
+ return this.props.field.Name;
+ }
+}
+
+@observer
+class ListViewer extends React.Component<{ field: ListField<Field> }>{
+ @observable
+ expanded = false;
+
+ render() {
+ let content;
+ if (this.expanded) {
+ content = (
+ <div>
+ {this.props.field.Data.map(field => <DebugViewer fieldId={field.Id} key={field.Id} />)}
+ </div>
+ )
+ } else {
+ content = <>[...] ({this.props.field.Id})</>
+ }
+ return (
+ <div>
+ <button onClick={action(() => this.expanded = !this.expanded)}>Toggle</button>
+ {content}
+ </div >
+ )
+ }
+}
+
+@observer
+class DocumentViewer extends React.Component<{ field: Document }> {
+ private keyMap: ObservableMap<string, Key> = new ObservableMap
+
+ private disposer?: Lambda;
+
+ componentDidMount() {
+ let f = () => {
+ Array.from(this.props.field._proxies.keys()).forEach(id => {
+ if (!this.keyMap.has(id)) {
+ Server.GetField(id, (field) => {
+ if (field && field instanceof Key) {
+ this.keyMap.set(id, field);
+ }
+ })
+ }
+ });
+ }
+ this.disposer = this.props.field._proxies.observe(f)
+ f()
+ }
+
+ componentWillUnmount() {
+ if (this.disposer) {
+ this.disposer();
+ }
+ }
+
+ render() {
+ let fields = Array.from(this.props.field._proxies.entries()).map(kv => {
+ let key = this.keyMap.get(kv[0]);
+ return (
+ <div key={kv[0]}>
+ <b>({key ? key.Name : kv[0]}): </b>
+ <DebugViewer fieldId={kv[1]!}></DebugViewer>
+ </div>
+ )
+ })
+ return (
+ <div>
+ Document ({this.props.field.Id})
+ <div style={{ paddingLeft: "25px" }}>
+ {fields}
+ </div>
+ </div>
+ )
+ }
+}
+
+@observer
+class DebugViewer extends React.Component<{ fieldId: string }> {
+ @observable
+ private field?: Field;
+
+ @observable
+ private error?: string;
+
+ constructor(props: { fieldId: string }) {
+ super(props)
+ this.update()
+ }
+
+ update() {
+ Server.GetField(this.props.fieldId, action((field: Opt<Field>) => {
+ this.field = field;
+ if (!field) {
+ this.error = `Field with id ${this.props.fieldId} not found`
+ }
+ }));
+
+ }
+
+ render() {
+ let content;
+ if (this.field) {
+ // content = this.field.ToJson();
+ if (this.field instanceof ListField) {
+ content = (<ListViewer field={this.field} />)
+ } else if (this.field instanceof Document) {
+ content = (<DocumentViewer field={this.field} />)
+ } else if (this.field instanceof BasicField) {
+ content = (<FieldViewer field={this.field} />)
+ } else if (this.field instanceof Key) {
+ content = (<KeyViewer field={this.field} />)
+ }
+ } else if (this.error) {
+ content = <span>Field <b>{this.props.fieldId}</b> not found <button onClick={() => this.update()}>Refresh</button></span>
+ } else {
+ content = <>Field loading</>
+ }
+ return content;
+ }
+}
+
+@observer
+class Viewer extends React.Component {
+ @observable
+ private idToAdd: string = '';
+
+ @observable
+ private ids: string[] = [];
+
+ @action
+ inputOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.idToAdd = e.target.value;
+ }
+
+ @action
+ onKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
+ if (e.key === "Enter") {
+ this.ids.push(this.idToAdd)
+ this.idToAdd = ""
+ }
+ }
+
+ render() {
+ return (
+ <>
+ <input value={this.idToAdd}
+ onChange={this.inputOnChange}
+ onKeyDown={this.onKeyPress} />
+ <div>
+ {this.ids.map(id => {
+ return <DebugViewer fieldId={id} key={id}></DebugViewer>
+ })}
+ </div>
+ </>
+ )
+ }
+}
+
+ReactDOM.render((
+ <div style={{ position: "absolute", width: "100%", height: "100%" }}>
+ <Viewer />
+ </div>),
+ document.getElementById('root')
+); \ No newline at end of file
diff --git a/src/fields/BasicField.ts b/src/fields/BasicField.ts
index fb5cc773e..a92c4a236 100644
--- a/src/fields/BasicField.ts
+++ b/src/fields/BasicField.ts
@@ -1,15 +1,26 @@
-import { Field } from "./Field"
+import { Field, FieldId } from "./Field"
import { observable, computed, action } from "mobx";
+import { Server } from "../client/Server";
+import { UndoManager } from "../client/util/UndoManager";
export abstract class BasicField<T> extends Field {
- constructor(data: T) {
- super();
+ constructor(data: T, save: boolean, id?: FieldId) {
+ super(id);
this.data = data;
+ if (save) {
+ Server.UpdateField(this)
+ }
+ }
+
+ UpdateFromServer(data: any) {
+ if (this.data !== data) {
+ this.data = data;
+ }
}
@observable
- private data: T;
+ protected data: T;
@computed
get Data(): T {
@@ -20,6 +31,16 @@ export abstract class BasicField<T> extends Field {
if (this.data === value) {
return;
}
+ let oldValue = this.data;
+ this.setData(value);
+ UndoManager.AddEvent({
+ undo: () => this.Data = oldValue,
+ redo: () => this.Data = value
+ })
+ Server.UpdateField(this);
+ }
+
+ protected setData(value: T) {
this.data = value;
}
diff --git a/src/fields/Document.ts b/src/fields/Document.ts
index c682d8e94..6193ea56c 100644
--- a/src/fields/Document.ts
+++ b/src/fields/Document.ts
@@ -1,14 +1,36 @@
-import { Field, Cast, Opt, FieldWaiting, FieldId, FieldValue } from "./Field"
-import { Key, KeyStore } from "./Key"
+import { Key } from "./Key"
+import { KeyStore } from "./KeyStore";
+import { Field, Cast, FieldWaiting, FieldValue, FieldId } from "./Field"
import { NumberField } from "./NumberField";
-import { ObservableMap, computed, action, observable } from "mobx";
+import { ObservableMap, computed, action } from "mobx";
import { TextField } from "./TextField";
import { ListField } from "./ListField";
import { Server } from "../client/Server";
+import { Types } from "../server/Message";
+import { UndoManager } from "../client/util/UndoManager";
export class Document extends Field {
- public fields: ObservableMap<Key, Field> = new ObservableMap();
- public _proxies: ObservableMap<Key, FieldId> = new ObservableMap();
+ public fields: ObservableMap<string, { key: Key, field: Field }> = new ObservableMap();
+ public _proxies: ObservableMap<string, FieldId> = new ObservableMap();
+
+ constructor(id?: string, save: boolean = true) {
+ super(id)
+
+ if (save) {
+ Server.UpdateField(this)
+ }
+ }
+
+ UpdateFromServer(data: [string, string][]) {
+ for (const key in data) {
+ const element = data[key];
+ this._proxies.set(element[0], element[1]);
+ }
+ }
+
+ public Width = () => { return this.GetNumber(KeyStore.Width, 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
public get Title() {
@@ -18,33 +40,67 @@ export class Document extends Field {
Get(key: Key, ignoreProto: boolean = false): FieldValue<Field> {
let field: FieldValue<Field>;
if (ignoreProto) {
- if (this.fields.has(key)) {
- field = this.fields.get(key);
- } else if (this._proxies.has(key)) {
- field = Server.GetDocumentField(this, key);
+ if (this.fields.has(key.Id)) {
+ field = this.fields.get(key.Id)!.field;
+ } else if (this._proxies.has(key.Id)) {
+ Server.GetDocumentField(this, key);
+ /*
+ The field might have been instantly filled from the cache
+ Maybe we want to just switch back to returning the value
+ from Server.GetDocumentField if it's in the cache
+ */
+ if (this.fields.has(key.Id)) {
+ field = this.fields.get(key.Id)!.field;
+ } else {
+ field = FieldWaiting;
+ }
}
} else {
let doc: FieldValue<Document> = this;
while (doc && doc != FieldWaiting && field != FieldWaiting) {
- if (!doc.fields.has(key)) {
- if (doc._proxies.has(key)) {
- field = Server.GetDocumentField(doc, key);
+ let curField = doc.fields.get(key.Id);
+ let curProxy = doc._proxies.get(key.Id);
+ if (!curField || (curProxy && curField.field.Id !== curProxy)) {
+ if (curProxy) {
+ Server.GetDocumentField(doc, key);
+ /*
+ The field might have been instantly filled from the cache
+ Maybe we want to just switch back to returning the value
+ from Server.GetDocumentField if it's in the cache
+ */
+ if (this.fields.has(key.Id)) {
+ field = this.fields.get(key.Id)!.field;
+ } else {
+ field = FieldWaiting;
+ }
break;
}
- if ((doc.fields.has(KeyStore.Prototype) || doc._proxies.has(KeyStore.Prototype))) {
+ if ((doc.fields.has(KeyStore.Prototype.Id) || doc._proxies.has(KeyStore.Prototype.Id))) {
doc = doc.GetPrototype();
- } else
+ } else {
break;
+ }
} else {
- field = doc.fields.get(key);
+ field = curField.field;
break;
}
}
+ if (doc == FieldWaiting)
+ field = FieldWaiting;
}
return field;
}
+ GetAsync(key: Key, callback: (field: Field) => void): boolean {
+ //This currently doesn't deal with prototypes
+ if (this._proxies.has(key.Id)) {
+ Server.GetDocumentField(this, key, callback);
+ return true;
+ }
+ return false;
+ }
+
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) {
@@ -83,29 +139,37 @@ export class Document extends Field {
@action
Set(key: Key, field: Field | undefined): void {
+ let old = this.fields.get(key.Id);
+ let oldField = old ? old.field : undefined;
if (field) {
- this.fields.set(key, field);
- Server.AddDocumentField(this, key, field);
+ this.fields.set(key.Id, { key, field });
+ this._proxies.set(key.Id, field.Id)
+ // Server.AddDocumentField(this, key, field);
} else {
- this.fields.delete(key);
- Server.DeleteDocumentField(this, key);
+ this.fields.delete(key.Id);
+ this._proxies.delete(key.Id)
+ // Server.DeleteDocumentField(this, key);
+ }
+ if (oldField || field) {
+ UndoManager.AddEvent({
+ undo: () => this.Set(key, oldField),
+ redo: () => this.Set(key, field)
+ })
}
+ Server.UpdateField(this);
}
@action
SetData<T, U extends Field & { Data: T }>(key: Key, value: T, ctor: { new(): U }, replaceWrongType = true) {
let field = this.Get(key, true);
- //if (field != WAITING) { // do we want to wait for the field to come back from the server to set it, or do we overwrite?
if (field instanceof ctor) {
field.Data = value;
- Server.SetFieldValue(field, value);
} else if (!field || replaceWrongType) {
let newField = new ctor();
newField.Data = value;
this.Set(key, newField);
}
- //}
}
@action
@@ -132,8 +196,8 @@ export class Document extends Field {
return protos;
}
- MakeDelegate(): Document {
- let delegate = new Document();
+ MakeDelegate(id?: string): Document {
+ let delegate = new Document(id);
delegate.Set(KeyStore.Prototype, this);
@@ -148,11 +212,26 @@ export class Document extends Field {
throw new Error("Method not implemented.");
}
GetValue() {
- throw new Error("Method not implemented.");
+ var title = (this._proxies.has(KeyStore.Title.Id) ? "???" : this.Title) + "(" + this.Id + ")";
+ return title;
+ //throw new Error("Method not implemented.");
}
Copy(): Field {
throw new Error("Method not implemented.");
}
+ ToJson(): { type: Types, data: [string, string][], _id: string } {
+ let fields: [string, string][] = []
+ this._proxies.forEach((field, key) => {
+ if (field) {
+ fields.push([key, field as string])
+ }
+ });
+ return {
+ type: Types.Document,
+ data: fields,
+ _id: this.Id
+ }
+ }
} \ No newline at end of file
diff --git a/src/fields/DocumentReference.ts b/src/fields/DocumentReference.ts
index 983b162a3..9d3c209b4 100644
--- a/src/fields/DocumentReference.ts
+++ b/src/fields/DocumentReference.ts
@@ -1,6 +1,8 @@
-import { Field, Opt, FieldValue } from "./Field";
+import { Field, Opt, FieldValue, FieldId } from "./Field";
import { Document } from "./Document";
import { Key } from "./Key";
+import { Types } from "../server/Message";
+import { ObjectID } from "bson";
export class DocumentReference extends Field {
get Key(): Key {
@@ -15,6 +17,10 @@ export class DocumentReference extends Field {
super();
}
+ UpdateFromServer() {
+
+ }
+
Dereference(): FieldValue<Field> {
return this.document.Get(this.key);
}
@@ -41,4 +47,11 @@ export class DocumentReference extends Field {
return "";
}
+ ToJson(): { type: Types, data: FieldId, _id: string } {
+ return {
+ type: Types.DocumentReference,
+ data: this.document.Id,
+ _id: this.Id
+ }
+ }
} \ No newline at end of file
diff --git a/src/fields/Field.ts b/src/fields/Field.ts
index 4a3968699..d48509a47 100644
--- a/src/fields/Field.ts
+++ b/src/fields/Field.ts
@@ -1,5 +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) {
@@ -19,7 +21,13 @@ export type FieldValue<T> = Opt<T> | FIELD_WAITING;
export abstract class Field {
//FieldUpdated: TypedEvent<Opt<FieldUpdatedArgs>> = new TypedEvent<Opt<FieldUpdatedArgs>>();
+ init(callback: (res: Field) => any) {
+ callback(this);
+ }
+
private id: FieldId;
+
+ @computed
get Id(): FieldId {
return this.id;
}
@@ -47,6 +55,8 @@ export abstract class Field {
return this.id === other.id;
}
+ abstract UpdateFromServer(serverData: any): void;
+
abstract ToScriptString(): string;
abstract TrySetValue(value: any): boolean;
@@ -55,4 +65,5 @@ export abstract class Field {
abstract Copy(): Field;
+ abstract ToJson(): { _id: string, type: Types, data: any }
} \ No newline at end of file
diff --git a/src/fields/HtmlField.ts b/src/fields/HtmlField.ts
new file mode 100644
index 000000000..a07326095
--- /dev/null
+++ b/src/fields/HtmlField.ts
@@ -0,0 +1,25 @@
+import { BasicField } from "./BasicField";
+import { Types } from "../server/Message";
+import { FieldId } from "./Field";
+
+export class HtmlField extends BasicField<string> {
+ constructor(data: string = "<html></html>", id?: FieldId, save: boolean = true) {
+ super(data, save, id);
+ }
+
+ ToScriptString(): string {
+ return `new HtmlField("${this.Data}")`;
+ }
+
+ Copy() {
+ return new HtmlField(this.Data);
+ }
+
+ ToJson(): { _id: string; type: Types; data: any; } {
+ return {
+ type: Types.Html,
+ data: this.Data,
+ _id: this.Id,
+ }
+ }
+} \ No newline at end of file
diff --git a/src/fields/ImageField.ts b/src/fields/ImageField.ts
index d82260f54..b2226d55a 100644
--- a/src/fields/ImageField.ts
+++ b/src/fields/ImageField.ts
@@ -1,9 +1,11 @@
import { BasicField } from "./BasicField";
-import { Field } from "./Field";
+import { Field, FieldId } from "./Field";
+import { Types } from "../server/Message";
+import { ObjectID } from "bson";
export class ImageField extends BasicField<URL> {
- constructor(data: URL | undefined = undefined) {
- super(data == undefined ? new URL("http://cs.brown.edu/~bcz/bob_fettucine.jpg") : data);
+ constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) {
+ super(data == undefined ? new URL("http://cs.brown.edu/~bcz/bob_fettucine.jpg") : data, save, id);
}
toString(): string {
@@ -18,4 +20,11 @@ export class ImageField extends BasicField<URL> {
return new ImageField(this.Data);
}
+ ToJson(): { type: Types, data: URL, _id: string } {
+ return {
+ type: Types.Image,
+ 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 8d92b89b6..c16a00878 100644
--- a/src/fields/Key.ts
+++ b/src/fields/Key.ts
@@ -1,6 +1,9 @@
-import { Field } from "./Field"
+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 {
private name: string;
@@ -9,10 +12,17 @@ export class Key extends Field {
return this.name;
}
- constructor(name: string) {
- super(Utils.GenerateDeterministicGuid(name));
+ constructor(name: string, id?: string, save: boolean = true) {
+ super(id || Utils.GenerateDeterministicGuid(name));
this.name = name;
+ if (save) {
+ Server.UpdateField(this)
+ }
+ }
+
+ UpdateFromServer(data: string) {
+ this.name = data;
}
TrySetValue(value: any): boolean {
@@ -31,27 +41,11 @@ export class Key extends Field {
return name;
}
-}
-
-export namespace KeyStore {
- export const Prototype = new Key("Prototype");
- export const X = new Key("X");
- export const Y = new Key("Y");
- export const Title = new Key("Title");
- export const Author = new Key("Author");
- export const PanX = new Key("PanX");
- export const PanY = new Key("PanY");
- export const Scale = new Key("Scale");
- export const NativeWidth = new Key("NativeWidth");
- export const NativeHeight = new Key("NativeHeight");
- export const Width = new Key("Width");
- export const Height = new Key("Height");
- export const ZIndex = new Key("ZIndex");
- export const Data = new Key("Data");
- export const Annotations = new Key("Annotations");
- export const Layout = new Key("Layout");
- export const BackgroundLayout = new Key("BackgroundLayout");
- export const LayoutKeys = new Key("LayoutKeys");
- export const LayoutFields = new Key("LayoutFields");
- export const ColumnsKey = new Key("SchemaColumns");
+ ToJson(): { type: Types, data: string, _id: string } {
+ return {
+ type: Types.Key,
+ data: this.name,
+ _id: this.Id
+ }
+ }
} \ No newline at end of file
diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts
new file mode 100644
index 000000000..a3b39735d
--- /dev/null
+++ b/src/fields/KeyStore.ts
@@ -0,0 +1,29 @@
+import { Key } from "./Key";
+
+export namespace KeyStore {
+ export const Prototype = new Key("Prototype");
+ export const X = new Key("X");
+ export const Y = new Key("Y");
+ export const Title = new Key("Title");
+ export const Author = new Key("Author");
+ export const PanX = new Key("PanX");
+ export const PanY = new Key("PanY");
+ export const Scale = new Key("Scale");
+ export const NativeWidth = new Key("NativeWidth");
+ export const NativeHeight = new Key("NativeHeight");
+ export const Width = new Key("Width");
+ export const Height = new Key("Height");
+ export const ZIndex = new Key("ZIndex");
+ export const Data = new Key("Data");
+ export const Annotations = new Key("Annotations");
+ export const ViewType = new Key("ViewType");
+ export const Layout = new Key("Layout");
+ export const BackgroundLayout = new Key("BackgroundLayout");
+ export const OverlayLayout = new Key("OverlayLayout");
+ export const LayoutKeys = new Key("LayoutKeys");
+ export const LayoutFields = new Key("LayoutFields");
+ export const ColumnsKey = new Key("SchemaColumns");
+ export const Caption = new Key("Caption");
+ export const ActiveFrame = new Key("ActiveFrame");
+ export const DocumentText = new Key("DocumentText");
+}
diff --git a/src/fields/ListField.ts b/src/fields/ListField.ts
index 8843338c1..700600804 100644
--- a/src/fields/ListField.ts
+++ b/src/fields/ListField.ts
@@ -1,9 +1,82 @@
-import { Field } from "./Field";
+import { action, IArrayChange, IArraySplice, IObservableArray, observe, observable, Lambda } from "mobx";
+import { Server } from "../client/Server";
+import { UndoManager } from "../client/util/UndoManager";
+import { Types } from "../server/Message";
import { BasicField } from "./BasicField";
+import { Field, FieldId } from "./Field";
export class ListField<T extends Field> extends BasicField<T[]> {
- constructor(data: T[] = []) {
- super(data.slice());
+ private _proxies: string[] = []
+ constructor(data: T[] = [], id?: FieldId, save: boolean = true) {
+ super(data, save, id);
+ this.updateProxies();
+ if (save) {
+ Server.UpdateField(this);
+ }
+ this.observeList();
+ }
+
+ private observeDisposer: Lambda | undefined;
+ private observeList(): void {
+ this.observeDisposer = observe(this.Data as IObservableArray<T>, (change: IArrayChange<T> | IArraySplice<T>) => {
+ this.updateProxies()
+ if (change.type == "splice") {
+ UndoManager.AddEvent({
+ undo: () => this.Data.splice(change.index, change.addedCount, ...change.removed),
+ redo: () => this.Data.splice(change.index, change.removedCount, ...change.added)
+ })
+ } else {
+ UndoManager.AddEvent({
+ undo: () => this.Data[change.index] = change.oldValue,
+ redo: () => this.Data[change.index] = change.newValue
+ })
+ }
+ Server.UpdateField(this);
+ });
+ }
+
+ protected setData(value: T[]) {
+ if (this.observeDisposer) {
+ this.observeDisposer()
+ }
+ this.data = observable(value);
+ this.observeList();
+ }
+
+ private updateProxies() {
+ this._proxies = this.Data.map(field => field.Id);
+ }
+
+ UpdateFromServer(fields: string[]) {
+ this._proxies = fields;
+ }
+ private arraysEqual(a: any[], b: any[]) {
+ if (a === b) return true;
+ if (a == null || b == null) return false;
+ if (a.length != b.length) return false;
+
+ // If you don't care about the order of the elements inside
+ // the array, you should sort both arrays here.
+ // Please note that calling sort on an array will modify that array.
+ // you might want to clone your array first.
+
+ for (var i = 0; i < a.length; ++i) {
+ if (a[i] !== b[i]) return false;
+ }
+ return true;
+ }
+
+ init(callback: (field: Field) => any) {
+ Server.GetFields(this._proxies, action((fields: { [index: string]: Field }) => {
+ if (!this.arraysEqual(this._proxies, this.Data.map(field => field.Id))) {
+ this.data = this._proxies.map(id => fields[id] as T)
+ observe(this.Data, () => {
+ this.updateProxies()
+ Server.UpdateField(this);
+ })
+ }
+ callback(this);
+ }))
}
ToScriptString(): string {
@@ -13,4 +86,18 @@ export class ListField<T extends Field> extends BasicField<T[]> {
Copy(): Field {
return new ListField<T>(this.Data);
}
+
+ ToJson(): { type: Types, data: string[], _id: string } {
+ return {
+ type: Types.List,
+ data: this._proxies,
+ _id: this.Id
+ }
+ }
+
+ static FromJson(id: string, ids: string[]): ListField<Field> {
+ let list = new ListField([], id, false);
+ list._proxies = ids;
+ return list
+ }
} \ No newline at end of file
diff --git a/src/fields/NumberField.ts b/src/fields/NumberField.ts
index 03926d696..47dfc74cb 100644
--- a/src/fields/NumberField.ts
+++ b/src/fields/NumberField.ts
@@ -1,8 +1,10 @@
import { BasicField } from "./BasicField"
+import { Types } from "../server/Message";
+import { FieldId } from "./Field";
export class NumberField extends BasicField<number> {
- constructor(data: number = 0) {
- super(data);
+ constructor(data: number = 0, id?: FieldId, save: boolean = true) {
+ super(data, save, id);
}
ToScriptString(): string {
@@ -12,4 +14,12 @@ export class NumberField extends BasicField<number> {
Copy() {
return new NumberField(this.Data);
}
+
+ ToJson(): { _id: string, type: Types, data: number } {
+ return {
+ _id: this.Id,
+ type: Types.Number,
+ data: this.Data
+ }
+ }
} \ No newline at end of file
diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts
index 4a77c669c..5efb43314 100644
--- a/src/fields/RichTextField.ts
+++ b/src/fields/RichTextField.ts
@@ -1,8 +1,10 @@
import { BasicField } from "./BasicField";
+import { Types } from "../server/Message";
+import { FieldId } from "./Field";
export class RichTextField extends BasicField<string> {
- constructor(data: string = "") {
- super(data);
+ constructor(data: string = "", id?: FieldId, save: boolean = true) {
+ super(data, save, id);
}
ToScriptString(): string {
@@ -13,4 +15,12 @@ export class RichTextField extends BasicField<string> {
return new RichTextField(this.Data);
}
+ ToJson(): { type: Types, data: string, _id: string } {
+ return {
+ type: Types.RichText,
+ data: this.Data,
+ _id: this.Id
+ }
+ }
+
} \ No newline at end of file
diff --git a/src/fields/TextField.ts b/src/fields/TextField.ts
index 11d2ed7cd..71d8ea310 100644
--- a/src/fields/TextField.ts
+++ b/src/fields/TextField.ts
@@ -1,8 +1,10 @@
import { BasicField } from "./BasicField"
+import { FieldId } from "./Field";
+import { Types } from "../server/Message";
export class TextField extends BasicField<string> {
- constructor(data: string = "") {
- super(data);
+ constructor(data: string = "", id?: FieldId, save: boolean = true) {
+ super(data, save, id);
}
ToScriptString(): string {
@@ -12,4 +14,12 @@ export class TextField extends BasicField<string> {
Copy() {
return new TextField(this.Data);
}
-}
+
+ ToJson(): { type: Types, data: string, _id: string } {
+ return {
+ type: Types.Text,
+ data: this.Data,
+ _id: this.Id
+ }
+ }
+} \ No newline at end of file
diff --git a/src/server/Client.ts b/src/server/Client.ts
new file mode 100644
index 000000000..6b8841658
--- /dev/null
+++ b/src/server/Client.ts
@@ -0,0 +1,15 @@
+import { computed } from "mobx";
+
+export class Client {
+ constructor(guid: string) {
+ this.guid = guid
+ }
+
+ private guid: string;
+
+ @computed
+ public get GUID(): string {
+ return this.guid
+ }
+
+} \ No newline at end of file
diff --git a/src/server/Message.ts b/src/server/Message.ts
new file mode 100644
index 000000000..80fc9a80d
--- /dev/null
+++ b/src/server/Message.ts
@@ -0,0 +1,125 @@
+import { Utils } from "../Utils";
+
+export class Message<T> {
+ private name: string;
+ private guid: string;
+
+ get Name(): string {
+ return this.name;
+ }
+
+ get Message(): string {
+ return this.guid
+ }
+
+ constructor(name: string) {
+ this.name = name;
+ this.guid = Utils.GenerateDeterministicGuid(name)
+ }
+
+ GetValue() {
+ return this.Name;
+ }
+}
+
+class TestMessageArgs {
+ hello: string = "";
+}
+
+export class SetFieldArgs {
+ field: string;
+ value: any;
+
+ constructor(f: string, v: any) {
+ this.field = f
+ this.value = v
+ }
+}
+
+export class GetFieldArgs {
+ field: string;
+
+ constructor(f: string) {
+ this.field = f
+ }
+}
+
+export enum Types {
+ Number, List, Key, Image, Document, Text, RichText, DocumentReference, Html
+}
+
+export class DocumentTransfer implements Transferable {
+ readonly type = Types.Document
+ _id: string
+
+ constructor(readonly obj: { type: Types, data: [string, string][], _id: string }) {
+ this._id = obj._id
+ }
+}
+
+export class ImageTransfer implements Transferable {
+ readonly type = Types.Image
+
+ constructor(readonly _id: string) { }
+}
+
+export class KeyTransfer implements Transferable {
+ name: string
+ readonly _id: string
+ readonly type = Types.Key
+
+ constructor(i: string, n: string) {
+ this.name = n
+ this._id = i
+ }
+}
+
+export class ListTransfer implements Transferable {
+ type = Types.List;
+
+ constructor(readonly _id: string) { }
+}
+
+export class NumberTransfer implements Transferable {
+ readonly type = Types.Number
+
+ constructor(readonly value: number, readonly _id: string) { }
+}
+
+export class TextTransfer implements Transferable {
+ value: string
+ readonly _id: string
+ readonly type = Types.Text
+
+ constructor(t: string, i: string) {
+ this.value = t
+ this._id = i
+ }
+}
+
+export class RichTextTransfer implements Transferable {
+ value: string
+ readonly _id: string
+ readonly type = Types.Text
+
+ constructor(t: string, i: string) {
+ this.value = t
+ this._id = i
+ }
+}
+
+export interface Transferable {
+ readonly _id: string
+ readonly type: Types
+}
+
+export namespace MessageStore {
+ export const Foo = new Message<string>("Foo");
+ export const Bar = new Message<string>("Bar");
+ export const AddDocument = new Message<DocumentTransfer>("Add Document");
+ export const SetField = new Message<{ _id: string, data: any, type: Types }>("Set Field")
+ export const GetField = new Message<string>("Get Field")
+ export const GetFields = new Message<string[]>("Get Fields")
+ export const GetDocument = new Message<string>("Get Document");
+ export const DeleteAll = new Message<any>("Delete All");
+} \ No newline at end of file
diff --git a/src/server/ServerUtil.ts b/src/server/ServerUtil.ts
new file mode 100644
index 000000000..08e72fdae
--- /dev/null
+++ b/src/server/ServerUtil.ts
@@ -0,0 +1,52 @@
+import { Field } from './../fields/Field';
+import { TextField } from './../fields/TextField';
+import { NumberField } from './../fields/NumberField';
+import { RichTextField } from './../fields/RichTextField';
+import { Key } from './../fields/Key';
+import { ImageField } from './../fields/ImageField';
+import { ListField } from './../fields/ListField';
+import { Document } from './../fields/Document';
+import { Server } from './../client/Server';
+import { Types } from './Message';
+import { Utils } from '../Utils';
+import { HtmlField } from '../fields/HtmlField';
+
+export class ServerUtils {
+ public static FromJson(json: any): Field {
+ let obj = json
+ let data: any = obj.data
+ let id: string = obj._id
+ let type: Types = obj.type
+
+ if (!(data !== undefined && id && type !== undefined)) {
+ console.log("how did you manage to get an object that doesn't have a data or an id?")
+ return new TextField("Something to fill the space", Utils.GenerateGuid());
+ }
+
+ switch (type) {
+ case Types.Number:
+ return new NumberField(data, id, false)
+ case Types.Text:
+ return new TextField(data, id, false)
+ case Types.Html:
+ return new HtmlField(data, id, false)
+ case Types.RichText:
+ return new RichTextField(data, id, false)
+ case Types.Key:
+ return new Key(data, id, false)
+ case Types.Image:
+ return new ImageField(new URL(data), id, false)
+ case Types.List:
+ return ListField.FromJson(id, data)
+ case Types.Document:
+ let doc: Document = new Document(id, false)
+ let fields: [string, string][] = data as [string, string][]
+ fields.forEach(element => {
+ doc._proxies.set(element[0], element[1]);
+ });
+ return doc
+ default:
+ throw Error("Error, unrecognized field type received from server. If you just created a new field type, be sure to add it here");
+ }
+ }
+} \ No newline at end of file
diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts
new file mode 100644
index 000000000..05f6c3133
--- /dev/null
+++ b/src/server/authentication/config/passport.ts
@@ -0,0 +1,49 @@
+import * as passport from 'passport'
+import * as passportLocal from 'passport-local';
+import * as mongodb from 'mongodb';
+import * as _ from "lodash";
+import { default as User } from '../models/User';
+import { Request, Response, NextFunction } from "express";
+
+const LocalStrategy = passportLocal.Strategy;
+
+passport.serializeUser<any, any>((user, done) => {
+ done(undefined, user.id);
+});
+
+passport.deserializeUser<any, any>((id, done) => {
+ User.findById(id, (err, user) => {
+ done(err, user);
+ });
+});
+
+// AUTHENTICATE JUST WITH EMAIL AND PASSWORD
+passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, done) => {
+ User.findOne({ email: email.toLowerCase() }, (error: any, user: any) => {
+ if (error) return done(error);
+ if (!user) return done(undefined, false, { message: "Invalid email or password" }) // invalid email
+ user.comparePassword(password, (error: Error, isMatch: boolean) => {
+ if (error) return done(error);
+ if (!isMatch) return done(undefined, false, { message: "Invalid email or password" }); // invalid password
+ // valid authentication HERE
+ return done(undefined, user);
+ });
+ });
+}));
+
+export let isAuthenticated = (req: Request, res: Response, next: NextFunction) => {
+ if (req.isAuthenticated()) {
+ return next();
+ }
+ return res.redirect("/login");
+}
+
+export let isAuthorized = (req: Request, res: Response, next: NextFunction) => {
+ const provider = req.path.split("/").slice(-1)[0];
+
+ if (_.find(req.user.tokens, { kind: provider })) {
+ next();
+ } else {
+ res.redirect(`/auth/${provider}`);
+ }
+}; \ No newline at end of file
diff --git a/src/server/authentication/controllers/user.ts b/src/server/authentication/controllers/user.ts
new file mode 100644
index 000000000..f74ff9039
--- /dev/null
+++ b/src/server/authentication/controllers/user.ts
@@ -0,0 +1,107 @@
+import { default as User, UserModel, AuthToken } from "../models/User";
+import { Request, Response, NextFunction } from "express";
+import * as passport from "passport";
+import { IVerifyOptions } from "passport-local";
+import "../config/passport";
+import * as request from "express-validator";
+const flash = require("express-flash");
+import * as session from "express-session";
+import * as pug from 'pug';
+
+/**
+ * GET /signup
+ * Signup page.
+ */
+export let getSignup = (req: Request, res: Response) => {
+ if (req.user) {
+ return res.redirect("/");
+ }
+ res.render("signup.pug", {
+ title: "Sign Up"
+ });
+};
+
+/**
+ * POST /signup
+ * Create a new local account.
+ */
+export let postSignup = (req: Request, res: Response, next: NextFunction) => {
+ req.assert("email", "Email is not valid").isEmail();
+ req.assert("password", "Password must be at least 4 characters long").len({ min: 4 });
+ req.assert("confirmPassword", "Passwords do not match").equals(req.body.password);
+ req.sanitize("email").normalizeEmail({ gmail_remove_dots: false });
+
+ const errors = req.validationErrors();
+
+ if (errors) {
+ req.flash("errors", "Unable to facilitate sign up. Please try again.");
+ return res.redirect("/signup");
+ }
+
+ const user = new User({
+ email: req.body.email,
+ password: req.body.password
+ });
+
+ User.findOne({ email: req.body.email }, (err, existingUser) => {
+ if (err) { return next(err); }
+ if (existingUser) {
+ req.flash("errors", "Account with that email address already exists.");
+ return res.redirect("/signup");
+ }
+ user.save((err) => {
+ if (err) { return next(err); }
+ req.logIn(user, (err) => {
+ if (err) {
+ return next(err);
+ }
+ res.redirect("/");
+ });
+ });
+ });
+};
+
+
+/**
+ * GET /login
+ * Login page.
+ */
+export let getLogin = (req: Request, res: Response) => {
+ if (req.user) {
+ return res.redirect("/");
+ }
+ res.send("<p>dear lord please render</p>");
+ // res.render("account/login", {
+ // title: "Login"
+ // });
+};
+
+/**
+ * POST /login
+ * Sign in using email and password.
+ */
+export let postLogin = (req: Request, res: Response, next: NextFunction) => {
+ req.assert("email", "Email is not valid").isEmail();
+ req.assert("password", "Password cannot be blank").notEmpty();
+ req.sanitize("email").normalizeEmail({ gmail_remove_dots: false });
+
+ const errors = req.validationErrors();
+
+ if (errors) {
+ req.flash("errors", "Unable to login at this time. Please try again.");
+ return res.redirect("/login");
+ }
+
+ passport.authenticate("local", (err: Error, user: UserModel, info: IVerifyOptions) => {
+ if (err) { return next(err); }
+ if (!user) {
+ req.flash("errors", info.message);
+ return res.redirect("/login");
+ }
+ req.logIn(user, (err) => {
+ if (err) { return next(err); }
+ req.flash("success", "Success! You are logged in.");
+ res.redirect("/");
+ });
+ })(req, res, next);
+}; \ No newline at end of file
diff --git a/src/server/authentication/models/User.ts b/src/server/authentication/models/User.ts
new file mode 100644
index 000000000..9752c4260
--- /dev/null
+++ b/src/server/authentication/models/User.ts
@@ -0,0 +1,89 @@
+//@ts-ignore
+import * as bcrypt from "bcrypt-nodejs";
+import * as crypto from "crypto";
+//@ts-ignore
+import * as mongoose from "mongoose";
+var url = 'mongodb://localhost:27017/Dash'
+
+mongoose.connect(url, { useNewUrlParser: true });
+
+mongoose.connection.on('connected', function () {
+ console.log('Stablished connection on ' + url);
+});
+mongoose.connection.on('error', function (error) {
+ console.log('Something wrong happened: ' + error);
+});
+mongoose.connection.on('disconnected', function () {
+ console.log('connection closed');
+});
+export type UserModel = mongoose.Document & {
+ email: string,
+ password: string,
+ passwordResetToken: string,
+ passwordResetExpires: Date,
+ tokens: AuthToken[],
+
+ profile: {
+ name: string,
+ gender: string,
+ location: string,
+ website: string,
+ picture: string
+ },
+
+ comparePassword: comparePasswordFunction,
+};
+
+type comparePasswordFunction = (candidatePassword: string, cb: (err: any, isMatch: any) => {}) => void;
+
+export type AuthToken = {
+ accessToken: string,
+ kind: string
+};
+
+const userSchema = new mongoose.Schema({
+ email: { type: String, unique: true },
+ password: String,
+ passwordResetToken: String,
+ passwordResetExpires: Date,
+
+ facebook: String,
+ twitter: String,
+ google: String,
+ tokens: Array,
+
+ profile: {
+ name: String,
+ gender: String,
+ location: String,
+ website: String,
+ picture: String
+ }
+}, { timestamps: true });
+
+/**
+ * Password hash middleware.
+ */
+userSchema.pre("save", function save(next) {
+ const user = this as UserModel;
+ if (!user.isModified("password")) { return next(); }
+ bcrypt.genSalt(10, (err, salt) => {
+ if (err) { return next(err); }
+ bcrypt.hash(user.password, salt, () => void {}, (err: mongoose.Error, hash) => {
+ if (err) { return next(err); }
+ user.password = hash;
+ next();
+ });
+ });
+});
+
+const comparePassword: comparePasswordFunction = function (this: UserModel, candidatePassword, cb) {
+ bcrypt.compare(candidatePassword, this.password, (err: mongoose.Error, isMatch: boolean) => {
+ cb(err, isMatch);
+ });
+};
+
+userSchema.methods.comparePassword = comparePassword;
+
+const User = mongoose.model("User", userSchema);
+export default User; \ No newline at end of file
diff --git a/src/server/database.ts b/src/server/database.ts
new file mode 100644
index 000000000..07c5819ab
--- /dev/null
+++ b/src/server/database.ts
@@ -0,0 +1,81 @@
+import { action, configure } from 'mobx';
+import * as mongodb from 'mongodb';
+import { ObjectID } from 'mongodb';
+import { Transferable } from './Message';
+import { Utils } from '../Utils';
+
+export class Database {
+ public static Instance = new Database()
+ private MongoClient = mongodb.MongoClient;
+ private url = 'mongodb://localhost:27017/Dash';
+ private db?: mongodb.Db;
+
+ constructor() {
+ this.MongoClient.connect(this.url, (err, client) => {
+ this.db = client.db()
+ })
+ }
+
+ public update(id: string, value: any) {
+ if (this.db) {
+ let collection = this.db.collection('documents');
+ collection.update({ _id: id }, { $set: value }, {
+ upsert: true
+ });
+ }
+ }
+
+ public delete(id: string) {
+ if (this.db) {
+ let collection = this.db.collection('documents');
+ collection.remove({ _id: id });
+ }
+ }
+
+ public deleteAll() {
+ if (this.db) {
+ let collection = this.db.collection('documents');
+ collection.deleteMany({});
+ }
+ }
+
+ public insert(kvpairs: any) {
+ if (this.db) {
+ let collection = this.db.collection('documents');
+ collection.insertOne(kvpairs, (err: any, res: any) => {
+ if (err) {
+ // console.log(err)
+ return
+ }
+ });
+ }
+ }
+
+ public getDocument(id: string, fn: (res: any) => void) {
+ var result: JSON;
+ if (this.db) {
+ let collection = this.db.collection('documents');
+ collection.findOne({ _id: id }, (err: any, res: any) => {
+ result = res
+ if (!result) {
+ fn(undefined)
+ }
+ fn(result)
+ })
+ };
+ }
+
+ public getDocuments(ids: string[], fn: (res: any) => void) {
+ if (this.db) {
+ let collection = this.db.collection('documents');
+ let cursor = collection.find({ _id: { "$in": ids } })
+ cursor.toArray((err, docs) => {
+ fn(docs);
+ })
+ };
+ }
+
+ public print() {
+ console.log("db says hi!")
+ }
+}
diff --git a/src/server/index.js b/src/server/index.js
deleted file mode 100644
index 15e763f9d..000000000
--- a/src/server/index.js
+++ /dev/null
@@ -1,13 +0,0 @@
-"use strict";
-exports.__esModule = true;
-var express = require("express");
-var app = express();
-var port = 8080; // default port to listen
-// define a route handler for the default home page
-app.get("/", function (req, res) {
- res.send("Hello world!");
-});
-// start the Express server
-app.listen(port, function () {
- console.log("server started at http://localhost:" + port);
-});
diff --git a/src/server/index.ts b/src/server/index.ts
index 640ad8180..eb0527ee7 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -4,9 +4,77 @@ import * as webpack from 'webpack'
import * as wdm from 'webpack-dev-middleware';
import * as whm from 'webpack-hot-middleware';
import * as path from 'path'
-const config = require('../../webpack.config')
-const compiler = webpack(config)
+import * as passport from 'passport';
+import { MessageStore, Message, SetFieldArgs, GetFieldArgs, Transferable } from "./Message";
+import { Client } from './Client';
+import { Socket } from 'socket.io';
+import { Utils } from '../Utils';
+import { ObservableMap } from 'mobx';
+import { FieldId, Field } from '../fields/Field';
+import { Database } from './database';
+import { ServerUtils } from './ServerUtil';
+import { ObjectID } from 'mongodb';
+import { Document } from '../fields/Document';
+import * as io from 'socket.io'
+import * as passportConfig from './authentication/config/passport';
+import { getLogin, postLogin, getSignup, postSignup } from './authentication/controllers/user';
+const config = require('../../webpack.config');
+const compiler = webpack(config);
const port = 1050; // default port to listen
+const serverPort = 1234;
+import * as expressValidator from 'express-validator';
+import expressFlash = require('express-flash');
+import * as bodyParser from 'body-parser';
+import * as session from 'express-session';
+import c = require("crypto");
+const MongoStore = require('connect-mongo')(session);
+const mongoose = require('mongoose');
+const bluebird = require('bluebird');
+import { performance } from 'perf_hooks'
+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.Promise = bluebird;
+mongoose.connect(mongoUrl)//.then(
+// () => { /** ready to use. The `mongoose.connect()` promise resolves to undefined. */ },
+// ).catch((err: any) => {
+// console.log("MongoDB connection error. Please make sure MongoDB is running. " + err);
+// process.exit();
+// });
+mongoose.connection.on('connected', function () {
+ console.log("connected");
+})
+
+app.use(bodyParser.json());
+app.use(bodyParser.urlencoded({ extended: true }));
+app.use(expressValidator());
+app.use(expressFlash());
+app.use(require('express-session')({
+ secret: `${c.randomBytes(64)}`,
+ resave: true,
+ saveUninitialized: true,
+ store: new MongoStore({
+ url: 'mongodb://localhost:27017/Dash'
+ })
+}));
+app.use(passport.initialize());
+app.use(passport.session());
+app.use((req, res, next) => {
+ res.locals.user = req.user;
+ next();
+});
+
+app.get("/signup", getSignup);
+app.post("/signup", postSignup);
+app.get("/login", getLogin);
+app.post("/login", postLogin);
+
+let FieldStore: ObservableMap<FieldId, Field> = new ObservableMap();
// define a route handler for the default home page
app.get("/", (req, res) => {
@@ -17,6 +85,11 @@ app.get("/hello", (req, res) => {
res.send("<p>Hello</p>");
})
+app.get("/delete", (req, res) => {
+ deleteAll();
+ res.redirect("/");
+});
+
app.use(wdm(compiler, {
publicPath: config.output.publicPath
}))
@@ -26,4 +99,58 @@ app.use(whm(compiler))
// start the Express server
app.listen(port, () => {
console.log(`server started at http://localhost:${port}`);
-}); \ No newline at end of file
+})
+
+const server = io();
+interface Map {
+ [key: string]: Client;
+}
+let clients: Map = {}
+
+server.on("connection", function (socket: Socket) {
+ console.log("a user has connected")
+
+ Utils.Emit(socket, MessageStore.Foo, "handshooken")
+
+ Utils.AddServerHandler(socket, MessageStore.Bar, barReceived)
+ Utils.AddServerHandler(socket, MessageStore.SetField, (args) => setField(socket, args))
+ Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField)
+ Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields)
+ Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteAll)
+})
+
+function deleteAll() {
+ Database.Instance.deleteAll();
+}
+
+function barReceived(guid: String) {
+ clients[guid.toString()] = new Client(guid.toString());
+ // Database.Instance.print()
+}
+
+function addDocument(document: Document) {
+
+}
+
+function getField([id, callback]: [string, (result: any) => void]) {
+ Database.Instance.getDocument(id, (result: any) => {
+ if (result) {
+ callback(result)
+ }
+ else {
+ callback(undefined)
+ }
+ })
+}
+
+function getFields([ids, callback]: [string[], (result: any) => void]) {
+ Database.Instance.getDocuments(ids, callback);
+}
+
+function setField(socket: Socket, newValue: Transferable) {
+ Database.Instance.update(newValue._id, newValue)
+ socket.broadcast.emit(MessageStore.SetField.Message, newValue)
+}
+
+server.listen(serverPort);
+console.log(`listening on port ${serverPort}`); \ No newline at end of file