aboutsummaryrefslogtreecommitdiff
path: root/src/new_fields
diff options
context:
space:
mode:
Diffstat (limited to 'src/new_fields')
-rw-r--r--src/new_fields/DateField.ts6
-rw-r--r--src/new_fields/Doc.ts167
-rw-r--r--src/new_fields/List.ts5
-rw-r--r--src/new_fields/Proxy.ts4
-rw-r--r--src/new_fields/RichTextField.ts2
-rw-r--r--src/new_fields/Schema.ts10
-rw-r--r--src/new_fields/ScriptField.ts123
-rw-r--r--src/new_fields/Types.ts7
-rw-r--r--src/new_fields/URLField.ts11
-rw-r--r--src/new_fields/util.ts75
10 files changed, 378 insertions, 32 deletions
diff --git a/src/new_fields/DateField.ts b/src/new_fields/DateField.ts
index fc8abb9d9..abec91e06 100644
--- a/src/new_fields/DateField.ts
+++ b/src/new_fields/DateField.ts
@@ -2,7 +2,9 @@ import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, date } from "serializr";
import { ObjectField } from "./ObjectField";
import { Copy, ToScriptString } from "./FieldSymbols";
+import { scriptingGlobal, Scripting } from "../client/util/Scripting";
+@scriptingGlobal
@Deserializable("date")
export class DateField extends ObjectField {
@serializable(date())
@@ -21,3 +23,7 @@ export class DateField extends ObjectField {
return `new DateField(new Date(${this.date.toISOString()}))`;
}
}
+
+Scripting.addGlobal(function d(...dateArgs: any[]) {
+ return new DateField(new (Date as any)(...dateArgs));
+});
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index de4bc4d5d..59e61023f 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -2,14 +2,26 @@ 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 { setter, getter, getField, updateFunction, deleteProperty, makeEditable, makeReadOnly } from "./util";
+import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast, BoolCast, StrCast } from "./Types";
import { listSpec } from "./Schema";
import { ObjectField } from "./ObjectField";
import { RefField, FieldId } from "./RefField";
import { ToScriptString, SelfProxy, Parent, OnUpdate, Self, HandleUpdate, Update, Id } from "./FieldSymbols";
+import { scriptingGlobal } from "../client/util/Scripting";
+import { List } from "./List";
+import { ComputedField } from "./ScriptField";
export namespace Field {
+ export function toKeyValueString(doc: Doc, key: string): string {
+ const onDelegate = Object.keys(doc).includes(key);
+
+ let field = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
+ if (Field.IsField(field)) {
+ return (onDelegate ? "=" : "") + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field));
+ }
+ return "";
+ }
export function toScriptString(field: Field): string {
if (typeof field === "string") {
return `"${field}"`;
@@ -55,6 +67,7 @@ export function DocListCast(field: FieldResult): Doc[] {
export const WidthSym = Symbol("Width");
export const HeightSym = Symbol("Height");
+@scriptingGlobal
@Deserializable("doc").withFields(["id"])
export class Doc extends RefField {
constructor(id?: FieldId, forceSave?: boolean) {
@@ -156,6 +169,15 @@ export namespace Doc {
// return Cast(field, ctor);
// });
// }
+ export function MakeReadOnly(): { end(): void } {
+ makeReadOnly();
+ return {
+ end() {
+ makeEditable();
+ }
+ };
+ }
+
export function Get(doc: Doc, key: string, ignoreProto: boolean = false): FieldResult {
const self = doc[Self];
return getField(self, key, ignoreProto);
@@ -166,6 +188,14 @@ export namespace Doc {
export function IsPrototype(doc: Doc) {
return GetT(doc, "isPrototype", "boolean", true);
}
+ export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) {
+ let hasProto = doc.proto instanceof Doc;
+ let onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1;
+ let onProto = hasProto && Object.getOwnPropertyNames(doc.proto).indexOf(key) !== -1;
+ if (onDeleg || !hasProto || (!onProto && !defaultProto)) {
+ doc[key] = value;
+ } else doc.proto![key] = value;
+ }
export async function SetOnPrototype(doc: Doc, key: string, value: Field) {
const proto = Object.getOwnPropertyNames(doc).indexOf("isPrototype") === -1 ? doc.proto : doc;
@@ -182,6 +212,18 @@ export namespace Doc {
}
return protos;
}
+
+ /**
+ * This function is intended to model Object.assign({}, {}) [https://mzl.la/1Mo3l21], which copies
+ * the values of the properties of a source object into the target.
+ *
+ * This is just a specific, Dash-authored version that serves the same role for our
+ * Doc class.
+ *
+ * @param doc the target document into which you'd like to insert the new fields
+ * @param fields the fields to project onto the target. Its type signature defines a mapping from some string key
+ * to a potentially undefined field, where each entry in this mapping is optional.
+ */
export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<Field>>>) {
for (const key in fields) {
if (fields.hasOwnProperty(key)) {
@@ -222,11 +264,99 @@ export namespace Doc {
return Array.from(results);
}
+ export function AddDocToList(target: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean) {
+ if (target[key] === undefined) {
+ Doc.GetProto(target)[key] = new List<Doc>();
+ }
+ let list = Cast(target[key], listSpec(Doc));
+ if (list) {
+ if (allowDuplicates !== true) {
+ let pind = list.reduce((l, d, i) => d instanceof Doc && Doc.AreProtosEqual(d, doc) ? i : l, -1);
+ if (pind !== -1) {
+ list.splice(pind, 1);
+ }
+ }
+ if (first) list.splice(0, 0, doc);
+ else {
+ let ind = relativeTo ? list.indexOf(relativeTo) : -1;
+ if (ind === -1) list.push(doc);
+ else list.splice(before ? ind : ind + 1, 0, doc);
+ }
+ }
+ return true;
+ }
+
+ //
+ // Computes the bounds of the contents of a set of documents.
+ //
+ export function ComputeContentBounds(docList: Doc[]) {
+ let bounds = docList.reduce((bounds, doc) => {
+ var [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)];
+ let [bptX, bptY] = [sptX + doc[WidthSym](), sptY + doc[HeightSym]()];
+ return {
+ x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
+ r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
+ };
+ }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE });
+ return bounds;
+ }
+
+ //
+ // Resolves a reference to a field by returning 'doc' if field extension is specified,
+ // otherwise, it returns the extension document stored in doc.<fieldKey>_ext.
+ // This mechanism allows any fields to be extended with an extension document that can
+ // be used to capture field-specific metadata. For example, an image field can be extended
+ // to store annotations, ink, and other data.
+ //
+ export function resolvedFieldDataDoc(doc: Doc, fieldKey: string, fieldExt: string) {
+ return fieldExt && doc[fieldKey + "_ext"] instanceof Doc ? doc[fieldKey + "_ext"] as Doc : doc;
+ }
+
+ export function UpdateDocumentExtensionForField(doc: Doc, fieldKey: string) {
+ let extensionDoc = doc[fieldKey + "_ext"];
+ if (extensionDoc === undefined) {
+ setTimeout(() => {
+ let docExtensionForField = new Doc(doc[Id] + fieldKey, true);
+ docExtensionForField.title = "Extension of " + doc.title + "'s field:" + fieldKey;
+ docExtensionForField.extendsDoc = doc;
+ let proto: Doc | undefined = doc;
+ while (proto && !Doc.IsPrototype(proto)) {
+ proto = proto.proto;
+ }
+ (proto ? proto : doc)[fieldKey + "_ext"] = docExtensionForField;
+ }, 0);
+ } else if (extensionDoc instanceof Doc && extensionDoc.extendsDoc === undefined) {
+ setTimeout(() => (extensionDoc as Doc).extendsDoc = doc, 0);
+ }
+ }
export function MakeAlias(doc: Doc) {
if (!GetT(doc, "isPrototype", "boolean", true)) {
return Doc.MakeCopy(doc);
}
- return new Doc;
+ return Doc.MakeDelegate(doc); // bcz?
+ }
+
+ export function expandTemplateLayout(templateLayoutDoc: Doc, dataDoc?: Doc) {
+ let resolvedDataDoc = (templateLayoutDoc !== dataDoc) ? dataDoc : undefined;
+ if (!dataDoc || !(templateLayoutDoc && !(Cast(templateLayoutDoc.layout, Doc) instanceof Doc) && resolvedDataDoc && resolvedDataDoc !== templateLayoutDoc)) {
+ return templateLayoutDoc;
+ }
+ // if we have a data doc that doesn't match the layout, then we're rendering a template.
+ // ... which means we change the layout to be an expanded view of the template layout.
+ // This allows the view override the template's properties and be referenceable as its own document.
+
+ let expandedTemplateLayout = templateLayoutDoc["_expanded_" + dataDoc[Id]];
+ if (expandedTemplateLayout instanceof Doc) {
+ return expandedTemplateLayout;
+ }
+ if (expandedTemplateLayout === undefined && BoolCast(templateLayoutDoc.isTemplate)) {
+ setTimeout(() => {
+ templateLayoutDoc["_expanded_" + dataDoc[Id]] = Doc.MakeDelegate(templateLayoutDoc);
+ (templateLayoutDoc["_expanded_" + dataDoc[Id]] as Doc).title = templateLayoutDoc.title + " applied to " + dataDoc.title;
+ (templateLayoutDoc["_expanded_" + dataDoc[Id]] as Doc).isExpandedTemplate = templateLayoutDoc;
+ }, 0);
+ }
+ return templateLayoutDoc;
}
export function MakeCopy(doc: Doc, copyProto: boolean = false): Doc {
@@ -242,11 +372,14 @@ export namespace Doc {
copy[key] = field;
} else if (field instanceof ObjectField) {
copy[key] = ObjectField.MakeCopy(field);
+ } else if (field instanceof Promise) {
+ field.then(f => (copy[key] === undefined) && (copy[key] = f)); //TODO what should we do here?
} else {
copy[key] = field;
}
}
});
+
return copy;
}
@@ -260,4 +393,32 @@ export namespace Doc {
delegate.proto = doc;
return delegate;
}
+
+ export function MakeTemplate(fieldTemplate: Doc, metaKey: string, proto: Doc) {
+ // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)
+ let backgroundLayout = StrCast(fieldTemplate.backgroundLayout);
+ let fieldLayoutDoc = fieldTemplate;
+ if (fieldTemplate.layout instanceof Doc) {
+ fieldLayoutDoc = Doc.MakeDelegate(fieldTemplate.layout);
+ }
+ let layout = StrCast(fieldLayoutDoc.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
+ if (backgroundLayout) {
+ layout = StrCast(fieldLayoutDoc.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"} fieldExt={"annotations"}`);
+ backgroundLayout = backgroundLayout.replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
+ }
+ let nw = Cast(fieldTemplate.nativeWidth, "number");
+ let nh = Cast(fieldTemplate.nativeHeight, "number");
+
+ let layoutDelegate = fieldTemplate.layout instanceof Doc ? fieldLayoutDoc : fieldTemplate;
+ layoutDelegate.layout = layout;
+
+ fieldTemplate.title = metaKey;
+ fieldTemplate.layout = layoutDelegate !== fieldTemplate ? layoutDelegate : layout;
+ fieldTemplate.backgroundLayout = backgroundLayout;
+ fieldTemplate.nativeWidth = nw;
+ fieldTemplate.nativeHeight = nh;
+ fieldTemplate.isTemplate = true;
+ fieldTemplate.showTitle = "title";
+ setTimeout(() => fieldTemplate.proto = proto);
+ }
} \ No newline at end of file
diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts
index f1e4c4721..a2133a990 100644
--- a/src/new_fields/List.ts
+++ b/src/new_fields/List.ts
@@ -7,6 +7,7 @@ import { ObjectField } from "./ObjectField";
import { RefField } from "./RefField";
import { ProxyField } from "./Proxy";
import { Self, Update, Parent, OnUpdate, SelfProxy, ToScriptString, Copy } from "./FieldSymbols";
+import { Scripting } from "../client/util/Scripting";
const listHandlers: any = {
/// Mutator methods
@@ -294,4 +295,6 @@ class ListImpl<T extends Field> extends ObjectField {
}
}
export type List<T extends Field> = ListImpl<T> & (T | (T extends RefField ? Promise<T> : never))[];
-export const List: { new <T extends Field>(fields?: T[]): List<T> } = ListImpl as any; \ No newline at end of file
+export const List: { new <T extends Field>(fields?: T[]): List<T> } = ListImpl as any;
+
+Scripting.addGlobal("List", List); \ No newline at end of file
diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts
index 130ec066e..38d874a68 100644
--- a/src/new_fields/Proxy.ts
+++ b/src/new_fields/Proxy.ts
@@ -48,9 +48,8 @@ export class ProxyField<T extends RefField> extends ObjectField {
private failed = false;
private promise?: Promise<any>;
- value(callback?: ((field: T | undefined) => void)): T | undefined | FieldWaiting {
+ value(): T | undefined | FieldWaiting {
if (this.cache) {
- callback && callback(this.cache);
return this.cache;
}
if (this.failed) {
@@ -64,7 +63,6 @@ export class ProxyField<T extends RefField> extends ObjectField {
return field;
}));
}
- callback && this.promise.then(callback);
return this.promise;
}
}
diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts
index 89d077a47..78a3a4067 100644
--- a/src/new_fields/RichTextField.ts
+++ b/src/new_fields/RichTextField.ts
@@ -2,7 +2,9 @@ import { ObjectField } from "./ObjectField";
import { serializable } from "serializr";
import { Deserializable } from "../client/util/SerializationHelper";
import { Copy, ToScriptString } from "./FieldSymbols";
+import { scriptingGlobal } from "../client/util/Scripting";
+@scriptingGlobal
@Deserializable("RichTextField")
export class RichTextField extends ObjectField {
@serializable(true)
diff --git a/src/new_fields/Schema.ts b/src/new_fields/Schema.ts
index 40ffaecd5..b1a449e08 100644
--- a/src/new_fields/Schema.ts
+++ b/src/new_fields/Schema.ts
@@ -2,6 +2,7 @@ import { Interface, ToInterface, Cast, ToConstructor, HasTail, Head, Tail, ListS
import { Doc, Field } from "./Doc";
import { ObjectField } from "./ObjectField";
import { RefField } from "./RefField";
+import { SelfProxy } from "./FieldSymbols";
type AllToInterface<T extends Interface[]> = {
1: ToInterface<Head<T>> & AllToInterface<Tail<T>>,
@@ -56,9 +57,10 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu
}
});
const fn = (doc: Doc) => {
- if (!(doc instanceof Doc)) {
- throw new Error("Currently wrapping a schema in another schema isn't supported");
- }
+ doc = doc[SelfProxy];
+ // 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;
};
@@ -102,7 +104,7 @@ export function makeStrictInterface<T extends Interface>(schema: T): (doc: Doc)
}
export function createSchema<T extends Interface>(schema: T): T & { proto: ToConstructor<Doc> } {
- schema.proto = Doc;
+ (schema as any).proto = Doc;
return schema as any;
}
diff --git a/src/new_fields/ScriptField.ts b/src/new_fields/ScriptField.ts
new file mode 100644
index 000000000..b5b1595cf
--- /dev/null
+++ b/src/new_fields/ScriptField.ts
@@ -0,0 +1,123 @@
+import { ObjectField } from "./ObjectField";
+import { CompiledScript, CompileScript, scriptingGlobal } from "../client/util/Scripting";
+import { Copy, ToScriptString, Parent, SelfProxy } from "./FieldSymbols";
+import { serializable, createSimpleSchema, map, primitive, object, deserialize, PropSchema, custom, SKIP } from "serializr";
+import { Deserializable } from "../client/util/SerializationHelper";
+import { Doc } from "../new_fields/Doc";
+import { Plugins } from "./util";
+
+function optional(propSchema: PropSchema) {
+ return custom(value => {
+ if (value !== undefined) {
+ return propSchema.serializer(value);
+ }
+ return SKIP;
+ }, (jsonValue: any, context: any, oldValue: any, callback: (err: any, result: any) => void) => {
+ if (jsonValue !== undefined) {
+ return propSchema.deserializer(jsonValue, callback, context, oldValue);
+ }
+ return SKIP;
+ });
+}
+
+const optionsSchema = createSimpleSchema({
+ requiredType: true,
+ addReturn: true,
+ typecheck: true,
+ readonly: true,
+ params: optional(map(primitive()))
+});
+
+const scriptSchema = createSimpleSchema({
+ options: object(optionsSchema),
+ originalScript: true
+});
+
+function deserializeScript(script: ScriptField) {
+ const comp = CompileScript(script.script.originalScript, script.script.options);
+ if (!comp.compiled) {
+ throw new Error("Couldn't compile loaded script");
+ }
+ (script as any).script = comp;
+}
+
+@scriptingGlobal
+@Deserializable("script", deserializeScript)
+export class ScriptField extends ObjectField {
+ @serializable(object(scriptSchema))
+ readonly script: CompiledScript;
+
+ constructor(script: CompiledScript) {
+ super();
+
+ this.script = script;
+ }
+
+ // init(callback: (res: Field) => any) {
+ // const options = this.options!;
+ // const keys = Object.keys(options.options.capturedIds);
+ // Server.GetFields(keys).then(fields => {
+ // let captured: { [name: string]: Field } = {};
+ // keys.forEach(key => captured[options.options.capturedIds[key]] = fields[key]);
+ // const opts: ScriptOptions = {
+ // addReturn: options.options.addReturn,
+ // params: options.options.params,
+ // requiredType: options.options.requiredType,
+ // capturedVariables: captured
+ // };
+ // const script = CompileScript(options.script, opts);
+ // if (!script.compiled) {
+ // throw new Error("Can't compile script");
+ // }
+ // this._script = script;
+ // callback(this);
+ // });
+ // }
+
+ [Copy](): ObjectField {
+ return new ScriptField(this.script);
+ }
+
+ [ToScriptString]() {
+ return "script field";
+ }
+}
+
+@scriptingGlobal
+@Deserializable("computed", deserializeScript)
+export class ComputedField extends ScriptField {
+ //TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc
+ value(doc: Doc) {
+ const val = this.script.run({ this: doc });
+ if (val.success) {
+ return val.result;
+ }
+ return undefined;
+ }
+}
+
+export namespace ComputedField {
+ let useComputed = true;
+ export function DisableComputedFields() {
+ useComputed = false;
+ }
+
+ export function EnableComputedFields() {
+ useComputed = true;
+ }
+
+ export function WithoutComputed<T>(fn: () => T) {
+ DisableComputedFields();
+ try {
+ return fn();
+ } finally {
+ EnableComputedFields();
+ }
+ }
+
+ Plugins.addGetterPlugin((doc, _, value) => {
+ if (useComputed && value instanceof ComputedField) {
+ return { value: value.value(doc), shouldReturn: true };
+ }
+ });
+} \ No newline at end of file
diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts
index 8dd893aa4..f8a4a30b4 100644
--- a/src/new_fields/Types.ts
+++ b/src/new_fields/Types.ts
@@ -1,6 +1,7 @@
import { Field, Opt, FieldResult, Doc } from "./Doc";
import { List } from "./List";
import { RefField } from "./RefField";
+import { DateField } from "./DateField";
export type ToType<T extends InterfaceValue> =
T extends "string" ? string :
@@ -45,9 +46,10 @@ export interface Interface {
[key: string]: InterfaceValue;
// [key: string]: ToConstructor<Field> | ListSpec<Field[]>;
}
+export type WithoutRefField<T extends Field> = T extends RefField ? never : T;
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: WithoutList<WithoutRefField<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;
@@ -79,6 +81,9 @@ export function StrCast(field: FieldResult, defaultVal: string | null = "") {
export function BoolCast(field: FieldResult, defaultVal: boolean | null = null) {
return Cast(field, "boolean", defaultVal);
}
+export function DateCast(field: FieldResult) {
+ return Cast(field, DateField, null);
+}
type WithoutList<T extends Field> = T extends List<infer R> ? (R extends RefField ? (R | Promise<R>)[] : R[]) : T;
diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts
index 4a2841fb6..d935a61af 100644
--- a/src/new_fields/URLField.ts
+++ b/src/new_fields/URLField.ts
@@ -2,6 +2,7 @@ import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, custom } from "serializr";
import { ObjectField } from "./ObjectField";
import { ToScriptString, Copy } from "./FieldSymbols";
+import { Scripting, scriptingGlobal } from "../client/util/Scripting";
function url() {
return custom(
@@ -37,8 +38,8 @@ export abstract class URLField extends ObjectField {
}
}
-@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
+@scriptingGlobal @Deserializable("audio") export class AudioField extends URLField { }
+@scriptingGlobal @Deserializable("image") export class ImageField extends URLField { }
+@scriptingGlobal @Deserializable("video") export class VideoField extends URLField { }
+@scriptingGlobal @Deserializable("pdf") export class PdfField extends URLField { }
+@scriptingGlobal @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
index 8cb1db953..b59ec9b9a 100644
--- a/src/new_fields/util.ts
+++ b/src/new_fields/util.ts
@@ -1,15 +1,32 @@
import { UndoManager } from "../client/util/UndoManager";
-import { Doc, Field } from "./Doc";
+import { Doc, Field, FieldResult } from "./Doc";
import { SerializationHelper } from "../client/util/SerializationHelper";
import { ProxyField } from "./Proxy";
-import { FieldValue } from "./Types";
import { RefField } from "./RefField";
import { ObjectField } from "./ObjectField";
import { action } from "mobx";
-import { Parent, OnUpdate, Update, Id, SelfProxy } from "./FieldSymbols";
-import { ComputedField } from "../fields/ScriptField";
+import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols";
+import { ComputedField } from "./ScriptField";
-export const setter = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
+function _readOnlySetter(): never {
+ throw new Error("Documents can't be modified in read-only mode");
+}
+
+export interface GetterResult {
+ value: FieldResult;
+ shouldReturn: boolean;
+}
+export type GetterPlugin = (receiver: any, prop: string | number, currentValue: any) => GetterResult | undefined;
+const getterPlugins: GetterPlugin[] = [];
+
+export namespace Plugins {
+ export function addGetterPlugin(plugin: GetterPlugin) {
+ getterPlugins.push(plugin);
+ }
+}
+
+const _setterImpl = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
+ //console.log("-set " + target[SelfProxy].title + "(" + target[SelfProxy][prop] + ")." + prop.toString() + " = " + value);
if (SerializationHelper.IsSerializing()) {
target[prop] = value;
return true;
@@ -18,6 +35,9 @@ export const setter = action(function (target: any, prop: string | symbol | numb
target[prop] = value;
return true;
}
+ if (value !== undefined) {
+ value = value[SelfProxy] || value;
+ }
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
@@ -28,11 +48,10 @@ export const setter = action(function (target: any, prop: string | symbol | numb
value = new ProxyField(value);
}
if (value instanceof ObjectField) {
- //TODO Instead of target, maybe use target[Self]
- if (value[Parent] && value[Parent] !== target) {
+ if (value[Parent] && value[Parent] !== receiver) {
throw new Error("Can't put the same object in multiple documents at the same time");
}
- value[Parent] = target;
+ value[Parent] = receiver;
value[OnUpdate] = updateFunction(target, prop, value, receiver);
}
if (curValue instanceof ObjectField) {
@@ -45,6 +64,7 @@ export const setter = action(function (target: any, prop: string | symbol | numb
target.__fields[prop] = value;
}
if (value === undefined) target[Update]({ '$unset': { ["fields." + prop]: "" } });
+ if (typeof value === "object" && !(value instanceof ObjectField)) debugger;
else target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } });
UndoManager.AddEvent({
redo: () => receiver[prop] = value,
@@ -53,6 +73,20 @@ export const setter = action(function (target: any, prop: string | symbol | numb
return true;
});
+let _setter: (target: any, prop: string | symbol | number, value: any, receiver: any) => boolean = _setterImpl;
+
+export function makeReadOnly() {
+ _setter = _readOnlySetter;
+}
+
+export function makeEditable() {
+ _setter = _setterImpl;
+}
+
+export function setter(target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
+ return _setter(target, prop, value, receiver);
+}
+
export function getter(target: any, prop: string | symbol | number, receiver: any): any {
if (typeof prop === "symbol") {
return target.__fields[prop] || target[prop];
@@ -60,25 +94,36 @@ export function getter(target: any, prop: string | symbol | number, receiver: an
if (SerializationHelper.IsSerializing()) {
return target[prop];
}
- return getField(target, prop);
+ return getFieldImpl(target, prop, receiver);
}
-export function getField(target: any, prop: string | number, ignoreProto: boolean = false): any {
- const field = target.__fields[prop];
+function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreProto: boolean = false): any {
+ receiver = receiver || target[SelfProxy];
+ let field = target.__fields[prop];
if (field instanceof ProxyField) {
return field.value();
}
- if (field instanceof ComputedField) {
- return field.value;
+ for (const plugin of getterPlugins) {
+ const res = plugin(receiver, prop, field);
+ if (res === undefined) continue;
+ if (res.shouldReturn) {
+ return res.value;
+ } else {
+ field = res.value;
+ }
}
if (field === undefined && !ignoreProto && prop !== "proto") {
- const proto = getField(target, "proto", true);
+ const proto = getFieldImpl(target, "proto", receiver, true);//TODO tfs: instead of receiver we could use target[SelfProxy]... I don't which semantics we want or if it really matters
if (proto instanceof Doc) {
- return proto[prop];
+ return getFieldImpl(proto[Self], prop, receiver, ignoreProto);
}
return undefined;
}
return field;
+
+}
+export function getField(target: any, prop: string | number, ignoreProto: boolean = false): any {
+ return getFieldImpl(target, prop, undefined, ignoreProto);
}
export function deleteProperty(target: any, prop: string | number | symbol) {