diff options
35 files changed, 1348 insertions, 0 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 72fa608ad..d8db6ee79 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -9,6 +9,8 @@ import { CollectionDockingView } from "../views/collections/CollectionDockingVie import { CollectionSchemaView } from "../views/collections/CollectionSchemaView"; import { ImageField } from "../../fields/ImageField"; import { ImageBox } from "../views/nodes/ImageBox"; +import { WebField } from "../../fields/WebField"; +import { WebBox } from "../views/nodes/WebBox"; import { CollectionFreeFormView } from "../views/collections/CollectionFreeFormView"; import { FieldId } from "../../fields/Field"; @@ -150,6 +152,37 @@ export namespace Documents { return sdoc; } + let webProtoId: FieldId; + function GetWebPrototype(): Document { + if (webProtoId === undefined) { + let webProto = new Document(); + webProtoId = webProto.Id; + webProto.Set(KeyStore.Title, new TextField("WEB PROTO")); + webProto.Set(KeyStore.X, new NumberField(0)); + webProto.Set(KeyStore.Y, new NumberField(0)); + webProto.Set(KeyStore.NativeWidth, new NumberField(300)); + webProto.Set(KeyStore.NativeHeight, new NumberField(300)); + webProto.Set(KeyStore.Width, new NumberField(300)); + webProto.Set(KeyStore.Height, new NumberField(300)); + webProto.Set(KeyStore.Layout, new TextField(CollectionFreeFormView.LayoutString("AnnotationsKey"))); + webProto.Set(KeyStore.BackgroundLayout, new TextField(WebBox.LayoutString())); + webProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data, KeyStore.Annotations])); + Server.AddDocument(webProto); + return webProto; + } + return Server.GetField(webProtoId) as Document; + } + + export function WebDocument(url: string, options: DocumentOptions = {}): Document { + let doc = GetWebPrototype().MakeDelegate(); + setupOptions(doc, options); + doc.Set(KeyStore.Data, new WebField(new URL(url))); + Server.AddDocument(doc); + var sdoc = Server.GetField(doc.Id) as Document; + console.log(sdoc); + return sdoc; + } + let collectionProto: Document; function GetCollectionPrototype(): Document { if (!collectionProto) { diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index ba92cc17e..3e5118379 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -78,6 +78,12 @@ document.addEventListener("pointerdown", action(function (e: PointerEvent) { // mainNodes.Data.push(doc1); // mainNodes.Data.push(doc2); mainNodes.Data.push(doc6); + + let doc9 = Documents.WebDocument("https://cs.brown.edu/", { + x: 450, y: 100, title: "cat 1", width: 606, height: 386, nativeWidth: 606, nativeHeight: 386 + }); + mainNodes.Data.push(doc9); + mainContainer.Set(KeyStore.Data, mainNodes); } //} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 12371eb2e..df6a409ec 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -7,9 +7,11 @@ import { TextField } from "../../../fields/TextField"; import { NumberField } from "../../../fields/NumberField"; import { RichTextField } from "../../../fields/RichTextField"; import { ImageField } from "../../../fields/ImageField"; +import { WebField } from "../../../fields/WebField"; import { Key } from "../../../fields/Key"; import { FormattedTextBox } from "./FormattedTextBox"; import { ImageBox } from "./ImageBox"; +import { WebBox } from "./WebBox"; import { DocumentView } from "./DocumentView"; // @@ -45,6 +47,9 @@ export class FieldView extends React.Component<FieldViewProps> { else if (field instanceof ImageField) { return <ImageBox {...this.props} /> } + else if (field instanceof WebField) { + return <WebBox {...this.props} /> + } else if (field instanceof NumberField) { return <p>{field.Data}</p> } else if (field != FieldWaiting) { diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss new file mode 100644 index 000000000..2bd8b1d3c --- /dev/null +++ b/src/client/views/nodes/WebBox.scss @@ -0,0 +1,13 @@ + +.imageBox-cont { + padding: 0vw; + position: absolute; + width: 100% +} + +.imageBox-button { + padding : 0vw; + border: none; + width : 100%; + height: 100%; +}
\ No newline at end of file diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx new file mode 100644 index 000000000..f50f8c60e --- /dev/null +++ b/src/client/views/nodes/WebBox.tsx @@ -0,0 +1,73 @@ + +import Lightbox from 'react-image-lightbox'; +import { SelectionManager } from "../../util/SelectionManager"; +import "./WebBox.scss"; +import React = require("react") +import { WebField } from '../../../fields/WebField'; +import { FieldViewProps, FieldView } from './FieldView'; +import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView'; +import { FieldWaiting } from '../../../fields/Field'; +import { observer } from "mobx-react" +import { observable, action, spy } from 'mobx'; +import { KeyStore } from '../../../fields/Key'; + +@observer +export class WebBox extends React.Component<FieldViewProps> { + + public static LayoutString() { return FieldView.LayoutString("WebBox"); } + private _ref: React.RefObject<HTMLDivElement>; + private _downX: number = 0; + private _downY: number = 0; + private _lastTap: number = 0; + @observable private _isOpen: boolean = false; + + constructor(props: FieldViewProps) { + super(props); + + this._ref = React.createRef(); + this.state = { + isOpen: false, + }; + } + + componentDidMount() { + } + + componentWillUnmount() { + } + + 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)) { + e.stopPropagation(); + this._downX = e.clientX; + this._downY = e.clientY; + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + } + } else { + this._lastTap = Date.now(); + } + } + @action + onPointerUp = (e: PointerEvent): void => { + document.removeEventListener("pointerup", this.onPointerUp); + if (Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2) { + this._isOpen = true; + } + e.stopPropagation(); + } + + render() { + let field = this.props.doc.Get(this.props.fieldKey); + let path = field == FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" : + field instanceof WebField ? field.Data.href : "https://cs.brown.edu/"; + let nativeWidth = this.props.doc.GetNumber(KeyStore.NativeWidth, 1); + + return ( + <div className="webBox-cont" onPointerDown={this.onPointerDown} ref={this._ref} > + <iframe src={path} width={nativeWidth}></iframe> + </div>) + } +}
\ No newline at end of file diff --git a/src/documents/Documents.ts b/src/documents/Documents.ts new file mode 100644 index 000000000..beee44bee --- /dev/null +++ b/src/documents/Documents.ts @@ -0,0 +1,92 @@ +import { Document } from "../fields/Document"; +import { KeyStore } from "../fields/Key"; +import { TextField } from "../fields/TextField"; +import { NumberField } from "../fields/NumberField"; +import { ListField } from "../fields/ListField"; + +interface DocumentOptions { + x?: number; + y?: number; + width?: number; + height?: number; +} + +export namespace Documents { + function setupOptions(doc: Document, options: DocumentOptions): void { + if(options.x) { + doc.SetFieldValue(KeyStore.X, options.x, NumberField); + } + if(options.y) { + doc.SetFieldValue(KeyStore.Y, options.y, NumberField); + } + if(options.width) { + doc.SetFieldValue(KeyStore.Width, options.width, NumberField); + } + if(options.height) { + doc.SetFieldValue(KeyStore.Height, options.height, NumberField); + } + } + + let textProto:Document; + function GetTextPrototype(): Document { + if(!textProto) { + textProto = new Document(); + textProto.SetField(KeyStore.X, new NumberField(0)); + textProto.SetField(KeyStore.Y, new NumberField(0)); + textProto.SetField(KeyStore.Width, new NumberField(300)); + textProto.SetField(KeyStore.Height, new NumberField(150)); + textProto.SetField(KeyStore.Layout, new TextField("<FieldTextBox doc={doc} fieldKey={DataKey} />")); + textProto.SetField(KeyStore.LayoutKeys, new ListField([KeyStore.Data])); + } + return textProto; + } + + export function TextDocument(text: string, options:DocumentOptions = {}): Document { + let doc = GetTextPrototype().MakeDelegate(); + setupOptions(doc, options); + // doc.SetField(KeyStore.Data, new TextField(text)); + return doc; + } + + let imageProto:Document; + function GetImagePrototype(): Document { + if(!imageProto) { + imageProto = new Document(); + imageProto.SetField(KeyStore.X, new NumberField(0)); + imageProto.SetField(KeyStore.Y, new NumberField(0)); + imageProto.SetField(KeyStore.Width, new NumberField(300)); + imageProto.SetField(KeyStore.Height, new NumberField(300)); + imageProto.SetField(KeyStore.Layout, new TextField('<img src={Data} alt="Image not found"/>')); + imageProto.SetField(KeyStore.LayoutFields, new ListField([KeyStore.Data])); + } + return imageProto; + } + + export function ImageDocument(url: string, options:DocumentOptions = {}): Document { + let doc = GetImagePrototype().MakeDelegate(); + setupOptions(doc, options); + doc.SetField(KeyStore.Data, new TextField(url)); + return doc; + } + + let collectionProto:Document; + function GetCollectionPrototype(): Document { + if(!collectionProto) { + collectionProto = new Document(); + collectionProto.SetField(KeyStore.X, new NumberField(150)); + collectionProto.SetField(KeyStore.Y, new NumberField(0)); + collectionProto.SetField(KeyStore.Width, new NumberField(300)); + collectionProto.SetField(KeyStore.Height, new NumberField(300)); + collectionProto.SetField(KeyStore.Layout, new TextField('<CollectionFreeFormView doc={doc} fieldKey={DataKey}/>')); + collectionProto.SetField(KeyStore.LayoutKeys, new ListField([KeyStore.Data])); + } + return collectionProto; + } + + export function CollectionDocument( documents: Array<Document>, options:DocumentOptions = {}): Document { + let doc = GetCollectionPrototype().MakeDelegate(); + setupOptions(doc, options); + doc.SetField(KeyStore.Data, new ListField(documents)); + return doc; + } +}
\ No newline at end of file diff --git a/src/fields/BasicField 2.ts b/src/fields/BasicField 2.ts new file mode 100644 index 000000000..437024d07 --- /dev/null +++ b/src/fields/BasicField 2.ts @@ -0,0 +1,38 @@ +import { Field } from "./Field" +import { observable, computed, action } from "mobx"; + +export abstract class BasicField<T> extends Field { + constructor(data: T) { + super(); + + this.data = data; + } + + @observable + private data:T; + + @computed + get Data(): T { + return this.data; + } + + set Data(value: T) { + if(this.data === value) { + return; + } + this.data = value; + } + + @action + TrySetValue(value: any): boolean { + if (typeof value == typeof this.data) { + this.Data = value; + return true; + } + return false; + } + + GetValue(): any { + return this.Data; + } +} diff --git a/src/fields/Document 2.ts b/src/fields/Document 2.ts new file mode 100644 index 000000000..0bba9c21e --- /dev/null +++ b/src/fields/Document 2.ts @@ -0,0 +1,93 @@ +import { Field, Cast, Opt } from "./Field" +import { Key, KeyStore } from "./Key" +import { ObservableMap } from "mobx"; + +export class Document extends Field { + private fields: ObservableMap<Key, Field> = new ObservableMap(); + + GetField(key: Key, ignoreProto: boolean = false): Opt<Field> { + let field: Opt<Field>; + if (ignoreProto) { + if (this.fields.has(key)) { + field = this.fields.get(key); + } + } else { + let doc: Opt<Document> = this; + while (doc && !(doc.fields.has(key))) { + doc = doc.GetPrototype(); + } + + if (doc) { + field = doc.fields.get(key); + } + } + + return field; + } + + GetFieldT<T extends Field = Field>(key: Key, ctor: { new(): T }, ignoreProto?: boolean): Opt<T> { + return Cast(this.GetField(key, ignoreProto), ctor); + } + + GetFieldValue<T, U extends { Data: T }>(key: Key, ctor: { new(): U }, defaultVal: T): T { + let val = this.GetField(key); + return (val && val instanceof ctor) ? val.Data : defaultVal; + } + + SetField(key: Key, field: Opt<Field>): void { + if (field) { + this.fields.set(key, field); + } else { + this.fields.delete(key); + } + } + + SetFieldValue<T extends Field>(key: Key, value: any, ctor: { new(): T }): boolean { + let field = this.GetField(key, true); + if (field != null) { + return field.TrySetValue(value); + } else { + field = new ctor(); + if (field.TrySetValue(value)) { + this.SetField(key, field); + return true; + } else { + return false; + } + } + } + + GetPrototype(): Opt<Document> { + return this.GetFieldT(KeyStore.Prototype, Document, true); + } + + GetAllPrototypes(): Document[] { + let protos: Document[] = []; + let doc: Opt<Document> = this; + while (doc != null) { + protos.push(doc); + doc = doc.GetPrototype(); + } + return protos; + } + + MakeDelegate(): Document { + let delegate = new Document(); + + delegate.SetField(KeyStore.Prototype, this); + + return delegate; + } + + TrySetValue(value: any): boolean { + throw new Error("Method not implemented."); + } + GetValue() { + throw new Error("Method not implemented."); + } + Copy(): Field { + throw new Error("Method not implemented."); + } + + +}
\ No newline at end of file diff --git a/src/fields/DocumentReference 2.ts b/src/fields/DocumentReference 2.ts new file mode 100644 index 000000000..936067bd2 --- /dev/null +++ b/src/fields/DocumentReference 2.ts @@ -0,0 +1,46 @@ +import { Field, Opt } from "./Field"; +import { Document } from "./Document"; +import { Key } from "./Key"; +import { DocumentUpdatedArgs } from "./FieldUpdatedArgs"; + +export class DocumentReference extends Field { + get Key(): Key{ + return this.key; + } + + get Document(): Document { + return this.document; + } + + constructor(private document: Document, private key: Key) { + super(); + } + + private DocFieldUpdated(args: DocumentUpdatedArgs):void{ + // this.FieldUpdated.emit(args.fieldArgs); + } + + Dereference() : Opt<Field> { + return this.document.GetField(this.key); + } + + DereferenceToRoot(): Opt<Field> { + let field: Opt<Field> = this; + while (field instanceof DocumentReference) { + field = field.Dereference(); + } + return field; + } + + TrySetValue(value: any): boolean { + throw new Error("Method not implemented."); + } + GetValue() { + throw new Error("Method not implemented."); + } + Copy(): Field { + throw new Error("Method not implemented."); + } + + +}
\ No newline at end of file diff --git a/src/fields/Field 2.ts b/src/fields/Field 2.ts new file mode 100644 index 000000000..46f92f203 --- /dev/null +++ b/src/fields/Field 2.ts @@ -0,0 +1,54 @@ +import { TypedEvent } from "../util/TypedEvent"; +import { FieldUpdatedArgs } from "./FieldUpdatedArgs"; +import { DocumentReference } from "./DocumentReference"; +import { Utils } from "../Utils"; + +export function Cast<T extends Field>(field: Opt<Field>, ctor: { new(): T }): Opt<T> { + if (field) { + if (ctor && field instanceof ctor) { + return field; + } + } + return undefined; +} + +export type Opt<T> = T | undefined; + +export abstract class Field { + //FieldUpdated: TypedEvent<Opt<FieldUpdatedArgs>> = new TypedEvent<Opt<FieldUpdatedArgs>>(); + + private id: string; + get Id(): string { + return this.id; + } + + constructor(id: Opt<string> = undefined) { + this.id = id || Utils.GenerateGuid(); + } + + Dereference(): Opt<Field> { + return this; + } + DereferenceToRoot(): Opt<Field> { + return this; + } + + DereferenceT<T extends Field = Field>(ctor: { new(): T }): Opt<T> { + return Cast(this.Dereference(), ctor); + } + + DereferenceToRootT<T extends Field = Field>(ctor: { new(): T }): Opt<T> { + return Cast(this.DereferenceToRoot(), ctor); + } + + Equals(other: Field): boolean { + return this.id === other.id; + } + + abstract TrySetValue(value: any): boolean; + + abstract GetValue(): any; + + abstract Copy(): Field; + +}
\ No newline at end of file diff --git a/src/fields/FieldUpdatedArgs 2.ts b/src/fields/FieldUpdatedArgs 2.ts new file mode 100644 index 000000000..23ccf2a5a --- /dev/null +++ b/src/fields/FieldUpdatedArgs 2.ts @@ -0,0 +1,27 @@ +import { Field, Opt } from "./Field"; +import { Document } from "./Document"; +import { Key } from "./Key"; + +export enum FieldUpdatedAction { + Add, + Remove, + Replace, + Update +} + +export interface FieldUpdatedArgs { + field: Field; + action: FieldUpdatedAction; +} + +export interface DocumentUpdatedArgs { + field: Document; + key: Key; + + oldValue: Opt<Field>; + newValue: Opt<Field>; + + fieldArgs?: FieldUpdatedArgs; + + action: FieldUpdatedAction; +} diff --git a/src/fields/Key 2.ts b/src/fields/Key 2.ts new file mode 100644 index 000000000..db30f545d --- /dev/null +++ b/src/fields/Key 2.ts @@ -0,0 +1,45 @@ +import { Field } from "./Field" +import { Utils } from "../Utils"; +import { observable } from "mobx"; + +export class Key extends Field { + private name:string; + + get Name():string { + return this.name; + } + + constructor(name:string){ + super(Utils.GenerateDeterministicGuid(name)); + + this.name = name; + } + + TrySetValue(value: any): boolean { + throw new Error("Method not implemented."); + } + + GetValue() { + return this.Name; + } + + Copy(): Field { + return this; + } + + +} + +export namespace KeyStore { + export let Prototype = new Key("Prototype"); + export let X = new Key("X"); + export let Y = new Key("Y"); + export let PanX = new Key("PanX"); + export let PanY = new Key("PanY"); + export let Width = new Key("Width"); + export let Height = new Key("Height"); + export let Data = new Key("Data"); + export let Layout = new Key("Layout"); + export let LayoutKeys = new Key("LayoutKeys"); + export let LayoutFields = new Key("LayoutFields"); +}
\ No newline at end of file diff --git a/src/fields/ListField 2.ts b/src/fields/ListField 2.ts new file mode 100644 index 000000000..dd96ea8c8 --- /dev/null +++ b/src/fields/ListField 2.ts @@ -0,0 +1,21 @@ +import { Field } from "./Field"; +import { BasicField } from "./BasicField"; + +export class ListField<T extends Field> extends BasicField<T[]> { + constructor(data: T[] = []) { + super(data.slice()); + } + + Get(index:number) : T{ + return this.Data[index]; + } + + Set(index:number, value:T):void { + this.Data[index] = value; + } + + Copy(): Field { + return new ListField<T>(this.Data); + } + +}
\ No newline at end of file diff --git a/src/fields/NumberField 2.ts b/src/fields/NumberField 2.ts new file mode 100644 index 000000000..cbe15f987 --- /dev/null +++ b/src/fields/NumberField 2.ts @@ -0,0 +1,13 @@ +import { BasicField } from "./BasicField" + +export class NumberField extends BasicField<number> { + constructor(data: number = 0) { + super(data); + } + + Copy() { + return new NumberField(this.Data); + } + + +}
\ No newline at end of file diff --git a/src/fields/TextField 2.ts b/src/fields/TextField 2.ts new file mode 100644 index 000000000..a7c221788 --- /dev/null +++ b/src/fields/TextField 2.ts @@ -0,0 +1,13 @@ +import { BasicField } from "./BasicField" + +export class TextField extends BasicField<string> { + constructor(data: string = "") { + super(data); + } + + Copy() { + return new TextField(this.Data); + } + + +} diff --git a/src/fields/WebField.ts b/src/fields/WebField.ts new file mode 100644 index 000000000..88e6130e0 --- /dev/null +++ b/src/fields/WebField.ts @@ -0,0 +1,21 @@ +import { BasicField } from "./BasicField"; +import { Field } from "./Field"; + +export class WebField extends BasicField<URL> { + constructor(data: URL | undefined = undefined) { + super(data == undefined ? new URL("https://cs.brown.edu/") : data); + } + + toString(): string { + return this.Data.href; + } + + ToScriptString(): string { + return `new WebField("${this.Data}")`; + } + + Copy(): Field { + return new WebField(this.Data); + } + +}
\ No newline at end of file diff --git a/src/stores/NodeCollectionStore.ts b/src/stores/NodeCollectionStore.ts new file mode 100644 index 000000000..ac4f515f1 --- /dev/null +++ b/src/stores/NodeCollectionStore.ts @@ -0,0 +1,25 @@ +import { computed, observable, action } from "mobx"; +import { NodeStore } from "./NodeStore"; +import { Document } from "../fields/Document"; + +export class NodeCollectionStore extends NodeStore { + + @observable + public Scale: number = 1; + + @observable + public Nodes: NodeStore[] = new Array<NodeStore>(); + + @observable + public Docs: Document[] = []; + + @computed + public get Transform(): string { + return "translate(" + this.X + "px," + this.Y + "px) scale(" + this.Scale + "," + this.Scale + ")"; + } + + @action + public AddNodes(stores: NodeStore[]): void { + stores.forEach(store => this.Nodes.push(store)); + } +}
\ No newline at end of file diff --git a/src/stores/NodeStore.ts b/src/stores/NodeStore.ts new file mode 100644 index 000000000..6a734cf44 --- /dev/null +++ b/src/stores/NodeStore.ts @@ -0,0 +1,24 @@ +import { computed, observable } from "mobx"; +import { Utils } from "../Utils"; + +export class NodeStore { + + public Id: string = Utils.GenerateGuid(); + + @observable + public X: number = 0; + + @observable + public Y: number = 0; + + @observable + public Width: number = 0; + + @observable + public Height: number = 0; + + @computed + public get Transform(): string { + return "translate(" + this.X + "px, " + this.Y + "px)"; + } +}
\ No newline at end of file diff --git a/src/stores/RootStore.ts b/src/stores/RootStore.ts new file mode 100644 index 000000000..fa551c1d1 --- /dev/null +++ b/src/stores/RootStore.ts @@ -0,0 +1,16 @@ +import { action, observable } from "mobx"; +import { NodeStore } from "./NodeStore"; + +// This globally accessible store might come in handy, although you may decide that you don't need it. +export class RootStore { + + private constructor() { + // initialization code + } + + private static _instance: RootStore; + + public static get Instance():RootStore { + return this._instance || (this._instance = new this()); + } +}
\ No newline at end of file diff --git a/src/stores/StaticTextNodeStore.ts b/src/stores/StaticTextNodeStore.ts new file mode 100644 index 000000000..7c342a7a2 --- /dev/null +++ b/src/stores/StaticTextNodeStore.ts @@ -0,0 +1,16 @@ +import { observable } from "mobx"; +import { NodeStore } from "./NodeStore"; + +export class StaticTextNodeStore extends NodeStore { + + constructor(initializer: Partial<StaticTextNodeStore>) { + super(); + Object.assign(this, initializer); + } + + @observable + public Title: string = ""; + + @observable + public Text: string = ""; +}
\ No newline at end of file diff --git a/src/stores/VideoNodeStore.ts b/src/stores/VideoNodeStore.ts new file mode 100644 index 000000000..e5187ab07 --- /dev/null +++ b/src/stores/VideoNodeStore.ts @@ -0,0 +1,17 @@ +import { observable } from "mobx"; +import { NodeStore } from "./NodeStore"; + +export class VideoNodeStore extends NodeStore { + + constructor(initializer: Partial<VideoNodeStore>) { + super(); + Object.assign(this, initializer); + } + + @observable + public Title: string = ""; + + @observable + public Url: string = ""; + +}
\ No newline at end of file diff --git a/src/util/TypedEvent.ts b/src/util/TypedEvent.ts new file mode 100644 index 000000000..0714a7f5c --- /dev/null +++ b/src/util/TypedEvent.ts @@ -0,0 +1,42 @@ +export interface Listener<T> { + (event: T): any; +} + +export interface Disposable { + dispose(): void; +} + +/** passes through events as they happen. You will not get events from before you start listening */ +export class TypedEvent<T> { + private listeners: Listener<T>[] = []; + private listenersOncer: Listener<T>[] = []; + + on = (listener: Listener<T>): Disposable => { + this.listeners.push(listener); + return { + dispose: () => this.off(listener) + }; + } + + once = (listener: Listener<T>): void => { + this.listenersOncer.push(listener); + } + + off = (listener: Listener<T>) => { + var callbackIndex = this.listeners.indexOf(listener); + if (callbackIndex > -1) this.listeners.splice(callbackIndex, 1); + } + + emit = (event: T) => { + /** Update any general listeners */ + this.listeners.forEach((listener) => listener(event)); + + /** Clear the `once` queue */ + this.listenersOncer.forEach((listener) => listener(event)); + this.listenersOncer = []; + } + + pipe = (te: TypedEvent<T>): Disposable => { + return this.on((e) => te.emit(e)); + } +}
\ No newline at end of file diff --git a/src/viewmodels/DocumentViewModel.ts b/src/viewmodels/DocumentViewModel.ts new file mode 100644 index 000000000..008275f3c --- /dev/null +++ b/src/viewmodels/DocumentViewModel.ts @@ -0,0 +1,11 @@ +import { Document } from "../fields/Document"; + +export class DocumentViewModel { + constructor(private doc: Document) { + + } + + get Doc(): Document { + return this.doc; + } +}
\ No newline at end of file diff --git a/src/views/freeformcanvas/CollectionFreeFormView.scss b/src/views/freeformcanvas/CollectionFreeFormView.scss new file mode 100644 index 000000000..cb4805eb3 --- /dev/null +++ b/src/views/freeformcanvas/CollectionFreeFormView.scss @@ -0,0 +1,15 @@ +.collectionfreeformview-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + + .collectionfreeformview { + position: absolute; + top: 0; + left: 0; + } +} + diff --git a/src/views/freeformcanvas/CollectionFreeFormView.tsx b/src/views/freeformcanvas/CollectionFreeFormView.tsx new file mode 100644 index 000000000..d5343536d --- /dev/null +++ b/src/views/freeformcanvas/CollectionFreeFormView.tsx @@ -0,0 +1,98 @@ +import { observer } from "mobx-react"; +import { Key, KeyStore } from "../../fields/Key"; +import "./FreeFormCanvas.scss"; +import React = require("react"); +import { action } from "mobx"; +import { Document } from "../../fields/Document"; +import { DocumentViewModel } from "../../viewmodels/DocumentViewModel"; +import { DocumentView } from "../nodes/DocumentView"; +import { ListField } from "../../fields/ListField"; +import { NumberField } from "../../fields/NumberField"; +import { SSL_OP_SINGLE_DH_USE } from "constants"; + +interface IProps { + fieldKey: Key; + doc: Document; +} + +@observer +export class CollectionFreeFormView extends React.Component<IProps> { + + private _isPointerDown: boolean = false; + + constructor(props: IProps) { + super(props); + } + + @action + onPointerDown = (e: React.PointerEvent): void => { + e.stopPropagation(); + if (e.button === 2) { + this._isPointerDown = true; + document.removeEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + } + } + + @action + onPointerUp = (e: PointerEvent): void => { + e.stopPropagation(); + if (e.button === 2) { + this._isPointerDown = false; + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + } + + @action + onPointerMove = (e: PointerEvent): void => { + e.preventDefault(); + e.stopPropagation(); + if (!this._isPointerDown) { + return; + } + const { doc } = this.props; + let x = doc.GetFieldValue(KeyStore.PanX, NumberField, Number(0)); + let y = doc.GetFieldValue(KeyStore.PanY, NumberField, Number(0)); + doc.SetFieldValue(KeyStore.PanX, x + e.movementX, NumberField); + doc.SetFieldValue(KeyStore.PanY, y + e.movementY, NumberField); + } + + @action + onPointerWheel = (e: React.WheelEvent): void => { + e.stopPropagation(); + + let scaleAmount = 1 - (e.deltaY / 1000); + //this.props.store.Scale *= scaleAmount; + } + + render() { + const { fieldKey, doc } = this.props; + const value: Document[] = doc.GetFieldValue(fieldKey, ListField, []); + const panx: number = doc.GetFieldValue(KeyStore.PanX, NumberField, Number(0)); + const pany: number = doc.GetFieldValue(KeyStore.PanY, NumberField, Number(0)); + return ( + + <div className="border" style={{ + borderStyle: "solid", + borderWidth: "2px" + }}> + <div className="collectionfreeformview-container" onPointerDown={this.onPointerDown} onWheel={this.onPointerWheel} onContextMenu={(e) => e.preventDefault()} style={{ + width: "100%", + height: "calc(100% - 4px)", + overflow: "hidden" + }}> + <div className="collectionfreeformview" style={{ transform: `translate(${panx}px, ${pany}px)`, transformOrigin: '50% 50%' }}> + <div className="node-container"> + {value.map(doc => { + return (<DocumentView key={doc.Id} dvm={new DocumentViewModel(doc)} />); + })} + </div> + </div> + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/views/freeformcanvas/FreeFormCanvas.scss b/src/views/freeformcanvas/FreeFormCanvas.scss new file mode 100644 index 000000000..884ef90e6 --- /dev/null +++ b/src/views/freeformcanvas/FreeFormCanvas.scss @@ -0,0 +1,15 @@ +.freeformcanvas-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + + .freeformcanvas { + position: absolute; + top: 0; + left: 0; + } +} + diff --git a/src/views/freeformcanvas/FreeFormCanvas.tsx b/src/views/freeformcanvas/FreeFormCanvas.tsx new file mode 100644 index 000000000..9ef5ab8f7 --- /dev/null +++ b/src/views/freeformcanvas/FreeFormCanvas.tsx @@ -0,0 +1,86 @@ +import { observer } from "mobx-react"; +import { Key } from "../../fields/Key"; +import { NodeCollectionStore } from "../../stores/NodeCollectionStore"; +import "./FreeFormCanvas.scss"; +import React = require("react"); +import { action } from "mobx"; +import { Document } from "../../fields/Document"; +import {DocumentViewModel} from "../../viewmodels/DocumentViewModel"; +import {DocumentView} from "../nodes/DocumentView"; +import {TextField} from "../../fields/TextField"; +import {ListField} from "../../fields/ListField"; +import {Field} from "../../fields/Field"; + +interface IProps { + store: NodeCollectionStore; +} + +@observer +export class FreeFormCanvas extends React.Component<IProps> { + + private _isPointerDown: boolean = false; + + constructor(props:IProps) { + super(props); + } + + @action + onPointerDown = (e: React.PointerEvent): void => { + e.stopPropagation(); + if (e.button === 2) { + this._isPointerDown = true; + document.removeEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + } + } + + @action + onPointerUp = (e: PointerEvent): void => { + e.stopPropagation(); + if (e.button === 2) { + this._isPointerDown = false; + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + + // let doc = this.props.store.Docs[0]; + // let dataField = doc.GetFieldT(KeyStore.Data, TextField); + // let data = dataField ? dataField.Data : ""; + // this.props.store.Docs[0].SetFieldValue(KeyStore.Data, data + " hello", TextField); + } + + @action + onPointerMove = (e: PointerEvent): void => { + e.stopPropagation(); + if (!this._isPointerDown) { + return; + } + this.props.store.X += e.movementX; + this.props.store.Y += e.movementY; + } + + @action + onPointerWheel = (e: React.WheelEvent): void => { + e.stopPropagation(); + + let scaleAmount = 1 - (e.deltaY / 1000); + this.props.store.Scale *= scaleAmount; + } + + render() { + let store = this.props.store; + return ( + <div className="freeformcanvas-container" onPointerDown={this.onPointerDown} onWheel={this.onPointerWheel} onContextMenu={(e) => e.preventDefault()}> + <div className="freeformcanvas" style={{ transform: store.Transform, transformOrigin: '50% 50%' }}> + <div className="node-container"> + {this.props.store.Docs.map(doc => { + return (<DocumentView key={doc.Id} dvm={new DocumentViewModel(doc)} />); + })} + </div> + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/views/nodes/DocumentView.tsx b/src/views/nodes/DocumentView.tsx new file mode 100644 index 000000000..f955a8c39 --- /dev/null +++ b/src/views/nodes/DocumentView.tsx @@ -0,0 +1,134 @@ +import { observer } from "mobx-react"; +import React = require("react"); +import { computed } from "mobx"; +import { KeyStore, Key } from "../../fields/Key"; +import { NumberField } from "../../fields/NumberField"; +import { TextField } from "../../fields/TextField"; +import { DocumentViewModel } from "../../viewmodels/DocumentViewModel"; +import { ListField } from "../../fields/ListField"; +import { FieldTextBox } from "../nodes/FieldTextBox" +import { FreeFormCanvas } from "../freeformcanvas/FreeFormCanvas" +import { CollectionFreeFormView } from "../freeformcanvas/CollectionFreeFormView" +import "./NodeView.scss" +const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this? + +interface IProps { + dvm: DocumentViewModel; +} + +@observer +export class DocumentView extends React.Component<IProps> { + @computed + get x(): number { + return this.props.dvm.Doc.GetFieldValue(KeyStore.X, NumberField, Number(0)); + } + + @computed + get y(): number { + return this.props.dvm.Doc.GetFieldValue(KeyStore.Y, NumberField, Number(0)); + } + + set x(x: number) { + this.props.dvm.Doc.SetFieldValue(KeyStore.X, x, NumberField) + } + + set y(y: number) { + this.props.dvm.Doc.SetFieldValue(KeyStore.Y, y, NumberField) + } + + @computed + get transform(): string { + return `translate(${this.x}px, ${this.y}px)`; + } + + @computed + get width(): number { + return this.props.dvm.Doc.GetFieldValue(KeyStore.Width, NumberField, Number(0)); + } + + @computed + get height(): number { + return this.props.dvm.Doc.GetFieldValue(KeyStore.Height, NumberField, Number(0)); + } + + @computed + get layout(): string { + return this.props.dvm.Doc.GetFieldValue(KeyStore.Layout, TextField, String("<p>Error loading layout data</p>")); + } + + @computed + get layoutKeys(): Key[] { + return this.props.dvm.Doc.GetFieldValue(KeyStore.LayoutKeys, ListField, new Array<Key>()); + } + + @computed + get layoutFields(): Key[] { + return this.props.dvm.Doc.GetFieldValue(KeyStore.LayoutFields, ListField, new Array<Key>()); + } + + private _isPointerDown = false; + + onPointerDown = (e: React.PointerEvent): void => { + e.stopPropagation(); + if (e.button === 2) { + this._isPointerDown = true; + document.removeEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + } + } + + onPointerUp = (e: PointerEvent): void => { + e.stopPropagation(); + if (e.button === 2) { + e.preventDefault(); + this._isPointerDown = false; + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + } + + onPointerMove = (e: PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + if (!this._isPointerDown) { + return; + } + this.x += e.movementX; + this.y += e.movementY; + } + + render() { + let doc = this.props.dvm.Doc; + let bindings: any = { + doc: doc + }; + for (const key of this.layoutKeys) { + bindings[key.Name + "Key"] = key; + } + for (const key of this.layoutFields) { + let field = doc.GetField(key); + if (field) { + bindings[key.Name] = field.GetValue(); + } + } + return ( + <div className="node" style={{ + transform: this.transform, + width: this.width, + height: this.height + }} onPointerDown={this.onPointerDown} onContextMenu={ + (e) => { + e.preventDefault() + }}> + <JsxParser + components={{ FieldTextBox, FreeFormCanvas, CollectionFreeFormView }} + bindings={bindings} + jsx={this.layout} + /> + </div> + ); + } + +}
\ No newline at end of file diff --git a/src/views/nodes/FieldTextBox.tsx b/src/views/nodes/FieldTextBox.tsx new file mode 100644 index 000000000..dbac3906a --- /dev/null +++ b/src/views/nodes/FieldTextBox.tsx @@ -0,0 +1,117 @@ +import { Key, KeyStore } from "../../fields/Key"; +import { Document } from "../../fields/Document"; +import { observer } from "mobx-react"; +import { TextField } from "../../fields/TextField"; +import React = require("react") +import { action, observable, reaction, IReactionDisposer } from "mobx"; + +import {schema} from "prosemirror-schema-basic"; +import {EditorState, Transaction} from "prosemirror-state" +import {EditorView} from "prosemirror-view" +import {keymap} from "prosemirror-keymap" +import {baseKeymap} from "prosemirror-commands" +import {undo, redo, history} from "prosemirror-history" +import { Opt } from "../../fields/Field"; + +interface IProps { + fieldKey:Key; + doc:Document; +} + +// FieldTextBox: Displays an editable plain text node that maps to a specified Key of a Document +// +// HTML Markup: <FieldTextBox Doc={Document's ID} FieldKey={Key's name + "Key"} +// +// In Code, the node's HTML is specified in the document's parameterized structure as: +// document.SetField(KeyStore.Layout, "<FieldTextBox doc={doc} fieldKey={<KEYNAME>Key} />"); +// and the node's binding to the specified document KEYNAME as: +// document.SetField(KeyStore.LayoutKeys, new ListField([KeyStore.<KEYNAME>])); +// The Jsx parser at run time will bind: +// 'fieldKey' property to the Key stored in LayoutKeys +// and 'doc' property to the document that is being rendered +// +// When rendered() by React, this extracts the TextController from the Document stored at the +// 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 FieldTextBox extends React.Component<IProps> { + private _ref: React.RefObject<HTMLDivElement>; + private _editorView: Opt<EditorView>; + private _reactionDisposer: Opt<IReactionDisposer>; + + constructor(props:IProps) { + super(props); + + this._ref = React.createRef(); + + this.onChange = this.onChange.bind(this); + } + + dispatchTransaction = (tx: Transaction) => { + if(this._editorView) { + const state = this._editorView.state.apply(tx); + this._editorView.updateState(state); + const {doc, fieldKey} = this.props; + doc.SetFieldValue(fieldKey, JSON.stringify(state.toJSON()), TextField); + } + } + + componentDidMount() { + let state:EditorState; + const {doc, fieldKey} = this.props; + const config = { + schema, + plugins: [ + history(), + keymap({"Mod-z": undo, "Mod-y": redo}), + keymap(baseKeymap) + ] + }; + + let field = doc.GetFieldT(fieldKey, TextField); + if(field) { + state = EditorState.fromJSON(config, JSON.parse(field.Data)); + } else { + state = EditorState.create(config); + } + if(this._ref.current) { + this._editorView = new EditorView(this._ref.current, { + state, + dispatchTransaction: this.dispatchTransaction + }); + } + + this._reactionDisposer = reaction(() => { + const field = this.props.doc.GetFieldT(this.props.fieldKey, TextField); + return field ? field.Data : undefined; + }, (field) => { + if(field && this._editorView) { + this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field))); + } + }) + } + + componentWillUnmount() { + if(this._editorView) { + this._editorView.destroy(); + } + if(this._reactionDisposer) { + this._reactionDisposer(); + } + } + + shouldComponentUpdate() { + return false; + } + + @action + onChange(e: React.ChangeEvent<HTMLInputElement>) { + const {fieldKey, doc} = this.props; + doc.SetFieldValue(fieldKey, e.target.value, TextField); + } + + render() { + return (<div ref={this._ref} />) + } +}
\ No newline at end of file diff --git a/src/views/nodes/NodeView.scss b/src/views/nodes/NodeView.scss new file mode 100644 index 000000000..a68335f87 --- /dev/null +++ b/src/views/nodes/NodeView.scss @@ -0,0 +1,31 @@ +.node { + position: absolute; + background: #cdcdcd; + + overflow: hidden; + + + &.minimized { + width: 30px; + height: 30px; + } + + .top { + background: #232323; + height: 20px; + cursor: pointer; + } + + .content { + padding: 20px 20px; + height: auto; + box-sizing: border-box; + + } + + .scroll-box { + overflow-y: scroll; + height: calc(100% - 20px); + } +} + diff --git a/src/views/nodes/RichTextView.tsx b/src/views/nodes/RichTextView.tsx new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/views/nodes/RichTextView.tsx diff --git a/src/views/nodes/TextNodeView.tsx b/src/views/nodes/TextNodeView.tsx new file mode 100644 index 000000000..4831e658c --- /dev/null +++ b/src/views/nodes/TextNodeView.tsx @@ -0,0 +1,28 @@ +import { observer } from "mobx-react"; +import { StaticTextNodeStore } from "../../stores/StaticTextNodeStore"; +import "./NodeView.scss"; +import { TopBar } from "./TopBar"; +import React = require("react"); + +interface IProps { + store: StaticTextNodeStore; +} + +@observer +export class TextNodeView extends React.Component<IProps> { + + render() { + let store = this.props.store; + return ( + <div className="node text-node" style={{ transform: store.Transform }}> + <TopBar store={store} /> + <div className="scroll-box"> + <div className="content"> + <h3 className="title">{store.Title}</h3> + <p className="paragraph">{store.Text}</p> + </div> + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/views/nodes/TopBar.tsx b/src/views/nodes/TopBar.tsx new file mode 100644 index 000000000..bb126e8b5 --- /dev/null +++ b/src/views/nodes/TopBar.tsx @@ -0,0 +1,46 @@ +import { observer } from "mobx-react"; +import { NodeStore } from "../../stores/NodeStore"; +import "./NodeView.scss"; +import React = require("react"); + +interface IProps { + store: NodeStore; +} + +@observer +export class TopBar extends React.Component<IProps> { + + private _isPointerDown = false; + + onPointerDown = (e: React.PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + this._isPointerDown = true; + document.removeEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + } + + onPointerUp = (e: PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + this._isPointerDown = false; + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + + onPointerMove = (e: PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + if (!this._isPointerDown) { + return; + } + this.props.store.X += e.movementX; + this.props.store.Y += e.movementY; + } + + render() { + return <div className="top" onPointerDown={this.onPointerDown}></div> + } +} diff --git a/src/views/nodes/VideoNodeView.scss b/src/views/nodes/VideoNodeView.scss new file mode 100644 index 000000000..f412c3519 --- /dev/null +++ b/src/views/nodes/VideoNodeView.scss @@ -0,0 +1,5 @@ +.node { + video { + width: 100%; + } +}
\ No newline at end of file diff --git a/src/views/nodes/VideoNodeView.tsx b/src/views/nodes/VideoNodeView.tsx new file mode 100644 index 000000000..0a7b3d174 --- /dev/null +++ b/src/views/nodes/VideoNodeView.tsx @@ -0,0 +1,29 @@ +import { observer } from "mobx-react"; +import { VideoNodeStore } from "../../stores/VideoNodeStore"; +import "./NodeView.scss"; +import { TopBar } from "./TopBar"; +import "./VideoNodeView.scss"; +import React = require("react"); + +interface IProps { + store: VideoNodeStore; +} + +@observer +export class VideoNodeView extends React.Component<IProps> { + + render() { + let store = this.props.store; + return ( + <div className="node text-node" style={{ transform: store.Transform }}> + <TopBar store={store} /> + <div className="scroll-box"> + <div className="content"> + <h3 className="title">{store.Title}</h3> + <video src={store.Url} controls /> + </div> + </div> + </div> + ); + } +}
\ No newline at end of file |