From 9740d8e9879d95a984a842cf382260bb5572b8ef Mon Sep 17 00:00:00 2001 From: bob Date: Tue, 19 Feb 2019 16:00:41 -0500 Subject: clean up a bit. --- src/fields/Document.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/fields') diff --git a/src/fields/Document.ts b/src/fields/Document.ts index fcc8adbcf..0c2ad0fdb 100644 --- a/src/fields/Document.ts +++ b/src/fields/Document.ts @@ -49,7 +49,7 @@ export class Document extends Field { if (this.fields.has(key.Id)) { field = this.fields.get(key.Id)!.field; } else { - field = ""; + field = FieldWaiting; } } } else { @@ -68,7 +68,7 @@ export class Document extends Field { if (this.fields.has(key.Id)) { field = this.fields.get(key.Id)!.field; } else { - field = ""; + field = FieldWaiting; } break; } -- cgit v1.2.3-70-g09d2 From 67a2592f5b267df3f3d2e1e96eb6dd6c0c8032a5 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Wed, 20 Feb 2019 20:18:21 -0500 Subject: Added undo/redo --- src/client/util/UndoManager.ts | 121 +++++++++++++++++++++ src/client/views/Main.tsx | 24 ++++ .../views/collections/CollectionFreeFormView.tsx | 2 + src/fields/BasicField.ts | 11 +- src/fields/Document.ts | 10 +- src/fields/ListField.ts | 16 ++- 6 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 src/client/util/UndoManager.ts (limited to 'src/fields') diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts new file mode 100644 index 000000000..34910bac3 --- /dev/null +++ b/src/client/util/UndoManager.ts @@ -0,0 +1,121 @@ +import { observable, action } from "mobx"; +import { Opt } from "../../fields/Field"; + +export function undoBatch(target: any, key: string | symbol, descriptor?: TypedPropertyDescriptor): any { + let fn: (...args: any[]) => any; + let patchedFn: Opt<(...args: any[]) => any>; + + if (descriptor) { + fn = descriptor.value; + } + + return { + configurable: true, + enumerable: false, + get() { + if (!patchedFn) { + patchedFn = (...args: any[]) => { + try { + UndoManager.StartBatch() + return fn.call(this, ...args) + } finally { + UndoManager.EndBatch() + } + }; + } + return patchedFn; + }, + set(newFn: any) { + patchedFn = undefined; + fn = newFn; + } + } +} +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/Main.tsx b/src/client/views/Main.tsx index f44ad0a74..6d3d4d4c2 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -16,6 +16,7 @@ import { MessageStore, DocumentTransfer } from '../../server/Message'; import { Transform } from '../util/Transform'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { FieldWaiting } from '../../fields/Field'; +import { UndoManager } from '../util/UndoManager'; configure({ @@ -94,6 +95,11 @@ Documents.initProtos(() => { x: 0, y: 300, width: 200, height: 200, title: "added note" })); }) + let addSchemaNode = action(() => { + mainfreeform.GetList(KeyStore.Data, []).push(Documents.SchemaDocument([Documents.TextDocument()], { + x: 0, y: 300, width: 200, height: 200, title: "added note" + })); + }) let clearDatabase = action(() => { Utils.Emit(Server.Socket, MessageStore.DeleteAll, {}); @@ -126,12 +132,30 @@ Documents.initProtos(() => { left: '0px', width: '150px' }} onClick={addColNode}>Add Collection + + + ), document.getElementById('root')); }) diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx index 54757cce5..a6f34dfdf 100644 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -13,6 +13,7 @@ import { Documents } from "../../documents/Documents"; import { FieldWaiting } from "../../../fields/Field"; import { Transform } from "../../util/Transform"; import { DocumentView } from "../nodes/DocumentView"; +import { undoBatch } from "../../util/UndoManager"; @observer export class CollectionFreeFormView extends CollectionViewBase { @@ -37,6 +38,7 @@ export class CollectionFreeFormView extends CollectionViewBase { @computed get resizeScaling() { return this.isAnnotationOverlay ? this.props.Document.GetNumber(KeyStore.Width, 0) / this.nativeWidth : 1; } + @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { const doc: DocumentView = de.data["document"]; diff --git a/src/fields/BasicField.ts b/src/fields/BasicField.ts index 8728b7145..91977b243 100644 --- a/src/fields/BasicField.ts +++ b/src/fields/BasicField.ts @@ -1,6 +1,7 @@ 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 extends Field { constructor(data: T, save: boolean, id?: FieldId) { @@ -27,9 +28,15 @@ export abstract class BasicField extends Field { } set Data(value: T) { - if (this.data != value) { - this.data = value; + if (this.data === value) { + return; } + let oldValue = this.data; + this.data = value; + UndoManager.AddEvent({ + undo: () => this.Data = oldValue, + redo: () => this.Data = value + }) Server.UpdateField(this); } diff --git a/src/fields/Document.ts b/src/fields/Document.ts index 0c2ad0fdb..d8522fb5b 100644 --- a/src/fields/Document.ts +++ b/src/fields/Document.ts @@ -7,6 +7,7 @@ import { TextField } from "./TextField"; import { ListField } from "./ListField"; import { Server } from "../client/Server"; import { Types } from "../server/Message"; +import { UndoManager } from "../client/util/UndoManager"; export class Document extends Field { public fields: ObservableMap = new ObservableMap(); @@ -127,7 +128,8 @@ export class Document extends Field { @action Set(key: Key, field: Field | undefined): void { - console.log("Assign: " + key.Name + " = " + (field ? field.GetValue() : "") + " (" + (field ? field.Id : "") + ")"); + let old = this.fields.get(key.Id); + let oldField = old ? old.field : undefined; if (field) { this.fields.set(key.Id, { key, field }); this._proxies.set(key.Id, field.Id) @@ -137,6 +139,12 @@ export class Document extends Field { 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); } diff --git a/src/fields/ListField.ts b/src/fields/ListField.ts index 2e192bf90..ad5374dc9 100644 --- a/src/fields/ListField.ts +++ b/src/fields/ListField.ts @@ -1,9 +1,10 @@ import { Field, FieldId, FieldValue, Opt } from "./Field"; import { BasicField } from "./BasicField"; import { Types } from "../server/Message"; -import { observe, action } from "mobx"; +import { observe, action, IArrayChange, IArraySplice, IObservableArray } from "mobx"; import { Server } from "../client/Server"; import { ServerUtils } from "../server/ServerUtil"; +import { UndoManager } from "../client/util/UndoManager"; export class ListField extends BasicField { private _proxies: string[] = [] @@ -13,8 +14,19 @@ export class ListField extends BasicField { if (save) { Server.UpdateField(this); } - observe(this.Data, () => { + observe(this.Data as IObservableArray, (change: IArrayChange | IArraySplice) => { 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); }) } -- cgit v1.2.3-70-g09d2 From 9e4403f9c14cdb7e05901af5f8509753269eeb07 Mon Sep 17 00:00:00 2001 From: bob Date: Thu, 21 Feb 2019 14:13:44 -0500 Subject: clean up --- src/client/util/UndoManager.ts | 1 - .../views/collections/CollectionDockingView.tsx | 21 ++++++++++----------- .../views/collections/CollectionSchemaView.tsx | 2 +- .../views/nodes/CollectionFreeFormDocumentView.tsx | 12 +++--------- src/fields/Document.ts | 5 ----- src/fields/ListField.ts | 9 ++++----- 6 files changed, 18 insertions(+), 32 deletions(-) (limited to 'src/fields') diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index edb75b55f..46ad558f3 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -1,5 +1,4 @@ import { observable, action } from "mobx"; -import { Opt } from "../../fields/Field"; function propertyDecorator(target: any, key: string | symbol) { Object.defineProperty(target, key, { diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 35b130a9a..60dc24b5f 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,22 +1,21 @@ 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, observable, reaction, trace, untracked } from "mobx"; -import { DragManager } from "../../util/DragManager"; -import { DocumentView } from "../nodes/DocumentView"; -import { Document } from "../../../fields/Document"; -import "./CollectionDockingView.scss"; -import { CollectionViewBase, COLLECTION_BORDER_WIDTH, CollectionViewProps } from "./CollectionViewBase"; -import React = require("react"); +import { action, computed, observable, reaction } from "mobx"; +import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; import Measure from "react-measure"; +import { Document } from "../../../fields/Document"; +import { FieldId, Opt } from "../../../fields/Field"; +import { KeyStore } from "../../../fields/KeyStore"; import { Utils } from "../../../Utils"; -import { FieldId } from "../../../fields/Field"; import { Server } from "../../Server"; -import { observer } from "mobx-react"; -import { KeyStore } from "../../../fields/KeyStore"; -import { Opt } from "../../../fields/Field"; +import { DragManager } from "../../util/DragManager"; import { undoBatch } from "../../util/UndoManager"; +import { DocumentView } from "../nodes/DocumentView"; +import "./CollectionDockingView.scss"; +import { CollectionViewBase, CollectionViewProps, COLLECTION_BORDER_WIDTH } from "./CollectionViewBase"; +import React = require("react"); @observer export class CollectionDockingView extends CollectionViewBase { diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index f3217d55d..5ec288b13 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -1,5 +1,5 @@ import React = require("react") -import { action, computed, observable } from "mobx"; +import { action, observable } from "mobx"; import { observer } from "mobx-react"; import Measure from "react-measure"; import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table"; diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 7cad6ffc1..5568935fa 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,17 +1,11 @@ -import { action, computed } from "mobx"; +import { computed } from "mobx"; import { observer } from "mobx-react"; 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 { Utils } from "../../../Utils"; -import { Transform } from "../../util/Transform"; @observer diff --git a/src/fields/Document.ts b/src/fields/Document.ts index d8522fb5b..6667485b6 100644 --- a/src/fields/Document.ts +++ b/src/fields/Document.ts @@ -152,16 +152,13 @@ export class Document extends Field { SetData(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 @@ -213,14 +210,12 @@ export class Document extends Field { } ToJson(): { type: Types, data: [string, string][], _id: string } { - // console.log(this.fields) let fields: [string, string][] = [] this._proxies.forEach((field, key) => { if (field) { fields.push([key, field as string]) } }); - // console.log(fields) return { type: Types.Document, diff --git a/src/fields/ListField.ts b/src/fields/ListField.ts index ad5374dc9..75c2eb343 100644 --- a/src/fields/ListField.ts +++ b/src/fields/ListField.ts @@ -1,10 +1,9 @@ -import { Field, FieldId, FieldValue, Opt } from "./Field"; -import { BasicField } from "./BasicField"; -import { Types } from "../server/Message"; -import { observe, action, IArrayChange, IArraySplice, IObservableArray } from "mobx"; +import { action, IArrayChange, IArraySplice, IObservableArray, observe } from "mobx"; import { Server } from "../client/Server"; -import { ServerUtils } from "../server/ServerUtil"; import { UndoManager } from "../client/util/UndoManager"; +import { Types } from "../server/Message"; +import { BasicField } from "./BasicField"; +import { Field, FieldId } from "./Field"; export class ListField extends BasicField { private _proxies: string[] = [] -- cgit v1.2.3-70-g09d2 From 38ca0618dcdeb57d5a91557b392c1aff095868be Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Fri, 22 Feb 2019 03:01:30 -0500 Subject: Added CollectionView Switched sub-collections to not inherit from CollectionViewBase --- src/client/Server.ts | 5 +- src/client/documents/Documents.ts | 81 +++++--------- src/client/views/Main.tsx | 8 +- .../views/collections/CollectionDockingView.tsx | 11 +- .../views/collections/CollectionFreeFormView.tsx | 25 ++--- .../views/collections/CollectionSchemaView.tsx | 13 ++- src/client/views/collections/CollectionView.tsx | 116 +++++++++++++++++++++ .../views/collections/CollectionViewBase.tsx | 61 ----------- src/client/views/nodes/DocumentView.tsx | 13 ++- src/client/views/nodes/FieldView.tsx | 3 +- src/fields/BasicField.ts | 6 +- src/fields/Document.ts | 9 ++ src/fields/KeyStore.ts | 1 + src/fields/ListField.ts | 19 +++- 14 files changed, 212 insertions(+), 159 deletions(-) create mode 100644 src/client/views/collections/CollectionView.tsx delete mode 100644 src/client/views/collections/CollectionViewBase.tsx (limited to 'src/fields') diff --git a/src/client/Server.ts b/src/client/Server.ts index 3e61729ab..06ac22c61 100644 --- a/src/client/Server.ts +++ b/src/client/Server.ts @@ -60,13 +60,16 @@ export class Server { }); } - public static GetDocumentField(doc: Document, key: Key) { + 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) => { if (fieldfromserver) { doc.fields.set(key.Id, { key, field: fieldfromserver }); + if (callback) { + callback(fieldfromserver); + } } })); } diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 920068273..bfa6cb7a9 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -11,6 +11,7 @@ import { ImageField } from "../../fields/ImageField"; import { ImageBox } from "../views/nodes/ImageBox"; import { CollectionFreeFormView } from "../views/collections/CollectionFreeFormView"; import { FieldId } from "../../fields/Field"; +import { CollectionView, CollectionViewType } from "../views/collections/CollectionView"; interface DocumentOptions { x?: number; @@ -24,12 +25,10 @@ interface DocumentOptions { export namespace Documents { export function initProtos(callback: () => void) { - Server.GetFields([collectionProtoId, textProtoId, imageProtoId, schemaProtoId, dockProtoId], (fields) => { + Server.GetFields([collectionProtoId, textProtoId, imageProtoId], (fields) => { collectionProto = fields[collectionProtoId] as Document; imageProto = fields[imageProtoId] as Document; textProto = fields[textProtoId] as Document; - dockProto = fields[dockProtoId] as Document; - schemaProto = fields[schemaProtoId] as Document; callback() }); } @@ -83,52 +82,6 @@ export namespace Documents { return doc; } - let schemaProto: Document; - const schemaProtoId = "schemaProto"; - function GetSchemaPrototype(): Document { - if (!schemaProto) { - schemaProto = new Document(schemaProtoId); - 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 SchemaDocument(documents: Array, options: DocumentOptions = {}): Document { - let doc = GetSchemaPrototype().MakeDelegate(); - setupOptions(doc, options); - doc.Set(KeyStore.Data, new ListField(documents)); - return doc; - } - - - let dockProto: Document; - const dockProtoId = "dockProto"; - function GetDockPrototype(): Document { - if (!dockProto) { - dockProto = new Document(); - dockProto.Set(KeyStore.X, new NumberField(0)); - dockProto.Set(KeyStore.Y, new NumberField(0)); - dockProto.Set(KeyStore.Width, new NumberField(300)); - dockProto.Set(KeyStore.Height, new NumberField(150)); - dockProto.Set(KeyStore.Layout, new TextField(CollectionDockingView.LayoutString())); - dockProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data])); - } - return dockProto; - } - - export function DockDocument(config: string, options: DocumentOptions = {}, id?: string): Document { - let doc = GetDockPrototype().MakeDelegate(id); - setupOptions(doc, options); - doc.SetText(KeyStore.Data, config); - return doc; - } - - let imageProto: Document; const imageProtoId = "imageProto"; function GetImagePrototype(): Document { @@ -141,7 +94,8 @@ export namespace Documents { 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.Layout, new TextField(CollectionView.LayoutString("AnnotationsKey"))); + imageProto.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform) imageProto.Set(KeyStore.BackgroundLayout, new TextField(ImageBox.LayoutString())); // imageProto.SetField(KeyStore.Layout, new TextField('
')); imageProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data, KeyStore.Annotations])); @@ -165,23 +119,36 @@ export namespace Documents { function GetCollectionPrototype(): Document { if (!collectionProto) { collectionProto = new Document(collectionProtoId); - 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.Layout, new TextField(CollectionView.LayoutString("DataKey"))); collectionProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data])); } return collectionProto; } - export function CollectionDocument(documents: Array, options: DocumentOptions = {}, id?: string): Document { + export function CollectionDocument(data: Array | string, viewType: CollectionViewType, options: DocumentOptions = {}, id?: string): Document { let doc = GetCollectionPrototype().MakeDelegate(id); setupOptions(doc, options); - doc.Set(KeyStore.Data, new ListField(documents)); + if (typeof data === "string") { + doc.SetText(KeyStore.Data, data); + } else { + doc.SetData(KeyStore.Data, data, ListField); + } + doc.SetNumber(KeyStore.ViewType, viewType); return doc; } + + export function FreeformDocument(documents: Array, options: DocumentOptions, id?: string) { + return CollectionDocument(documents, CollectionViewType.Freeform, options, id) + } + + export function SchemaDocument(documents: Array, options: DocumentOptions, id?: string) { + return CollectionDocument(documents, CollectionViewType.Schema, options, id) + } + + export function DockDocument(config: string, options: DocumentOptions, id?: string) { + return CollectionDocument(config, CollectionViewType.Docking, options, id) + } } \ No newline at end of file diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index c7a6a44e8..fe1a999ec 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -61,16 +61,14 @@ Documents.initProtos(() => { let mainfreeform: Document; if (res) { mainContainer = ServerUtils.FromJson(res) as Document; - mainfreeform = mainContainer.Get(KeyStore.ActiveFrame) as Document; - if (!mainfreeform) - Server.GetField(mainContainer._proxies.get(KeyStore.ActiveFrame.Id)!, (field) => mainfreeform = field as Document); + mainContainer.GetAsync(KeyStore.ActiveFrame, field => mainfreeform = field as Document); } else { mainContainer = Documents.DockDocument(JSON.stringify({ content: [{ type: 'row', content: [] }] }), { title: "main container" }, mainDocId); Utils.Emit(Server.Socket, MessageStore.AddDocument, new DocumentTransfer(mainContainer.ToJson())) setTimeout(() => { - mainfreeform = Documents.CollectionDocument([], { x: 0, y: 400, title: "mini collection" }); + mainfreeform = Documents.FreeformDocument([], { x: 0, y: 400, title: "mini collection" }); Utils.Emit(Server.Socket, MessageStore.AddDocument, new DocumentTransfer(mainfreeform.ToJson())); var docs = [mainfreeform].map(doc => CollectionDockingView.makeDocumentConfig(doc)); @@ -90,7 +88,7 @@ Documents.initProtos(() => { })); }) let addColNode = action(() => { - mainfreeform.GetList(KeyStore.Data, []).push(Documents.CollectionDocument([], { + mainfreeform.GetList(KeyStore.Data, []).push(Documents.FreeformDocument([], { x: 0, y: 300, width: 200, height: 200, title: "added note" })); }) diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 60dc24b5f..6d91aef5e 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -14,13 +14,12 @@ import { DragManager } from "../../util/DragManager"; import { undoBatch } from "../../util/UndoManager"; import { DocumentView } from "../nodes/DocumentView"; import "./CollectionDockingView.scss"; -import { CollectionViewBase, CollectionViewProps, COLLECTION_BORDER_WIDTH } from "./CollectionViewBase"; +import { CollectionViewProps, COLLECTION_BORDER_WIDTH, SubCollectionViewProps } from "./CollectionView"; import React = require("react"); @observer -export class CollectionDockingView extends CollectionViewBase { +export class CollectionDockingView extends React.Component { public static Instance: CollectionDockingView; - public static LayoutString() { return CollectionViewBase.LayoutString("CollectionDockingView"); } public static makeDocumentConfig(document: Document) { return { type: 'react-component', @@ -41,7 +40,7 @@ export class CollectionDockingView extends CollectionViewBase { private _containerRef = React.createRef(); private _fullScreen: any = null; - constructor(props: CollectionViewProps) { + constructor(props: SubCollectionViewProps) { super(props); CollectionDockingView.Instance = this; (window as any).React = React; @@ -187,7 +186,7 @@ export class CollectionDockingView extends CollectionViewBase { } @action onPointerDown = (e: React.PointerEvent): void => { - if (e.button === 2 && this.active) { + if (e.button === 2 && this.props.active()) { e.stopPropagation(); e.preventDefault(); } else { @@ -195,7 +194,7 @@ export class CollectionDockingView extends CollectionViewBase { 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.active) { + if (e.buttons === 1 && this.props.active()) { e.stopPropagation(); } } diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx index 986bcdcee..bd7ca5b6f 100644 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -4,7 +4,7 @@ import { action, computed } from "mobx"; import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; import { DragManager } from "../../util/DragManager"; import "./CollectionFreeFormView.scss"; -import { CollectionViewBase, COLLECTION_BORDER_WIDTH, CollectionViewProps } from "./CollectionViewBase"; +import { COLLECTION_BORDER_WIDTH, CollectionViewProps, SubCollectionViewProps } from "./CollectionView"; import { KeyStore } from "../../../fields/KeyStore"; import { Document } from "../../../fields/Document"; import { ListField } from "../../../fields/ListField"; @@ -16,8 +16,7 @@ import { DocumentView } from "../nodes/DocumentView"; import { undoBatch } from "../../util/UndoManager"; @observer -export class CollectionFreeFormView extends CollectionViewBase { - public static LayoutString(fieldKey: string = "DataKey") { return CollectionViewBase.LayoutString("CollectionFreeFormView", fieldKey); } +export class CollectionFreeFormView extends React.Component { private _canvasRef = React.createRef(); private _lastX: number = 0; private _lastY: number = 0; @@ -38,7 +37,7 @@ export class CollectionFreeFormView extends CollectionViewBase { @computed get resizeScaling() { return this.isAnnotationOverlay ? this.props.Document.GetNumber(KeyStore.Width, 0) / this.nativeWidth : 1; } - constructor(props: CollectionViewProps) { + constructor(props: SubCollectionViewProps) { super(props); } @@ -46,9 +45,11 @@ export class CollectionFreeFormView extends CollectionViewBase { @action drop = (e: Event, de: DragManager.DropEvent) => { const doc: DocumentView = de.data["document"]; - if (doc.props.ContainingCollectionView && doc.props.ContainingCollectionView !== this) { - doc.props.ContainingCollectionView.removeDocument(doc.props.Document); - this.addDocument(doc.props.Document); + if (doc.props.ContainingCollectionView && doc.props.ContainingCollectionView !== this.props.CollectionView) { + if (doc.props.RemoveDocument) { + doc.props.RemoveDocument(doc.props.Document); + } + this.props.addDocument(doc.props.Document); } const xOffset = de.data["xOffset"] as number || 0; const yOffset = de.data["yOffset"] as number || 0; @@ -79,7 +80,7 @@ export class CollectionFreeFormView extends CollectionViewBase { @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); @@ -104,7 +105,7 @@ export class CollectionFreeFormView extends CollectionViewBase { @action onPointerMove = (e: PointerEvent): void => { - if (!e.cancelBubble && this.active) { + if (!e.cancelBubble && this.props.active()) { e.preventDefault(); e.stopPropagation(); let x = this.props.Document.GetNumber(KeyStore.PanX, 0); @@ -248,13 +249,13 @@ export class CollectionFreeFormView extends CollectionViewBase { {this.props.BackgroundView ? this.props.BackgroundView() : null} {lvalue.Data.map(doc => { return (); + ContainingCollectionView={this.props.CollectionView} />); })}
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 5ec288b13..9405c820f 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -13,12 +13,10 @@ import { EditableView } from "../EditableView"; import { DocumentView } from "../nodes/DocumentView"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; import "./CollectionSchemaView.scss"; -import { CollectionViewBase, COLLECTION_BORDER_WIDTH } from "./CollectionViewBase"; +import { COLLECTION_BORDER_WIDTH, CollectionViewProps, SubCollectionViewProps } from "./CollectionView"; @observer -export class CollectionSchemaView extends CollectionViewBase { - public static LayoutString(fieldKey: string = "DataKey") { return CollectionViewBase.LayoutString("CollectionSchemaView", fieldKey); } - +export class CollectionSchemaView extends React.Component { private _mainCont = React.createRef(); private DIVIDER_WIDTH = 5; @@ -34,6 +32,7 @@ export class CollectionSchemaView extends CollectionViewBase { doc: rowProps.value[0], fieldKey: rowProps.value[1], isSelected: () => false, + select: () => { }, isTopMost: false } let contents = ( @@ -112,7 +111,7 @@ export class CollectionSchemaView extends CollectionViewBase { // e.preventDefault(); // } else { - if (e.buttons === 1 && this.active) { + if (e.buttons === 1 && this.props.active()) { e.stopPropagation(); } } @@ -145,12 +144,12 @@ export class CollectionSchemaView extends CollectionViewBase { {({ measureRef }) =>
+ ContainingCollectionView={this.props.CollectionView} />
} diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx new file mode 100644 index 000000000..651d85879 --- /dev/null +++ b/src/client/views/collections/CollectionView.tsx @@ -0,0 +1,116 @@ +import { action, computed } from "mobx"; +import { observer } from "mobx-react"; +import { Document } from "../../../fields/Document"; +import { Key } from "../../../fields/Key"; +import { ListField } from "../../../fields/ListField"; +import { SelectionManager } from "../../util/SelectionManager"; +import { ContextMenu } from "../ContextMenu"; +import React = require("react"); +import { Transform } from "../../util/Transform"; +import { KeyStore } from "../../../fields/KeyStore"; +import { NumberField } from "../../../fields/NumberField"; +import { CollectionFreeFormView } from "./CollectionFreeFormView"; +import { CollectionDockingView } from "./CollectionDockingView"; +import { CollectionSchemaView } from "./CollectionSchemaView"; +import { Opt } from "../../../fields/Field"; + + +export interface CollectionViewProps { + fieldKey: Key; + Document: Document; + ScreenToLocalTransform: () => Transform; + isSelected: () => boolean; + isTopMost: boolean; + select: (ctrlPressed: boolean) => void; + BackgroundView?: () => JSX.Element; +} + +export interface SubCollectionViewProps extends CollectionViewProps { + active: () => boolean; + addDocument: (doc: Document) => void; + removeDocument: (doc: Document) => boolean; + CollectionView: Opt; +} + +export enum CollectionViewType { + Invalid, + Freeform, + Schema, + Docking, +} + +export const COLLECTION_BORDER_WIDTH = 2; + +@observer +export class CollectionView extends React.Component { + + public static LayoutString(fieldKey: string = "DataKey") { + return ``; + } + 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 => { + //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()) + value.push(doc); + } + + @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()) + let index = value.indexOf(doc); + 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 === "") { + 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 () + case CollectionViewType.Schema: + return () + case CollectionViewType.Docking: + return () + default: + return
+ } + } +} \ No newline at end of file diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx deleted file mode 100644 index 0acc890d8..000000000 --- a/src/client/views/collections/CollectionViewBase.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { action, computed } from "mobx"; -import { observer } from "mobx-react"; -import { Document } from "../../../fields/Document"; -import { Key } from "../../../fields/Key"; -import { ListField } from "../../../fields/ListField"; -import { SelectionManager } from "../../util/SelectionManager"; -import { ContextMenu } from "../ContextMenu"; -import React = require("react"); -import { Transform } from "../../util/Transform"; - - -export interface CollectionViewProps { - fieldKey: Key; - Document: Document; - ScreenToLocalTransform: () => Transform; - isSelected: () => boolean; - isTopMost: boolean; - select: (ctrlPressed: boolean) => void; - BackgroundView?: () => JSX.Element; -} - -export const COLLECTION_BORDER_WIDTH = 2; - -@observer -export class CollectionViewBase extends React.Component { - - public static LayoutString(collectionType: string, fieldKey: string = "DataKey") { - return `<${collectionType} Document={Document} - ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} isSelected={isSelected} select={select} - isTopMost={isTopMost} BackgroundView={BackgroundView} />`; - } - @computed - public get active(): boolean { - 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 => { - //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()) - value.push(doc); - } - - @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()) - let index = value.indexOf(doc); - if (index !== -1) { - value.splice(index, 1) - - SelectionManager.DeselectAll() - ContextMenu.Instance.clearItems() - return true; - } - return false - } - -} \ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 7cf00a116..2114a5697 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -9,7 +9,7 @@ import { Utils } from "../../../Utils"; import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionFreeFormView } from "../collections/CollectionFreeFormView"; import { CollectionSchemaView } from "../collections/CollectionSchemaView"; -import { CollectionViewBase, COLLECTION_BORDER_WIDTH } from "../collections/CollectionViewBase"; +import { COLLECTION_BORDER_WIDTH, CollectionView, CollectionViewType } from "../collections/CollectionView"; import { FormattedTextBox } from "../nodes/FormattedTextBox"; import { ImageBox } from "../nodes/ImageBox"; import "./DocumentView.scss"; @@ -22,7 +22,7 @@ import { TextField } from "../../../fields/TextField"; const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this? export interface DocumentViewProps { - ContainingCollectionView: Opt; + ContainingCollectionView: Opt; Document: Document; AddDocument?: (doc: Document) => void; @@ -114,7 +114,7 @@ export class DocumentView extends React.Component { @computed get active(): boolean { return SelectionManager.IsSelected(this) || !this.props.ContainingCollectionView || - this.props.ContainingCollectionView.active; + this.props.ContainingCollectionView.active(); } private _contextMenuCanOpen = false; @@ -233,6 +233,9 @@ export class DocumentView extends React.Component { 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: "Freeform", event: () => { this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform) } }) + ContextMenu.Instance.addItem({ description: "Schema", event: () => { this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Schema) } }) + ContextMenu.Instance.addItem({ description: "Docking", event: () => { this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) } }) ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) SelectionManager.SelectDoc(this, e.ctrlKey); } @@ -275,7 +278,7 @@ export class DocumentView extends React.Component { let backgroundLayout = this.backgroundLayout; if (backgroundLayout) { let backgroundView = () => ( { onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} > boolean; + select: () => void; isTopMost: boolean; } @observer export class FieldView extends React.Component { - public static LayoutString(fieldType: { name: string }) { return `<${fieldType.name} doc={Document} DocumentViewForField={DocumentView} fieldKey={DataKey} isSelected={isSelected} isTopMost={isTopMost} />`; } + public static LayoutString(fieldType: { name: string }) { return `<${fieldType.name} doc={Document} DocumentViewForField={DocumentView} fieldKey={DataKey} isSelected={isSelected} select={select} isTopMost={isTopMost} />`; } @computed get field(): FieldValue { const { doc, fieldKey } = this.props; diff --git a/src/fields/BasicField.ts b/src/fields/BasicField.ts index 91977b243..a92c4a236 100644 --- a/src/fields/BasicField.ts +++ b/src/fields/BasicField.ts @@ -32,7 +32,7 @@ export abstract class BasicField extends Field { return; } let oldValue = this.data; - this.data = value; + this.setData(value); UndoManager.AddEvent({ undo: () => this.Data = oldValue, redo: () => this.Data = value @@ -40,6 +40,10 @@ export abstract class BasicField extends Field { Server.UpdateField(this); } + protected setData(value: T) { + this.data = value; + } + @action TrySetValue(value: any): boolean { if (typeof value == typeof this.data) { diff --git a/src/fields/Document.ts b/src/fields/Document.ts index 6667485b6..ff13732b3 100644 --- a/src/fields/Document.ts +++ b/src/fields/Document.ts @@ -90,6 +90,15 @@ export class Document extends Field { 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(key: Key, ctor: { new(...args: any[]): T }, ignoreProto: boolean = false): FieldValue { var getfield = this.Get(key, ignoreProto); if (getfield != FieldWaiting) { diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts index 7056886aa..6d6c6a546 100644 --- a/src/fields/KeyStore.ts +++ b/src/fields/KeyStore.ts @@ -16,6 +16,7 @@ export namespace KeyStore { 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 LayoutKeys = new Key("LayoutKeys"); diff --git a/src/fields/ListField.ts b/src/fields/ListField.ts index 75c2eb343..700600804 100644 --- a/src/fields/ListField.ts +++ b/src/fields/ListField.ts @@ -1,4 +1,4 @@ -import { action, IArrayChange, IArraySplice, IObservableArray, observe } from "mobx"; +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"; @@ -13,7 +13,12 @@ export class ListField extends BasicField { if (save) { Server.UpdateField(this); } - observe(this.Data as IObservableArray, (change: IArrayChange | IArraySplice) => { + this.observeList(); + } + + private observeDisposer: Lambda | undefined; + private observeList(): void { + this.observeDisposer = observe(this.Data as IObservableArray, (change: IArrayChange | IArraySplice) => { this.updateProxies() if (change.type == "splice") { UndoManager.AddEvent({ @@ -27,7 +32,15 @@ export class ListField extends BasicField { }) } Server.UpdateField(this); - }) + }); + } + + protected setData(value: T[]) { + if (this.observeDisposer) { + this.observeDisposer() + } + this.data = observable(value); + this.observeList(); } private updateProxies() { -- cgit v1.2.3-70-g09d2 From 530be8dc0f049a4c05431c7b10ae47e46575fcbe Mon Sep 17 00:00:00 2001 From: bob Date: Fri, 22 Feb 2019 17:10:44 -0500 Subject: fixed re-render issue for collection free form documents. --- src/client/views/DocumentDecorations.tsx | 5 +-- src/client/views/Main.tsx | 6 +-- .../views/collections/CollectionDockingView.tsx | 10 ++--- .../views/collections/CollectionFreeFormView.tsx | 46 ++++++------------- .../views/collections/CollectionSchemaView.tsx | 52 ++++++++++------------ .../views/nodes/CollectionFreeFormDocumentView.tsx | 9 ++-- src/client/views/nodes/DocumentView.tsx | 8 ++-- src/fields/Document.ts | 4 ++ 8 files changed, 59 insertions(+), 81 deletions(-) (limited to 'src/fields') diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 7b64a4c2c..efd50e49e 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -5,7 +5,6 @@ import { observer } from "mobx-react"; import './DocumentDecorations.scss' import { KeyStore } from '../../fields/KeyStore' import { NumberField } from "../../fields/NumberField"; -import { number } from "prop-types"; @observer export class DocumentDecorations extends React.Component { @@ -26,9 +25,9 @@ export class DocumentDecorations extends React.Component { if (element.props.isTopMost) { return bounds; } - let transform = (element.props.ScreenToLocalTransform().scale(element.props.Scaling)).inverse(); + let transform = (element.props.ScreenToLocalTransform().scale(element.props.ContentScaling())).inverse(); var [sptX, sptY] = transform.transformPoint(0, 0); - let [bptX, bptY] = transform.transformPoint(element.props.PanelWidth, element.props.PanelHeight); + let [bptX, bptY] = transform.transformPoint(element.props.PanelWidth(), element.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) diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 6f6a89839..661a2ac20 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -106,9 +106,9 @@ Documents.initProtos(() => {
Transform.Identity} - Scaling={1} - PanelWidth={0} - PanelHeight={0} + ContentScaling={() => 1} + PanelWidth={() => 0} + PanelHeight={() => 0} isTopMost={true} ContainingCollectionView={undefined} /> diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 7ac8ea5e4..86dc66e39 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -272,13 +272,13 @@ export class DockedFrameRenderer extends React.Component { Server.GetField(this.props.documentId, action((f: Opt) => this._document = f as Document)); } - @computed private get _nativeWidth() { return this._document!.GetNumber(KeyStore.NativeWidth, 0); } - @computed private get _nativeHeight() { return this._document!.GetNumber(KeyStore.NativeHeight, 0); } - @computed private get _parentScaling() { return this._panelWidth / (this._nativeWidth ? this._nativeWidth : this._panelWidth); }; // 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, 0); } + private _nativeHeight = () => { return this._document!.GetNumber(KeyStore.NativeHeight, 0); } + private _contentScaling = () => { return this._panelWidth / (this._nativeWidth() ? this._nativeWidth() : this._panelWidth); } ScreenToLocalTransform = () => { let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current!); - return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(scale / this._parentScaling) + return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(scale / this._contentScaling()) } render() { @@ -289,7 +289,7 @@ export class DockedFrameRenderer extends React.Component { { return this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH).transform(this.getLocalTransform()) } getLocalTransform = (): Transform => { - const [x, y] = this.translate; - return Transform.Identity.translate(-x, -y).scale(1 / this.scale); + return Transform.Identity.translate(-this.panX, -this.panY).scale(1 / this.scale); } + noScaling = () => 1; + @computed get views() { const { fieldKey, Document } = this.props; @@ -186,9 +166,9 @@ export class CollectionFreeFormView extends CollectionViewBase { RemoveDocument={this.props.removeDocument} ScreenToLocalTransform={this.getTransform} isTopMost={false} - Scaling={1} - PanelWidth={doc.GetNumber(KeyStore.Width, 0)} - PanelHeight={doc.GetNumber(KeyStore.Height, 0)} + ContentScaling={this.noScaling} + PanelWidth={doc.Width} + PanelHeight={doc.Height} ContainingCollectionView={this.props.CollectionView} />); }) } diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 7e7d23fe4..ca47f6998 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -19,14 +19,14 @@ import { CollectionViewBase } from "./CollectionViewBase"; @observer export class CollectionSchemaView extends CollectionViewBase { private _mainCont = React.createRef(); - private DIVIDER_WIDTH = 5; - @observable - selectedIndex = 0; - - @observable - _splitPercentage: number = 50; + @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 = { @@ -67,7 +67,6 @@ export class CollectionSchemaView extends CollectionViewBase { ) } - private getTrProps: ComponentPropsGetterR = (state, rowInfo) => { const that = this; if (!rowInfo) { @@ -75,7 +74,7 @@ export class CollectionSchemaView extends CollectionViewBase { } return { onClick: action((e: React.MouseEvent, handleOriginal: Function) => { - that.selectedIndex = rowInfo.index; + that._selectedIndex = rowInfo.index; this._splitPercentage += 0.05; // bcz - ugh - needed to force Measure to do its thing and call onResize if (handleOriginal) { @@ -83,8 +82,8 @@ export class CollectionSchemaView extends CollectionViewBase { } }), style: { - background: rowInfo.index == this.selectedIndex ? "#00afec" : "white", - color: rowInfo.index == this.selectedIndex ? "white" : "black" + background: rowInfo.index == this._selectedIndex ? "#00afec" : "white", + color: rowInfo.index == this._selectedIndex ? "white" : "black" } }; } @@ -94,7 +93,6 @@ export class CollectionSchemaView extends CollectionViewBase { let nativeWidth = this._mainCont.current!.getBoundingClientRect(); this._splitPercentage = Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100); } - onDividerUp = (e: PointerEvent): void => { document.removeEventListener("pointermove", this.onDividerMove); document.removeEventListener('pointerup', this.onDividerUp); @@ -118,39 +116,37 @@ export class CollectionSchemaView extends CollectionViewBase { } } - - getTransform = (): Transform => { - return this.props.ScreenToLocalTransform().translate(- COLLECTION_BORDER_WIDTH - this.DIVIDER_WIDTH - this._dividerX, - COLLECTION_BORDER_WIDTH).scale(1 / this._parentScaling); - } - @action setScaling = (r: any) => { const children = this.props.Document.GetList(this.props.fieldKey, []); - const selected = children.length > this.selectedIndex ? children[this.selectedIndex] : undefined; + 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._parentScaling = r.entry.width / selected!.GetNumber(KeyStore.NativeWidth, r.entry.width); + 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 _parentScaling = 1; // used to transfer the dimensions of the content pane in the DOM to the ParentScaling prop of the DocumentView - @observable _dividerX = 0; - @observable _panelWidth = 0; - @observable _panelHeight = 0; render() { const columns = this.props.Document.GetList(KeyStore.ColumnsKey, [KeyStore.Title, KeyStore.Data, KeyStore.Author]) const children = this.props.Document.GetList(this.props.fieldKey, []); - const selected = children.length > this.selectedIndex ? children[this.selectedIndex] : undefined; - let content = this.selectedIndex == -1 || !selected ? (null) : ( + const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined; + let content = this._selectedIndex == -1 || !selected ? (null) : ( {({ measureRef }) =>
} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index ad6756918..17123bf52 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -24,7 +24,7 @@ export class CollectionFreeFormDocumentView extends React.Component { 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)).scale(1 / this.parentScaling); + translate(-this.props.Document.GetNumber(KeyStore.X, 0), -this.props.Document.GetNumber(KeyStore.Y, 0)).scale(1 / this.contentScaling()); } @computed get docView() { return } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index c026e13cd..34a230669 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -30,9 +30,9 @@ export interface DocumentViewProps { ScreenToLocalTransform: () => Transform; isTopMost: boolean; //tfs: This shouldn't be necessary I don't think - Scaling: number; - PanelWidth: number; - PanelHeight: number; + ContentScaling: () => number; + PanelWidth: () => number; + PanelHeight: () => number; } export interface JsxArgs extends DocumentViewProps { Keys: { [name: string]: Key } @@ -293,7 +293,7 @@ export class DocumentView extends React.Component { var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0); var nodeWidth = nativeWidth > 0 ? nativeWidth.toString() + "px" : "100%"; var nodeHeight = nativeHeight > 0 ? nativeHeight.toString() + "px" : "100%"; - var scaling = this.props.Scaling; + var scaling = this.props.ContentScaling(); return (
{ return this.GetNumber(KeyStore.Width, 0) } + public Height = () => { return this.GetNumber(KeyStore.Height, 0) } + public Scale = () => { return this.GetNumber(KeyStore.Scale, 1) } + @computed public get Title() { return this.GetText(KeyStore.Title, ""); -- cgit v1.2.3-70-g09d2 From f4ec29ad3143cf13bf7fa05985926242cabb6b1a Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Sat, 23 Feb 2019 02:34:58 -0500 Subject: css hacking to fix layout issues related to making a captioned image style --- src/client/documents/Documents.ts | 20 ++++++++++++++++---- .../views/collections/CollectionFreeFormView.scss | 2 ++ .../views/collections/CollectionFreeFormView.tsx | 5 +++-- src/client/views/nodes/FieldView.tsx | 3 ++- src/client/views/nodes/FormattedTextBox.scss | 4 ++-- src/client/views/nodes/FormattedTextBox.tsx | 5 ++--- src/client/views/nodes/ImageBox.scss | 3 ++- src/fields/KeyStore.ts | 1 + 8 files changed, 30 insertions(+), 13 deletions(-) (limited to 'src/fields') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 156a09316..6ec5aa711 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -5,13 +5,10 @@ import { TextField } from "../../fields/TextField"; import { NumberField } from "../../fields/NumberField"; import { ListField } from "../../fields/ListField"; import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; -import { CollectionDockingView } from "../views/collections/CollectionDockingView"; -import { CollectionSchemaView } from "../views/collections/CollectionSchemaView"; import { ImageField } from "../../fields/ImageField"; import { ImageBox } from "../views/nodes/ImageBox"; -import { CollectionFreeFormView } from "../views/collections/CollectionFreeFormView"; -import { FieldId } from "../../fields/Field"; import { CollectionView, CollectionViewType } from "../views/collections/CollectionView"; +import { FieldView } from "../views/nodes/FieldView"; export interface DocumentOptions { x?: number; @@ -102,12 +99,27 @@ export namespace Documents { return imageProto; } return imageProto; + } + // example of custom display string for an image that shows a caption. + function CaptionLayoutString() { + return `
+
` + + ImageBox.LayoutString() + + `
+
` + + FormattedTextBox.LayoutString("CaptionKey") + + `
+
` }; + export function ImageDocument(url: string, options: DocumentOptions = {}): Document { let doc = GetImagePrototype().MakeDelegate(); setupOptions(doc, options); doc.Set(KeyStore.Data, new ImageField(new URL(url))); + doc.Set(KeyStore.Caption, new TextField("my caption...")); + doc.Set(KeyStore.BackgroundLayout, new TextField(CaptionLayoutString())); + doc.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data, KeyStore.Annotations, KeyStore.Caption])); let annotation = Documents.TextDocument({ title: "hello" }); doc.Set(KeyStore.Annotations, new ListField([annotation])); diff --git a/src/client/views/collections/CollectionFreeFormView.scss b/src/client/views/collections/CollectionFreeFormView.scss index 4cf474f77..6d678528a 100644 --- a/src/client/views/collections/CollectionFreeFormView.scss +++ b/src/client/views/collections/CollectionFreeFormView.scss @@ -11,5 +11,7 @@ position: absolute; top: 0; left: 0; + width:100%; + height: 100% } } \ No newline at end of file diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx index c12a82617..6d2662105 100644 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -34,7 +34,7 @@ export class CollectionFreeFormView extends CollectionViewBase { @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 == KeyStore.Annotations; } + @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); } @@ -91,6 +91,7 @@ export class CollectionFreeFormView extends CollectionViewBase { let [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); this.SetPan(x + dx, y + dy); + console.log("px = " + x + " " + y) } this._lastX = e.pageX; this._lastY = e.pageY; @@ -209,7 +210,7 @@ export class CollectionFreeFormView extends CollectionViewBase { style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px`, }} ref={this.createDropTarget}>
{this.backgroundView} {this.views} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 97d3f2a85..9dda08d8d 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -27,7 +27,8 @@ export interface FieldViewProps { @observer export class FieldView extends React.Component { - public static LayoutString(fieldType: { name: string }) { return `<${fieldType.name} doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={DataKey} isSelected={isSelected} select={select} isTopMost={isTopMost} />`; } + public static LayoutString(fieldType: { name: string }, fieldStr: string = "DataKey") { return `<${fieldType.name} doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={${fieldStr}} isSelected={isSelected} select={select} isTopMost={isTopMost} />`; } + @computed get field(): FieldValue { const { doc, fieldKey } = this.props; diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 5139d5d6b..872a2138b 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 { diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 16728d471..c0969a8c3 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -10,8 +10,6 @@ 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 @@ -32,7 +30,7 @@ import { observer } from "mobx-react"; //] export class FormattedTextBox extends React.Component { - public static LayoutString() { return FieldView.LayoutString(FormattedTextBox) } + public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(FormattedTextBox, fieldStr) } private _ref: React.RefObject; private _editorView: Opt; private _reactionDisposer: Opt; @@ -118,6 +116,7 @@ export class FormattedTextBox extends React.Component { style={{ color: "initial", whiteSpace: "initial", + height: "auto" }} onPointerDown={this.onPointerDown} ref={this._ref} />) diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 36f5e0fe0..5b15b3329 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -1,7 +1,8 @@ .imageBox-cont { padding: 0vw; - position: absolute; + position: relative; + text-align: center; width: 100%; max-width: 100%; max-height: 100% diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts index 6d6c6a546..42e3f6b58 100644 --- a/src/fields/KeyStore.ts +++ b/src/fields/KeyStore.ts @@ -22,5 +22,6 @@ export namespace KeyStore { 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"); } -- cgit v1.2.3-70-g09d2 From 6c451fa68db11c80f452ca7c85242ad98d867ab0 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Sat, 23 Feb 2019 04:49:48 -0500 Subject: Added web clippings on drag and drop --- src/client/documents/Documents.ts | 24 +++++++++++++++++++++ .../views/collections/CollectionFreeFormView.tsx | 3 ++- .../views/collections/CollectionViewBase.tsx | 9 ++++++++ src/client/views/nodes/DocumentView.tsx | 3 ++- src/client/views/nodes/FieldView.tsx | 4 ++++ src/client/views/nodes/WebView.tsx | 22 +++++++++++++++++++ src/fields/HtmlField.ts | 25 ++++++++++++++++++++++ src/fields/KeyStore.ts | 1 + src/fields/TextField.ts | 2 +- src/server/Message.ts | 2 +- src/server/ServerUtil.ts | 6 +++++- 11 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 src/client/views/nodes/WebView.tsx create mode 100644 src/fields/HtmlField.ts (limited to 'src/fields') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 6ec5aa711..d8e1db0b0 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -9,6 +9,8 @@ import { ImageField } from "../../fields/ImageField"; import { ImageBox } from "../views/nodes/ImageBox"; import { CollectionView, CollectionViewType } from "../views/collections/CollectionView"; import { FieldView } from "../views/nodes/FieldView"; +import { HtmlField } from "../../fields/HtmlField"; +import { WebView } from "../views/nodes/WebView"; export interface DocumentOptions { x?: number; @@ -79,6 +81,28 @@ export namespace Documents { return doc; } + let htmlProto: Document; + const htmlProtoId = "htmlProto"; + function GetHtmlPrototype(): Document { + if (!htmlProto) { + htmlProto = new Document(htmlProtoId); + htmlProto.Set(KeyStore.X, new NumberField(0)); + htmlProto.Set(KeyStore.Y, new NumberField(0)); + htmlProto.Set(KeyStore.Width, new NumberField(300)); + htmlProto.Set(KeyStore.Height, new NumberField(150)); + htmlProto.Set(KeyStore.Layout, new TextField(WebView.LayoutString())); + htmlProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data])); + } + return htmlProto; + } + + export function HtmlDocument(html: string, options: DocumentOptions = {}): Document { + let doc = GetHtmlPrototype().MakeDelegate(); + setupOptions(doc, options); + doc.Set(KeyStore.Data, new HtmlField(html)); + return doc; + } + let imageProto: Document; const imageProtoId = "imageProto"; function GetImagePrototype(): Document { diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx index 4799eda97..b031c35a7 100644 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -13,6 +13,7 @@ import { CollectionSchemaView } from "../collections/CollectionSchemaView"; import { CollectionView } from "../collections/CollectionView"; import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; import { DocumentView } from "../nodes/DocumentView"; +import { WebView } from "../nodes/WebView"; import { FormattedTextBox } from "../nodes/FormattedTextBox"; import { ImageBox } from "../nodes/ImageBox"; import "./CollectionFreeFormView.scss"; @@ -185,7 +186,7 @@ export class CollectionFreeFormView extends CollectionViewBase { get backgroundView() { return !this.backgroundLayout ? (null) : ( e.preventDefault() let that = this; + let html = e.dataTransfer.getData("text/html"); + let text = e.dataTransfer.getData("text/plain"); + if (html) { + let htmlDoc = Documents.HtmlDocument(html, { ...options }); + htmlDoc.SetText(KeyStore.DocumentText, text); + this.props.addDocument(htmlDoc); + return; + } + 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) { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index a9e211431..ad1328e5d 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -12,6 +12,7 @@ import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionFreeFormView } from "../collections/CollectionFreeFormView"; import { CollectionSchemaView } from "../collections/CollectionSchemaView"; import { CollectionView, CollectionViewType } from "../collections/CollectionView"; +import { WebView } from "./WebView"; import { ContextMenu } from "../ContextMenu"; import { FormattedTextBox } from "../nodes/FormattedTextBox"; import { ImageBox } from "../nodes/ImageBox"; @@ -196,7 +197,7 @@ export class DocumentView extends React.Component { @computed get mainContent() { return { } else if (field instanceof NumberField) { return

{field.Data}

+ } else if (field instanceof HtmlField) { + return } else if (field != FieldWaiting) { return

{field.GetValue}

} else diff --git a/src/client/views/nodes/WebView.tsx b/src/client/views/nodes/WebView.tsx new file mode 100644 index 000000000..717aa8bf5 --- /dev/null +++ b/src/client/views/nodes/WebView.tsx @@ -0,0 +1,22 @@ +import { FieldViewProps, FieldView } from "./FieldView"; +import { computed } from "mobx"; +import { observer } from "mobx-react"; +import { KeyStore } from "../../../fields/KeyStore"; +import React = require('react') +import { TextField } from "../../../fields/TextField"; +import { HtmlField } from "../../../fields/HtmlField"; +import { RichTextField } from "../../../fields/RichTextField"; + +@observer +export class WebView extends React.Component { + public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(WebView, fieldStr) } + + @computed + get html(): string { + return this.props.doc.GetData(KeyStore.Data, HtmlField, "" as string); + } + + render() { + return + } +} \ 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 { + constructor(data: string = "", 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/KeyStore.ts b/src/fields/KeyStore.ts index 42e3f6b58..290fa2be7 100644 --- a/src/fields/KeyStore.ts +++ b/src/fields/KeyStore.ts @@ -24,4 +24,5 @@ export namespace KeyStore { 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/TextField.ts b/src/fields/TextField.ts index ad96ab6d9..71d8ea310 100644 --- a/src/fields/TextField.ts +++ b/src/fields/TextField.ts @@ -22,4 +22,4 @@ export class TextField extends BasicField { _id: this.Id } } -} +} \ No newline at end of file diff --git a/src/server/Message.ts b/src/server/Message.ts index 7f3190f7f..80fc9a80d 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -45,7 +45,7 @@ export class GetFieldArgs { } export enum Types { - Number, List, Key, Image, Document, Text, RichText, DocumentReference + Number, List, Key, Image, Document, Text, RichText, DocumentReference, Html } export class DocumentTransfer implements Transferable { diff --git a/src/server/ServerUtil.ts b/src/server/ServerUtil.ts index 46c409ec4..08e72fdae 100644 --- a/src/server/ServerUtil.ts +++ b/src/server/ServerUtil.ts @@ -9,6 +9,7 @@ import { Document } from './../fields/Document'; import { Server } from './../client/Server'; import { Types } from './Message'; import { Utils } from '../Utils'; +import { HtmlField } from '../fields/HtmlField'; export class ServerUtils { public static FromJson(json: any): Field { @@ -27,6 +28,8 @@ export class ServerUtils { return new NumberField(data, id, false) case Types.Text: return new TextField(data, id, false) + case Types.Html: + return new HtmlField(data, id, false) case Types.RichText: return new RichTextField(data, id, false) case Types.Key: @@ -42,7 +45,8 @@ export class ServerUtils { 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"); } - return new TextField(data, id) } } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 96327e5b393ba967a05b6c985a2641e3705c9e2c Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Sat, 23 Feb 2019 10:12:39 -0500 Subject: added an overlay layer to collections... but it blocks events. not sure how to fix. --- src/client/documents/Documents.ts | 7 +++++++ .../views/collections/CollectionFreeFormView.tsx | 24 ++++++++++++++++++++++ src/fields/KeyStore.ts | 1 + 3 files changed, 32 insertions(+) (limited to 'src/fields') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 6ec5aa711..e0d44e535 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -112,6 +112,12 @@ export namespace Documents { + FormattedTextBox.LayoutString("CaptionKey") + `
` }; + function OverlayLayoutString() { + return `
+
` + + FormattedTextBox.LayoutString("CaptionKey") + + `
+
` }; export function ImageDocument(url: string, options: DocumentOptions = {}): Document { let doc = GetImagePrototype().MakeDelegate(); @@ -119,6 +125,7 @@ export namespace Documents { doc.Set(KeyStore.Data, new ImageField(new URL(url))); doc.Set(KeyStore.Caption, new TextField("my caption...")); doc.Set(KeyStore.BackgroundLayout, new TextField(CaptionLayoutString())); + //doc.Set(KeyStore.OverlayLayout, new TextField(OverlayLayoutString())); doc.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data, KeyStore.Annotations, KeyStore.Caption])); let annotation = Documents.TextDocument({ title: "hello" }); diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx index 4799eda97..ee0f3730d 100644 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -161,6 +161,12 @@ export class CollectionFreeFormView extends CollectionViewBase { return field.Data; } } + @computed get overlayLayout(): string | undefined { + let field = this.props.Document.GetT(KeyStore.OverlayLayout, TextField); + if (field && field !== "") { + return field.Data; + } + } @computed get views() { const { fieldKey, Document } = this.props; @@ -192,6 +198,17 @@ export class CollectionFreeFormView extends CollectionViewBase { onError={(test: any) => console.log(test)} />); } + @computed + get overlayView() { + return !this.overlayLayout ? (null) : + ( console.log(test)} + />); + } getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH).transform(this.getLocalTransform()) getLocalTransform = (): Transform => Transform.Identity.translate(-this.panX, -this.panY).scale(1 / this.scale); noScaling = () => 1; @@ -199,6 +216,12 @@ export class CollectionFreeFormView extends CollectionViewBase { render() { const panx: number = this.props.Document.GetNumber(KeyStore.PanX, 0); const pany: number = this.props.Document.GetNumber(KeyStore.PanY, 0); + var overlay = this.overlayView ? +
+ {this.overlayView} +
+ : + (null); return (
+ {overlay}
); } diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts index 42e3f6b58..78ed09efd 100644 --- a/src/fields/KeyStore.ts +++ b/src/fields/KeyStore.ts @@ -19,6 +19,7 @@ export namespace KeyStore { 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"); -- cgit v1.2.3-70-g09d2 From a2bec39bc152f7493e4171b6446040fa381e6463 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sun, 17 Mar 2019 20:57:20 -0400 Subject: added tuple field, beginnig remote cursor display --- src/client/views/Main.tsx | 12 +++- .../views/collections/CollectionFreeFormView.tsx | 15 ++++ .../views/collections/CollectionViewBase.tsx | 34 ++++++++- src/fields/KeyStore.ts | 2 +- src/fields/TupleField.ts | 59 ++++++++++++++++ src/server/Message.ts | 2 +- src/server/RouteStore.ts | 3 + src/server/ServerUtil.ts | 7 +- src/server/index.ts | 80 +++++++++++----------- 9 files changed, 169 insertions(+), 45 deletions(-) create mode 100644 src/fields/TupleField.ts (limited to 'src/fields') diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index e76a5e04b..268f70de1 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -38,6 +38,7 @@ import { faPenNib } from '@fortawesome/free-solid-svg-icons'; import { faFilm } from '@fortawesome/free-solid-svg-icons'; import { faMusic } from '@fortawesome/free-solid-svg-icons'; import Measure from 'react-measure'; +import { DashUserModel } from '../../server/authentication/models/user_model'; @observer export class Main extends React.Component { @@ -49,12 +50,13 @@ export class Main extends React.Component { @observable public pheight: number = 0; private mainDocId: string | undefined; + private currentUser?: DashUserModel; constructor(props: Readonly<{}>) { super(props); // causes errors to be generated when modifying an observable outside of an action configure({ enforceActions: "observed" }); - if (window.location.pathname !== "/home") { + if (window.location.pathname !== RouteStore.home) { let pathname = window.location.pathname.split("/"); this.mainDocId = pathname[pathname.length - 1]; } @@ -75,6 +77,14 @@ export class Main extends React.Component { Documents.initProtos(() => { this.initAuthenticationRouters(); }); + + request.get(this.prepend(RouteStore.getCurrUser), (error, response, body) => { + if (body) { + this.currentUser = body as DashUserModel; + } else { + throw new Error("There should be a user! Why does Dash think there isn't one?") + } + }) } initEventListeners = () => { diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx index bb28dd20a..a3a596c37 100644 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -322,6 +322,7 @@ export class CollectionFreeFormView extends CollectionViewBase { return (
super.setCursorPosition(this.props.ScreenToLocalTransform().transformPoint(e.screenX, e.screenY))} onWheel={this.onPointerWheel} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} @@ -329,6 +330,20 @@ export class CollectionFreeFormView extends CollectionViewBase { style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px`, }} tabIndex={0} ref={this.createDropTarget}> + {super.getCursors().map(entry => { + let point = entry.Data[1] + return ( +
+ ); + })}
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx index 4a2761139..81d7f4077 100644 --- a/src/client/views/collections/CollectionViewBase.tsx +++ b/src/client/views/collections/CollectionViewBase.tsx @@ -1,4 +1,4 @@ -import { action, runInAction } from "mobx"; +import { action, runInAction, observable, computed } from "mobx"; import { Document } from "../../../fields/Document"; import { ListField } from "../../../fields/ListField"; import React = require("react"); @@ -12,6 +12,9 @@ import { Key } from "../../../fields/Key"; import { Transform } from "../../util/Transform"; import { CollectionView } from "./CollectionView"; import { RouteStore } from "../../../server/RouteStore"; +import { TupleField } from "../../../fields/TupleField"; +import { Server } from "mongodb"; +import { DashUserModel } from "../../../server/authentication/models/user_model"; export interface CollectionViewProps { fieldKey: Key; @@ -25,13 +28,17 @@ export interface CollectionViewProps { panelHeight: () => number; focus: (doc: Document) => void; } + export interface SubCollectionViewProps extends CollectionViewProps { active: () => boolean; addDocument: (doc: Document) => void; removeDocument: (doc: Document) => boolean; CollectionView: CollectionView; + currentUser?: DashUserModel; } +export type CursorEntry = TupleField; + export class CollectionViewBase extends React.Component { private dropDisposer?: DragManager.DragDropDisposer; protected createDropTarget = (ele: HTMLDivElement) => { @@ -43,6 +50,31 @@ export class CollectionViewBase extends React.Component } } + protected setCursorPosition(position: [number, number]) { + let user = this.props.currentUser; + if (user && user.id) { + let ind; + let doc = this.props.Document; + let cursors = doc.GetOrCreate>(KeyStore.Cursors, ListField, false).Data; + let entry = new TupleField([user.id, position]); + // if ((ind = cursors.findIndex(entry => entry.Data[0] === user.id)) > -1) { + // cursors[ind] = entry; + // } else { + // cursors.push(entry); + // } + } + } + + protected getCursors(): CursorEntry[] { + let user = this.props.currentUser; + if (user && user.id) { + let doc = this.props.Document; + // return doc.GetList(KeyStore.Cursors, []).filter(entry => entry.Data[0] !== user.id); + } + return []; + } + + @undoBatch @action protected drop(e: Event, de: DragManager.DropEvent) { diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts index 08a1c00b9..c6e58ee35 100644 --- a/src/fields/KeyStore.ts +++ b/src/fields/KeyStore.ts @@ -37,5 +37,5 @@ export namespace KeyStore { export const CurPage = new Key("CurPage"); export const NumPages = new Key("NumPages"); export const Ink = new Key("Ink"); - export const ActiveUsers = new Key("Active Users"); + export const Cursors = new Key("Cursors"); } diff --git a/src/fields/TupleField.ts b/src/fields/TupleField.ts new file mode 100644 index 000000000..778bd5d2f --- /dev/null +++ b/src/fields/TupleField.ts @@ -0,0 +1,59 @@ +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 TupleField extends BasicField<[T, U]> { + constructor(data: [T, U], id?: FieldId, save: boolean = true) { + super(data, save, id); + if (save) { + Server.UpdateField(this); + } + this.observeTuple(); + } + + private observeDisposer: Lambda | undefined; + private observeTuple(): void { + this.observeDisposer = observe(this.Data as (T | U)[] as IObservableArray, (change: IArrayChange | IArraySplice) => { + if (change.type === "update") { + UndoManager.AddEvent({ + undo: () => this.Data[change.index] = change.oldValue, + redo: () => this.Data[change.index] = change.newValue + }) + Server.UpdateField(this); + } else { + throw new Error("Why are you messing with the length of a tuple, huh?"); + } + }); + } + + protected setData(value: [T, U]) { + if (this.observeDisposer) { + this.observeDisposer() + } + this.data = observable(value) as (T | U)[] as [T, U]; + this.observeTuple(); + } + + UpdateFromServer(values: [T, U]) { + this.setData(values); + } + + ToScriptString(): string { + return `new TupleField([${this.Data[0], this.Data[1]}])`; + } + + Copy(): Field { + return new TupleField(this.Data); + } + + ToJson(): { type: Types, data: [T, U], _id: string } { + return { + type: Types.List, + data: this.Data, + _id: this.Id + } + } +} \ No newline at end of file diff --git a/src/server/Message.ts b/src/server/Message.ts index 8a00f6b59..a2d1ab829 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -45,7 +45,7 @@ export class GetFieldArgs { } export enum Types { - Number, List, Key, Image, Web, Document, Text, RichText, DocumentReference, Html, Video, Audio, Ink, PDF + Number, List, Key, Image, Web, Document, Text, RichText, DocumentReference, Html, Video, Audio, Ink, PDF, Tuple } export class DocumentTransfer implements Transferable { diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts index f12aed85e..683fd6d7e 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -13,12 +13,15 @@ export enum RouteStore { images = "/images", // USER AND WORKSPACES + getCurrUser = "/getCurrentUser", addWorkspace = "/addWorkspaceId", getAllWorkspaces = "/getAllWorkspaceIds", getActiveWorkspace = "/getActiveWorkspaceId", setActiveWorkspace = "/setActiveWorkspaceId", updateCursor = "/updateCursor", + openDocumentWithId = "/doc/:docId", + // AUTHENTICATION signup = "/signup", login = "/login", diff --git a/src/server/ServerUtil.ts b/src/server/ServerUtil.ts index 5331c9e30..dce4bada5 100644 --- a/src/server/ServerUtil.ts +++ b/src/server/ServerUtil.ts @@ -14,8 +14,9 @@ import { HtmlField } from '../fields/HtmlField'; import { WebField } from '../fields/WebField'; import { AudioField } from '../fields/AudioField'; import { VideoField } from '../fields/VideoField'; -import {InkField} from '../fields/InkField'; -import {PDFField} from '../fields/PDFField'; +import { InkField } from '../fields/InkField'; +import { PDFField } from '../fields/PDFField'; +import { TupleField } from '../fields/TupleField'; @@ -55,6 +56,8 @@ export class ServerUtils { return new AudioField(new URL(data), id, false) case Types.Video: return new VideoField(new URL(data), id, false) + case Types.Tuple: + return new TupleField(data, id, false); case Types.Ink: return InkField.FromJson(id, data); case Types.Document: diff --git a/src/server/index.ts b/src/server/index.ts index d0df95ca3..7baf896b7 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -77,6 +77,10 @@ app.use((req, res, next) => { next(); }); +app.get("/hello", (req, res) => { + res.send("

Hello

"); +}) + enum Method { GET, POST @@ -88,26 +92,26 @@ enum Method { * does not execute unless Passport authentication detects a user logged in. * @param method whether or not the request is a GET or a POST * @param route the forward slash prepended path name (reference and add to RouteStore.ts) - * @param handler the action to invoke, recieving a DashUserModel and the expected request and response + * @param handler the action to invoke, recieving a DashUserModel and, as expected, the Express.Request and Express.Response * @param onRejection an optional callback invoked on return if no user is found to be logged in */ function addSecureRoute(method: Method, route: string, - handler: (user: DashUserModel, req: express.Request, res: express.Response) => void, + handler: (user: DashUserModel, res: express.Response, req: express.Request) => void, onRejection: (res: express.Response) => any = (res) => res.redirect(RouteStore.logout)) { switch (method) { case Method.GET: app.get(route, (req, res) => { const dashUser: DashUserModel = req.user; if (!dashUser) return onRejection(res); - handler(dashUser, req, res); + handler(dashUser, res, req); }); break; case Method.POST: app.post(route, (req, res) => { const dashUser: DashUserModel = req.user; if (!dashUser) return onRejection(res); - handler(dashUser, req, res); + handler(dashUser, res, req); }); break; } @@ -120,66 +124,64 @@ let FieldStore: ObservableMap = new ObservableMap(); app.use(express.static(__dirname + RouteStore.public)); app.use(RouteStore.images, express.static(__dirname + RouteStore.public)) -addSecureRoute(Method.POST, RouteStore.upload, (user, req, res) => { - let form = new formidable.IncomingForm() - form.uploadDir = __dirname + "/public/files/" - form.keepExtensions = true - // let path = req.body.path; - console.log("upload") - form.parse(req, (err, fields, files) => { - console.log("parsing") - let names: any[] = []; - for (const name in files) { - let file = files[name]; - names.push(`/files/` + path.basename(file.path)); - } - res.send(names); - }); -}); +// GETTERS // anyone attempting to navigate to localhost at this port will // first have to login -addSecureRoute(Method.GET, RouteStore.root, (user, req, res) => { +addSecureRoute(Method.GET, RouteStore.root, (user, res) => { res.redirect(RouteStore.home); }); -// YAY! SHOW THEM THEIR WORKSPACES NOW -addSecureRoute(Method.GET, RouteStore.home, (user, req, res) => { +addSecureRoute(Method.GET, RouteStore.home, (user, res) => { res.sendFile(path.join(__dirname, '../../deploy/index.html')); }); -addSecureRoute(Method.GET, RouteStore.getActiveWorkspace, (user, req, res) => { +addSecureRoute(Method.GET, RouteStore.openDocumentWithId, (user, res) => { + res.sendFile(path.join(__dirname, '../../deploy/index.html')); +}); + +addSecureRoute(Method.GET, RouteStore.getActiveWorkspace, (user, res) => { res.send(user.activeWorkspaceId || ""); }); -addSecureRoute(Method.GET, RouteStore.getAllWorkspaces, (user, req, res) => { +addSecureRoute(Method.GET, RouteStore.getAllWorkspaces, (user, res) => { res.send(JSON.stringify(user.allWorkspaceIds)); }); -addSecureRoute(Method.POST, RouteStore.setActiveWorkspace, (user, req, res) => { +addSecureRoute(Method.GET, RouteStore.getCurrUser, (user, res) => { + res.send(JSON.stringify(user)); +}); + +// SETTERS + +addSecureRoute(Method.POST, RouteStore.setActiveWorkspace, (user, res, req) => { user.update({ $set: { activeWorkspaceId: req.body.target } }, (err, raw) => { res.sendStatus(err ? 500 : 200); }); }); -addSecureRoute(Method.POST, RouteStore.addWorkspace, (user, req, res) => { +addSecureRoute(Method.POST, RouteStore.addWorkspace, (user, res, req) => { user.update({ $push: { allWorkspaceIds: req.body.target } }, (err, raw) => { res.sendStatus(err ? 500 : 200); }); }); -// define a route handler for the default home page -// app.get("/", (req, res) => { -// res.redirect("/doc/mainDoc"); -// // res.sendFile(path.join(__dirname, '../../deploy/index.html')); -// }); - -app.get("/doc/:docId", (req, res) => { - res.sendFile(path.join(__dirname, '../../deploy/index.html')); -}) -app.get("/hello", (req, res) => { - res.send("

Hello

"); -}) +addSecureRoute(Method.POST, RouteStore.upload, (user, res, req) => { + let form = new formidable.IncomingForm() + form.uploadDir = __dirname + "/public/files/" + form.keepExtensions = true + // let path = req.body.path; + console.log("upload") + form.parse(req, (err, fields, files) => { + console.log("parsing") + let names: any[] = []; + for (const name in files) { + let file = files[name]; + names.push(`/files/` + path.basename(file.path)); + } + res.send(names); + }); +}); // AUTHENTICATION -- cgit v1.2.3-70-g09d2 From 748412d972bd466a372fcf384448d3a00b42ee9f Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 18 Mar 2019 00:29:20 -0400 Subject: implemented multi-client remote cursor --- src/client/views/Main.tsx | 24 ++- .../views/collections/CollectionFreeFormView.tsx | 38 +++-- .../views/collections/CollectionViewBase.tsx | 44 ++--- src/fields/TupleField.ts | 2 +- src/server/ServerUtil.ts | 2 + src/server/authentication/models/user_utils.ts | 22 +++ src/server/index.ts | 179 ++++++++++++--------- 7 files changed, 179 insertions(+), 132 deletions(-) create mode 100644 src/server/authentication/models/user_utils.ts (limited to 'src/fields') diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 268f70de1..fd756972b 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -39,6 +39,8 @@ import { faFilm } from '@fortawesome/free-solid-svg-icons'; import { faMusic } from '@fortawesome/free-solid-svg-icons'; import Measure from 'react-measure'; import { DashUserModel } from '../../server/authentication/models/user_model'; +import { ServerUtils } from '../../server/ServerUtil'; +import { UserUtils } from '../../server/authentication/models/user_utils'; @observer export class Main extends React.Component { @@ -61,6 +63,8 @@ export class Main extends React.Component { this.mainDocId = pathname[pathname.length - 1]; } + UserUtils.loadCurrentUserId(); + library.add(faFont); library.add(faImage); library.add(faFilePdf); @@ -77,14 +81,6 @@ export class Main extends React.Component { Documents.initProtos(() => { this.initAuthenticationRouters(); }); - - request.get(this.prepend(RouteStore.getCurrUser), (error, response, body) => { - if (body) { - this.currentUser = body as DashUserModel; - } else { - throw new Error("There should be a user! Why does Dash think there isn't one?") - } - }) } initEventListeners = () => { @@ -101,7 +97,7 @@ export class Main extends React.Component { initAuthenticationRouters = () => { // Load the user's active workspace, or create a new one if initial session after signup - request.get(this.prepend(RouteStore.getActiveWorkspace), (error, response, body) => { + request.get(ServerUtils.prepend(RouteStore.getActiveWorkspace), (error, response, body) => { if (this.mainDocId || body) { Server.GetField(this.mainDocId || body, field => { if (field instanceof Document) { @@ -122,7 +118,7 @@ export class Main extends React.Component { createNewWorkspace = (init: boolean): void => { let mainDoc = Documents.DockDocument(JSON.stringify({ content: [{ type: 'row', content: [] }] }), { title: `Main Container ${this.userWorkspaces.length + 1}` }); let newId = mainDoc.Id; - request.post(this.prepend(RouteStore.addWorkspace), { + request.post(ServerUtils.prepend(RouteStore.addWorkspace), { body: { target: newId }, json: true }, () => { if (init) this.populateWorkspaces(); }); @@ -141,7 +137,7 @@ export class Main extends React.Component { @action populateWorkspaces = () => { // retrieve all workspace documents from the server - request.get(this.prepend(RouteStore.getAllWorkspaces), (error, res, body) => { + request.get(ServerUtils.prepend(RouteStore.getAllWorkspaces), (error, res, body) => { let ids = JSON.parse(body) as string[]; Server.GetFields(ids, action((fields: { [id: string]: Field }) => this.userWorkspaces = ids.map(id => fields[id] as Document))); }); @@ -149,7 +145,7 @@ export class Main extends React.Component { @action openWorkspace = (doc: Document): void => { - request.post(this.prepend(RouteStore.setActiveWorkspace), { + request.post(ServerUtils.prepend(RouteStore.setActiveWorkspace), { body: { target: doc.Id }, json: true }); @@ -163,8 +159,6 @@ export class Main extends React.Component { } } - prepend = (extension: string) => window.location.origin + extension; - render() { let imgRef = React.createRef(); let pdfRef = React.createRef(); @@ -233,7 +227,7 @@ export class Main extends React.Component {
-
+
{/* for the expandable add nodes menu. Not included with the above because once it expands it expands the whole div with it, making canvas interactions limited. */} diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx index a3a596c37..89edd1397 100644 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -322,7 +322,7 @@ export class CollectionFreeFormView extends CollectionViewBase { return (
super.setCursorPosition(this.props.ScreenToLocalTransform().transformPoint(e.screenX, e.screenY))} + onPointerMove={(e) => super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY))} onWheel={this.onPointerWheel} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} @@ -330,20 +330,6 @@ export class CollectionFreeFormView extends CollectionViewBase { style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px`, }} tabIndex={0} ref={this.createDropTarget}> - {super.getCursors().map(entry => { - let point = entry.Data[1] - return ( -
- ); - })}
@@ -351,11 +337,33 @@ export class CollectionFreeFormView extends CollectionViewBase { {this.views} + {super.getCursors().map(entry => { + if (entry.Data.length > 0) { + let point = entry.Data[1] + return ( +
+ ); + } + })}
{this.overlayView} +
); } diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx index 81d7f4077..02ee49a38 100644 --- a/src/client/views/collections/CollectionViewBase.tsx +++ b/src/client/views/collections/CollectionViewBase.tsx @@ -13,8 +13,8 @@ import { Transform } from "../../util/Transform"; import { CollectionView } from "./CollectionView"; import { RouteStore } from "../../../server/RouteStore"; import { TupleField } from "../../../fields/TupleField"; -import { Server } from "mongodb"; import { DashUserModel } from "../../../server/authentication/models/user_model"; +import { UserUtils } from "../../../server/authentication/models/user_utils"; export interface CollectionViewProps { fieldKey: Key; @@ -34,10 +34,9 @@ export interface SubCollectionViewProps extends CollectionViewProps { addDocument: (doc: Document) => void; removeDocument: (doc: Document) => boolean; CollectionView: CollectionView; - currentUser?: DashUserModel; } -export type CursorEntry = TupleField; +export type CursorEntry = TupleField; export class CollectionViewBase extends React.Component { private dropDisposer?: DragManager.DragDropDisposer; @@ -50,31 +49,34 @@ export class CollectionViewBase extends React.Component } } + @action protected setCursorPosition(position: [number, number]) { - let user = this.props.currentUser; - if (user && user.id) { - let ind; - let doc = this.props.Document; - let cursors = doc.GetOrCreate>(KeyStore.Cursors, ListField, false).Data; - let entry = new TupleField([user.id, position]); - // if ((ind = cursors.findIndex(entry => entry.Data[0] === user.id)) > -1) { - // cursors[ind] = entry; - // } else { - // cursors.push(entry); - // } + let ind; + let doc = this.props.Document; + let id = UserUtils.currentUserId; + if (id) { + doc.GetOrCreateAsync>(KeyStore.Cursors, ListField, field => { + let cursors = field.Data; + if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.Data[0] === id)) > -1) { + cursors[ind].Data[1] = position; + } else { + let entry = new TupleField([id, position]); + cursors.push(entry); + } + }) + + } } protected getCursors(): CursorEntry[] { - let user = this.props.currentUser; - if (user && user.id) { - let doc = this.props.Document; - // return doc.GetList(KeyStore.Cursors, []).filter(entry => entry.Data[0] !== user.id); - } - return []; + let doc = this.props.Document; + let id = UserUtils.currentUserId; + let cursors = doc.GetList(KeyStore.Cursors, []); + let notMe = cursors.filter(entry => entry.Data[0] !== id); + return id ? notMe : []; } - @undoBatch @action protected drop(e: Event, de: DragManager.DropEvent) { diff --git a/src/fields/TupleField.ts b/src/fields/TupleField.ts index 778bd5d2f..e2162c751 100644 --- a/src/fields/TupleField.ts +++ b/src/fields/TupleField.ts @@ -51,7 +51,7 @@ export class TupleField extends BasicField<[T, U]> { ToJson(): { type: Types, data: [T, U], _id: string } { return { - type: Types.List, + type: Types.Tuple, data: this.Data, _id: this.Id } diff --git a/src/server/ServerUtil.ts b/src/server/ServerUtil.ts index dce4bada5..f10f82deb 100644 --- a/src/server/ServerUtil.ts +++ b/src/server/ServerUtil.ts @@ -22,6 +22,8 @@ import { TupleField } from '../fields/TupleField'; export class ServerUtils { + public static prepend(extension: string): string { return window.location.origin + extension; } + public static FromJson(json: any): Field { let obj = json let data: any = obj.data diff --git a/src/server/authentication/models/user_utils.ts b/src/server/authentication/models/user_utils.ts new file mode 100644 index 000000000..1497a4ba4 --- /dev/null +++ b/src/server/authentication/models/user_utils.ts @@ -0,0 +1,22 @@ +import { DashUserModel } from "./user_model"; +import * as request from 'request' +import { RouteStore } from "../../RouteStore"; +import { ServerUtils } from "../../ServerUtil"; + +export class UserUtils { + private static current: string; + + public static get currentUserId() { + return UserUtils.current; + } + + public static loadCurrentUserId() { + request.get(ServerUtils.prepend(RouteStore.getCurrUser), (error, response, body) => { + if (body) { + UserUtils.current = JSON.parse(body) as string; + } else { + throw new Error("There should be a user! Why does Dash think there isn't one?") + } + }); + } +} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 17aba99ee..0512ebf72 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -6,19 +6,14 @@ import * as whm from 'webpack-hot-middleware'; import * as path from 'path' import * as formidable from 'formidable' import * as passport from 'passport'; -import { MessageStore, Message, SetFieldArgs, GetFieldArgs, Transferable } from "./Message"; +import { MessageStore, 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 * as bcrypt from "bcrypt-nodejs"; -import { Document } from '../fields/Document'; import * as io from 'socket.io' -import * as passportConfig from './authentication/config/passport'; import { getLogin, postLogin, getSignup, postSignup, getLogout, postReset, getForgot, postForgot, getReset } from './authentication/controllers/user_controller'; const config = require('../../webpack.config'); const compiler = webpack(config); @@ -29,17 +24,14 @@ import expressFlash = require('express-flash'); import flash = require('connect-flash'); import * as bodyParser from 'body-parser'; import * as session from 'express-session'; -// import cookieSession = require('cookie-session'); import * as cookieParser from 'cookie-parser'; import c = require("crypto"); const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); -import { performance } from 'perf_hooks' -import User, { DashUserModel } from './authentication/models/user_model'; +import { DashUserModel } from './authentication/models/user_model'; import * as fs from 'fs'; import * as request from 'request' import { RouteStore } from './RouteStore'; -import * as MobileDetect from 'mobile-detect'; const download = (url: string, dest: fs.PathLike) => { request.get(url).pipe(fs.createWriteStream(dest)); @@ -56,7 +48,7 @@ mongoose.connection.on('connected', function () { app.use(cookieParser()); app.use(session({ - secret: `our secret`, + secret: "64d6866242d3b5a5503c675b32c9605e4e90478e9b77bcf2bc", resave: true, cookie: { maxAge: 7 * 24 * 60 * 60 }, saveUninitialized: true, @@ -91,30 +83,30 @@ enum Method { * It ensures that any requests leading to or containing user-sensitive information * does not execute unless Passport authentication detects a user logged in. * @param method whether or not the request is a GET or a POST - * @param route the forward slash prepended path name (reference and add to RouteStore.ts) * @param handler the action to invoke, recieving a DashUserModel and, as expected, the Express.Request and Express.Response * @param onRejection an optional callback invoked on return if no user is found to be logged in + * @param subscribers the forward slash prepended path names (reference and add to RouteStore.ts) that will all invoke the given @param handler */ function addSecureRoute(method: Method, - route: string, handler: (user: DashUserModel, res: express.Response, req: express.Request) => void, - onRejection: (res: express.Response) => any = (res) => res.redirect(RouteStore.logout)) { - switch (method) { - case Method.GET: - app.get(route, (req, res) => { - const dashUser: DashUserModel = req.user; - if (!dashUser) return onRejection(res); - handler(dashUser, res, req); - }); - break; - case Method.POST: - app.post(route, (req, res) => { - const dashUser: DashUserModel = req.user; - if (!dashUser) return onRejection(res); - handler(dashUser, res, req); - }); - break; + onRejection: (res: express.Response) => any = (res) => res.redirect(RouteStore.logout), + ...subscribers: string[] +) { + let abstracted = (req: express.Request, res: express.Response) => { + const dashUser: DashUserModel = req.user; + if (!dashUser) return onRejection(res); + handler(dashUser, res, req); } + subscribers.forEach(route => { + switch (method) { + case Method.GET: + app.get(route, abstracted); + break; + case Method.POST: + app.post(route, abstracted); + break; + } + }); } // STATIC FILE SERVING @@ -128,60 +120,87 @@ app.use(RouteStore.images, express.static(__dirname + RouteStore.public)) // anyone attempting to navigate to localhost at this port will // first have to login -addSecureRoute(Method.GET, RouteStore.root, (user, res) => { - res.redirect(RouteStore.home); -}); - -addSecureRoute(Method.GET, RouteStore.home, (user, res) => { - res.sendFile(path.join(__dirname, '../../deploy/index.html')); -}); - -addSecureRoute(Method.GET, RouteStore.openDocumentWithId, (user, res) => { - res.sendFile(path.join(__dirname, '../../deploy/index.html')); -}); - -addSecureRoute(Method.GET, RouteStore.getActiveWorkspace, (user, res) => { - res.send(user.activeWorkspaceId || ""); -}); - -addSecureRoute(Method.GET, RouteStore.getAllWorkspaces, (user, res) => { - res.send(JSON.stringify(user.allWorkspaceIds)); -}); - -addSecureRoute(Method.GET, RouteStore.getCurrUser, (user, res) => { - res.send(JSON.stringify(user)); -}); +addSecureRoute( + Method.GET, + (user, res) => res.redirect(RouteStore.home), + undefined, + RouteStore.root +); + +addSecureRoute( + Method.GET, + (user, res) => res.sendFile(path.join(__dirname, '../../deploy/index.html')), + undefined, + RouteStore.home, + RouteStore.openDocumentWithId +); + +addSecureRoute( + Method.GET, + (user, res) => res.send(user.activeWorkspaceId || ""), + undefined, + RouteStore.getActiveWorkspace, +); + +addSecureRoute( + Method.GET, + (user, res) => res.send(JSON.stringify(user.allWorkspaceIds)), + undefined, + RouteStore.getAllWorkspaces +); + +addSecureRoute( + Method.GET, + (user, res) => res.send(JSON.stringify(user.id)), + undefined, + RouteStore.getCurrUser +); // SETTERS -addSecureRoute(Method.POST, RouteStore.setActiveWorkspace, (user, res, req) => { - user.update({ $set: { activeWorkspaceId: req.body.target } }, (err, raw) => { - res.sendStatus(err ? 500 : 200); - }); -}); - -addSecureRoute(Method.POST, RouteStore.addWorkspace, (user, res, req) => { - user.update({ $push: { allWorkspaceIds: req.body.target } }, (err, raw) => { - res.sendStatus(err ? 500 : 200); - }); -}); - -addSecureRoute(Method.POST, RouteStore.upload, (user, res, req) => { - let form = new formidable.IncomingForm() - form.uploadDir = __dirname + "/public/files/" - form.keepExtensions = true - // let path = req.body.path; - console.log("upload") - form.parse(req, (err, fields, files) => { - console.log("parsing") - let names: any[] = []; - for (const name in files) { - let file = files[name]; - names.push(`/files/` + path.basename(file.path)); - } - res.send(names); - }); -}); +addSecureRoute( + Method.POST, + (user, res, req) => { + user.update({ $set: { activeWorkspaceId: req.body.target } }, (err, raw) => { + res.sendStatus(err ? 500 : 200); + }); + }, + undefined, + RouteStore.setActiveWorkspace +); + +addSecureRoute( + Method.POST, + (user, res, req) => { + user.update({ $push: { allWorkspaceIds: req.body.target } }, (err, raw) => { + res.sendStatus(err ? 500 : 200); + }); + }, + undefined, + RouteStore.addWorkspace +); + +addSecureRoute( + Method.POST, + (user, res, req) => { + let form = new formidable.IncomingForm() + form.uploadDir = __dirname + "/public/files/" + form.keepExtensions = true + // let path = req.body.path; + console.log("upload") + form.parse(req, (err, fields, files) => { + console.log("parsing") + let names: any[] = []; + for (const name in files) { + let file = files[name]; + names.push(`/files/` + path.basename(file.path)); + } + res.send(names); + }); + }, + undefined, + RouteStore.upload +); // AUTHENTICATION -- cgit v1.2.3-70-g09d2