diff options
Diffstat (limited to 'src/fields/Doc.ts')
-rw-r--r-- | src/fields/Doc.ts | 607 |
1 files changed, 322 insertions, 285 deletions
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index fc89dcbe7..0d11b9743 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -17,7 +17,7 @@ import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToJavascriptString, ToScr import { InkEraserTool, InkInkTool, InkTool } from './InkField'; import { List } from './List'; import { ObjectField, serverOpType } from './ObjectField'; -import { PrefetchProxy, ProxyField } from './Proxy'; +import { PrefetchProxy } from './Proxy'; import { FieldId, RefField } from './RefField'; import { RichTextField } from './RichTextField'; import { listSpec } from './Schema'; @@ -25,6 +25,7 @@ import { ComputedField, ScriptField } from './ScriptField'; import { BoolCast, Cast, DocCast, FieldValue, ImageCastWithSuffix, NumCast, RTFCast, StrCast, ToConstructor, toList } from './Types'; import { containedFieldChangedHandler, deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, setter, SharingPermissions } from './util'; import { gptImageLabel } from '../client/apis/gpt/GPT'; +import { DateField } from './DateField'; export let ObjGetRefField: (id: string, force?: boolean) => Promise<Doc | undefined>; export let ObjGetRefFields: (ids: string[]) => Promise<Map<string, Doc | undefined>>; @@ -43,11 +44,11 @@ export namespace Field { * @param doc doc containing key * @param key field key to display * @param showComputedValue whether copmuted function should display its value instead of its function + * @param schemaCell * @returns string representation of the field */ export function toKeyValueString(doc: Doc, key: string, showComputedValue?: boolean, schemaCell?: boolean): string { - const isOnDelegate = !Doc.IsDataProto(doc) && Object.keys(doc).includes(key.replace(/^_/, '')); - const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); + const cfield = ComputedField.DisableCompute(() => FieldValue(doc[key])); const valFunc = (field: FieldType): string => { const res = field instanceof ComputedField && showComputedValue @@ -64,7 +65,9 @@ export namespace Field { .trim() .replace(/^new List\((.*)\)$/, '$1'); }; - return !Field.IsField(cfield) ? (key.startsWith('_') ? '=' : '') : (isOnDelegate ? '=' : '') + valFunc(cfield); + const notOnTemplate = !key.startsWith('_') || doc[DocLayout] === doc; + const isOnDelegate = notOnTemplate && !Doc.IsDataProto(doc) && ((key.startsWith('_') && !Field.IsField(cfield)) || Object.keys(doc).includes(key.replace(/^_/, ''))); + return (isOnDelegate ? '=' : '') + (!Field.IsField(cfield) ? '' : valFunc(cfield)); } export function toScriptString(field: FieldType, schemaCell?: boolean) { switch (typeof field) { @@ -131,13 +134,13 @@ export function DocListCastAsync(field: FieldResult, defaultValue?: Doc[]) { return list ? Promise.all(list).then(() => list) : Promise.resolve(defaultValue); } export function NumListCast(field: FieldResult, defaultVal: number[] = []) { - return Cast(field, listSpec('number'), defaultVal); + return Cast(field, listSpec('number'), defaultVal)!; } export function StrListCast(field: FieldResult, defaultVal: string[] = []) { - return Cast(field, listSpec('string'), defaultVal); + return Cast(field, listSpec('string'), defaultVal)!; } export function DocListCast(field: FieldResult, defaultVal: Doc[] = []) { - return Cast(field, listSpec(Doc), defaultVal).filter(d => d instanceof Doc) as Doc[]; + return Cast(field, listSpec(Doc), defaultVal)!.filter(d => d instanceof Doc); } export enum aclLevel { @@ -174,13 +177,27 @@ export function updateCachedAcls(doc: Doc) { } if (doc.proto instanceof Promise) { - doc.proto.then(proto => updateCachedAcls(DocCast(proto))); + doc.proto.then(proto => DocCast(proto) && updateCachedAcls(DocCast(proto)!)); return doc.proto; } } return undefined; } +/** + * computes a field name for where to store and expanded template Doc + * The format is layout_[ROOT_TEMPLATE_NMAE]_[ROOT_TEMPLATE_CHILD_NAME]_... + * @param template the template (either a root or a root child Doc) + * @param layoutFieldKey the fieldKey of the container of the template + * @returns field key to store expanded template Doc + */ +export function expandedFieldName(template: Doc, layoutFieldKey?: string) { + const layout_key = !layoutFieldKey?.endsWith(']') + ? 'layout' // layout_SOMETHING = SOMETHING => layout_[SOMETHING] = [SOMETHING] + : layoutFieldKey; // prettier-ignore + const tempTitle = '[' + StrCast(template.title).replace(/^\[(.*)\]$/, '$1') + ']'; + return `${layout_key}_${tempTitle}`; // prettier-ignore +} @scriptingGlobal @Deserializable('Doc', (obj: unknown) => updateCachedAcls(obj as Doc), ['id']) export class Doc extends RefField { @@ -215,7 +232,7 @@ export class Doc extends RefField { public static AddLink: (link: Doc, checkExists?: boolean) => void; public static DeleteLink: (link: Doc) => void; public static Links: (link: Doc | undefined) => Doc[]; - public static getOppositeAnchor: (linkDoc: Doc, anchor: Doc) => Doc | undefined; + public static getOppositeAnchor: (linkDoc: Doc | undefined, anchor: Doc | undefined) => Doc | undefined; // KeyValueBox SetField (defined there) public static SetField: (doc: Doc, key: string, value: string, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) => boolean; // UserDoc "API" @@ -237,7 +254,7 @@ export class Doc extends RefField { public static get MyPublishedDocs() { return DocListCast(Doc.ActiveDashboard?.myPublishedDocs).concat(DocListCast(DocCast(Doc.UserDoc().myPublishedDocs)?.data)); } // prettier-ignore public static get MyDashboards() { return DocCast(Doc.UserDoc().myDashboards); } // prettier-ignore public static get MyTemplates() { return DocCast(Doc.UserDoc().myTemplates); } // prettier-ignore - public static get MyStickers() { return DocCast(Doc.UserDoc().myStickers); } // prettier-ignore + public static get MyStickers() { return DocCast(Doc.UserDoc().myStickers); } // prettier-ignore public static get MyLightboxDrawings() { return DocCast(Doc.UserDoc().myLightboxDrawings); } // prettier-ignore public static get MyImports() { return DocCast(Doc.UserDoc().myImports); } // prettier-ignore public static get MyFilesystem() { return DocCast(Doc.UserDoc().myFilesystem); } // prettier-ignore @@ -264,22 +281,28 @@ export class Doc extends RefField { public static set ActiveDashboard(val: Opt<Doc>) { Doc.UserDoc().activeDashboard = val; } // prettier-ignore public static get MyFilterHotKeys() { return DocListCast(DocCast(DocCast(Doc.UserDoc().myContextMenuBtns)?.Filter)?.data).filter(key => key.toolType !== "-opts-"); } // prettier-ignore public static RemFromFilterHotKeys(doc: Doc) { - return Doc.RemoveDocFromList(DocCast(DocCast(Doc.UserDoc().myContextMenuBtns)?.Filter), 'data', doc); + return (filters => filters && Doc.RemoveDocFromList(filters, 'data', doc))(DocCast(DocCast(Doc.UserDoc().myContextMenuBtns)?.Filter)); } public static AddToFilterHotKeys(doc: Doc) { - return Doc.AddDocToList(DocCast(DocCast(Doc.UserDoc().myContextMenuBtns)?.Filter), 'data', doc); + return (btns => btns && Doc.AddDocToList(btns, 'data', doc))(DocCast(DocCast(Doc.UserDoc().myContextMenuBtns)?.Filter)); } public static IsInMyOverlay(doc: Doc) { return Doc.MyOverlayDocs.includes(doc); } // prettier-ignore - public static AddToMyOverlay(doc: Doc) { return Doc.ActiveDashboard ? Doc.AddDocToList(Doc.ActiveDashboard, 'myOverlayDocs', doc) : Doc.AddDocToList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore - public static RemFromMyOverlay(doc: Doc) { return Doc.ActiveDashboard ? Doc.RemoveDocFromList(Doc.ActiveDashboard,'myOverlayDocs', doc) : Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore - public static AddToMyPublished(doc: Doc) { - doc[DocData].title_custom = true; - doc[DocData].layout_showTitle = 'title'; - Doc.ActiveDashboard ? Doc.AddDocToList(Doc.ActiveDashboard, 'myPublishedDocs', doc) : Doc.AddDocToList(DocCast(Doc.UserDoc().myPublishedDocs), undefined, doc); } // prettier-ignore - public static RemFromMyPublished(doc: Doc){ - doc[DocData].title_custom = false; - doc[DocData].layout_showTitle = undefined; - Doc.ActiveDashboard ? Doc.RemoveDocFromList(Doc.ActiveDashboard,'myPublishedDocs', doc) : Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myPublishedDocs), undefined, doc); } // prettier-ignore + public static AddToMyOverlay(doc: Doc) { + return Doc.ActiveDashboard && Doc.AddDocToList(Doc.ActiveDashboard, 'myOverlayDocs', doc); + } // : Doc.AddDocToList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore + public static RemFromMyOverlay(doc: Doc) { + return Doc.ActiveDashboard && Doc.RemoveDocFromList(Doc.ActiveDashboard, 'myOverlayDocs', doc); + } // : Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore + public static AddToMyPublished(doc: Doc) { + doc.$title_custom = true; + doc.$layout_showTitle = 'title'; + Doc.ActiveDashboard && Doc.AddDocToList(Doc.ActiveDashboard, 'myPublishedDocs', doc); + } // : Doc.AddDocToList(DocCast(Doc.UserDoc().myPublishedDocs), undefined, doc); } // prettier-ignore + public static RemFromMyPublished(doc: Doc) { + doc.$title_custom = false; + doc.$layout_showTitle = undefined; + Doc.ActiveDashboard && Doc.RemoveDocFromList(Doc.ActiveDashboard, 'myPublishedDocs', doc); + } // : Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myPublishedDocs), undefined, doc); } // prettier-ignore public static IsComicStyle(doc?: Doc) { return doc && Doc.ActiveDashboard && !Doc.IsSystem(doc) && Doc.UserDoc().renderStyle === 'comic' ; } // prettier-ignore constructor(id?: FieldId, forceSave?: boolean) { @@ -320,10 +343,11 @@ export class Doc extends RefField { UpdatingFromServer, Width, '__LAYOUT__', + '__DATA__', ]; }, getOwnPropertyDescriptor: (target, prop) => { - if (prop.toString() === '__LAYOUT__' || !(prop in target[FieldKeys])) { + if (prop.toString() === '__DATA__' || prop.toString() === '__LAYOUT__' || !(prop in target[FieldKeys])) { return Reflect.getOwnPropertyDescriptor(target, prop); } return { @@ -400,23 +424,23 @@ export class Doc extends RefField { public [ToString] = () => `Doc(${GetEffectiveAcl(this[SelfProxy]) === AclPrivate ? '-inaccessible-' : this[SelfProxy].title})`; public get [DocLayout]() { return this[SelfProxy].__LAYOUT__; } // prettier-ignore public get [DocData](): Doc { + return this[SelfProxy].__DATA__; + } + @computed get __DATA__(): Doc { const self = this[SelfProxy]; - return self.resolvedDataDoc && !self.isTemplateForField ? self : Doc.GetProto(Cast(Doc.Layout(self).resolvedDataDoc, Doc, null) || self); + return self.rootDocument && !self.isTemplateForField ? self : Doc.GetProto(DocCast(self[DocLayout].rootDocument, self)!); } - @computed get __LAYOUT__(): Doc | undefined { + @computed get __LAYOUT__(): Doc { const self = this[SelfProxy]; const templateLayoutDoc = Cast(Doc.LayoutField(self), Doc, null); if (templateLayoutDoc) { - let renderFieldKey: string = ''; const layoutField = templateLayoutDoc[StrCast(templateLayoutDoc.layout_fieldKey, 'layout')]; - if (typeof layoutField === 'string') { - [renderFieldKey] = layoutField.split("fieldKey={'")[1].split("'"); // layoutField.split("'")[1]; - } else { - return Cast(layoutField, Doc, null); + if (typeof layoutField !== 'string') { + return DocCast(layoutField, self)!; } - return Cast(self[renderFieldKey + '_layout[' + templateLayoutDoc[Id] + ']'], Doc, null) || templateLayoutDoc; + return DocCast(self[expandedFieldName(templateLayoutDoc)], templateLayoutDoc)!; } - return undefined; + return self; } public async [HandleUpdate](diff: { $set: { [key: string]: FieldType } } | { $unset?: unknown }) { @@ -591,7 +615,7 @@ export namespace Doc { // return the doc's proto, but rather recursively searches through the proto inheritance chain // and returns the document who's proto is undefined or whose proto is marked as a data doc ('isDataDoc'). export function GetProto(doc: Doc): Doc { - const proto = doc && (Doc.GetT(doc, 'isDataDoc', 'boolean', true) ? doc : DocCast(doc.proto, doc)); + const proto = doc && (Doc.GetT(doc, 'isDataDoc', 'boolean', true) ? doc : DocCast(doc.proto, doc)!); return proto === doc ? proto : Doc.GetProto(proto); } export function GetDataDoc(doc: Doc): Doc { @@ -624,8 +648,8 @@ export namespace Doc { * @returns true if successful, false otherwise. */ export function RemoveDocFromList(listDoc: Doc, fieldKey: string | undefined, doc: Doc, ignoreProto = false) { - const key = fieldKey || Doc.LayoutFieldKey(listDoc); - const list = Doc.Get(listDoc, key, ignoreProto) === undefined ? (listDoc[DocData][key] = new List<Doc>()) : Cast(listDoc[key], listSpec(Doc)); + const key = fieldKey || Doc.LayoutDataKey(listDoc); + const list = Doc.Get(listDoc, key, ignoreProto) === undefined ? (listDoc['$' + key] = new List<Doc>()) : Cast(listDoc[key], listSpec(Doc)); if (list) { const ind = list.indexOf(doc); if (ind !== -1) { @@ -641,8 +665,8 @@ export namespace Doc { * @returns true if successful, false otherwise. */ export function AddDocToList(listDoc: Doc, fieldKey: string | undefined, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean, reversed?: boolean, ignoreProto?: boolean) { - const key = fieldKey || Doc.LayoutFieldKey(listDoc); - const list = Doc.Get(listDoc, key, ignoreProto) === undefined ? (listDoc[DocData][key] = new List<Doc>()) : Cast(listDoc[key], listSpec(Doc)); + const key = fieldKey || Doc.LayoutDataKey(listDoc); + const list = Doc.Get(listDoc, key, ignoreProto) === undefined ? (listDoc['$' + key] = new List<Doc>()) : Cast(listDoc[key], listSpec(Doc)); if (list) { if (!allowDuplicates) { const pind = list.findIndex(d => d instanceof Doc && d[Id] === doc[Id]); @@ -680,41 +704,36 @@ export namespace Doc { return DocListCast(Doc.Get(doc[DocData], 'proto_embeddings', true)); } + /** + * Makes an embedding of a Doc. This Doc shares the data portion of the origiginal Doc. + * If the copied Doc has no prototype, then instead of copying the Doc, this just creates + * a new Doc that is a delegate of the original Doc. + * @param doc Doc to embed + * @param id id to use for embedded Doc + * @returns a new Doc that is an embedding of the original Doc + */ export function MakeEmbedding(doc: Doc, id?: string) { - const embedding = (!GetT(doc, 'isDataDoc', 'boolean', true) && doc.proto) || doc.type === DocumentType.CONFIG ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id); - const layout = Doc.LayoutField(embedding); - if (layout instanceof Doc && layout !== embedding && layout === Doc.Layout(embedding)) { - Doc.SetLayout(embedding, Doc.MakeEmbedding(layout)); - } + const embedding = (!Doc.IsDataProto(doc) && doc.proto) || doc.type === DocumentType.CONFIG ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id); embedding.createdFrom = doc; - embedding.proto_embeddingId = doc[DocData].proto_embeddingId = Doc.GetEmbeddings(doc).length - 1; - !Doc.GetT(embedding, 'title', 'string', true) && (embedding.title = ComputedField.MakeFunction(`renameEmbedding(this)`)); embedding.author = ClientUtils.CurrentUserEmail(); + embedding.proto_embeddingId = doc.$proto_embeddingId = Doc.GetEmbeddings(doc).length - 1; + embedding.title = ComputedField.MakeFunction(`renameEmbedding(this)`); return embedding; } export function BestEmbedding(doc: Doc) { - const dataDoc = doc[DocData]; - const availableEmbeddings = Doc.GetEmbeddings(dataDoc); - const bestEmbedding = [...(dataDoc !== doc ? [doc] : []), ...availableEmbeddings].find(d => !d.embedContainer && d.author === ClientUtils.CurrentUserEmail()); + const availableEmbeddings = Doc.GetEmbeddings(doc); + const bestEmbedding = [...(doc[DocData] !== doc ? [doc] : []), ...availableEmbeddings].find(d => !d.embedContainer && d.author === ClientUtils.CurrentUserEmail()); bestEmbedding && Doc.AddEmbedding(doc, doc); return bestEmbedding ?? Doc.MakeEmbedding(doc); } // this lists out all the tag ids that can be in a RichTextField that might contain document ids. // if a document is cloned, we need to make sure to clone all of these referenced documents as well; - export const DocsInTextFieldIds = ['audioId', 'textId', 'anchorId', 'docId']; - export async function makeClone( - doc: Doc, - cloneMap: Map<string, Doc>, - linkMap: Map<string, Doc>, - rtfs: { copy: Doc; key: string; field: RichTextField }[], - exclusions: string[], - pruneDocs: Doc[], - cloneLinks: boolean, - cloneTemplates: boolean - ): Promise<Doc> { + const FindDocsInRTF = new RegExp(/(audioId|textId|anchorId|docId)"\s*:\s*"(.*?)"/g); + + export function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<string, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], pruneDocs: Doc[], cloneLinks: boolean, cloneTemplates: boolean): Doc { if (Doc.IsBaseProto(doc) || ((Doc.isTemplateDoc(doc) || Doc.isTemplateForField(doc)) && !cloneTemplates)) { return doc; } @@ -722,81 +741,59 @@ export namespace Doc { const copy = new Doc(undefined, true); cloneMap.set(doc[Id], copy); const filter = [...exclusions, ...StrListCast(doc.cloneFieldFilter)]; - await Promise.all( - Object.keys(doc).map(async key => { - if (filter.includes(key)) return; - const assignKey = (val: Opt<FieldType>) => { - copy[key] = val; - }; - const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - const field = ProxyField.WithoutProxy(() => doc[key]); - const copyObjectField = async (objField: 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, pruneDocs, cloneLinks, cloneTemplates))); - assignKey(new List<Doc>(clones)); - } else { - assignKey(ObjectField.MakeCopy(objField)); - if (objField instanceof RichTextField) { - if (DocsInTextFieldIds.some(id => objField.Data.includes(`"${id}":`))) { - const docidsearch = new RegExp('(' + DocsInTextFieldIds.map(exp => '(' + exp + ')').join('|') + ')":"([a-z-A-Z0-9_]*)"', 'g'); - const rawdocids = objField.Data.match(docidsearch); - const docids = rawdocids?.map((str: string) => - DocsInTextFieldIds.reduce((output, exp) => output.replace(new RegExp(`${exp}":`, 'g'), ''), str) - .replace(/"/g, '') - .trim() - ); - const results = docids && (await DocServer.GetRefFields(docids)); - const rdocs = results && Array.from(Object.keys(results)).map(rkey => DocCast(results.get(rkey))); - rdocs?.map(d => d && Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); - rtfs.push({ copy, key, field: objField }); - } - } - } - }; - const docAtKey = doc[key]; + Object.keys(doc) + .filter(key => !filter.includes(key)) + .map(key => { + const assignKey = (val: Opt<FieldType>) => (copy[key] = val); + if (key === 'author') { - assignKey(ClientUtils.CurrentUserEmail()); - } else if (docAtKey instanceof Doc) { - if (pruneDocs.includes(docAtKey)) { - // prune doc and do nothing - } else if ( - !Doc.IsSystem(docAtKey) && - (key.includes('layout[') || - key.startsWith('layout') || // - ['embedContainer', 'annotationOn', 'proto'].includes(key) || - (['link_anchor_1', 'link_anchor_2'].includes(key) && doc.author === ClientUtils.CurrentUserEmail())) - ) { - assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); - } else { - assignKey(docAtKey); + return assignKey(ClientUtils.CurrentUserEmail()); + } + const cfield = ComputedField.DisableCompute(() => doc[key]); + if (cfield instanceof ComputedField) { + return assignKey(cfield[Copy]()); + } + const field = doc[key]; + if (field instanceof Doc) { + const doCopy = () => Doc.IsSystem(field) || + !( key.startsWith('layout') || + ['embedContainer', 'annotationOn', 'proto'].includes(key) || // + (['link_anchor_1', 'link_anchor_2'].includes(key) && doc.author === ClientUtils.CurrentUserEmail()) ); // prettier-ignore + return !pruneDocs.includes(field) && + assignKey(doCopy() + ? field // + : Doc.makeClone(field, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); // prettier-ignore + } + if (field instanceof RichTextField) { + rtfs.push({ copy, key, field }); + let docId: string | undefined; + while ((docId = (FindDocsInRTF.exec(field.Data) ?? [undefined, undefined, undefined])[2])) { + const docCopy = DocServer.GetCachedRefField(docId); + docCopy && Doc.makeClone(docCopy, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates); } - } else if (field instanceof RefField) { - assignKey(field); - } else if (cfield instanceof ComputedField) { - assignKey(cfield[Copy]()); - } else if (field instanceof ObjectField) { - await copyObjectField(field); - } else if (field instanceof Promise) { - // eslint-disable-next-line no-debugger - debugger; // This shouldn't happen... - } else { - assignKey(field); + return assignKey(ObjectField.MakeCopy(field)); } - }) - ); - Array.from(doc[DirectLinks]).forEach(async link => { + if (field instanceof ObjectField) { + if (DocListCast(field).length) { + return assignKey(new List<Doc>(DocListCast(field).map(d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)))); + } + return assignKey(ObjectField.MakeCopy(field)); // otherwise just copy the field + } + if (!(field instanceof Promise)) return assignKey(field); + // eslint-disable-next-line no-debugger + debugger; // This shouldn't happen... + }); + Array.from(doc[DirectLinks]).forEach(link => { if ( cloneLinks || - ((cloneMap.has(DocCast(link.link_anchor_1)?.[Id]) || cloneMap.has(DocCast(DocCast(link.link_anchor_1)?.annotationOn)?.[Id])) && - (cloneMap.has(DocCast(link.link_anchor_2)?.[Id]) || cloneMap.has(DocCast(DocCast(link.link_anchor_2)?.annotationOn)?.[Id]))) + ((cloneMap.has(DocCast(link.link_anchor_1)?.[Id] ?? '') || cloneMap.has(DocCast(DocCast(link.link_anchor_1)?.annotationOn)?.[Id] ?? '')) && + (cloneMap.has(DocCast(link.link_anchor_2)?.[Id] ?? '') || cloneMap.has(DocCast(DocCast(link.link_anchor_2)?.annotationOn)?.[Id] ?? ''))) ) { - linkMap.set(link[Id], await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); + linkMap.set(link[Id], Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); } }); copy.cloneOf = doc; - const cfield = ComputedField.WithoutComputed(() => FieldValue(doc.title)); + const cfield = ComputedField.DisableCompute(() => FieldValue(doc.title)); if (Doc.Get(copy, 'title', true) && !(cfield instanceof ComputedField)) copy.title = '>:' + doc.title; cloneMap.set(doc[Id], copy); @@ -823,25 +820,27 @@ export namespace Doc { return docs.map(doc => Doc.MakeClone(doc, cloneLinks, cloneTemplates, cloneMap)); } - export async function MakeClone(doc: Doc, cloneLinks = true, cloneTemplates = true, cloneMap: Map<string, Doc> = new Map()) { + /** + * Copies a Doc and all of the Docs that it references. This is a deep copy of the Doc. + * However, the additional flags allow you to control whether to copy links and templates. + * @param doc Doc to clone + * @param cloneLinks whether to clone links to this Doc + * @param cloneTemplates whether to clone the templates used by this Doc + * @param cloneMap a map from the Doc ids of the original Doc to the new Docs + * @returns a clone of the original Doc + */ + export function MakeClone(doc: Doc, cloneLinks = true, cloneTemplates = true, cloneMap: Map<string, Doc> = new Map()) { const linkMap = new Map<string, Doc>(); const rtfMap: { copy: Doc; key: string; field: RichTextField }[] = []; - const clone = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], doc.embedContainer ? [DocCast(doc.embedContainer)] : [], cloneLinks, cloneTemplates); + const clone = Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], DocCast(doc.embedContainer) ? [DocCast(doc.embedContainer)!] : [], cloneLinks, cloneTemplates); const repaired = new Set<Doc>(); const linkedDocs = Array.from(linkMap.values()); linkedDocs.forEach(link => Doc.AddLink?.(link, true)); rtfMap.forEach(({ copy, key, field }) => { - const replacer = (match: string, attr: string, id: string /* , offset: any, string: any */) => { - const mapped = cloneMap.get(id); - return attr + '"' + (mapped ? mapped[Id] : id) + '"'; - }; - const replacer2 = (match: string, href: string, id: string /* , offset: any, string: any */) => { - const mapped = cloneMap.get(id); - return href + (mapped ? mapped[Id] : id); - }; + const replacer = (match: string, attr: string, id: string) => attr + '":"' + (cloneMap.get(id)?.[Id] ?? id) + '"'; + const replacer2 = (match: string, href: string, id: string) => href + (cloneMap.get(id)?.[Id] ?? id); const re = new RegExp(`(${Doc.localServerPath()})([^"]*)`, 'g'); - const docidsearch = new RegExp('(' + DocsInTextFieldIds.map(exp => `"${exp}":`).join('|') + ')"([^"]+)"', 'g'); - copy[key] = new RichTextField(field.Data.replace(docidsearch, replacer).replace(re, replacer2), field.Text); + copy[key] = new RichTextField(field.Data.replace(FindDocsInRTF, replacer).replace(re, replacer2), field.Text); }); const clonedDocs = [...Array.from(cloneMap.values()), ...linkedDocs]; clonedDocs.forEach(cloneDoc => Doc.repairClone(cloneDoc, cloneMap, cloneTemplates, repaired)); @@ -849,34 +848,40 @@ export namespace Doc { } const _pendingMap = new Set<string>(); - // - // Returns an expanded template layout for a target data document if there is a template relationship - // between the two. If so, the layoutDoc is expanded into a new document that inherits the properties - // of the original layout while allowing for individual layout properties to be overridden in the expanded layout. - export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc) { + /** + * Returns an expanded template layout for a target data document if there is a template relationship + * between the two. If so, the layoutDoc is expanded into a new document that inherits the properties + * of the original layout while allowing for individual layout properties to be overridden in the expanded layout + * @param templateLayoutDoc a rendering template Doc + * @param targetDoc the Doc that the render template will be applied to + * @param layoutFieldKey the accumulated layoutFieldKey for the container of this expanded template + * @returns a Doc to use to render the targetDoc in the style of the template layout + */ + export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc, layoutFieldKey?: string) { // nothing to do if the layout isn't a template or we don't have a target that's different than the template if (!targetDoc || templateLayoutDoc === targetDoc || (!Doc.isTemplateForField(templateLayoutDoc) && !Doc.isTemplateDoc(templateLayoutDoc))) { return templateLayoutDoc; } - const templateField = StrCast(templateLayoutDoc.isTemplateForField, Doc.LayoutFieldKey(templateLayoutDoc)); // the field that the template renders + const templateField = StrCast(templateLayoutDoc.isTemplateForField, Doc.LayoutDataKey(templateLayoutDoc)); // 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 expandedLayoutFieldKey = templateField + '_layout[' + templateLayoutDoc[Id] + ']'; + const expandedLayoutFieldKey = expandedFieldName(templateLayoutDoc, layoutFieldKey); let expandedTemplateLayout = targetDoc?.[expandedLayoutFieldKey]; - if (templateLayoutDoc.resolvedDataDoc instanceof Promise) { + if (templateLayoutDoc.rootDocument instanceof Promise) { expandedTemplateLayout = undefined; _pendingMap.add(targetDoc[Id] + expandedLayoutFieldKey); } else if (expandedTemplateLayout === undefined && !_pendingMap.has(targetDoc[Id] + expandedLayoutFieldKey)) { - if (templateLayoutDoc.resolvedDataDoc === targetDoc[DocData]) { + if (DocCast(templateLayoutDoc.rootDocument)?.[DocData] === targetDoc[DocData]) { expandedTemplateLayout = templateLayoutDoc; // reuse an existing template layout if its for the same document with the same params } else { // eslint-disable-next-line no-param-reassign - templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = DocCast(templateLayoutDoc.proto, templateLayoutDoc)); // if the template has already been applied (ie, a nested template), then use the template's prototype + templateLayoutDoc.rootDocument && (templateLayoutDoc = DocCast(templateLayoutDoc.proto, templateLayoutDoc)!); // if the template has already been applied (ie, a nested template), then use the template's prototype if (!targetDoc[expandedLayoutFieldKey]) { _pendingMap.add(targetDoc[Id] + expandedLayoutFieldKey); setTimeout( @@ -885,7 +890,7 @@ export namespace Doc { const dataDoc = Doc.GetProto(targetDoc); newLayoutDoc.rootDocument = targetDoc; newLayoutDoc.embedContainer = targetDoc; - newLayoutDoc.resolvedDataDoc = dataDoc; + newLayoutDoc.cloneOnCopy = true; newLayoutDoc.acl_Guest = SharingPermissions.Edit; if (dataDoc[templateField] === undefined && (templateLayoutDoc[templateField] as List<Doc>)?.length) { dataDoc[templateField] = ObjectField.MakeCopy(templateLayoutDoc[templateField] as List<Doc>); @@ -902,87 +907,78 @@ export namespace Doc { return expandedTemplateLayout instanceof Doc ? expandedTemplateLayout : undefined; // layout is undefined if the expandedTemplateLayout is pending. } - // if the childDoc is a template for a field, then this will return the expanded layout with its data doc. - // otherwise, it just returns the childDoc - export function GetLayoutDataDocPair(containerDoc: Doc, containerDataDoc: Opt<Doc>, childDoc: Doc) { + /** + * Returns a layout and data Doc pair to use to render the specified childDoc of the container. + * if the childDoc is a template for a field, then this will return the expanded layout with its data doc. + * otherwise, only a layout is returned since that will contain the data as its prototype. + * @param containerDoc the template container (that may be nested within a template, the root of a template, or not a template) + * @param containerDataDoc the template container's data doc (if the container is nested within the template) + * @param childDoc the doc to render + * @param layoutFieldKey the accumulated layoutFieldKey for the container of the doc being rendered + * @returns a layout Doc to render and an optional data Doc if the layout is a template + */ + export function GetLayoutDataDocPair(containerDoc: Doc, containerDataDoc: Opt<Doc>, childDoc: Doc, layoutFieldKey?: string) { if (!childDoc || childDoc instanceof Promise || !Doc.GetProto(childDoc)) { console.log('Warning: GetLayoutDataDocPair childDoc not defined'); return { layout: childDoc, data: childDoc }; } - const resolvedDataDoc = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!Doc.isTemplateDoc(childDoc) && !Doc.isTemplateForField(childDoc)) ? undefined : containerDataDoc; + const data = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!Doc.isTemplateDoc(childDoc) && !Doc.isTemplateForField(childDoc)) ? undefined : containerDataDoc; const templateRoot = DocCast(containerDoc?.rootDocument); - return { layout: Doc.expandTemplateLayout(childDoc, templateRoot), data: resolvedDataDoc }; + return { layout: Doc.expandTemplateLayout(childDoc, templateRoot, layoutFieldKey), data }; } - export function FindReferences(infield: Doc | List<Doc>, references: Set<Doc>, system: boolean | undefined) { - if (infield instanceof Promise) return; - if (!(infield instanceof Doc)) { - infield?.forEach(val => (val instanceof Doc || val instanceof List) && FindReferences(val, references, system)); - return; + /** + * Recursively travels through all the metadata reachable from the Doc to find all referenced Docs + * @param doc Doc to search + * @param references all Docs reachable from the Doc + * @param system whether to include system Docs + * @returns all Docs reachable from the Doc + */ + export function FindReferences(doc: Doc | List<Doc> | undefined, references: Set<Doc>, system: boolean | undefined) { + if (!doc || doc instanceof Promise) { + return references; + } + if (!(doc instanceof Doc)) { + doc?.forEach(val => (val instanceof Doc || val instanceof List) && FindReferences(val, references, system)); + return references; } - const doc = infield as Doc; if (references.has(doc)) { - references.add(doc); - return; + return references; + } + if (system !== undefined && ((system && !Doc.IsSystem(doc)) || (!system && Doc.IsSystem(doc)))) { + return references; } - const excludeLists = [Doc.MyRecentlyClosed, Doc.MyHeaderBar, Doc.MyDashboards].includes(doc); - if (system !== undefined && ((system && !Doc.IsSystem(doc)) || (!system && Doc.IsSystem(doc)))) return; + references.add(doc); - Object.keys(doc).forEach(key => { - if (key === 'proto') { - if (doc.proto instanceof Doc) { - Doc.FindReferences(doc.proto, references, system); + Object.keys(doc) + .filter(key => key !== 'author' && !(ComputedField.DisableCompute(() => FieldValue(doc[key])) instanceof ComputedField)) + .map(key => doc[key]) + .forEach(field => { + if (field instanceof List) { + return ![Doc.MyRecentlyClosed, Doc.MyHeaderBar, Doc.MyDashboards].includes(doc) && Doc.FindReferences(field, references, system); } - } else { - const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - const field = key === 'author' ? ClientUtils.CurrentUserEmail() : ProxyField.WithoutProxy(() => doc[key]); - if (field instanceof RefField) { - if (field instanceof Doc) { - if (key === 'myLinkDatabase') { - field instanceof Doc && references.add(field); - // skip docs that have been closed and are scheduled for garbage collection - } else { - Doc.FindReferences(field, references, system); - } - } - } else if (cfield instanceof ComputedField) { - /* empty */ - } else if (field instanceof ObjectField) { - if (field instanceof Doc) { - Doc.FindReferences(field, references, system); - } else if (field instanceof List) { - !excludeLists && Doc.FindReferences(field, references, system); - } else if (field instanceof ProxyField) { - if (key === 'myLinkDatabase') { - field instanceof Doc && references.add(field); - // skip docs that have been closed and are scheduled for garbage collection - } else { - Doc.FindReferences(field.value, references, system); - } - } else if (field instanceof PrefetchProxy) { - Doc.FindReferences(field.value, references, system); - } else if (field instanceof RichTextField) { - const re = /"docId"\s*:\s*"(.*?)"/g; - let match: string[] | null; - while ((match = re.exec(field.Data)) !== null) { - const urlString = match[1]; - if (urlString) { - const rdoc = DocServer.GetCachedRefField(urlString); - if (rdoc) { - references.add(rdoc); - Doc.FindReferences(rdoc, references, system); - } - } - } + if (field instanceof Doc) { + return Doc.FindReferences(field, references, system); + } + if (field instanceof RichTextField) { + let docId: string | undefined; + while ((docId = (FindDocsInRTF.exec(field.Data) ?? [undefined, undefined, undefined])[2])) { + Doc.FindReferences(DocServer.GetCachedRefField(docId), references, system); } - } else if (field instanceof Promise) { - // eslint-disable-next-line no-debugger - debugger; // This shouldn't happend... } - } - }); + }); + return references; } + /** + * Copies a Doc by copying its embedding (and optionally its prototype). Values within the Doc are copied except for Docs which are + * sipmly referenced (except if they're marked to be copied - eg., template layout Docs) + * @param doc Doc to copy + * @param copyProto whether to copy the Docs proto + * @param copyProtoId the id to use for the proto if copied + * @param retitle whether to retitle the copy by adding a copy number to the title + * @returns the copied Doc + */ export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string, retitle = false): Doc { const copy = runInAction(() => new Doc(copyProtoId, true)); updateCachedAcls(copy); @@ -995,24 +991,18 @@ export namespace Doc { copy[key] = Doc.MakeCopy(doc.proto, false); } } else { - const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - const field = key === 'author' ? ClientUtils.CurrentUserEmail() : ProxyField.WithoutProxy(() => doc[key]); - if (field instanceof RefField) { - copy[key] = field; - } else if (cfield instanceof ComputedField) { - copy[key] = cfield[Copy](); // ComputedField.MakeFunction(cfield.script.originalScript); - } else if (field instanceof ObjectField) { - const docAtKey = doc[key]; - copy[key] = - docAtKey instanceof Doc && (key.includes('layout[') || docAtKey.cloneOnCopy) - ? new ProxyField(Doc.MakeCopy(docAtKey)) // copy the expanded render template - : ObjectField.MakeCopy(field); - } else if (field instanceof Promise) { + const field = key === 'author' ? ClientUtils.CurrentUserEmail() : doc[key]; + if (field instanceof Promise) { // eslint-disable-next-line no-debugger debugger; // This shouldn't happend... - } else { - copy[key] = field; } + copy[key] = (() => { + const cfield = ComputedField.DisableCompute(() => FieldValue(doc[key])); + if (cfield instanceof ComputedField) return cfield[Copy](); + if (field instanceof Doc) return field.cloneOnCopy ? Doc.MakeCopy(field) : field; // copy the expanded render template + if (field instanceof ObjectField) return ObjectField.MakeCopy(field); + return field; + })(); } }); if (copyProto) { @@ -1028,6 +1018,14 @@ export namespace Doc { return copy; } + /** + * Makes a delegate of a prototype Doc. Delegates inherit all of the properties of the + * prototype Doc, but can add new properties or mask existing prototype properties. + * @param doc prototype Doc to make a delgate of + * @param id id to use for delegate + * @param title title to use for delegate + * @returns a new Doc that is a delegate of the original Doc + */ 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> { @@ -1063,14 +1061,14 @@ export namespace Doc { return ndoc; } - let _applyCount: number = 0; export function ApplyTemplate(templateDoc: Doc) { if (templateDoc) { const proto = new Doc(); + const applyCount = NumCast(templateDoc.dragFactory_count); proto.author = ClientUtils.CurrentUserEmail(); const target = Doc.MakeDelegate(proto); const targetKey = StrCast(templateDoc.layout_fieldKey, 'layout'); - const applied = ApplyTemplateTo(templateDoc, target, targetKey, templateDoc.title + '(...' + _applyCount++ + ')'); + const applied = ApplyTemplateTo(templateDoc, target, targetKey, templateDoc.title + '(...' + applyCount + ')'); target.layout_fieldKey = targetKey; //this and line above applied && (Doc.GetProto(applied).type = templateDoc.type); return applied; @@ -1080,7 +1078,7 @@ export namespace Doc { export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetKey: string, titleTarget: string | undefined) { if (!Doc.AreProtosEqual(target[targetKey] as Doc, templateDoc)) { - if (target.resolvedDataDoc) { + if (target.rootDocument) { target[targetKey] = new PrefetchProxy(templateDoc); } else { titleTarget && (Doc.GetProto(target).title = titleTarget); @@ -1097,13 +1095,13 @@ export namespace Doc { // export function MakeMetadataFieldTemplate(templateField: Doc, templateDoc: Opt<Doc>, keepFieldKey = false): boolean { // find the metadata field key that this template field doc will display (indicated by its title) - const metadataFieldKey = keepFieldKey ? Doc.LayoutFieldKey(templateField) : StrCast(templateField.isTemplateForField) || StrCast(templateField.title).replace(/^-/, '') || Doc.LayoutFieldKey(templateField); + const metadataFieldKey = keepFieldKey ? Doc.LayoutDataKey(templateField) : StrCast(templateField.isTemplateForField) || StrCast(templateField.title).replace(/^-/, '') || Doc.LayoutDataKey(templateField); // update the original template to mark it as a template templateField.isTemplateForField = metadataFieldKey; !keepFieldKey && (templateField.title = metadataFieldKey); - const templateFieldValue = templateField[metadataFieldKey] || templateField[Doc.LayoutFieldKey(templateField)]; + const templateFieldValue = templateField[metadataFieldKey] || templateField[Doc.LayoutDataKey(templateField)]; // move any data that the template field had been rendering over to the template doc so that things will still be rendered // when the template field is adjusted to point to the new metadatafield key. // note 1: if the template field contained a list of documents, each of those documents will be converted to templates as well. @@ -1112,11 +1110,15 @@ export namespace Doc { Cast(templateFieldValue, listSpec(Doc), [])?.map(d => d instanceof Doc && MakeMetadataFieldTemplate(d, templateDoc)); Doc.GetProto(templateField)[metadataFieldKey] = ObjectField.MakeCopy(templateFieldValue); } + if (templateField.type === DocumentType.IMG) { + // bcz: should be a better way .. but, if the image is a template, then we can't expect to know the aspect ratio. When the image is replaced by data and rendered, we want to recomputed the native dimensions. + templateField[DocData].layout_resetNativeDim = true; + } // get the layout string that the template uses to specify its layout - const templateFieldLayoutString = StrCast(Doc.LayoutField(Doc.Layout(templateField))); + const templateFieldLayoutString = StrCast(Doc.LayoutField(templateField[DocLayout])); // change it to render the target metadata field instead of what it was rendering before and assign it to the template field layout document. - Doc.Layout(templateField).layout = templateFieldLayoutString.replace(/fieldKey={'[^']*'}/, `fieldKey={'${metadataFieldKey}'}`); + templateField[DocLayout].layout = templateFieldLayoutString.replace(/fieldKey={'[^']*'}/, `fieldKey={'${metadataFieldKey}'}`); return true; } @@ -1149,58 +1151,96 @@ export namespace Doc { @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); - return overrideLayout || doc[DocLayout] || doc; - } - export function SetLayout(doc: Doc, layout: Doc | string) { - doc[StrCast(doc.layout_fieldKey, 'layout')] = layout; + /** + * The layout Doc containing the view layout information - this will be : + * a) the Doc being rendered itself unless + * b) a template Doc stored in the field sepcified by Doc's layout_fieldKey + * c) or the specifeid 'template' Doc; + * If a template is specified, it will be expanded to create an instance specific to the rendered doc; + * @param doc the doc to render + * @param template a Doc to use as a template for the layout + * @returns + */ + export function LayoutDoc(doc: Doc, template?: Doc): Doc { + const expandedTemplate = template && Cast(doc[expandedFieldName(template)], Doc, null); + return expandedTemplate || doc[DocLayout]; } + /** + * The JSX or Doc value defining how to render the Doc. + * @param doc Doc to render + * @returns a JSX string or Doc that describes how to render the Doc + */ export function LayoutField(doc: Doc) { return doc[StrCast(doc.layout_fieldKey, 'layout')]; } - export function LayoutFieldKey(doc: Doc, templateLayoutString?: string): string { - const match = StrCast(templateLayoutString || Doc.Layout(doc).layout).match(/fieldKey={'([^']+)'}/); - return match?.[1] || ''; // bcz: TODO check on this . used to always reference 'layout', now it uses the layout speicfied by the current layout_fieldKey + /** + * The field key of the Doc where the primary Data can be found to render the Doc. + * eg., for an image, this is likely to be the 'data' field which contains an image url, + * and for a text doc, this is likely to be the 'text' field ontaingin the text of the doc. + * @param doc Doc to render + * @param templateLayoutString optional JSX string that specifies the doc's data field key + * @returns field key where data is stored on Doc + */ + export function LayoutDataKey(doc: Doc, templateLayoutString?: string): string { + const match = StrCast(templateLayoutString || doc[DocLayout].layout).match(/fieldKey={'([^']+)'}/); + return match?.[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 ? NumCast(doc._width) : 0)); + // if this is a field template, then don't use the doc's nativeWidth/height + return !doc ? 0 : NumCast(doc.isTemplateForField ? undefined : doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeWidth'], !doc.isTemplateForField && useWidth ? NumCast(doc._width) : 0)); } export function NativeHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) { if (!doc) return 0; const nheight = (Doc.NativeWidth(doc, dataDoc, useHeight) / NumCast(doc._width)) * NumCast(doc._height); // divide before multiply to avoid floating point errrorin case nativewidth = width - const dheight = NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '_nativeHeight'], useHeight ? NumCast(doc._height) : 0); - return NumCast(doc._nativeHeight, nheight || dheight); + const dheight = NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeHeight'], useHeight ? NumCast(doc._height) : 0); + // if this is a field template, then don't use the doc's nativeWidth/height + return NumCast(doc.isTemplateForField ? undefined : doc._nativeHeight, nheight || dheight); + } + + export function OutpaintingWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) { + return !doc ? 0 : NumCast(doc._outpaintingWidth, NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_outpaintingWidth'], useWidth ? NumCast(doc._width) : 0)); + } + + export function OutpaintingHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) { + if (!doc) return 0; + const oheight = (Doc.OutpaintingWidth(doc, dataDoc, useHeight) / NumCast(doc._width)) * NumCast(doc._height); + const dheight = NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_outpaintingHeight'], useHeight ? NumCast(doc._height) : 0); + return NumCast(doc._outpaintingHeight, oheight || dheight); + } + + export function SetOutpaintingWidth(doc: Doc, width: number | undefined, fieldKey?: string) { + doc[(fieldKey || Doc.LayoutDataKey(doc)) + '_outpaintingWidth'] = width; + } + + export function SetOutpaintingHeight(doc: Doc, height: number | undefined, fieldKey?: string) { + doc[(fieldKey || Doc.LayoutDataKey(doc)) + '_outpaintingHeight'] = height; } + export function SetNativeWidth(doc: Doc, width: number | undefined, fieldKey?: string) { - doc[(fieldKey || Doc.LayoutFieldKey(doc)) + '_nativeWidth'] = width; + doc[(fieldKey || Doc.LayoutDataKey(doc)) + '_nativeWidth'] = width; } export function SetNativeHeight(doc: Doc, height: number | undefined, fieldKey?: string) { - doc[(fieldKey || Doc.LayoutFieldKey(doc)) + '_nativeHeight'] = height; + doc[(fieldKey || Doc.LayoutDataKey(doc)) + '_nativeHeight'] = height; } const manager = new UserDocData(); - export function SearchQuery(): string { + export function SearchQuery() { return manager._searchQuery; } export function SetSearchQuery(query: string) { - runInAction(() => { - manager._searchQuery = query; - }); + manager._searchQuery = query; } - export function UserDoc(): Doc { + export function UserDoc() { return manager._user_doc; } - export function SharingDoc(): Doc { + export function SharingDoc() { return Doc.MySharedDocs; } - export function LinkDBDoc(): Doc { - return Cast(Doc.UserDoc().myLinkDatabase, Doc, null); + export function LinkDBDoc() { + return DocCast(Doc.UserDoc().myLinkDatabase); } export function SetUserDoc(doc: Doc) { return (manager._user_doc = doc); @@ -1225,7 +1265,7 @@ export namespace Doc { if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(doc[DocData]) === AclPrivate) return doc; const result = brushManager.SearchMatchDoc.get(doc); const num = Math.abs(result?.searchMatch || 0) + 1; - runInAction(() => result && brushManager.SearchMatchDoc.set(doc, { searchMatch: backward ? -num : num })); + result && brushManager.SearchMatchDoc.set(doc, { searchMatch: backward ? -num : num }); return doc; } export function ClearSearchMatches() { @@ -1298,12 +1338,12 @@ export namespace Doc { highlightedDocs.add(doc); doc[Highlight] = true; doc[Animation] = presentationEffect; - if (dataAndDisplayDocs && !doc.resolvedDataDoc) { + if (dataAndDisplayDocs && !doc.rootDocument) { // if doc is a layout template then we don't want to highlight the proto since that will be the entire template, not just the specific layout field highlightedDocs.add(doc[DocData]); doc[DocData][Highlight] = true; // want to highlight the targets of presentation docs explicitly since following a pres target does not highlight PDf <Annotations> which are not DocumentViews - if (doc.presentation_targetDoc) DocCast(doc.presentation_targetDoc)[Highlight] = true; + if (DocCast(doc.presentation_targetDoc)) DocCast(doc.presentation_targetDoc)![Highlight] = true; } }); } @@ -1315,23 +1355,13 @@ export namespace Doc { highlightedDocs.delete(doc[DocData]); doc[Highlight] = doc[DocData][Highlight] = false; doc[Animation] = undefined; - if (doc.presentation_targetDoc) DocCast(doc.presentation_targetDoc)[Highlight] = false; + if (DocCast(doc.presentation_targetDoc)) DocCast(doc.presentation_targetDoc)![Highlight] = false; }); }); } 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 : doc[DocLayout].isTemplateDoc ? (doc[DocLayout].rootDocument ? doc[DocLayout].proto : doc[DocLayout]) : undefined; } export function toggleLockedPosition(doc: Doc) { @@ -1352,7 +1382,7 @@ export namespace Doc { export function setDocRangeFilter(container: Opt<Doc>, key: string, range?: readonly number[], modifiers?: 'remove') { if (!container) return; - const childFiltersByRanges = Cast(container._childFiltersByRanges, listSpec('string'), []); + const childFiltersByRanges = StrListCast(container._childFiltersByRanges); for (let i = 0; i < childFiltersByRanges.length; i += 3) { if (childFiltersByRanges[i] === key) { @@ -1360,7 +1390,7 @@ export namespace Doc { break; } } - if (range !== undefined) { + if (range) { childFiltersByRanges.push(key); childFiltersByRanges.push(range[0].toString()); childFiltersByRanges.push(range[1].toString()); @@ -1415,7 +1445,7 @@ export namespace Doc { }); } export function readDocRangeFilter(doc: Doc, key: string) { - const childFiltersByRanges = Cast(doc._childFiltersByRanges, listSpec('string'), []); + const childFiltersByRanges = StrListCast(doc._childFiltersByRanges); for (let i = 0; i < childFiltersByRanges.length; i += 3) { if (childFiltersByRanges[i] === key) { return [Number(childFiltersByRanges[i + 1]), Number(childFiltersByRanges[i + 2])]; @@ -1450,8 +1480,9 @@ export namespace Doc { DocServer.GetRefFields(docids).then(async fieldlist => { const list = Array.from(fieldlist.values()) .map(d => DocCast(d)) - .filter(d => d); - const docs = clone ? (await Promise.all(Doc.MakeClones(list, false, false))).map(res => res.clone) : list; + .filter(d => d) + .map(d => d!); + const docs = clone ? Doc.MakeClones(list, false, false).map(res => res.clone) : list; if (ptx !== undefined && pty !== undefined && newPoint !== undefined) { const firstx = list.length ? NumCast(list[0].x) + ptx - newPoint[0] : 0; const firsty = list.length ? NumCast(list[0].y) + pty - newPoint[1] : 0; @@ -1471,16 +1502,22 @@ export namespace Doc { * @returns */ export function getDescription(doc: Doc) { - const curDescription = StrCast(doc[DocData][Doc.LayoutFieldKey(doc) + '_description']); + const curDescription = StrCast(doc['$' + Doc.LayoutDataKey(doc) + '_description']); const docText = (async (tdoc:Doc) => { switch (tdoc.type) { case DocumentType.PDF: return curDescription || StrCast(tdoc.text).split(/\s+/).slice(0, 50).join(' '); // first 50 words of pdf text - case DocumentType.IMG: return curDescription || imageUrlToBase64(ImageCastWithSuffix(Doc.LayoutField(tdoc), '_o') ?? '') + case DocumentType.IMG: return curDescription || imageUrlToBase64(ImageCastWithSuffix(tdoc[Doc.LayoutDataKey(tdoc)], '_o') ?? '') .then(hrefBase64 => gptImageLabel(hrefBase64, 'Give three to five labels to describe this image.')); - case DocumentType.RTF: return RTFCast(tdoc[Doc.LayoutFieldKey(tdoc)]).Text; + case DocumentType.RTF: return RTFCast(tdoc[Doc.LayoutDataKey(tdoc)])?.Text ?? StrCast(tdoc[Doc.LayoutDataKey(tdoc)]); default: return StrCast(tdoc.title).startsWith("Untitled") ? "" : StrCast(tdoc.title); }}); // prettier-ignore - return docText(doc).then(text => (doc[DocData][Doc.LayoutFieldKey(doc) + '_description'] = text)); + return docText(doc).then( + action(text => { + // set the time when the date changes. This also allows a live textbox view to react to the update, otherwise, it wouldn't take effect until the next time the view is rerendered. + doc['$' + Doc.LayoutDataKey(doc) + '_description_modificationDate'] = new DateField(); + return (doc['$' + Doc.LayoutDataKey(doc) + '_description'] = text); + }) + ); } // prettier-ignore @@ -1728,12 +1765,12 @@ export function IdToDoc(id: string) { return DocCast(DocServer.GetCachedRefField(id)); } // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function idToDoc(id: string): Doc { +ScriptingGlobals.add(function idToDoc(id: string): Doc | undefined { return IdToDoc(id); }); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function renameEmbedding(doc: Doc) { - return StrCast(doc[DocData].title).replace(/\([0-9]*\)/, '') + `(${doc.proto_embeddingId})`; + return StrCast(doc.title).replace(/\([0-9]*\)/, '') + `(${doc.proto_embeddingId})`; }); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function getProto(doc: Doc) { @@ -1782,7 +1819,7 @@ ScriptingGlobals.add(function docCastAsync(doc: FieldResult): FieldResult<Doc> { // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function activePresentationItem() { const curPres = Doc.ActivePresentation; - return curPres && DocListCast(curPres[Doc.LayoutFieldKey(curPres)])[NumCast(curPres._itemIndex)]; + return curPres && DocListCast(curPres[Doc.LayoutDataKey(curPres)])[NumCast(curPres._itemIndex)]; }); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function setDocFilter(container: Doc, key: string, value: string, modifiers: 'match' | 'check' | 'x' | 'remove') { |