aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/documents/Documents.ts33
-rw-r--r--src/client/views/Main.tsx6
-rw-r--r--src/client/views/nodes/FieldView.tsx5
-rw-r--r--src/client/views/nodes/WebBox.scss13
-rw-r--r--src/client/views/nodes/WebBox.tsx73
-rw-r--r--src/documents/Documents.ts92
-rw-r--r--src/fields/BasicField 2.ts38
-rw-r--r--src/fields/Document 2.ts93
-rw-r--r--src/fields/DocumentReference 2.ts46
-rw-r--r--src/fields/Field 2.ts54
-rw-r--r--src/fields/FieldUpdatedArgs 2.ts27
-rw-r--r--src/fields/Key 2.ts45
-rw-r--r--src/fields/ListField 2.ts21
-rw-r--r--src/fields/NumberField 2.ts13
-rw-r--r--src/fields/TextField 2.ts13
-rw-r--r--src/fields/WebField.ts21
-rw-r--r--src/stores/NodeCollectionStore.ts25
-rw-r--r--src/stores/NodeStore.ts24
-rw-r--r--src/stores/RootStore.ts16
-rw-r--r--src/stores/StaticTextNodeStore.ts16
-rw-r--r--src/stores/VideoNodeStore.ts17
-rw-r--r--src/util/TypedEvent.ts42
-rw-r--r--src/viewmodels/DocumentViewModel.ts11
-rw-r--r--src/views/freeformcanvas/CollectionFreeFormView.scss15
-rw-r--r--src/views/freeformcanvas/CollectionFreeFormView.tsx98
-rw-r--r--src/views/freeformcanvas/FreeFormCanvas.scss15
-rw-r--r--src/views/freeformcanvas/FreeFormCanvas.tsx86
-rw-r--r--src/views/nodes/DocumentView.tsx134
-rw-r--r--src/views/nodes/FieldTextBox.tsx117
-rw-r--r--src/views/nodes/NodeView.scss31
-rw-r--r--src/views/nodes/RichTextView.tsx0
-rw-r--r--src/views/nodes/TextNodeView.tsx28
-rw-r--r--src/views/nodes/TopBar.tsx46
-rw-r--r--src/views/nodes/VideoNodeView.scss5
-rw-r--r--src/views/nodes/VideoNodeView.tsx29
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