diff options
Diffstat (limited to 'src/fields')
| -rw-r--r-- | src/fields/Doc.ts | 942 | ||||
| -rw-r--r-- | src/fields/RichTextUtils.ts | 230 | ||||
| -rw-r--r-- | src/fields/ScriptField.ts | 176 | ||||
| -rw-r--r-- | src/fields/URLField.ts | 69 | ||||
| -rw-r--r-- | src/fields/util.ts | 303 |
5 files changed, 990 insertions, 730 deletions
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index b30ca644d..0c7504913 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -1,59 +1,54 @@ -import { IconProp } from "@fortawesome/fontawesome-svg-core"; -import { saveAs } from "file-saver"; -import { action, computed, observable, ObservableMap, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; -import { alias, map, serializable } from "serializr"; -import { DocServer } from "../client/DocServer"; -import { DocumentType } from "../client/documents/DocumentTypes"; -import { CurrentUserUtils } from "../client/util/CurrentUserUtils"; -import { LinkManager } from "../client/util/LinkManager"; -import { scriptingGlobal, ScriptingGlobals } from "../client/util/ScriptingGlobals"; -import { SelectionManager } from "../client/util/SelectionManager"; -import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from "../client/util/SerializationHelper"; -import { UndoManager } from "../client/util/UndoManager"; -import { DashColor, incrementTitleCopy, intersectRect, Utils } from "../Utils"; -import { DateField } from "./DateField"; -import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from "./FieldSymbols"; -import { List } from "./List"; -import { ObjectField } from "./ObjectField"; -import { PrefetchProxy, ProxyField } from "./Proxy"; -import { FieldId, RefField } from "./RefField"; -import { RichTextField } from "./RichTextField"; -import { listSpec } from "./Schema"; -import { ComputedField, ScriptField } from "./ScriptField"; -import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types"; -import { AudioField, ImageField, MapField, PdfField, VideoField, WebField } from "./URLField"; -import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from "./util"; -import JSZip = require("jszip"); +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { saveAs } from 'file-saver'; +import { action, computed, observable, ObservableMap, runInAction } from 'mobx'; +import { computedFn } from 'mobx-utils'; +import { alias, map, serializable } from 'serializr'; +import { DocServer } from '../client/DocServer'; +import { DocumentType } from '../client/documents/DocumentTypes'; +import { LinkManager } from '../client/util/LinkManager'; +import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; +import { SelectionManager } from '../client/util/SelectionManager'; +import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from '../client/util/SerializationHelper'; +import { UndoManager } from '../client/util/UndoManager'; +import { DashColor, incrementTitleCopy, intersectRect, Utils } from '../Utils'; +import { DateField } from './DateField'; +import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from './FieldSymbols'; +import { InkTool } from './InkField'; +import { List } from './List'; +import { ObjectField } from './ObjectField'; +import { PrefetchProxy, ProxyField } from './Proxy'; +import { FieldId, RefField } from './RefField'; +import { RichTextField } from './RichTextField'; +import { listSpec } from './Schema'; +import { ComputedField, ScriptField } from './ScriptField'; +import { Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor } from './Types'; +import { AudioField, ImageField, MapField, PdfField, VideoField, WebField } from './URLField'; +import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from './util'; +import JSZip = require('jszip'); export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { const onDelegate = Object.keys(doc).includes(key); const field = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - return !Field.IsField(field) ? "" : (onDelegate ? "=" : "") + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field)); + return !Field.IsField(field) ? '' : (onDelegate ? '=' : '') + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field)); } export function toScriptString(field: Field): string { - if (typeof field === "string") return `"${field}"`; - if (typeof field === "number" || typeof field === "boolean") return String(field); - if (field === undefined || field === null) return "null"; + if (typeof field === 'string') return `"${field}"`; + if (typeof field === 'number' || typeof field === 'boolean') return String(field); + if (field === undefined || field === null) return 'null'; return field[ToScriptString](); } export function toString(field: Field): string { - if (typeof field === "string") return field; - if (typeof field === "number" || typeof field === "boolean") return String(field); + if (typeof field === 'string') return field; + if (typeof field === 'number' || typeof field === 'boolean') return String(field); if (field instanceof ObjectField) return field[ToString](); if (field instanceof RefField) return field[ToString](); - return ""; + return ''; } export function IsField(field: any): field is Field; export function IsField(field: any, includeUndefined: true): field is Field | undefined; export function IsField(field: any, includeUndefined: boolean = false): field is Field | undefined { - return (typeof field === "string") - || (typeof field === "number") - || (typeof field === "boolean") - || (field instanceof ObjectField) - || (field instanceof RefField) - || (includeUndefined && field === undefined); + return typeof field === 'string' || typeof field === 'number' || typeof field === 'boolean' || field instanceof ObjectField || field instanceof RefField || (includeUndefined && field === undefined); } export function Copy(field: any) { return field instanceof ObjectField ? ObjectField.MakeCopy(field) : field; @@ -77,40 +72,50 @@ export function DocListCastAsync(field: FieldResult, defaultValue?: Doc[]) { return list ? Promise.all(list).then(() => list) : Promise.resolve(defaultValue); } -export async function DocCastAsync(field: FieldResult): Promise<Opt<Doc>> { return Cast(field, Doc); } - -export function NumListCast(field: FieldResult) { return Cast(field, listSpec("number"), []); } -export function StrListCast(field: FieldResult) { return Cast(field, listSpec("string"), []); } -export function DocListCast(field: FieldResult) { return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[]; } -export function DocListCastOrNull(field: FieldResult) { return Cast(field, listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[] | undefined; } - -export const WidthSym = Symbol("Width"); -export const HeightSym = Symbol("Height"); -export const DataSym = Symbol("Data"); -export const LayoutSym = Symbol("Layout"); -export const FieldsSym = Symbol("Fields"); -export const AclSym = Symbol("Acl"); -export const DirectLinksSym = Symbol("DirectLinks"); -export const AclUnset = Symbol("AclUnset"); -export const AclPrivate = Symbol("AclOwnerOnly"); -export const AclReadonly = Symbol("AclReadOnly"); -export const AclAugment = Symbol("AclAugment"); -export const AclSelfEdit = Symbol("AclSelfEdit"); -export const AclEdit = Symbol("AclEdit"); -export const AclAdmin = Symbol("AclAdmin"); -export const UpdatingFromServer = Symbol("UpdatingFromServer"); -export const Initializing = Symbol("Initializing"); -export const ForceServerWrite = Symbol("ForceServerWrite"); -export const CachedUpdates = Symbol("Cached updates"); +export async function DocCastAsync(field: FieldResult): Promise<Opt<Doc>> { + return Cast(field, Doc); +} + +export function NumListCast(field: FieldResult) { + return Cast(field, listSpec('number'), []); +} +export function StrListCast(field: FieldResult) { + return Cast(field, listSpec('string'), []); +} +export function DocListCast(field: FieldResult) { + return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[]; +} +export function DocListCastOrNull(field: FieldResult) { + return Cast(field, listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[] | undefined; +} + +export const WidthSym = Symbol('Width'); +export const HeightSym = Symbol('Height'); +export const DataSym = Symbol('Data'); +export const LayoutSym = Symbol('Layout'); +export const FieldsSym = Symbol('Fields'); +export const AclSym = Symbol('Acl'); +export const DirectLinksSym = Symbol('DirectLinks'); +export const AclUnset = Symbol('AclUnset'); +export const AclPrivate = Symbol('AclOwnerOnly'); +export const AclReadonly = Symbol('AclReadOnly'); +export const AclAugment = Symbol('AclAugment'); +export const AclSelfEdit = Symbol('AclSelfEdit'); +export const AclEdit = Symbol('AclEdit'); +export const AclAdmin = Symbol('AclAdmin'); +export const UpdatingFromServer = Symbol('UpdatingFromServer'); +export const Initializing = Symbol('Initializing'); +export const ForceServerWrite = Symbol('ForceServerWrite'); +export const CachedUpdates = Symbol('Cached updates'); const AclMap = new Map<string, symbol>([ - ["None", AclUnset], + ['None', AclUnset], [SharingPermissions.None, AclPrivate], [SharingPermissions.View, AclReadonly], [SharingPermissions.Augment, AclAugment], [SharingPermissions.SelfEdit, AclSelfEdit], [SharingPermissions.Edit, AclEdit], - [SharingPermissions.Admin, AclAdmin] + [SharingPermissions.Admin, AclAdmin], ]); // caches the document access permissions for the current user. @@ -120,7 +125,7 @@ export function updateCachedAcls(doc: Doc) { const permissions: { [key: string]: symbol } = {}; doc[UpdatingFromServer] = true; - Object.keys(doc).filter(key => key.startsWith("acl") && (permissions[key] = AclMap.get(StrCast(doc[key]))!)); + Object.keys(doc).filter(key => key.startsWith('acl') && (permissions[key] = AclMap.get(StrCast(doc[key]))!)); doc[UpdatingFromServer] = false; if (Object.keys(permissions).length) { @@ -134,8 +139,97 @@ export function updateCachedAcls(doc: Doc) { } @scriptingGlobal -@Deserializable("Doc", updateCachedAcls).withFields(["id"]) +@Deserializable('Doc', updateCachedAcls).withFields(['id']) export class Doc extends RefField { + //TODO tfs: these should be temporary... + private static mainDocId: string | undefined; + public static get MainDocId() { + return this.mainDocId; + } + public static set MainDocId(id: string | undefined) { + this.mainDocId = id; + } + @observable public static GuestDashboard: Doc | undefined; + @observable public static GuestTarget: Doc | undefined; + @observable public static GuestMobile: Doc | undefined; + public static get MySharedDocs() { + return DocCast(Doc.UserDoc().mySharedDocs); + } + public static get MyUserDocView() { + return DocCast(Doc.UserDoc().myUserDocView); + } + public static get MyDockedBtns() { + return DocCast(Doc.UserDoc().myDockedBtns); + } + public static get MySearcher() { + return DocCast(Doc.UserDoc().mySearcher); + } + public static get MyHeaderBar() { + return DocCast(Doc.UserDoc().myHeaderBar); + } + public static get MyLeftSidebarMenu() { + return DocCast(Doc.UserDoc().myLeftSidebarMenu); + } + public static get MyLeftSidebarPanel() { + return DocCast(Doc.UserDoc().myLeftSidebarPanel); + } + public static get MyContextMenuBtns() { + return DocCast(Doc.UserDoc().myContextMenuBtns); + } + public static get MyRecentlyClosed() { + return DocCast(Doc.UserDoc().myRecentlyClosed); + } + public static get MyTrails() { + return DocCast(Doc.UserDoc().myTrails); + } + public static get MyOverlayDocs() { + return DocCast(Doc.UserDoc().myOverlayDocs); + } + public static get MyPublishedDocs() { + return DocCast(Doc.UserDoc().myPublishedDocs); + } + public static get MyDashboards() { + return DocCast(Doc.UserDoc().myDashboards); + } + public static get MyTemplates() { + return DocCast(Doc.UserDoc().myTemplates); + } + public static get MyImports() { + return DocCast(Doc.UserDoc().myImports); + } + public static get MyFilesystem() { + return DocCast(Doc.UserDoc().myFilesystem); + } + public static get MyFileOrphans() { + return DocCast(Doc.UserDoc().myFileOrphans); + } + public static get MyTools() { + return DocCast(Doc.UserDoc().myTools); + } + public static get ActivePage() { + return StrCast(Doc.UserDoc().activePage); + } + public static set ActivePage(val) { + Doc.UserDoc().activePage = val; + } + public static get ActiveDashboard() { + return DocCast(Doc.UserDoc().activeDashboard); + } + public static set ActiveDashboard(val: Doc | undefined) { + Doc.UserDoc().activeDashboard = val; + } + public static set ActiveTool(tool: InkTool) { + Doc.UserDoc().activeTool = tool; + } + public static get ActiveTool(): InkTool { + return StrCast(Doc.UserDoc().activeTool, InkTool.None) as InkTool; + } + public static get ActivePresentation() { + return DocCast(Doc.UserDoc().activePresentation); + } + public static set ActivePresentation(val) { + Doc.UserDoc().activePresentation = val; + } constructor(id?: FieldId, forceSave?: boolean) { super(id); const doc = new Proxy<this>(this, { @@ -146,24 +240,26 @@ export class Doc extends RefField { ownKeys: target => { const obj = {} as any; if (GetEffectiveAcl(target) !== AclPrivate) Object.assign(obj, target.___fieldKeys); - runInAction(() => obj.__LAYOUT__ = target.__LAYOUT__); + runInAction(() => (obj.__LAYOUT__ = target.__LAYOUT__)); return Object.keys(obj); }, getOwnPropertyDescriptor: (target, prop) => { - if (prop.toString() === "__LAYOUT__") { + if (prop.toString() === '__LAYOUT__') { return Reflect.getOwnPropertyDescriptor(target, prop); } if (prop in target.__fieldKeys) { return { - configurable: true,//TODO Should configurable be true? + configurable: true, //TODO Should configurable be true? enumerable: true, - value: 0//() => target.__fields[prop]) + value: 0, //() => target.__fields[prop]) }; } return Reflect.getOwnPropertyDescriptor(target, prop); }, deleteProperty: deleteProperty, - defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); }, + defineProperty: () => { + throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); + }, }); this[SelfProxy] = doc; if (!id || forceSave) { @@ -175,24 +271,30 @@ export class Doc extends RefField { proto: Opt<Doc>; [key: string]: FieldResult; - @serializable(alias("fields", map(autoObject(), { afterDeserialize: afterDocDeserialize }))) - private get __fields() { return this.___fields; } + @serializable(alias('fields', map(autoObject(), { afterDeserialize: afterDocDeserialize }))) + private get __fields() { + return this.___fields; + } private set __fields(value) { this.___fields = value; for (const key in value) { const field = value[key]; - (field !== undefined) && (this.__fieldKeys[key] = true); + field !== undefined && (this.__fieldKeys[key] = true); if (!(field instanceof ObjectField)) continue; field[Parent] = this[Self]; field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]); } } - private get __fieldKeys() { return this.___fieldKeys; } - private set __fieldKeys(value) { this.___fieldKeys = value; } + private get __fieldKeys() { + return this.___fieldKeys; + } + private set __fieldKeys(value) { + this.___fieldKeys = value; + } @observable private ___fields: any = {}; @observable private ___fieldKeys: any = {}; - @observable public [AclSym]: { [key: string]: symbol }; + @observable public [AclSym]: { [key: string]: symbol } = {}; @observable public [DirectLinksSym]: Set<Doc> = new Set(); private [UpdatingFromServer]: boolean = false; @@ -201,7 +303,7 @@ export class Doc extends RefField { private [Update] = (diff: any) => { (!this[UpdatingFromServer] || this[ForceServerWrite]) && DocServer.UpdateField(this[Id], diff); - } + }; private [Self] = this; private [SelfProxy]: any; @@ -209,42 +311,52 @@ export class Doc extends RefField { public [WidthSym] = () => NumCast(this[SelfProxy]._width); public [HeightSym] = () => NumCast(this[SelfProxy]._height); public [ToScriptString] = () => `idToDoc("${this[Self][Id]}")`; - public [ToString] = () => `Doc(${GetEffectiveAcl(this[SelfProxy]) === AclPrivate ? "-inaccessible-" : this[SelfProxy].title})`; - public get [LayoutSym]() { return this[SelfProxy].__LAYOUT__; } + public [ToString] = () => `Doc(${GetEffectiveAcl(this[SelfProxy]) === AclPrivate ? '-inaccessible-' : this[SelfProxy].title})`; + public get [LayoutSym]() { + return this[SelfProxy].__LAYOUT__; + } public get [DataSym]() { const self = this[SelfProxy]; - return self.resolvedDataDoc && !self.isTemplateForField ? self : - Doc.GetProto(Cast(Doc.Layout(self).resolvedDataDoc, Doc, null) || self); + return self.resolvedDataDoc && !self.isTemplateForField ? self : Doc.GetProto(Cast(Doc.Layout(self).resolvedDataDoc, Doc, null) || self); } @computed get __LAYOUT__(): Doc | undefined { const templateLayoutDoc = Cast(Doc.LayoutField(this[SelfProxy]), Doc, null); if (templateLayoutDoc) { let renderFieldKey: any; - const layoutField = templateLayoutDoc[StrCast(templateLayoutDoc.layoutKey, "layout")]; - if (typeof layoutField === "string") { - renderFieldKey = layoutField.split("fieldKey={'")[1].split("'")[0];//layoutField.split("'")[1]; + const layoutField = templateLayoutDoc[StrCast(templateLayoutDoc.layoutKey, 'layout')]; + if (typeof layoutField === 'string') { + renderFieldKey = layoutField.split("fieldKey={'")[1].split("'")[0]; //layoutField.split("'")[1]; } else { return Cast(layoutField, Doc, null); } - return Cast(this[SelfProxy][renderFieldKey + "-layout[" + templateLayoutDoc[Id] + "]"], Doc, null) || templateLayoutDoc; + return Cast(this[SelfProxy][renderFieldKey + '-layout[' + templateLayoutDoc[Id] + ']'], Doc, null) || templateLayoutDoc; } return undefined; - } private [CachedUpdates]: { [key: string]: () => void | Promise<any> } = {}; - public static get noviceMode() { return Doc.UserDoc().noviceMode as boolean; } - public static set noviceMode(val) { Doc.UserDoc().noviceMode = val; } - public static get defaultAclPrivate() { return Doc.UserDoc().defaultAclPrivate; } - public static set defaultAclPrivate(val) { Doc.UserDoc().defaultAclPrivate = val; } - public static CurrentUserEmail: string = ""; - public static get CurrentUserEmailNormalized() { return normalizeEmail(Doc.CurrentUserEmail); } + public static get noviceMode() { + return Doc.UserDoc().noviceMode as boolean; + } + public static set noviceMode(val) { + Doc.UserDoc().noviceMode = val; + } + public static get defaultAclPrivate() { + return Doc.UserDoc().defaultAclPrivate; + } + public static set defaultAclPrivate(val) { + Doc.UserDoc().defaultAclPrivate = val; + } + public static CurrentUserEmail: string = ''; + public static get CurrentUserEmailNormalized() { + return normalizeEmail(Doc.CurrentUserEmail); + } public async [HandleUpdate](diff: any) { const set = diff.$set; const sameAuthor = this.author === Doc.CurrentUserEmail; if (set) { for (const key in set) { - const fprefix = "fields."; + const fprefix = 'fields.'; if (!key.startsWith(fprefix)) { continue; } @@ -255,7 +367,7 @@ export class Doc extends RefField { this[UpdatingFromServer] = true; this[fKey] = value; this[UpdatingFromServer] = false; - if (fKey.startsWith("acl")) { + if (fKey.startsWith('acl')) { updateCachedAcls(this); } if (prev === AclPrivate && GetEffectiveAcl(this) !== AclPrivate) { @@ -263,7 +375,7 @@ export class Doc extends RefField { } }; const writeMode = DocServer.getFieldWriteMode(fKey); - if (fKey.startsWith("acl") || writeMode !== DocServer.WriteMode.Playground) { + if (fKey.startsWith('acl') || writeMode !== DocServer.WriteMode.Playground) { delete this[CachedUpdates][fKey]; await fn(); } else { @@ -274,7 +386,7 @@ export class Doc extends RefField { const unset = diff.$unset; if (unset) { for (const key in unset) { - if (!key.startsWith("fields.")) { + if (!key.startsWith('fields.')) { continue; } const fKey = key.substring(7); @@ -326,7 +438,7 @@ export namespace Doc { return { end() { makeEditable(); - } + }, }; } @@ -341,16 +453,16 @@ export namespace Doc { return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult<T>; } export function IsPrototype(doc: Doc) { - return GetT(doc, "isPrototype", "boolean", true); + return GetT(doc, 'isPrototype', 'boolean', true); } export function IsBaseProto(doc: Doc) { - return GetT(doc, "baseProto", "boolean", true); + return GetT(doc, 'baseProto', 'boolean', true); } export function IsSystem(doc: Doc) { - return GetT(doc, "system", "boolean", true); + return GetT(doc, 'system', 'boolean', true); } export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) { - if (key.startsWith("_")) key = key.substring(1); + if (key.startsWith('_')) key = key.substring(1); const hasProto = doc.proto instanceof Doc; const onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1; const onProto = hasProto && Object.getOwnPropertyNames(doc.proto).indexOf(key) !== -1; @@ -359,7 +471,7 @@ export namespace Doc { } 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; + const proto = Object.getOwnPropertyNames(doc).indexOf('isPrototype') === -1 ? doc.proto : doc; if (proto) { proto[key] = value; @@ -391,7 +503,8 @@ export namespace Doc { for (const key in fields) { if (fields.hasOwnProperty(key)) { const value = fields[key]; - if (!skipUndefineds || value !== undefined) { // Do we want to filter out undefineds? + if (!skipUndefineds || value !== undefined) { + // Do we want to filter out undefineds? doc[key] = value; } } @@ -403,10 +516,10 @@ export namespace Doc { // compare whether documents or their protos match export function AreProtosEqual(doc?: Doc, other?: Doc) { if (!doc || !other) return false; - const r = (doc === other); - const r2 = (Doc.GetProto(doc) === other); - const r3 = (Doc.GetProto(other) === doc); - const r4 = (Doc.GetProto(doc) === Doc.GetProto(other) && Doc.GetProto(other) !== undefined); + const r = doc === other; + const r2 = Doc.GetProto(doc) === other; + const r3 = Doc.GetProto(other) === doc; + const r4 = Doc.GetProto(doc) === Doc.GetProto(other) && Doc.GetProto(other) !== undefined; return r || r2 || r3 || r4; } @@ -417,7 +530,7 @@ export namespace Doc { if (doc instanceof Promise) { // console.log("GetProto: warning: got Promise insead of Doc"); } - const proto = doc && (Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc)); + const proto = doc && (Doc.GetT(doc, 'isPrototype', 'boolean', true) ? doc : doc.proto || doc); return proto === doc ? proto : Doc.GetProto(proto); } export function GetDataDoc(doc: Doc): Doc { @@ -426,7 +539,7 @@ export namespace Doc { } export function allKeys(doc: Doc): string[] { - const results: Set<string> = new Set; + const results: Set<string> = new Set(); let proto: Doc | undefined = doc; while (proto) { @@ -441,8 +554,8 @@ export namespace Doc { * @returns the index of doc toFind in list of docs, -1 otherwise */ export function IndexOf(toFind: Doc, list: Doc[], allowProtos: boolean = true) { - let index = list.reduce((p, v, i) => (v instanceof Doc && v === toFind) ? i : p, -1); - index = allowProtos && index !== -1 ? index : list.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, toFind)) ? i : p, -1); + let index = list.reduce((p, v, i) => (v instanceof Doc && v === toFind ? i : p), -1); + index = allowProtos && index !== -1 ? index : list.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, toFind) ? i : p), -1); return index; // list.findIndex(doc => doc === toFind || Doc.AreProtosEqual(doc, toFind)); } @@ -478,7 +591,7 @@ export namespace Doc { const list = Cast(listDoc[key], listSpec(Doc)); if (list) { if (allowDuplicates !== true) { - const pind = list.reduce((l, d, i) => d instanceof Doc && d[Id] === doc[Id] ? i : l, -1); + const pind = list.reduce((l, d, i) => (d instanceof Doc && d[Id] === doc[Id] ? i : l), -1); if (pind !== -1) { return true; //list.splice(pind, 1); // bcz: this causes schemaView docs in the Catalog to move to the bottom of the schema view when they are dragged even though they haven't left the collection @@ -486,15 +599,13 @@ export namespace Doc { } if (first) { list.splice(0, 0, doc); - } - else { + } else { const ind = relativeTo ? list.indexOf(relativeTo) : -1; if (ind === -1) { if (reversed) list.splice(0, 0, doc); else list.push(doc); - } - else { - if (reversed) list.splice(before ? (list.length - ind) + 1 : list.length - ind, 0, doc); + } else { + if (reversed) list.splice(before ? list.length - ind + 1 : list.length - ind, 0, doc); else list.splice(before ? ind : ind + 1, 0, doc); } } @@ -507,19 +618,24 @@ export namespace Doc { * Computes the bounds of the contents of a set of documents. */ export function ComputeContentBounds(docList: Doc[]) { - const bounds = docList.reduce((bounds, doc) => { - const [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)]; - const [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.MAX_VALUE, b: -Number.MAX_VALUE }); + const bounds = docList.reduce( + (bounds, doc) => { + const [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)]; + const [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.MAX_VALUE, b: -Number.MAX_VALUE } + ); return bounds; } export function MakeAlias(doc: Doc, id?: string) { - const alias = !GetT(doc, "isPrototype", "boolean", true) && doc.proto ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id); + const alias = !GetT(doc, 'isPrototype', 'boolean', true) && doc.proto ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id); const layout = Doc.LayoutField(alias); if (layout instanceof Doc && layout !== alias && layout === Doc.Layout(alias)) { Doc.SetLayout(alias, Doc.MakeAlias(layout)); @@ -529,71 +645,73 @@ export namespace Doc { alias.title = ComputedField.MakeFunction(`renameAlias(this)`); alias.author = Doc.CurrentUserEmail; - Doc.AddDocToList(Doc.GetProto(doc)[DataSym], "aliases", alias); + Doc.AddDocToList(Doc.GetProto(doc)[DataSym], 'aliases', alias); return alias; } - export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<Doc, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[], exclusions: string[], dontCreate: boolean, asBranch: boolean): Promise<Doc> { + export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<Doc, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], dontCreate: boolean, asBranch: boolean): Promise<Doc> { if (Doc.IsBaseProto(doc)) return doc; if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!; - const copy = dontCreate ? asBranch ? (Cast(doc.branchMaster, Doc, null) || doc) : doc : new Doc(undefined, true); + const copy = dontCreate ? (asBranch ? Cast(doc.branchMaster, Doc, null) || doc : doc) : new Doc(undefined, true); cloneMap.set(doc[Id], copy); - const fieldExclusions = doc.type === DocumentType.MARKER ? exclusions.filter(ex => ex !== "annotationOn") : exclusions; - const filter = [...fieldExclusions, ...Cast(doc.cloneFieldFilter, listSpec("string"), [])]; - await Promise.all(Object.keys(doc).map(async key => { - if (filter.includes(key)) return; - const assignKey = (val: any) => !dontCreate && (copy[key] = val); - const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - const field = ProxyField.WithoutProxy(() => doc[key]); - const copyObjectField = async (field: ObjectField) => { - const list = await Cast(doc[key], listSpec(Doc)); - const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc); - if (docs !== undefined && docs.length) { - const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch))); - !dontCreate && assignKey(new List<Doc>(clones)); - } else if (doc[key] instanceof Doc) { - assignKey(key.includes("layout[") ? undefined : key.startsWith("layout") ? doc[key] as Doc : await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)); // reference documents except copy documents that are expanded template fields - } else { - !dontCreate && assignKey(ObjectField.MakeCopy(field)); - if (field instanceof RichTextField) { - if (field.Data.includes('"audioId":') || field.Data.includes('"textId":') || field.Data.includes('"anchorId":')) { - rtfs.push({ copy, key, field }); + const fieldExclusions = doc.type === DocumentType.MARKER ? exclusions.filter(ex => ex !== 'annotationOn') : exclusions; + const filter = [...fieldExclusions, ...Cast(doc.cloneFieldFilter, listSpec('string'), [])]; + await Promise.all( + Object.keys(doc).map(async key => { + if (filter.includes(key)) return; + const assignKey = (val: any) => !dontCreate && (copy[key] = val); + const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); + const field = ProxyField.WithoutProxy(() => doc[key]); + const copyObjectField = async (field: ObjectField) => { + const list = await Cast(doc[key], listSpec(Doc)); + const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc); + if (docs !== undefined && docs.length) { + const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch))); + !dontCreate && assignKey(new List<Doc>(clones)); + } else if (doc[key] instanceof Doc) { + assignKey(key.includes('layout[') ? undefined : key.startsWith('layout') ? (doc[key] as Doc) : await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)); // reference documents except copy documents that are expanded template fields + } else { + !dontCreate && assignKey(ObjectField.MakeCopy(field)); + if (field instanceof RichTextField) { + if (field.Data.includes('"audioId":') || field.Data.includes('"textId":') || field.Data.includes('"anchorId":')) { + rtfs.push({ copy, key, field }); + } } } - } - }; - if (key === "proto") { - if (doc[key] instanceof Doc) { - assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)); - } - } else if (key === "anchor1" || key === "anchor2") { - if (doc[key] instanceof Doc) { - assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, true, asBranch)); - } - } else { - if (field instanceof RefField) { - assignKey(field); - } else if (cfield instanceof ComputedField) { - !dontCreate && assignKey(ComputedField.MakeFunction(cfield.script.originalScript)); - } else if (field instanceof ObjectField) { - await copyObjectField(field); - } else if (field instanceof Promise) { - debugger; //This shouldn't happen... + }; + if (key === 'proto') { + if (doc[key] instanceof Doc) { + assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)); + } + } else if (key === 'anchor1' || key === 'anchor2') { + if (doc[key] instanceof Doc) { + assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, true, asBranch)); + } } else { - assignKey(field); + if (field instanceof RefField) { + assignKey(field); + } else if (cfield instanceof ComputedField) { + !dontCreate && assignKey(ComputedField.MakeFunction(cfield.script.originalScript)); + } else if (field instanceof ObjectField) { + await copyObjectField(field); + } else if (field instanceof Promise) { + debugger; //This shouldn't happen... + } else { + assignKey(field); + } } - } - })); + }) + ); for (const link of Array.from(doc[DirectLinksSym])) { const linkClone = await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch); linkMap.set(link, linkClone); } if (!dontCreate) { - Doc.SetInPlace(copy, "title", (asBranch ? "BRANCH: " : "CLONE: ") + doc.title, true); + Doc.SetInPlace(copy, 'title', (asBranch ? 'BRANCH: ' : 'CLONE: ') + doc.title, true); asBranch ? (copy.branchOf = doc) : (copy.cloneOf = doc); if (!Doc.IsPrototype(copy)) { - Doc.AddDocToList(doc, "branches", Doc.GetProto(copy)); + Doc.AddDocToList(doc, 'branches', Doc.GetProto(copy)); } cloneMap.set(doc[Id], copy); } @@ -601,20 +719,20 @@ export namespace Doc { } export async function MakeClone(doc: Doc, dontCreate: boolean = false, asBranch = false, cloneMap: Map<string, Doc> = new Map()) { const linkMap = new Map<Doc, Doc>(); - const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = []; - const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ["cloneOf", "branches", "branchOf"], dontCreate, asBranch); + const rtfMap: { copy: Doc; key: string; field: RichTextField }[] = []; + const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf', 'branches', 'branchOf'], dontCreate, asBranch); Array.from(linkMap.entries()).map((links: Doc[]) => LinkManager.Instance.addLink(links[1], true)); rtfMap.map(({ copy, key, field }) => { const replacer = (match: any, attr: string, id: string, offset: any, string: any) => { const mapped = cloneMap.get(id); - return attr + "\"" + (mapped ? mapped[Id] : id) + "\""; + return attr + '"' + (mapped ? mapped[Id] : id) + '"'; }; const replacer2 = (match: any, href: string, id: string, offset: any, string: any) => { const mapped = cloneMap.get(id); return href + (mapped ? mapped[Id] : id); }; const regex = `(${Doc.localServerPath()})([^"]*)`; - const re = new RegExp(regex, "g"); + const re = new RegExp(regex, 'g'); copy[key] = new RichTextField(field.Data.replace(/("textId":|"audioId":|"anchorId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text); }); return { clone: copy, map: cloneMap }; @@ -628,37 +746,36 @@ export namespace Doc { // a.click(); const { clone, map } = await Doc.MakeClone(doc, true); function replacer(key: any, value: any) { - if (["branchOf", "cloneOf", "context", "cursors"].includes(key)) return undefined; + if (['branchOf', 'cloneOf', 'context', 'cursors'].includes(key)) return undefined; else if (value instanceof Doc) { - if (key !== "field" && Number.isNaN(Number(key))) { + if (key !== 'field' && Number.isNaN(Number(key))) { const __fields = value[FieldsSym](); - return { id: value[Id], __type: "Doc", fields: __fields }; + return { id: value[Id], __type: 'Doc', fields: __fields }; } else { - return { fieldId: value[Id], __type: "proxy" }; + return { fieldId: value[Id], __type: 'proxy' }; } - } - else if (value instanceof ScriptField) return { script: value.script, __type: "script" }; - else if (value instanceof RichTextField) return { Data: value.Data, Text: value.Text, __type: "RichTextField" }; - else if (value instanceof ImageField) return { url: value.url.href, __type: "image" }; - else if (value instanceof PdfField) return { url: value.url.href, __type: "pdf" }; - else if (value instanceof AudioField) return { url: value.url.href, __type: "audio" }; - else if (value instanceof VideoField) return { url: value.url.href, __type: "video" }; - else if (value instanceof WebField) return { url: value.url.href, __type: "web" }; - else if (value instanceof MapField) return { url: value.url.href, __type: "map" }; - else if (value instanceof DateField) return { date: value.toString(), __type: "date" }; - else if (value instanceof ProxyField) return { fieldId: value.fieldId, __type: "proxy" }; - else if (value instanceof Array && key !== "fields") return { fields: value, __type: "list" }; - else if (value instanceof ComputedField) return { script: value.script, __type: "computed" }; + } else if (value instanceof ScriptField) return { script: value.script, __type: 'script' }; + else if (value instanceof RichTextField) return { Data: value.Data, Text: value.Text, __type: 'RichTextField' }; + else if (value instanceof ImageField) return { url: value.url.href, __type: 'image' }; + else if (value instanceof PdfField) return { url: value.url.href, __type: 'pdf' }; + else if (value instanceof AudioField) return { url: value.url.href, __type: 'audio' }; + else if (value instanceof VideoField) return { url: value.url.href, __type: 'video' }; + else if (value instanceof WebField) return { url: value.url.href, __type: 'web' }; + else if (value instanceof MapField) return { url: value.url.href, __type: 'map' }; + else if (value instanceof DateField) return { date: value.toString(), __type: 'date' }; + else if (value instanceof ProxyField) return { fieldId: value.fieldId, __type: 'proxy' }; + else if (value instanceof Array && key !== 'fields') return { fields: value, __type: 'list' }; + else if (value instanceof ComputedField) return { script: value.script, __type: 'computed' }; else return value; } const docs: { [id: string]: any } = {}; - Array.from(map.entries()).forEach(f => docs[f[0]] = f[1]); + Array.from(map.entries()).forEach(f => (docs[f[0]] = f[1])); const docString = JSON.stringify({ id: doc[Id], docs }, replacer); const zip = new JSZip(); - zip.file("doc.json", docString); + zip.file('doc.json', docString); // // Generate a directory within the Zip file structure // var img = zip.folder("images"); @@ -667,11 +784,10 @@ export namespace Doc { // img.file("smile.gif", imgData, {base64: true}); // Generate the zip file asynchronously - zip.generateAsync({ type: "blob" }) - .then((content: any) => { - // Force down of the Zip file - saveAs(content, doc.title + ".zip"); // glr: Possibly change the name of the document to match the title? - }); + zip.generateAsync({ type: 'blob' }).then((content: any) => { + // Force down of the Zip file + saveAs(content, doc.title + '.zip'); // glr: Possibly change the name of the document to match the title? + }); } // // Determines whether the layout needs to be expanded (as a template). @@ -694,46 +810,47 @@ export namespace Doc { // layout_mytemplate(somparam=somearg). // then any references to @someparam would be rewritten as accesses to 'somearg' on the rootDocument export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc, templateArgs?: string) { - const args = templateArgs?.match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1].replace("()", "") || StrCast(templateLayoutDoc.PARAMS); - if (!args && !WillExpandTemplateLayout(templateLayoutDoc, targetDoc) || !targetDoc) return templateLayoutDoc; + const args = templateArgs?.match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1].replace('()', '') || StrCast(templateLayoutDoc.PARAMS); + if ((!args && !WillExpandTemplateLayout(templateLayoutDoc, targetDoc)) || !targetDoc) return templateLayoutDoc; - const templateField = StrCast(templateLayoutDoc.isTemplateForField); // the field that the template renders + const templateField = StrCast(templateLayoutDoc.isTemplateForField); // the field that the template renders // First it checks if an expanded layout already exists -- if so it will be stored on the dataDoc // using the template layout doc's id as the field key. // If it doesn't find the expanded layout, then it makes a delegate of the template layout and // saves it on the data doc indexed by the template layout's id. // - const params = args.split("=").length > 1 ? args.split("=")[0] : "PARAMS"; + const params = args.split('=').length > 1 ? args.split('=')[0] : 'PARAMS'; const layoutFielddKey = Doc.LayoutFieldKey(templateLayoutDoc); - const expandedLayoutFieldKey = (templateField || layoutFielddKey) + "-layout[" + templateLayoutDoc[Id] + (args ? `(${args})` : "") + "]"; + const expandedLayoutFieldKey = (templateField || layoutFielddKey) + '-layout[' + templateLayoutDoc[Id] + (args ? `(${args})` : '') + ']'; let expandedTemplateLayout = targetDoc?.[expandedLayoutFieldKey]; if (templateLayoutDoc.resolvedDataDoc instanceof Promise) { expandedTemplateLayout = undefined; _pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey, true); - } - else if (expandedTemplateLayout === undefined && !_pendingMap.get(targetDoc[Id] + expandedLayoutFieldKey + args)) { + } else if (expandedTemplateLayout === undefined && !_pendingMap.get(targetDoc[Id] + expandedLayoutFieldKey + args)) { if (templateLayoutDoc.resolvedDataDoc === (targetDoc.rootDocument || Doc.GetProto(targetDoc)) && templateLayoutDoc.PARAMS === StrCast(targetDoc.PARAMS)) { expandedTemplateLayout = templateLayoutDoc; // reuse an existing template layout if its for the same document with the same params } else { templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = Cast(templateLayoutDoc.proto, Doc, null) || templateLayoutDoc); // if the template has already been applied (ie, a nested template), then use the template's prototype if (!targetDoc[expandedLayoutFieldKey]) { _pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey + args, true); - setTimeout(action(() => { - const newLayoutDoc = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]"); - // the template's arguments are stored in params which is derefenced to find - // the actual field key where the parameterized template data is stored. - newLayoutDoc[params] = args !== "..." ? args : ""; // ... signifies the layout has sub template(s) -- so we have to expand the layout for them so that they can get the correct 'rootDocument' field, but we don't need to reassign their params. it would be better if the 'rootDocument' field could be passed dynamically to avoid have to create instances - newLayoutDoc.rootDocument = targetDoc; - const dataDoc = Doc.GetProto(targetDoc); - newLayoutDoc.resolvedDataDoc = dataDoc; - if (dataDoc[templateField] === undefined && templateLayoutDoc[templateField] instanceof List && (templateLayoutDoc[templateField] as any).length) { - dataDoc[templateField] = ComputedField.MakeFunction(`ObjectField.MakeCopy(templateLayoutDoc["${templateField}"] as List)`, { templateLayoutDoc: Doc.name }, { templateLayoutDoc }); - } - targetDoc[expandedLayoutFieldKey] = newLayoutDoc; - - _pendingMap.delete(targetDoc[Id] + expandedLayoutFieldKey + args); - })); + setTimeout( + action(() => { + const newLayoutDoc = Doc.MakeDelegate(templateLayoutDoc, undefined, '[' + templateLayoutDoc.title + ']'); + // the template's arguments are stored in params which is derefenced to find + // the actual field key where the parameterized template data is stored. + newLayoutDoc[params] = args !== '...' ? args : ''; // ... signifies the layout has sub template(s) -- so we have to expand the layout for them so that they can get the correct 'rootDocument' field, but we don't need to reassign their params. it would be better if the 'rootDocument' field could be passed dynamically to avoid have to create instances + newLayoutDoc.rootDocument = targetDoc; + const dataDoc = Doc.GetProto(targetDoc); + newLayoutDoc.resolvedDataDoc = dataDoc; + if (dataDoc[templateField] === undefined && templateLayoutDoc[templateField] instanceof List && (templateLayoutDoc[templateField] as any).length) { + dataDoc[templateField] = ComputedField.MakeFunction(`ObjectField.MakeCopy(templateLayoutDoc["${templateField}"] as List)`, { templateLayoutDoc: Doc.name }, { templateLayoutDoc }); + } + targetDoc[expandedLayoutFieldKey] = newLayoutDoc; + + _pendingMap.delete(targetDoc[Id] + expandedLayoutFieldKey + args); + }) + ); } } } @@ -744,17 +861,17 @@ export namespace Doc { // otherwise, it just returns the childDoc export function GetLayoutDataDocPair(containerDoc: Doc, containerDataDoc: Opt<Doc>, childDoc: Doc) { if (!childDoc || childDoc instanceof Promise || !Doc.GetProto(childDoc)) { - console.log("No, no, no!"); + console.log('No, no, no!'); return { layout: childDoc, data: childDoc }; } - const resolvedDataDoc = (Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!childDoc.isTemplateDoc && !childDoc.isTemplateForField && !childDoc.PARAMS) ? undefined : containerDataDoc); - return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc, "(" + StrCast(containerDoc.PARAMS) + ")"), data: resolvedDataDoc }; + const resolvedDataDoc = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!childDoc.isTemplateDoc && !childDoc.isTemplateForField && !childDoc.PARAMS) ? undefined : containerDataDoc; + return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc, '(' + StrCast(containerDoc.PARAMS) + ')'), data: resolvedDataDoc }; } export function Overwrite(doc: Doc, overwrite: Doc, copyProto: boolean = false): Doc { Object.keys(doc).forEach(key => { const field = ProxyField.WithoutProxy(() => doc[key]); - if (key === "proto" && copyProto) { + if (key === 'proto' && copyProto) { if (doc.proto instanceof Doc && overwrite.proto instanceof Doc) { overwrite[key] = Doc.Overwrite(doc[key]!, overwrite.proto); } @@ -776,12 +893,12 @@ export namespace Doc { export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string, retitle = false): Doc { const copy = new Doc(copyProtoId, true); - const exclude = Cast(doc.cloneFieldFilter, listSpec("string"), []); + const exclude = Cast(doc.cloneFieldFilter, listSpec('string'), []); Object.keys(doc).forEach(key => { if (exclude.includes(key)) return; const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); const field = ProxyField.WithoutProxy(() => doc[key]); - if (key === "proto" && copyProto) { + if (key === 'proto' && copyProto) { if (doc[key] instanceof Doc) { copy[key] = Doc.MakeCopy(doc[key]!, false); } @@ -789,11 +906,14 @@ export namespace Doc { if (field instanceof RefField) { copy[key] = field; } else if (cfield instanceof ComputedField) { - copy[key] = cfield[Copy]();// ComputedField.MakeFunction(cfield.script.originalScript); + copy[key] = cfield[Copy](); // ComputedField.MakeFunction(cfield.script.originalScript); } else if (field instanceof ObjectField) { - copy[key] = doc[key] instanceof Doc ? - key.includes("layout[") ? undefined : doc[key] : // reference documents except remove documents that are expanded teplate fields - ObjectField.MakeCopy(field); + copy[key] = + doc[key] instanceof Doc + ? key.includes('layout[') + ? undefined + : doc[key] // reference documents except remove documents that are expanded teplate fields + : ObjectField.MakeCopy(field); } else if (field instanceof Promise) { debugger; //This shouldn't happend... } else { @@ -806,17 +926,16 @@ export namespace Doc { Doc.GetProto(copy).context = undefined; Doc.GetProto(copy).aliases = new List<Doc>([copy]); } else { - Doc.AddDocToList(Doc.GetProto(copy)[DataSym], "aliases", copy); + Doc.AddDocToList(Doc.GetProto(copy)[DataSym], 'aliases', copy); } copy.context = undefined; - Doc.defaultAclPrivate && (copy["acl-Public"] = "Not Shared"); + Doc.defaultAclPrivate && (copy['acl-Public'] = 'Not Shared'); if (retitle) { copy.title = incrementTitleCopy(StrCast(copy.title)); } return copy; } - export function MakeDelegate(doc: Doc, id?: string, title?: string): Doc; export function MakeDelegate(doc: Opt<Doc>, id?: string, title?: string): Opt<Doc>; export function MakeDelegate(doc: Opt<Doc>, id?: string, title?: string): Opt<Doc> { @@ -825,8 +944,10 @@ export namespace Doc { delegate[Initializing] = true; delegate.proto = doc; delegate.author = Doc.CurrentUserEmail; - Object.keys(doc).filter(key => key.startsWith("acl")).forEach(key => delegate[key] = doc[key]); - if (!Doc.IsSystem(doc)) Doc.AddDocToList(doc[DataSym], "aliases", delegate); + Object.keys(doc) + .filter(key => key.startsWith('acl')) + .forEach(key => (delegate[key] = doc[key])); + if (!Doc.IsSystem(doc)) Doc.AddDocToList(doc[DataSym], 'aliases', delegate); title && (delegate.title = title); delegate[Initializing] = false; return delegate; @@ -849,7 +970,7 @@ export namespace Doc { delegate[Initializing] = true; delegate.proto = delegateProto; delegate.author = Doc.CurrentUserEmail; - Doc.AddDocToList(delegateProto[DataSym], "aliases", delegate); + Doc.AddDocToList(delegateProto[DataSym], 'aliases', delegate); delegate[Initializing] = false; delegateProto[Initializing] = false; return delegate; @@ -861,11 +982,11 @@ export namespace Doc { const proto = new Doc(); proto.author = Doc.CurrentUserEmail; const target = Doc.MakeDelegate(proto); - const targetKey = StrCast(templateDoc.layoutKey, "layout"); - const applied = ApplyTemplateTo(templateDoc, target, targetKey, templateDoc.title + "(..." + _applyCount++ + ")"); + const targetKey = StrCast(templateDoc.layoutKey, 'layout'); + const applied = ApplyTemplateTo(templateDoc, target, targetKey, templateDoc.title + '(...' + _applyCount++ + ')'); target.layoutKey = targetKey; applied && (Doc.GetProto(applied).type = templateDoc.type); - Doc.defaultAclPrivate && (applied["acl-Public"] = "Not Shared"); + Doc.defaultAclPrivate && (applied['acl-Public'] = 'Not Shared'); return applied; } return undefined; @@ -888,9 +1009,8 @@ export namespace Doc { // metadata field indicated by the title of the template field (not the default field that it was rendering) // export function MakeMetadataFieldTemplate(templateField: Doc, templateDoc: Opt<Doc>): boolean { - // find the metadata field key that this template field doc will display (indicated by its title) - const metadataFieldKey = StrCast(templateField.isTemplateForField) || StrCast(templateField.title).replace(/^-/, ""); + const metadataFieldKey = StrCast(templateField.isTemplateForField) || StrCast(templateField.title).replace(/^-/, ''); // update the original template to mark it as a template templateField.isTemplateForField = metadataFieldKey; @@ -904,7 +1024,7 @@ export namespace Doc { // note 2: this will not overwrite any field that already exists on the template doc at the field key if (!templateDoc?.[metadataFieldKey] && templateFieldValue instanceof ObjectField) { Cast(templateFieldValue, listSpec(Doc), [])?.map(d => d instanceof Doc && MakeMetadataFieldTemplate(d, templateDoc)); - (Doc.GetProto(templateField)[metadataFieldKey] = ObjectField.MakeCopy(templateFieldValue)); + Doc.GetProto(templateField)[metadataFieldKey] = ObjectField.MakeCopy(templateFieldValue); } // get the layout string that the template uses to specify its layout const templateFieldLayoutString = StrCast(Doc.LayoutField(Doc.Layout(templateField))); @@ -913,19 +1033,18 @@ export namespace Doc { Doc.Layout(templateField).layout = templateFieldLayoutString.replace(/fieldKey={'[^']*'}/, `fieldKey={'${metadataFieldKey}'}`); // assign the template field doc a delegate of any extension document that was previously used to render the template field (since extension doc's carry rendering informatino) - Doc.Layout(templateField)[metadataFieldKey + "_ext"] = Doc.MakeDelegate(templateField[templateFieldLayoutString?.split("'")[1] + "_ext"] as Doc); + Doc.Layout(templateField)[metadataFieldKey + '_ext'] = Doc.MakeDelegate(templateField[templateFieldLayoutString?.split("'")[1] + '_ext'] as Doc); return true; } - // converts a document id to a url path on the server - export function globalServerPath(doc: Doc | string = ""): string { - return Utils.prepend("/doc/" + (doc instanceof Doc ? doc[Id] : doc)); + export function globalServerPath(doc: Doc | string = ''): string { + return Utils.prepend('/doc/' + (doc instanceof Doc ? doc[Id] : doc)); } // converts a document id to a url path on the server export function localServerPath(doc?: Doc): string { - return "/doc/" + (doc ? doc[Id] : ""); + return '/doc/' + (doc ? doc[Id] : ''); } export function overlapping(doc1: Doc, doc2: Doc, clusterDistance: number) { @@ -958,47 +1077,70 @@ export namespace Doc { export class DocData { @observable _user_doc: Doc = undefined!; @observable _sharing_doc: Doc = undefined!; - @observable _searchQuery: string = ""; + @observable _searchQuery: string = ''; } // the document containing the view layout information - will be the Document itself unless the Document has // a layout field or 'layout' is given. export function Layout(doc: Doc, layout?: Doc): Doc { - const overrideLayout = layout && Cast(doc[`${StrCast(layout.isTemplateForField, "data")}-layout[` + layout[Id] + "]"], Doc, null); + const overrideLayout = layout && Cast(doc[`${StrCast(layout.isTemplateForField, 'data')}-layout[` + layout[Id] + ']'], Doc, null); return overrideLayout || doc[LayoutSym] || doc; } - export function SetLayout(doc: Doc, layout: Doc | string) { doc[StrCast(doc.layoutKey, "layout")] = layout; } - export function LayoutField(doc: Doc) { return doc[StrCast(doc.layoutKey, "layout")]; } - export function LayoutFieldKey(doc: Doc): string { return StrCast(Doc.Layout(doc).layout).split("'")[1]; } + export function SetLayout(doc: Doc, layout: Doc | string) { + doc[StrCast(doc.layoutKey, 'layout')] = layout; + } + export function LayoutField(doc: Doc) { + return doc[StrCast(doc.layoutKey, 'layout')]; + } + export function LayoutFieldKey(doc: Doc): string { + return StrCast(Doc.Layout(doc).layout).split("'")[1]; + } export function NativeAspect(doc: Doc, dataDoc?: Doc, useDim?: boolean) { return Doc.NativeWidth(doc, dataDoc, useDim) / (Doc.NativeHeight(doc, dataDoc, useDim) || 1); } - export function NativeWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) { return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + "-nativeWidth"], useWidth ? doc[WidthSym]() : 0)); } + export function NativeWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) { + return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '-nativeWidth'], useWidth ? doc[WidthSym]() : 0)); + } export function NativeHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) { - const dheight = doc ? NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + "-nativeHeight"], useHeight ? doc[HeightSym]() : 0) : 0; - const nheight = doc ? Doc.NativeWidth(doc, dataDoc, useHeight) * doc[HeightSym]() / doc[WidthSym]() : 0; + const dheight = doc ? NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '-nativeHeight'], useHeight ? doc[HeightSym]() : 0) : 0; + const nheight = doc ? (Doc.NativeWidth(doc, dataDoc, useHeight) * doc[HeightSym]()) / doc[WidthSym]() : 0; return !doc ? 0 : NumCast(doc._nativeHeight, nheight || dheight); } - export function SetNativeWidth(doc: Doc, width: number | undefined, fieldKey?: string) { doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + "-nativeWidth"] = width; } - export function SetNativeHeight(doc: Doc, height: number | undefined, fieldKey?: string) { doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + "-nativeHeight"] = height; } - + export function SetNativeWidth(doc: Doc, width: number | undefined, fieldKey?: string) { + doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + '-nativeWidth'] = width; + } + export function SetNativeHeight(doc: Doc, height: number | undefined, fieldKey?: string) { + doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + '-nativeHeight'] = height; + } const manager = new DocData(); - export function SearchQuery(): string { return manager._searchQuery; } - export function SetSearchQuery(query: string) { runInAction(() => manager._searchQuery = query); } - export function UserDoc(): Doc { return manager._user_doc; } - export function SharingDoc(): Doc { return CurrentUserUtils.MySharedDocs; } - export function LinkDBDoc(): Doc { return Cast(Doc.UserDoc().myLinkDatabase, Doc, null); } - export function SetUserDoc(doc: Doc) { return (manager._user_doc = doc); } + export function SearchQuery(): string { + return manager._searchQuery; + } + export function SetSearchQuery(query: string) { + runInAction(() => (manager._searchQuery = query)); + } + export function UserDoc(): Doc { + return manager._user_doc; + } + export function SharingDoc(): Doc { + return Doc.MySharedDocs; + } + export function LinkDBDoc(): Doc { + return Cast(Doc.UserDoc().myLinkDatabase, Doc, null); + } + export function SetUserDoc(doc: Doc) { + return (manager._user_doc = doc); + } const isSearchMatchCache = computedFn(function IsSearchMatch(doc: Doc) { - return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) : - brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined; + return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) : brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined; }); - export function IsSearchMatch(doc: Doc) { return isSearchMatchCache(doc); } + export function IsSearchMatch(doc: Doc) { + return isSearchMatchCache(doc); + } export function IsSearchMatchUnmemoized(doc: Doc) { - return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) : - brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined; + return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) : brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined; } export function SetSearchMatch(doc: Doc, results: { searchMatch: number }) { if (doc && GetEffectiveAcl(doc) !== AclPrivate && GetEffectiveAcl(Doc.GetProto(doc)) !== AclPrivate) { @@ -1017,8 +1159,12 @@ export namespace Doc { brushManager.SearchMatchDoc.clear(); } - const isBrushedCache = computedFn(function IsBrushed(doc: Doc) { return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetProto(doc)); }); - export function IsBrushed(doc: Doc) { return isBrushedCache(doc); } + const isBrushedCache = computedFn(function IsBrushed(doc: Doc) { + return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetProto(doc)); + }); + export function IsBrushed(doc: Doc) { + return isBrushedCache(doc); + } export enum DocBrushStatus { unbrushed = 0, @@ -1037,9 +1183,7 @@ export namespace Doc { for (const link of LinkManager.Instance.getAllDirectLinks(lastBrushed)) { const a1 = Cast(link.anchor1, Doc, null); const a2 = Cast(link.anchor2, Doc, null); - if (Doc.AreProtosEqual(a1, doc) || Doc.AreProtosEqual(a2, doc) || - (Doc.AreProtosEqual(Cast(a1.annotationOn, Doc, null), doc)) || - (Doc.AreProtosEqual(Cast(a2.annotationOn, Doc, null), doc))) { + if (Doc.AreProtosEqual(a1, doc) || Doc.AreProtosEqual(a2, doc) || Doc.AreProtosEqual(Cast(a1.annotationOn, Doc, null), doc) || Doc.AreProtosEqual(Cast(a2.annotationOn, Doc, null), doc)) { return DocBrushStatus.linkHighlighted; } } @@ -1070,22 +1214,21 @@ export namespace Doc { } export function LinkEndpoint(linkDoc: Doc, anchorDoc: Doc) { - return Doc.AreProtosEqual(anchorDoc, (linkDoc.anchor1 as Doc).annotationOn as Doc) || - Doc.AreProtosEqual(anchorDoc, linkDoc.anchor1 as Doc) ? "1" : "2"; + return Doc.AreProtosEqual(anchorDoc, (linkDoc.anchor1 as Doc).annotationOn as Doc) || Doc.AreProtosEqual(anchorDoc, linkDoc.anchor1 as Doc) ? '1' : '2'; } export function linkFollowUnhighlight() { Doc.UnhighlightAll(); - document.removeEventListener("pointerdown", linkFollowUnhighlight); + document.removeEventListener('pointerdown', linkFollowUnhighlight); } let _lastDate = 0; export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true) { linkFollowUnhighlight(); (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs)); - document.removeEventListener("pointerdown", linkFollowUnhighlight); - document.addEventListener("pointerdown", linkFollowUnhighlight); - const lastDate = _lastDate = Date.now(); + document.removeEventListener('pointerdown', linkFollowUnhighlight); + document.addEventListener('pointerdown', linkFollowUnhighlight); + const lastDate = (_lastDate = Date.now()); window.setTimeout(() => _lastDate === lastDate && linkFollowUnhighlight(), 5000); } @@ -1116,53 +1259,57 @@ export namespace Doc { const targetDoc = docEntry.value; targetDoc && Doc.UnHighlightDoc(targetDoc); } - } export function UnBrushAllDocs() { brushManager.BrushedDoc.clear(); } export function getDocTemplate(doc?: Doc) { - return !doc ? undefined : - doc.isTemplateDoc ? doc : - Cast(doc.dragFactory, Doc, null)?.isTemplateDoc ? doc.dragFactory : - Cast(Doc.Layout(doc), Doc, null)?.isTemplateDoc ? - (Cast(Doc.Layout(doc), Doc, null).resolvedDataDoc ? Doc.Layout(doc).proto : Doc.Layout(doc)) : - undefined; + return !doc + ? undefined + : doc.isTemplateDoc + ? doc + : Cast(doc.dragFactory, Doc, null)?.isTemplateDoc + ? doc.dragFactory + : Cast(Doc.Layout(doc), Doc, null)?.isTemplateDoc + ? Cast(Doc.Layout(doc), Doc, null).resolvedDataDoc + ? Doc.Layout(doc).proto + : Doc.Layout(doc) + : undefined; } export function matchFieldValue(doc: Doc, key: string, value: any): boolean { if (Utils.HasTransparencyFilter(value)) { - const isTransparent = (color: string) => color !== "" && (DashColor(color).alpha() !== 1); + const isTransparent = (color: string) => color !== '' && DashColor(color).alpha() !== 1; return isTransparent(StrCast(doc[key])); } - if (typeof value === "string") { - value = value.replace(`,${Utils.noRecursionHack}`, ""); + if (typeof value === 'string') { + value = value.replace(`,${Utils.noRecursionHack}`, ''); } - const fieldVal = key === "#" ? (StrCast(doc.tags).includes(":#" + value + ":") ? StrCast(doc.tags) : undefined) : doc[key]; - if (Cast(fieldVal, listSpec("string"), []).length) { - const vals = Cast(fieldVal, listSpec("string"), []); + const fieldVal = key === '#' ? (StrCast(doc.tags).includes(':#' + value + ':') ? StrCast(doc.tags) : undefined) : doc[key]; + if (Cast(fieldVal, listSpec('string'), []).length) { + const vals = Cast(fieldVal, listSpec('string'), []); const docs = vals.some(v => (v as any) instanceof Doc); if (docs) return value === Field.toString(fieldVal as Field); - return vals.some(v => v.includes(value)); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring + return vals.some(v => v.includes(value)); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring } const fieldStr = Field.toString(fieldVal as Field); return fieldStr.includes(value); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring } export function deiconifyView(doc: Doc) { - StrCast(doc.layoutKey).split("_")[1] === "icon" && setNativeView(doc); + StrCast(doc.layoutKey).split('_')[1] === 'icon' && setNativeView(doc); } export function setNativeView(doc: any) { - const prevLayout = StrCast(doc.layoutKey).split("_")[1]; - const deiconify = prevLayout === "icon" && StrCast(doc.deiconifyLayout) ? "layout_" + StrCast(doc.deiconifyLayout) : ""; - prevLayout === "icon" && (doc.deiconifyLayout = undefined); - doc.layoutKey = deiconify || "layout"; + const prevLayout = StrCast(doc.layoutKey).split('_')[1]; + const deiconify = prevLayout === 'icon' && StrCast(doc.deiconifyLayout) ? 'layout_' + StrCast(doc.deiconifyLayout) : ''; + prevLayout === 'icon' && (doc.deiconifyLayout = undefined); + doc.layoutKey = deiconify || 'layout'; } export function setDocRangeFilter(container: Opt<Doc>, key: string, range?: number[]) { if (!container) return; - const docRangeFilters = Cast(container._docRangeFilters, listSpec("string"), []); + const docRangeFilters = Cast(container._docRangeFilters, listSpec('string'), []); for (let i = 0; i < docRangeFilters.length; i += 3) { if (docRangeFilters[i] === key) { docRangeFilters.splice(i, 3); @@ -1180,16 +1327,16 @@ export namespace Doc { // filters document in a container collection: // all documents with the specified value for the specified key are included/excluded // based on the modifiers :"check", "x", undefined - export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: "remove" | "match" | "check" | "x" | "exists" | "unset", toggle?: boolean, fieldSuffix?: string, append: boolean = true) { + export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: 'remove' | 'match' | 'check' | 'x' | 'exists' | 'unset', toggle?: boolean, fieldSuffix?: string, append: boolean = true) { if (!container) return; - const filterField = "_" + (fieldSuffix ? fieldSuffix + "-" : "") + "docFilters"; - const docFilters = Cast(container[filterField], listSpec("string"), []); + const filterField = '_' + (fieldSuffix ? fieldSuffix + '-' : '') + 'docFilters'; + const docFilters = Cast(container[filterField], listSpec('string'), []); runInAction(() => { for (let i = 0; i < docFilters.length; i++) { - const fields = docFilters[i].split(":"); // split key:value:modifier - if (fields[0] === key && (fields[1] === value || modifiers === "match")) { + const fields = docFilters[i].split(':'); // split key:value:modifier + if (fields[0] === key && (fields[1] === value || modifiers === 'match')) { if (fields[2] === modifiers && modifiers && fields[1] === value) { - if (toggle) modifiers = "remove"; + if (toggle) modifiers = 'remove'; else return; } docFilters.splice(i, 1); @@ -1197,17 +1344,17 @@ export namespace Doc { break; } } - if (!docFilters.length && modifiers === "match" && value === undefined) { + if (!docFilters.length && modifiers === 'match' && value === undefined) { container[filterField] = undefined; - } else if (modifiers !== "remove") { + } else if (modifiers !== 'remove') { !append && (docFilters.length = 0); - docFilters.push(key + ":" + value + ":" + modifiers); + docFilters.push(key + ':' + value + ':' + modifiers); container[filterField] = new List<string>(docFilters); } }); } export function readDocRangeFilter(doc: Doc, key: string) { - const docRangeFilters = Cast(doc._docRangeFilters, listSpec("string"), []); + const docRangeFilters = Cast(doc._docRangeFilters, listSpec('string'), []); for (let i = 0; i < docRangeFilters.length; i += 3) { if (docRangeFilters[i] === key) { return [Number(docRangeFilters[i + 1]), Number(docRangeFilters[i + 2])]; @@ -1225,8 +1372,7 @@ export namespace Doc { layoutDoc._viewScale = NumCast(layoutDoc._viewScale, 1) * contentScale; layoutDoc._nativeWidth = undefined; layoutDoc._nativeHeight = undefined; - } - else { + } else { layoutDoc._autoHeight = false; if (!Doc.NativeWidth(layoutDoc)) { layoutDoc._nativeWidth = NumCast(layoutDoc._width, panelWidth); @@ -1238,45 +1384,46 @@ export namespace Doc { export function isDocPinned(doc: Doc) { //add this new doc to props.Document - const curPres = CurrentUserUtils.ActivePresentation; - return !curPres ? false : DocListCast(curPres.data).findIndex((val) => Doc.AreProtosEqual(val, doc)) !== -1; + const curPres = Doc.ActivePresentation; + return !curPres ? false : DocListCast(curPres.data).findIndex(val => Doc.AreProtosEqual(val, doc)) !== -1; } + // prettier-ignore export function toIcon(doc?: Doc, isOpen?: boolean) { switch (StrCast(doc?.type)) { - case DocumentType.IMG: return "image"; - case DocumentType.COMPARISON: return "columns"; - case DocumentType.RTF: return "sticky-note"; + case DocumentType.IMG: return 'image'; + case DocumentType.COMPARISON: return 'columns'; + case DocumentType.RTF: return 'sticky-note'; case DocumentType.COL: - const folder: IconProp = isOpen ? "folder-open" : "folder"; - const chevron: IconProp = isOpen ? "chevron-down" : "chevron-right"; + const folder: IconProp = isOpen ? 'folder-open' : 'folder'; + const chevron: IconProp = isOpen ? 'chevron-down' : 'chevron-right'; return !doc?.isFolder ? folder : chevron; - case DocumentType.WEB: return "globe-asia"; - case DocumentType.SCREENSHOT: return "photo-video"; - case DocumentType.WEBCAM: return "video"; - case DocumentType.AUDIO: return "microphone"; - case DocumentType.BUTTON: return "bolt"; - case DocumentType.PRES: return "tv"; - case DocumentType.SCRIPTING: return "terminal"; - case DocumentType.IMPORT: return "cloud-upload-alt"; - case DocumentType.VID: return "video"; - case DocumentType.INK: return "pen-nib"; - case DocumentType.PDF: return "file-pdf"; - case DocumentType.LINK: return "link"; - case DocumentType.MAP: return "map-marker-alt"; - default: return "question"; + case DocumentType.WEB: return 'globe-asia'; + case DocumentType.SCREENSHOT: return 'photo-video'; + case DocumentType.WEBCAM: return 'video'; + case DocumentType.AUDIO: return 'microphone'; + case DocumentType.BUTTON: return 'bolt'; + case DocumentType.PRES: return 'tv'; + case DocumentType.SCRIPTING: return 'terminal'; + case DocumentType.IMPORT: return 'cloud-upload-alt'; + case DocumentType.VID: return 'video'; + case DocumentType.INK: return 'pen-nib'; + case DocumentType.PDF: return 'file-pdf'; + case DocumentType.LINK: return 'link'; + case DocumentType.MAP: return 'map-marker-alt'; + default: return 'question'; } } - export async function importDocument(file:File) { - const upload = Utils.prepend("/uploadDoc"); + export async function importDocument(file: File) { + const upload = Utils.prepend('/uploadDoc'); const formData = new FormData(); if (file) { formData.append('file', file); - formData.append('remap', "true"); - const response = await fetch(upload, { method: "POST", body: formData }); + formData.append('remap', 'true'); + const response = await fetch(upload, { method: 'POST', body: formData }); const json = await response.json(); - if (json !== "error") { + if (json !== 'error') { const doc = await DocServer.GetRefField(json); return doc; } @@ -1285,17 +1432,16 @@ export namespace Doc { } export namespace Get { - - const primitives = ["string", "number", "boolean"]; + const primitives = ['string', 'number', 'boolean']; export interface JsonConversionOpts { data: any; title?: string; - appendToExisting?: { targetDoc: Doc, fieldKey?: string }; + appendToExisting?: { targetDoc: Doc; fieldKey?: string }; excludeEmptyObjects?: boolean; } - const defaultKey = "json"; + const defaultKey = 'json'; /** * This function takes any valid JSON(-like) data, i.e. parsed or unparsed, and at arbitrarily @@ -1340,17 +1486,17 @@ export namespace Doc { if (excludeEmptyObjects === undefined) { excludeEmptyObjects = true; } - if (data === undefined || data === null || ![...primitives, "object"].includes(typeof data)) { + if (data === undefined || data === null || ![...primitives, 'object'].includes(typeof data)) { return undefined; } let resolved: any; try { - resolved = JSON.parse(typeof data === "string" ? data : JSON.stringify(data)); + resolved = JSON.parse(typeof data === 'string' ? data : JSON.stringify(data)); } catch (e) { return undefined; } let output: Opt<Doc>; - if (typeof resolved === "object" && !(resolved instanceof Array)) { + if (typeof resolved === 'object' && !(resolved instanceof Array)) { output = convertObject(resolved, excludeEmptyObjects, title, appendToExisting?.targetDoc); } else { // give the proper types to the data extracted from the JSON @@ -1358,7 +1504,7 @@ export namespace Doc { if (appendToExisting) { (output = appendToExisting.targetDoc)[appendToExisting.fieldKey || defaultKey] = result; } else { - (output = new Doc).json = result; + (output = new Doc()).json = result; } } title && output && (output.title = title); @@ -1375,14 +1521,14 @@ export namespace Doc { const convertObject = (object: any, excludeEmptyObjects: boolean, title?: string, target?: Doc): Opt<Doc> => { const hasEntries = Object.keys(object).length; if (hasEntries || !excludeEmptyObjects) { - const resolved = target ?? new Doc; + const resolved = target ?? new Doc(); if (hasEntries) { let result: Opt<Field>; Object.keys(object).map(key => { // if excludeEmptyObjects is true, any qualifying conversions from toField will // be undefined, and thus the results that would have // otherwise been empty (List or Doc)s will just not be written - if (result = toField(object[key], excludeEmptyObjects, key)) { + if ((result = toField(object[key], excludeEmptyObjects, key))) { resolved[key] = result; } }); @@ -1418,40 +1564,78 @@ export namespace Doc { if (primitives.includes(typeof data)) { return data; } - if (typeof data === "object") { + if (typeof data === 'object') { return data instanceof Array ? convertList(data, excludeEmptyObjects) : convertObject(data, excludeEmptyObjects, title, undefined); } throw new Error(`How did ${data} of type ${typeof data} end up in JSON?`); }; } - } -ScriptingGlobals.add(function idToDoc(id: string): any { return DocServer.GetCachedRefField(id); }); -ScriptingGlobals.add(function renameAlias(doc: any) { return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, "") + `(${doc.aliasNumber})`; }); -ScriptingGlobals.add(function getProto(doc: any) { return Doc.GetProto(doc); }); -ScriptingGlobals.add(function getDocTemplate(doc?: any) { return Doc.getDocTemplate(doc); }); -ScriptingGlobals.add(function getAlias(doc: any) { return Doc.MakeAlias(doc); }); -ScriptingGlobals.add(function getCopy(doc: any, copyProto: any) { return doc.isTemplateDoc ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto); }); -ScriptingGlobals.add(function copyField(field: any) { return Field.Copy(field); }); -ScriptingGlobals.add(function docList(field: any) { return DocListCast(field); }); -ScriptingGlobals.add(function addDocToList(doc: Doc, field: string, added:Doc) { return Doc.AddDocToList(doc,field, added); }); -ScriptingGlobals.add(function setInPlace(doc: any, field: any, value: any) { return Doc.SetInPlace(doc, field, value, false); }); -ScriptingGlobals.add(function sameDocs(doc1: any, doc2: any) { return Doc.AreProtosEqual(doc1, doc2); }); -ScriptingGlobals.add(function undo() { SelectionManager.DeselectAll(); return UndoManager.Undo(); }); -ScriptingGlobals.add(function redo() { SelectionManager.DeselectAll(); return UndoManager.Redo(); }); -ScriptingGlobals.add(function DOC(id: string) { console.log("Can't parse a document id in a script"); return "invalid"; }); -ScriptingGlobals.add(function assignDoc(doc: Doc, field: string, id: string) { return Doc.assignDocToField(doc, field, id); }); -ScriptingGlobals.add(function docCast(doc: FieldResult): any { return DocCastAsync(doc); }); +ScriptingGlobals.add(function idToDoc(id: string): any { + return DocServer.GetCachedRefField(id); +}); +ScriptingGlobals.add(function renameAlias(doc: any) { + return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, '') + `(${doc.aliasNumber})`; +}); +ScriptingGlobals.add(function getProto(doc: any) { + return Doc.GetProto(doc); +}); +ScriptingGlobals.add(function getDocTemplate(doc?: any) { + return Doc.getDocTemplate(doc); +}); +ScriptingGlobals.add(function getAlias(doc: any) { + return Doc.MakeAlias(doc); +}); +ScriptingGlobals.add(function getCopy(doc: any, copyProto: any) { + return doc.isTemplateDoc ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto); +}); +ScriptingGlobals.add(function copyField(field: any) { + return Field.Copy(field); +}); +ScriptingGlobals.add(function docList(field: any) { + return DocListCast(field); +}); +ScriptingGlobals.add(function addDocToList(doc: Doc, field: string, added: Doc) { + return Doc.AddDocToList(doc, field, added); +}); +ScriptingGlobals.add(function setInPlace(doc: any, field: any, value: any) { + return Doc.SetInPlace(doc, field, value, false); +}); +ScriptingGlobals.add(function sameDocs(doc1: any, doc2: any) { + return Doc.AreProtosEqual(doc1, doc2); +}); +ScriptingGlobals.add(function undo() { + SelectionManager.DeselectAll(); + return UndoManager.Undo(); +}); +ScriptingGlobals.add(function redo() { + SelectionManager.DeselectAll(); + return UndoManager.Redo(); +}); +ScriptingGlobals.add(function DOC(id: string) { + console.log("Can't parse a document id in a script"); + return 'invalid'; +}); +ScriptingGlobals.add(function assignDoc(doc: Doc, field: string, id: string) { + return Doc.assignDocToField(doc, field, id); +}); +ScriptingGlobals.add(function docCast(doc: FieldResult): any { + return DocCastAsync(doc); +}); ScriptingGlobals.add(function activePresentationItem() { - const curPres = CurrentUserUtils.ActivePresentation; + const curPres = Doc.ActivePresentation; return curPres && DocListCast(curPres[Doc.LayoutFieldKey(curPres)])[NumCast(curPres._itemIndex)]; }); ScriptingGlobals.add(function selectedDocs(container: Doc, excludeCollections: boolean, prevValue: any) { - const docs = SelectionManager.Views().map(dv => dv.props.Document). - filter(d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.KVP && - (!excludeCollections || d.type !== DocumentType.COL || !Cast(d.data, listSpec(Doc), null))); + const docs = SelectionManager.Views() + .map(dv => dv.props.Document) + .filter(d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.KVP && (!excludeCollections || d.type !== DocumentType.COL || !Cast(d.data, listSpec(Doc), null))); return docs.length ? new List(docs) : prevValue; }); -ScriptingGlobals.add(function setDocFilter(container: Doc, key: string, value: any, modifiers: "match" | "check" | "x" | "remove") { Doc.setDocFilter(container, key, value, modifiers); }); -ScriptingGlobals.add(function setDocRangeFilter(container: Doc, key: string, range: number[]) { Doc.setDocRangeFilter(container, key, range); }); +ScriptingGlobals.add(function setDocFilter(container: Doc, key: string, value: any, modifiers: 'match' | 'check' | 'x' | 'remove') { + Doc.setDocFilter(container, key, value, modifiers); +}); +ScriptingGlobals.add(function setDocRangeFilter(container: Doc, key: string, range: number[]) { + Doc.setDocRangeFilter(container, key, range); +}); diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts index a19be5df9..bf055cd8b 100644 --- a/src/fields/RichTextUtils.ts +++ b/src/fields/RichTextUtils.ts @@ -1,48 +1,46 @@ -import { AssertionError } from "assert"; -import { docs_v1 } from "googleapis"; -import { Fragment, Mark, Node } from "prosemirror-model"; -import { sinkListItem } from "prosemirror-schema-list"; -import { Utils, DashColor } from "../Utils"; -import { Docs, DocUtils } from "../client/documents/Documents"; -import { schema } from "../client/views/nodes/formattedText/schema_rts"; -import { GooglePhotos } from "../client/apis/google_docs/GooglePhotosClientUtils"; -import { DocServer } from "../client/DocServer"; -import { Networking } from "../client/Network"; -import { FormattedTextBox } from "../client/views/nodes/formattedText/FormattedTextBox"; -import { Doc, Opt } from "./Doc"; -import { Id } from "./FieldSymbols"; -import { RichTextField } from "./RichTextField"; -import { Cast, StrCast } from "./Types"; +import { AssertionError } from 'assert'; +import { docs_v1 } from 'googleapis'; +import { Fragment, Mark, Node } from 'prosemirror-model'; +import { sinkListItem } from 'prosemirror-schema-list'; +import { Utils, DashColor } from '../Utils'; +import { Docs, DocUtils } from '../client/documents/Documents'; +import { schema } from '../client/views/nodes/formattedText/schema_rts'; +import { GooglePhotos } from '../client/apis/google_docs/GooglePhotosClientUtils'; +import { DocServer } from '../client/DocServer'; +import { Networking } from '../client/Network'; +import { FormattedTextBox } from '../client/views/nodes/formattedText/FormattedTextBox'; +import { Doc, Opt } from './Doc'; +import { Id } from './FieldSymbols'; +import { RichTextField } from './RichTextField'; +import { Cast, StrCast } from './Types'; import Color = require('color'); -import { EditorState, TextSelection, Transaction } from "prosemirror-state"; -import { GoogleApiClientUtils } from "../client/apis/google_docs/GoogleApiClientUtils"; +import { EditorState, TextSelection, Transaction } from 'prosemirror-state'; +import { GoogleApiClientUtils } from '../client/apis/google_docs/GoogleApiClientUtils'; export namespace RichTextUtils { - - const delimiter = "\n"; - const joiner = ""; - + const delimiter = '\n'; + const joiner = ''; export const Initialize = (initial?: string) => { const content: any[] = []; const state = { doc: { - type: "doc", + type: 'doc', content, }, selection: { - type: "text", + type: 'text', anchor: 0, - head: 0 - } + head: 0, + }, }; if (initial && initial.length) { content.push({ - type: "paragraph", + type: 'paragraph', content: { - type: "text", - text: initial - } + type: 'text', + text: initial, + }, }); state.selection.anchor = state.selection.head = initial.length + 1; } @@ -56,8 +54,8 @@ export namespace RichTextUtils { export const ToPlainText = (state: EditorState) => { // Because we're working with plain text, just concatenate all paragraphs const content = state.doc.content; - const paragraphs: Node<any>[] = []; - content.forEach(node => node.type.name === "paragraph" && paragraphs.push(node)); + const paragraphs: Node[] = []; + content.forEach(node => node.type.name === 'paragraph' && paragraphs.push(node)); // Functions to flatten ProseMirror paragraph objects (and their components) to plain text // Concatentate paragraphs and string the result together @@ -80,22 +78,21 @@ export namespace RichTextUtils { // Preserve the current state, but re-write the content to be the blocks const parsed = JSON.parse(oldState ? oldState.Data : Initialize()); parsed.doc.content = elements.map(text => { - const paragraph: any = { type: "paragraph" }; - text.length && (paragraph.content = [{ type: "text", marks: [], text }]); // An empty paragraph gets treated as a line break + const paragraph: any = { type: 'paragraph' }; + text.length && (paragraph.content = [{ type: 'text', marks: [], text }]); // An empty paragraph gets treated as a line break return paragraph; }); // If the new content is shorter than the previous content and selection is unchanged, may throw an out of bounds exception, so we reset it - parsed.selection = { type: "text", anchor: 1, head: 1 }; + parsed.selection = { type: 'text', anchor: 1, head: 1 }; // Export the ProseMirror-compatible state object we've just built return JSON.stringify(parsed); }; export namespace GoogleDocs { - export const Export = async (state: EditorState): Promise<GoogleApiClientUtils.Docs.Content> => { - const nodes: (Node<any> | null)[] = []; + const nodes: (Node | null)[] = []; const text = ToPlainText(state); state.doc.content.forEach(node => { if (!node.childCount) { @@ -126,10 +123,10 @@ export namespace RichTextUtils { return { baseUrl: embeddedObject.imageProperties!.contentUri! }; }); - const uploads = await Networking.PostToServer("/googlePhotosMediaGet", { mediaItems }); + const uploads = await Networking.PostToServer('/googlePhotosMediaGet', { mediaItems }); if (uploads.length !== mediaItems.length) { - throw new AssertionError({ expected: mediaItems.length, actual: uploads.length, message: "Error with internally uploading inlineObjects!" }); + throw new AssertionError({ expected: mediaItems.length, actual: uploads.length, message: 'Error with internally uploading inlineObjects!' }); } for (let i = 0; i < objects.length; i++) { @@ -144,14 +141,14 @@ export namespace RichTextUtils { title: embeddedObject.title || `Imported Image from ${document.title}`, width, url: Utils.prepend(_m.client), - agnostic: Utils.prepend(agnostic.client) + agnostic: Utils.prepend(agnostic.client), }); } } return inlineObjectMap; }; - type BulletPosition = { value: number, sinks: number }; + type BulletPosition = { value: number; sinks: number }; interface MediaItem { baseUrl: string; @@ -172,7 +169,7 @@ export namespace RichTextUtils { const lists: ListGroup[] = []; const indentMap = new Map<ListGroup, BulletPosition[]>(); let globalOffset = 0; - const nodes: Node<any>[] = []; + const nodes: Node[] = []; for (const element of structured) { if (Array.isArray(element)) { lists.push(element); @@ -182,7 +179,7 @@ export namespace RichTextUtils { const sinks = paragraph.bullet!; positions.push({ value: position + globalOffset, - sinks + sinks, }); position += item.nodeSize; globalOffset += 2 * sinks; @@ -191,13 +188,13 @@ export namespace RichTextUtils { indentMap.set(element, positions); nodes.push(list(state.schema, items)); } else { - if (element.contents.some(child => "inlineObjectId" in child)) { + if (element.contents.some(child => 'inlineObjectId' in child)) { const group = element.contents; group.forEach((child, i) => { - let node: Opt<Node<any>>; - if ("inlineObjectId" in child) { + let node: Opt<Node>; + if ('inlineObjectId' in child) { node = imageNode(state.schema, inlineObjectMap.get(child.inlineObjectId!)!, textNote); - } else if ("content" in child && (i !== group.length - 1 || child.content!.removeTrailingNewlines().length)) { + } else if ('content' in child && (i !== group.length - 1 || child.content!.removeTrailingNewlines().length)) { node = paragraphNode(state.schema, [child]); } if (node) { @@ -215,7 +212,7 @@ export namespace RichTextUtils { state = state.apply(state.tr.replaceWith(0, 2, nodes)); const sink = sinkListItem(state.schema.nodes.list_item); - const dispatcher = (tr: Transaction) => state = state.apply(tr); + const dispatcher = (tr: Transaction) => (state = state.apply(tr)); for (const list of lists) { for (const pos of indentMap.get(list)!) { const resolved = state.doc.resolve(pos.value); @@ -252,17 +249,17 @@ export namespace RichTextUtils { }; const listItem = (schema: any, runs: docs_v1.Schema$TextRun[]): Node => { - return schema.node("list_item", null, paragraphNode(schema, runs)); + return schema.node('list_item', null, paragraphNode(schema, runs)); }; const list = (schema: any, items: Node[]): Node => { - return schema.node("ordered_list", { mapStyle: "bullet" }, items); + return schema.node('ordered_list', { mapStyle: 'bullet' }, items); }; const paragraphNode = (schema: any, runs: docs_v1.Schema$TextRun[]): Node => { const children = runs.map(run => textNode(schema, run)).filter(child => child !== undefined); const fragment = children.length ? Fragment.from(children) : undefined; - return schema.node("paragraph", null, fragment); + return schema.node('paragraph', null, fragment); }; const imageNode = (schema: any, image: ImageTemplate, textNote: Doc) => { @@ -278,7 +275,7 @@ export namespace RichTextUtils { } else { docid = backingDocId; } - return schema.node("image", { src, agnostic, width, docid, float: null, location: "add:right" }); + return schema.node('image', { src, agnostic, width, docid, float: null, location: 'add:right' }); }; const textNode = (schema: any, run: docs_v1.Schema$TextRun) => { @@ -287,10 +284,10 @@ export namespace RichTextUtils { }; const StyleToMark = new Map<keyof docs_v1.Schema$TextStyle, keyof typeof schema.marks>([ - ["bold", "strong"], - ["italic", "em"], - ["foregroundColor", "pFontColor"], - ["fontSize", "pFontSize"] + ['bold', 'strong'], + ['italic', 'em'], + ['foregroundColor', 'pFontColor'], + ['fontSize', 'pFontSize'], ]); const styleToMarks = (schema: any, textStyle?: docs_v1.Schema$TextStyle) => { @@ -301,21 +298,21 @@ export namespace RichTextUtils { Object.keys(textStyle).forEach(key => { let value: any; const targeted = key as keyof docs_v1.Schema$TextStyle; - if (value = textStyle[targeted]) { + if ((value = textStyle[targeted])) { const attributes: any = {}; let converted = StyleToMark.get(targeted) || targeted; value.url && (attributes.href = value.url); if (value.color) { const object = value.color.rgbColor; - attributes.color = Color.rgb(["red", "green", "blue"].map(color => object[color] * 255 || 0)).hex(); + attributes.color = Color.rgb(['red', 'green', 'blue'].map(color => object[color] * 255 || 0)).hex(); } if (value.magnitude) { attributes.fontSize = value.magnitude; } - if (converted === "weightedFontFamily") { - converted = ImportFontFamilyMapping.get(value.fontFamily) || "timesNewRoman"; + if (converted === 'weightedFontFamily') { + converted = ImportFontFamilyMapping.get(value.fontFamily) || 'timesNewRoman'; } const mapped = schema.marks[converted]; @@ -332,38 +329,38 @@ export namespace RichTextUtils { }; const MarkToStyle = new Map<keyof typeof schema.marks, keyof docs_v1.Schema$TextStyle>([ - ["strong", "bold"], - ["em", "italic"], - ["pFontColor", "foregroundColor"], - ["pFontSize", "fontSize"], - ["timesNewRoman", "weightedFontFamily"], - ["georgia", "weightedFontFamily"], - ["comicSans", "weightedFontFamily"], - ["tahoma", "weightedFontFamily"], - ["impact", "weightedFontFamily"] + ['strong', 'bold'], + ['em', 'italic'], + ['pFontColor', 'foregroundColor'], + ['pFontSize', 'fontSize'], + ['timesNewRoman', 'weightedFontFamily'], + ['georgia', 'weightedFontFamily'], + ['comicSans', 'weightedFontFamily'], + ['tahoma', 'weightedFontFamily'], + ['impact', 'weightedFontFamily'], ]); const ExportFontFamilyMapping = new Map<string, string>([ - ["timesNewRoman", "Times New Roman"], - ["arial", "Arial"], - ["georgia", "Georgia"], - ["comicSans", "Comic Sans MS"], - ["tahoma", "Tahoma"], - ["impact", "Impact"] + ['timesNewRoman', 'Times New Roman'], + ['arial', 'Arial'], + ['georgia', 'Georgia'], + ['comicSans', 'Comic Sans MS'], + ['tahoma', 'Tahoma'], + ['impact', 'Impact'], ]); const ImportFontFamilyMapping = new Map<string, string>([ - ["Times New Roman", "timesNewRoman"], - ["Arial", "arial"], - ["Georgia", "georgia"], - ["Comic Sans MS", "comicSans"], - ["Tahoma", "tahoma"], - ["Impact", "impact"] + ['Times New Roman', 'timesNewRoman'], + ['Arial', 'arial'], + ['Georgia', 'georgia'], + ['Comic Sans MS', 'comicSans'], + ['Tahoma', 'tahoma'], + ['Impact', 'impact'], ]); - const ignored = ["user_mark"]; + const ignored = ['user_mark']; - const marksToStyle = async (nodes: (Node<any> | null)[]): Promise<docs_v1.Schema$Request[]> => { + const marksToStyle = async (nodes: (Node | null)[]): Promise<docs_v1.Schema$Request[]> => { const requests: docs_v1.Schema$Request[] = []; let position = 1; for (const node of nodes) { @@ -376,25 +373,25 @@ export namespace RichTextUtils { const information: LinkInformation = { startIndex: position, endIndex: position + nodeSize, - textStyle + textStyle, }; - let mark: Mark<any>; + let mark: Mark; const markMap = BuildMarkMap(marks); for (const markName of Object.keys(schema.marks)) { if (ignored.includes(markName) || !(mark = markMap[markName])) { continue; } - let converted = MarkToStyle.get(markName) || markName as keyof docs_v1.Schema$TextStyle; + let converted = MarkToStyle.get(markName) || (markName as keyof docs_v1.Schema$TextStyle); let value: any = true; if (!converted) { continue; } const { attrs } = mark; switch (converted) { - case "link": - let url = attrs.allLinks.length ? attrs.allLinks[0].href : ""; - const delimiter = "/doc/"; - const alreadyShared = "?sharing=true"; + case 'link': + let url = attrs.allLinks.length ? attrs.allLinks[0].href : ''; + const delimiter = '/doc/'; + const alreadyShared = '?sharing=true'; if (new RegExp(window.location.origin + delimiter).test(url) && !url.endsWith(alreadyShared)) { const linkDoc = await DocServer.GetRefField(url.split(delimiter)[1]); if (linkDoc instanceof Doc) { @@ -411,41 +408,43 @@ export namespace RichTextUtils { textStyle.foregroundColor = fromRgb.blue; textStyle.bold = true; break; - case "fontSize": - value = { magnitude: attrs.fontSize, unit: "PT" }; + case 'fontSize': + value = { magnitude: attrs.fontSize, unit: 'PT' }; break; - case "foregroundColor": + case 'foregroundColor': value = fromHex(attrs.color); break; - case "weightedFontFamily": + case 'weightedFontFamily': value = { fontFamily: ExportFontFamilyMapping.get(markName) }; } let matches: RegExpExecArray | null; if ((matches = /p(\d+)/g.exec(markName)) !== null) { - converted = "fontSize"; - value = { magnitude: parseInt(matches[1].replace("px", "")), unit: "PT" }; + converted = 'fontSize'; + value = { magnitude: parseInt(matches[1].replace('px', '')), unit: 'PT' }; } textStyle[converted] = value; } if (Object.keys(textStyle).length) { requests.push(EncodeStyleUpdate(information)); } - if (node.type.name === "image") { + if (node.type.name === 'image') { const width = attrs.width; - requests.push(await EncodeImage({ - startIndex: position + nodeSize - 1, - uri: attrs.agnostic, - width: Number(typeof width === "string" ? width.replace("px", "") : width) - })); + requests.push( + await EncodeImage({ + startIndex: position + nodeSize - 1, + uri: attrs.agnostic, + width: Number(typeof width === 'string' ? width.replace('px', '') : width), + }) + ); } position += nodeSize; } return requests; }; - const BuildMarkMap = (marks: Mark<any>[]) => { - const markMap: { [type: string]: Mark<any> } = {}; - marks.forEach(mark => markMap[mark.type.name] = mark); + const BuildMarkMap = (marks: readonly Mark[]) => { + const markMap: { [type: string]: Mark } = {}; + marks.forEach(mark => (markMap[mark.type.name] = mark)); return markMap; }; @@ -462,23 +461,21 @@ export namespace RichTextUtils { } namespace fromRgb { - export const convert = (red: number, green: number, blue: number): docs_v1.Schema$OptionalColor => { return { color: { rgbColor: { red: red / 255, green: green / 255, - blue: blue / 255 - } - } + blue: blue / 255, + }, + }, }; }; export const red = convert(255, 0, 0); export const green = convert(0, 255, 0); export const blue = convert(0, 0, 255); - } const fromHex = (color: string): docs_v1.Schema$OptionalColor => { @@ -490,10 +487,10 @@ export namespace RichTextUtils { const { startIndex, endIndex, textStyle } = information; return { updateTextStyle: { - fields: "*", + fields: '*', range: { startIndex, endIndex }, - textStyle - } as docs_v1.Schema$UpdateTextStyleRequest + textStyle, + } as docs_v1.Schema$UpdateTextStyleRequest, }; }; @@ -507,13 +504,12 @@ export namespace RichTextUtils { return { insertInlineImage: { uri: baseUrls[0], - objectSize: { width: { magnitude: width, unit: "PT" } }, - location: { index: startIndex } - } + objectSize: { width: { magnitude: width, unit: 'PT' } }, + location: { index: startIndex }, + }, }; } return {}; }; } - -}
\ No newline at end of file +} diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index 40ca0ce22..68fb45987 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -1,29 +1,32 @@ -import { computedFn } from "mobx-utils"; -import { createSimpleSchema, custom, map, object, primitive, PropSchema, serializable, SKIP } from "serializr"; -import { CompiledScript, CompileScript } from "../client/util/Scripting"; -import { scriptingGlobal, ScriptingGlobals } from "../client/util/ScriptingGlobals"; -import { autoObject, Deserializable } from "../client/util/SerializationHelper"; -import { numberRange } from "../Utils"; -import { Doc, Field, Opt } from "./Doc"; -import { Copy, ToScriptString, ToString } from "./FieldSymbols"; -import { List } from "./List"; -import { ObjectField } from "./ObjectField"; -import { ProxyField } from "./Proxy"; -import { Cast, NumCast } from "./Types"; -import { Plugins } from "./util"; +import { computedFn } from 'mobx-utils'; +import { createSimpleSchema, custom, map, object, primitive, PropSchema, serializable, SKIP } from 'serializr'; +import { DocServer } from '../client/DocServer'; +import { CompiledScript, CompileScript, ScriptOptions } from '../client/util/Scripting'; +import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; +import { autoObject, Deserializable } from '../client/util/SerializationHelper'; +import { numberRange } from '../Utils'; +import { Doc, Field, Opt } from './Doc'; +import { Copy, Id, ToScriptString, ToString } from './FieldSymbols'; +import { List } from './List'; +import { ObjectField } from './ObjectField'; +import { Cast, NumCast } from './Types'; +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 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; } - return SKIP; - }); + ); } const optionsSchema = createSimpleSchema({ @@ -32,31 +35,19 @@ const optionsSchema = createSimpleSchema({ typecheck: true, editable: true, readonly: true, - params: optional(map(primitive())) + params: optional(map(primitive())), }); const scriptSchema = createSimpleSchema({ options: object(optionsSchema), - originalScript: true + originalScript: true, }); -async function deserializeScript(script: ScriptField) { - const captures: ProxyField<Doc> = (script as any).captures; - const cache = captures ? undefined : ScriptField.GetScriptFieldCache(script.script.originalScript); - if (cache) return (script as any).script = cache; - if (captures) { - const doc = (await captures.value())!; - const captured: any = {}; - const keys = Object.keys(doc); - const vals = await Promise.all(keys.map(key => doc[key]) as any); - keys.forEach((key, i) => captured[key] = vals[i]); - (script.script.options as any).capturedVariables = captured; - } +function finalizeScript(script: ScriptField, captures: boolean) { 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; !captures && ScriptField._scriptFieldCache.set(script.script.originalScript, comp); if (script.setterscript) { const compset = CompileScript(script.setterscript?.originalScript, script.setterscript.options); @@ -65,32 +56,56 @@ async function deserializeScript(script: ScriptField) { } (script as any).setterscript = compset; } + return comp; +} +async function deserializeScript(script: ScriptField) { + if (script.captures) { + const captured: any = {}; + (script.script.options as ScriptOptions).capturedVariables = captured; + Promise.all( + script.captures.map(async capture => { + const key = capture.split(':')[0]; + const val = capture.split(':')[1]; + if (val === 'true') captured[key] = true; + else if (val === 'false') captured[key] = false; + else if (val.startsWith('ID->')) captured[key] = await DocServer.GetRefField(val.replace('ID->', '')); + else if (!isNaN(Number(val))) captured[key] = Number(val); + else captured[key] = val; + }) + ).then(() => ((script as any).script = finalizeScript(script, true))); + } else { + (script as any).script = ScriptField.GetScriptFieldCache(script.script.originalScript) ?? finalizeScript(script, false); + } } @scriptingGlobal -@Deserializable("script", deserializeScript) +@Deserializable('script', deserializeScript) export class ScriptField extends ObjectField { + @serializable + readonly rawscript: string | undefined; @serializable(object(scriptSchema)) readonly script: CompiledScript; @serializable(object(scriptSchema)) readonly setterscript: CompiledScript | undefined; @serializable(autoObject()) - private captures?: ProxyField<Doc>; + captures?: List<string>; public static _scriptFieldCache: Map<string, Opt<CompiledScript>> = new Map(); - public static GetScriptFieldCache(field: string) { return this._scriptFieldCache.get(field); } + public static GetScriptFieldCache(field: string) { + return this._scriptFieldCache.get(field); + } - constructor(script: CompiledScript, setterscript?: CompiledScript) { + constructor(script: CompiledScript | undefined, setterscript?: CompiledScript, rawscript?: string) { super(); - if (script?.options.capturedVariables) { - const doc = Doc.assign(new Doc, script.options.capturedVariables); - doc.system = true; - this.captures = new ProxyField(doc); + const captured = script?.options.capturedVariables; + if (captured) { + this.captures = new List<string>(Object.keys(captured).map(key => key + ':' + (captured[key] instanceof Doc ? 'ID->' + (captured[key] as Doc)[Id] : captured[key].toString()))); } + this.rawscript = rawscript; this.setterscript = setterscript; - this.script = script; + this.script = script ?? (CompileScript('false') as CompiledScript); } // init(callback: (res: Field) => any) { @@ -115,63 +130,62 @@ export class ScriptField extends ObjectField { // } [Copy](): ObjectField { - return new ScriptField(this.script, this.setterscript); + return new ScriptField(this.script, this.setterscript, this.rawscript); } toString() { return `${this.script.originalScript} + ${this.setterscript?.originalScript}`; } [ToScriptString]() { - return "script field"; + return 'script field'; } [ToString]() { return this.script.originalScript; } - public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Field }) { + public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { const compiled = CompileScript(script, { params: { - this: Doc?.name || "Doc", // this is the doc that executes the script - self: Doc?.name || "Doc", // self is the root doc of the doc that executes the script - _last_: "any", // _last_ is the previous value of a computed field when it is being triggered to re-run. - _readOnly_: "boolean", // _readOnly_ is set when a computed field is executed to indicate that it should not have mobx side-effects. used for checking the value of a set function (see FontIconBox) - ...params + this: Doc?.name || 'Doc', // this is the doc that executes the script + self: Doc?.name || 'Doc', // self is the root doc of the doc that executes the script + _last_: 'any', // _last_ is the previous value of a computed field when it is being triggered to re-run. + _readOnly_: 'boolean', // _readOnly_ is set when a computed field is executed to indicate that it should not have mobx side-effects. used for checking the value of a set function (see FontIconBox) + ...params, }, typecheck: false, editable: true, addReturn: addReturn, - capturedVariables + capturedVariables, }); return compiled; } - public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Field }) { + public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { const compiled = ScriptField.CompileScript(script, params, true, capturedVariables); return compiled.compiled ? new ScriptField(compiled) : undefined; } - public static MakeScript(script: string, params: object = {}, capturedVariables?: { [name: string]: Field }) { + public static MakeScript(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { const compiled = ScriptField.CompileScript(script, params, false, capturedVariables); return compiled.compiled ? new ScriptField(compiled) : undefined; } } @scriptingGlobal -@Deserializable("computed", deserializeScript) +@Deserializable('computed', deserializeScript) export class ComputedField extends ScriptField { _lastComputedResult: any; //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 = computedFn((doc: Doc) => this._valueOutsideReaction(doc)); - _valueOutsideReaction = (doc: Doc) => this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) || doc, _last_: this._lastComputedResult, _readOnly_: true }, console.log).result; - + _valueOutsideReaction = (doc: Doc) => (this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) || doc, _last_: this._lastComputedResult, _readOnly_: true }, console.log).result); [Copy](): ObjectField { - return new ComputedField(this.script, this.setterscript); + return new ComputedField(this.script, this.setterscript, this.rawscript); } public static MakeScript(script: string, params: object = {}) { const compiled = ScriptField.CompileScript(script, params, false); return compiled.compiled ? new ComputedField(compiled) : undefined; } - public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Field }) { + public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { const compiled = ScriptField.CompileScript(script, params, true, capturedVariables); return compiled.compiled ? new ComputedField(compiled) : undefined; } @@ -182,7 +196,7 @@ export class ComputedField extends ScriptField { doc[`${fieldKey}-indexed`] = flist; } const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey})`, {}, true, {}); - const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: "any" }, true, {}); + const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {}); return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; } } @@ -196,7 +210,7 @@ export namespace ComputedField { useComputed = true; } - export const undefined = "__undefined"; + export const undefined = '__undefined'; export function WithoutComputed<T>(fn: () => T) { DisableComputedFields(); @@ -216,15 +230,27 @@ export namespace ComputedField { } } -ScriptingGlobals.add(function setIndexVal(list: any[], index: number, value: any) { - while (list.length <= index) list.push(undefined); - list[index] = value; -}, "sets the value at a given index of a list", "(list: any[], index: number, value: any)"); +ScriptingGlobals.add( + function setIndexVal(list: any[], index: number, value: any) { + while (list.length <= index) list.push(undefined); + list[index] = value; + }, + 'sets the value at a given index of a list', + '(list: any[], index: number, value: any)' +); -ScriptingGlobals.add(function getIndexVal(list: any[], index: number) { - return list?.reduce((p, x, i) => (i <= index && x !== undefined) || p === undefined ? x : p, undefined as any); -}, "returns the value at a given index of a list", "(list: any[], index: number)"); +ScriptingGlobals.add( + function getIndexVal(list: any[], index: number) { + return list?.reduce((p, x, i) => ((i <= index && x !== undefined) || p === undefined ? x : p), undefined as any); + }, + 'returns the value at a given index of a list', + '(list: any[], index: number)' +); -ScriptingGlobals.add(function makeScript(script: string) { - return ScriptField.MakeScript(script); -}, "returns the value at a given index of a list", "(list: any[], index: number)"); +ScriptingGlobals.add( + function makeScript(script: string) { + return ScriptField.MakeScript(script); + }, + 'returns the value at a given index of a list', + '(list: any[], index: number)' +); diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts index 36dd56a1a..00c78e231 100644 --- a/src/fields/URLField.ts +++ b/src/fields/URLField.ts @@ -1,16 +1,14 @@ -import { Deserializable } from "../client/util/SerializationHelper"; -import { serializable, custom } from "serializr"; -import { ObjectField } from "./ObjectField"; -import { ToScriptString, ToString, Copy } from "./FieldSymbols"; -import { scriptingGlobal } from "../client/util/ScriptingGlobals"; -import { Utils } from "../Utils"; +import { Deserializable } from '../client/util/SerializationHelper'; +import { serializable, custom } from 'serializr'; +import { ObjectField } from './ObjectField'; +import { ToScriptString, ToString, Copy } from './FieldSymbols'; +import { scriptingGlobal } from '../client/util/ScriptingGlobals'; +import { Utils } from '../Utils'; function url() { return custom( function (value: URL) { - return value.origin === window.location.origin ? - value.pathname : - value.href; + return value?.origin === window.location.origin ? value.pathname : value?.href; }, function (jsonValue: string) { return new URL(jsonValue, window.location.origin); @@ -26,23 +24,23 @@ export abstract class URLField extends ObjectField { constructor(url: URL); constructor(url: URL | string) { super(); - if (typeof url === "string") { - url = url.startsWith("http") ? new URL(url) : new URL(url, window.location.origin); + if (typeof url === 'string') { + url = url.startsWith('http') ? new URL(url) : new URL(url, window.location.origin); } this.url = url; } [ToScriptString]() { - if (Utils.prepend(this.url.pathname) === this.url.href) { + if (Utils.prepend(this.url?.pathname) === this.url?.href) { return `new ${this.constructor.name}("${this.url.pathname}")`; } return `new ${this.constructor.name}("${this.url.href}")`; } [ToString]() { - if (Utils.prepend(this.url.pathname) === this.url.href) { + if (Utils.prepend(this.url?.pathname) === this.url?.href) { return this.url.pathname; } - return this.url.href; + return this.url?.href; } [Copy](): this { @@ -50,16 +48,35 @@ export abstract class URLField extends ObjectField { } } -export const nullAudio = "https://actions.google.com/sounds/v1/alarms/beep_short.ogg"; - -@scriptingGlobal @Deserializable("audio") export class AudioField extends URLField { } -@scriptingGlobal @Deserializable("recording") export class RecordingField 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 { } -@scriptingGlobal @Deserializable("map") export class MapField extends URLField { } -@scriptingGlobal @Deserializable("csv") export class CsvField extends URLField { } -@scriptingGlobal @Deserializable("youtube") export class YoutubeField extends URLField { } -@scriptingGlobal @Deserializable("webcam") export class WebCamField extends URLField { } +export const nullAudio = 'https://actions.google.com/sounds/v1/alarms/beep_short.ogg'; +@scriptingGlobal +@Deserializable('audio') +export class AudioField extends URLField {} +@scriptingGlobal +@Deserializable('recording') +export class RecordingField 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 {} +@scriptingGlobal +@Deserializable('map') +export class MapField extends URLField {} +@scriptingGlobal +@Deserializable('csv') +export class CsvField extends URLField {} +@scriptingGlobal +@Deserializable('youtube') +export class YoutubeField extends URLField {} +@scriptingGlobal +@Deserializable('webcam') +export class WebCamField extends URLField {} diff --git a/src/fields/util.ts b/src/fields/util.ts index 8fb35981b..41e723119 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,20 +1,40 @@ -import { UndoManager } from "../client/util/UndoManager"; -import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAugment, AclSym, DataSym, DocListCast, AclAdmin, HeightSym, WidthSym, updateCachedAcls, AclUnset, DocListCastAsync, ForceServerWrite, Initializing, AclSelfEdit } from "./Doc"; -import { SerializationHelper } from "../client/util/SerializationHelper"; -import { ProxyField, PrefetchProxy } from "./Proxy"; -import { RefField } from "./RefField"; -import { ObjectField } from "./ObjectField"; -import { action, trace, } from "mobx"; -import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols"; -import { DocServer } from "../client/DocServer"; -import { ComputedField } from "./ScriptField"; -import { ScriptCast, StrCast } from "./Types"; -import { returnZero } from "../Utils"; -import CursorField from "./CursorField"; -import { List } from "./List"; -import { SnappingManager } from "../client/util/SnappingManager"; -import { computedFn } from "mobx-utils"; -import { RichTextField } from "./RichTextField"; +import { action, observable, runInAction, trace } from 'mobx'; +import { computedFn } from 'mobx-utils'; +import { DocServer } from '../client/DocServer'; +import { SerializationHelper } from '../client/util/SerializationHelper'; +import { UndoManager } from '../client/util/UndoManager'; +import { returnZero } from '../Utils'; +import CursorField from './CursorField'; +import { + AclAdmin, + AclAugment, + AclEdit, + AclPrivate, + AclReadonly, + AclSelfEdit, + AclSym, + AclUnset, + DataSym, + Doc, + DocListCast, + DocListCastAsync, + FieldResult, + ForceServerWrite, + HeightSym, + Initializing, + LayoutSym, + updateCachedAcls, + UpdatingFromServer, + WidthSym, +} from './Doc'; +import { Id, OnUpdate, Parent, Self, SelfProxy, Update } from './FieldSymbols'; +import { List } from './List'; +import { ObjectField } from './ObjectField'; +import { PrefetchProxy, ProxyField } from './Proxy'; +import { RefField } from './RefField'; +import { RichTextField } from './RichTextField'; +import { ComputedField } from './ScriptField'; +import { ScriptCast, StrCast } from './Types'; function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); @@ -44,7 +64,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number return true; } - if (typeof prop === "symbol") { + if (typeof prop === 'symbol') { target[prop] = value; return true; } @@ -76,15 +96,13 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number const writeMode = DocServer.getFieldWriteMode(prop as string); const fromServer = target[UpdatingFromServer]; - const sameAuthor = fromServer || (receiver.author === Doc.CurrentUserEmail); - const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (writeMode !== DocServer.WriteMode.LiveReadonly); - const writeToServer = - (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (effectiveAcl === AclSelfEdit && (value instanceof RichTextField))) && - !DocServer.Control.isReadOnly(); + const sameAuthor = fromServer || receiver.author === Doc.CurrentUserEmail; + const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode !== DocServer.WriteMode.LiveReadonly; + const writeToServer = (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (effectiveAcl === AclSelfEdit && value instanceof RichTextField)) && !DocServer.Control.isReadOnly(); if (writeToDoc) { if (value === undefined) { - target.__fieldKeys && (delete target.__fieldKeys[prop]); + target.__fieldKeys && delete target.__fieldKeys[prop]; delete target.__fields[prop]; } else { target.__fieldKeys && (target.__fieldKeys[prop] = true); @@ -96,16 +114,17 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number //if (typeof value === "object" && !(value instanceof ObjectField)) debugger; if (writeToServer) { - if (value === undefined) target[Update]({ '$unset': { ["fields." + prop]: "" } }); - else target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } }); + if (value === undefined) target[Update]({ $unset: { ['fields.' + prop]: '' } }); + else target[Update]({ $set: { ['fields.' + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : value === undefined ? null : value } }); } else { DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue); } - !receiver[Initializing] && (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) && + !receiver[Initializing] && + (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) && UndoManager.AddEvent({ - redo: () => receiver[prop] = value, - undo: () => receiver[prop] = curValue, - prop: prop?.toString() + redo: () => (receiver[prop] = value), + undo: () => (receiver[prop] = curValue), + prop: prop?.toString(), }); return true; } @@ -136,7 +155,6 @@ export function denormalizeEmail(email: string) { // playgroundMode = !playgroundMode; // } - /** * Copies parent's acl fields to the child */ @@ -145,35 +163,37 @@ export function inheritParentAcls(parent: Doc, child: Doc) { const dataDoc = parent[DataSym]; for (const key of Object.keys(dataDoc)) { // if the default acl mode is private, then don't inherit the acl-Public permission, but set it to private. - const permission = (key === "acl-Public" && Doc.defaultAclPrivate) ? AclPrivate : dataDoc[key]; - key.startsWith("acl") && distributeAcls(key, permission, child); + const permission = key === 'acl-Public' && Doc.defaultAclPrivate ? AclPrivate : dataDoc[key]; + key.startsWith('acl') && distributeAcls(key, permission, child); } } /** * These are the various levels of access a user can have to a document. - * + * * Admin: a user with admin access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), as well as change others' access rights to that document. - * + * * Edit: a user with edit access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), but not change any access rights to that document. - * + * * Add: a user with add access to a document can augment documents/annotations to that document but cannot edit or delete anything. - * + * * View: a user with view access to a document can only view it - they cannot add/remove/edit anything. - * + * * None: the document is not shared with that user. */ export enum SharingPermissions { - Admin = "Admin", - Edit = "Edit", - SelfEdit = "Self Edit", - Augment = "Augment", - View = "View", - None = "Not Shared" + Admin = 'Admin', + Edit = 'Edit', + SelfEdit = 'Self Edit', + Augment = 'Augment', + View = 'View', + None = 'Not Shared', } // return acl from cache or cache the acl and return. -const getEffectiveAclCache = computedFn(function (target: any, user?: string) { return getEffectiveAcl(target, user); }, true); +const getEffectiveAclCache = computedFn(function (target: any, user?: string) { + return getEffectiveAcl(target, user); +}, true); /** * Calculates the effective access right to a document for the current user. @@ -183,33 +203,47 @@ export function GetEffectiveAcl(target: any, user?: string): symbol { if (target[UpdatingFromServer]) return AclAdmin; // authored documents are private until an ACL is set. if (!target[AclSym] && target.author && target.author !== Doc.CurrentUserEmail) return AclPrivate; - return getEffectiveAclCache(target, user);// all changes received from the server must be processed as Admin. return this directly so that the acls aren't cached (UpdatingFromServer is not observable) + return getEffectiveAclCache(target, user); // all changes received from the server must be processed as Admin. return this directly so that the acls aren't cached (UpdatingFromServer is not observable) } function getPropAcl(target: any, prop: string | symbol | number) { - if (prop === UpdatingFromServer || prop === Initializing || target[UpdatingFromServer] || prop === AclSym) return AclAdmin; // requesting the UpdatingFromServer prop or AclSym must always go through to keep the local DB consistent + if (prop === UpdatingFromServer || prop === Initializing || target[UpdatingFromServer] || prop === AclSym) return AclAdmin; // requesting the UpdatingFromServer prop or AclSym must always go through to keep the local DB consistent if (prop && DocServer.IsPlaygroundField(prop.toString())) return AclEdit; // playground props are always editable return GetEffectiveAcl(target); } let HierarchyMapping: Map<symbol, number> | undefined; +let cachedGroups = observable([] as string[]); +/// bcz; argh!! TODO; These do not belong here, but there were include order problems with leaving them in util.ts +// need to investigate further what caused the mobx update problems and move to a better location. +const getCachedGroupByNameCache = computedFn(function (name: string) { + return cachedGroups.includes(name); +}, true); +export function GetCachedGroupByName(name: string) { + return getCachedGroupByNameCache(name); +} +export function SetCachedGroups(groups: string[]) { + runInAction(() => cachedGroups.push(...groups)); +} function getEffectiveAcl(target: any, user?: string): symbol { const targetAcls = target[AclSym]; - const userChecked = user || Doc.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group - const targetAuthor = (target.__fields?.author || target.author); // target may be a Doc of Proxy, so check __fields.author and .author + const userChecked = user || Doc.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group + const targetAuthor = target.__fields?.author || target.author; // target may be a Doc of Proxy, so check __fields.author and .author if (userChecked === targetAuthor || !targetAuthor) return AclAdmin; - if (SnappingManager.GetCachedGroupByName("Admin")) return AclAdmin; + if (GetCachedGroupByName('Admin')) return AclAdmin; if (targetAcls && Object.keys(targetAcls).length) { - HierarchyMapping = HierarchyMapping || new Map<symbol, number>([ - [AclPrivate, 0], - [AclReadonly, 1], - [AclAugment, 2], - [AclSelfEdit, 2.5], - [AclEdit, 3], - [AclAdmin, 4] - ]); + HierarchyMapping = + HierarchyMapping || + new Map<symbol, number>([ + [AclPrivate, 0], + [AclReadonly, 1], + [AclAugment, 2], + [AclSelfEdit, 2.5], + [AclEdit, 3], + [AclAdmin, 4], + ]); let effectiveAcl = AclPrivate; for (const [key, value] of Object.entries(targetAcls)) { @@ -217,14 +251,14 @@ function getEffectiveAcl(target: any, user?: string): symbol { // as a result we need to restore them again during this comparison. const entity = denormalizeEmail(key.substring(4)); // an individual or a group if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) { - if (SnappingManager.GetCachedGroupByName(entity) || userChecked === entity) { + if (GetCachedGroupByName(entity) || userChecked === entity) { effectiveAcl = value as symbol; } } } // if there's an overriding acl set through the properties panel or sharing menu, that's what's returned if the user isn't an admin of the document - const override = targetAcls["acl-Override"]; + const override = targetAcls['acl-Override']; if (override !== AclUnset && override !== undefined) effectiveAcl = override; // if we're in playground mode, return AclEdit (or AclAdmin if that's the user's effectiveAcl) @@ -246,12 +280,12 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc visited.push(target); const HierarchyMapping = new Map<string, number>([ - ["Not Shared", 0], - ["Can View", 1], - ["Can Augment", 2], - ["Self Edit", 2.5], - ["Can Edit", 3], - ["Admin", 4] + ['Not Shared', 0], + ['Can View', 1], + ['Can Augment', 2], + ['Self Edit', 2.5], + ['Can Edit', 3], + ['Admin', 4], ]); let layoutDocChanged = false; // determines whether fetchProto should be called or not (i.e. is there a change that should be reflected in target[AclSym]) @@ -271,7 +305,6 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc } if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) { - if (GetEffectiveAcl(dataDoc) === AclAdmin) { dataDoc[key] = acl; dataDocChanged = true; @@ -282,7 +315,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc links.forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited)); // maps over the children of the document - DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + (isDashboard ? "-all" : "")]).map(d => { + DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + (isDashboard ? '-all' : '')]).map(d => { distributeAcls(key, acl, d, inheritingFromCollection, visited); // } const data = d[DataSym]; @@ -292,7 +325,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc }); // maps over the annotations of the document - DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + "-annotations"]).map(d => { + DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '-annotations']).map(d => { distributeAcls(key, acl, d, inheritingFromCollection, visited); // } const data = d[DataSym]; @@ -311,11 +344,11 @@ export function setter(target: any, in_prop: string | symbol | number, value: an const effectiveAcl = getPropAcl(target, prop); if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin && !(effectiveAcl === AclSelfEdit && value instanceof RichTextField)) return true; // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't - if (typeof prop === "string" && prop.startsWith("acl") && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined, "None"].includes(value))) return true; + if (typeof prop === 'string' && prop.startsWith('acl') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined, 'None'].includes(value))) return true; // if (typeof prop === "string" && prop.startsWith("acl") && !["Can Edit", "Can Augment", "Can View", "Not Shared", undefined].includes(value)) return true; - if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && prop.startsWith("_")) { - if (!prop.startsWith("__")) prop = prop.substring(1); + if (typeof prop === 'string' && prop !== '__id' && prop !== '__fields' && prop.startsWith('_')) { + if (!prop.startsWith('__')) prop = prop.substring(1); if (target.__LAYOUT__) { target.__LAYOUT__[prop] = value; return true; @@ -331,17 +364,18 @@ export function getter(target: any, in_prop: string | symbol | number, receiver: let prop = in_prop; if (in_prop === AclSym) return target[AclSym]; - if (in_prop === "toString" || (in_prop !== HeightSym && in_prop !== WidthSym && in_prop !== LayoutSym && typeof prop === "symbol")) return target.__fields[prop] || target[prop]; + if (in_prop === 'toString' || (in_prop !== HeightSym && in_prop !== WidthSym && in_prop !== LayoutSym && typeof prop === 'symbol')) return target.__fields[prop] || target[prop]; if (GetEffectiveAcl(target) === AclPrivate) return prop === HeightSym || prop === WidthSym ? returnZero : undefined; if (prop === LayoutSym) return target.__LAYOUT__; - if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && prop.startsWith("_")) { - if (!prop.startsWith("__")) prop = prop.substring(1); + if (typeof prop === 'string' && prop !== '__id' && prop !== '__fields' && prop.startsWith('_')) { + if (!prop.startsWith('__')) prop = prop.substring(1); if (target.__LAYOUT__) return target.__LAYOUT__[prop]; } - if (prop === "then") {//If we're being awaited + if (prop === 'then') { + //If we're being awaited return undefined; } - if (typeof prop === "symbol") { + if (typeof prop === 'symbol') { return target.__fields[prop] || target[prop]; } if (SerializationHelper.IsSerializing()) { @@ -362,22 +396,21 @@ function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreP field = res.value; } } - if (field === undefined && !ignoreProto && prop !== "proto") { - 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 (field === undefined && !ignoreProto && prop !== 'proto') { + 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 && GetEffectiveAcl(proto) !== AclPrivate) { 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) { - if (typeof prop === "symbol") { + if (typeof prop === 'symbol') { delete target[prop]; return true; } @@ -389,64 +422,68 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any let lastValue = ObjectField.MakeCopy(value); return (diff?: any) => { const op = - diff?.op === "$addToSet" ? { '$addToSet': { ["fields." + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } } : - diff?.op === "$remFromSet" ? { '$remFromSet': { ["fields." + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } } - : { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } }; + diff?.op === '$addToSet' + ? { $addToSet: { ['fields.' + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } } + : diff?.op === '$remFromSet' + ? { $remFromSet: { ['fields.' + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } } + : { $set: { ['fields.' + prop]: SerializationHelper.Serialize(value) } }; !op.$set && ((op as any).length = diff.length); const prevValue = ObjectField.MakeCopy(lastValue as List<any>); lastValue = ObjectField.MakeCopy(value); const newValue = ObjectField.MakeCopy(value); - if (!(value instanceof CursorField) && !(value?.some?.((v: any) => v instanceof CursorField))) { - !receiver[UpdatingFromServer] && UndoManager.AddEvent( - diff?.op === "$addToSet" ? - { - redo: () => { - receiver[prop].push(...diff.items.map((item: any) => item.value ? item.value() : item)); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }, - undo: action(() => { - // console.log("undo $add: " + prop, diff.items) // bcz: uncomment to log undo - diff.items.forEach((item: any) => { - const ind = receiver[prop].indexOf(item.value ? item.value() : item); - ind !== -1 && receiver[prop].splice(ind, 1); - }); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }), - prop: "" - } : - diff?.op === "$remFromSet" ? - { - redo: action(() => { - diff.items.forEach((item: any) => { - const ind = receiver[prop].indexOf(item.value ? item.value() : item); - ind !== -1 && receiver[prop].splice(ind, 1); - }); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }), - undo: () => { - // console.log("undo $rem: " + prop, diff.items) // bcz: uncomment to log undo - diff.items.forEach((item: any) => { - const ind = (prevValue as List<any>).indexOf(item.value ? item.value() : item); - ind !== -1 && receiver[prop].indexOf(item.value ? item.value() : item) === -1 && receiver[prop].splice(ind, 0, item); - }); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }, - prop: "" - } + if (!(value instanceof CursorField) && !value?.some?.((v: any) => v instanceof CursorField)) { + !receiver[UpdatingFromServer] && + UndoManager.AddEvent( + diff?.op === '$addToSet' + ? { + redo: () => { + receiver[prop].push(...diff.items.map((item: any) => (item.value ? item.value() : item))); + lastValue = ObjectField.MakeCopy(receiver[prop]); + }, + undo: action(() => { + // console.log("undo $add: " + prop, diff.items) // bcz: uncomment to log undo + diff.items.forEach((item: any) => { + const ind = receiver[prop].indexOf(item.value ? item.value() : item); + ind !== -1 && receiver[prop].splice(ind, 1); + }); + lastValue = ObjectField.MakeCopy(receiver[prop]); + }), + prop: '', + } + : diff?.op === '$remFromSet' + ? { + redo: action(() => { + diff.items.forEach((item: any) => { + const ind = receiver[prop].indexOf(item.value ? item.value() : item); + ind !== -1 && receiver[prop].splice(ind, 1); + }); + lastValue = ObjectField.MakeCopy(receiver[prop]); + }), + undo: () => { + // console.log("undo $rem: " + prop, diff.items) // bcz: uncomment to log undo + diff.items.forEach((item: any) => { + const ind = (prevValue as List<any>).indexOf(item.value ? item.value() : item); + ind !== -1 && receiver[prop].indexOf(item.value ? item.value() : item) === -1 && receiver[prop].splice(ind, 0, item); + }); + lastValue = ObjectField.MakeCopy(receiver[prop]); + }, + prop: '', + } : { - redo: () => { - receiver[prop] = ObjectField.MakeCopy(newValue as List<any>); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }, - undo: () => { - // console.log("undo list: " + prop, receiver[prop]) // bcz: uncomment to log undo - receiver[prop] = ObjectField.MakeCopy(prevValue as List<any>); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }, - prop: "" - }); + redo: () => { + receiver[prop] = ObjectField.MakeCopy(newValue as List<any>); + lastValue = ObjectField.MakeCopy(receiver[prop]); + }, + undo: () => { + // console.log("undo list: " + prop, receiver[prop]) // bcz: uncomment to log undo + receiver[prop] = ObjectField.MakeCopy(prevValue as List<any>); + lastValue = ObjectField.MakeCopy(receiver[prop]); + }, + prop: '', + } + ); } target[Update](op); }; -}
\ No newline at end of file +} |
