diff options
Diffstat (limited to 'src')
61 files changed, 3346 insertions, 1093 deletions
diff --git a/src/.DS_Store b/src/.DS_Store Binary files differnew file mode 100644 index 000000000..4d6acb95a --- /dev/null +++ b/src/.DS_Store diff --git a/src/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 f954b37a8..41f1c9b3f 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1,18 +1,22 @@ 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 { WebField } from "../../fields/WebField"; +import { WebBox } from "../views/nodes/WebBox"; +import { CollectionView, CollectionViewType } from "../views/collections/CollectionView"; +import { HtmlField } from "../../fields/HtmlField"; +import { Key } from "../../fields/Key" +import { Field } from "../../fields/Field"; +import { KeyValuePane } from "../views/nodes/KeyValuePane" +import { KVPField } from "../../fields/KVPField"; -interface DocumentOptions { +export interface DocumentOptions { x?: number; y?: number; width?: number; @@ -20,158 +24,139 @@ interface DocumentOptions { nativeWidth?: number; nativeHeight?: number; title?: string; + panx?: number; + pany?: number; + scale?: number; + layout?: string; + layoutKeys?: Key[]; + viewType?: number; } export namespace Documents { - function setupOptions(doc: Document, options: DocumentOptions): void { - if (options.x) { - doc.SetData(KeyStore.X, options.x, NumberField); - } - if (options.y) { - doc.SetData(KeyStore.Y, options.y, NumberField); - } - if (options.width) { - doc.SetData(KeyStore.Width, options.width, NumberField); - } - if (options.height) { - doc.SetData(KeyStore.Height, options.height, NumberField); - } - if (options.nativeWidth) { - doc.SetData(KeyStore.NativeWidth, options.nativeWidth, NumberField); - } - if (options.nativeHeight) { - doc.SetData(KeyStore.NativeHeight, options.nativeHeight, NumberField); - } - if (options.title) { - doc.SetData(KeyStore.Title, options.title, TextField); - } - doc.SetData(KeyStore.Scale, 1, NumberField); - doc.SetData(KeyStore.PanX, 0, NumberField); - doc.SetData(KeyStore.PanY, 0, NumberField); - } - let textProto: Document; - function GetTextPrototype(): Document { - if (!textProto) { - textProto = new Document(); - textProto.Set(KeyStore.X, new NumberField(0)); - textProto.Set(KeyStore.Y, new NumberField(0)); - textProto.Set(KeyStore.Width, new NumberField(300)); - textProto.Set(KeyStore.Height, new NumberField(150)); - textProto.Set(KeyStore.Layout, new TextField(FormattedTextBox.LayoutString())); - textProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data])); - } - return textProto; - } + let imageProto: Document; + let webProto: Document; + let collProto: Document; + let kvpProto: Document; + const textProtoId = "textProto"; + const imageProtoId = "imageProto"; + const webProtoId = "webProto"; + const collProtoId = "collectionProto"; + const kvpProtoId = "kvpProto"; - export function TextDocument(options: DocumentOptions = {}): Document { - let doc = GetTextPrototype().MakeDelegate(); - setupOptions(doc, options); - // doc.SetField(KeyStore.Data, new RichTextField()); - 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])); - } - return schemaProto; + export function initProtos(mainDocId: string, callback: (mainDoc?: Document) => void) { + Server.GetFields([collProtoId, textProtoId, imageProtoId, mainDocId], (fields) => { + collProto = fields[collProtoId] as Document; + imageProto = fields[imageProtoId] as Document; + textProto = fields[textProtoId] as Document; + webProto = fields[webProtoId] as Document; + kvpProto = fields[kvpProtoId] as Document; + callback(fields[mainDocId] as Document) + }); } - - export function SchemaDocument(documents: Array<Document>, options: DocumentOptions = {}): Document { - let doc = GetSchemaPrototype().MakeDelegate(); - setupOptions(doc, options); - doc.Set(KeyStore.Data, new ListField(documents)); + function assignOptions(doc: Document, options: DocumentOptions): Document { + if (options.x !== undefined) { doc.SetNumber(KeyStore.X, options.x); } + if (options.y !== undefined) { doc.SetNumber(KeyStore.Y, options.y); } + if (options.width !== undefined) { doc.SetNumber(KeyStore.Width, options.width); } + if (options.height !== undefined) { doc.SetNumber(KeyStore.Height, options.height); } + if (options.nativeWidth !== undefined) { doc.SetNumber(KeyStore.NativeWidth, options.nativeWidth); } + if (options.nativeHeight !== undefined) { doc.SetNumber(KeyStore.NativeHeight, options.nativeHeight); } + if (options.title !== undefined) { doc.SetText(KeyStore.Title, options.title); } + if (options.panx !== undefined) { doc.SetNumber(KeyStore.PanX, options.panx); } + if (options.pany !== undefined) { doc.SetNumber(KeyStore.PanY, options.pany); } + if (options.scale !== undefined) { doc.SetNumber(KeyStore.Scale, options.scale); } + if (options.viewType !== undefined) { doc.SetNumber(KeyStore.ViewType, options.viewType); } + if (options.layout !== undefined) { doc.SetText(KeyStore.Layout, options.layout); } + if (options.layoutKeys !== undefined) { doc.Set(KeyStore.LayoutKeys, new ListField(options.layoutKeys)); } return doc; } - - - 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; + function setupPrototypeOptions(protoId: string, title: string, layout: string, options: DocumentOptions): Document { + return assignOptions(new Document(protoId), { ...options, title: title, layout: layout }); } - - export function DockDocument(config: string, options: DocumentOptions = {}): Document { - let doc = GetDockPrototype().MakeDelegate(); - setupOptions(doc, options); - doc.SetText(KeyStore.Data, config); - return doc; + function SetInstanceOptions<T, U extends Field & { Data: T }>(doc: Document, options: DocumentOptions, value: T, ctor: { new(): U }, id?: string) { + var deleg = doc.MakeDelegate(id); + deleg.SetData(KeyStore.Data, value, ctor); + return assignOptions(deleg, options); } - - let imageProtoId: FieldId; function GetImagePrototype(): Document { - if (imageProtoId === undefined) { - let imageProto = new Document(); - imageProtoId = imageProto.Id; - 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.LayoutString("AnnotationsKey"))); - 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; + if (!imageProto) { + imageProto = setupPrototypeOptions(imageProtoId, "IMAGE_PROTO", CollectionView.LayoutString("AnnotationsKey"), + { x: 0, y: 0, nativeWidth: 300, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations] }); + imageProto.SetText(KeyStore.BackgroundLayout, ImageBox.LayoutString()); } - return Server.GetField(imageProtoId) as Document; + return imageProto; } - - 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; + function GetTextPrototype(): Document { + return textProto ? textProto : + textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(), + { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] }); + } + function GetWebPrototype(): Document { + return webProto ? webProto : + webProto = setupPrototypeOptions(webProtoId, "WEB_PROTO", WebBox.LayoutString(), + { x: 0, y: 0, width: 300, height: 300, layoutKeys: [KeyStore.Data] }); } - - let collectionProto: Document; function GetCollectionPrototype(): Document { - if (!collectionProto) { - collectionProto = new Document(); - collectionProto.Set(KeyStore.X, new NumberField(0)); - collectionProto.Set(KeyStore.Y, new NumberField(0)); - 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("DataKey"))); - collectionProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data])); - } - return collectionProto; + return collProto ? collProto : + collProto = setupPrototypeOptions(collProtoId, "COLLECTION_PROTO", CollectionView.LayoutString("DataKey"), + { panx: 0, pany: 0, scale: 1, layoutKeys: [KeyStore.Data] }); + } + + function GetKVPPrototype(): Document { + return kvpProto ? kvpProto : + kvpProto = setupPrototypeOptions(kvpProtoId, "KVP_PROTO", KeyValuePane.LayoutString(), + { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] }) } - export function CollectionDocument(documents: Array<Document>, options: DocumentOptions = {}): Document { - let doc = GetCollectionPrototype().MakeDelegate(); - Server.AddDocument(doc); - setupOptions(doc, options); - doc.Set(KeyStore.Data, new ListField(documents)); + export function ImageDocument(url: string, options: DocumentOptions = {}) { + let doc = SetInstanceOptions(GetImagePrototype(), { ...options, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] }, + new URL(url), ImageField); + doc.SetText(KeyStore.Caption, "my caption..."); + doc.SetText(KeyStore.BackgroundLayout, EmbeddedCaption()); + doc.SetText(KeyStore.OverlayLayout, FixedCaption()); return doc; } + export function TextDocument(options: DocumentOptions = {}) { + return SetInstanceOptions(GetTextPrototype(), options, "", TextField); + } + export function WebDocument(url: string, options: DocumentOptions = {}) { + return SetInstanceOptions(GetWebPrototype(), options, new URL(url), WebField); + } + export function HtmlDocument(html: string, options: DocumentOptions = {}) { + return SetInstanceOptions(GetWebPrototype(), options, html, HtmlField); + } + export function FreeformDocument(documents: Array<Document>, options: DocumentOptions, id?: string) { + return SetInstanceOptions(GetCollectionPrototype(), { ...options, viewType: CollectionViewType.Freeform }, documents, ListField, id) + } + export function SchemaDocument(documents: Array<Document>, options: DocumentOptions, id?: string) { + return SetInstanceOptions(GetCollectionPrototype(), { ...options, viewType: CollectionViewType.Schema }, documents, ListField, id) + } + export function DockDocument(config: string, options: DocumentOptions, id?: string) { + return SetInstanceOptions(GetCollectionPrototype(), { ...options, viewType: CollectionViewType.Docking }, config, TextField, id) + } + export function KVPDocument(document: Document, options: DocumentOptions = {}, id?: string) { + var deleg = GetKVPPrototype().MakeDelegate(id); + deleg.Set(KeyStore.Data, document); + return assignOptions(deleg, options); + } + + + + // 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; 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%; text-align:center;bottom:0;">` + + FormattedTextBox.LayoutString("CaptionKey") + + `</div> + </div>` }; }
\ No newline at end of file diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 337ec855a..60910a40b 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,3 +1,37 @@ +import { DocumentDecorations } from "../views/DocumentDecorations"; +import { CollectionDockingView } from "../views/collections/CollectionDockingView"; +import { Document } from "../../fields/Document" +import { action } from "mobx"; +import { DocumentView } from "../views/nodes/DocumentView"; + +export function setupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc: () => Document) { + let onRowMove = action((e: PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + + document.removeEventListener("pointermove", onRowMove); + document.removeEventListener('pointerup', onRowUp); + DragManager.StartDrag(_reference.current!, { document: docFunc() }); + }); + let onRowUp = action((e: PointerEvent): void => { + document.removeEventListener("pointermove", onRowMove); + document.removeEventListener('pointerup', onRowUp); + }); + let onItemDown = (e: React.PointerEvent) => { + // if (this.props.isSelected() || this.props.isTopMost) { + if (e.button == 0) { + e.stopPropagation(); + if (e.shiftKey) { + CollectionDockingView.Instance.StartOtherDrag(docFunc(), e); + } else { + document.addEventListener("pointermove", onRowMove); + document.addEventListener('pointerup', onRowUp); + } + } + //} + } + return onItemDown; +} export namespace DragManager { export function Root() { @@ -43,6 +77,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 +94,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,6 +108,8 @@ export namespace DragManager { let dragElement = ele.cloneNode(true) as HTMLElement; dragElement.style.opacity = "0.7"; dragElement.style.position = "absolute"; + dragElement.style.bottom = ""; + dragElement.style.left = ""; dragElement.style.transformOrigin = "0 0"; dragElement.style.zIndex = "1000"; dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`; @@ -87,38 +122,54 @@ export namespace DragManager { dragDiv.appendChild(dragElement); let hideSource = false; - if (typeof options.hideSource === "boolean") { - hideSource = options.hideSource; - } else { - hideSource = options.hideSource(); + if (options) { + if (typeof options.hideSource === "boolean") { + hideSource = options.hideSource; + } else { + hideSource = options.hideSource(); + } } const wasHidden = ele.hidden; if (hideSource) { ele.hidden = true; } - const moveHandler = (e: PointerEvent) => { e.stopPropagation(); e.preventDefault(); x += e.movementX; y += e.movementY; + if (e.shiftKey) { + abortDrag(); + const docView: DocumentView = dragData["documentView"]; + const doc: Document = docView ? docView.props.Document : dragData["document"]; + CollectionDockingView.Instance.StartOtherDrag(doc, { pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 }); + } dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`; }; - const upHandler = (e: PointerEvent) => { + + const abortDrag = () => { document.removeEventListener("pointermove", moveHandler, true); document.removeEventListener("pointerup", upHandler); - FinishDrag(dragElement, e, options, dragData); + dragDiv.removeChild(dragElement); if (hideSource && !wasHidden) { ele.hidden = false; } + } + const upHandler = (e: PointerEvent) => { + abortDrag(); + FinishDrag(ele, e, dragData, options); }; document.addEventListener("pointermove", moveHandler, true); document.addEventListener("pointerup", upHandler); } - function FinishDrag(dragEle: HTMLElement, e: PointerEvent, options: DragOptions, dragData: { [index: string]: any }) { - dragDiv.removeChild(dragEle); + function FinishDrag(dragEle: HTMLElement, e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions) { + let parent = dragEle.parentElement; + if (parent) + parent.removeChild(dragEle); const target = document.elementFromPoint(e.x, e.y); + if (parent) + parent.appendChild(dragEle); if (!target) { return; } @@ -130,6 +181,9 @@ export namespace DragManager { data: dragData } })); - options.handlers.dragComplete({}); + if (options) { + options.handlers.dragComplete({}); + } + DocumentDecorations.Instance.Hidden = false; } }
\ No newline at end of file diff --git a/src/client/util/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/Transform.ts b/src/client/util/Transform.ts index 9fd4f7bef..3e1039166 100644 --- a/src/client/util/Transform.ts +++ b/src/client/util/Transform.ts @@ -102,6 +102,12 @@ export class Transform { return [x * this._scale, y * this._scale]; } + transformBounds(x: number, y: number, width: number, height: number): { x: number, y: number, width: number, height: number } { + [x, y] = this.transformPoint(x, y); + [width, height] = this.transformDirection(width, height); + return { x, y, width, height }; + } + inverse = () => { return new Transform(-this._translateX / this._scale, -this._translateY / this._scale, 1 / this._scale) } diff --git a/src/client/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 d385bcdef..975a125f7 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,10 +1,9 @@ -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/Key' +import { KeyStore } from '../../fields/KeyStore' import { NumberField } from "../../fields/NumberField"; @observer @@ -12,7 +11,7 @@ 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) @@ -22,17 +21,13 @@ 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; } - let transform = element.props.ScreenToLocalTransform().inverse(); + let transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse(); var [sptX, sptY] = transform.transformPoint(0, 0); - // var [bptX, bptY] = transform.transformDirection(element.width, element.height); - let doc = element.props.Document; - let [bptX, bptY] = [doc.GetNumber(KeyStore.Width, 0), doc.GetNumber(KeyStore.Height, 0)]; - [bptX, bptY] = transform.transformPoint(bptX, bptY); + let [bptX, bptY] = transform.transformPoint(documentView.props.PanelWidth(), documentView.props.PanelHeight()); return { x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y), r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b) @@ -40,14 +35,10 @@ export class DocumentDecorations extends React.Component { }, { 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(); @@ -110,33 +101,29 @@ export class DocumentDecorations extends React.Component { } SelectionManager.SelectedDocuments().forEach(element => { - 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; - // } + const rect = element.screenRect(); if (rect.width !== 0) { let doc = element.props.Document; - let width = doc.GetOrCreate(KeyStore.Width, NumberField); - let height = doc.GetOrCreate(KeyStore.Height, NumberField); + let width = doc.GetNumber(KeyStore.Width, 0); + let nwidth = doc.GetNumber(KeyStore.NativeWidth, 0); + let nheight = doc.GetNumber(KeyStore.NativeHeight, 0); + let height = doc.GetNumber(KeyStore.Height, nwidth ? nheight / nwidth * width : 0); let x = doc.GetOrCreate(KeyStore.X, NumberField); - let y = doc.GetOrCreate(KeyStore.X, NumberField); - let scale = width.Data / rect.width; - let actualdW = Math.max(width.Data + (dW * scale), 20); - let actualdH = Math.max(height.Data + (dH * scale), 20); - x.Data += dX * (actualdW - width.Data); - y.Data += dY * (actualdH - height.Data); - if (Math.abs(dW) > Math.abs(dH)) - width.Data = actualdW; - else - height.Data = actualdH; + 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); } }) } @@ -153,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> @@ -170,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 3d1c2ebf4..8d9a47eaa 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -30,7 +30,7 @@ export class EditableView extends React.Component<EditableProps> { style={{ width: "100%" }}></input> } else { return ( - <div style={{ alignItems: "center", display: "flex", height: "100%", maxHeight: `${this.props.height}` }} + <div className="editableView-container-editing" style={{ display: "flex", height: "100%", maxHeight: `${this.props.height}` }} onClick={action(() => this.editing = true)} > {this.props.contents} diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index e73f62904..4334ed299 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -28,4 +28,24 @@ h1 { p { margin: 0px; padding: 0px; -}
\ No newline at end of file +} +::-webkit-scrollbar { + -webkit-appearance: none; + height:5px; + width:5px; +} +::-webkit-scrollbar-thumb { + border-radius: 2px; + background-color: rgba(0,0,0,.5); +} + +.main-buttonDiv { + position: absolute; + width: 150px; + left: 0px; +} +.main-undoButtons { + position: absolute; + width: 150px; + right: 0px; +} diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index a86fdb9b0..16e95ad82 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -2,33 +2,25 @@ import { action, configure } 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 "./Main.scss"; -import { ContextMenu } from './ContextMenu'; -import { DocumentView } from './nodes/DocumentView'; -import { ImageField } from '../../fields/ImageField'; +import { KeyStore } from '../../fields/KeyStore'; +import { DocumentTransfer, MessageStore } from '../../server/Message'; +import { Utils } from '../../Utils'; +import { Documents } from '../documents/Documents'; +import { Server } from '../Server'; +import { setupDrag } from '../util/DragManager'; import { Transform } from '../util/Transform'; +import { UndoManager } from '../util/UndoManager'; import { CollectionDockingView } from './collections/CollectionDockingView'; +import { ContextMenu } from './ContextMenu'; +import { DocumentDecorations } from './DocumentDecorations'; +import { DocumentView } from './nodes/DocumentView'; +import "./Main.scss"; -configure({ - enforceActions: "observed" -}); - -const mainNodeCollection = new Array<Document>(); - -window.addEventListener("drop", function (e) { - e.preventDefault(); -}, false) -window.addEventListener("dragover", function (e) { - e.preventDefault(); -}, false) +configure({ enforceActions: "observed" }); // causes errors to be generated when modifying an observable outside of an action +window.addEventListener("drop", (e) => e.preventDefault(), false) +window.addEventListener("dragover", (e) => e.preventDefault(), false) document.addEventListener("pointerdown", action(function (e: PointerEvent) { if (!ContextMenu.Instance.intersects(e.pageX, e.pageY)) { ContextMenu.Instance.clearItems() @@ -36,50 +28,74 @@ document.addEventListener("pointerdown", action(function (e: PointerEvent) { }), true) -let doc1 = Documents.TextDocument({ title: "hello", width: 400, height: 300 }); -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: "dog", 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.push(doc3); -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 = [doc3, doc4, doc2]; -let doc6 = Documents.CollectionDocument(docset2, { - x: 350, y: 100, width: 600, height: 600, title: "docking collection" -}); +const mainDocId = "mainDoc"; +let mainContainer: Document; +let mainfreeform: Document; +console.log("HELLO WORLD") +Documents.initProtos(mainDocId, (res?: Document) => { + console.log("Response => " + JSON.stringify(res as Document)) + if (res instanceof Document) { + mainContainer = res; + 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())) -var docs = [doc4, doc3, doc6].map(doc => CollectionDockingView.makeDocumentConfig(doc)); -var config = { - settings: { selectionEnabled: false }, content: [{ type: 'row', content: docs }] -}; -let mainContainer = Documents.DockDocument(JSON.stringify(config), { - x: 0, y: 0, title: "main container" -}) + // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) + setTimeout(() => { + mainfreeform = Documents.FreeformDocument([], { x: 0, y: 400, title: "mini collection" }); + Utils.Emit(Server.Socket, MessageStore.AddDocument, new DocumentTransfer(mainfreeform.ToJson())); + + var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(mainfreeform)] }] }; + mainContainer.SetText(KeyStore.Data, JSON.stringify(dockingLayout)); + mainContainer.Set(KeyStore.ActiveFrame, mainfreeform); + }, 0); + } + + let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; + let weburl = "https://cs.brown.edu/courses/cs166/"; + let clearDatabase = action(() => Utils.Emit(Server.Socket, MessageStore.DeleteAll, {})) + let addTextNode = action(() => Documents.TextDocument({ width: 200, height: 200, title: "a text note" })) + let addColNode = action(() => Documents.FreeformDocument([], { width: 200, height: 200, title: "a feeform collection" })); + let addSchemaNode = action(() => Documents.SchemaDocument([Documents.TextDocument()], { width: 200, height: 200, title: "a schema collection" })); + let addImageNode = action(() => Documents.ImageDocument(imgurl, { width: 200, height: 200, title: "an image of a cat" })); + let addWebNode = action(() => Documents.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" })); + + let addClick = (creator: () => Document) => action(() => mainfreeform.GetList<Document>(KeyStore.Data, []).push(creator())); -ReactDOM.render(( - <div style={{ position: "absolute", width: "100%", height: "100%" }}> - <DocumentView Document={mainContainer} - AddDocument={undefined} RemoveDocument={undefined} ScreenToLocalTransform={() => Transform.Identity} - Scaling={1} - isTopMost={true} - ContainingCollectionView={undefined} /> - <DocumentDecorations /> - <ContextMenu /> - </div>), - document.getElementById('root'));
\ No newline at end of file + let imgRef = React.createRef<HTMLDivElement>(); + let webRef = React.createRef<HTMLDivElement>(); + let textRef = React.createRef<HTMLDivElement>(); + let schemaRef = React.createRef<HTMLDivElement>(); + let colRef = React.createRef<HTMLDivElement>(); + + 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} + SelectOnLoad={false} + ContainingCollectionView={undefined} /> + <DocumentDecorations /> + <ContextMenu /> + <div className="main-buttonDiv" style={{ bottom: '0px' }} ref={imgRef} > + <button onPointerDown={setupDrag(imgRef, addImageNode)} onClick={addClick(addImageNode)}>Add Image</button></div> + <div className="main-buttonDiv" style={{ bottom: '25px' }} ref={webRef} > + <button onPointerDown={setupDrag(webRef, addWebNode)} onClick={addClick(addWebNode)}>Add Web</button></div> + <div className="main-buttonDiv" style={{ bottom: '50px' }} ref={textRef}> + <button onPointerDown={setupDrag(textRef, addTextNode)} onClick={addClick(addTextNode)}>Add Text</button></div> + <div className="main-buttonDiv" style={{ bottom: '75px' }} ref={colRef}> + <button onPointerDown={setupDrag(colRef, addColNode)} onClick={addClick(addColNode)}>Add Collection</button></div> + <div className="main-buttonDiv" style={{ bottom: '100px' }} ref={schemaRef}> + <button onPointerDown={setupDrag(schemaRef, addSchemaNode)} onClick={addClick(addSchemaNode)}>Add Schema</button></div> + <div className="main-buttonDiv" style={{ bottom: '125px' }} > + <button onClick={clearDatabase}>Clear Database</button></div> + <button className="main-undoButtons" style={{ bottom: '25px' }} onClick={() => UndoManager.Undo()}>Undo</button> + <button className="main-undoButtons" style={{ bottom: '0px' }} onClick={() => UndoManager.Redo()}>Redo</button> + </div>), + document.getElementById('root')); +}) 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 885a4bece..915e33533 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,106 +1,89 @@ 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, reaction, observable } from "mobx"; +import { action, computed, observable, reaction } from "mobx"; import { observer } from "mobx-react"; -import { Document } from "../../../fields/Document"; -import { KeyStore } from "../../../fields/Key"; -import { ListField } from "../../../fields/ListField"; -import { DragManager } from "../../util/DragManager"; -import { DocumentView } from "../nodes/DocumentView"; -import "./CollectionDockingView.scss"; -import { CollectionViewBase, CollectionViewProps, COLLECTION_BORDER_WIDTH } from "./CollectionViewBase"; -import React = require("react"); import * as ReactDOM from 'react-dom'; import Measure from "react-measure"; +import { Document } from "../../../fields/Document"; +import { FieldId, Opt, Field } from "../../../fields/Field"; +import { KeyStore } from "../../../fields/KeyStore"; import { Utils } from "../../../Utils"; -import { FieldId } from "../../../fields/Field"; import { Server } from "../../Server"; +import { undoBatch } from "../../util/UndoManager"; +import { DocumentView } from "../nodes/DocumentView"; +import "./CollectionDockingView.scss"; +import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; +import React = require("react"); +import { SubCollectionViewProps } from "./CollectionViewBase"; @observer -export class CollectionDockingView extends CollectionViewBase { - +export class CollectionDockingView extends React.Component<SubCollectionViewProps> { public static Instance: CollectionDockingView; - public static LayoutString() { return CollectionViewBase.LayoutString("CollectionDockingView"); } public static makeDocumentConfig(document: Document) { return { type: 'react-component', component: 'DocumentFrameRenderer', title: document.Title, props: { - documentId: document.Id + documentId: document.Id, + //collectionDockingView: CollectionDockingView.Instance } } } private _goldenLayout: any = null; - private _dragDiv: any = null; - private _dragParent: HTMLElement | null = null; - private _dragElement: HTMLDivElement | undefined; - private _dragFakeElement: HTMLDivElement | undefined; private _containerRef = React.createRef<HTMLDivElement>(); - private _makeFullScreen: boolean = false; - private _maximizedStack: any = null; + private _fullScreen: any = null; - constructor(props: CollectionViewProps) { + constructor(props: SubCollectionViewProps) { super(props); CollectionDockingView.Instance = this; (window as any).React = React; (window as any).ReactDOM = ReactDOM; } - - public StartOtherDrag(dragElement: HTMLDivElement, dragDoc: Document) { - this._dragElement = dragElement; - this._dragParent = dragElement.parentElement; - // bcz: we want to copy this document into the header, not move it there. - // However, GoldenLayout is setup to move things, so we have to do some kludgy stuff: - - // - create a temporary invisible div and register that as a DragSource with GoldenLayout - this._dragDiv = document.createElement("div"); - this._dragDiv.style.opacity = 0; - DragManager.Root().appendChild(this._dragDiv); - this._goldenLayout.createDragSource(this._dragDiv, CollectionDockingView.makeDocumentConfig(dragDoc)); - - // - add our document to that div so that GoldenLayout will get the move events its listening for - this._dragDiv.appendChild(this._dragElement); - - // - add a duplicate of our document to the original document's container - // (GoldenLayout will be removing our original one) - this._dragFakeElement = dragElement.cloneNode(true) as HTMLDivElement; - this._dragParent!.appendChild(this._dragFakeElement); - - // all of this must be undone when the document has been dropped (see tabCreated) + public StartOtherDrag(dragDoc: Document, e: any) { + this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener.onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: e.button }) } + @action public OpenFullScreen(document: Document) { - this._makeFullScreen = true; - this._goldenLayout.root.contentItems[0].addChild(CollectionDockingView.makeDocumentConfig(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(); } + @action public CloseFullScreen() { - if (this._maximizedStack) { - this._maximizedStack.header.controlsContainer.find('.lm_close').click(); - this._maximizedStack = null; + 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 AddRightSplit(document: Document) { + @action + public AddRightSplit(document: Document, minimize: boolean = false) { + this._goldenLayout.emit('stateChanged'); let newItemStackConfig = { type: 'stack', content: [CollectionDockingView.makeDocumentConfig(document)] - }; - var newContentItem = new this._goldenLayout._typeToItem[newItemStackConfig.type](this._goldenLayout, newItemStackConfig, parent); + } - if (this._goldenLayout.root.contentItems[0].isRow) { - var rowlayout = this._goldenLayout.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 = this._goldenLayout.root.contentItems[0]; @@ -112,91 +95,130 @@ export class CollectionDockingView extends CollectionViewBase { collayout.config["width"] = 50; newContentItem.config["width"] = 50; - collayout.parent.callDownwards('setSize'); } + if (minimize) { + newContentItem.config["width"] = 10; + newContentItem.config["height"] = 10; + } + newContentItem.callDownwards('_$init'); + this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]); + this._goldenLayout.emit('stateChanged'); + this.stateChanged(); + return newContentItem; } - componentDidMount: () => void = () => { - if (this._containerRef.current) { - - this._goldenLayout = new GoldenLayout(JSON.parse(this.props.Document.GetText(KeyStore.Data, ""))); + setupGoldenLayout() { + var config = this.props.Document.GetText(KeyStore.Data, ""); + if (config) { + if (!this._goldenLayout) { + this._goldenLayout = new GoldenLayout(JSON.parse(config)); + } + 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)); + } + 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; + } + } 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.props.ContainingDocumentView!.MainContent.current; + var cur = this._containerRef.current; // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed this._goldenLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height); } + _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.active) { + if (e.button === 2 && this.props.active()) { e.stopPropagation(); e.preventDefault(); } else { - if (e.buttons === 1 && this.active) { + 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.setTitle(tab.contentItem.config.componentState.title); - tab.closeElement.off('click') //unbind the current click handler - .click(function () { - tab.contentItem.remove(); - }); - } + tab.closeElement.off('click') //unbind the current click handler + .click(function () { + tab.contentItem.remove(); + }); } stackCreated = (stack: any) => { - if (this._makeFullScreen) { - this._maximizedStack = stack; - setTimeout(function () { stack.header.controlsContainer.find('.lm_maximise').click() }, 10); - this._makeFullScreen = false; - } //stack.header.controlsContainer.find('.lm_popout').hide(); stack.header.controlsContainer.find('.lm_close') //get the close icon .off('click') //unbind the current click handler - .click(function () { + .click(action(function () { //if (confirm('really close this?')) { stack.remove(); //} - }); + })); } - render() { - const { fieldKey: fieldKey, Document: 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. - // tfs: we should be able to use this.props.ScreenToLocalTransform to get s right? - var s = this.props.ContainingDocumentView != undefined ? this.props.ContainingDocumentView!.ScalingToScreenSpace : 1; - var w = Document.GetNumber(KeyStore.Width, 0) / s; - var h = Document.GetNumber(KeyStore.Height, 0) / s; return ( <div className="collectiondockingview-container" id="menuContainer" - onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} ref={this._containerRef} + onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} style={{ width: "100%", height: "100%", @@ -209,45 +231,49 @@ export class CollectionDockingView extends CollectionViewBase { interface DockedFrameProps { documentId: FieldId, + //collectionDockingView: CollectionDockingView } @observer export class DockedFrameRenderer extends React.Component<DockedFrameProps> { - private _mainCont = React.createRef<HTMLDivElement>(); + @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)); } - @observable - private _parentScaling = 1; // used to transfer the dimensions of the content pane in the DOM to the ParentScaling prop of the DocumentView + 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); } - @computed - private get Document() { return Server.GetField(this.props.documentId, () => { }) as Document } + ScreenToLocalTransform = () => { + let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current!); + return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(scale / this._contentScaling()) + } render() { - let nativeWidth = this.Document.GetNumber(KeyStore.NativeWidth, 0); - var layout = this.Document.GetText(KeyStore.Layout, ""); + if (!this._document) + return (null); var content = - <div ref={this._mainCont}> - <DocumentView key={this.Document.Id} Document={this.Document} + <div className="collectionDockingView-content" ref={this._mainCont}> + <DocumentView key={this._document.Id} Document={this._document} AddDocument={undefined} RemoveDocument={undefined} - Scaling={this._parentScaling} - ScreenToLocalTransform={() => { - let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current!); - var props = CollectionDockingView.Instance.props; - return props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(scale / this._parentScaling) - }} + ContentScaling={this._contentScaling} + PanelWidth={this._nativeWidth} + PanelHeight={this._nativeHeight} + ScreenToLocalTransform={this.ScreenToLocalTransform} isTopMost={true} + SelectOnLoad={false} ContainingCollectionView={undefined} /> </div> - if (nativeWidth > 0 && - (layout.indexOf("CollectionFreeForm") == -1 || layout.indexOf("AnnotationsKey") != -1)) { // contents of documents should be scaled if document is not a freeform view, or if the freeformview is an annotation layer (presumably on a document that is not a freeformview) - return <Measure onResize={action((r: any) => this._parentScaling = nativeWidth > 0 ? r.entry.width / nativeWidth : 1)}> - {({ measureRef }) => <div ref={measureRef}> {content} </div>} - </Measure> - } - return content + 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..f432e8cc3 100644 --- a/src/client/views/collections/CollectionFreeFormView.scss +++ b/src/client/views/collections/CollectionFreeFormView.scss @@ -1,4 +1,10 @@ .collectionfreeformview-container { + + .collectionfreeformview > .jsx-parser{ + position:absolute; + height: 100%; + } + border-style: solid; box-sizing: border-box; position: relative; @@ -11,5 +17,25 @@ position: absolute; top: 0; left: 0; + width:100%; + height: 100% } +} + +.border { + border-style: solid; + box-sizing: border-box; + width: 100%; + height: 100%; +} + +//this is an animation for the blinking cursor! +@keyframes blink { + 0% {opacity: 0} + 49%{opacity: 0} + 50% {opacity: 1} +} + +#prevCursor { + animation: blink 1s infinite; }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx index 2c0a3f478..137bcf706 100644 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -1,90 +1,80 @@ +import { observable, 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 { CollectionView } from "../collections/CollectionView"; +import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; import { DocumentView } from "../nodes/DocumentView"; +import { FormattedTextBox } from "../nodes/FormattedTextBox"; +import { ImageBox } from "../nodes/ImageBox"; +import { WebBox } from "../nodes/WebBox"; +import "./CollectionFreeFormView.scss"; +import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; +import { CollectionViewBase } from "./CollectionViewBase"; +import { Documents } from "../../documents/Documents"; +import React = require("react"); +const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this? @observer export class CollectionFreeFormView extends CollectionViewBase { - public static LayoutString(fieldKey: string = "DataKey") { return CollectionViewBase.LayoutString("CollectionFreeFormView", fieldKey); } - private _containerRef = React.createRef<HTMLDivElement>(); private _canvasRef = React.createRef<HTMLDivElement>(); private _lastX: number = 0; private _lastY: number = 0; + private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type) + + @observable private _downX: number = 0; + @observable private _downY: number = 0; - constructor(props: CollectionViewProps) { - super(props); - } - - @computed - get isAnnotationOverlay() { return this.props.fieldKey == KeyStore.Annotations; } - - @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); } + //determines whether the blinking cursor for indicating whether a text will be made on key down is visible + @observable + private _previewCursorVisible: boolean = false; - @computed - get resizeScaling() { return this.isAnnotationOverlay ? this.props.Document.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: DocumentView = de.data["document"]; - var me = this; - 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; - //this should be able to use translate and scale methods on an Identity transform, no? - const transform = me.getTransform(); - const screenX = de.x - xOffset; - const screenY = de.y - yOffset; - const [x, y] = transform.transformPoint(screenX, screenY); - doc.props.Document.SetNumber(KeyStore.X, x); - doc.props.Document.SetNumber(KeyStore.Y, y); + 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); - e.stopPropagation(); - } - - componentDidMount() { - if (this._containerRef.current) { - DragManager.MakeDropTarget(this._containerRef.current, { - handlers: { - drop: this.drop - } - }); - } } @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); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointerup", this.onPointerUp); - this._downX = this._lastX = e.pageX; - this._downY = this._lastY = e.pageY; + this._lastX = e.pageX; + this._lastY = e.pageY; + this._downX = e.pageX; + this._downY = e.pageY; } } @@ -94,21 +84,24 @@ export class CollectionFreeFormView extends CollectionViewBase { document.removeEventListener("pointerup", this.onPointerUp); e.stopPropagation(); if (Math.abs(this._downX - e.clientX) < 3 && Math.abs(this._downY - e.clientY) < 3) { + //show preview text cursor on tap + this._previewCursorVisible = true; + //select is not already selected if (!this.props.isSelected()) { this.props.select(false); } } + } @action onPointerMove = (e: PointerEvent): void => { - if (!e.cancelBubble && this.active) { - e.preventDefault(); + if (!e.cancelBubble && this.props.active()) { e.stopPropagation(); let x = this.props.Document.GetNumber(KeyStore.PanX, 0); let y = this.props.Document.GetNumber(KeyStore.PanY, 0); let [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); - + this._previewCursorVisible = false; this.SetPan(x + dx, y + dy); } this._lastX = e.pageX; @@ -119,20 +112,33 @@ 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 transform = this.getTransform(); - let deltaScale = (1 - (e.deltaY / coefficient)); - let [x, y] = transform.transformPoint(e.clientX, e.clientY); + if (e.ctrlKey) { + var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); + var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0); + const coefficient = 1000; + let deltaScale = (1 - (e.deltaY / coefficient)); + this.props.Document.SetNumber(KeyStore.NativeWidth, nativeWidth * deltaScale); + this.props.Document.SetNumber(KeyStore.NativeHeight, nativeHeight * deltaScale); + e.stopPropagation(); + e.preventDefault(); + } else { + // if (modes[e.deltaMode] == 'pixels') coefficient = 50; + // else if (modes[e.deltaMode] == 'lines') coefficient = 1000; // This should correspond to line-height?? + let transform = this.getTransform(); - let localTransform = this.getLocalTransform(); - localTransform = localTransform.inverse().scaleAbout(deltaScale, x, y) + 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); - this.props.Document.SetNumber(KeyStore.Scale, localTransform.Scale); - this.SetPan(localTransform.TranslateX, localTransform.TranslateY); + let localTransform = this.getLocalTransform(); + localTransform = localTransform.inverse().scaleAbout(deltaScale, x, y) + + this.props.Document.SetNumber(KeyStore.Scale, localTransform.Scale); + this.SetPan(localTransform.TranslateX, localTransform.TranslateY); + } } @action @@ -145,113 +151,146 @@ export class CollectionFreeFormView extends CollectionViewBase { @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.Document.GetNumber(KeyStore.PanX, 0); - const pany: number = this.props.Document.GetNumber(KeyStore.PanY, 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.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) + var pt = this.getTransform().transformPoint(e.pageX, e.pageY); + super.onDrop(e, { x: pt[0], y: pt[1] }); + } - if (file) { - fReader.readAsDataURL(file) - } + onDragOver = (): void => { } - onDragOver = (e: React.DragEvent): void => { + @action + onKeyDown = (e: React.KeyboardEvent<Element>) => { + //if not these keys, make a textbox if preview cursor is active! + if (!e.ctrlKey && !e.altKey && !e.shiftKey) { + if (this._previewCursorVisible) { + //make textbox and add it to this collection + let [x, y] = this.getTransform().transformPoint(this._downX, this._downY); (this._downX, this._downY); + let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "new" }); + // mark this collection so that when the text box is created we can send it the SelectOnLoad prop to focus itself + this._selectOnLoaded = newBox.Id; + //set text to be the typed key and get focus on text box + this.props.CollectionView.addDocument(newBox); + //remove cursor from screen + this._previewCursorVisible = false; + } + } } @action - bringToFront(doc: DocumentView) { + bringToFront(doc: Document) { const { fieldKey: fieldKey, Document: 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); + 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.Document.GetNumber(KeyStore.PanX, 0); - const y = this.props.Document.GetNumber(KeyStore.PanY, 0); - return [x, y]; + get views() { + const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField); + if (lvalue && lvalue != FieldWaiting) { + return lvalue.Data.map(doc => { + return (<CollectionFreeFormDocumentView key={doc.Id} Document={doc} ref={focus} + AddDocument={this.props.addDocument} + RemoveDocument={this.props.removeDocument} + ScreenToLocalTransform={this.getTransform} + isTopMost={false} + SelectOnLoad={doc.Id === this._selectOnLoaded} + ContentScaling={this.noScaling} + PanelWidth={doc.Width} + PanelHeight={doc.Height} + ContainingCollectionView={this.props.CollectionView} />); + }) + } + return null; } @computed - get scale(): number { - return this.props.Document.GetNumber(KeyStore.Scale, 1); + get backgroundView() { + return !this.backgroundLayout ? (null) : + (<JsxParser + components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox }} + bindings={this.props.bindings} + jsx={this.backgroundLayout} + showWarnings={true} + onError={(test: any) => console.log(test)} + />); } - - getTransform = (): Transform => { - return this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH).transform(this.getLocalTransform()) + @computed + get overlayView() { + return !this.overlayLayout ? (null) : + (<JsxParser + components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox }} + bindings={this.props.bindings} + jsx={this.overlayLayout} + showWarnings={true} + onError={(test: any) => console.log(test)} + />); } - getLocalTransform = (): Transform => { - const [x, y] = this.translate; - return Transform.Identity.translate(-x, -y).scale(1 / this.scale); + 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; + + //when focus is lost, this will remove the preview cursor + @action + onBlur = (e: React.FocusEvent<HTMLInputElement>): void => { + this._previewCursorVisible = false; } render() { - const Document: Document = this.props.Document; - const value: Document[] = Document.GetList<Document>(this.props.fieldKey, []); - const panx: number = Document.GetNumber(KeyStore.PanX, 0); - const pany: number = Document.GetNumber(KeyStore.PanY, 0); - var me = this; + + //determines whether preview text cursor should be visible (ie when user taps this collection it should) + let cursor = null; + if (this._previewCursorVisible) { + //get local position and place cursor there! + let [x, y] = this.getTransform().transformPoint(this._downX, this._downY); + cursor = <div id="prevCursor" onKeyPress={this.onKeyDown} style={{ color: "black", position: "absolute", transformOrigin: "left top", transform: `translate(${x}px, ${y}px)` }}>I</div> + } + + 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} + onKeyPress={this.onKeyDown} 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}> + onBlur={this.onBlur} + style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px`, }} + tabIndex={0} + ref={this.createDropTarget}> <div className="collectionfreeformview" - style={{ width: "100%", transformOrigin: "left top", transform: ` translate(${panx}px, ${pany}px) scale(${this.zoomScaling}, ${this.zoomScaling})` }} + style={{ transformOrigin: "left top", transform: ` translate(${panx}px, ${pany}px) scale(${this.zoomScaling}, ${this.zoomScaling})` }} ref={this._canvasRef}> - - {this.props.BackgroundView ? this.props.BackgroundView() : null} - {value.map(doc => { - return (<CollectionFreeFormDocumentView key={doc.Id} Document={doc} - AddDocument={this.addDocument} - RemoveDocument={this.removeDocument} - ScreenToLocalTransform={this.getTransform} - isTopMost={false} - Scaling={1} - ContainingCollectionView={this} />); - })} + {this.backgroundView} + {cursor} + {this.views} </div> + {this.overlayView} </div> ); } diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index 633e3ca1b..d40e6d314 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -1,58 +1,98 @@ + .collectionSchemaView-container { border-style: solid; box-sizing: border-box; position: absolute; width: 100%; height: 100%; - .collectionfreeformview-container { - border-width: 0px; - .collectionfreeformview > .jsx-parser{ - position:absolute - } + .collectionSchemaView-previewRegion { + position: relative; + background: black; + float: left; + height: 100%; + } + .collectionSchemaView-previewHandle { + position: absolute; + height: 37px; + width: 20px; + z-index: 20; + right: 0; + top: 0; + background: Black ; + } + .collectionSchemaView-dividerDragger{ + position: relative; + background: black; + float: left; + height: 100%; } - .imageBox-cont { - position:relative; - max-height:100%; + .collectionSchemaView-tableContainer { + position: relative; + float: left; + height: 100%; } + .ReactTable { position: absolute; - display: inline-block; + // display: inline-block; + // overflow: auto; width: 100%; - overflow: auto; 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: 75px; + 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; } - .ReactTable .rt-td { - border-width: 1; - border-right-color: #aaa - } - .ReactTable .rt-tr-group { - border-width: 1; - border-bottom-color: #aaa - } - .imageBox-cont img { - object-fit: contain; - height: 100% - } .documentView-node:first-child { background: grey; .imageBox-cont img { object-fit: contain; - max-width: 100%; - height: 100% } } } diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 5c95aca99..5bcd501cc 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -1,64 +1,82 @@ 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, COLLECTION_BORDER_WIDTH } from "./CollectionViewBase"; -import { DocumentView } from "../nodes/DocumentView"; -import { EditableView } from "../EditableView"; -import { CompileScript, ToField } from "../../util/Scripting"; -import { KeyStore as KS, Key, KeyStore } 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 Measure from "react-measure"; +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 { setupDrag } from "../../util/DragManager"; + +// bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657 + @observer export class CollectionSchemaView extends CollectionViewBase { - public static LayoutString() { return CollectionViewBase.LayoutString("CollectionSchemaView"); } + private _mainCont = React.createRef<HTMLDivElement>(); + private DIVIDER_WIDTH = 5; - @observable - selectedIndex = 0; + @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; renderCell = (rowProps: CellInfo) => { let props: FieldViewProps = { doc: rowProps.value[0], fieldKey: rowProps.value[1], isSelected: () => false, - isTopMost: false + select: () => { }, + isTopMost: false, + bindings: {}, + selectOnLoad: false, } let contents = ( <FieldView {...props} /> ) + let reference = React.createRef<HTMLDivElement>(); + let onItemDown = setupDrag(reference, () => props.doc); return ( - <EditableView contents={contents} height={36} GetValue={() => { - let field = props.doc.Get(props.fieldKey); - if (field && field instanceof Field) { - return field.ToScriptString(); - } - return field || ""; - }} SetValue={(value: string) => { - let script = CompileScript(value); - if (!script.compiled) { - return false; - } - let field = script(); - if (field instanceof Field) { - props.doc.Set(props.fieldKey, field); - return true; - } else { - let dataField = ToField(field); - if (dataField) { - props.doc.Set(props.fieldKey, dataField); - return true; - } - } - return false; - }}></EditableView> + <div onPointerDown={onItemDown} key={props.doc.Id} ref={reference}> + <EditableView contents={contents} + height={36} GetValue={() => { + let field = props.doc.Get(props.fieldKey); + if (field && field instanceof Field) { + return field.ToScriptString(); + } + return field || ""; + }} + SetValue={(value: string) => { + let script = CompileScript(value); + if (!script.compiled) { + return false; + } + let field = script(); + if (field instanceof Field) { + props.doc.Set(props.fieldKey, field); + return true; + } else { + let dataField = ToField(field); + if (dataField) { + props.doc.Set(props.fieldKey, dataField); + return true; + } + } + return false; + }}> + </EditableView> + </div> ) } @@ -69,97 +87,153 @@ 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 ? "lightGray" : "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(); + } } } } + @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); + } - @observable - private _parentScaling = 1; // used to transfer the dimensions of the content pane in the DOM to the ParentScaling prop of the DocumentView render() { - const { Document: Document, fieldKey: fieldKey } = this.props; - const children = Document.GetList<Document>(fieldKey, []); - const columns = Document.GetList(KS.ColumnsKey, - [KS.Title, KS.Data, KS.Author]) - let content; - var me = this; - if (this.selectedIndex != -1) { - content = ( - <Measure onResize={action((r: any) => { - var doc = children[this.selectedIndex]; - var n = doc.GetNumber(KeyStore.NativeWidth, 0); - if (n > 0 && r.entry.width > 0) { - this._parentScaling = r.entry.width / n; - } - })}> - {({ measureRef }) => - <div ref={measureRef}> - <DocumentView Document={children[this.selectedIndex]} - AddDocument={this.addDocument} RemoveDocument={this.removeDocument} - ScreenToLocalTransform={() => Transform.Identity}//TODO This should probably be an actual transform - Scaling={this._parentScaling} - isTopMost={false} - ContainingCollectionView={me} /> - </div> - } - </Measure> - ) - } 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} + SelectOnLoad={false} + ScreenToLocalTransform={this.getTransform} + ContentScaling={this.getContentScaling} + PanelWidth={this.getPanelWidth} + PanelHeight={this.getPanelHeight} + ContainingCollectionView={this.props.CollectionView} /> + </div> + } + </Measure> + ) + let previewHandle = !this.props.active() ? (null) : ( + <div className="collectionSchemaView-previewHandle" onPointerDown={this.onExpanderDown} />); return ( - <div onPointerDown={this.onPointerDown} className="collectionSchemaView-container" - style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px`, }} > - <SplitPane split={"vertical"} defaultSize="60%" style={{ height: "100%", position: "relative", overflow: "none" }}> - <ReactTable - data={children} - pageSize={children.length} - page={0} - showPagination={false} - 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} - /> - {content} - </SplitPane> - </div> + <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} ref={this._mainCont} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} > + <div className="collectionSchemaView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}> + <Measure onResize={action((r: any) => { + this._dividerX = r.entry.width; + this._panelHeight = r.entry.height; + })}> + {({ measureRef }) => + <div ref={measureRef} className="collectionSchemaView-tableContainer" style={{ width: `${this._splitPercentage}%` }}> + <ReactTable + data={children} + pageSize={children.length} + page={0} + showPagination={false} + columns={columns.map(col => ({ + Header: col.Name, + accessor: (doc: Document) => [doc, col], + id: col.Id + }))} + column={{ + ...ReactTableDefaults.column, + Cell: this.renderCell, + + }} + getTrProps={this.getTrProps} + /> + </div> + } + </Measure> + <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} /> + <div className="collectionSchemaView-previewRegion" style={{ width: `calc(${100 - this._splitPercentage}% - ${this.DIVIDER_WIDTH}px)` }}> + {content} + </div> + {previewHandle} + </div> + </div > ) } }
\ 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..c488e2894 --- /dev/null +++ b/src/client/views/collections/CollectionTreeView.scss @@ -0,0 +1,34 @@ +ul { + list-style: none; +} + +li { + margin: 5px 0; +} + +.no-indent { + padding-left: 0; +} + +/* ALL THESE SPACINGS ARE SUPER HACKY RIGHT NOW HANNAH PLS HELP */ + +li:before { + content: '\2014'; + margin-right: 0.7em; +} + +.collapsed:before { + content: '\25b6'; + margin-right: 0.65em; +} + +.uncollapsed:before { + content: '\25bc'; + margin-right: 0.5em; +} + +.collectionTreeView-dropTarget { + border-style: solid; + box-sizing: border-box; + height:100%; +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx new file mode 100644 index 000000000..55c804337 --- /dev/null +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -0,0 +1,86 @@ +import { observer } from "mobx-react"; +import { CollectionViewBase } from "./CollectionViewBase"; +import { Document } from "../../../fields/Document"; +import { KeyStore } from "../../../fields/KeyStore"; +import { ListField } from "../../../fields/ListField"; +import React = require("react") +import { TextField } from "../../../fields/TextField"; +import { observable, action } from "mobx"; +import "./CollectionTreeView.scss"; +import { setupDrag } from "../../util/DragManager"; +import { FieldWaiting } from "../../../fields/Field"; +import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; + +export interface TreeViewProps { + document: Document; +} + +@observer +/** + * Component that takes in a document prop and a boolean whether it's collapsed or not. + */ +class TreeView extends React.Component<TreeViewProps> { + + @observable + collapsed: boolean = false; + + /** + * Renders a single child document. If this child is a collection, it will call renderTreeView again. Otherwise, it will just append a list element. + * @param childDocument The document to render. + */ + renderChild(childDocument: Document) { + let reference = React.createRef<HTMLDivElement>(); + + var children = childDocument.GetT<ListField<Document>>(KeyStore.Data, ListField); + let title = childDocument.GetT<TextField>(KeyStore.Title, TextField); + let onItemDown = setupDrag(reference, () => childDocument); + + if (title && title != FieldWaiting) { + let subView = !children || this.collapsed || children === FieldWaiting ? (null) : + <ul> + <TreeView document={childDocument} /> + </ul>; + return <div className="treeViewItem-container" onPointerDown={onItemDown} ref={reference}> + <li className={!children ? "leaf" : this.collapsed ? "collapsed" : "uncollapsed"} + onClick={action(() => this.collapsed = !this.collapsed)} > + {title.Data} + {subView} + </li> + </div> + } + return (null); + } + + render() { + var children = this.props.document.GetT<ListField<Document>>(KeyStore.Data, ListField); + return !children || children === FieldWaiting ? (null) : + (children.Data.map(value => + <div key={value.Id}> + {this.renderChild(value)} + </div>) + ) + } +} + + +@observer +export class CollectionTreeView extends CollectionViewBase { + + render() { + let titleStr = ""; + let title = this.props.Document.GetT<TextField>(KeyStore.Title, TextField); + if (title && title !== FieldWaiting) { + titleStr = title.Data; + } + return ( + <div className="collectionTreeView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} > + <h3>{titleStr}</h3> + <ul className="no-indent"> + <TreeView + document={this.props.Document} + /> + </ul> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx new file mode 100644 index 000000000..f938d2237 --- /dev/null +++ b/src/client/views/collections/CollectionView.tsx @@ -0,0 +1,117 @@ +import { action, computed, observable } 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} SelectOnLoad={selectOnLoad} 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 1e20da8d2..7067724c8 100644 --- a/src/client/views/collections/CollectionViewBase.tsx +++ b/src/client/views/collections/CollectionViewBase.tsx @@ -1,67 +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 { fieldKey: Key; Document: Document; - ContainingDocumentView: Opt<DocumentView>; ScreenToLocalTransform: () => Transform; isSelected: () => boolean; isTopMost: boolean; select: (ctrlPressed: boolean) => void; - BackgroundView?: () => JSX.Element; + 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, fieldKey: string = "DataKey") { - return `<${collectionType} Document={Document} - ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} isSelected={isSelected} select={select} - isTopMost={isTopMost} - ContainingDocumentView={DocumentView} BackgroundView={BackgroundView} />`; - } - @computed - public get active(): boolean { - var isSelected = (this.props.ContainingDocumentView && SelectionManager.IsSelected(this.props.ContainingDocumentView)); - var childSelected = SelectionManager.SelectedDocuments().some(view => view.props.ContainingCollectionView == this); - var topMost = this.props.isTopMost; - return isSelected || childSelected || topMost; +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) } }); + } } + + @undoBatch @action - addDocument = (doc: Document): void => { - //TODO This won't create the field if it doesn't already exist - const value = this.props.Document.GetData(this.props.fieldKey, ListField, new Array<Document>()) - value.push(doc); + 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.Document.GetData(this.props.fieldKey, 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, width: 300, height: 300 }); + 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 2eb0c5857..50dc5a619 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,16 +1,11 @@ -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 { Transform } from "../../util/Transform"; +import { DocumentView, DocumentViewProps } from "./DocumentView"; import "./DocumentView.scss"; import React = require("react"); -import { DocumentView, DocumentViewProps } from "./DocumentView"; -import { Transform } from "../../util/Transform"; @observer @@ -29,60 +24,53 @@ export class CollectionFreeFormDocumentView extends React.Component<DocumentView @computed get transform(): string { - return `scale(${this.props.Scaling}, ${this.props.Scaling}) translate(${this.props.Document.GetNumber(KeyStore.X, 0)}px, ${this.props.Document.GetNumber(KeyStore.Y, 0)}px)`; + return `scale(${this.props.ContentScaling()}, ${this.props.ContentScaling()}) translate(${this.props.Document.GetNumber(KeyStore.X, 0)}px, ${this.props.Document.GetNumber(KeyStore.Y, 0)}px)`; } - @computed - get width(): number { - return this.props.Document.GetNumber(KeyStore.Width, 0); - } - - @computed - get nativeWidth(): number { - return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); - } + @computed get zIndex(): number { return this.props.Document.GetNumber(KeyStore.ZIndex, 0); } + @computed get width(): number { return this.props.Document.Width(); } + @computed get height(): number { return this.props.Document.Height(); } + @computed get nativeWidth(): number { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } + @computed get nativeHeight(): number { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } set width(w: number) { this.props.Document.SetData(KeyStore.Width, w, NumberField) - if (this.nativeWidth > 0 && this.nativeHeight > 0) { + if (this.nativeWidth && this.nativeHeight) { this.props.Document.SetNumber(KeyStore.Height, this.nativeHeight / this.nativeWidth * w) } } - @computed - get height(): number { - return this.props.Document.GetNumber(KeyStore.Height, 0); - } - @computed - get nativeHeight(): number { - return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); - } - set height(h: number) { this.props.Document.SetData(KeyStore.Height, h, NumberField); - if (this.nativeWidth > 0 && this.nativeHeight > 0) { + if (this.nativeWidth && this.nativeHeight) { this.props.Document.SetNumber(KeyStore.Width, this.nativeWidth / this.nativeHeight * h) } } - @computed - get zIndex(): number { - return this.props.Document.GetNumber(KeyStore.ZIndex, 0); - } - set zIndex(h: number) { this.props.Document.SetData(KeyStore.ZIndex, h, NumberField) } + contentScaling = () => { + return this.nativeWidth > 0 ? this.width / this.nativeWidth : 1; + } getTransform = (): Transform => { - return this.props.ScreenToLocalTransform().translate(-this.props.Document.GetNumber(KeyStore.X, 0), -this.props.Document.GetNumber(KeyStore.Y, 0)); + 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 docView() { + return <DocumentView {...this.props} + ContentScaling={this.contentScaling} + ScreenToLocalTransform={this.getTransform} + /> } render() { - var parentScaling = this.nativeWidth > 0 ? this.width / this.nativeWidth : 1; return ( - <div ref={this._mainCont} style={{ + <div className="collectionFreeFormDocumentView-container" ref={this._mainCont} style={{ transformOrigin: "left top", transform: this.transform, width: this.width, @@ -91,8 +79,7 @@ export class CollectionFreeFormDocumentView extends React.Component<DocumentView zIndex: this.zIndex, backgroundColor: "transparent" }} > - - <DocumentView {...this.props} Scaling={parentScaling} ScreenToLocalTransform={this.getTransform} /> + {this.docView} </div> ); } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 3d9b0c190..23cfa6df3 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,128 +1,137 @@ 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 { 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 { ContextMenu } from "../ContextMenu"; import { FormattedTextBox } from "../nodes/FormattedTextBox"; import { ImageBox } from "../nodes/ImageBox"; +import { Documents } from "../../documents/Documents" +import { KeyValuePane } from "../nodes/KeyValuePane" +import { WebBox } from "../nodes/WebBox"; import "./DocumentView.scss"; import React = require("react"); -import { Transform } from "../../util/Transform"; -import { SelectionManager } from "../../util/SelectionManager"; -import { DragManager } from "../../util/DragManager"; -import { ContextMenu } from "../ContextMenu"; -import { TextField } from "../../../fields/TextField"; +import { CollectionViewProps } from "../collections/CollectionViewBase"; const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this? + export interface DocumentViewProps { - ContainingCollectionView: Opt<CollectionViewBase>; + ContainingCollectionView: Opt<CollectionView>; + Document: Document; AddDocument?: (doc: Document) => void; RemoveDocument?: (doc: Document) => boolean; ScreenToLocalTransform: () => Transform; isTopMost: boolean; //tfs: This shouldn't be necessary I don't think - Scaling: number; + ContentScaling: () => number; + PanelWidth: () => number; + PanelHeight: () => number; + SelectOnLoad: boolean; +} +export interface JsxArgs extends DocumentViewProps { + Keys: { [name: string]: Key } + Fields: { [name: string]: Field } +} + +/* +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 } + ) + } +*/ +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; + } + 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; } @observer export class DocumentView extends React.Component<DocumentViewProps> { private _mainCont = React.createRef<HTMLDivElement>(); - get MainContent() { - return this._mainCont; - } - get screenRect(): ClientRect | DOMRect { - if (this._mainCont.current) { - return this._mainCont.current.getBoundingClientRect(); - } - return new DOMRect(); - } - @computed - get layout(): string { - return this.props.Document.GetText(KeyStore.Layout, "<p>Error loading layout data</p>"); - } - - @computed - get backgroundLayout(): string | undefined { - let field = this.props.Document.GetT(KeyStore.BackgroundLayout, TextField); - if (field && field !== "<Waiting>") { - return field.Data; - } - } - - @computed - get layoutKeys(): Key[] { - return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array<Key>()); - } + private _documentBindings: any = null; + private _downX: number = 0; + private _downY: number = 0; - @computed - get layoutFields(): Key[] { - return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array<Key>()); - } + @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>()); } - @computed - get active(): boolean { - return SelectionManager.IsSelected(this) || this.props.ContainingCollectionView === undefined || - this.props.ContainingCollectionView.active; - } + screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect(); - private _contextMenuCanOpen = false; - private _downX: number = 0; - private _downY: number = 0; 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(); - return; - } - this._contextMenuCanOpen = e.button == 2; - if (this.active && !e.isDefaultPrevented()) { + CollectionDockingView.Instance.StartOtherDrag(this.props.Document, e); e.stopPropagation(); - if (e.buttons === 2) { - e.preventDefault(); + } else { + 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); } - document.removeEventListener("pointermove", this.onPointerMove) - document.addEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp) - document.addEventListener("pointerup", this.onPointerUp); } } - @action - dragComplete = (e: DragManager.DragCompleteEvent) => { - } - - @computed - get topMost(): boolean { - return this.props.ContainingCollectionView == undefined || this.props.ContainingCollectionView instanceof CollectionDockingView; - } - onPointerMove = (e: PointerEvent): void => { if (e.cancelBubble) { - this._contextMenuCanOpen = false; return; } if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { - this._contextMenuCanOpen = false; + document.removeEventListener("pointermove", this.onPointerMove) + document.removeEventListener("pointerup", this.onPointerUp) if (this._mainCont.current != null && !this.topMost) { - this._contextMenuCanOpen = false; const [left, top] = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); let dragData: { [id: string]: any } = {}; - dragData["document"] = this; + dragData["documentView"] = this; dragData["xOffset"] = e.x - left; dragData["yOffset"] = e.y - top; DragManager.StartDrag(this._mainCont.current, dragData, { handlers: { - dragComplete: this.dragComplete, + dragComplete: action(() => { }), }, hideSource: true }) @@ -141,28 +150,24 @@ export class DocumentView extends React.Component<DocumentViewProps> { } } - openRight = (e: React.MouseEvent): void => { - CollectionDockingView.Instance.AddRightSplit(this.props.Document); - } - - deleteClicked = (e: React.MouseEvent): void => { + deleteClicked = (): void => { if (this.props.RemoveDocument) { this.props.RemoveDocument(this.props.Document); } } fieldsClicked = (e: React.MouseEvent): void => { - //TODO: open kvp + if (this.props.AddDocument) { + this.props.AddDocument(Documents.KVPDocument(this.props.Document)); + } } - - @action 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) } - @action + closeFullScreenClicked = (e: React.MouseEvent): void => { CollectionDockingView.Instance.CloseFullScreen(); ContextMenu.Instance.clearItems(); @@ -172,102 +177,78 @@ export class DocumentView extends React.Component<DocumentViewProps> { @action onContextMenu = (e: React.MouseEvent): void => { - if (!SelectionManager.IsSelected(this)) { + e.stopPropagation(); + let moved = Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3; + if (moved || e.isDefaultPrevented()) { + e.preventDefault() return; } e.preventDefault() - if (!this._contextMenuCanOpen) { - return; - } - - 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 propogation of this event + ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }) + ContextMenu.Instance.addItem({ description: "Fields", event: this.fieldsClicked }) + ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document) }) + ContextMenu.Instance.addItem({ description: "Freeform", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform) }) + ContextMenu.Instance.addItem({ description: "Schema", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Schema) }) + ContextMenu.Instance.addItem({ description: "Treeview", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Tree) }) + //ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) }) + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) + if (!this.topMost) { + // DocumentViews should stop propagation of this event e.stopPropagation(); - - ContextMenu.Instance.clearItems(); - ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }) - ContextMenu.Instance.addItem({ description: "Open Right", event: this.openRight }) - ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked }) - ContextMenu.Instance.addItem({ description: "Fields", event: this.fieldsClicked }) - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) - SelectionManager.SelectDoc(this, e.ctrlKey); - } - } - - // - // returns the cumulative scaling between the document and the screen - // tfs: I don't think this should be necessary - // - @computed - public get ScalingToScreenSpace(): number { - if (this.props.ContainingCollectionView != undefined && - this.props.ContainingCollectionView.props.ContainingDocumentView != undefined) { - let ss = this.props.ContainingCollectionView.props.Document.GetNumber(KeyStore.Scale, 1); - return this.props.ContainingCollectionView.props.ContainingDocumentView.ScalingToScreenSpace * ss; } - return 1; - } - isSelected = () => { - return SelectionManager.IsSelected(this); + ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked }) + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) + SelectionManager.SelectDoc(this, e.ctrlKey); } - select = (ctrlPressed: boolean) => { - SelectionManager.SelectDoc(this, ctrlPressed) + @computed get mainContent() { + return <JsxParser + components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValuePane }} + bindings={this._documentBindings} + jsx={this.layout} + showWarnings={true} + onError={(test: any) => { console.log(test) }} + /> } render() { - let bindings = { ...this.props } as any; - bindings.isSelected = this.isSelected; - bindings.select = this.select; + 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; + this._documentBindings[key.Name] = field && field != FieldWaiting ? field.GetValue() : field; } - /* - tfs: - Should this be moved to CollectionFreeformView or another component that renders - Document backgrounds (or contents based on a layout key, which could be used here as well) - that CollectionFreeformView uses? It seems like a lot for it to be here considering only one view currently uses it... - */ - let backgroundLayout = this.backgroundLayout; - if (backgroundLayout) { - let backgroundView = () => (<JsxParser - components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView }} - bindings={bindings} - jsx={this.backgroundLayout} - showWarnings={true} - onError={(test: any) => { console.log(test) }} - />); - bindings.BackgroundView = backgroundView; - } - - bindings.DocumentView = this; + this._documentBindings.bindings = this._documentBindings; - var width = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); - var strwidth = width > 0 ? width.toString() + "px" : "100%"; - var height = this.props.Document.GetNumber(KeyStore.NativeHeight, 0); - var strheight = height > 0 ? height.toString() + "px" : "100%"; + 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="documentView-node" ref={this._mainCont} style={{ width: strwidth, height: strheight, transformOrigin: "left top", transform: `scale(${this.props.Scaling},${this.props.Scaling})` }} + <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} > - <JsxParser - components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView }} - bindings={bindings} - jsx={this.layout} - showWarnings={true} - onError={(test: any) => { console.log(test) }} - /> + onPointerDown={this.onPointerDown} + > + {this.mainContent} </div> ) } diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 08de53e1c..056f834d1 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -7,9 +7,13 @@ import { TextField } from "../../../fields/TextField"; import { NumberField } from "../../../fields/NumberField"; import { RichTextField } from "../../../fields/RichTextField"; import { ImageField } from "../../../fields/ImageField"; +import { WebField } from "../../../fields/WebField"; import { Key } from "../../../fields/Key"; import { FormattedTextBox } from "./FormattedTextBox"; import { ImageBox } from "./ImageBox"; +import { WebBox } from "./WebBox"; +import { KVPField } from "../../../fields/KVPField"; +import { KeyValuePane } from "./KeyValuePane"; // // these properties get assigned through the render() method of the DocumentView when it creates this node. @@ -20,12 +24,18 @@ export interface FieldViewProps { fieldKey: Key; doc: Document; isSelected: () => boolean; + select: () => void; isTopMost: boolean; + selectOnLoad: boolean; + bindings: any; } @observer export class FieldView extends React.Component<FieldViewProps> { - public static LayoutString(fieldType: string) { return `<${fieldType} doc={Document} DocumentViewForField={DocumentView} fieldKey={DataKey} isSelected={isSelected} isTopMost={isTopMost} />`; } + public static LayoutString(fieldType: { name: string }, fieldStr: string = "DataKey") { + return `<${fieldType.name} doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={${fieldStr}} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost} />`; + } + @computed get field(): FieldValue<Field> { const { doc, fieldKey } = this.props; @@ -45,11 +55,20 @@ export class FieldView extends React.Component<FieldViewProps> { else if (field instanceof ImageField) { return <ImageBox {...this.props} /> } + else if (field instanceof WebField) { + return <WebBox {...this.props} /> + } + // bcz: this belongs here, but it doesn't render well so taking it out for now + // else if (field instanceof HtmlField) { + // return <WebBox {...this.props} /> + // } else if (field instanceof NumberField) { return <p>{field.Data}</p> - } else if (field != FieldWaiting) { - return <p>{field.GetValue}</p> - } else + } + else if (field != FieldWaiting) { + return <p>{JSON.stringify(field.GetValue())}</p> + } + else return <p> {"Waiting for server..."} </p> } diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 5139d5d6b..21bd43b6e 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,12 @@ } .formattedTextBox-cont { - background: beige; - padding: 1vw; + background: white; + padding: 1; + border: black; + border-width: 10; + 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 2e3d396c1..e65615af4 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -3,16 +3,15 @@ import { baseKeymap } from "prosemirror-commands"; import { history, redo, undo } from "prosemirror-history"; import { keymap } from "prosemirror-keymap"; import { schema } from "prosemirror-schema-basic"; -import { EditorState, Transaction } from "prosemirror-state"; +import { EditorState, Transaction, } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; -import { Opt, FieldWaiting, FieldValue } from "../../../fields/Field"; -import { SelectionManager } from "../../util/SelectionManager"; +import { Opt, FieldWaiting } from "../../../fields/Field"; 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"; + + // FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document @@ -33,7 +32,7 @@ import { observer } from "mobx-react"; //] 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>; @@ -42,7 +41,6 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { super(props); this._ref = React.createRef(); - this.onChange = this.onChange.bind(this); } @@ -50,25 +48,23 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { if (this._editorView) { const state = this._editorView.state.apply(tx); this._editorView.updateState(state); - const { doc, fieldKey } = this.props; - doc.SetData(fieldKey, JSON.stringify(state.toJSON()), RichTextField); + this.props.doc.SetData(this.props.fieldKey, JSON.stringify(state.toJSON()), RichTextField); } } componentDidMount() { let state: EditorState; - const { doc, fieldKey } = this.props; const config = { schema, plugins: [ history(), keymap({ "Mod-z": undo, "Mod-y": redo }), - keymap(baseKeymap) + keymap(baseKeymap), ] }; - let field = doc.GetT(fieldKey, RichTextField); - if (field && field != FieldWaiting) { // bcz: don't think this works + let field = this.props.doc.GetT(this.props.fieldKey, RichTextField); + if (field && field != FieldWaiting) { state = EditorState.fromJSON(config, JSON.parse(field.Data)); } else { state = EditorState.create(config); @@ -88,6 +84,10 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field))); } }) + if (this.props.selectOnLoad) { + this.props.select(); + this._editorView!.focus(); + } } componentWillUnmount() { @@ -105,22 +105,20 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { @action onChange(e: React.ChangeEvent<HTMLInputElement>) { - const { fieldKey, doc } = this.props; - doc.SetData(fieldKey, e.target.value, RichTextField); + this.props.doc.SetData(this.props.fieldKey, e.target.value, RichTextField); } onPointerDown = (e: React.PointerEvent): void => { - let me = this; if (e.buttons === 1 && this.props.isSelected()) { e.stopPropagation(); } } + onPointerWheel = (e: React.WheelEvent): void => { + e.stopPropagation(); + } render() { return (<div className="formattedTextBox-cont" - style={{ - color: "initial", - whiteSpace: "initial", - }} onPointerDown={this.onPointerDown} + 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 36f5e0fe0..ea459b911 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -1,14 +1,17 @@ .imageBox-cont { padding: 0vw; - position: absolute; + position: relative; + text-align: center; width: 100%; + height: auto; max-width: 100%; max-height: 100% } + .imageBox-cont img { - max-width: 100%; - max-height: 100% + object-fit: contain; + height: 100%; } .imageBox-button { diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index e1fa26e2f..e206bf8d5 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,22 +1,21 @@ 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 { 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; @@ -27,12 +26,20 @@ export class ImageBox extends React.Component<FieldViewProps> { super(props); this._ref = React.createRef(); + this._imgRef = React.createRef(); this.state = { photoIndex: 0, isOpen: false, }; } + @action + onLoad = (target: any) => { + var h = this._imgRef.current!.naturalHeight; + var w = this._imgRef.current!.naturalWidth; + this.props.doc.SetNumber(KeyStore.NativeHeight, this.props.doc.GetNumber(KeyStore.NativeWidth, 0) * h / w) + } + componentDidMount() { } @@ -86,10 +93,9 @@ export class ImageBox extends React.Component<FieldViewProps> { let path = field == FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" : field instanceof ImageField ? field.Data.href : "http://www.cs.brown.edu/~bcz/face.gif"; let nativeWidth = this.props.doc.GetNumber(KeyStore.NativeWidth, 1); - return ( <div className="imageBox-cont" onPointerDown={this.onPointerDown} ref={this._ref} > - <img src={path} width={nativeWidth} alt="Image not found" /> + <img src={path} width={nativeWidth} alt="Image not found" ref={this._imgRef} onLoad={this.onLoad} /> {this.lightbox(path)} </div>) } diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx new file mode 100644 index 000000000..b8a0cca48 --- /dev/null +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -0,0 +1,55 @@ +import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app +import "./KeyValuePane.scss"; +import React = require("react") +import { FieldViewProps, FieldView } from './FieldView'; +import { Opt, Field } from '../../../fields/Field'; +import { observer } from "mobx-react" +import { observable, action } from 'mobx'; +import { Document } from '../../../fields/Document'; +import { Key } from '../../../fields/Key'; +import { Server } from "../../Server" + +// Represents one row in a key value plane + +export interface KeyValuePairProps { + fieldId: string; + doc: Document; +} +@observer +export class KeyValuePair extends React.Component<KeyValuePairProps> { + + @observable + private key: Opt<Key> + + constructor(props: KeyValuePairProps) { + super(props); + Server.GetField(this.props.fieldId, + action((field: Opt<Field>) => { + if (field) { + this.key = field as Key; + } + })); + + } + + + + render() { + if (!this.key) { + return <tr><td>error</td><td></td></tr> + + } + let props: FieldViewProps = { + doc: this.props.doc, + fieldKey: this.key, + isSelected: () => false, + select: () => { }, + isTopMost: false, + bindings: {}, + selectOnLoad: false, + } + return ( + <tr><td>{this.key.Name}</td><td><FieldView {...props} /></td></tr> + ) + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValuePane.scss b/src/client/views/nodes/KeyValuePane.scss new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/client/views/nodes/KeyValuePane.scss diff --git a/src/client/views/nodes/KeyValuePane.tsx b/src/client/views/nodes/KeyValuePane.tsx new file mode 100644 index 000000000..90f5b653c --- /dev/null +++ b/src/client/views/nodes/KeyValuePane.tsx @@ -0,0 +1,96 @@ + +import Lightbox from 'react-image-lightbox'; +import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app +import "./KeyValuePane.scss"; +import React = require("react") +import { FieldViewProps, FieldView } from './FieldView'; +import { FieldWaiting, Opt, Field } from '../../../fields/Field'; +import { observer } from "mobx-react" +import { observable, action, IReactionDisposer, reaction, ObservableMap } from 'mobx'; +import { KeyStore } from '../../../fields/KeyStore'; +import { RichTextField } from "../../../fields/RichTextField"; +import { element } from 'prop-types'; +import { props } from 'bluebird'; +import { EditorView } from 'prosemirror-view'; +import { Transaction, EditorState } from 'prosemirror-state'; +import { schema } from 'prosemirror-schema-basic'; +import { keymap } from 'prosemirror-keymap'; +import { undo, redo } from 'prosemirror-history'; +import { baseKeymap } from 'prosemirror-commands'; +import { KVPField } from '../../../fields/KVPField'; +import { Document } from '../../../fields/Document'; +import { Key } from '../../../fields/Key'; +import { JSXElement } from 'babel-types'; +import { KeyValuePair } from "./KeyValuePair" + +@observer +export class KeyValuePane extends React.Component<FieldViewProps> { + + public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(KeyValuePane, fieldStr) } + private _ref: React.RefObject<HTMLDivElement>; + private _editorView: Opt<EditorView>; + private _reactionDisposer: Opt<IReactionDisposer>; + + + constructor(props: FieldViewProps) { + super(props); + + this._ref = React.createRef(); + } + + + + shouldComponentUpdate() { + return false; + } + + + onPointerDown = (e: React.PointerEvent): void => { + if (e.buttons === 1 && this.props.isSelected()) { + e.stopPropagation(); + } + } + onPointerWheel = (e: React.WheelEvent): void => { + e.stopPropagation(); + } + + createTable = () => { + let table: Array<JSX.Element> = [] + let ret = "waiting" + let doc = this.props.doc.GetT(KeyStore.Data, Document); + if (!doc || doc == "<Waiting>") { + return <tr><td>Loading...</td></tr> + } + let realDoc = doc; + + let ids: { [key: string]: string } = {}; + let protos = doc.GetAllPrototypes(); + for (const proto of protos) { + proto._proxies.forEach((val, key) => { + if (!(key in ids)) { + ids[key] = key; + } + }) + } + + let rows: JSX.Element[] = []; + for (let key in ids) { + rows.push(<KeyValuePair doc={realDoc} fieldId={key} />) + } + return rows; + } + + + render() { + + return (<table> + <tbody> + <tr> + <th>Key</th> + <th>Fields</th> + </tr> + {this.createTable()} + </tbody> + </table>) + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss new file mode 100644 index 000000000..e72b3c4da --- /dev/null +++ b/src/client/views/nodes/WebBox.scss @@ -0,0 +1,14 @@ + +.webBox-cont { + padding: 0vw; + position: absolute; + width: 100%; + height: 100%; +} + +.webBox-button { + padding : 0vw; + border: none; + width : 100%; + height: 100%; +}
\ No newline at end of file diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx new file mode 100644 index 000000000..2ca8d49ce --- /dev/null +++ b/src/client/views/nodes/WebBox.tsx @@ -0,0 +1,38 @@ +import "./WebBox.scss"; +import React = require("react") +import { WebField } from '../../../fields/WebField'; +import { FieldViewProps, FieldView } from './FieldView'; +import { FieldWaiting } from '../../../fields/Field'; +import { observer } from "mobx-react" +import { computed } from 'mobx'; +import { KeyStore } from '../../../fields/KeyStore'; + +@observer +export class WebBox extends React.Component<FieldViewProps> { + + public static LayoutString() { return FieldView.LayoutString(WebBox); } + + constructor(props: FieldViewProps) { + super(props); + } + + @computed get html(): string { return this.props.doc.GetHtml(KeyStore.Data, ""); } + + render() { + let field = this.props.doc.Get(this.props.fieldKey); + let path = field == FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" : + field instanceof WebField ? field.Data.href : "https://crossorigin.me/" + "https://cs.brown.edu"; + + let content = this.html ? + <span dangerouslySetInnerHTML={{ __html: this.html }}></span> : + <div style={{ width: "100%", height: "100%", position: "absolute" }}> + <iframe src={path} style={{ position: "absolute", width: "100%", height: "100%" }}></iframe> + {this.props.isSelected() ? (null) : <div style={{ width: "100%", height: "100%", position: "absolute" }} />} + </div>; + + return ( + <div className="webBox-cont" > + {content} + </div>) + } +}
\ No newline at end of file diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx new file mode 100644 index 000000000..7bc70615f --- /dev/null +++ b/src/debug/Test.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +class TestInternal extends React.Component { + onContextMenu = (e: React.MouseEvent) => { + console.log("Internal"); + e.stopPropagation(); + } + + onPointerDown = (e: React.MouseEvent) => { + console.log("pointer down") + e.preventDefault(); + } + + render() { + return <div onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} + onPointerUp={this.onPointerDown}>Hello world</div> + } +} + +class TestChild extends React.Component { + onContextMenu = () => { + console.log("Child"); + } + + render() { + return <div onContextMenu={this.onContextMenu}><TestInternal /></div> + } +} + +class TestParent extends React.Component { + onContextMenu = () => { + console.log("Parent"); + } + + render() { + return <div onContextMenu={this.onContextMenu}><TestChild /></div> + } +} + +ReactDOM.render(( + <div style={{ position: "absolute", width: "100%", height: "100%" }}> + <TestParent /> + </div>), + document.getElementById('root') +);
\ No newline at end of file diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx 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..5b91de6ed 100644 --- a/src/fields/Document.ts +++ b/src/fields/Document.ts @@ -1,14 +1,37 @@ -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"; +import { HtmlField } from "./HtmlField"; 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 +41,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) { @@ -69,6 +126,10 @@ export class Document extends Field { return vval; } + GetHtml(key: Key, defaultVal: string): string { + return this.GetData(key, HtmlField, defaultVal); + } + GetNumber(key: Key, defaultVal: number): number { return this.GetData(key, NumberField, defaultVal); } @@ -83,29 +144,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 +201,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 +217,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/KVPField b/src/fields/KVPField new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/fields/KVPField diff --git a/src/fields/KVPField.ts b/src/fields/KVPField.ts new file mode 100644 index 000000000..a7ecc0768 --- /dev/null +++ b/src/fields/KVPField.ts @@ -0,0 +1,30 @@ +import { BasicField } from "./BasicField" +import { FieldId } from "./Field"; +import { Types } from "../server/Message"; +import { Document } from "./Document" + +export class KVPField extends BasicField<Document> { + constructor(data: Document | undefined = undefined, id?: FieldId, save: boolean = true) { + super(data == undefined ? new Document() : data, save, id); + } + + toString(): string { + return this.Data.Title; + } + + ToScriptString(): string { + return `new KVPField("${this.Data}")`; + } + + Copy() { + return new KVPField(this.Data); + } + + ToJson(): { type: Types, data: Document, _id: string } { + return { + type: Types.Text, + data: this.Data, + _id: this.Id + } + } +}
\ No newline at end of file diff --git a/src/fields/Key.ts b/src/fields/Key.ts index 8d92b89b6..00d78d516 100644 --- a/src/fields/Key.ts +++ b/src/fields/Key.ts @@ -1,6 +1,8 @@ -import { Field } from "./Field" +import { Field, FieldId } from "./Field" import { Utils } from "../Utils"; import { observable } from "mobx"; +import { Types } from "../server/Message"; +import { Server } from "../client/Server"; export class Key extends Field { private name: string; @@ -9,10 +11,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 +40,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/fields/WebField.ts b/src/fields/WebField.ts new file mode 100644 index 000000000..8f945d686 --- /dev/null +++ b/src/fields/WebField.ts @@ -0,0 +1,30 @@ +import { BasicField } from "./BasicField"; +import { Field, FieldId } from "./Field"; +import { Types } from "../server/Message"; + +export class WebField extends BasicField<URL> { + constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) { + super(data == undefined ? new URL("https://crossorigin.me/" + "https://cs.brown.edu/") : data, save, id); + } + + toString(): string { + return this.Data.href; + } + + ToScriptString(): string { + return `new WebField("${this.Data}")`; + } + + Copy(): Field { + return new WebField(this.Data); + } + + ToJson(): { type: Types, data: URL, _id: string } { + return { + type: Types.Web, + data: this.Data, + _id: this.Id + } + } + +}
\ No newline at end of file diff --git a/src/server/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..148e6e723 --- /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, Web, 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..a53fb5d2b --- /dev/null +++ b/src/server/ServerUtil.ts @@ -0,0 +1,55 @@ +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'; +import { WebField } from '../fields/WebField'; + +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.Web: + return new WebField(new URL(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 |