aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package.json1
-rw-r--r--src/client/DocServer.ts72
-rw-r--r--src/client/util/SerializationHelper.ts125
-rw-r--r--src/client/views/DocComponent.tsx14
-rw-r--r--src/client/views/nodes/DocumentView.tsx82
-rw-r--r--src/client/views/nodes/ImageBox.tsx2
-rw-r--r--src/debug/Test.tsx98
-rw-r--r--src/fields/AudioField.ts31
-rw-r--r--src/fields/BasicField.ts59
-rw-r--r--src/fields/BooleanField.ts25
-rw-r--r--src/fields/DocumentReference.ts57
-rw-r--r--src/fields/Field.ts69
-rw-r--r--src/fields/FieldUpdatedArgs.ts27
-rw-r--r--src/fields/HtmlField.ts25
-rw-r--r--src/fields/ImageField.ts29
-rw-r--r--src/fields/Key.ts50
-rw-r--r--src/fields/KeyStore.ts67
-rw-r--r--src/fields/ListField.ts196
-rw-r--r--src/fields/NumberField.ts25
-rw-r--r--src/fields/PDFField.ts36
-rw-r--r--src/fields/TextField.ts25
-rw-r--r--src/fields/TupleField.ts59
-rw-r--r--src/fields/VideoField.ts30
-rw-r--r--src/fields/WebField.ts30
-rw-r--r--src/new_fields/Doc.ts98
-rw-r--r--src/new_fields/HtmlField.ts14
-rw-r--r--src/new_fields/List.ts35
-rw-r--r--src/new_fields/Proxy.ts55
-rw-r--r--src/new_fields/Schema.ts48
-rw-r--r--src/new_fields/Types.ts58
-rw-r--r--src/new_fields/URLField.ts25
-rw-r--r--src/new_fields/util.ts73
-rw-r--r--src/server/Message.ts12
-rw-r--r--src/server/database.ts16
-rw-r--r--src/server/index.ts19
35 files changed, 763 insertions, 924 deletions
diff --git a/package.json b/package.json
index 2463afa74..1eb546a80 100644
--- a/package.json
+++ b/package.json
@@ -163,6 +163,7 @@
"react-table": "^6.9.2",
"request": "^2.88.0",
"request-promise": "^4.2.4",
+ "serializr": "^1.5.1",
"socket.io": "^2.2.0",
"socket.io-client": "^2.2.0",
"typescript-collections": "^1.3.2",
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
new file mode 100644
index 000000000..615e48af0
--- /dev/null
+++ b/src/client/DocServer.ts
@@ -0,0 +1,72 @@
+import * as OpenSocket from 'socket.io-client';
+import { MessageStore, Types } from "./../server/Message";
+import { Opt, FieldWaiting, RefField, HandleUpdate } from '../new_fields/Doc';
+import { Utils } from '../Utils';
+import { SerializationHelper } from './util/SerializationHelper';
+
+export namespace DocServer {
+ const _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {};
+ const _socket = OpenSocket(`${window.location.protocol}//${window.location.hostname}:4321`);
+ const GUID: string = Utils.GenerateGuid();
+
+ export async function GetRefField(id: string): Promise<Opt<RefField>> {
+ let cached = _cache[id];
+ if (cached === undefined) {
+ const prom = Utils.EmitCallback(_socket, MessageStore.GetRefField, id).then(fieldJson => {
+ const field = fieldJson === undefined ? fieldJson : SerializationHelper.Deserialize(fieldJson);
+ if (field) {
+ _cache[id] = field;
+ } else {
+ delete _cache[id];
+ }
+ return field;
+ });
+ _cache[id] = prom;
+ return prom;
+ } else if (cached instanceof Promise) {
+ return cached;
+ } else {
+ return cached;
+ }
+ }
+
+ export function UpdateField(id: string, diff: any) {
+ Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
+ }
+
+ export function CreateField(initialState: any) {
+ if (!("id" in initialState)) {
+ throw new Error("Can't create a field on the server without an id");
+ }
+ Utils.Emit(_socket, MessageStore.CreateField, initialState);
+ }
+
+ function respondToUpdate(diff: any) {
+ const id = diff.id;
+ if (id === undefined) {
+ return;
+ }
+ const field = _cache[id];
+ const update = (f: Opt<RefField>) => {
+ if (f === undefined) {
+ return;
+ }
+ const handler = f[HandleUpdate];
+ if (handler) {
+ handler(diff);
+ }
+ };
+ if (field instanceof Promise) {
+ field.then(update);
+ } else {
+ update(field);
+ }
+ }
+
+ function connected(message: string) {
+ _socket.emit(MessageStore.Bar.Message, GUID);
+ }
+
+ Utils.AddServerHandler(_socket, MessageStore.Foo, connected);
+ Utils.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate);
+} \ No newline at end of file
diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts
new file mode 100644
index 000000000..ac70aba9d
--- /dev/null
+++ b/src/client/util/SerializationHelper.ts
@@ -0,0 +1,125 @@
+import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema, primitive, SKIP } from "serializr";
+import { Field } from "../../new_fields/Doc";
+
+export namespace SerializationHelper {
+ let serializing: number = 0;
+ export function IsSerializing() {
+ return serializing > 0;
+ }
+
+ export function Serialize(obj: Field): any {
+ if (!obj) {
+ return null;
+ }
+
+ if (typeof obj !== 'object') {
+ return obj;
+ }
+
+ serializing += 1;
+ if (!(obj.constructor.name in reverseMap)) {
+ throw Error(`type '${obj.constructor.name}' not registered. Make sure you register it using a @Deserializable decorator`);
+ }
+
+ const json = serialize(obj);
+ json.__type = reverseMap[obj.constructor.name];
+ serializing -= 1;
+ return json;
+ }
+
+ export function Deserialize(obj: any): any {
+ if (!obj) {
+ return null;
+ }
+
+ if (typeof obj !== 'object') {
+ return obj;
+ }
+
+ serializing += 1;
+ if (!obj.__type) {
+ throw Error("No property 'type' found in JSON.");
+ }
+
+ if (!(obj.__type in serializationTypes)) {
+ throw Error(`type '${obj.__type}' not registered. Make sure you register it using a @Deserializable decorator`);
+ }
+
+ const value = deserialize(serializationTypes[obj.__type], obj);
+ serializing -= 1;
+ return value;
+ }
+}
+
+let serializationTypes: { [name: string]: any } = {};
+let reverseMap: { [ctor: string]: string } = {};
+
+export interface DeserializableOpts {
+ (constructor: Function): void;
+ withFields(fields: string[]): Function;
+}
+
+export function Deserializable(name: string): DeserializableOpts;
+export function Deserializable(constructor: Function): void;
+export function Deserializable(constructor: Function | string): DeserializableOpts | void {
+ function addToMap(name: string, ctor: Function) {
+ if (!(name in serializationTypes)) {
+ serializationTypes[name] = ctor;
+ reverseMap[ctor.name] = name;
+ } else {
+ throw new Error(`Name ${name} has already been registered as deserializable`);
+ }
+ }
+ if (typeof constructor === "string") {
+ return Object.assign((ctor: Function) => {
+ addToMap(constructor, ctor);
+ }, { withFields: Deserializable.withFields });
+ }
+ addToMap(constructor.name, constructor);
+}
+
+export namespace Deserializable {
+ export function withFields(fields: string[]) {
+ return function (constructor: { new(...fields: any[]): any }) {
+ Deserializable(constructor);
+ let schema = getDefaultModelSchema(constructor);
+ if (schema) {
+ schema.factory = context => {
+ const args = fields.map(key => context.json[key]);
+ return new constructor(...args);
+ };
+ // TODO A modified version of this would let us not reassign fields that we're passing into the constructor later on in deserializing
+ // fields.forEach(field => {
+ // if (field in schema.props) {
+ // let propSchema = schema.props[field];
+ // if (propSchema === false) {
+ // return;
+ // } else if (propSchema === true) {
+ // propSchema = primitive();
+ // }
+ // schema.props[field] = custom(propSchema.serializer,
+ // () => {
+ // return SKIP;
+ // });
+ // }
+ // });
+ } else {
+ schema = {
+ props: {},
+ factory: context => {
+ const args = fields.map(key => context.json[key]);
+ return new constructor(...args);
+ }
+ };
+ setDefaultModelSchema(constructor, schema);
+ }
+ };
+ }
+}
+
+export function autoObject(): PropSchema {
+ return custom(
+ (s) => SerializationHelper.Serialize(s),
+ (s) => SerializationHelper.Deserialize(s)
+ );
+} \ No newline at end of file
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
new file mode 100644
index 000000000..31282744b
--- /dev/null
+++ b/src/client/views/DocComponent.tsx
@@ -0,0 +1,14 @@
+import * as React from 'react';
+import { Doc } from '../../new_fields/Doc';
+import { computed } from 'mobx';
+
+export function DocComponent<P extends { Document: Doc }, T>(schemaCtor: (doc: Doc) => T) {
+ class Component extends React.Component<P> {
+ //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
+ @computed
+ get Document() {
+ return schemaCtor(this.props.Document);
+ }
+ }
+ return Component;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index d571e7c3c..c06fe4c8d 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,10 +1,5 @@
import { action, computed, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { Document } from "../../../fields/Document";
-import { Field, Opt } from "../../../fields/Field";
-import { Key } from "../../../fields/Key";
-import { KeyStore } from "../../../fields/KeyStore";
-import { ListField } from "../../../fields/ListField";
import { ServerUtils } from "../../../server/ServerUtil";
import { emptyFunction, Utils } from "../../../Utils";
import { Documents } from "../../documents/Documents";
@@ -21,11 +16,15 @@ import { ContextMenu } from "../ContextMenu";
import { DocumentContentsView } from "./DocumentContentsView";
import "./DocumentView.scss";
import React = require("react");
-import { TextField } from "../../../fields/TextField";
+import { Field, Opt, Doc, Id } from "../../../new_fields/Doc";
+import { DocComponent } from "../DocComponent";
+import { createSchema, makeInterface } from "../../../new_fields/Schema";
+import { FieldValue } from "../../../new_fields/Types";
+
export interface DocumentViewProps {
ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
- Document: Document;
+ Document: Doc;
addDocument?: (doc: Document, allowDuplicates?: boolean) => boolean;
removeDocument?: (doc: Document) => boolean;
moveDocument?: (doc: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean;
@@ -39,48 +38,19 @@ export interface DocumentViewProps {
parentActive: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
}
-export interface JsxArgs extends DocumentViewProps {
- Keys: { [name: string]: Key };
- Fields: { [name: string]: Field };
-}
-/*
-This function is pretty much a hack that lets us fill out the fields in JsxArgs with something that
-jsx-to-string can recover the jsx from
-Example usage of this function:
- public static LayoutString() {
- let args = FakeJsxArgs(["Data"]);
- return jsxToString(
- <CollectionFreeFormView
- doc={args.Document}
- fieldKey={args.Keys.Data}
- DocumentViewForField={args.DocumentView} />,
- { useFunctionCode: true, functionNameOnly: true }
- )
- }
-*/
-export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs {
- let Keys: { [name: string]: any } = {};
- let Fields: { [name: string]: any } = {};
- for (const key of keys) {
- Object.defineProperty(emptyFunction, "name", { value: key + "Key" });
- Keys[key] = emptyFunction;
- }
- for (const field of fields) {
- Object.defineProperty(emptyFunction, "name", { value: field });
- Fields[field] = emptyFunction;
- }
- let args: JsxArgs = {
- Document: function Document() { },
- DocumentView: function DocumentView() { },
- Keys,
- Fields
- } as any;
- return args;
-}
+const schema = createSchema({
+ layout: "string",
+ nativeWidth: "number",
+ nativeHeight: "number",
+ backgroundColor: "string"
+});
+
+type Document = makeInterface<typeof schema>;
+const Document = makeInterface(schema);
@observer
-export class DocumentView extends React.Component<DocumentViewProps> {
+export class DocumentView extends DocComponent<DocumentViewProps, Document>(Document) {
private _downX: number = 0;
private _downY: number = 0;
private _mainCont = React.createRef<HTMLDivElement>();
@@ -89,9 +59,6 @@ export class DocumentView extends React.Component<DocumentViewProps> {
public get ContentDiv() { return this._mainCont.current; }
@computed get active(): boolean { return SelectionManager.IsSelected(this) || this.props.parentActive(); }
@computed get topMost(): boolean { return this.props.isTopMost; }
- @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>()); }
onPointerDown = (e: React.PointerEvent): void => {
this._downX = e.clientX;
@@ -207,7 +174,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
}
}
fullScreenClicked = (e: React.MouseEvent): void => {
- CollectionDockingView.Instance.OpenFullScreen((this.props.Document.GetPrototype() as Document).MakeDelegate());
+ CollectionDockingView.Instance.OpenFullScreen(Doc.MakeDelegate(this.Document.prototype));
ContextMenu.Instance.clearItems();
ContextMenu.Instance.addItem({ description: "Close Full Screen", event: this.closeFullScreenClicked });
ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
@@ -347,9 +314,9 @@ export class DocumentView extends React.Component<DocumentViewProps> {
onDrop = (e: React.DragEvent) => {
let text = e.dataTransfer.getData("text/plain");
if (!e.isDefaultPrevented() && text && text.startsWith("<div")) {
- let oldLayout = this.props.Document.GetText(KeyStore.Layout, "");
+ let oldLayout = FieldValue(this.Document.layout) || "";
let layout = text.replace("{layout}", oldLayout);
- this.props.Document.SetText(KeyStore.Layout, layout);
+ this.Document.layout = layout;
e.stopPropagation();
e.preventDefault();
}
@@ -370,19 +337,20 @@ export class DocumentView extends React.Component<DocumentViewProps> {
ContextMenu.Instance.addItem({ description: "Center", event: () => this.props.focus(this.props.Document) });
ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document) });
ContextMenu.Instance.addItem({ description: "Copy URL", event: () => Utils.CopyText(ServerUtils.prepend("/doc/" + this.props.Document.Id)) });
- ContextMenu.Instance.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document.Id) });
+ ContextMenu.Instance.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]) });
//ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) })
ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked });
ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
- if (!SelectionManager.IsSelected(this))
+ if (!SelectionManager.IsSelected(this)) {
SelectionManager.SelectDoc(this, false);
+ }
}
isSelected = () => SelectionManager.IsSelected(this);
select = (ctrlPressed: boolean) => SelectionManager.SelectDoc(this, ctrlPressed);
- @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); }
- @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); }
+ @computed get nativeWidth() { return FieldValue(this.Document.nativeWidth) || 0; }
+ @computed get nativeHeight() { return FieldValue(this.Document.nativeHeight) || 0; }
@computed get contents() { return (<DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} layoutKey={KeyStore.Layout} />); }
@@ -395,7 +363,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
<div className={`documentView-node${this.props.isTopMost ? "-topmost" : ""}`}
ref={this._mainCont}
style={{
- background: this.props.Document.GetText(KeyStore.BackgroundColor, ""),
+ background: FieldValue(this.Document.backgroundColor) || "",
width: nativeWidth, height: nativeHeight,
transform: `scale(${scaling}, ${scaling})`
}}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index ce855384c..fd5381b77 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -149,7 +149,7 @@ export class ImageBox extends React.Component<FieldViewProps> {
let left = (nativeWidth - paths.length * dist) / 2;
return paths.map((p, i) =>
<div className="imageBox-placer" key={i} >
- <div className="imageBox-dot" style={{ background: (i == this._photoIndex ? "black" : "gray"), transform: `translate(${i * dist + left}px, 0px)` }} onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); this.onDotDown(i); }} />
+ <div className="imageBox-dot" style={{ background: (i === this._photoIndex ? "black" : "gray"), transform: `translate(${i * dist + left}px, 0px)` }} onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); this.onDotDown(i); }} />
</div>
);
}
diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx
index 11f2b0c4e..8b9c9fa0b 100644
--- a/src/debug/Test.tsx
+++ b/src/debug/Test.tsx
@@ -1,29 +1,91 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
-import JsxParser from 'react-jsx-parser';
+import { SerializationHelper } from '../client/util/SerializationHelper';
+import { createSchema, makeInterface, makeStrictInterface } from '../new_fields/Schema';
+import { URLField } from '../new_fields/URLField';
+import { Doc } from '../new_fields/Doc';
+import { ListSpec } from '../new_fields/Types';
+import { List } from '../new_fields/List';
+const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
-class Hello extends React.Component<{ firstName: string, lastName: string }> {
- render() {
- return <div>Hello {this.props.firstName} {this.props.lastName}</div>;
- }
-}
+const schema1 = createSchema({
+ hello: "number",
+ test: "string",
+ fields: "boolean",
+ url: URLField,
+ testDoc: Doc
+});
+
+const TestDoc = makeInterface(schema1);
+type TestDoc = makeInterface<typeof schema1>;
+
+const schema2 = createSchema({
+ hello: URLField,
+ test: "boolean",
+ fields: { List: "number" } as ListSpec<number>,
+ url: "number",
+ testDoc: URLField
+});
+
+const Test2Doc = makeStrictInterface(schema2);
+type Test2Doc = makeStrictInterface<typeof schema2>;
+
+const schema3 = createSchema({
+ test: "boolean",
+});
+
+const Test3Doc = makeStrictInterface(schema3);
+type Test3Doc = makeStrictInterface<typeof schema3>;
+
+const assert = (bool: boolean) => {
+ if (!bool) throw new Error();
+};
class Test extends React.Component {
+ onClick = () => {
+ const url = new URLField(new URL("http://google.com"));
+ const doc = new Doc();
+ const doc2 = new Doc();
+ doc.hello = 5;
+ doc.fields = "test";
+ doc.test = "hello doc";
+ doc.url = url;
+ doc.testDoc = doc2;
+
+
+ const test1: TestDoc = TestDoc(doc);
+ const test2: Test2Doc = Test2Doc(doc);
+ assert(test1.hello === 5);
+ assert(test1.fields === undefined);
+ assert(test1.test === "hello doc");
+ assert(test1.url === url);
+ assert(test1.testDoc === doc2);
+ test1.myField = 20;
+ assert(test1.myField === 20);
+
+ assert(test2.hello === undefined);
+ // assert(test2.fields === "test");
+ assert(test2.test === undefined);
+ assert(test2.url === undefined);
+ assert(test2.testDoc === undefined);
+ test2.url = 35;
+ assert(test2.url === 35);
+ const l = new List<number>();
+ //TODO push, and other array functions don't go through the proxy
+ l.push(1);
+ //TODO currently length, and any other string fields will get serialized
+ l.length = 3;
+ l[2] = 5;
+ console.log(l.slice());
+ console.log(SerializationHelper.Serialize(l));
+ }
+
render() {
- let jsx = "<Hello {...props}/>";
- let bindings = {
- props: {
- firstName: "First",
- lastName: "Last"
- }
- };
- return <JsxParser jsx={jsx} bindings={bindings} components={{ Hello }}></JsxParser>;
+ return <button onClick={this.onClick}>Click me</button>;
}
}
-ReactDOM.render((
- <div style={{ position: "absolute", width: "100%", height: "100%" }}>
- <Test />
- </div>),
+ReactDOM.render(
+ <Test />,
document.getElementById('root')
); \ No newline at end of file
diff --git a/src/fields/AudioField.ts b/src/fields/AudioField.ts
deleted file mode 100644
index 87e47a715..000000000
--- a/src/fields/AudioField.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-import { Types } from "../server/Message";
-
-export class AudioField extends BasicField<URL> {
- constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) {
- super(data === undefined ? new URL("http://techslides.com/demos/samples/sample.mp3") : data, save, id);
- }
-
- toString(): string {
- return this.Data.href;
- }
-
-
- ToScriptString(): string {
- return `new AudioField("${this.Data}")`;
- }
-
- Copy(): Field {
- return new AudioField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Audio,
- data: this.Data.href,
- id: this.Id
- };
- }
-
-} \ No newline at end of file
diff --git a/src/fields/BasicField.ts b/src/fields/BasicField.ts
deleted file mode 100644
index 17b1fc4e8..000000000
--- a/src/fields/BasicField.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-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?: FieldId) {
- super(id);
-
- this.data = data;
- if (save) {
- Server.UpdateField(this);
- }
- }
-
- UpdateFromServer(data: any) {
- if (this.data !== data) {
- this.data = data;
- }
- }
-
- @observable
- protected data: T;
-
- @computed
- get Data(): T {
- return this.data;
- }
-
- set Data(value: T) {
- 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) {
- this.Data = value;
- return true;
- }
- return false;
- }
-
- GetValue(): any {
- return this.Data;
- }
-}
diff --git a/src/fields/BooleanField.ts b/src/fields/BooleanField.ts
deleted file mode 100644
index d49bfe82b..000000000
--- a/src/fields/BooleanField.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { BasicField } from "./BasicField";
-import { FieldId } from "./Field";
-import { Types } from "../server/Message";
-
-export class BooleanField extends BasicField<boolean> {
- constructor(data: boolean = false as boolean, id?: FieldId, save: boolean = true as boolean) {
- super(data, save, id);
- }
-
- ToScriptString(): string {
- return `new BooleanField("${this.Data}")`;
- }
-
- Copy() {
- return new BooleanField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Boolean,
- data: this.Data,
- id: this.Id
- };
- }
-}
diff --git a/src/fields/DocumentReference.ts b/src/fields/DocumentReference.ts
deleted file mode 100644
index 303754177..000000000
--- a/src/fields/DocumentReference.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { Field, Opt, FieldValue, FieldId } from "./Field";
-import { Document } from "./Document";
-import { Key } from "./Key";
-import { Types } from "../server/Message";
-import { ObjectID } from "bson";
-
-export class DocumentReference extends Field {
- get Key(): Key {
- return this.key;
- }
-
- get Document(): Document {
- return this.document;
- }
-
- constructor(private document: Document, private key: Key) {
- super();
- }
-
- UpdateFromServer() {
-
- }
-
- Dereference(): FieldValue<Field> {
- return this.document.Get(this.key);
- }
-
- DereferenceToRoot(): FieldValue<Field> {
- let field: FieldValue<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.");
- }
-
- ToScriptString(): string {
- return "";
- }
-
- ToJson() {
- return {
- type: Types.DocumentReference,
- data: this.document.Id,
- id: this.Id
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/Field.ts b/src/fields/Field.ts
deleted file mode 100644
index 3b3e95c2b..000000000
--- a/src/fields/Field.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-
-import { Utils } from "../Utils";
-import { Types, Transferable } from "../server/Message";
-import { computed } from "mobx";
-
-export function Cast<T extends Field>(field: FieldValue<Field>, ctor: { new(): T }): Opt<T> {
- if (field) {
- if (ctor && field instanceof ctor) {
- return field;
- }
- }
- return undefined;
-}
-
-export const FieldWaiting: FIELD_WAITING = null;
-export type FIELD_WAITING = null;
-export type FieldId = string;
-export type Opt<T> = T | undefined;
-export type FieldValue<T> = Opt<T> | FIELD_WAITING;
-
-export abstract class Field {
- //FieldUpdated: TypedEvent<Opt<FieldUpdatedArgs>> = new TypedEvent<Opt<FieldUpdatedArgs>>();
-
- init(callback: (res: Field) => any) {
- callback(this);
- }
-
- private id: FieldId;
-
- @computed
- get Id(): FieldId {
- return this.id;
- }
-
- constructor(id: Opt<FieldId> = undefined) {
- this.id = id || Utils.GenerateGuid();
- }
-
- Dereference(): FieldValue<Field> {
- return this;
- }
- DereferenceToRoot(): FieldValue<Field> {
- return this;
- }
-
- DereferenceT<T extends Field = Field>(ctor: { new(): T }): FieldValue<T> {
- return Cast(this.Dereference(), ctor);
- }
-
- DereferenceToRootT<T extends Field = Field>(ctor: { new(): T }): FieldValue<T> {
- return Cast(this.DereferenceToRoot(), ctor);
- }
-
- Equals(other: Field): boolean {
- return this.id === other.id;
- }
-
- abstract UpdateFromServer(serverData: any): void;
-
- abstract ToScriptString(): string;
-
- abstract TrySetValue(value: any): boolean;
-
- abstract GetValue(): any;
-
- abstract Copy(): Field;
-
- abstract ToJson(): Transferable;
-} \ No newline at end of file
diff --git a/src/fields/FieldUpdatedArgs.ts b/src/fields/FieldUpdatedArgs.ts
deleted file mode 100644
index 23ccf2a5a..000000000
--- a/src/fields/FieldUpdatedArgs.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-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/HtmlField.ts b/src/fields/HtmlField.ts
deleted file mode 100644
index a1d880070..000000000
--- a/src/fields/HtmlField.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-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() {
- 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
deleted file mode 100644
index bce20f242..000000000
--- a/src/fields/ImageField.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-import { Types } from "../server/Message";
-
-export class ImageField extends BasicField<URL> {
- constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) {
- super(data === undefined ? new URL("http://cs.brown.edu/~bcz/bob_fettucine.jpg") : data, save, id);
- }
-
- toString(): string {
- return this.Data.href;
- }
-
- ToScriptString(): string {
- return `new ImageField("${this.Data}")`;
- }
-
- Copy(): Field {
- return new ImageField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Image,
- data: this.Data.href,
- id: this.Id
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/Key.ts b/src/fields/Key.ts
deleted file mode 100644
index 57e2dadf0..000000000
--- a/src/fields/Key.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { Field, FieldId } from "./Field";
-import { Utils } from "../Utils";
-import { observable } from "mobx";
-import { Types } from "../server/Message";
-import { Server } from "../client/Server";
-
-export class Key extends Field {
- private name: string;
-
- get Name(): string {
- return this.name;
- }
-
- constructor(name: string, id?: string, save: boolean = true) {
- super(id || Utils.GenerateDeterministicGuid(name));
-
- this.name = name;
- if (save) {
- Server.UpdateField(this);
- }
- }
-
- UpdateFromServer(data: string) {
- this.name = data;
- }
-
- TrySetValue(value: any): boolean {
- throw new Error("Method not implemented.");
- }
-
- GetValue() {
- return this.Name;
- }
-
- Copy(): Field {
- return this;
- }
-
- ToScriptString(): string {
- return name;
- }
-
- ToJson() {
- return {
- type: Types.Key,
- data: this.name,
- id: this.Id
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts
deleted file mode 100644
index a347f8bcf..000000000
--- a/src/fields/KeyStore.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { Key } from "./Key";
-
-export namespace KeyStore {
- export const Prototype = new Key("Prototype");
- export const X = new Key("X");
- export const Y = new Key("Y");
- export const Page = new Key("Page");
- export const Title = new Key("Title");
- export const Author = new Key("Author");
- export const PanX = new Key("PanX");
- export const PanY = new Key("PanY");
- export const Scale = new Key("Scale");
- export const NativeWidth = new Key("NativeWidth");
- export const NativeHeight = new Key("NativeHeight");
- export const Width = new Key("Width");
- export const Height = new Key("Height");
- export const ZIndex = new Key("ZIndex");
- export const Zoom = new Key("Zoom");
- 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 BackgroundColor = new Key("BackgroundColor");
- export const BackgroundLayout = new Key("BackgroundLayout");
- export const OverlayLayout = new Key("OverlayLayout");
- export const LayoutKeys = new Key("LayoutKeys");
- export const LayoutFields = new Key("LayoutFields");
- export const ColumnsKey = new Key("SchemaColumns");
- export const SchemaSplitPercentage = new Key("SchemaSplitPercentage");
- export const Caption = new Key("Caption");
- export const ActiveWorkspace = new Key("ActiveWorkspace");
- export const DocumentText = new Key("DocumentText");
- export const BrushingDocs = new Key("BrushingDocs");
- export const LinkedToDocs = new Key("LinkedToDocs");
- export const LinkedFromDocs = new Key("LinkedFromDocs");
- export const LinkDescription = new Key("LinkDescription");
- export const LinkTags = new Key("LinkTag");
- export const Thumbnail = new Key("Thumbnail");
- export const ThumbnailPage = new Key("ThumbnailPage");
- export const CurPage = new Key("CurPage");
- export const AnnotationOn = new Key("AnnotationOn");
- export const NumPages = new Key("NumPages");
- export const Ink = new Key("Ink");
- export const Cursors = new Key("Cursors");
- export const OptionalRightCollection = new Key("OptionalRightCollection");
- export const Archives = new Key("Archives");
- export const Workspaces = new Key("Workspaces");
- export const IsMinimized = new Key("IsMinimized");
- export const MinimizedDoc = new Key("MinimizedDoc");
- export const MaximizedDoc = new Key("MaximizedDoc");
- export const CopyDraggedItems = new Key("CopyDraggedItems");
-
- export const KeyList: Key[] = [Prototype, X, Y, Page, Title, Author, PanX, PanY, Scale, NativeWidth, NativeHeight,
- Width, Height, ZIndex, Zoom, Data, Annotations, ViewType, Layout, BackgroundColor, BackgroundLayout, OverlayLayout, LayoutKeys,
- LayoutFields, ColumnsKey, SchemaSplitPercentage, Caption, ActiveWorkspace, DocumentText, BrushingDocs, LinkedToDocs, LinkedFromDocs,
- LinkDescription, LinkTags, Thumbnail, ThumbnailPage, CurPage, AnnotationOn, NumPages, Ink, Cursors, OptionalRightCollection,
- Archives, Workspaces, IsMinimized, MinimizedDoc, MaximizedDoc, CopyDraggedItems
- ];
- export function KeyLookup(keyid: string) {
- for (const key of KeyList) {
- if (key.Id === keyid) {
- return key;
- }
- }
- return undefined;
- }
-}
diff --git a/src/fields/ListField.ts b/src/fields/ListField.ts
deleted file mode 100644
index e24099126..000000000
--- a/src/fields/ListField.ts
+++ /dev/null
@@ -1,196 +0,0 @@
-import { action, IArrayChange, IArraySplice, IObservableArray, observe, observable, Lambda } from "mobx";
-import { Server } from "../client/Server";
-import { UndoManager } from "../client/util/UndoManager";
-import { Types } from "../server/Message";
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-import { FieldMap } from "../client/SocketStub";
-import { ScriptField } from "./ScriptField";
-
-export class ListField<T extends Field> extends BasicField<T[]> {
- private _proxies: string[] = [];
- private _scriptIds: string[] = [];
- private scripts: ScriptField[] = [];
-
- constructor(data: T[] = [], scripts: ScriptField[] = [], id?: FieldId, save: boolean = true) {
- super(data, save, id);
- this.scripts = scripts;
- this.updateProxies();
- this._scriptIds = this.scripts.map(script => script.Id);
- if (save) {
- Server.UpdateField(this);
- }
- this.observeList();
- }
-
- private _processingServerUpdate: boolean = false;
-
- private observeDisposer: Lambda | undefined;
- private observeList(): void {
- if (this.observeDisposer) {
- this.observeDisposer();
- }
- this.observeDisposer = observe(this.Data as IObservableArray<T>, (change: IArrayChange<T> | IArraySplice<T>) => {
- const target = change.object;
- this.updateProxies();
- if (change.type === "splice") {
- this.runScripts(change.removed, false);
- UndoManager.AddEvent({
- undo: () => target.splice(change.index, change.addedCount, ...change.removed),
- redo: () => target.splice(change.index, change.removedCount, ...change.added)
- });
- this.runScripts(change.added, true);
- } else {
- this.runScripts([change.oldValue], false);
- UndoManager.AddEvent({
- undo: () => target[change.index] = change.oldValue,
- redo: () => target[change.index] = change.newValue
- });
- this.runScripts([change.newValue], true);
- }
- if (!this._processingServerUpdate) {
- Server.UpdateField(this);
- }
- });
- }
-
- private runScripts(fields: T[], added: boolean) {
- for (const script of this.scripts) {
- this.runScript(fields, script, added);
- }
- }
-
- private runScript(fields: T[], script: ScriptField, added: boolean) {
- if (!this._processingServerUpdate) {
- for (const field of fields) {
- script.script.run({ field, added });
- }
- }
- }
-
- addScript(script: ScriptField) {
- this.scripts.push(script);
- this._scriptIds.push(script.Id);
-
- this.runScript(this.Data, script, true);
- UndoManager.AddEvent({
- undo: () => this.removeScript(script),
- redo: () => this.addScript(script),
- });
- Server.UpdateField(this);
- }
-
- removeScript(script: ScriptField) {
- const index = this.scripts.indexOf(script);
- if (index === -1) {
- return;
- }
- this.scripts.splice(index, 1);
- this._scriptIds.splice(index, 1);
- UndoManager.AddEvent({
- undo: () => this.addScript(script),
- redo: () => this.removeScript(script),
- });
- this.runScript(this.Data, script, false);
- Server.UpdateField(this);
- }
-
- protected setData(value: T[]) {
- this.runScripts(this.data, false);
-
- this.data = observable(value);
- this.updateProxies();
- this.observeList();
- this.runScripts(this.data, true);
- }
-
- private updateProxies() {
- this._proxies = this.Data.map(field => field.Id);
- }
-
- private arraysEqual(a: any[], b: any[]) {
- if (a === b) return true;
- if (a === null || b === null) return false;
- if (a.length !== b.length) return false;
-
- // If you don't care about the order of the elements inside
- // the array, you should sort both arrays here.
- // Please note that calling sort on an array will modify that array.
- // you might want to clone your array first.
-
- for (var i = 0; i < a.length; ++i) {
- if (a[i] !== b[i]) return false;
- }
- return true;
- }
-
- init(callback: (field: Field) => any) {
- const fieldsPromise = Server.GetFields(this._proxies).then(action((fields: FieldMap) => {
- if (!this.arraysEqual(this._proxies, this.data.map(field => field.Id))) {
- var dataids = this.data.map(d => d.Id);
- var proxies = this._proxies.map(p => p);
- var added = this.data.length < this._proxies.length;
- var deleted = this.data.length > this._proxies.length;
- for (let i = 0; i < dataids.length && added; i++) {
- added = proxies.indexOf(dataids[i]) !== -1;
- }
- for (let i = 0; i < this._proxies.length && deleted; i++) {
- deleted = dataids.indexOf(proxies[i]) !== -1;
- }
-
- this._processingServerUpdate = true;
- for (let i = 0; i < proxies.length && added; i++) {
- if (dataids.indexOf(proxies[i]) === -1) {
- this.Data.splice(i, 0, fields[proxies[i]] as T);
- }
- }
- for (let i = dataids.length - 1; i >= 0 && deleted; i--) {
- if (proxies.indexOf(dataids[i]) === -1) {
- this.Data.splice(i, 1);
- }
- }
- if (!added && !deleted) {// otherwise, just rebuild the whole list
- this.setData(proxies.map(id => fields[id] as T));
- }
- this._processingServerUpdate = false;
- }
- }));
-
- const scriptsPromise = Server.GetFields(this._scriptIds).then((fields: FieldMap) => {
- this.scripts = this._scriptIds.map(id => fields[id] as ScriptField);
- });
-
- Promise.all([fieldsPromise, scriptsPromise]).then(() => callback(this));
- }
-
- ToScriptString(): string {
- return "new ListField([" + this.Data.map(field => field.ToScriptString()).join(", ") + "])";
- }
-
- Copy(): Field {
- return new ListField<T>(this.Data);
- }
-
-
- UpdateFromServer(data: { fields: string[], scripts: string[] }) {
- this._proxies = data.fields;
- this._scriptIds = data.scripts;
- }
- ToJson() {
- return {
- type: Types.List,
- data: {
- fields: this._proxies,
- scripts: this._scriptIds,
- },
- id: this.Id
- };
- }
-
- static FromJson(id: string, data: { fields: string[], scripts: string[] }): ListField<Field> {
- let list = new ListField([], [], id, false);
- list._proxies = data.fields;
- list._scriptIds = data.scripts;
- return list;
- }
-} \ No newline at end of file
diff --git a/src/fields/NumberField.ts b/src/fields/NumberField.ts
deleted file mode 100644
index 7eea360c0..000000000
--- a/src/fields/NumberField.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Types } from "../server/Message";
-import { FieldId } from "./Field";
-
-export class NumberField extends BasicField<number> {
- constructor(data: number = 0, id?: FieldId, save: boolean = true) {
- super(data, save, id);
- }
-
- ToScriptString(): string {
- return `new NumberField(${this.Data})`;
- }
-
- Copy() {
- return new NumberField(this.Data);
- }
-
- ToJson() {
- return {
- id: this.Id,
- type: Types.Number,
- data: this.Data
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/PDFField.ts b/src/fields/PDFField.ts
deleted file mode 100644
index 718a1a4c0..000000000
--- a/src/fields/PDFField.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-import { observable } from "mobx";
-import { Types } from "../server/Message";
-
-
-
-export class PDFField extends BasicField<URL> {
- constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) {
- super(data === undefined ? new URL("http://cs.brown.edu/~bcz/bob_fettucine.jpg") : data, save, id);
- }
-
- toString(): string {
- return this.Data.href;
- }
-
- Copy(): Field {
- return new PDFField(this.Data);
- }
-
- ToScriptString(): string {
- return `new PDFField("${this.Data}")`;
- }
-
- ToJson() {
- return {
- type: Types.PDF,
- data: this.Data.href,
- id: this.Id
- };
- }
-
- @observable
- Page: Number = 1;
-
-} \ No newline at end of file
diff --git a/src/fields/TextField.ts b/src/fields/TextField.ts
deleted file mode 100644
index ddedec9b1..000000000
--- a/src/fields/TextField.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { BasicField } from "./BasicField";
-import { FieldId } from "./Field";
-import { Types } from "../server/Message";
-
-export class TextField extends BasicField<string> {
- constructor(data: string = "", id?: FieldId, save: boolean = true) {
- super(data, save, id);
- }
-
- ToScriptString(): string {
- return `new TextField("${this.Data}")`;
- }
-
- Copy() {
- return new TextField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Text,
- data: this.Data,
- id: this.Id
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/TupleField.ts b/src/fields/TupleField.ts
deleted file mode 100644
index 347f1fa05..000000000
--- a/src/fields/TupleField.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { action, IArrayChange, IArraySplice, IObservableArray, observe, observable, Lambda } from "mobx";
-import { Server } from "../client/Server";
-import { UndoManager } from "../client/util/UndoManager";
-import { Types } from "../server/Message";
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-
-export class TupleField<T, U> extends BasicField<[T, U]> {
- constructor(data: [T, U], id?: FieldId, save: boolean = true) {
- super(data, save, id);
- if (save) {
- Server.UpdateField(this);
- }
- this.observeTuple();
- }
-
- private observeDisposer: Lambda | undefined;
- private observeTuple(): void {
- this.observeDisposer = observe(this.Data as (T | U)[] as IObservableArray<T | U>, (change: IArrayChange<T | U> | IArraySplice<T | U>) => {
- if (change.type === "update") {
- UndoManager.AddEvent({
- undo: () => this.Data[change.index] = change.oldValue,
- redo: () => this.Data[change.index] = change.newValue
- });
- Server.UpdateField(this);
- } else {
- throw new Error("Why are you messing with the length of a tuple, huh?");
- }
- });
- }
-
- protected setData(value: [T, U]) {
- if (this.observeDisposer) {
- this.observeDisposer();
- }
- this.data = observable(value) as (T | U)[] as [T, U];
- this.observeTuple();
- }
-
- UpdateFromServer(values: [T, U]) {
- this.setData(values);
- }
-
- ToScriptString(): string {
- return `new TupleField([${this.Data[0], this.Data[1]}])`;
- }
-
- Copy(): Field {
- return new TupleField<T, U>(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Tuple,
- data: this.Data,
- id: this.Id
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/VideoField.ts b/src/fields/VideoField.ts
deleted file mode 100644
index 838b811b1..000000000
--- a/src/fields/VideoField.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-import { Types } from "../server/Message";
-
-export class VideoField extends BasicField<URL> {
- constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) {
- super(data === undefined ? new URL("http://techslides.com/demos/sample-videos/small.mp4") : data, save, id);
- }
-
- toString(): string {
- return this.Data.href;
- }
-
- ToScriptString(): string {
- return `new VideoField("${this.Data}")`;
- }
-
- Copy(): Field {
- return new VideoField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Video,
- data: this.Data.href,
- id: this.Id
- };
- }
-
-} \ No newline at end of file
diff --git a/src/fields/WebField.ts b/src/fields/WebField.ts
deleted file mode 100644
index 8b276a552..000000000
--- a/src/fields/WebField.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-import { Types } from "../server/Message";
-
-export class WebField extends BasicField<URL> {
- constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) {
- super(data === undefined ? new URL("https://crossorigin.me/" + "https://cs.brown.edu/") : data, save, id);
- }
-
- toString(): string {
- return this.Data.href;
- }
-
- ToScriptString(): string {
- return `new WebField("${this.Data}")`;
- }
-
- Copy(): Field {
- return new WebField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Web,
- data: this.Data.href,
- id: this.Id
- };
- }
-
-} \ No newline at end of file
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
new file mode 100644
index 000000000..23a8c05cc
--- /dev/null
+++ b/src/new_fields/Doc.ts
@@ -0,0 +1,98 @@
+import { observable, action } from "mobx";
+import { serializable, primitive, map, alias, list } from "serializr";
+import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper";
+import { Utils } from "../Utils";
+import { DocServer } from "../client/DocServer";
+import { setter, getter, getField } from "./util";
+import { Cast, FieldCtor } from "./Types";
+
+export const HandleUpdate = Symbol("HandleUpdate");
+export const Id = Symbol("Id");
+export abstract class RefField {
+ @serializable(alias("id", primitive()))
+ private __id: string;
+ readonly [Id]: string;
+
+ constructor(id?: string) {
+ this.__id = id || Utils.GenerateGuid();
+ this[Id] = this.__id;
+ }
+
+ protected [HandleUpdate]?(diff: any): void;
+}
+
+export const Update = Symbol("Update");
+export const OnUpdate = Symbol("OnUpdate");
+export const Parent = Symbol("Parent");
+export class ObjectField {
+ protected [OnUpdate]?: (diff?: any) => void;
+ private [Parent]?: Doc;
+}
+
+export type Field = number | string | boolean | ObjectField | RefField;
+export type Opt<T> = T | undefined;
+export type FieldWaiting = null;
+export const FieldWaiting: FieldWaiting = null;
+
+export const Self = Symbol("Self");
+
+@Deserializable("doc").withFields(["id"])
+export class Doc extends RefField {
+ constructor(id?: string, forceSave?: boolean) {
+ super(id);
+ const doc = new Proxy<this>(this, {
+ set: setter,
+ get: getter,
+ deleteProperty: () => { throw new Error("Currently properties can't be deleted from documents, assign to undefined instead"); },
+ defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); },
+ });
+ if (!id || forceSave) {
+ DocServer.CreateField(SerializationHelper.Serialize(doc));
+ }
+ return doc;
+ }
+
+ [key: string]: Field | null | undefined;
+
+ @serializable(alias("fields", map(autoObject())))
+ @observable
+ private __fields: { [key: string]: Field | null | undefined } = {};
+
+ private [Update] = (diff: any) => {
+ DocServer.UpdateField(this[Id], diff);
+ }
+
+ private [Self] = this;
+}
+
+export namespace Doc {
+ export function GetAsync(doc: Doc, key: string, ignoreProto: boolean = false): Promise<Field | undefined> {
+ const self = doc[Self];
+ return new Promise(res => getField(self, key, ignoreProto, res));
+ }
+ export function GetTAsync<T extends Field>(doc: Doc, key: string, ctor: FieldCtor<T>, ignoreProto: boolean = false): Promise<T | undefined> {
+ const self = doc[Self];
+ return new Promise(async res => {
+ const field = await GetAsync(doc, key, ignoreProto);
+ return Cast(field, ctor);
+ });
+ }
+ export function Get(doc: Doc, key: string, ignoreProto: boolean = false): Field | null | undefined {
+ const self = doc[Self];
+ return getField(self, key, ignoreProto);
+ }
+ export function GetT<T extends Field>(doc: Doc, key: string, ctor: FieldCtor<T>, ignoreProto: boolean = false): Field | null | undefined {
+ return Cast(Get(doc, key, ignoreProto), ctor);
+ }
+ export function MakeDelegate(doc: Opt<Doc>): Opt<Doc> {
+ if (!doc) {
+ return undefined;
+ }
+ const delegate = new Doc();
+ delegate.prototype = doc;
+ return delegate;
+ }
+ export const Prototype = Symbol("Prototype");
+}
+
+export const GetAsync = Doc.GetAsync; \ No newline at end of file
diff --git a/src/new_fields/HtmlField.ts b/src/new_fields/HtmlField.ts
new file mode 100644
index 000000000..f8e54ade5
--- /dev/null
+++ b/src/new_fields/HtmlField.ts
@@ -0,0 +1,14 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, primitive } from "serializr";
+import { ObjectField } from "./Doc";
+
+@Deserializable("html")
+export class URLField extends ObjectField {
+ @serializable(primitive())
+ readonly html: string;
+
+ constructor(html: string) {
+ super();
+ this.html = html;
+ }
+}
diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts
new file mode 100644
index 000000000..a1a623f83
--- /dev/null
+++ b/src/new_fields/List.ts
@@ -0,0 +1,35 @@
+import { Deserializable, autoObject } from "../client/util/SerializationHelper";
+import { Field, ObjectField, Update, OnUpdate, Self } from "./Doc";
+import { setter, getter } from "./util";
+import { serializable, alias, list } from "serializr";
+import { observable } from "mobx";
+
+@Deserializable("list")
+class ListImpl<T extends Field> extends ObjectField {
+ constructor() {
+ super();
+ const list = new Proxy<this>(this, {
+ set: function (a, b, c, d) { return setter(a, b, c, d); },
+ get: getter,
+ deleteProperty: () => { throw new Error("Currently properties can't be deleted from documents, assign to undefined instead"); },
+ defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); },
+ });
+ return list;
+ }
+
+ [key: number]: T | null | undefined;
+
+ @serializable(alias("fields", list(autoObject())))
+ @observable
+ private __fields: (T | null | undefined)[] = [];
+
+ private [Update] = (diff: any) => {
+ console.log(diff);
+ const update = this[OnUpdate];
+ update && update(diff);
+ }
+
+ private [Self] = this;
+}
+export type List<T extends Field> = ListImpl<T> & T[];
+export const List: { new <T extends Field>(): List<T> } = ListImpl as any; \ No newline at end of file
diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts
new file mode 100644
index 000000000..3b4b2e452
--- /dev/null
+++ b/src/new_fields/Proxy.ts
@@ -0,0 +1,55 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { RefField, Id, ObjectField } from "./Doc";
+import { primitive, serializable } from "serializr";
+import { observable } from "mobx";
+
+@Deserializable("proxy")
+export class ProxyField<T extends RefField> extends ObjectField {
+ constructor();
+ constructor(value: T);
+ constructor(value?: T) {
+ super();
+ if (value) {
+ this.cache = value;
+ this.fieldId = value[Id];
+ }
+ }
+
+ @serializable(primitive())
+ readonly fieldId: string = "";
+
+ // This getter/setter and nested object thing is
+ // because mobx doesn't play well with observable proxies
+ @observable.ref
+ private _cache: { readonly field: T | undefined } = { field: undefined };
+ private get cache(): T | undefined {
+ return this._cache.field;
+ }
+ private set cache(field: T | undefined) {
+ this._cache = { field };
+ }
+
+ private failed = false;
+ private promise?: Promise<any>;
+
+ value(callback?: ((field: T | undefined) => void)): T | undefined | null {
+ if (this.cache) {
+ callback && callback(this.cache);
+ return this.cache;
+ }
+ if (this.failed) {
+ return undefined;
+ }
+ if (!this.promise) {
+ // this.promise = Server.GetField(this.fieldId).then(action((field: any) => {
+ // this.promise = undefined;
+ // this.cache = field;
+ // if (field === undefined) this.failed = true;
+ // return field;
+ // }));
+ this.promise = new Promise(r => r());
+ }
+ callback && this.promise.then(callback);
+ return null;
+ }
+}
diff --git a/src/new_fields/Schema.ts b/src/new_fields/Schema.ts
new file mode 100644
index 000000000..1607d4c15
--- /dev/null
+++ b/src/new_fields/Schema.ts
@@ -0,0 +1,48 @@
+import { Interface, ToInterface, Cast, FieldCtor, ToConstructor } from "./Types";
+import { Doc } from "./Doc";
+
+export type makeInterface<T extends Interface, U extends Doc = Doc> = Partial<ToInterface<T>> & U;
+export function makeInterface<T extends Interface, U extends Doc>(schema: T): (doc: U) => makeInterface<T, U> {
+ return function (doc: any) {
+ return new Proxy(doc, {
+ get(target, prop) {
+ const field = target[prop];
+ if (prop in schema) {
+ return Cast(field, (schema as any)[prop]);
+ }
+ return field;
+ }
+ });
+ };
+}
+
+export type makeStrictInterface<T extends Interface> = Partial<ToInterface<T>>;
+export function makeStrictInterface<T extends Interface>(schema: T): (doc: Doc) => makeStrictInterface<T> {
+ const proto = {};
+ for (const key in schema) {
+ const type = schema[key];
+ Object.defineProperty(proto, key, {
+ get() {
+ return Cast(this.__doc[key], type as any);
+ },
+ set(value) {
+ value = Cast(value, type as any);
+ if (value !== undefined) {
+ this.__doc[key] = value;
+ return;
+ }
+ throw new TypeError("Expected type " + type);
+ }
+ });
+ }
+ return function (doc: any) {
+ const obj = Object.create(proto);
+ obj.__doc = doc;
+ return obj;
+ };
+}
+
+export function createSchema<T extends Interface>(schema: T): T & { prototype: ToConstructor<Doc> } {
+ schema.prototype = Doc;
+ return schema as any;
+}
diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts
new file mode 100644
index 000000000..cafb208ce
--- /dev/null
+++ b/src/new_fields/Types.ts
@@ -0,0 +1,58 @@
+import { Field, Opt } from "./Doc";
+import { List } from "./List";
+
+export type ToType<T> =
+ T extends "string" ? string :
+ T extends "number" ? number :
+ T extends "boolean" ? boolean :
+ T extends ListSpec<infer U> ? List<U> :
+ T extends { new(...args: any[]): infer R } ? R : never;
+
+export type ToConstructor<T> =
+ T extends string ? "string" :
+ T extends number ? "number" :
+ T extends boolean ? "boolean" : { new(...args: any[]): T };
+
+export type ToInterface<T> = {
+ [P in keyof T]: ToType<T[P]>;
+};
+
+// type ListSpec<T extends Field[]> = { List: FieldCtor<Head<T>> | ListSpec<Tail<T>> };
+export type ListSpec<T> = { List: FieldCtor<T> };
+
+// type ListType<U extends Field[]> = { 0: List<ListType<Tail<U>>>, 1: ToType<Head<U>> }[HasTail<U> extends true ? 0 : 1];
+
+export type Head<T extends any[]> = T extends [any, ...any[]] ? T[0] : never;
+export type Tail<T extends any[]> =
+ ((...t: T) => any) extends ((_: any, ...tail: infer TT) => any) ? TT : [];
+export type HasTail<T extends any[]> = T extends ([] | [any]) ? false : true;
+
+export interface Interface {
+ [key: string]: ToConstructor<Field> | ListSpec<Field>;
+ // [key: string]: ToConstructor<Field> | ListSpec<Field[]>;
+}
+
+export type FieldCtor<T extends Field> = ToConstructor<T> | ListSpec<T>;
+
+export function Cast<T extends FieldCtor<Field>>(field: Field | null | undefined, ctor: T): ToType<T> | null | undefined {
+ if (field !== undefined && field !== null) {
+ if (typeof ctor === "string") {
+ if (typeof field === ctor) {
+ return field as ToType<T>;
+ }
+ } else if (typeof ctor === "object") {
+ if (field instanceof List) {
+ return field as ToType<T>;
+ }
+ } else if (field instanceof (ctor as any)) {
+ return field as ToType<T>;
+ }
+ } else {
+ return field;
+ }
+ return undefined;
+}
+
+export function FieldValue<T extends Field>(field: Opt<T> | Promise<Opt<T>>): Opt<T> {
+ return field instanceof Promise ? undefined : field;
+}
diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts
new file mode 100644
index 000000000..d27a2b692
--- /dev/null
+++ b/src/new_fields/URLField.ts
@@ -0,0 +1,25 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable } from "serializr";
+import { ObjectField } from "./Doc";
+
+function url() {
+ return {
+ serializer: function (value: URL) {
+ return value.href;
+ },
+ deserializer: function (jsonValue: string, done: (err: any, val: any) => void) {
+ done(undefined, new URL(jsonValue));
+ }
+ };
+}
+
+@Deserializable("url")
+export class URLField extends ObjectField {
+ @serializable(url())
+ readonly url: URL;
+
+ constructor(url: URL) {
+ super();
+ this.url = url;
+ }
+} \ No newline at end of file
diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts
new file mode 100644
index 000000000..0f08ecf03
--- /dev/null
+++ b/src/new_fields/util.ts
@@ -0,0 +1,73 @@
+import { UndoManager } from "../client/util/UndoManager";
+import { Update, OnUpdate, Parent, ObjectField, RefField, Doc, Id, Field } from "./Doc";
+import { SerializationHelper } from "../client/util/SerializationHelper";
+import { ProxyField } from "./Proxy";
+
+export function setter(target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
+ if (SerializationHelper.IsSerializing()) {
+ target[prop] = value;
+ return true;
+ }
+ if (typeof prop === "symbol") {
+ target[prop] = value;
+ return true;
+ }
+ const curValue = target.__fields[prop];
+ if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) {
+ // TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically
+ // curValue should get filled in with value if it isn't already filled in, in case we fetched the referenced field some other way
+ return true;
+ }
+ if (value instanceof RefField) {
+ value = new ProxyField(value);
+ }
+ if (value instanceof ObjectField) {
+ if (value[Parent] && value[Parent] !== target) {
+ throw new Error("Can't put the same object in multiple documents at the same time");
+ }
+ value[Parent] = target;
+ value[OnUpdate] = (diff?: any) => {
+ if (!diff) diff = SerializationHelper.Serialize(value);
+ target[Update]({ [prop]: diff });
+ };
+ }
+ if (curValue instanceof ObjectField) {
+ delete curValue[Parent];
+ delete curValue[OnUpdate];
+ }
+ target.__fields[prop] = value;
+ target[Update]({ ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) });
+ UndoManager.AddEvent({
+ redo: () => receiver[prop] = value,
+ undo: () => receiver[prop] = curValue
+ });
+ return true;
+}
+
+export function getter(target: any, prop: string | symbol | number, receiver: any): any {
+ if (typeof prop === "symbol") {
+ return target.__fields[prop] || target[prop];
+ }
+ if (SerializationHelper.IsSerializing()) {
+ return target[prop];
+ }
+ return getField(target, prop, receiver);
+}
+
+export function getField(target: any, prop: string | number, ignoreProto: boolean = false, callback?: (field: Field | undefined) => void): any {
+ const field = target.__fields[prop];
+ if (field instanceof ProxyField) {
+ return field.value(callback);
+ }
+ if (field === undefined && !ignoreProto) {
+ const proto = getField(target, "prototype", true);
+ if (proto instanceof Doc) {
+ let field = proto[prop];
+ callback && callback(field === null ? undefined : field);
+ return field;
+ }
+ }
+ callback && callback(field);
+ return field;
+
+}
diff --git a/src/server/Message.ts b/src/server/Message.ts
index 15916ef12..b01934724 100644
--- a/src/server/Message.ts
+++ b/src/server/Message.ts
@@ -24,6 +24,14 @@ export interface Transferable {
readonly data?: any;
}
+export interface Reference {
+ readonly id: string;
+}
+
+export interface Diff extends Reference {
+ readonly diff: any;
+}
+
export namespace MessageStore {
export const Foo = new Message<string>("Foo");
export const Bar = new Message<string>("Bar");
@@ -32,4 +40,8 @@ export namespace MessageStore {
export const GetFields = new Message<string[]>("Get Fields"); // send string[] of 'id' get Transferable[] back
export const GetDocument = new Message<string>("Get Document");
export const DeleteAll = new Message<any>("Delete All");
+
+ export const GetRefField = new Message<string>("Get Ref Field");
+ export const UpdateField = new Message<Diff>("Update Ref Field");
+ export const CreateField = new Message<Reference>("Create Ref Field");
}
diff --git a/src/server/database.ts b/src/server/database.ts
index 5457e4dd5..a61b4d823 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -13,14 +13,14 @@ export class Database {
this.MongoClient.connect(this.url, (err, client) => this.db = client.db());
}
- public update(id: string, value: any, callback: () => void) {
+ public update(id: string, value: any, callback: () => void, upsert = true, collectionName = Database.DocumentsCollection) {
if (this.db) {
- let collection = this.db.collection('documents');
+ let collection = this.db.collection(collectionName);
const prom = this.currentWrites[id];
let newProm: Promise<void>;
const run = (): Promise<void> => {
return new Promise<void>(resolve => {
- collection.updateOne({ _id: id }, { $set: value }, { upsert: true }
+ collection.updateOne({ _id: id }, { $set: value }, { upsert }
, (err, res) => {
if (err) {
console.log(err.message);
@@ -51,10 +51,12 @@ export class Database {
this.db && this.db.collection(collectionName).deleteMany({}, res));
}
- public insert(kvpairs: any, collectionName = Database.DocumentsCollection) {
- this.db && this.db.collection(collectionName).insertOne(kvpairs, (err, res) =>
- err // && console.log(err)
- );
+ public insert(value: any, collectionName = Database.DocumentsCollection) {
+ if ("id" in value) {
+ value._id = value.id;
+ delete value.id;
+ }
+ this.db && this.db.collection(collectionName).insertOne(value);
}
public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = Database.DocumentsCollection) {
diff --git a/src/server/index.ts b/src/server/index.ts
index 70a7d266c..d6d5f0e55 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -22,7 +22,7 @@ import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLo
import { DashUserModel } from './authentication/models/user_model';
import { Client } from './Client';
import { Database } from './database';
-import { MessageStore, Transferable } from "./Message";
+import { MessageStore, Transferable, Diff } from "./Message";
import { RouteStore } from './RouteStore';
const app = express();
const config = require('../../webpack.config');
@@ -232,6 +232,10 @@ server.on("connection", function (socket: Socket) {
Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField);
Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields);
Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteFields);
+
+ Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField);
+ Utils.AddServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff));
+ Utils.AddServerHandler(socket, MessageStore.GetRefField, GetRefField);
});
function deleteFields() {
@@ -262,5 +266,18 @@ function setField(socket: Socket, newValue: Transferable) {
socket.broadcast.emit(MessageStore.SetField.Message, newValue));
}
+function GetRefField([id, callback]: [string, (result?: Transferable) => void]) {
+ Database.Instance.getDocument(id, callback, "newDocuments");
+}
+
+function UpdateField(socket: Socket, diff: Diff) {
+ Database.Instance.update(diff.id, diff.diff,
+ () => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false, "newDocuments");
+}
+
+function CreateField(newValue: any) {
+ Database.Instance.insert(newValue, "newDocuments");
+}
+
server.listen(serverPort);
console.log(`listening on port ${serverPort}`); \ No newline at end of file