diff options
Diffstat (limited to 'src')
43 files changed, 1707 insertions, 987 deletions
diff --git a/src/Utils.ts b/src/Utils.ts index c9c006aa8..d4b7da52c 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -2,15 +2,6 @@ import v4 = require('uuid/v4'); import v5 = require("uuid/v5"); import { Socket } from 'socket.io'; import { Message, Types } from './server/Message'; -import { Field } from './fields/Field'; -import { TextField } from './fields/TextField'; -import { NumberField } from './fields/NumberField'; -import { RichTextField } from './fields/RichTextField'; -import { Key } from './fields/Key'; -import { ImageField } from './fields/ImageField'; -import { ListField } from './fields/ListField'; -import { Document } from './fields/Document'; -import { Server } from './client/Server'; export class Utils { @@ -23,6 +14,9 @@ export class Utils { } public static GetScreenTransform(ele: HTMLElement): { scale: number, translateX: number, translateY: number } { + if (!ele) { + return { scale: 1, translateX: 1, translateY: 1 } + } const rect = ele.getBoundingClientRect(); const scale = ele.offsetWidth == 0 && rect.width == 0 ? 1 : rect.width / ele.offsetWidth; const translateX = rect.left; diff --git a/src/client/Server.ts b/src/client/Server.ts index 2077c0c57..2d162b93a 100644 --- a/src/client/Server.ts +++ b/src/client/Server.ts @@ -1,7 +1,6 @@ -import { Field, FieldWaiting, FIELD_ID, FIELD_WAITING, FieldValue, Opt } from "../fields/Field" import { Key } from "../fields/Key" -import { KeyStore } from "../fields/KeyStore" -import { ObservableMap, action, reaction, when } from "mobx"; +import { ObservableMap, action, reaction } from "mobx"; +import { Field, FieldWaiting, FIELD_WAITING, Opt, FieldId } from "../fields/Field" import { Document } from "../fields/Document" import { SocketStub } from "./SocketStub"; import * as OpenSocket from 'socket.io-client'; @@ -9,7 +8,7 @@ import { Utils } from "./../Utils"; import { MessageStore, Types } from "./../server/Message"; export class Server { - public static ClientFieldsCached: ObservableMap<FIELD_ID, Field | FIELD_WAITING> = new ObservableMap(); + public static ClientFieldsCached: ObservableMap<FieldId, Field | FIELD_WAITING> = new ObservableMap(); static Socket: SocketIOClient.Socket = OpenSocket("http://localhost:1234"); static GUID: string = Utils.GenerateGuid() @@ -17,7 +16,7 @@ export class Server { // Retrieves the cached value of the field and sends a request to the server for the real value (if it's not cached). // Call this is from within a reaction and test whether the return value is FieldWaiting. // 'hackTimeout' is here temporarily for simplicity when debugging things. - public static GetField(fieldid: FIELD_ID, callback: (field: Opt<Field>) => void = (f) => { }, doc?: Document, key?: Key, hackTimeout: number = -1) { + public static GetField(fieldid: FieldId, callback: (field: Opt<Field>) => void): Opt<Field> | FIELD_WAITING { let cached = this.ClientFieldsCached.get(fieldid); if (!cached) { this.ClientFieldsCached.set(fieldid, FieldWaiting); @@ -35,7 +34,7 @@ export class Server { } })); } else if (cached != FieldWaiting) { - callback(cached); + setTimeout(() => callback(cached as Field), 0); } else { reaction(() => { return this.ClientFieldsCached.get(fieldid); @@ -49,7 +48,7 @@ export class Server { return cached; } - public static GetFields(fieldIds: FIELD_ID[], callback: (fields: { [id: string]: Field }) => any) { + public static GetFields(fieldIds: FieldId[], callback: (fields: { [id: string]: Field }) => any) { SocketStub.SEND_FIELDS_REQUEST(fieldIds, (fields) => { for (let key in fields) { let field = fields[key]; @@ -61,27 +60,19 @@ export class Server { }); } - static times = 0; // hack for testing - public static GetDocumentField(doc: Document, key: Key) { - // let keyId: string = element[0] - // let valueId: string = element[1] - // Server.GetField(keyId, (key: Field) => { - // if (key instanceof Key) { - // Server.GetField(valueId, (field: Field) => { - // console.log(field) - // doc.Set(key as Key, field) - // }) - // } - // else { - // console.log("how did you get a key that isnt a key wtf") - // } - // }) - return this.GetField(doc._proxies.get(key.Id), - action((fieldfromserver: Opt<Field>) => { - if (fieldfromserver) { - doc.fields.set(key.Id, { key, field: fieldfromserver }); - } - }), doc, 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<Field>) => { + if (fieldfromserver) { + doc.fields.set(key.Id, { key, field: fieldfromserver }); + if (callback) { + callback(fieldfromserver); + } + } + })); + } } public static AddDocument(document: Document) { diff --git a/src/client/SocketStub.ts b/src/client/SocketStub.ts index 2bddf4e97..18df4ca0a 100644 --- a/src/client/SocketStub.ts +++ b/src/client/SocketStub.ts @@ -1,16 +1,15 @@ -import { Field, FIELD_ID, Opt } from "../fields/Field" import { Key } from "../fields/Key" -import { KeyStore } from "../fields/KeyStore" -import { ObservableMap, action } from "mobx"; +import { Field, FieldId, Opt } from "../fields/Field" +import { ObservableMap } from "mobx"; import { Document } from "../fields/Document" -import { MessageStore, SetFieldArgs, GetFieldArgs, DocumentTransfer, Types } from "../server/Message"; +import { MessageStore, DocumentTransfer } from "../server/Message"; import { Utils } from "../Utils"; import { Server } from "./Server"; import { ServerUtils } from "../server/ServerUtil"; export class SocketStub { - static FieldStore: ObservableMap<FIELD_ID, Field> = new ObservableMap(); + static FieldStore: ObservableMap<FieldId, Field> = new ObservableMap(); public static SEND_ADD_DOCUMENT(document: Document) { // Send a serialized version of the document to the server @@ -35,7 +34,7 @@ export class SocketStub { Utils.Emit(Server.Socket, MessageStore.AddDocument, new DocumentTransfer(document.ToJson())) } - public static SEND_FIELD_REQUEST(fieldid: FIELD_ID, callback: (field: Opt<Field>) => void) { + public static SEND_FIELD_REQUEST(fieldid: FieldId, callback: (field: Opt<Field>) => void) { if (fieldid) { Utils.EmitCallback(Server.Socket, MessageStore.GetField, fieldid, (field: any) => { if (field) { @@ -47,7 +46,7 @@ export class SocketStub { } } - public static SEND_FIELDS_REQUEST(fieldIds: FIELD_ID[], callback: (fields: { [key: string]: Field }) => any) { + public static SEND_FIELDS_REQUEST(fieldIds: FieldId[], callback: (fields: { [key: string]: Field }) => any) { Utils.EmitCallback(Server.Socket, MessageStore.GetFields, fieldIds, (fields: any[]) => { let fieldMap: any = {}; for (let field of fields) { diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index f779dcd03..4c5f26fbd 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -5,47 +5,53 @@ 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 { FIELD_ID } from "../../fields/Field"; +import { CollectionView, CollectionViewType } from "../views/collections/CollectionView"; +import { FieldView } from "../views/nodes/FieldView"; +import { HtmlField } from "../../fields/HtmlField"; +import { WebView } from "../views/nodes/WebView"; -interface DocumentOptions { +export interface DocumentOptions { x?: number; y?: number; width?: number; height?: number; + nativeWidth?: number; + nativeHeight?: number; title?: string; } 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() }); } function setupOptions(doc: Document, options: DocumentOptions): void { - if (options.x != undefined) { + if (options.x !== undefined) { doc.SetData(KeyStore.X, options.x, NumberField); } - if (options.y != undefined) { + if (options.y !== undefined) { doc.SetData(KeyStore.Y, options.y, NumberField); } - if (options.width != undefined) { + if (options.width !== undefined) { doc.SetData(KeyStore.Width, options.width, NumberField); } - if (options.height != undefined) { + if (options.height !== undefined) { doc.SetData(KeyStore.Height, options.height, NumberField); } - if (options.title != undefined) { + if (options.nativeWidth !== undefined) { + doc.SetData(KeyStore.NativeWidth, options.nativeWidth, NumberField); + } + if (options.nativeHeight !== undefined) { + doc.SetData(KeyStore.NativeHeight, options.nativeHeight, NumberField); + } + if (options.title !== undefined) { doc.SetData(KeyStore.Title, options.title, TextField); } doc.SetData(KeyStore.Scale, 1, NumberField); @@ -75,52 +81,28 @@ 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])); + let htmlProto: Document; + const htmlProtoId = "htmlProto"; + function GetHtmlPrototype(): Document { + if (!htmlProto) { + htmlProto = new Document(htmlProtoId); + htmlProto.Set(KeyStore.X, new NumberField(0)); + htmlProto.Set(KeyStore.Y, new NumberField(0)); + htmlProto.Set(KeyStore.Width, new NumberField(300)); + htmlProto.Set(KeyStore.Height, new NumberField(150)); + htmlProto.Set(KeyStore.Layout, new TextField(WebView.LayoutString())); + htmlProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data])); } - return schemaProto; + return htmlProto; } - export function SchemaDocument(documents: Array<Document>, options: DocumentOptions = {}): Document { - let doc = GetSchemaPrototype().MakeDelegate(); + export function HtmlDocument(html: string, options: DocumentOptions = {}): Document { + let doc = GetHtmlPrototype().MakeDelegate(); setupOptions(doc, options); - doc.Set(KeyStore.Data, new ListField(documents)); + doc.Set(KeyStore.Data, new HtmlField(html)); return doc; } - - let dockProto: Document; - 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(documents: Array<Document>, options: DocumentOptions = {}): Document { - let doc = GetDockPrototype().MakeDelegate(); - setupOptions(doc, options); - doc.Set(KeyStore.Data, new ListField(documents)); - return doc; - } - - let imageProto: Document; const imageProtoId = "imageProto"; function GetImagePrototype(): Document { @@ -129,20 +111,49 @@ export namespace Documents { imageProto.Set(KeyStore.Title, new TextField("IMAGE PROTO")); imageProto.Set(KeyStore.X, new NumberField(0)); imageProto.Set(KeyStore.Y, new NumberField(0)); + imageProto.Set(KeyStore.NativeWidth, new NumberField(300)); + imageProto.Set(KeyStore.NativeHeight, new NumberField(300)); imageProto.Set(KeyStore.Width, new NumberField(300)); imageProto.Set(KeyStore.Height, new NumberField(300)); - imageProto.Set(KeyStore.Layout, new TextField(ImageBox.LayoutString())); + imageProto.Set(KeyStore.Layout, new TextField(CollectionView.LayoutString("AnnotationsKey"))); + imageProto.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform) + imageProto.Set(KeyStore.BackgroundLayout, new TextField(ImageBox.LayoutString())); // imageProto.SetField(KeyStore.Layout, new TextField('<div style={"background-image: " + {Data}} />')); - imageProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data])); + imageProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data, KeyStore.Annotations])); return imageProto; } return imageProto; + } + // example of custom display string for an image that shows a caption. + function EmbeddedCaption() { + return `<div style="position:absolute; height:100%"> + <div style="position:relative; margin:auto; width:85%; margin:auto" >` + + ImageBox.LayoutString() + + `</div> + <div style="position:relative; overflow:auto; height:15%; text-align:center; ">` + + FormattedTextBox.LayoutString("CaptionKey") + + `</div> + </div>` }; + function FixedCaption() { + return `<div style="position:absolute; height:30px; bottom:0; width:100%"> + <div style="position:absolute; width:100%; height:100%; overflow:auto;text-align:center;bottom:0;">` + + FormattedTextBox.LayoutString("CaptionKey") + + `</div> + </div>` }; + export function ImageDocument(url: string, options: DocumentOptions = {}): Document { let doc = GetImagePrototype().MakeDelegate(); setupOptions(doc, options); doc.Set(KeyStore.Data, new ImageField(new URL(url))); + doc.Set(KeyStore.Caption, new TextField("my caption...")); + doc.Set(KeyStore.BackgroundLayout, new TextField(EmbeddedCaption())); + doc.Set(KeyStore.OverlayLayout, new TextField(FixedCaption())); + doc.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data, KeyStore.Annotations, KeyStore.Caption])); + + let annotation = Documents.TextDocument({ title: "hello" }); + doc.Set(KeyStore.Annotations, new ListField([annotation])); return doc; } @@ -151,23 +162,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())); + collectionProto.Set(KeyStore.Layout, new TextField(CollectionView.LayoutString("DataKey"))); collectionProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data])); } return collectionProto; } - export function CollectionDocument(documents: Array<Document>, options: DocumentOptions = {}, id?: string): Document { + export function CollectionDocument(data: Array<Document> | string, viewType: CollectionViewType, options: DocumentOptions = {}, id?: string): Document { let doc = GetCollectionPrototype().MakeDelegate(id); setupOptions(doc, options); - doc.Set(KeyStore.Data, new ListField(documents)); + if (typeof data === "string") { + doc.SetText(KeyStore.Data, data); + } else { + doc.SetData(KeyStore.Data, data, ListField); + } + doc.SetNumber(KeyStore.ViewType, viewType); return doc; } + + export function FreeformDocument(documents: Array<Document>, options: DocumentOptions, id?: string) { + return CollectionDocument(documents, CollectionViewType.Freeform, options, id) + } + + export function SchemaDocument(documents: Array<Document>, options: DocumentOptions, id?: string) { + return CollectionDocument(documents, CollectionViewType.Schema, options, id) + } + + export function DockDocument(config: string, options: DocumentOptions, id?: string) { + return CollectionDocument(config, CollectionViewType.Docking, options, id) + } }
\ No newline at end of file diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index f4dcce7c8..eb4b3aeaa 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,3 +1,4 @@ +import { DocumentDecorations } from "../views/DocumentDecorations"; export namespace DragManager { export function Root() { @@ -43,6 +44,7 @@ export namespace DragManager { drop: (e: Event, de: DropEvent) => void; } + export function MakeDropTarget(element: HTMLElement, options: DropOptions): DragDropDisposer { if ("canDrop" in element.dataset) { throw new Error("Element is already droppable, can't make it droppable again"); @@ -59,10 +61,8 @@ export namespace DragManager { }; } - - let _lastPointerX: number = 0; - let _lastPointerY: number = 0; export function StartDrag(ele: HTMLElement, dragData: { [id: string]: any }, options: DragOptions) { + DocumentDecorations.Instance.Hidden = true; if (!dragDiv) { dragDiv = document.createElement("div"); DragManager.Root().appendChild(dragDiv); @@ -78,9 +78,13 @@ export namespace DragManager { dragElement.style.transformOrigin = "0 0"; dragElement.style.zIndex = "1000"; dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`; + dragElement.style.width = `${rect.width / scaleX}px`; + dragElement.style.height = `${rect.height / scaleY}px`; + // It seems like the above code should be able to just be this: + // dragElement.style.transform = `translate(${x}px, ${y}px)`; + // dragElement.style.width = `${rect.width}px`; + // dragElement.style.height = `${rect.height}px`; dragDiv.appendChild(dragElement); - _lastPointerX = dragData["xOffset"] + rect.left; - _lastPointerY = dragData["yOffset"] + rect.top; let hideSource = false; if (typeof options.hideSource === "boolean") { @@ -96,8 +100,8 @@ export namespace DragManager { const moveHandler = (e: PointerEvent) => { e.stopPropagation(); e.preventDefault(); - x += e.clientX - _lastPointerX; _lastPointerX = e.clientX; - y += e.clientY - _lastPointerY; _lastPointerY = e.clientY; + x += e.movementX; + y += e.movementY; dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`; }; const upHandler = (e: PointerEvent) => { @@ -127,5 +131,6 @@ export namespace DragManager { } })); options.handlers.dragComplete({}); + DocumentDecorations.Instance.Hidden = false; } }
\ No newline at end of file diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 0759ae110..1a711ae64 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -1,13 +1,13 @@ -import { CollectionFreeFormDocumentView } from "../views/nodes/CollectionFreeFormDocumentView"; import { observable, action } from "mobx"; +import { DocumentView } from "../views/nodes/DocumentView"; export namespace SelectionManager { class Manager { @observable - SelectedDocuments: Array<CollectionFreeFormDocumentView> = []; + SelectedDocuments: Array<DocumentView> = []; @action - SelectDoc(doc: CollectionFreeFormDocumentView, ctrlPressed: boolean): void { + SelectDoc(doc: DocumentView, ctrlPressed: boolean): void { // if doc is not in SelectedDocuments, add it if (!ctrlPressed) { manager.SelectedDocuments = []; @@ -21,11 +21,11 @@ export namespace SelectionManager { const manager = new Manager; - export function SelectDoc(doc: CollectionFreeFormDocumentView, ctrlPressed: boolean): void { + export function SelectDoc(doc: DocumentView, ctrlPressed: boolean): void { manager.SelectDoc(doc, ctrlPressed) } - export function IsSelected(doc: CollectionFreeFormDocumentView): boolean { + export function IsSelected(doc: DocumentView): boolean { return manager.SelectedDocuments.indexOf(doc) !== -1; } @@ -33,7 +33,7 @@ export namespace SelectionManager { manager.SelectedDocuments = [] } - export function SelectedDocuments(): Array<CollectionFreeFormDocumentView> { + export function SelectedDocuments(): Array<DocumentView> { return manager.SelectedDocuments; } }
\ No newline at end of file diff --git a/src/client/util/Transform.ts b/src/client/util/Transform.ts new file mode 100644 index 000000000..9fd4f7bef --- /dev/null +++ b/src/client/util/Transform.ts @@ -0,0 +1,113 @@ +export class Transform { + private _translateX: number = 0; + private _translateY: number = 0; + private _scale: number = 1; + + static get Identity(): Transform { + return new Transform(0, 0, 1); + } + + get TranslateX(): number { return this._translateX; } + get TranslateY(): number { return this._translateY; } + get Scale(): number { return this._scale; } + + constructor(x: number, y: number, scale: number) { + this._translateX = x; + this._translateY = y; + this._scale = scale; + } + + translate = (x: number, y: number): Transform => { + this._translateX += x; + this._translateY += y; + return this; + } + + scale = (scale: number): Transform => { + this._scale *= scale; + this._translateX *= scale; + this._translateY *= scale; + return this; + } + + scaleAbout = (scale: number, x: number, y: number): Transform => { + this._translateX += x * this._scale - x * this._scale * scale; + this._translateY += y * this._scale - y * this._scale * scale; + this._scale *= scale; + return this; + } + + transform = (transform: Transform): Transform => { + this._translateX = transform._translateX + transform._scale * this._translateX; + this._translateY = transform._translateY + transform._scale * this._translateY; + this._scale *= transform._scale; + return this; + } + + preTranslate = (x: number, y: number): Transform => { + this._translateX += this._scale * x; + this._translateY += this._scale * y; + return this; + } + + preScale = (scale: number): Transform => { + this._scale *= scale; + return this; + } + + preTransform = (transform: Transform): Transform => { + this._translateX += transform._translateX * this._scale; + this._translateY += transform._translateY * this._scale; + this._scale *= transform._scale; + return this; + } + + translated = (x: number, y: number): Transform => { + return this.copy().translate(x, y); + } + + preTranslated = (x: number, y: number): Transform => { + return this.copy().preTranslate(x, y); + } + + scaled = (scale: number): Transform => { + return this.copy().scale(scale); + } + + scaledAbout = (scale: number, x: number, y: number): Transform => { + return this.copy().scaleAbout(scale, x, y); + } + + preScaled = (scale: number): Transform => { + return this.copy().preScale(scale); + } + + transformed = (transform: Transform): Transform => { + return this.copy().transform(transform); + } + + preTransformed = (transform: Transform): Transform => { + return this.copy().preTransform(transform); + } + + transformPoint = (x: number, y: number): [number, number] => { + x *= this._scale; + x += this._translateX; + y *= this._scale; + y += this._translateY; + return [x, y]; + } + + transformDirection = (x: number, y: number): [number, number] => { + return [x * this._scale, y * this._scale]; + } + + inverse = () => { + return new Transform(-this._translateX / this._scale, -this._translateY / this._scale, 1 / this._scale) + } + + copy = () => { + return new Transform(this._translateX, this._translateY, this._scale); + } + +}
\ No newline at end of file diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts new file mode 100644 index 000000000..46ad558f3 --- /dev/null +++ b/src/client/util/UndoManager.ts @@ -0,0 +1,133 @@ +import { observable, action } from "mobx"; + +function propertyDecorator(target: any, key: string | symbol) { + Object.defineProperty(target, key, { + configurable: true, + enumerable: false, + get: function () { + return 5; + }, + set: function (value: any) { + Object.defineProperty(this, key, { + enumerable: false, + writable: true, + configurable: true, + value: function (...args: any[]) { + try { + UndoManager.StartBatch(); + return value.apply(this, args); + } finally { + UndoManager.EndBatch(); + } + } + }) + } + }) +} +export function undoBatch(target: any, key: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any { + if (!descriptor) { + propertyDecorator(target, key); + return; + } + const oldFunction = descriptor.value; + + descriptor.value = function (...args: any[]) { + try { + UndoManager.StartBatch() + return oldFunction.apply(this, args) + } finally { + UndoManager.EndBatch() + } + } + + return descriptor; +} + +export namespace UndoManager { + export interface UndoEvent { + undo: () => void; + redo: () => void; + } + type UndoBatch = UndoEvent[]; + + let undoStack: UndoBatch[] = observable([]); + let redoStack: UndoBatch[] = observable([]); + let currentBatch: UndoBatch | undefined; + let batchCounter = 0; + let undoing = false; + + export function AddEvent(event: UndoEvent): void { + if (currentBatch && batchCounter && !undoing) { + currentBatch.push(event); + } + } + + export function CanUndo(): boolean { + return undoStack.length > 0; + } + + export function CanRedo(): boolean { + return redoStack.length > 0; + } + + export function StartBatch(): void { + batchCounter++; + if (batchCounter > 0) { + currentBatch = []; + } + } + + export const EndBatch = action(() => { + batchCounter--; + if (batchCounter === 0 && currentBatch && currentBatch.length) { + undoStack.push(currentBatch); + redoStack.length = 0; + currentBatch = undefined; + } + }) + + export function RunInBatch(fn: () => void) { + StartBatch(); + fn(); + EndBatch(); + } + + export const Undo = action(() => { + if (undoStack.length === 0) { + return; + } + + let commands = undoStack.pop(); + if (!commands) { + return; + } + + undoing = true; + for (let i = commands.length - 1; i >= 0; i--) { + commands[i].undo(); + } + undoing = false; + + redoStack.push(commands); + }) + + export const Redo = action(() => { + if (redoStack.length === 0) { + return; + } + + let commands = redoStack.pop(); + if (!commands) { + return; + } + + undoing = true; + for (let i = 0; i < commands.length; i++) { + commands[i].redo(); + } + undoing = false; + + undoStack.push(commands); + }) + +}
\ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 564cb59f6..2f012913d 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,16 +1,17 @@ -import { observable, computed } from "mobx"; +import { observable, computed, action } from "mobx"; import React = require("react"); import { SelectionManager } from "../util/SelectionManager"; import { observer } from "mobx-react"; import './DocumentDecorations.scss' -import { CollectionFreeFormView } from "./collections/CollectionFreeFormView"; +import { KeyStore } from '../../fields/KeyStore' +import { NumberField } from "../../fields/NumberField"; @observer export class DocumentDecorations extends React.Component { static Instance: DocumentDecorations private _resizer = "" private _isPointerDown = false; - @observable private _opacity = 1; + @observable private _hidden = false; constructor(props: Readonly<{}>) { super(props) @@ -20,28 +21,24 @@ export class DocumentDecorations extends React.Component { @computed get Bounds(): { x: number, y: number, b: number, r: number } { - return SelectionManager.SelectedDocuments().reduce((bounds, element) => { - if (element.props.ContainingCollectionView != undefined && - !(element.props.ContainingCollectionView instanceof CollectionFreeFormView)) { + return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => { + if (documentView.props.isTopMost) { return bounds; } - var spt = element.TransformToScreenPoint(0, 0); - var bpt = element.TransformToScreenPoint(element.width, element.height); + let transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse(); + var [sptX, sptY] = transform.transformPoint(0, 0); + let [bptX, bptY] = transform.transformPoint(documentView.props.PanelWidth(), documentView.props.PanelHeight()); return { - x: Math.min(spt.ScreenX, bounds.x), y: Math.min(spt.ScreenY, bounds.y), - r: Math.max(bpt.ScreenX, bounds.r), b: Math.max(bpt.ScreenY, bounds.b) + x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y), + r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b) } }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE }); } - @computed - get opacity(): number { - return this._opacity - } - set opacity(o: number) { - this._opacity = Math.min(Math.max(0, o), 1) - } + @computed + public get Hidden() { return this._hidden; } + public set Hidden(value: boolean) { this._hidden = value; } onPointerDown = (e: React.PointerEvent): void => { e.stopPropagation(); @@ -104,15 +101,28 @@ export class DocumentDecorations extends React.Component { } SelectionManager.SelectedDocuments().forEach(element => { - const rect = element.screenRect; + const rect = element.screenRect(); if (rect.width !== 0) { - let scale = element.width / rect.width; - let actualdW = Math.max(element.width + (dW * scale), 20); - let actualdH = Math.max(element.height + (dH * scale), 20); - element.x += dX * (actualdW - element.width); - element.y += dY * (actualdH - element.height); - element.width = actualdW; - element.height = actualdH; + let doc = element.props.Document; + let width = doc.GetOrCreate(KeyStore.Width, NumberField); + let height = doc.GetOrCreate(KeyStore.Height, NumberField); + let x = doc.GetOrCreate(KeyStore.X, NumberField); + let y = doc.GetOrCreate(KeyStore.Y, NumberField); + let scale = width.Data / rect.width; + let actualdW = Math.max(width.Data + (dW * scale), 20); + let actualdH = Math.max(height.Data + (dH * scale), 20); + x.Data += dX * (actualdW - width.Data); + y.Data += dY * (actualdH - height.Data); + var nativeWidth = doc.GetNumber(KeyStore.NativeWidth, 0); + var nativeHeight = doc.GetNumber(KeyStore.NativeHeight, 0); + if (nativeWidth > 0 && nativeHeight > 0) { + if (Math.abs(dW) > Math.abs(dH)) + actualdH = nativeHeight / nativeWidth * actualdW; + else + actualdW = nativeWidth / nativeHeight * actualdH; + } + width.Data = actualdW; + height.Data = actualdH; } }) } @@ -129,13 +139,19 @@ export class DocumentDecorations extends React.Component { render() { var bounds = this.Bounds; + if (this.Hidden) { + return (null); + } + if (isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { + console.log("DocumentDecorations: Bounds Error") + return (null); + } return ( <div id="documentDecorations-container" style={{ width: (bounds.r - bounds.x + 40) + "px", height: (bounds.b - bounds.y + 40) + "px", left: bounds.x - 20, top: bounds.y - 20, - opacity: this.opacity }}> <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> <div id="documentDecorations-topResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 2e784d3f9..3d1c2ebf4 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -6,6 +6,7 @@ export interface EditableProps { GetValue(): string; SetValue(value: string): boolean; contents: any; + height: number } @observer @@ -29,9 +30,10 @@ export class EditableView extends React.Component<EditableProps> { style={{ width: "100%" }}></input> } else { return ( - <div> + <div style={{ alignItems: "center", display: "flex", height: "100%", maxHeight: `${this.props.height}` }} + onClick={action(() => this.editing = true)} + > {this.props.contents} - <button onClick={action(() => this.editing = true)}>Edit</button> </div> ) } diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 97f68f4f5..cbc19d7fe 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -1,4 +1,4 @@ -import { action, configure } from 'mobx'; +import { action, configure, reaction, computed } from 'mobx'; import "normalize.css"; import * as React from 'react'; import * as ReactDOM from 'react-dom'; @@ -6,31 +6,24 @@ import { DocumentDecorations } from './DocumentDecorations'; import { Documents } from '../documents/Documents'; import { Document } from '../../fields/Document'; import { KeyStore } from '../../fields/KeyStore'; -import { ListField } from '../../fields/ListField'; -import { NumberField } from '../../fields/NumberField'; -import { TextField } from '../../fields/TextField'; import "./Main.scss"; import { ContextMenu } from './ContextMenu'; import { DocumentView } from './nodes/DocumentView'; -import { ImageField } from '../../fields/ImageField'; -import { CompileScript } from './../util/Scripting'; import { Server } from '../Server'; import { Utils } from '../../Utils'; import { ServerUtils } from '../../server/ServerUtil'; import { MessageStore, DocumentTransfer } from '../../server/Message'; import { Database } from '../../server/database'; import * as request from 'request' +import { Transform } from '../util/Transform'; +import { CollectionDockingView } from './collections/CollectionDockingView'; +import { FieldWaiting } from '../../fields/Field'; +import { UndoManager } from '../util/UndoManager'; configure({ enforceActions: "observed" }); - -// const mainNodeCollection = new Array<Document>(); -// let mainContainer = Documents.DockDocument(mainNodeCollection, { -// x: 0, y: 0, title: "main container" -// }) - window.addEventListener("drop", function (e) { e.preventDefault(); }, false) @@ -81,30 +74,42 @@ function init() { console.log("HELLO WORLD") console.log("RESPONSE: " + res) let mainContainer: Document; + let mainfreeform: Document; if (res) { - var lid = KeyStore.Layout.Id; - let obj = ServerUtils.FromJson(res) as Document - mainContainer = obj + mainContainer = ServerUtils.FromJson(res) as Document; + mainContainer.GetAsync(KeyStore.ActiveFrame, field => mainfreeform = field as Document); } else { - const docset: Document[] = []; - mainContainer = Documents.CollectionDocument(docset, { x: 0, y: 400, title: "mini collection" }, mainDocId); - let args = new DocumentTransfer(mainContainer.ToJson()) - Utils.Emit(Server.Socket, MessageStore.AddDocument, args) + mainContainer = Documents.DockDocument(JSON.stringify({ content: [{ type: 'row', content: [] }] }), { title: "main container" }, mainDocId); + Utils.Emit(Server.Socket, MessageStore.AddDocument, new DocumentTransfer(mainContainer.ToJson())) + + setTimeout(() => { + mainfreeform = Documents.FreeformDocument([], { x: 0, y: 400, title: "mini collection" }); + Utils.Emit(Server.Socket, MessageStore.AddDocument, new DocumentTransfer(mainfreeform.ToJson())); + + var docs = [mainfreeform].map(doc => CollectionDockingView.makeDocumentConfig(doc)); + mainContainer.SetText(KeyStore.Data, JSON.stringify({ content: [{ type: 'row', content: docs }] })); + mainContainer.Set(KeyStore.ActiveFrame, mainfreeform); + }, 0); } let addImageNode = action(() => { - mainContainer.GetList<Document>(KeyStore.Data, []).push(Documents.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { + mainfreeform.GetList<Document>(KeyStore.Data, []).push(Documents.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { x: 0, y: 300, width: 200, height: 200, title: "added note" })); }) let addTextNode = action(() => { - mainContainer.GetList<Document>(KeyStore.Data, []).push(Documents.TextDocument({ + mainfreeform.GetList<Document>(KeyStore.Data, []).push(Documents.TextDocument({ x: 0, y: 300, width: 200, height: 200, title: "added note" })); }) let addColNode = action(() => { - mainContainer.GetList<Document>(KeyStore.Data, []).push(Documents.CollectionDocument([], { + mainfreeform.GetList<Document>(KeyStore.Data, []).push(Documents.FreeformDocument([], { + x: 0, y: 300, width: 200, height: 200, title: "added note" + })); + }) + let addSchemaNode = action(() => { + mainfreeform.GetList<Document>(KeyStore.Data, []).push(Documents.SchemaDocument([Documents.TextDocument()], { x: 0, y: 300, width: 200, height: 200, title: "added note" })); }) @@ -115,20 +120,13 @@ function init() { ReactDOM.render(( <div style={{ position: "absolute", width: "100%", height: "100%" }}> - <a href="/logout"> - <img - src="http://aux.iconspalace.com/uploads/logout-icon-256-510450847.png" - style={{ - position: "absolute", - width: "20px", - height: "20px", - top: "20px", - zIndex: 15, - right: "20px", - }} - /> - </a> - <DocumentView Document={mainContainer} ContainingCollectionView={undefined} DocumentView={undefined} /> + <DocumentView Document={mainContainer} + AddDocument={undefined} RemoveDocument={undefined} ScreenToLocalTransform={() => Transform.Identity} + ContentScaling={() => 1} + PanelWidth={() => 0} + PanelHeight={() => 0} + isTopMost={true} + ContainingCollectionView={undefined} /> <DocumentDecorations /> <ContextMenu /> <button style={{ @@ -151,10 +149,28 @@ function init() { }} onClick={addColNode}>Add Collection</button> <button style={{ position: 'absolute', + bottom: '100', + left: '0px', + width: '150px' + }} onClick={addSchemaNode}>Add Schema</button> + <button style={{ + position: 'absolute', bottom: '75px', left: '0px', width: '150px' }} onClick={clearDatabase}>Clear Database</button> + <button style={{ + position: 'absolute', + bottom: '25', + right: '0px', + width: '150px' + }} onClick={() => UndoManager.Undo()}>Undo</button> + <button style={{ + position: 'absolute', + bottom: '0', + right: '0px', + width: '150px' + }} onClick={() => UndoManager.Redo()}>Redo</button> </div>), document.getElementById('root')); }) diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index d9e261f55..86dc66e39 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,118 +1,54 @@ -import { observer } from "mobx-react"; -import { KeyStore } from "../../../fields/KeyStore"; -import React = require("react"); -import FlexLayout from "flexlayout-react"; -import { action, computed } from "mobx"; -import { Document } from "../../../fields/Document"; -import { DocumentView } from "../nodes/DocumentView"; -import { ListField } from "../../../fields/ListField"; -import { NumberField } from "../../../fields/NumberField"; -import "./CollectionDockingView.scss" +import * as GoldenLayout from "golden-layout"; import 'golden-layout/src/css/goldenlayout-base.css'; import 'golden-layout/src/css/goldenlayout-dark-theme.css'; -import * as GoldenLayout from "golden-layout"; +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, Field } from "../../../fields/Field"; +import { KeyStore } from "../../../fields/KeyStore"; +import { Utils } from "../../../Utils"; +import { Server } from "../../Server"; import { DragManager } from "../../util/DragManager"; -import { CollectionViewBase, COLLECTION_BORDER_WIDTH } from "./CollectionViewBase"; -import { FieldView } from "../nodes/FieldView"; +import { undoBatch } from "../../util/UndoManager"; +import { DocumentView } from "../nodes/DocumentView"; +import "./CollectionDockingView.scss"; +import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; +import React = require("react"); +import { SubCollectionViewProps } from "./CollectionViewBase"; @observer -export class CollectionDockingView extends CollectionViewBase { - - private static UseGoldenLayout = true; - public static LayoutString() { return FieldView.LayoutString(CollectionDockingView) } - private _containerRef = React.createRef<HTMLDivElement>(); - @computed - private get modelForFlexLayout() { - const { fieldKey: fieldKey, doc: Document } = this.props; - const value: Document[] = Document.GetData(fieldKey, ListField, []); - var docs = value.map(doc => { - return { type: 'tabset', weight: 50, selected: 0, children: [{ type: "tab", name: doc.Title, component: doc.Id }] }; - }); - return FlexLayout.Model.fromJson({ - global: {}, borders: [], - layout: { - "type": "row", - "weight": 100, - "children": docs +export class CollectionDockingView extends React.Component<SubCollectionViewProps> { + public static Instance: CollectionDockingView; + public static makeDocumentConfig(document: Document) { + return { + type: 'react-component', + component: 'DocumentFrameRenderer', + title: document.Title, + props: { + documentId: document.Id, + //collectionDockingView: CollectionDockingView.Instance } - }); - } - @computed - private get modelForGoldenLayout(): any { - const { fieldKey: fieldKey, doc: Document } = this.props; - const value: Document[] = Document.GetData(fieldKey, ListField, []); - var docs = value.map(doc => { - return { type: 'component', componentName: 'documentViewComponent', componentState: { doc: doc } }; - }); - return new GoldenLayout({ - settings: { - selectionEnabled: true - }, content: [{ type: 'row', content: docs }] - }); - } - - componentDidMount: () => void = () => { - if (this._containerRef.current && CollectionDockingView.UseGoldenLayout) { - this.goldenLayoutFactory(); - window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window } } - componentWillUnmount: () => void = () => { - window.removeEventListener('resize', this.onResize); - } - private nextId = (function () { var _next_id = 0; return function () { return _next_id++; } })(); - - @action - onResize = () => { - var cur = this.props.DocumentViewForField!.MainContent.current; - - // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed - CollectionDockingView.myLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height); - } + private _goldenLayout: any = null; + private _dragDiv: any = null; + private _dragParent: HTMLElement | null = null; + private _dragElement: HTMLDivElement | undefined; + private _dragFakeElement: HTMLDivElement | undefined; + private _containerRef = React.createRef<HTMLDivElement>(); + private _fullScreen: any = null; - @action - onPointerDown = (e: React.PointerEvent): void => { - if (e.button === 2 && this.active) { - e.stopPropagation(); - e.preventDefault(); - } else { - if (e.buttons === 1 && this.active) { - e.stopPropagation(); - } - } + constructor(props: SubCollectionViewProps) { + super(props); + CollectionDockingView.Instance = this; + (window as any).React = React; + (window as any).ReactDOM = ReactDOM; } - flexLayoutFactory = (node: any): any => { - var component = node.getComponent(); - if (component === "button") { - return <button>{node.getName()}</button>; - } - const { fieldKey: fieldKey, doc: Document } = this.props; - const value: Document[] = Document.GetData(fieldKey, ListField, []); - for (var i: number = 0; i < value.length; i++) { - if (value[i].Id === component) { - return (<DocumentView key={value[i].Id} ContainingCollectionView={this} Document={value[i]} DocumentView={undefined} />); - } - } - if (component === "text") { - return (<div className="panel">Panel {node.getName()}</div>); - } - } - - public static myLayout: any = null; - - private static _dragDiv: any = null; - private static _dragParent: HTMLElement | null = null; - private static _dragElement: HTMLDivElement; - private static _dragFakeElement: HTMLDivElement; - public static StartOtherDrag(dragElement: HTMLDivElement, dragDoc: Document) { - var newItemConfig = { - type: 'component', - componentName: 'documentViewComponent', - componentState: { doc: dragDoc } - }; + public StartOtherDrag(dragElement: HTMLDivElement, dragDoc: Document) { this._dragElement = dragElement; this._dragParent = dragElement.parentElement; // bcz: we want to copy this document into the header, not move it there. @@ -122,7 +58,7 @@ export class CollectionDockingView extends CollectionViewBase { this._dragDiv = document.createElement("div"); this._dragDiv.style.opacity = 0; DragManager.Root().appendChild(this._dragDiv); - CollectionDockingView.myLayout.createDragSource(this._dragDiv, newItemConfig); + this._goldenLayout.createDragSource(this._dragDiv, CollectionDockingView.makeDocumentConfig(dragDoc)); // - add our document to that div so that GoldenLayout will get the move events its listening for this._dragDiv.appendChild(this._dragElement); @@ -135,51 +71,48 @@ export class CollectionDockingView extends CollectionViewBase { // all of this must be undone when the document has been dropped (see tabCreated) } - _makeFullScreen: boolean = false; - _maximizedStack: any = null; - public static OpenFullScreen(document: Document) { - var newItemConfig = { - type: 'component', - componentName: 'documentViewComponent', - componentState: { doc: document } - }; - CollectionDockingView.myLayout._makeFullScreen = true; - CollectionDockingView.myLayout.root.contentItems[0].addChild(newItemConfig); + @action + public OpenFullScreen(document: Document) { + let newItemStackConfig = { + type: 'stack', + content: [CollectionDockingView.makeDocumentConfig(document)] + } + var docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout); + this._goldenLayout.root.contentItems[0].addChild(docconfig); + docconfig.callDownwards('_$init'); + this._goldenLayout._$maximiseItem(docconfig); + this._fullScreen = docconfig; + this.stateChanged(); } - public static CloseFullScreen() { - if (CollectionDockingView.myLayout._maximizedStack != null) { - CollectionDockingView.myLayout._maximizedStack.header.controlsContainer.find('.lm_close').click(); - CollectionDockingView.myLayout._maximizedStack = null; + @action + public CloseFullScreen() { + if (this._fullScreen) { + this._goldenLayout._$minimiseItem(this._fullScreen); + this._goldenLayout.root.contentItems[0].removeChild(this._fullScreen); + this._fullScreen = null; + this.stateChanged(); } } // // Creates a vertical split on the right side of the docking view, and then adds the Document to that split // - public static AddRightSplit(document: Document) { - var newItemConfig = { - type: 'component', - componentName: 'documentViewComponent', - componentState: { doc: document } - } + @action + public AddRightSplit(document: Document) { + this._goldenLayout.emit('stateChanged'); let newItemStackConfig = { type: 'stack', - content: [newItemConfig] - }; - var newContentItem = new CollectionDockingView.myLayout._typeToItem[newItemStackConfig.type](CollectionDockingView.myLayout, newItemStackConfig, parent); + content: [CollectionDockingView.makeDocumentConfig(document)] + } - if (CollectionDockingView.myLayout.root.contentItems[0].isRow) { - var rowlayout = CollectionDockingView.myLayout.root.contentItems[0]; - var lastRowItem = rowlayout.contentItems[rowlayout.contentItems.length - 1]; + var newContentItem = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout); - lastRowItem.config["width"] *= 0.5; - newContentItem.config["width"] = lastRowItem.config["width"]; - rowlayout.addChild(newContentItem, rowlayout.contentItems.length, true); - rowlayout.callDownwards('setSize'); + if (this._goldenLayout.root.contentItems[0].isRow) { + this._goldenLayout.root.contentItems[0].addChild(newContentItem); } else { - var collayout = CollectionDockingView.myLayout.root.contentItems[0]; - var newRow = collayout.layoutManager.createContentItem({ type: "row" }, CollectionDockingView.myLayout); + var collayout = this._goldenLayout.root.contentItems[0]; + var newRow = collayout.layoutManager.createContentItem({ type: "row" }, this._goldenLayout); collayout.parent.replaceChild(collayout, newRow); newRow.addChild(newContentItem, undefined, true); @@ -187,91 +120,185 @@ export class CollectionDockingView extends CollectionViewBase { collayout.config["width"] = 50; newContentItem.config["width"] = 50; - collayout.parent.callDownwards('setSize'); } + newContentItem.callDownwards('_$init'); + this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]); + this._goldenLayout.emit('stateChanged'); + this.stateChanged(); } - goldenLayoutFactory() { - CollectionDockingView.myLayout = this.modelForGoldenLayout; - CollectionDockingView.myLayout.on('tabCreated', function (tab: any) { - if (CollectionDockingView._dragDiv) { - CollectionDockingView._dragDiv.removeChild(CollectionDockingView._dragElement); - CollectionDockingView._dragParent!.removeChild(CollectionDockingView._dragFakeElement); - CollectionDockingView._dragParent!.appendChild(CollectionDockingView._dragElement); - DragManager.Root().removeChild(CollectionDockingView._dragDiv); - CollectionDockingView._dragDiv = null; + setupGoldenLayout() { + var config = this.props.Document.GetText(KeyStore.Data, ""); + if (config) { + if (!this._goldenLayout) { + this._goldenLayout = new GoldenLayout(JSON.parse(config)); } - tab.setTitle(tab.contentItem.config.componentState.doc.Title); - tab.closeElement.off('click') //unbind the current click handler - .click(function () { - tab.contentItem.remove(); - }); - }); - - CollectionDockingView.myLayout.on('stackCreated', function (stack: any) { - if (CollectionDockingView.myLayout._makeFullScreen) { - CollectionDockingView.myLayout._maximizedStack = stack; - CollectionDockingView.myLayout._maxstack = stack.header.controlsContainer.find('.lm_maximise'); + else { + if (config == JSON.stringify(this._goldenLayout.toConfig())) + return; + try { + this._goldenLayout.unbind('itemDropped', this.itemDropped); + this._goldenLayout.unbind('tabCreated', this.tabCreated); + this._goldenLayout.unbind('stackCreated', this.stackCreated); + } catch (e) { } + this._goldenLayout.destroy(); + this._goldenLayout = new GoldenLayout(JSON.parse(config)); } - //stack.header.controlsContainer.find('.lm_popout').hide(); - stack.header.controlsContainer.find('.lm_close') //get the close icon - .off('click') //unbind the current click handler - .click(function () { - //if (confirm('really close this?')) { - stack.remove(); - //} - }); - }); - - var me = this; - CollectionDockingView.myLayout.registerComponent('documentViewComponent', function (container: any, state: any) { - // bcz: this is crufty - // calling html() causes a div tag to be added in the DOM with id 'containingDiv'. - // Apparently, we need to wait to allow a live html div element to actually be instantiated. - // After a timeout, we lookup the live html div element and add our React DocumentView to it. - var containingDiv = "component_" + me.nextId(); - container.getElement().html("<div id='" + containingDiv + "'></div>"); - setTimeout(function () { - ReactDOM.render(( - <DocumentView key={state.doc.Id} Document={state.doc} ContainingCollectionView={me} DocumentView={undefined} /> - ), - document.getElementById(containingDiv) - ); - if (CollectionDockingView.myLayout._maxstack != null) { - CollectionDockingView.myLayout._maxstack.click(); + this._goldenLayout.on('itemDropped', this.itemDropped); + this._goldenLayout.on('tabCreated', this.tabCreated); + this._goldenLayout.on('stackCreated', this.stackCreated); + this._goldenLayout.registerComponent('DocumentFrameRenderer', DockedFrameRenderer); + this._goldenLayout.container = this._containerRef.current; + if (this._goldenLayout.config.maximisedItemId === '__glMaximised') { + try { + this._goldenLayout.config.root.getItemsById(this._goldenLayout.config.maximisedItemId)[0].toggleMaximise(); + } catch (e) { + this._goldenLayout.config.maximisedItemId = null; } - }, 0); - }); - CollectionDockingView.myLayout.container = this._containerRef.current; - CollectionDockingView.myLayout.init(); + } + this._goldenLayout.init(); + } } + componentDidMount: () => void = () => { + if (this._containerRef.current) { + reaction( + () => this.props.Document.GetText(KeyStore.Data, ""), + () => this.setupGoldenLayout(), { fireImmediately: true }); + window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window + } + } + componentWillUnmount: () => void = () => { + this._goldenLayout.unbind('itemDropped', this.itemDropped); + this._goldenLayout.unbind('tabCreated', this.tabCreated); + this._goldenLayout.unbind('stackCreated', this.stackCreated); + this._goldenLayout.destroy(); + this._goldenLayout = null; + window.removeEventListener('resize', this.onResize); + } + @action + onResize = (event: any) => { + var cur = this._containerRef.current; - render() { - const { fieldKey: fieldKey, doc: Document } = this.props; - // bcz: not sure why, but I need these to force the flexlayout to update when the collection size changes. - var s = this.props.DocumentViewForField != undefined ? this.props.DocumentViewForField!.ScalingToScreenSpace : 1; - var w = Document.GetData(KeyStore.Width, NumberField, Number(0)) / s; - var h = Document.GetData(KeyStore.Height, NumberField, Number(0)) / s; + // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed + this._goldenLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height); + } + + _flush: boolean = false; + @action + onPointerUp = (e: React.PointerEvent): void => { + if (this._flush) { + this._flush = false; + setTimeout(() => this.stateChanged(), 10); + } + } + @action + onPointerDown = (e: React.PointerEvent): void => { + if (e.button === 2 && this.props.active()) { + e.stopPropagation(); + e.preventDefault(); + } else { + var className = (e.target as any).className; + if (className == "lm_drag_handle" || className == "lm_close" || className == "lm_maximise" || className == "lm_minimise" || className == "lm_close_tab") { + this._flush = true; + } + if (e.buttons === 1 && this.props.active()) { + e.stopPropagation(); + } + } + } - var chooseLayout = () => { - if (!CollectionDockingView.UseGoldenLayout) - return <FlexLayout.Layout model={this.modelForFlexLayout} factory={this.flexLayoutFactory} />; + @undoBatch + stateChanged = () => { + var json = JSON.stringify(this._goldenLayout.toConfig()); + this.props.Document.SetText(KeyStore.Data, json) + } + + itemDropped = () => { + this.stateChanged(); + } + tabCreated = (tab: any) => { + if (this._dragDiv) { + this._dragDiv.removeChild(this._dragElement); + this._dragParent!.removeChild(this._dragFakeElement!); + this._dragParent!.appendChild(this._dragElement!); + DragManager.Root().removeChild(this._dragDiv); + this._dragDiv = null; } + tab.closeElement.off('click') //unbind the current click handler + .click(function () { + tab.contentItem.remove(); + }); + } + + stackCreated = (stack: any) => { + //stack.header.controlsContainer.find('.lm_popout').hide(); + stack.header.controlsContainer.find('.lm_close') //get the close icon + .off('click') //unbind the current click handler + .click(function () { + //if (confirm('really close this?')) { + stack.remove(); + //} + }); + } + render() { return ( - <div className="border" style={{ - borderStyle: "solid", - borderWidth: `${COLLECTION_BORDER_WIDTH}px`, - }}> - <div className="collectiondockingview-container" id="menuContainer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} ref={this._containerRef} - style={{ - width: CollectionDockingView.UseGoldenLayout || s > 1 ? "100%" : w - 2 * COLLECTION_BORDER_WIDTH, - height: CollectionDockingView.UseGoldenLayout || s > 1 ? "100%" : h - 2 * COLLECTION_BORDER_WIDTH - }} > - {chooseLayout()} - </div> - </div> + <div className="collectiondockingview-container" id="menuContainer" + onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} onContextMenu={(e) => e.preventDefault()} ref={this._containerRef} + style={{ + width: "100%", + height: "100%", + borderStyle: "solid", + borderWidth: `${COLLECTION_BORDER_WIDTH}px`, + }} /> ); } +} + +interface DockedFrameProps { + documentId: FieldId, + //collectionDockingView: CollectionDockingView +} +@observer +export class DockedFrameRenderer extends React.Component<DockedFrameProps> { + + @observable private _mainCont = React.createRef<HTMLDivElement>(); + @observable private _panelWidth = 0; + @observable private _document: Opt<Document>; + + constructor(props: any) { + super(props); + Server.GetField(this.props.documentId, action((f: Opt<Field>) => this._document = f as Document)); + } + + private _nativeWidth = () => { return this._document!.GetNumber(KeyStore.NativeWidth, 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._contentScaling()) + } + + render() { + if (!this._document) + return (null); + var content = + <div ref={this._mainCont}> + <DocumentView key={this._document.Id} Document={this._document} + AddDocument={undefined} + RemoveDocument={undefined} + ContentScaling={this._contentScaling} + PanelWidth={this._nativeWidth} + PanelHeight={this._nativeHeight} + ScreenToLocalTransform={this.ScreenToLocalTransform} + isTopMost={true} + ContainingCollectionView={undefined} /> + </div> + + return <Measure onResize={action((r: any) => this._panelWidth = r.entry.width)}> + {({ measureRef }) => <div ref={measureRef}> {content} </div>} + </Measure> + } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionFreeFormView.scss b/src/client/views/collections/CollectionFreeFormView.scss index e9d134e7b..e682b9815 100644 --- a/src/client/views/collections/CollectionFreeFormView.scss +++ b/src/client/views/collections/CollectionFreeFormView.scss @@ -1,4 +1,15 @@ .collectionfreeformview-container { + + ::-webkit-scrollbar { + -webkit-appearance: none; + width: 10px; + } + ::-webkit-scrollbar-thumb { + border-radius: 5px; + background-color: rgba(0,0,0,.5); + } + border-style: solid; + box-sizing: border-box; position: relative; top: 0; left: 0; @@ -9,12 +20,7 @@ position: absolute; top: 0; left: 0; + width:100%; + height: 100% } -} - -.border { - border-style: solid; - box-sizing: border-box; - 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 e6b1d103d..cb6668634 100644 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -1,77 +1,72 @@ +import { action, computed } from "mobx"; import { observer } from "mobx-react"; -import React = require("react"); -import { action, observable, computed } from "mobx"; -import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; -import { DragManager } from "../../util/DragManager"; -import "./CollectionFreeFormView.scss"; -import { Utils } from "../../../Utils"; -import { CollectionViewBase, COLLECTION_BORDER_WIDTH } from "./CollectionViewBase"; -import { SelectionManager } from "../../util/SelectionManager"; -import { KeyStore } from "../../../fields/KeyStore"; import { Document } from "../../../fields/Document"; +import { FieldWaiting } from "../../../fields/Field"; +import { KeyStore } from "../../../fields/KeyStore"; import { ListField } from "../../../fields/ListField"; -import { NumberField } from "../../../fields/NumberField"; +import { TextField } from "../../../fields/TextField"; +import { DragManager } from "../../util/DragManager"; +import { Transform } from "../../util/Transform"; +import { undoBatch } from "../../util/UndoManager"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { CollectionSchemaView } from "../collections/CollectionSchemaView"; +import { CollectionView } from "../collections/CollectionView"; +import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; +import { DocumentView } from "../nodes/DocumentView"; +import { WebView } from "../nodes/WebView"; +import { FormattedTextBox } from "../nodes/FormattedTextBox"; +import { ImageBox } from "../nodes/ImageBox"; +import "./CollectionFreeFormView.scss"; +import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; +import { CollectionViewBase } from "./CollectionViewBase"; +import React = require("react"); import { Documents } from "../../documents/Documents"; -import { FieldWaiting } from "../../../fields/Field"; -import { FakeJsxArgs } from "../nodes/DocumentView"; -import { FieldView } from "../nodes/FieldView"; +const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this? @observer export class CollectionFreeFormView extends CollectionViewBase { - public static LayoutString() { return FieldView.LayoutString(CollectionFreeFormView); } private _canvasRef = React.createRef<HTMLDivElement>(); - private _nodeContainerRef = React.createRef<HTMLDivElement>(); private _lastX: number = 0; private _lastY: number = 0; + private _downX: number = 0; + private _downY: number = 0; + + + @computed get panX(): number { return this.props.Document.GetNumber(KeyStore.PanX, 0) } + @computed get panY(): number { return this.props.Document.GetNumber(KeyStore.PanY, 0) } + @computed get scale(): number { return this.props.Document.GetNumber(KeyStore.Scale, 1); } + @computed get isAnnotationOverlay() { return this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's? + @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } + @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } + @computed get zoomScaling() { return this.props.Document.GetNumber(KeyStore.Scale, 1); } + @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { - const doc = de.data["document"]; - var me = this; - if (doc instanceof CollectionFreeFormDocumentView) { - if (doc.props.ContainingCollectionView && doc.props.ContainingCollectionView !== this) { - doc.props.ContainingCollectionView.removeDocument(doc.props.Document); - this.addDocument(doc.props.Document); - } - const xOffset = de.data["xOffset"] as number || 0; - const yOffset = de.data["yOffset"] as number || 0; - const { scale, translateX, translateY } = Utils.GetScreenTransform(this._canvasRef.current!); - let sscale = this.props.DocumentViewForField!.props.Document.GetData(KeyStore.Scale, NumberField, Number(1)) - const screenX = de.x - xOffset; - const screenY = de.y - yOffset; - const docX = (screenX - translateX) / sscale / scale; - const docY = (screenY - translateY) / sscale / scale; - doc.x = docX; - doc.y = docY; - this.bringToFront(doc); - } - e.stopPropagation(); - } - - private dropDisposer?: DragManager.DragDropDisposer; - createDropTarget = (ele: HTMLDivElement) => { - if (this.dropDisposer) { - this.dropDisposer(); - } - if (ele) { - this.dropDisposer = DragManager.MakeDropTarget(ele, { - handlers: { - drop: this.drop - } - }); - } + super.drop(e, de); + const doc: DocumentView = de.data["document"]; + const xOffset = de.data["xOffset"] as number || 0; + const yOffset = de.data["yOffset"] as number || 0; + //this should be able to use translate and scale methods on an Identity transform, no? + const transform = this.getTransform(); + const screenX = de.x - xOffset; + const screenY = de.y - yOffset; + const [x, y] = transform.transformPoint(screenX, screenY); + doc.props.Document.SetNumber(KeyStore.X, x); + doc.props.Document.SetNumber(KeyStore.Y, y); + this.bringToFront(doc); } @action onPointerDown = (e: React.PointerEvent): void => { - if ((e.button === 2 && this.active) || + if ((e.button === 2 && this.props.active()) || !e.defaultPrevented) { document.removeEventListener("pointermove", this.onPointerMove); document.addEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointerup", this.onPointerUp); - this._lastX = e.pageX; - this._lastY = e.pageY; + this._downX = this._lastX = e.pageX; + this._downY = this._lastY = e.pageY; } } @@ -80,20 +75,23 @@ export class CollectionFreeFormView extends CollectionViewBase { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); e.stopPropagation(); - SelectionManager.DeselectAll(); + if (Math.abs(this._downX - e.clientX) < 3 && Math.abs(this._downY - e.clientY) < 3) { + if (!this.props.isSelected()) { + this.props.select(false); + } + } } @action onPointerMove = (e: PointerEvent): void => { - var me = this; - if (!e.cancelBubble && this.active) { + if (!e.cancelBubble && this.props.active()) { e.preventDefault(); e.stopPropagation(); - let currScale: number = this.props.DocumentViewForField!.ScalingToScreenSpace; - let x = this.props.doc.GetData(KeyStore.PanX, NumberField, Number(0)); - let y = this.props.doc.GetData(KeyStore.PanY, NumberField, Number(0)); - this.props.doc.SetData(KeyStore.PanX, x + (e.pageX - this._lastX) / currScale, NumberField); - this.props.doc.SetData(KeyStore.PanY, y + (e.pageY - this._lastY) / currScale, NumberField); + let x = this.props.Document.GetNumber(KeyStore.PanX, 0); + let y = this.props.Document.GetNumber(KeyStore.PanY, 0); + let [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); + + this.SetPan(x + dx, y + dy); } this._lastX = e.pageX; this._lastY = e.pageY; @@ -102,108 +100,145 @@ export class CollectionFreeFormView extends CollectionViewBase { @action onPointerWheel = (e: React.WheelEvent): void => { e.stopPropagation(); + e.preventDefault(); + let coefficient = 1000; + // if (modes[e.deltaMode] == 'pixels') coefficient = 50; + // else if (modes[e.deltaMode] == 'lines') coefficient = 1000; // This should correspond to line-height?? + let transform = this.getTransform(); - let { LocalX, Ss, Panxx, Xx, LocalY, Panyy, Yy, ContainerX, ContainerY } = this.props.DocumentViewForField!.TransformToLocalPoint(e.pageX, e.pageY); - - var deltaScale = (1 - (e.deltaY / 1000)) * Ss; + let deltaScale = (1 - (e.deltaY / coefficient)); + let [x, y] = transform.transformPoint(e.clientX, e.clientY); - var newContainerX = LocalX * deltaScale + Panxx + Xx; - var newContainerY = LocalY * deltaScale + Panyy + Yy; + let localTransform = this.getLocalTransform(); + localTransform = localTransform.inverse().scaleAbout(deltaScale, x, y) - let dx = ContainerX - newContainerX; - let dy = ContainerY - newContainerY; + this.props.Document.SetNumber(KeyStore.Scale, localTransform.Scale); + this.SetPan(localTransform.TranslateX, localTransform.TranslateY); + } - this.props.doc.Set(KeyStore.Scale, new NumberField(deltaScale)); - this.props.doc.SetData(KeyStore.PanX, Panxx + dx, NumberField); - this.props.doc.SetData(KeyStore.PanY, Panyy + dy, NumberField); + @action + private SetPan(panX: number, panY: number) { + const newPanX = Math.max((1 - this.zoomScaling) * this.nativeWidth, Math.min(0, panX)); + const newPanY = Math.max((1 - this.zoomScaling) * this.nativeHeight, Math.min(0, panY)); + this.props.Document.SetNumber(KeyStore.PanX, false && this.isAnnotationOverlay ? newPanX : panX); + this.props.Document.SetNumber(KeyStore.PanY, false && this.isAnnotationOverlay ? newPanY : panY); } @action onDrop = (e: React.DragEvent): void => { - e.stopPropagation() - e.preventDefault() - let fReader = new FileReader() - let file = e.dataTransfer.items[0].getAsFile(); - let that = this; - const panx: number = this.props.doc.GetData(KeyStore.PanX, NumberField, Number(0)); - const pany: number = this.props.doc.GetData(KeyStore.PanY, NumberField, Number(0)); - let x = e.pageX - panx - let y = e.pageY - pany - - fReader.addEventListener("load", action("drop", (event) => { - if (fReader.result) { - let url = "" + fReader.result; - let doc = Documents.ImageDocument(url, { - x: x, y: y - }) - let docs = that.props.doc.GetT(KeyStore.Data, ListField); - if (docs != FieldWaiting) { - if (!docs) { - docs = new ListField<Document>(); - that.props.doc.Set(KeyStore.Data, docs) - } - docs.Data.push(doc); - } - } - }), false) + const panx: number = this.props.Document.GetNumber(KeyStore.PanX, 0); + const pany: number = this.props.Document.GetNumber(KeyStore.PanY, 0); + let transform = this.getTransform(); - if (file) { - fReader.readAsDataURL(file) - } + var pt = transform.transformPoint(e.pageX, e.pageY); + super.onDrop(e, { x: pt[0], y: pt[1] }); } - onDragOver = (e: React.DragEvent): void => { + onDragOver = (): void => { } @action - bringToFront(doc: CollectionFreeFormDocumentView) { - const { fieldKey: fieldKey, doc: Document } = this.props; - - const value: Document[] = Document.GetList<Document>(fieldKey, []); - var topmost = value.reduce((topmost, d) => Math.max(d.GetNumber(KeyStore.ZIndex, 0), topmost), -1000); - value.map(d => { - var zind = d.GetNumber(KeyStore.ZIndex, 0); - if (zind != topmost - 1 - (topmost - zind) && d != doc.props.Document) { - d.SetData(KeyStore.ZIndex, topmost - 1 - (topmost - zind), NumberField); + bringToFront(doc: DocumentView) { + const { fieldKey: fieldKey, Document: Document } = this.props; + + const value: Document[] = Document.GetList<Document>(fieldKey, []).slice(); + value.sort((doc1, doc2) => { + if (doc1 === doc.props.Document) { + return 1; + } + if (doc2 === doc.props.Document) { + return -1; } - }) + return doc1.GetNumber(KeyStore.ZIndex, 0) - doc2.GetNumber(KeyStore.ZIndex, 0); + }).map((doc, index) => { + doc.SetNumber(KeyStore.ZIndex, index + 1) + }); + } + - if (doc.props.Document.GetNumber(KeyStore.ZIndex, 0) != 0) { - doc.props.Document.SetData(KeyStore.ZIndex, 0, NumberField); + @computed get backgroundLayout(): string | undefined { + let field = this.props.Document.GetT(KeyStore.BackgroundLayout, TextField); + if (field && field !== "<Waiting>") { + return field.Data; } } - - render() { - const { fieldKey: fieldKey, doc: Document } = this.props; - // const value: Document[] = Document.GetList<Document>(fieldKey, []); + @computed get overlayLayout(): string | undefined { + let field = this.props.Document.GetT(KeyStore.OverlayLayout, TextField); + if (field && field !== "<Waiting>") { + return field.Data; + } + } + @computed + get views() { + const { fieldKey, Document } = this.props; const lvalue = Document.GetT<ListField<Document>>(fieldKey, ListField); - if (!lvalue || lvalue === "<Waiting>") { - return <p>Error loading collection data</p> + if (lvalue && lvalue != FieldWaiting) { + return lvalue.Data.map(doc => { + return (<CollectionFreeFormDocumentView key={doc.Id} Document={doc} + AddDocument={this.props.addDocument} + RemoveDocument={this.props.removeDocument} + ScreenToLocalTransform={this.getTransform} + isTopMost={false} + ContentScaling={this.noScaling} + PanelWidth={doc.Width} + PanelHeight={doc.Height} + ContainingCollectionView={this.props.CollectionView} />); + }) } - const panx: number = Document.GetNumber(KeyStore.PanX, 0); - const pany: number = Document.GetNumber(KeyStore.PanY, 0); - const currScale: number = Document.GetNumber(KeyStore.Scale, 1); + return null; + } + @computed + get backgroundView() { + return !this.backgroundLayout ? (null) : + (<JsxParser + components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebView }} + bindings={this.props.bindings} + jsx={this.backgroundLayout} + showWarnings={true} + onError={(test: any) => console.log(test)} + />); + } + @computed + get overlayView() { + return !this.overlayLayout ? (null) : + (<JsxParser + components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView }} + bindings={this.props.bindings} + jsx={this.overlayLayout} + showWarnings={true} + onError={(test: any) => console.log(test)} + />); + } + 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; + + 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 ? + <div style={{ position: "absolute", width: "100%", height: "100%" }}> + {this.overlayView} + </div> + : + (null); return ( - <div className="border" style={{ - borderWidth: `${COLLECTION_BORDER_WIDTH}px`, - }}> - <div className="collectionfreeformview-container" - onPointerDown={this.onPointerDown} - onWheel={this.onPointerWheel} - onContextMenu={(e) => e.preventDefault()} - onDrop={this.onDrop} - onDragOver={this.onDragOver} - ref={this.createDropTarget}> - <div className="collectionfreeformview" style={{ transform: `translate(${panx}px, ${pany}px) scale(${currScale}, ${currScale})`, transformOrigin: `left, top` }} ref={this._canvasRef}> - - <div className="node-container" ref={this._nodeContainerRef}> - {lvalue.Data.map(doc => { - return (<CollectionFreeFormDocumentView key={doc.Id} ContainingCollectionView={this} Document={doc} DocumentView={undefined} />); - })} - </div> - </div> + <div className="collectionfreeformview-container" + onPointerDown={this.onPointerDown} + onWheel={this.onPointerWheel} + onContextMenu={(e) => e.preventDefault()} + onDrop={this.onDrop.bind(this)} + onDragOver={this.onDragOver} + style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px`, }} + ref={this.createDropTarget}> + <div className="collectionfreeformview" + style={{ transformOrigin: "left top", transform: ` translate(${panx}px, ${pany}px) scale(${this.zoomScaling}, ${this.zoomScaling})` }} + ref={this._canvasRef}> + {this.backgroundView} + {this.views} </div> + {this.overlayView} </div> ); } diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index 707b44db6..ba9afee62 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -1,3 +1,88 @@ + +.collectionSchemaView-container { + border-style: solid; + box-sizing: border-box; + position: absolute; + width: 100%; + height: 100%; + ::-webkit-scrollbar { + -webkit-appearance: none; + width: 10px; + } + ::-webkit-scrollbar-thumb { + border-radius: 5px; + background-color: rgba(0,0,0,.5); + } + + .collectionfreeformview-container { + border-width: 0px; + .collectionfreeformview > .jsx-parser{ + position:absolute + } + } + .imageBox-cont { + position:relative; + max-height:100%; + } + .ReactTable { + position: absolute; + // display: inline-block; + // overflow: auto; + width: 100%; + height: 100%; + background: white; + box-sizing: border-box; + } + .ReactTable .rt-table { + overflow-y: auto; + overflow-x: auto; + height: 100%; + + display: -webkit-inline-box; + direction: ltr; + // direction:rtl; + // display:block; + } + .ReactTable .rt-tbody { + //direction: ltr; + direction: rtl; + } + .ReactTable .rt-tr-group { + direction: ltr; + } + .ReactTable .rt-thead.-header { + background:grey; + } + .ReactTable .rt-th, .ReactTable .rt-td { + max-height: 75px; + } + .ReactTable .rt-tbody .rt-tr-group:last-child { + border-bottom: grey; + border-bottom-style: solid; + border-bottom-width: 1; + } + .ReactTable .rt-td { + border-width: 1; + border-right-color: #aaa + } + .ReactTable .rt-tr-group { + border-width: 1; + border-bottom-color: #aaa + } + .imageBox-cont img { + object-fit: contain; + height: 100% + } + .documentView-node:first-child { + background: grey; + .imageBox-cont img { + object-fit: contain; + max-width: 100%; + height: 100% + } + } +} + .Resizer { box-sizing: border-box; background: #000; diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 9f32ccb72..d2db93120 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -1,38 +1,47 @@ import React = require("react") -import ReactTable, { ReactTableDefaults, CellInfo, ComponentPropsGetterRC, ComponentPropsGetterR } from "react-table"; +import { action, observable, trace } from "mobx"; import { observer } from "mobx-react"; +import Measure from "react-measure"; +import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table"; +import "react-table/react-table.css"; +import { Document } from "../../../fields/Document"; +import { Field, FieldWaiting } from "../../../fields/Field"; +import { KeyStore } from "../../../fields/KeyStore"; +import { CompileScript, ToField } from "../../util/Scripting"; +import { Transform } from "../../util/Transform"; +import { EditableView } from "../EditableView"; +import { DocumentView } from "../nodes/DocumentView"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; -import "react-table/react-table.css" -import { observable, action, computed } from "mobx"; -import SplitPane from "react-split-pane" -import "./CollectionSchemaView.scss" -import { ScrollBox } from "../../util/ScrollBox"; +import "./CollectionSchemaView.scss"; +import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; import { CollectionViewBase } from "./CollectionViewBase"; -import { DocumentView } from "../nodes/DocumentView"; -import { EditableView } from "../EditableView"; -import { CompileScript, ToField } from "../../util/Scripting"; -import { KeyStore as KS } from "../../../fields/KeyStore"; -import { Document } from "../../../fields/Document"; -import { Field } from "../../../fields/Field"; @observer export class CollectionSchemaView extends CollectionViewBase { - public static LayoutString() { return FieldView.LayoutString(CollectionSchemaView); } + private _mainCont = React.createRef<HTMLDivElement>(); + private DIVIDER_WIDTH = 5; - @observable - selectedIndex = 0; + @observable _contentScaling = 1; // used to transfer the dimensions of the content pane in the DOM to the ContentScaling prop of the DocumentView + @observable _dividerX = 0; + @observable _panelWidth = 0; + @observable _panelHeight = 0; + @observable _selectedIndex = 0; + @observable _splitPercentage: number = 50; renderCell = (rowProps: CellInfo) => { let props: FieldViewProps = { doc: rowProps.value[0], fieldKey: rowProps.value[1], - DocumentViewForField: undefined + isSelected: () => false, + select: () => { }, + isTopMost: false, + bindings: {} } let contents = ( <FieldView {...props} /> ) return ( - <EditableView contents={contents} GetValue={() => { + <EditableView contents={contents} height={36} GetValue={() => { let field = props.doc.Get(props.fieldKey); if (field && field instanceof Field) { return field.ToScriptString(); @@ -66,79 +75,119 @@ export class CollectionSchemaView extends CollectionViewBase { } return { onClick: action((e: React.MouseEvent, handleOriginal: Function) => { - that.selectedIndex = rowInfo.index; - const doc: Document = rowInfo.original; - console.log("Row clicked: ", doc.Title) + that._selectedIndex = rowInfo.index; + this._splitPercentage += 0.05; // bcz - ugh - needed to force Measure to do its thing and call onResize if (handleOriginal) { handleOriginal() } }), style: { - background: rowInfo.index == this.selectedIndex ? "#00afec" : "white", - color: rowInfo.index == this.selectedIndex ? "white" : "black" + background: rowInfo.index == this._selectedIndex ? "#00afec" : "white", + color: rowInfo.index == this._selectedIndex ? "white" : "black" } }; } + @action + onDividerMove = (e: PointerEvent): void => { + 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); + } + onDividerDown = (e: React.PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + document.addEventListener("pointermove", this.onDividerMove); + document.addEventListener('pointerup', this.onDividerUp); + } + onPointerDown = (e: React.PointerEvent) => { - let target = e.target as HTMLElement; - if (target.tagName == "SPAN" && target.className.includes("Resizer")) { - e.stopPropagation(); - } - if (e.button === 2 && this.active) { - e.stopPropagation(); - e.preventDefault(); - } else { - if (e.buttons === 1 && this.active) { + // if (e.button === 2 && this.active) { + // e.stopPropagation(); + // e.preventDefault(); + // } else + { + if (e.buttons === 1 && this.props.active()) { e.stopPropagation(); } } } + @action + setScaling = (r: any) => { + const children = this.props.Document.GetList<Document>(this.props.fieldKey, []); + const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined; + this._panelWidth = r.entry.width; + this._panelHeight = r.entry.height ? r.entry.height : this._panelHeight; + this._contentScaling = r.entry.width / selected!.GetNumber(KeyStore.NativeWidth, r.entry.width); + } + + getContentScaling = (): number => this._contentScaling; + getPanelWidth = (): number => this._panelWidth; + getPanelHeight = (): number => this._panelHeight; + getTransform = (): Transform => { + return this.props.ScreenToLocalTransform().translate(- COLLECTION_BORDER_WIDTH - this.DIVIDER_WIDTH - this._dividerX, - COLLECTION_BORDER_WIDTH).scale(1 / this._contentScaling); + } + render() { - const { doc: Document, fieldKey: fieldKey } = this.props; - const children = Document.GetList<Document>(fieldKey, []); - const columns = Document.GetList(KS.ColumnsKey, - [KS.Title, KS.Data, KS.Author]) - let content; - if (this.selectedIndex != -1) { - content = ( - <DocumentView Document={children[this.selectedIndex]} DocumentView={undefined} ContainingCollectionView={this} /> - ) - } else { - content = <div /> - } + const columns = this.props.Document.GetList(KeyStore.ColumnsKey, [KeyStore.Title, KeyStore.Data, KeyStore.Author]) + const children = this.props.Document.GetList<Document>(this.props.fieldKey, []); + const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined; + let content = this._selectedIndex == -1 || !selected ? (null) : ( + <Measure onResize={this.setScaling}> + {({ measureRef }) => + <div ref={measureRef}> + <DocumentView Document={selected} + AddDocument={this.props.addDocument} RemoveDocument={this.props.removeDocument} + isTopMost={false} + ScreenToLocalTransform={this.getTransform} + ContentScaling={this.getContentScaling} + PanelWidth={this.getPanelWidth} + PanelHeight={this.getPanelHeight} + ContainingCollectionView={this.props.CollectionView} /> + </div> + } + </Measure> + ) return ( - <div onPointerDown={this.onPointerDown} > - <SplitPane split={"vertical"} defaultSize="60%"> - <ScrollBox> - <ReactTable - data={children} - pageSize={children.length} - page={0} - showPagination={false} - style={{ - display: "inline-block" - }} - columns={columns.map(col => { - return ( - { - Header: col.Name, - accessor: (doc: Document) => [doc, col], - id: col.Id - }) - })} - column={{ - ...ReactTableDefaults.column, - Cell: this.renderCell - }} - getTrProps={this.getTrProps} - /> - </ScrollBox> + <div onPointerDown={this.onPointerDown} ref={this._mainCont} className="collectionSchemaView-container" style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} > + <Measure onResize={action((r: any) => { + this._dividerX = r.entry.width; + this._panelHeight = r.entry.height; + })}> + {({ measureRef }) => + <div ref={measureRef} className="collectionSchemaView-tableContainer" style={{ position: "relative", float: "left", width: `${this._splitPercentage}%`, height: "100%" }}> + <ReactTable + data={children} + pageSize={children.length} + page={0} + showPagination={false} + columns={columns.map(col => ({ + Header: col.Name, + accessor: (doc: Document) => [doc, col], + id: col.Id + }))} + column={{ + ...ReactTableDefaults.column, + Cell: this.renderCell + }} + getTrProps={this.getTrProps} + /> + </div> + } + </Measure> + <div className="collectionSchemaView-dividerDragger" style={{ position: "relative", background: "black", float: "left", width: `${this.DIVIDER_WIDTH}px`, height: "100%" }} onPointerDown={this.onDividerDown} /> + <div className="collectionSchemaView-previewRegion" + onDrop={(e: React.DragEvent) => this.onDrop(e, {})} + ref={this.createDropTarget} + style={{ position: "relative", float: "left", width: `calc(${100 - this._splitPercentage}% - ${this.DIVIDER_WIDTH}px)`, height: "100%" }}> {content} - </SplitPane> - </div> + </div> + </div > ) } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx new file mode 100644 index 000000000..90080ab43 --- /dev/null +++ b/src/client/views/collections/CollectionView.tsx @@ -0,0 +1,98 @@ +import { action, computed } from "mobx"; +import { observer } from "mobx-react"; +import { Document } from "../../../fields/Document"; +import { ListField } from "../../../fields/ListField"; +import { SelectionManager } from "../../util/SelectionManager"; +import { ContextMenu } from "../ContextMenu"; +import React = require("react"); +import { KeyStore } from "../../../fields/KeyStore"; +import { NumberField } from "../../../fields/NumberField"; +import { CollectionFreeFormView } from "./CollectionFreeFormView"; +import { CollectionDockingView } from "./CollectionDockingView"; +import { CollectionSchemaView } from "./CollectionSchemaView"; +import { CollectionViewProps } from "./CollectionViewBase"; + + + +export enum CollectionViewType { + Invalid, + Freeform, + Schema, + Docking, +} + +export const COLLECTION_BORDER_WIDTH = 2; + +@observer +export class CollectionView extends React.Component<CollectionViewProps> { + + public static LayoutString(fieldKey: string = "DataKey") { + return `<CollectionView Document={Document} + ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} isSelected={isSelected} select={select} bindings={bindings} + isTopMost={isTopMost} BackgroundView={BackgroundView} />`; + } + public active = () => { + var isSelected = this.props.isSelected(); + var childSelected = SelectionManager.SelectedDocuments().some(view => view.props.ContainingCollectionView == this); + var topMost = this.props.isTopMost; + return isSelected || childSelected || topMost; + } + @action + addDocument = (doc: Document): void => { + //TODO This won't create the field if it doesn't already exist + const value = this.props.Document.GetData(this.props.fieldKey, ListField, new Array<Document>()) + value.push(doc); + } + + @action + removeDocument = (doc: Document): boolean => { + //TODO This won't create the field if it doesn't already exist + const value = this.props.Document.GetData(this.props.fieldKey, ListField, new Array<Document>()) + let index = value.indexOf(doc); + if (index !== -1) { + value.splice(index, 1) + + SelectionManager.DeselectAll() + ContextMenu.Instance.clearItems() + return true; + } + return false + } + + get collectionViewType(): CollectionViewType { + let Document = this.props.Document; + let viewField = Document.GetT(KeyStore.ViewType, NumberField); + if (viewField === "<Waiting>") { + return CollectionViewType.Invalid; + } else if (viewField) { + return viewField.Data; + } else { + return CollectionViewType.Freeform; + } + } + + set collectionViewType(type: CollectionViewType) { + let Document = this.props.Document; + Document.SetData(KeyStore.ViewType, type, NumberField); + } + + render() { + let viewType = this.collectionViewType; + switch (viewType) { + case CollectionViewType.Freeform: + return (<CollectionFreeFormView {...this.props} + addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active} + CollectionView={this} />) + case CollectionViewType.Schema: + return (<CollectionSchemaView {...this.props} + addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active} + CollectionView={this} />) + case CollectionViewType.Docking: + return (<CollectionDockingView {...this.props} + addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active} + CollectionView={this} />) + default: + return <div></div> + } + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx index 4fcbb1699..7e269caf1 100644 --- a/src/client/views/collections/CollectionViewBase.tsx +++ b/src/client/views/collections/CollectionViewBase.tsx @@ -1,48 +1,114 @@ import { action, computed } from "mobx"; -import { observer } from "mobx-react"; import { Document } from "../../../fields/Document"; -import { Opt } from "../../../fields/Field"; -import { Key } from "../../../fields/Key"; import { ListField } from "../../../fields/ListField"; -import { SelectionManager } from "../../util/SelectionManager"; -import { ContextMenu } from "../ContextMenu"; import React = require("react"); +import { KeyStore } from "../../../fields/KeyStore"; +import { Opt, FieldWaiting } from "../../../fields/Field"; +import { undoBatch } from "../../util/UndoManager"; +import { DragManager } from "../../util/DragManager"; import { DocumentView } from "../nodes/DocumentView"; -import { CollectionDockingView } from "./CollectionDockingView"; -import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; -import { FieldViewProps } from "../nodes/FieldView"; - -export const COLLECTION_BORDER_WIDTH = 2; - -@observer -export class CollectionViewBase extends React.Component<FieldViewProps> { - - @computed - public get active(): boolean { - var isSelected = (this.props.DocumentViewForField instanceof CollectionFreeFormDocumentView && SelectionManager.IsSelected(this.props.DocumentViewForField)); - var childSelected = SelectionManager.SelectedDocuments().some(view => view.props.ContainingCollectionView == this); - var topMost = this.props.DocumentViewForField != undefined && ( - this.props.DocumentViewForField.props.ContainingCollectionView == undefined || - this.props.DocumentViewForField.props.ContainingCollectionView instanceof CollectionDockingView); - return isSelected || childSelected || topMost; +import { Documents, DocumentOptions } from "../../documents/Documents"; +import { Key } from "../../../fields/Key"; +import { Transform } from "../../util/Transform"; + + +export interface CollectionViewProps { + fieldKey: Key; + Document: Document; + ScreenToLocalTransform: () => Transform; + isSelected: () => boolean; + isTopMost: boolean; + select: (ctrlPressed: boolean) => void; + bindings: any; +} +export interface SubCollectionViewProps extends CollectionViewProps { + active: () => boolean; + addDocument: (doc: Document) => void; + removeDocument: (doc: Document) => boolean; + CollectionView: any; +} + +export class CollectionViewBase extends React.Component<SubCollectionViewProps> { + private dropDisposer?: DragManager.DragDropDisposer; + protected createDropTarget = (ele: HTMLDivElement) => { + if (this.dropDisposer) { + this.dropDisposer(); + } + if (ele) { + this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + } } + + @undoBatch @action - addDocument = (doc: Document): void => { - //TODO This won't create the field if it doesn't already exist - const value = this.props.doc.GetData(this.props.fieldKey, ListField, new Array<Document>()) - value.push(doc); + protected drop(e: Event, de: DragManager.DropEvent) { + const doc: DocumentView = de.data["document"]; + if (doc.props.ContainingCollectionView && doc.props.ContainingCollectionView !== this.props.CollectionView) { + if (doc.props.RemoveDocument) { + doc.props.RemoveDocument(doc.props.Document); + } + this.props.addDocument(doc.props.Document); + } + e.stopPropagation(); } @action - removeDocument = (doc: Document): void => { - //TODO This won't create the field if it doesn't already exist - const value = this.props.doc.GetData(this.props.fieldKey, ListField, new Array<Document>()) - if (value.indexOf(doc) !== -1) { - value.splice(value.indexOf(doc), 1) - - SelectionManager.DeselectAll() - ContextMenu.Instance.clearItems() + protected onDrop(e: React.DragEvent, options: DocumentOptions): void { + e.stopPropagation() + 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; } - } -}
\ No newline at end of file + for (let i = 0; i < e.dataTransfer.items.length; i++) { + let item = e.dataTransfer.items[i]; + if (item.kind === "string" && item.type.indexOf("uri") != -1) { + e.dataTransfer.items[i].getAsString(function (s) { + action(() => { + var img = Documents.ImageDocument(s, { ...options, nativeWidth: 300, nativeHeight: 300, width: 300, height: 300 }) + + let docs = that.props.Document.GetT(KeyStore.Data, ListField); + if (docs != FieldWaiting) { + if (!docs) { + docs = new ListField<Document>(); + that.props.Document.Set(KeyStore.Data, docs) + } + docs.Data.push(img); + } + })() + + }) + } + if (item.kind == "file" && item.type.indexOf("image")) { + let fReader = new FileReader() + let file = item.getAsFile(); + + fReader.addEventListener("load", action("drop", () => { + if (fReader.result) { + let url = "" + fReader.result; + let doc = Documents.ImageDocument(url, options) + let docs = that.props.Document.GetT(KeyStore.Data, ListField); + if (docs != FieldWaiting) { + if (!docs) { + docs = new ListField<Document>(); + that.props.Document.Set(KeyStore.Data, docs) + } + docs.Data.push(doc); + } + } + }), false) + + if (file) { + fReader.readAsDataURL(file) + } + } + } + } +} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index bfd50da81..17123bf52 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,23 +1,16 @@ -import { action, computed } from "mobx"; +import { computed, trace } 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 "./NodeView.scss"; -import React = require("react"); +import { Transform } from "../../util/Transform"; import { DocumentView, DocumentViewProps } from "./DocumentView"; -import { Utils } from "../../../Utils"; +import "./DocumentView.scss"; +import React = require("react"); @observer -export class CollectionFreeFormDocumentView extends DocumentView { - private _contextMenuCanOpen = false; - private _downX: number = 0; - private _downY: number = 0; +export class CollectionFreeFormDocumentView extends React.Component<DocumentViewProps> { + private _mainCont = React.createRef<HTMLDivElement>(); constructor(props: DocumentViewProps) { super(props); @@ -30,198 +23,81 @@ export class CollectionFreeFormDocumentView extends DocumentView { } @computed - get x(): number { - return this.props.Document.GetData(KeyStore.X, NumberField, Number(0)); - } - - @computed - get y(): number { - return this.props.Document.GetData(KeyStore.Y, NumberField, Number(0)); - } - - set x(x: number) { - this.props.Document.SetData(KeyStore.X, x, NumberField) - } - - set y(y: number) { - this.props.Document.SetData(KeyStore.Y, y, NumberField) + get transform(): string { + return `scale(${this.props.ContentScaling()}, ${this.props.ContentScaling()}) translate(${this.props.Document.GetNumber(KeyStore.X, 0)}px, ${this.props.Document.GetNumber(KeyStore.Y, 0)}px)`; } @computed - get transform(): string { - return `translate(${this.x}px, ${this.y}px)`; + get width(): number { + return this.props.Document.GetNumber(KeyStore.Width, 0); } @computed - get width(): number { - return this.props.Document.GetData(KeyStore.Width, NumberField, Number(0)); + get nativeWidth(): number { + return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } set width(w: number) { this.props.Document.SetData(KeyStore.Width, w, NumberField) + if (this.nativeWidth > 0 && this.nativeHeight > 0) { + this.props.Document.SetNumber(KeyStore.Height, this.nativeHeight / this.nativeWidth * w) + } } @computed get height(): number { - return this.props.Document.GetData(KeyStore.Height, NumberField, Number(0)); + return this.props.Document.GetNumber(KeyStore.Height, 0); + } + @computed + get nativeHeight(): number { + return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } set height(h: number) { - this.props.Document.SetData(KeyStore.Height, h, NumberField) + this.props.Document.SetData(KeyStore.Height, h, NumberField); + if (this.nativeWidth > 0 && this.nativeHeight > 0) { + this.props.Document.SetNumber(KeyStore.Width, this.nativeWidth / this.nativeHeight * h) + } } @computed get zIndex(): number { - return this.props.Document.GetData(KeyStore.ZIndex, NumberField, Number(0)); + return this.props.Document.GetNumber(KeyStore.ZIndex, 0); } set zIndex(h: number) { this.props.Document.SetData(KeyStore.ZIndex, h, NumberField) } - @action - dragComplete = (e: DragManager.DragCompleteEvent) => { + contentScaling = () => { + return this.nativeWidth > 0 ? this.width / this.nativeWidth : 1; } - @computed - get active(): boolean { - return SelectionManager.IsSelected(this) || this.props.ContainingCollectionView === undefined || - this.props.ContainingCollectionView.active; + getTransform = (): Transform => { + return this.props.ScreenToLocalTransform(). + translate(-this.props.Document.GetNumber(KeyStore.X, 0), -this.props.Document.GetNumber(KeyStore.Y, 0)).scale(1 / this.contentScaling()); } @computed - get topMost(): boolean { - return this.props.ContainingCollectionView == undefined || this.props.ContainingCollectionView instanceof CollectionDockingView; - } - - onPointerDown = (e: React.PointerEvent): void => { - this._downX = e.clientX; - this._downY = e.clientY; - var me = this; - if (e.shiftKey && e.buttons === 1) { - CollectionDockingView.StartOtherDrag(this._mainCont.current!, this.props.Document); - e.stopPropagation(); - return; - } - this._contextMenuCanOpen = e.button == 2; - if (this.active && !e.isDefaultPrevented()) { - e.stopPropagation(); - if (e.buttons === 2) { - e.preventDefault(); - } - document.removeEventListener("pointermove", this.onPointerMove) - document.addEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp) - document.addEventListener("pointerup", this.onPointerUp); - } - } - - onPointerMove = (e: PointerEvent): void => { - if (e.cancelBubble) { - this._contextMenuCanOpen = false; - return; - } - if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { - this._contextMenuCanOpen = false; - if (this._mainCont.current != null && !this.topMost) { - this._contextMenuCanOpen = false; - const rect = this.screenRect; - let dragData: { [id: string]: any } = {}; - dragData["document"] = this; - dragData["xOffset"] = e.x - rect.left; - dragData["yOffset"] = e.y - rect.top; - DragManager.StartDrag(this._mainCont.current, dragData, { - handlers: { - dragComplete: this.dragComplete, - }, - hideSource: true - }) - } - } - e.stopPropagation(); - e.preventDefault(); - } - - onPointerUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onPointerMove) - document.removeEventListener("pointerup", this.onPointerUp) - e.stopPropagation(); - if (Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientY - this._downY) < 4) { - SelectionManager.SelectDoc(this, e.ctrlKey); - } - } - - openRight = (e: React.MouseEvent): void => { - CollectionDockingView.AddRightSplit(this.props.Document); - } - - deleteClicked = (e: React.MouseEvent): void => { - if (this.props.ContainingCollectionView instanceof CollectionFreeFormView) { - this.props.ContainingCollectionView.removeDocument(this.props.Document) - } - } - @action - fullScreenClicked = (e: React.MouseEvent): void => { - CollectionDockingView.OpenFullScreen(this.props.Document); - ContextMenu.Instance.clearItems(); - ContextMenu.Instance.addItem({ description: "Close Full Screen", event: this.closeFullScreenClicked }); - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) - } - @action - closeFullScreenClicked = (e: React.MouseEvent): void => { - CollectionDockingView.CloseFullScreen(); - ContextMenu.Instance.clearItems(); - ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }) - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) - } - - @action - onContextMenu = (e: React.MouseEvent): void => { - if (!SelectionManager.IsSelected(this)) { - return; - } - e.preventDefault() - - if (!this._contextMenuCanOpen) { - return; - } - - if (this.topMost) { - ContextMenu.Instance.clearItems() - ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }) - } - else { - // DocumentViews should stop propogation of this event - e.stopPropagation(); - - ContextMenu.Instance.clearItems(); - ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }) - ContextMenu.Instance.addItem({ description: "Open Right", event: this.openRight }) - ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked }) - SelectionManager.SelectDoc(this, e.ctrlKey); - } - - ContextMenu.Instance.addItem({ - description: "Copy ID", event: () => { - Utils.CopyText(this.props.Document.Id) - } - }) - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) + get docView() { + return <DocumentView {...this.props} + ContentScaling={this.contentScaling} + ScreenToLocalTransform={this.getTransform} + /> } render() { return ( - <div className="node" ref={this._mainCont} style={{ + <div ref={this._mainCont} style={{ + transformOrigin: "left top", transform: this.transform, width: this.width, height: this.height, position: "absolute", zIndex: this.zIndex, - }} - onContextMenu={this.onContextMenu} - onPointerDown={this.onPointerDown}> - - <DocumentView {...this.props} DocumentView={this} /> + backgroundColor: "transparent" + }} > + {this.docView} </div> ); } diff --git a/src/client/views/nodes/NodeView.scss b/src/client/views/nodes/DocumentView.scss index dac1c0a8e..8e2ebd690 100644 --- a/src/client/views/nodes/NodeView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -1,4 +1,4 @@ -.node { +.documentView-node { position: absolute; background: #cdcdcd; overflow: hidden; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 3767d28c6..ad1328e5d 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,27 +1,37 @@ import { action, computed } from "mobx"; import { observer } from "mobx-react"; import { Document } from "../../../fields/Document"; -import { Opt, FieldWaiting, Field } from "../../../fields/Field"; +import { Field, FieldWaiting, Opt } from "../../../fields/Field"; import { Key } from "../../../fields/Key"; import { KeyStore } from "../../../fields/KeyStore"; import { ListField } from "../../../fields/ListField"; -import { NumberField } from "../../../fields/NumberField"; -import { TextField } from "../../../fields/TextField"; -import { Utils } from "../../../Utils"; +import { DragManager } from "../../util/DragManager"; +import { SelectionManager } from "../../util/SelectionManager"; +import { Transform } from "../../util/Transform"; import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionFreeFormView } from "../collections/CollectionFreeFormView"; import { CollectionSchemaView } from "../collections/CollectionSchemaView"; -import { CollectionViewBase, COLLECTION_BORDER_WIDTH } from "../collections/CollectionViewBase"; +import { CollectionView, CollectionViewType } from "../collections/CollectionView"; +import { WebView } from "./WebView"; +import { ContextMenu } from "../ContextMenu"; import { FormattedTextBox } from "../nodes/FormattedTextBox"; import { ImageBox } from "../nodes/ImageBox"; -import "./NodeView.scss"; +import "./DocumentView.scss"; import React = require("react"); const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this? export interface DocumentViewProps { + ContainingCollectionView: Opt<CollectionView>; + Document: Document; - DocumentView: Opt<DocumentView> // needed only to set ContainingDocumentView on CollectionViewProps when invoked from JsxParser -- is there a better way? - ContainingCollectionView: Opt<CollectionViewBase>; + AddDocument?: (doc: Document) => void; + RemoveDocument?: (doc: Document) => boolean; + ScreenToLocalTransform: () => Transform; + isTopMost: boolean; + //tfs: This shouldn't be necessary I don't think + ContentScaling: () => number; + PanelWidth: () => number; + PanelHeight: () => number; } export interface JsxArgs extends DocumentViewProps { Keys: { [name: string]: Key } @@ -68,132 +78,167 @@ export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs { @observer export class DocumentView extends React.Component<DocumentViewProps> { - protected _mainCont = React.createRef<any>(); - get MainContent() { - return this._mainCont; - } - @computed - get layout(): string { - return this.props.Document.GetData(KeyStore.Layout, TextField, String("<p>Error loading layout data</p>")); - } - - @computed - get layoutKeys(): Key[] { - return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array<Key>()); + private _mainCont = React.createRef<HTMLDivElement>(); + private _documentBindings: any = null; + private _contextMenuCanOpen = false; + private _downX: number = 0; + private _downY: number = 0; + + @computed get active(): boolean { return SelectionManager.IsSelected(this) || !this.props.ContainingCollectionView || this.props.ContainingCollectionView.active(); } + @computed get topMost(): boolean { return !this.props.ContainingCollectionView || this.props.ContainingCollectionView.collectionViewType == CollectionViewType.Docking; } + @computed get layout(): string { return this.props.Document.GetText(KeyStore.Layout, "<p>Error loading layout data</p>"); } + @computed get layoutKeys(): Key[] { return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array<Key>()); } + @computed get layoutFields(): Key[] { return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array<Key>()); } + + screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect(); + + onPointerDown = (e: React.PointerEvent): void => { + this._downX = e.clientX; + this._downY = e.clientY; + if (e.shiftKey && e.buttons === 1) { + CollectionDockingView.Instance.StartOtherDrag(this._mainCont.current!, this.props.Document); + e.stopPropagation(); + } else { + this._contextMenuCanOpen = true; + if (this.active && !e.isDefaultPrevented()) { + e.stopPropagation(); + if (e.buttons === 2) { + e.preventDefault(); + } + document.removeEventListener("pointermove", this.onPointerMove) + document.addEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp) + document.addEventListener("pointerup", this.onPointerUp); + } + } } - @computed - get layoutFields(): Key[] { - return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array<Key>()); + onPointerMove = (e: PointerEvent): void => { + if (e.cancelBubble) { + this._contextMenuCanOpen = false; + return; + } + if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { + this._contextMenuCanOpen = false; + if (this._mainCont.current != null && !this.topMost) { + this._contextMenuCanOpen = false; + const [left, top] = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + let dragData: { [id: string]: any } = {}; + dragData["document"] = this; + dragData["xOffset"] = e.x - left; + dragData["yOffset"] = e.y - top; + DragManager.StartDrag(this._mainCont.current, dragData, { + handlers: { + dragComplete: action((e: DragManager.DragCompleteEvent) => { }), + }, + hideSource: true + }) + } + } + e.stopPropagation(); + e.preventDefault(); } - // - // returns the cumulative scaling between the document and the screen - // - @computed - public get ScalingToScreenSpace(): number { - if (this.props.ContainingCollectionView != undefined && - this.props.ContainingCollectionView.props.DocumentViewForField != undefined) { - let ss = this.props.ContainingCollectionView.props.doc.GetData(KeyStore.Scale, NumberField, Number(1)); - return this.props.ContainingCollectionView.props.DocumentViewForField.ScalingToScreenSpace * ss; + onPointerUp = (e: PointerEvent): void => { + document.removeEventListener("pointermove", this.onPointerMove) + document.removeEventListener("pointerup", this.onPointerUp) + e.stopPropagation(); + if (Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientY - this._downY) < 4) { + SelectionManager.SelectDoc(this, e.ctrlKey); } - return 1; } - // - // Converts a coordinate in the screen space of the app into a local document coordinate. - // - public TransformToLocalPoint(screenX: number, screenY: number) { - // if this collection view is nested within another collection view, then - // first transform the screen point into the parent collection's coordinate space. - let { LocalX: parentX, LocalY: parentY } = this.props.ContainingCollectionView != undefined && - this.props.ContainingCollectionView.props.DocumentViewForField != undefined ? - this.props.ContainingCollectionView.props.DocumentViewForField.TransformToLocalPoint(screenX, screenY) : - { LocalX: screenX, LocalY: screenY }; - let ContainerX: number = parentX - COLLECTION_BORDER_WIDTH; - let ContainerY: number = parentY - COLLECTION_BORDER_WIDTH; - - var Xx = this.props.Document.GetData(KeyStore.X, NumberField, Number(0)); - var Yy = this.props.Document.GetData(KeyStore.Y, NumberField, Number(0)); - // CollectionDockingViews change the location of their children frames without using a Dash transformation. - // They also ignore any transformation that may have been applied to their content document. - // NOTE: this currently assumes CollectionDockingViews aren't nested. - if (this.props.ContainingCollectionView instanceof CollectionDockingView) { - var { translateX: rx, translateY: ry } = Utils.GetScreenTransform(this.MainContent.current!); - Xx = rx - COLLECTION_BORDER_WIDTH; - Yy = ry - COLLECTION_BORDER_WIDTH; + deleteClicked = (e: React.MouseEvent): void => { + if (this.props.RemoveDocument) { + this.props.RemoveDocument(this.props.Document); } - - let Ss = this.props.Document.GetData(KeyStore.Scale, NumberField, Number(1)); - let Panxx = this.props.Document.GetData(KeyStore.PanX, NumberField, Number(0)); - let Panyy = this.props.Document.GetData(KeyStore.PanY, NumberField, Number(0)); - let LocalX = (ContainerX - (Xx + Panxx)) / Ss; - let LocalY = (ContainerY - (Yy + Panyy)) / Ss; - - return { LocalX, Ss, Panxx, Xx, LocalY, Panyy, Yy, ContainerX, ContainerY }; + } + fullScreenClicked = (e: React.MouseEvent): void => { + CollectionDockingView.Instance.OpenFullScreen(this.props.Document); + ContextMenu.Instance.clearItems(); + ContextMenu.Instance.addItem({ description: "Close Full Screen", event: this.closeFullScreenClicked }); + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) + } + closeFullScreenClicked = (e: React.MouseEvent): void => { + CollectionDockingView.Instance.CloseFullScreen(); + ContextMenu.Instance.clearItems(); + ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }) + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) } - // - // Converts a point in the coordinate space of a document to a screen space coordinate. - // - public TransformToScreenPoint(localX: number, localY: number, Ss: number = 1, Panxx: number = 0, Panyy: number = 0): { ScreenX: number, ScreenY: number } { - - var Xx = this.props.Document.GetData(KeyStore.X, NumberField, Number(0)); - var Yy = this.props.Document.GetData(KeyStore.Y, NumberField, Number(0)); - // CollectionDockingViews change the location of their children frames without using a Dash transformation. - // They also ignore any transformation that may have been applied to their content document. - // NOTE: this currently assumes CollectionDockingViews aren't nested. - if (this.props.ContainingCollectionView instanceof CollectionDockingView) { - var { translateX: rx, translateY: ry } = Utils.GetScreenTransform(this.MainContent.current!); - Xx = rx - COLLECTION_BORDER_WIDTH; - Yy = ry - COLLECTION_BORDER_WIDTH; + @action + onContextMenu = (e: React.MouseEvent): void => { + e.preventDefault() + e.stopPropagation(); + if (!SelectionManager.IsSelected(this) || !this._contextMenuCanOpen) { + return; } - let W = COLLECTION_BORDER_WIDTH; - let H = COLLECTION_BORDER_WIDTH; - let parentX = (localX - W) * Ss + (Xx + Panxx) + W; - let parentY = (localY - H) * Ss + (Yy + Panyy) + H; - - // if this collection view is nested within another collection view, then - // first transform the local point into the parent collection's coordinate space. - let containingDocView = this.props.ContainingCollectionView != undefined ? this.props.ContainingCollectionView.props.DocumentViewForField : undefined; - if (containingDocView != undefined) { - let ss = containingDocView.props.Document.GetData(KeyStore.Scale, NumberField, Number(1)); - let panxx = containingDocView.props.Document.GetData(KeyStore.PanX, NumberField, Number(0)) + COLLECTION_BORDER_WIDTH * ss; - let panyy = containingDocView.props.Document.GetData(KeyStore.PanY, NumberField, Number(0)) + COLLECTION_BORDER_WIDTH * ss; - let { ScreenX, ScreenY } = containingDocView.TransformToScreenPoint(parentX, parentY, ss, panxx, panyy); - parentX = ScreenX; - parentY = ScreenY; + if (this.topMost) { + ContextMenu.Instance.clearItems() + ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }) + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) + } + else { + // DocumentViews should stop propagation of this event + e.stopPropagation(); + + ContextMenu.Instance.clearItems(); + ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }) + ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document) }) + ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked }) + ContextMenu.Instance.addItem({ description: "Freeform", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform) }) + ContextMenu.Instance.addItem({ description: "Schema", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Schema) }) + ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) }) + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) + SelectionManager.SelectDoc(this, e.ctrlKey); } - return { ScreenX: parentX, ScreenY: parentY }; } - + @computed get mainContent() { + return <JsxParser + components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebView }} + bindings={this._documentBindings} + jsx={this.layout} + showWarnings={true} + onError={(test: any) => { console.log(test) }} + /> + } render() { - let bindings = { ...this.props } as any; + if (!this.props.Document) + return <div></div> let lkeys = this.props.Document.GetT(KeyStore.LayoutKeys, ListField); if (!lkeys || lkeys === "<Waiting>") { return <p>Error loading layout keys</p>; } + this._documentBindings = { + ...this.props, + isSelected: () => SelectionManager.IsSelected(this), + select: (ctrlPressed: boolean) => SelectionManager.SelectDoc(this, ctrlPressed) + }; for (const key of this.layoutKeys) { - bindings[key.Name + "Key"] = key; // this maps string values of the form <keyname>Key to an actual key Kestore.keyname e.g, "DataKey" => KeyStore.Data + this._documentBindings[key.Name + "Key"] = key; // this maps string values of the form <keyname>Key to an actual key Kestore.keyname e.g, "DataKey" => KeyStore.Data } for (const key of this.layoutFields) { let field = this.props.Document.Get(key); - bindings[key.Name] = field && field != FieldWaiting ? field.GetValue() : field; - } - if (bindings.DocumentView === undefined) { - bindings.DocumentView = this; // set the DocumentView to this if it hasn't already been set by a sub-class during its render method. + this._documentBindings[key.Name] = field && field != FieldWaiting ? field.GetValue() : field; } + this._documentBindings.bindings = this._documentBindings; + + var scaling = this.props.ContentScaling(); + var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); + var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0); return ( - <div className="node" ref={this._mainCont} style={{ width: "100%", height: "100%", }}> - <JsxParser - components={{ FormattedTextBox: FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView }} - bindings={bindings} - jsx={this.layout} - showWarnings={true} - onError={(test: any) => { console.log(test) }} - /> + <div className="documentView-node" ref={this._mainCont} + style={{ + width: nativeWidth > 0 ? nativeWidth.toString() + "px" : "100%", + height: nativeHeight > 0 ? nativeHeight.toString() + "px" : "100%", + transformOrigin: "left top", + transform: `scale(${scaling},${scaling})` + }} + onContextMenu={this.onContextMenu} + onPointerDown={this.onPointerDown} > + {this.mainContent} </div> ) } diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index fae124528..1a9d325db 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -10,7 +10,8 @@ import { ImageField } from "../../../fields/ImageField"; import { Key } from "../../../fields/Key"; import { FormattedTextBox } from "./FormattedTextBox"; import { ImageBox } from "./ImageBox"; -import { DocumentView } from "./DocumentView"; +import { HtmlField } from "../../../fields/HtmlField"; +import { WebView } from "./WebView"; // // these properties get assigned through the render() method of the DocumentView when it creates this node. @@ -20,12 +21,16 @@ import { DocumentView } from "./DocumentView"; export interface FieldViewProps { fieldKey: Key; doc: Document; - DocumentViewForField: Opt<DocumentView> + isSelected: () => boolean; + select: () => void; + isTopMost: boolean; + bindings: any; } @observer export class FieldView extends React.Component<FieldViewProps> { - public static LayoutString(fieldType: { name: string }) { return `<${fieldType.name} doc={Document} DocumentViewForField={DocumentView} fieldKey={DataKey} />`; } + public static LayoutString(fieldType: { name: string }, fieldStr: string = "DataKey") { return `<${fieldType.name} doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={${fieldStr}} isSelected={isSelected} select={select} isTopMost={isTopMost} />`; } + @computed get field(): FieldValue<Field> { const { doc, fieldKey } = this.props; @@ -47,6 +52,8 @@ export class FieldView extends React.Component<FieldViewProps> { } else if (field instanceof NumberField) { return <p>{field.Data}</p> + } else if (field instanceof HtmlField) { + return <WebView {...this.props} /> } else if (field != FieldWaiting) { return <p>{field.GetValue}</p> } else diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 492367fce..226456fab 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -1,7 +1,7 @@ .ProseMirror { - margin-top: -1em; width: 100%; - height: 100%; + height: auto; + min-height: 100% } .ProseMirror:focus { @@ -9,6 +9,6 @@ } .formattedTextBox-cont { - background: white; - padding: 1vw; + background: beige; + padding: 0; }
\ No newline at end of file diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 39d7bf4f0..c0969a8c3 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,5 +1,4 @@ import { action, IReactionDisposer, reaction } from "mobx"; -import { observer } from "mobx-react" import { baseKeymap } from "prosemirror-commands"; import { history, redo, undo } from "prosemirror-history"; import { keymap } from "prosemirror-keymap"; @@ -7,12 +6,10 @@ import { schema } from "prosemirror-schema-basic"; import { EditorState, Transaction } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { Opt, FieldWaiting, FieldValue } from "../../../fields/Field"; -import { SelectionManager } from "../../util/SelectionManager"; import "./FormattedTextBox.scss"; import React = require("react") import { RichTextField } from "../../../fields/RichTextField"; import { FieldViewProps, FieldView } from "./FieldView"; -import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView"; // FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document @@ -31,10 +28,9 @@ import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView // specified Key and assigns it to an HTML input node. When changes are made tot his node, // this will edit the document and assign the new value to that field. //] -@observer export class FormattedTextBox extends React.Component<FieldViewProps> { - public static LayoutString() { return FieldView.LayoutString(FormattedTextBox) } + public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(FormattedTextBox, fieldStr) } private _ref: React.RefObject<HTMLDivElement>; private _editorView: Opt<EditorView>; private _reactionDisposer: Opt<IReactionDisposer>; @@ -111,7 +107,7 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { } onPointerDown = (e: React.PointerEvent): void => { let me = this; - if (e.buttons === 1 && me.props.DocumentViewForField instanceof CollectionFreeFormDocumentView && SelectionManager.IsSelected(me.props.DocumentViewForField)) { + if (e.buttons === 1 && this.props.isSelected()) { e.stopPropagation(); } } @@ -119,7 +115,8 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { return (<div className="formattedTextBox-cont" style={{ color: "initial", - whiteSpace: "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 136fda1d0..5b15b3329 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -1,6 +1,15 @@ .imageBox-cont { padding: 0vw; + position: relative; + text-align: center; + width: 100%; + max-width: 100%; + max-height: 100% +} +.imageBox-cont img { + max-width: 100%; + max-height: 100% } .imageBox-button { diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 013b8b7d3..b5ce8b28c 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,15 +1,14 @@ import Lightbox from 'react-image-lightbox'; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app -import { SelectionManager } from "../../util/SelectionManager"; import "./ImageBox.scss"; import React = require("react") import { ImageField } from '../../../fields/ImageField'; import { FieldViewProps, FieldView } from './FieldView'; -import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView'; import { FieldWaiting } from '../../../fields/Field'; import { observer } from "mobx-react" import { observable, action } from 'mobx'; +import { KeyStore } from '../../../fields/KeyStore'; @observer export class ImageBox extends React.Component<FieldViewProps> { @@ -40,7 +39,7 @@ export class ImageBox extends React.Component<FieldViewProps> { onPointerDown = (e: React.PointerEvent): void => { if (Date.now() - this._lastTap < 300) { - if (e.buttons === 1 && this.props.DocumentViewForField instanceof CollectionFreeFormDocumentView && SelectionManager.IsSelected(this.props.DocumentViewForField)) { + if (e.buttons === 1 && this.props.isSelected()) { e.stopPropagation(); this._downX = e.clientX; this._downY = e.clientY; @@ -62,12 +61,14 @@ export class ImageBox extends React.Component<FieldViewProps> { lightbox = (path: string) => { const images = [path, "http://www.cs.brown.edu/~bcz/face.gif"]; - if (this._isOpen && this.props.DocumentViewForField instanceof CollectionFreeFormDocumentView && SelectionManager.IsSelected(this.props.DocumentViewForField)) { + if (this._isOpen && this.props.isSelected()) { return (<Lightbox mainSrc={images[this._photoIndex]} nextSrc={images[(this._photoIndex + 1) % images.length]} prevSrc={images[(this._photoIndex + images.length - 1) % images.length]} - onCloseRequest={() => this.setState({ isOpen: false })} + onCloseRequest={action(() => + this._isOpen = false + )} onMovePrevRequest={action(() => this._photoIndex = (this._photoIndex + images.length - 1) % images.length )} @@ -82,10 +83,11 @@ export class ImageBox extends React.Component<FieldViewProps> { let field = this.props.doc.Get(this.props.fieldKey); let path = field == FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" : field instanceof ImageField ? field.Data.href : "http://www.cs.brown.edu/~bcz/face.gif"; + let nativeWidth = this.props.doc.GetNumber(KeyStore.NativeWidth, 1); return ( <div className="imageBox-cont" onPointerDown={this.onPointerDown} ref={this._ref} > - <img src={path} width="100%" alt="Image not found" /> + <img src={path} width={nativeWidth} alt="Image not found" /> {this.lightbox(path)} </div>) } diff --git a/src/client/views/nodes/WebView.tsx b/src/client/views/nodes/WebView.tsx new file mode 100644 index 000000000..717aa8bf5 --- /dev/null +++ b/src/client/views/nodes/WebView.tsx @@ -0,0 +1,22 @@ +import { FieldViewProps, FieldView } from "./FieldView"; +import { computed } from "mobx"; +import { observer } from "mobx-react"; +import { KeyStore } from "../../../fields/KeyStore"; +import React = require('react') +import { TextField } from "../../../fields/TextField"; +import { HtmlField } from "../../../fields/HtmlField"; +import { RichTextField } from "../../../fields/RichTextField"; + +@observer +export class WebView extends React.Component<FieldViewProps> { + public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(WebView, fieldStr) } + + @computed + get html(): string { + return this.props.doc.GetData(KeyStore.Data, HtmlField, "" as string); + } + + render() { + return <span dangerouslySetInnerHTML={{ __html: this.html }}></span> + } +}
\ No newline at end of file diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx index ddfe884ed..aff77fca3 100644 --- a/src/debug/Viewer.tsx +++ b/src/debug/Viewer.tsx @@ -7,7 +7,7 @@ import { Document } from '../fields/Document'; import { BasicField } from '../fields/BasicField'; import { ListField } from '../fields/ListField'; import { Key } from '../fields/Key'; -import { Field } from '../fields/Field'; +import { Opt, Field } from '../fields/Field'; import { Server } from '../client/Server'; configure({ @@ -116,7 +116,7 @@ class DebugViewer extends React.Component<{ fieldId: string }> { } update() { - Server.GetField(this.props.fieldId, (field => { + Server.GetField(this.props.fieldId, action((field: Opt<Field>) => { this.field = field; if (!field) { this.error = `Field with id ${this.props.fieldId} not found` diff --git a/src/fields/BasicField.ts b/src/fields/BasicField.ts index 15eb067a0..a92c4a236 100644 --- a/src/fields/BasicField.ts +++ b/src/fields/BasicField.ts @@ -1,9 +1,10 @@ -import { Field, FIELD_ID } from "./Field" +import { Field, FieldId } from "./Field" import { observable, computed, action } from "mobx"; import { Server } from "../client/Server"; +import { UndoManager } from "../client/util/UndoManager"; export abstract class BasicField<T> extends Field { - constructor(data: T, save: boolean, id: FIELD_ID = undefined) { + constructor(data: T, save: boolean, id?: FieldId) { super(id); this.data = data; @@ -27,12 +28,22 @@ export abstract class BasicField<T> extends Field { } set Data(value: T) { - if (this.data != value) { - this.data = value; + if (this.data === value) { + return; } + let oldValue = this.data; + this.setData(value); + UndoManager.AddEvent({ + undo: () => this.Data = oldValue, + redo: () => this.Data = value + }) Server.UpdateField(this); } + protected setData(value: T) { + this.data = value; + } + @action TrySetValue(value: any): boolean { if (typeof value == typeof this.data) { diff --git a/src/fields/Document.ts b/src/fields/Document.ts index 3067621be..0d7d357a0 100644 --- a/src/fields/Document.ts +++ b/src/fields/Document.ts @@ -1,16 +1,17 @@ -import { Field, Cast, Opt, FieldWaiting, FIELD_ID, FieldValue } from "./Field" import { Key } from "./Key" import { KeyStore } from "./KeyStore"; +import { Field, Cast, FieldWaiting, FieldValue, FieldId } from "./Field" import { NumberField } from "./NumberField"; -import { ObservableMap, computed, action, observable } from "mobx"; +import { ObservableMap, computed, action } from "mobx"; import { TextField } from "./TextField"; import { ListField } from "./ListField"; import { Server } from "../client/Server"; import { Types } from "../server/Message"; +import { UndoManager } from "../client/util/UndoManager"; export class Document extends Field { public fields: ObservableMap<string, { key: Key, field: Field }> = new ObservableMap(); - public _proxies: ObservableMap<string, FIELD_ID> = new ObservableMap(); + public _proxies: ObservableMap<string, FieldId> = new ObservableMap(); constructor(id?: string, save: boolean = true) { super(id) @@ -29,6 +30,10 @@ export class Document extends Field { } } + public Width = () => { 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, "<untitled>"); @@ -40,7 +45,17 @@ export class Document extends Field { if (this.fields.has(key.Id)) { field = this.fields.get(key.Id)!.field; } else if (this._proxies.has(key.Id)) { - field = Server.GetDocumentField(this, key); + Server.GetDocumentField(this, key); + /* + The field might have been instantly filled from the cache + Maybe we want to just switch back to returning the value + from Server.GetDocumentField if it's in the cache + */ + if (this.fields.has(key.Id)) { + field = this.fields.get(key.Id)!.field; + } else { + field = FieldWaiting; + } } } else { let doc: FieldValue<Document> = this; @@ -49,7 +64,17 @@ export class Document extends Field { let curProxy = doc._proxies.get(key.Id); if (!curField || (curProxy && curField.field.Id !== curProxy)) { if (curProxy) { - field = Server.GetDocumentField(doc, key); + Server.GetDocumentField(doc, key); + /* + The field might have been instantly filled from the cache + Maybe we want to just switch back to returning the value + from Server.GetDocumentField if it's in the cache + */ + if (this.fields.has(key.Id)) { + field = this.fields.get(key.Id)!.field; + } else { + field = FieldWaiting; + } break; } if ((doc.fields.has(KeyStore.Prototype.Id) || doc._proxies.has(KeyStore.Prototype.Id))) { @@ -69,6 +94,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<T extends Field = Field>(key: Key, ctor: { new(...args: any[]): T }, ignoreProto: boolean = false): FieldValue<T> { var getfield = this.Get(key, ignoreProto); if (getfield != FieldWaiting) { @@ -107,7 +141,8 @@ export class Document extends Field { @action Set(key: Key, field: Field | undefined): void { - console.log("Assign: " + key.Name + " = " + (field ? field.GetValue() : "<undefined>") + " (" + (field ? field.Id : "<undefined>") + ")"); + 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) @@ -117,6 +152,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); } @@ -124,16 +165,13 @@ export class Document extends Field { SetData<T, U extends Field & { Data: T }>(key: Key, value: T, ctor: { new(): U }, replaceWrongType = true) { let field = this.Get(key, true); - //if (field != WAITING) { // do we want to wait for the field to come back from the server to set it, or do we overwrite? if (field instanceof ctor) { field.Data = value; - // Server.SetFieldValue(field, value); } else if (!field || replaceWrongType) { let newField = new ctor(); newField.Data = value; this.Set(key, newField); } - //} } @action @@ -185,14 +223,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/DocumentReference.ts b/src/fields/DocumentReference.ts index 4096cbb92..9d3c209b4 100644 --- a/src/fields/DocumentReference.ts +++ b/src/fields/DocumentReference.ts @@ -1,4 +1,4 @@ -import { Field, Opt, FieldValue, FIELD_ID } from "./Field"; +import { Field, Opt, FieldValue, FieldId } from "./Field"; import { Document } from "./Document"; import { Key } from "./Key"; import { Types } from "../server/Message"; @@ -47,7 +47,7 @@ export class DocumentReference extends Field { return ""; } - ToJson(): { type: Types, data: FIELD_ID, _id: string } { + ToJson(): { type: Types, data: FieldId, _id: string } { return { type: Types.DocumentReference, data: this.document.Id, diff --git a/src/fields/Field.ts b/src/fields/Field.ts index 853fb9327..c7e0232af 100644 --- a/src/fields/Field.ts +++ b/src/fields/Field.ts @@ -11,9 +11,9 @@ export function Cast<T extends Field>(field: FieldValue<Field>, ctor: { new(): T return undefined; } -export let FieldWaiting: FIELD_WAITING = "<Waiting>"; +export const FieldWaiting: FIELD_WAITING = "<Waiting>"; export type FIELD_WAITING = "<Waiting>"; -export type FIELD_ID = string | undefined; +export type FieldId = string; export type Opt<T> = T | undefined; export type FieldValue<T> = Opt<T> | FIELD_WAITING; @@ -24,12 +24,12 @@ export abstract class Field { callback(this); } - private id: string; - get Id(): string { + private id: FieldId; + get Id(): FieldId { return this.id; } - constructor(id: FIELD_ID = undefined) { + constructor(id: Opt<FieldId> = undefined) { this.id = id || Utils.GenerateGuid(); } diff --git a/src/fields/HtmlField.ts b/src/fields/HtmlField.ts new file mode 100644 index 000000000..a07326095 --- /dev/null +++ b/src/fields/HtmlField.ts @@ -0,0 +1,25 @@ +import { BasicField } from "./BasicField"; +import { Types } from "../server/Message"; +import { FieldId } from "./Field"; + +export class HtmlField extends BasicField<string> { + constructor(data: string = "<html></html>", id?: FieldId, save: boolean = true) { + super(data, save, id); + } + + ToScriptString(): string { + return `new HtmlField("${this.Data}")`; + } + + Copy() { + return new HtmlField(this.Data); + } + + ToJson(): { _id: string; type: Types; data: any; } { + return { + type: Types.Html, + data: this.Data, + _id: this.Id, + } + } +}
\ No newline at end of file diff --git a/src/fields/ImageField.ts b/src/fields/ImageField.ts index aba011199..b2226d55a 100644 --- a/src/fields/ImageField.ts +++ b/src/fields/ImageField.ts @@ -1,10 +1,10 @@ import { BasicField } from "./BasicField"; -import { Field, FIELD_ID } from "./Field"; +import { Field, FieldId } from "./Field"; import { Types } from "../server/Message"; import { ObjectID } from "bson"; export class ImageField extends BasicField<URL> { - constructor(data: URL | undefined = undefined, id: FIELD_ID = undefined, save: boolean = true) { + constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) { super(data == undefined ? new URL("http://cs.brown.edu/~bcz/bob_fettucine.jpg") : data, save, id); } diff --git a/src/fields/Key.ts b/src/fields/Key.ts index d3958df2d..c16a00878 100644 --- a/src/fields/Key.ts +++ b/src/fields/Key.ts @@ -1,4 +1,4 @@ -import { Field, FIELD_ID } from "./Field" +import { Field, FieldId } from "./Field" import { Utils } from "../Utils"; import { observable } from "mobx"; import { Types } from "../server/Message"; diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts index 1e0b12729..a3b39735d 100644 --- a/src/fields/KeyStore.ts +++ b/src/fields/KeyStore.ts @@ -9,16 +9,21 @@ export namespace KeyStore { export const PanX = new Key("PanX"); export const PanY = new Key("PanY"); export const Scale = new Key("Scale"); + export const NativeWidth = new Key("NativeWidth"); + export const NativeHeight = new Key("NativeHeight"); export const Width = new Key("Width"); export const Height = new Key("Height"); export const ZIndex = new Key("ZIndex"); export const Data = new Key("Data"); + export const Annotations = new Key("Annotations"); + export const ViewType = new Key("ViewType"); export const Layout = new Key("Layout"); + export const BackgroundLayout = new Key("BackgroundLayout"); + export const OverlayLayout = new Key("OverlayLayout"); export const LayoutKeys = new Key("LayoutKeys"); export const LayoutFields = new Key("LayoutFields"); export const ColumnsKey = new Key("SchemaColumns"); - - export function Get(name: string): Key { - return new Key(name) - } + export const Caption = new Key("Caption"); + export const ActiveFrame = new Key("ActiveFrame"); + export const DocumentText = new Key("DocumentText"); } diff --git a/src/fields/ListField.ts b/src/fields/ListField.ts index 1357dc445..700600804 100644 --- a/src/fields/ListField.ts +++ b/src/fields/ListField.ts @@ -1,22 +1,46 @@ -import { Field, FIELD_ID, FieldValue, Opt } from "./Field"; -import { BasicField } from "./BasicField"; -import { Types } from "../server/Message"; -import { observe, action } from "mobx"; +import { action, IArrayChange, IArraySplice, IObservableArray, observe, observable, Lambda } 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<T extends Field> extends BasicField<T[]> { private _proxies: string[] = [] - constructor(data: T[] = [], id: FIELD_ID = undefined, save: boolean = true) { + constructor(data: T[] = [], id?: FieldId, save: boolean = true) { super(data, save, id); this.updateProxies(); if (save) { Server.UpdateField(this); } - observe(this.Data, () => { + this.observeList(); + } + + private observeDisposer: Lambda | undefined; + private observeList(): void { + this.observeDisposer = observe(this.Data as IObservableArray<T>, (change: IArrayChange<T> | IArraySplice<T>) => { this.updateProxies() + if (change.type == "splice") { + UndoManager.AddEvent({ + undo: () => this.Data.splice(change.index, change.addedCount, ...change.removed), + redo: () => this.Data.splice(change.index, change.removedCount, ...change.added) + }) + } else { + UndoManager.AddEvent({ + undo: () => this.Data[change.index] = change.oldValue, + redo: () => this.Data[change.index] = change.newValue + }) + } Server.UpdateField(this); - }) + }); + } + + protected setData(value: T[]) { + if (this.observeDisposer) { + this.observeDisposer() + } + this.data = observable(value); + this.observeList(); } private updateProxies() { diff --git a/src/fields/NumberField.ts b/src/fields/NumberField.ts index 29e285201..47dfc74cb 100644 --- a/src/fields/NumberField.ts +++ b/src/fields/NumberField.ts @@ -1,9 +1,9 @@ import { BasicField } from "./BasicField" import { Types } from "../server/Message"; -import { FIELD_ID } from "./Field"; +import { FieldId } from "./Field"; export class NumberField extends BasicField<number> { - constructor(data: number = 0, id: FIELD_ID = undefined, save: boolean = true) { + constructor(data: number = 0, id?: FieldId, save: boolean = true) { super(data, save, id); } diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts index 9783107e3..5efb43314 100644 --- a/src/fields/RichTextField.ts +++ b/src/fields/RichTextField.ts @@ -1,9 +1,9 @@ import { BasicField } from "./BasicField"; import { Types } from "../server/Message"; -import { FIELD_ID } from "./Field"; +import { FieldId } from "./Field"; export class RichTextField extends BasicField<string> { - constructor(data: string = "", id: FIELD_ID = undefined, save: boolean = true) { + constructor(data: string = "", id?: FieldId, save: boolean = true) { super(data, save, id); } diff --git a/src/fields/TextField.ts b/src/fields/TextField.ts index efb3c035f..71d8ea310 100644 --- a/src/fields/TextField.ts +++ b/src/fields/TextField.ts @@ -1,9 +1,9 @@ import { BasicField } from "./BasicField" -import { FIELD_ID } from "./Field"; +import { FieldId } from "./Field"; import { Types } from "../server/Message"; export class TextField extends BasicField<string> { - constructor(data: string = "", id: FIELD_ID = undefined, save: boolean = true) { + constructor(data: string = "", id?: FieldId, save: boolean = true) { super(data, save, id); } @@ -22,4 +22,4 @@ export class TextField extends BasicField<string> { _id: this.Id } } -} +}
\ No newline at end of file diff --git a/src/server/Message.ts b/src/server/Message.ts index e78c36ef1..80fc9a80d 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -1,5 +1,4 @@ import { Utils } from "../Utils"; -import { FIELD_ID, Field } from "../fields/Field"; export class Message<T> { private name: string; @@ -46,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 diff --git a/src/server/index.ts b/src/server/index.ts index e99e5f98a..0968cbd05 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -9,7 +9,7 @@ import { Client } from './Client'; import { Socket } from 'socket.io'; import { Utils } from '../Utils'; import { ObservableMap } from 'mobx'; -import { FIELD_ID, Field } from '../fields/Field'; +import { FieldId, Field } from '../fields/Field'; import { Database } from './database'; import { ServerUtils } from './ServerUtil'; import { ObjectID } from 'mongodb'; @@ -175,8 +175,7 @@ app.post('/forgot', function (req, res, next) { res.redirect('/forgot'); }) }) - -let FieldStore: ObservableMap<FIELD_ID, Field> = new ObservableMap(); +let FieldStore: ObservableMap<FieldId, Field> = new ObservableMap(); app.get("/hello", (req, res) => { res.send("<p>Hello</p>"); |