aboutsummaryrefslogtreecommitdiff
path: root/src/new_fields
diff options
context:
space:
mode:
Diffstat (limited to 'src/new_fields')
-rw-r--r--src/new_fields/Doc.ts223
-rw-r--r--src/new_fields/HtmlField.ts18
-rw-r--r--src/new_fields/IconField.ts18
-rw-r--r--src/new_fields/InkField.ts44
-rw-r--r--src/new_fields/List.ts240
-rw-r--r--src/new_fields/ObjectField.ts17
-rw-r--r--src/new_fields/Proxy.ts65
-rw-r--r--src/new_fields/RefField.ts18
-rw-r--r--src/new_fields/RichTextField.ts18
-rw-r--r--src/new_fields/Schema.ts82
-rw-r--r--src/new_fields/Types.ts87
-rw-r--r--src/new_fields/URLField.ts34
-rw-r--r--src/new_fields/util.ts104
13 files changed, 968 insertions, 0 deletions
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
new file mode 100644
index 000000000..3055af1bf
--- /dev/null
+++ b/src/new_fields/Doc.ts
@@ -0,0 +1,223 @@
+import { observable, action } from "mobx";
+import { serializable, primitive, map, alias, list } from "serializr";
+import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper";
+import { DocServer } from "../client/DocServer";
+import { setter, getter, getField, updateFunction, deleteProperty } from "./util";
+import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast } from "./Types";
+import { UndoManager, undoBatch } from "../client/util/UndoManager";
+import { listSpec } from "./Schema";
+import { List } from "./List";
+import { ObjectField, Parent, OnUpdate } from "./ObjectField";
+import { RefField, FieldId, Id, HandleUpdate } from "./RefField";
+import { Docs } from "../client/documents/Documents";
+
+export function IsField(field: any): field is Field {
+ return (typeof field === "string")
+ || (typeof field === "number")
+ || (typeof field === "boolean")
+ || (field instanceof ObjectField)
+ || (field instanceof RefField);
+}
+export type Field = number | string | boolean | ObjectField | RefField;
+export type Opt<T> = T | undefined;
+export type FieldWaiting<T extends RefField = RefField> = T extends undefined ? never : Promise<T | undefined>;
+export type FieldResult<T extends Field = Field> = Opt<T> | FieldWaiting<Extract<T, RefField>>;
+
+export const Update = Symbol("Update");
+export const Self = Symbol("Self");
+const SelfProxy = Symbol("SelfProxy");
+export const WidthSym = Symbol("Width");
+export const HeightSym = Symbol("Height");
+
+@Deserializable("doc").withFields(["id"])
+export class Doc extends RefField {
+ constructor(id?: FieldId, forceSave?: boolean) {
+ super(id);
+ const doc = new Proxy<this>(this, {
+ set: setter,
+ get: getter,
+ has: (target, key) => key in target.__fields,
+ ownKeys: target => Object.keys(target.__fields),
+ getOwnPropertyDescriptor: (target, prop) => {
+ if (prop in target.__fields) {
+ return {
+ configurable: true,//TODO Should configurable be true?
+ enumerable: true,
+ };
+ }
+ return Reflect.getOwnPropertyDescriptor(target, prop);
+ },
+ deleteProperty: deleteProperty,
+ defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); },
+ });
+ this[SelfProxy] = doc;
+ if (!id || forceSave) {
+ DocServer.CreateField(doc);
+ }
+ return doc;
+ }
+
+ proto: Opt<Doc>;
+ [key: string]: FieldResult;
+
+ @serializable(alias("fields", map(autoObject())))
+ private get __fields() {
+ return this.___fields;
+ }
+
+ private set __fields(value) {
+ this.___fields = value;
+ for (const key in value) {
+ const field = value[key];
+ if (!(field instanceof ObjectField)) continue;
+ field[Parent] = this[Self];
+ field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]);
+ }
+ }
+
+ @observable
+ //{ [key: string]: Field | FieldWaiting | undefined }
+ private ___fields: any = {};
+
+ private [Update] = (diff: any) => {
+ DocServer.UpdateField(this[Id], diff);
+ }
+
+ private [Self] = this;
+ private [SelfProxy]: any;
+ public [WidthSym] = () => NumCast(this.__fields.width); // bcz: is this the right way to access width/height? it didn't work with : this.width
+ public [HeightSym] = () => NumCast(this.__fields.height);
+
+ public [HandleUpdate](diff: any) {
+ console.log(diff);
+ const set = diff.$set;
+ if (set) {
+ for (const key in set) {
+ if (!key.startsWith("fields.")) {
+ continue;
+ }
+ const value = SerializationHelper.Deserialize(set[key]);
+ const fKey = key.substring(7);
+ this[fKey] = value;
+ }
+ }
+ }
+}
+
+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: ToConstructor<T>, ignoreProto: boolean = false): Promise<T | undefined> {
+ // 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): FieldResult {
+ const self = doc[Self];
+ return getField(self, key, ignoreProto);
+ }
+ export function GetT<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): T | null | undefined {
+ return Cast(Get(doc, key, ignoreProto), ctor) as T | null | undefined;
+ }
+ export async function SetOnPrototype(doc: Doc, key: string, value: Field) {
+ const proto = doc.proto;
+ if (proto) {
+ proto[key] = value;
+ }
+ }
+ export function GetAllPrototypes(doc: Doc): Doc[] {
+ const protos: Doc[] = [];
+ let d: Opt<Doc> = doc;
+ while (d) {
+ protos.push(d);
+ d = FieldValue(d.proto);
+ }
+ return protos;
+ }
+ export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<Field>>>) {
+ for (const key in fields) {
+ if (fields.hasOwnProperty(key)) {
+ const value = fields[key];
+ if (value !== undefined) {
+ doc[key] = value;
+ }
+ }
+ }
+ return doc;
+ }
+
+ export function MakeAlias(doc: Doc) {
+ const alias = new Doc;
+
+ PromiseValue(Cast(doc.proto, Doc)).then(proto => {
+ if (proto) {
+ alias.proto = proto;
+ }
+ });
+
+ return alias;
+ }
+
+ export function MakeCopy(doc: Doc, copyProto: boolean = false): Doc {
+ const copy = new Doc;
+ Object.keys(doc).forEach(key => {
+ const field = doc[key];
+ if (key === "proto" && copyProto) {
+ if (field instanceof Doc) {
+ copy[key] = Doc.MakeCopy(field);
+ }
+ } else {
+ if (field instanceof RefField) {
+ copy[key] = field;
+ } else if (field instanceof ObjectField) {
+ copy[key] = ObjectField.MakeCopy(field);
+ } else {
+ copy[key] = field;
+ }
+ }
+ });
+ return copy;
+ }
+
+ export function MakeLink(source: Doc, target: Doc) {
+ UndoManager.RunInBatch(() => {
+ let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 });
+ //let linkDoc = new Doc;
+ linkDoc.title = "-link name-";
+ linkDoc.linkDescription = "";
+ linkDoc.linkTags = "Default";
+
+ linkDoc.linkedTo = target;
+ linkDoc.linkedFrom = source;
+
+ let linkedFrom = Cast(target.linkedFromDocs, listSpec(Doc));
+ if (!linkedFrom) {
+ target.linkedFromDocs = linkedFrom = new List<Doc>();
+ }
+ linkedFrom.push(linkDoc);
+
+ let linkedTo = Cast(source.linkedToDocs, listSpec(Doc));
+ if (!linkedTo) {
+ source.linkedToDocs = linkedTo = new List<Doc>();
+ }
+ linkedTo.push(linkDoc);
+ return linkDoc;
+ }, "make link");
+ }
+
+ export function MakeDelegate(doc: Doc): Doc;
+ export function MakeDelegate(doc: Opt<Doc>): Opt<Doc>;
+ export function MakeDelegate(doc: Opt<Doc>): Opt<Doc> {
+ if (!doc) {
+ return undefined;
+ }
+ const delegate = new Doc();
+ //TODO Does this need to be doc[Self]?
+ delegate.proto = doc;
+ return delegate;
+ }
+ export const Prototype = Symbol("Prototype");
+} \ 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..d998746bb
--- /dev/null
+++ b/src/new_fields/HtmlField.ts
@@ -0,0 +1,18 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, primitive } from "serializr";
+import { ObjectField, Copy } from "./ObjectField";
+
+@Deserializable("html")
+export class HtmlField extends ObjectField {
+ @serializable(primitive())
+ readonly html: string;
+
+ constructor(html: string) {
+ super();
+ this.html = html;
+ }
+
+ [Copy]() {
+ return new HtmlField(this.html);
+ }
+}
diff --git a/src/new_fields/IconField.ts b/src/new_fields/IconField.ts
new file mode 100644
index 000000000..1a928389d
--- /dev/null
+++ b/src/new_fields/IconField.ts
@@ -0,0 +1,18 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, primitive } from "serializr";
+import { ObjectField, Copy } from "./ObjectField";
+
+@Deserializable("icon")
+export class IconField extends ObjectField {
+ @serializable(primitive())
+ readonly icon: string;
+
+ constructor(icon: string) {
+ super();
+ this.icon = icon;
+ }
+
+ [Copy]() {
+ return new IconField(this.icon);
+ }
+}
diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts
new file mode 100644
index 000000000..86a8bd18a
--- /dev/null
+++ b/src/new_fields/InkField.ts
@@ -0,0 +1,44 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, custom, createSimpleSchema, list, object, map } from "serializr";
+import { ObjectField, Copy } from "./ObjectField";
+import { number } from "prop-types";
+import { any } from "bluebird";
+import { deepCopy } from "../Utils";
+
+export enum InkTool {
+ None,
+ Pen,
+ Highlighter,
+ Eraser
+}
+export interface StrokeData {
+ pathData: Array<{ x: number, y: number }>;
+ color: string;
+ width: string;
+ tool: InkTool;
+ page: number;
+}
+
+const pointSchema = createSimpleSchema({
+ x: true, y: true
+});
+
+const strokeDataSchema = createSimpleSchema({
+ pathData: list(object(pointSchema)),
+ "*": true
+});
+
+@Deserializable("ink")
+export class InkField extends ObjectField {
+ @serializable(map(object(strokeDataSchema)))
+ readonly inkData: Map<string, StrokeData>;
+
+ constructor(data?: Map<string, StrokeData>) {
+ super();
+ this.inkData = data || new Map;
+ }
+
+ [Copy]() {
+ return new InkField(deepCopy(this.inkData))
+ }
+}
diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts
new file mode 100644
index 000000000..96018dafa
--- /dev/null
+++ b/src/new_fields/List.ts
@@ -0,0 +1,240 @@
+import { Deserializable, autoObject } from "../client/util/SerializationHelper";
+import { Field, Update, Self, FieldResult } from "./Doc";
+import { setter, getter, deleteProperty } from "./util";
+import { serializable, alias, list } from "serializr";
+import { observable, action } from "mobx";
+import { ObjectField, OnUpdate, Copy } from "./ObjectField";
+import { RefField } from "./RefField";
+import { ProxyField } from "./Proxy";
+
+const listHandlers: any = {
+ /// Mutator methods
+ copyWithin() {
+ throw new Error("copyWithin not supported yet");
+ },
+ fill(value: any, start?: number, end?: number) {
+ if (value instanceof RefField) {
+ throw new Error("fill with RefFields not supported yet");
+ }
+ const res = this[Self].__fields.fill(value, start, end);
+ this[Update]();
+ return res;
+ },
+ pop(): any {
+ const field = toRealField(this[Self].__fields.pop());
+ this[Update]();
+ return field;
+ },
+ push: action(function (this: any, ...items: any[]) {
+ items = items.map(toObjectField);
+ const res = this[Self].__fields.push(...items);
+ this[Update]();
+ return res;
+ }),
+ reverse() {
+ const res = this[Self].__fields.reverse();
+ this[Update]();
+ return res;
+ },
+ shift() {
+ const res = toRealField(this[Self].__fields.shift());
+ this[Update]();
+ return res;
+ },
+ sort(cmpFunc: any) {
+ const res = this[Self].__fields.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined);
+ this[Update]();
+ return res;
+ },
+ splice: action(function (this: any, start: number, deleteCount: number, ...items: any[]) {
+ items = items.map(toObjectField);
+ const res = this[Self].__fields.splice(start, deleteCount, ...items);
+ this[Update]();
+ return res.map(toRealField);
+ }),
+ unshift(...items: any[]) {
+ items = items.map(toObjectField);
+ const res = this[Self].__fields.unshift(...items);
+ this[Update]();
+ return res;
+
+ },
+ /// Accessor methods
+ concat: action(function (this: any, ...items: any[]) {
+ return this[Self].__fields.map(toRealField).concat(...items);
+ }),
+ includes(valueToFind: any, fromIndex: number) {
+ const fields = this[Self].__fields;
+ if (valueToFind instanceof RefField) {
+ return fields.map(toRealField).includes(valueToFind, fromIndex);
+ } else {
+ return fields.includes(valueToFind, fromIndex);
+ }
+ },
+ indexOf(valueToFind: any, fromIndex: number) {
+ const fields = this[Self].__fields;
+ if (valueToFind instanceof RefField) {
+ return fields.map(toRealField).indexOf(valueToFind, fromIndex);
+ } else {
+ return fields.indexOf(valueToFind, fromIndex);
+ }
+ },
+ join(separator: any) {
+ return this[Self].__fields.map(toRealField).join(separator);
+ },
+ lastIndexOf(valueToFind: any, fromIndex: number) {
+ const fields = this[Self].__fields;
+ if (valueToFind instanceof RefField) {
+ return fields.map(toRealField).lastIndexOf(valueToFind, fromIndex);
+ } else {
+ return fields.lastIndexOf(valueToFind, fromIndex);
+ }
+ },
+ slice(begin: number, end: number) {
+ return this[Self].__fields.slice(begin, end).map(toRealField);
+ },
+
+ /// Iteration methods
+ entries() {
+ return this[Self].__fields.map(toRealField).entries();
+ },
+ every(callback: any, thisArg: any) {
+ return this[Self].__fields.map(toRealField).every(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.every((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ filter(callback: any, thisArg: any) {
+ return this[Self].__fields.map(toRealField).filter(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ find(callback: any, thisArg: any) {
+ return this[Self].__fields.map(toRealField).find(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ findIndex(callback: any, thisArg: any) {
+ return this[Self].__fields.map(toRealField).findIndex(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ forEach(callback: any, thisArg: any) {
+ return this[Self].__fields.map(toRealField).forEach(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ map(callback: any, thisArg: any) {
+ return this[Self].__fields.map(toRealField).map(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ reduce(callback: any, initialValue: any) {
+ return this[Self].__fields.map(toRealField).reduce(callback, initialValue);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue);
+ },
+ reduceRight(callback: any, initialValue: any) {
+ return this[Self].__fields.map(toRealField).reduceRight(callback, initialValue);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue);
+ },
+ some(callback: any, thisArg: any) {
+ return this[Self].__fields.map(toRealField).some(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ values() {
+ return this[Self].__fields.map(toRealField).values();
+ },
+ [Symbol.iterator]() {
+ return this[Self].__fields.map(toRealField).values();
+ }
+};
+
+function toObjectField(field: Field) {
+ return field instanceof RefField ? new ProxyField(field) : field;
+}
+
+function toRealField(field: Field) {
+ return field instanceof ProxyField ? field.value() : field;
+}
+
+function listGetter(target: any, prop: string | number | symbol, receiver: any): any {
+ if (listHandlers.hasOwnProperty(prop)) {
+ return listHandlers[prop];
+ }
+ return getter(target, prop, receiver);
+}
+
+interface ListSpliceUpdate<T> {
+ type: "splice";
+ index: number;
+ added: T[];
+ removedCount: number;
+}
+
+interface ListIndexUpdate<T> {
+ type: "update";
+ index: number;
+ newValue: T;
+}
+
+type ListUpdate<T> = ListSpliceUpdate<T> | ListIndexUpdate<T>;
+
+type StoredType<T extends Field> = T extends RefField ? ProxyField<T> : T;
+
+@Deserializable("list")
+class ListImpl<T extends Field> extends ObjectField {
+ constructor(fields: T[] = []) {
+ super();
+ const list = new Proxy<this>(this, {
+ set: setter,
+ get: listGetter,
+ deleteProperty: deleteProperty,
+ defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); },
+ });
+ (list as any).push(...fields);
+ return list;
+ }
+
+ [key: number]: FieldResult<T>;
+
+ @serializable(alias("fields", list(autoObject())))
+ private get __fields() {
+ return this.___fields;
+ }
+
+ private set __fields(value) {
+ this.___fields = value;
+ }
+
+ [Copy]() {
+ let copiedData = this[Self].__fields.map(f => f instanceof ObjectField ? f[Copy]() : f);
+ let deepCopy = new ListImpl<T>(copiedData as any);
+ return deepCopy;
+ }
+
+ // @serializable(alias("fields", list(autoObject())))
+ @observable
+ private ___fields: StoredType<T>[] = [];
+
+ private [Update] = (diff: any) => {
+ // console.log(diff);
+ const update = this[OnUpdate];
+ // update && update(diff);
+ update && update();
+ }
+
+ private [Self] = this;
+}
+export type List<T extends Field> = ListImpl<T> & T[];
+export const List: { new <T extends Field>(fields?: T[]): List<T> } = ListImpl as any; \ No newline at end of file
diff --git a/src/new_fields/ObjectField.ts b/src/new_fields/ObjectField.ts
new file mode 100644
index 000000000..0f3777af6
--- /dev/null
+++ b/src/new_fields/ObjectField.ts
@@ -0,0 +1,17 @@
+import { Doc } from "./Doc";
+
+export const OnUpdate = Symbol("OnUpdate");
+export const Parent = Symbol("Parent");
+export const Copy = Symbol("Copy");
+
+export abstract class ObjectField {
+ protected [OnUpdate]?: (diff?: any) => void;
+ private [Parent]?: Doc;
+ abstract [Copy](): ObjectField;
+}
+
+export namespace ObjectField {
+ export function MakeCopy<T extends ObjectField>(field: T) {
+ return field[Copy]();
+ }
+}
diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts
new file mode 100644
index 000000000..fd99ae1c0
--- /dev/null
+++ b/src/new_fields/Proxy.ts
@@ -0,0 +1,65 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { FieldWaiting } from "./Doc";
+import { primitive, serializable } from "serializr";
+import { observable, action } from "mobx";
+import { DocServer } from "../client/DocServer";
+import { RefField, Id } from "./RefField";
+import { ObjectField, Copy } from "./ObjectField";
+
+@Deserializable("proxy")
+export class ProxyField<T extends RefField> extends ObjectField {
+ constructor();
+ constructor(value: T);
+ constructor(fieldId: string);
+ constructor(value?: T | string) {
+ super();
+ if (typeof value === "string") {
+ this.fieldId = value;
+ } else if (value) {
+ this.cache = value;
+ this.fieldId = value[Id];
+ }
+ }
+
+ [Copy]() {
+ if (this.cache) return new ProxyField<T>(this.cache);
+ return new ProxyField<T>(this.fieldId);
+ }
+
+ @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 | FieldWaiting {
+ if (this.cache) {
+ callback && callback(this.cache);
+ return this.cache;
+ }
+ if (this.failed) {
+ return undefined;
+ }
+ if (!this.promise) {
+ this.promise = DocServer.GetRefField(this.fieldId).then(action((field: any) => {
+ this.promise = undefined;
+ this.cache = field;
+ if (field === undefined) this.failed = true;
+ return field;
+ }));
+ }
+ callback && this.promise.then(callback);
+ return this.promise;
+ }
+}
diff --git a/src/new_fields/RefField.ts b/src/new_fields/RefField.ts
new file mode 100644
index 000000000..202c65f21
--- /dev/null
+++ b/src/new_fields/RefField.ts
@@ -0,0 +1,18 @@
+import { serializable, primitive, alias } from "serializr";
+import { Utils } from "../Utils";
+
+export type FieldId = string;
+export const HandleUpdate = Symbol("HandleUpdate");
+export const Id = Symbol("Id");
+export abstract class RefField {
+ @serializable(alias("id", primitive()))
+ private __id: FieldId;
+ readonly [Id]: FieldId;
+
+ constructor(id?: FieldId) {
+ this.__id = id || Utils.GenerateGuid();
+ this[Id] = this.__id;
+ }
+
+ protected [HandleUpdate]?(diff: any): void;
+}
diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts
new file mode 100644
index 000000000..eb30e76de
--- /dev/null
+++ b/src/new_fields/RichTextField.ts
@@ -0,0 +1,18 @@
+import { ObjectField, Copy } from "./ObjectField";
+import { serializable } from "serializr";
+import { Deserializable } from "../client/util/SerializationHelper";
+
+@Deserializable("RichTextField")
+export class RichTextField extends ObjectField {
+ @serializable(true)
+ readonly Data: string;
+
+ constructor(data: string) {
+ super();
+ this.Data = data;
+ }
+
+ [Copy]() {
+ return new RichTextField(this.Data);
+ }
+} \ No newline at end of file
diff --git a/src/new_fields/Schema.ts b/src/new_fields/Schema.ts
new file mode 100644
index 000000000..b821baec9
--- /dev/null
+++ b/src/new_fields/Schema.ts
@@ -0,0 +1,82 @@
+import { Interface, ToInterface, Cast, ToConstructor, HasTail, Head, Tail, ListSpec, ToType } from "./Types";
+import { Doc, Field } from "./Doc";
+
+type AllToInterface<T extends Interface[]> = {
+ 1: ToInterface<Head<T>> & AllToInterface<Tail<T>>,
+ 0: ToInterface<Head<T>>
+}[HasTail<T> extends true ? 1 : 0];
+
+export const emptySchema = createSchema({});
+export const Document = makeInterface(emptySchema);
+export type Document = makeInterface<[typeof emptySchema]>;
+
+export type makeInterface<T extends Interface[]> = Partial<AllToInterface<T>> & Doc & { proto: Doc | undefined };
+// export function makeInterface<T extends Interface[], U extends Doc>(schemas: T): (doc: U) => All<T, U>;
+// export function makeInterface<T extends Interface, U extends Doc>(schema: T): (doc: U) => makeInterface<T, U>;
+export function makeInterface<T extends Interface[]>(...schemas: T): (doc?: Doc) => makeInterface<T> {
+ let schema: Interface = {};
+ for (const s of schemas) {
+ for (const key in s) {
+ schema[key] = s[key];
+ }
+ }
+ const proto = new Proxy({}, {
+ get(target: any, prop, receiver) {
+ const field = receiver.doc[prop];
+ if (prop in schema) {
+ return Cast(field, (schema as any)[prop]);
+ }
+ return field;
+ },
+ set(target: any, prop, value, receiver) {
+ receiver.doc[prop] = value;
+ return true;
+ }
+ });
+ return function (doc?: Doc) {
+ doc = doc || new Doc;
+ if (!(doc instanceof Doc)) {
+ throw new Error("Currently wrapping a schema in another schema isn't supported");
+ }
+ const obj = Object.create(proto, { doc: { value: doc, writable: false } });
+ return obj;
+ };
+}
+
+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) {
+ if (!(doc instanceof Doc)) {
+ throw new Error("Currently wrapping a schema in another schema isn't supported");
+ }
+ const obj = Object.create(proto);
+ obj.__doc = doc;
+ return obj;
+ };
+}
+
+export function createSchema<T extends Interface>(schema: T): T & { proto: ToConstructor<Doc> } {
+ schema.proto = Doc;
+ return schema as any;
+}
+
+export function listSpec<U extends ToConstructor<Field>>(type: U): ListSpec<ToType<U>> {
+ return { List: type as any };//TODO Types
+} \ No newline at end of file
diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts
new file mode 100644
index 000000000..60f08dc90
--- /dev/null
+++ b/src/new_fields/Types.ts
@@ -0,0 +1,87 @@
+import { Field, Opt, FieldResult } from "./Doc";
+import { List } from "./List";
+
+export type ToType<T extends ToConstructor<Field> | ListSpec<Field>> =
+ 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 | Promise<R>) : never;
+ T extends { new(...args: any[]): List<Field> } ? never :
+ T extends { new(...args: any[]): infer R } ? R : never;
+
+export type ToConstructor<T extends Field> =
+ T extends string ? "string" :
+ T extends number ? "number" :
+ T extends boolean ? "boolean" :
+ T extends List<infer U> ? ListSpec<U> :
+ new (...args: any[]) => T;
+
+export type ToInterface<T extends Interface> = {
+ [P in Exclude<keyof T, "proto">]: FieldResult<ToType<T[P]>>;
+};
+
+// type ListSpec<T extends Field[]> = { List: ToContructor<Head<T>> | ListSpec<Tail<T>> };
+export type ListSpec<T extends Field> = { List: ToConstructor<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;
+
+//TODO Allow you to optionally specify default values for schemas, which should then make that field not be partial
+export interface Interface {
+ [key: string]: ToConstructor<Field> | ListSpec<Field>;
+ // [key: string]: ToConstructor<Field> | ListSpec<Field[]>;
+}
+
+export function Cast<T extends ToConstructor<Field> | ListSpec<Field>>(field: FieldResult, ctor: T): FieldResult<ToType<T>>;
+export function Cast<T extends ToConstructor<Field> | ListSpec<Field>>(field: FieldResult, ctor: T, defaultVal: WithoutList<ToType<T>> | null): WithoutList<ToType<T>>;
+export function Cast<T extends ToConstructor<Field> | ListSpec<Field>>(field: FieldResult, ctor: T, defaultVal?: ToType<T> | null): FieldResult<ToType<T>> | undefined {
+ if (field instanceof Promise) {
+ return defaultVal === undefined ? field.then(f => Cast(f, ctor) as any) as any : defaultVal === null ? undefined : defaultVal;
+ }
+ if (field !== undefined && !(field instanceof Promise)) {
+ 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 any;
+ }
+ } else if (field instanceof (ctor as any)) {
+ return field as ToType<T>;
+ }
+ }
+ return defaultVal === null ? undefined : defaultVal;
+}
+
+export function NumCast(field: FieldResult, defaultVal: number | null = 0) {
+ return Cast(field, "number", defaultVal);
+}
+
+export function StrCast(field: FieldResult, defaultVal: string | null = "") {
+ return Cast(field, "string", defaultVal);
+}
+
+export function BoolCast(field: FieldResult, defaultVal: boolean | null = null) {
+ return Cast(field, "boolean", defaultVal);
+}
+
+type WithoutList<T extends Field> = T extends List<infer R> ? R[] : T;
+
+export function FieldValue<T extends Field, U extends WithoutList<T>>(field: FieldResult<T>, defaultValue: U): WithoutList<T>;
+export function FieldValue<T extends Field>(field: FieldResult<T>): Opt<T>;
+export function FieldValue<T extends Field>(field: FieldResult<T>, defaultValue?: T): Opt<T> {
+ return (field instanceof Promise || field === undefined) ? defaultValue : field;
+}
+
+export interface PromiseLike<T> {
+ then(callback: (field: Opt<T>) => void): void;
+}
+export function PromiseValue<T extends Field>(field: FieldResult<T>): PromiseLike<Opt<T>> {
+ return field instanceof Promise ? field : { then(cb: ((field: Opt<T>) => void)) { return cb(field); } };
+} \ No newline at end of file
diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts
new file mode 100644
index 000000000..d00a95a16
--- /dev/null
+++ b/src/new_fields/URLField.ts
@@ -0,0 +1,34 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, custom } from "serializr";
+import { ObjectField, Copy } from "./ObjectField";
+
+function url() {
+ return custom(
+ function (value: URL) {
+ return value.href;
+ },
+ function (jsonValue: string) {
+ return new URL(jsonValue);
+ }
+ );
+}
+
+export class URLField extends ObjectField {
+ @serializable(url())
+ readonly url: URL;
+
+ constructor(url: URL) {
+ super();
+ this.url = url;
+ }
+
+ [Copy](): this {
+ return new (this.constructor as any)(this.url);
+ }
+}
+
+@Deserializable("audio") export class AudioField extends URLField { }
+@Deserializable("image") export class ImageField extends URLField { }
+@Deserializable("video") export class VideoField extends URLField { }
+@Deserializable("pdf") export class PdfField extends URLField { }
+@Deserializable("web") export class WebField extends URLField { } \ 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..bbd8157f6
--- /dev/null
+++ b/src/new_fields/util.ts
@@ -0,0 +1,104 @@
+import { UndoManager } from "../client/util/UndoManager";
+import { Update, Doc, Field } from "./Doc";
+import { SerializationHelper } from "../client/util/SerializationHelper";
+import { ProxyField } from "./Proxy";
+import { FieldValue } from "./Types";
+import { RefField, Id } from "./RefField";
+import { ObjectField, Parent, OnUpdate } from "./ObjectField";
+import { action } from "mobx";
+
+export const setter = action(function (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) {
+ //TODO Instead of target, maybe use target[Self]
+ 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] = updateFunction(target, prop, value, receiver);
+ }
+ if (curValue instanceof ObjectField) {
+ delete curValue[Parent];
+ delete curValue[OnUpdate];
+ }
+ target.__fields[prop] = value;
+ target[Update]({ '$set': { ["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);
+}
+
+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, "proto", true);
+ if (proto instanceof Doc) {
+ let field = proto[prop];
+ if (field instanceof Promise) {
+ callback && field.then(callback);
+ return undefined;
+ } else {
+ callback && callback(field);
+ return field;
+ }
+ }
+ }
+ callback && callback(field);
+ return field;
+}
+
+export function deleteProperty(target: any, prop: string | number | symbol) {
+ if (typeof prop === "symbol") {
+ delete target[prop];
+ return true;
+ }
+ throw new Error("Currently properties can't be deleted from documents, assign to undefined instead");
+}
+
+export function updateFunction(target: any, prop: any, value: any, receiver: any) {
+ let current = ObjectField.MakeCopy(value);
+ return (diff?: any) => {
+ if (true || !diff) {
+ diff = { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } };
+ const oldValue = current;
+ const newValue = ObjectField.MakeCopy(value);
+ current = newValue;
+ UndoManager.AddEvent({
+ redo() { receiver[prop] = newValue; },
+ undo() { receiver[prop] = oldValue; }
+ });
+ }
+ target[Update](diff);
+ };
+} \ No newline at end of file