aboutsummaryrefslogtreecommitdiff
path: root/src/fields/Doc.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/fields/Doc.ts')
-rw-r--r--src/fields/Doc.ts473
1 files changed, 314 insertions, 159 deletions
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 8d56ebf8c..cc024d83a 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1,6 +1,6 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { saveAs } from 'file-saver';
-import { action, computed, observable, ObservableMap, runInAction } from 'mobx';
+import { action, computed, observable, ObservableMap, ObservableSet, runInAction } from 'mobx';
import { computedFn } from 'mobx-utils';
import { alias, map, serializable } from 'serializr';
import { DocServer } from '../client/DocServer';
@@ -10,6 +10,7 @@ import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGloba
import { SelectionManager } from '../client/util/SelectionManager';
import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from '../client/util/SerializationHelper';
import { UndoManager } from '../client/util/UndoManager';
+import { decycle } from '../decycler/decycler';
import { DashColor, incrementTitleCopy, intersectRect, Utils } from '../Utils';
import { DateField } from './DateField';
import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from './FieldSymbols';
@@ -25,25 +26,27 @@ import { Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor } from './Ty
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 * as JSZipUtils from '../JSZipUtils';
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 instanceof ScriptField ? `$=${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';
- return field[ToScriptString]();
+ switch (typeof field) {
+ case 'string':
+ if (field.startsWith('{"')) return `'${field}'`; // bcz: hack ... want to quote the string the right way. if there are nested "'s, then use ' instead of ". In this case, test for the start of a JSON string of the format {"property": ... } and use outer 's instead of "s
+ return `"${field}"`;
+ case 'number':
+ case 'boolean':
+ return String(field);
+ }
+ return field?.[ToScriptString]?.() ?? 'null';
}
export function toString(field: Field): string {
- 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 '';
+ if (typeof field === 'string' || typeof field === 'number' || typeof field === 'boolean') return String(field);
+ return field?.[ToString]?.() || '';
}
export function IsField(field: any): field is Field;
export function IsField(field: any, includeUndefined: true): field is Field | undefined;
@@ -76,21 +79,20 @@ 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 NumListCast(field: FieldResult, defaultVal: number[] = []) {
+ return Cast(field, listSpec('number'), defaultVal);
}
-export function StrListCast(field: FieldResult) {
- return Cast(field, listSpec('string'), []);
+export function StrListCast(field: FieldResult, defaultVal: string[] = []) {
+ return Cast(field, listSpec('string'), defaultVal);
}
-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 function DocListCast(field: FieldResult, defaultVal: Doc[] = []) {
+ return Cast(field, listSpec(Doc), defaultVal).filter(d => d instanceof Doc) as Doc[];
}
export const WidthSym = Symbol('Width');
export const HeightSym = Symbol('Height');
+export const AnimationSym = Symbol('Animation');
+export const HighlightSym = Symbol('Highlight');
export const DataSym = Symbol('Data');
export const LayoutSym = Symbol('Layout');
export const FieldsSym = Symbol('Fields');
@@ -108,28 +110,37 @@ export const Initializing = Symbol('Initializing');
export const ForceServerWrite = Symbol('ForceServerWrite');
export const CachedUpdates = Symbol('Cached updates');
-const AclMap = new Map<string, symbol>([
- ['None', AclUnset],
- [SharingPermissions.None, AclPrivate],
- [SharingPermissions.View, AclReadonly],
- [SharingPermissions.Augment, AclAugment],
- [SharingPermissions.SelfEdit, AclSelfEdit],
- [SharingPermissions.Edit, AclEdit],
- [SharingPermissions.Admin, AclAdmin],
+export enum aclLevel {
+ unset = -1,
+ unshared = 0,
+ viewable = 1,
+ augmentable = 2,
+ selfEditable = 2.5,
+ editable = 3,
+ admin = 4,
+}
+// prettier-ignore
+export const HierarchyMapping: Map<symbol, { level:aclLevel; name: SharingPermissions }> = new Map([
+ [AclPrivate, { level: aclLevel.unshared, name: SharingPermissions.None }],
+ [AclReadonly, { level: aclLevel.viewable, name: SharingPermissions.View }],
+ [AclAugment, { level: aclLevel.augmentable, name: SharingPermissions.Augment}],
+ [AclSelfEdit, { level: aclLevel.selfEditable, name: SharingPermissions.SelfEdit }],
+ [AclEdit, { level: aclLevel.editable, name: SharingPermissions.Edit }],
+ [AclAdmin, { level: aclLevel.admin, name: SharingPermissions.Admin }],
+ [AclUnset, { level: aclLevel.unset, name: SharingPermissions.Unset }],
]);
+export const ReverseHierarchyMap: Map<string, { level: aclLevel; acl: symbol }> = new Map(Array.from(HierarchyMapping.entries()).map(value => [value[1].name, { level: value[1].level, acl: value[0] }]));
// caches the document access permissions for the current user.
// this recursively updates all protos as well.
export function updateCachedAcls(doc: Doc) {
if (!doc) return;
- const permissions: { [key: string]: symbol } = {};
- doc[UpdatingFromServer] = true;
- Object.keys(doc).filter(key => key.startsWith('acl') && (permissions[key] = AclMap.get(StrCast(doc[key]))!));
- doc[UpdatingFromServer] = false;
-
- if (Object.keys(permissions).length) {
- doc[AclSym] = permissions;
+ const target = (doc as any)?.__fields ?? doc;
+ const permissions: { [key: string]: symbol } = !target.author || target.author === Doc.CurrentUserEmail ? { 'acl-Me': AclAdmin } : {};
+ Object.keys(target).filter(key => key.startsWith('acl') && (permissions[key] = ReverseHierarchyMap.get(StrCast(target[key]))!.acl));
+ if (Object.keys(permissions).length || doc[AclSym]?.length) {
+ runInAction(() => (doc[AclSym] = permissions));
}
if (doc.proto instanceof Promise) {
@@ -139,16 +150,29 @@ export function updateCachedAcls(doc: Doc) {
}
@scriptingGlobal
-@Deserializable('Doc', updateCachedAcls).withFields(['id'])
+@Deserializable('Doc', updateCachedAcls, ['id'])
export class Doc extends RefField {
- //TODO tfs: these should be temporary...
- private static mainDocId: string | undefined;
- public static get MainDocId() {
- return this.mainDocId;
+ @observable public static CurrentlyLoading: Doc[];
+ // removes from currently loading display
+ @action
+ public static removeCurrentlyLoading(doc: Doc) {
+ if (Doc.CurrentlyLoading) {
+ const index = Doc.CurrentlyLoading.indexOf(doc);
+ index !== -1 && Doc.CurrentlyLoading.splice(index, 1);
+ }
}
- public static set MainDocId(id: string | undefined) {
- this.mainDocId = id;
+
+ // adds doc to currently loading display
+ @action
+ public static addCurrentlyLoading(doc: Doc) {
+ if (!Doc.CurrentlyLoading) {
+ Doc.CurrentlyLoading = [];
+ }
+ if (Doc.CurrentlyLoading.indexOf(doc) === -1) {
+ Doc.CurrentlyLoading.push(doc);
+ }
}
+
@observable public static GuestDashboard: Doc | undefined;
@observable public static GuestTarget: Doc | undefined;
@observable public static GuestMobile: Doc | undefined;
@@ -180,7 +204,7 @@ export class Doc extends RefField {
return DocCast(Doc.UserDoc().myRecentlyClosed);
}
public static get MyTrails() {
- return DocCast(Doc.UserDoc().myTrails);
+ return DocCast(Doc.ActiveDashboard?.myTrails);
}
public static get MyOverlayDocs() {
return DocCast(Doc.UserDoc().myOverlayDocs);
@@ -203,6 +227,19 @@ export class Doc extends RefField {
public static get MyFileOrphans() {
return DocCast(Doc.UserDoc().myFileOrphans);
}
+ public static AddFileOrphan(doc: Doc) {
+ if (
+ doc &&
+ Doc.MyFileOrphans instanceof Doc &&
+ Doc.IsPrototype(doc) &&
+ !Doc.IsSystem(doc) &&
+ ![DocumentType.MARKER, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR].includes(doc.type as any) &&
+ !doc.isFolder &&
+ !doc.annotationOn
+ ) {
+ Doc.AddDocToList(Doc.MyFileOrphans, undefined, doc);
+ }
+ }
public static get MyTools() {
return DocCast(Doc.UserDoc().myTools);
}
@@ -213,10 +250,15 @@ export class Doc extends RefField {
Doc.UserDoc().activePage = val;
DocServer.UPDATE_SERVER_CACHE();
}
+ public static IsComicStyle(doc?: Doc) {
+ return doc && Doc.ActiveDashboard && !Doc.IsSystem(doc) && Doc.UserDoc().renderStyle === 'comic';
+ }
public static get ActiveDashboard() {
return DocCast(Doc.UserDoc().activeDashboard);
}
public static set ActiveDashboard(val: Doc | undefined) {
+ const overlays = Cast(Doc.MyOverlayDocs.data, listSpec(Doc), null);
+ overlays && (overlays.length = 0);
Doc.UserDoc().activeDashboard = val;
}
public static set ActiveTool(tool: InkTool) {
@@ -225,15 +267,17 @@ export class Doc extends RefField {
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 get ActivePresentation(): Opt<Doc> {
+ return DocCast(Doc.ActiveDashboard?.activePresentation);
}
public static set ActivePresentation(val) {
- Doc.UserDoc().activePresentation = new PrefetchProxy(val);
+ if (Doc.ActiveDashboard) {
+ Doc.ActiveDashboard.activePresentation = val;
+ }
}
constructor(id?: FieldId, forceSave?: boolean) {
super(id);
- const doc = new Proxy<this>(this, {
+ const docProxy = new Proxy<this>(this, {
set: setter,
get: getter,
// getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter
@@ -262,11 +306,11 @@ export class Doc extends RefField {
throw new Error("Currently properties can't be defined on documents using Object.defineProperty");
},
});
- this[SelfProxy] = doc;
+ this[SelfProxy] = docProxy;
if (!id || forceSave) {
- DocServer.CreateField(doc);
+ DocServer.CreateField(docProxy);
}
- return doc;
+ return docProxy;
}
proto: Opt<Doc>;
@@ -295,8 +339,15 @@ export class Doc extends RefField {
@observable private ___fields: any = {};
@observable private ___fieldKeys: any = {};
+ /// all of the raw acl's that have been set on this document. Use GetEffectiveAcl to determine the actual ACL of the doc for editing
@observable public [AclSym]: { [key: string]: symbol } = {};
@observable public [DirectLinksSym]: Set<Doc> = new Set();
+ @observable public [AnimationSym]: Opt<Doc>;
+ @observable public [HighlightSym]: boolean = false;
+ static __Anim(Doc: Doc) {
+ // for debugging to print AnimationSym field easily.
+ return Doc[AnimationSym];
+ }
private [UpdatingFromServer]: boolean = false;
private [ForceServerWrite]: boolean = false;
@@ -462,22 +513,18 @@ export namespace Doc {
export function IsSystem(doc: Doc) {
return GetT(doc, 'system', 'boolean', true);
}
+ export function IsDelegateField(doc: Doc, fieldKey: string) {
+ return doc && Get(doc, fieldKey, true) !== undefined;
+ }
export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) {
if (key.startsWith('_')) key = key.substring(1);
- const hasProto = doc.proto instanceof Doc;
+ const hasProto = Doc.GetProto(doc) !== doc ? Doc.GetProto(doc) : undefined;
const onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1;
const onProto = hasProto && Object.getOwnPropertyNames(doc.proto).indexOf(key) !== -1;
if (onDeleg || !hasProto || (!onProto && !defaultProto)) {
doc[key] = value;
} else doc.proto![key] = value;
}
- export async function SetOnPrototype(doc: Doc, key: string, value: Field) {
- const proto = Object.getOwnPropertyNames(doc).indexOf('isPrototype') === -1 ? doc.proto : doc;
-
- if (proto) {
- proto[key] = value;
- }
- }
export function GetAllPrototypes(doc: Doc): Doc[] {
const protos: Doc[] = [];
let d: Opt<Doc> = doc;
@@ -516,21 +563,13 @@ 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;
- return r || r2 || r3 || r4;
+ return doc && other && Doc.GetProto(doc) === Doc.GetProto(other);
}
// Gets the data document for the document. Note: this is mis-named -- it does not specifically
// 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 base prototype ('isPrototype').
export function GetProto(doc: Doc): 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);
return proto === doc ? proto : Doc.GetProto(proto);
}
@@ -651,78 +690,121 @@ export namespace Doc {
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 function BestAlias(doc: Doc) {
+ const bestAlias = Doc.GetProto(doc) ? DocListCast(doc.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail) : doc;
+ return bestAlias ?? Doc.MakeAlias(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[], cloneLinks: 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 = 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'), [])];
+ const filter = [...exclusions, ...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 assignKey = (val: any) => (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
+ const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, cloneLinks)));
+ assignKey(new List<Doc>(clones));
} else {
- !dontCreate && assignKey(ObjectField.MakeCopy(field));
+ assignKey(ObjectField.MakeCopy(field));
if (field instanceof RichTextField) {
- if (field.Data.includes('"audioId":') || field.Data.includes('"textId":') || field.Data.includes('"anchorId":')) {
+ if (DocsInTextFieldIds.some(id => field.Data.includes(`"${id}":`))) {
+ const docidsearch = new RegExp('(' + DocsInTextFieldIds.map(exp => '(' + exp + ')').join('|') + ')":"([a-z-A-Z0-9_]*)"', 'g');
+ const rawdocids = field.Data.match(docidsearch);
+ const docids = rawdocids?.map((str: string) =>
+ DocsInTextFieldIds.reduce((output, exp) => {
+ return output.replace(new RegExp(`${exp}":`, 'g'), '');
+ }, str)
+ .replace(/"/g, '')
+ .trim()
+ );
+ const results = docids && (await DocServer.GetRefFields(docids));
+ const docs = results && Array.from(Object.keys(results)).map(key => DocCast(results[key]));
+ docs && docs.map(doc => Doc.makeClone(doc, cloneMap, linkMap, rtfs, exclusions, cloneLinks));
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...
+ const docAtKey = doc[key];
+ if (docAtKey instanceof Doc) {
+ if (!Doc.IsSystem(docAtKey) && (key.startsWith('layout') || key === 'annotationOn' || key === 'proto' || ((key === 'anchor1' || key === 'anchor2') && doc.author === Doc.CurrentUserEmail))) {
+ assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, cloneLinks));
} else {
- assignKey(field);
+ assignKey(docAtKey);
}
+ } else if (field instanceof RefField) {
+ assignKey(field);
+ } else if (cfield instanceof ComputedField) {
+ assignKey(cfield[Copy]());
+ // 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);
- asBranch ? (copy.branchOf = doc) : (copy.cloneOf = doc);
- if (!Doc.IsPrototype(copy)) {
- Doc.AddDocToList(doc, 'branches', Doc.GetProto(copy));
+ Array.from(doc[DirectLinksSym]).forEach(async link => {
+ if (
+ cloneLinks ||
+ ((cloneMap.has(DocCast(link.anchor1)?.[Id]) || cloneMap.has(DocCast(DocCast(link.anchor1)?.annotationOn)?.[Id])) && (cloneMap.has(DocCast(link.anchor2)?.[Id]) || cloneMap.has(DocCast(DocCast(link.anchor2)?.annotationOn)?.[Id])))
+ ) {
+ linkMap.set(link[Id], await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, cloneLinks));
}
- cloneMap.set(doc[Id], copy);
- }
+ });
+ Doc.SetInPlace(copy, 'title', 'CLONE: ' + doc.title, true);
+ copy.cloneOf = doc;
+ cloneMap.set(doc[Id], copy);
+
+ Doc.AddFileOrphan(copy);
return copy;
}
- export async function MakeClone(doc: Doc, dontCreate: boolean = false, asBranch = false, cloneMap: Map<string, Doc> = new Map()) {
- const linkMap = new Map<Doc, Doc>();
+ export function repairClone(clone: Doc, cloneMap: Map<string, Doc>, visited: Set<Doc>) {
+ if (visited.has(clone)) return;
+ visited.add(clone);
+ Object.keys(clone)
+ .filter(key => key !== 'cloneOf')
+ .map(key => {
+ const docAtKey = DocCast(clone[key]);
+ if (docAtKey && !Doc.IsSystem(docAtKey)) {
+ if (!Array.from(cloneMap.values()).includes(docAtKey)) {
+ if (cloneMap.has(docAtKey[Id])) {
+ clone[key] = cloneMap.get(docAtKey[Id]);
+ } else clone[key] = undefined;
+ } else {
+ repairClone(docAtKey, cloneMap, visited);
+ }
+ }
+ });
+ }
+ export function MakeClones(docs: Doc[], cloneLinks: boolean) {
+ const cloneMap = new Map<string, Doc>();
+ return docs.map(doc => Doc.MakeClone(doc, cloneLinks, cloneMap));
+ }
+
+ export async function MakeClone(doc: Doc, cloneLinks = true, cloneMap: Map<string, Doc> = new Map()) {
+ const linkMap = new Map<string, Doc>();
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));
+ const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], cloneLinks);
+ const repaired = new Set<Doc>();
+ const linkedDocs = Array.from(linkMap.values());
+ const clonedDocs = [...Array.from(cloneMap.values()), ...linkedDocs];
+ clonedDocs.map(clone => Doc.repairClone(clone, cloneMap, repaired));
+ linkedDocs.map((link: Doc) => LinkManager.Instance.addLink(link, true));
rtfMap.map(({ copy, key, field }) => {
const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
const mapped = cloneMap.get(id);
@@ -734,9 +816,10 @@ export namespace Doc {
};
const regex = `(${Doc.localServerPath()})([^"]*)`;
const re = new RegExp(regex, 'g');
- copy[key] = new RichTextField(field.Data.replace(/("textId":|"audioId":|"anchorId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
+ const docidsearch = new RegExp('(' + DocsInTextFieldIds.map(exp => `"${exp}":`).join('|') + ')"([^"]+)"', 'g');
+ copy[key] = new RichTextField(field.Data.replace(docidsearch, replacer).replace(re, replacer2), field.Text);
});
- return { clone: copy, map: cloneMap };
+ return { clone: copy, map: cloneMap, linkMap };
}
export async function Zip(doc: Doc) {
@@ -745,9 +828,11 @@ export namespace Doc {
// a.href = url;
// a.download = `DocExport-${this.props.Document[Id]}.zip`;
// a.click();
- const { clone, map } = await Doc.MakeClone(doc, true);
+ const { clone, map, linkMap } = await Doc.MakeClone(doc);
+ clone.LINKS = new List<Doc>(Array.from(linkMap.values()));
+ const proms = [] as string[];
function replacer(key: any, value: any) {
- if (['branchOf', 'cloneOf', 'context', 'cursors'].includes(key)) return undefined;
+ if (key && ['branchOf', 'cloneOf', 'cursors'].includes(key)) return undefined;
else if (value instanceof Doc) {
if (key !== 'field' && Number.isNaN(Number(key))) {
const __fields = value[FieldsSym]();
@@ -757,9 +842,14 @@ export namespace Doc {
}
} 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 ImageField) {
+ const extension = value.url.href.replace(/.*\./, '');
+ proms.push(value.url.href.replace('.' + extension, '_o.' + extension));
+ return { url: value.url.href, __type: 'image' };
+ } else if (value instanceof PdfField) {
+ proms.push(value.url.href);
+ 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' };
@@ -772,8 +862,34 @@ export namespace Doc {
const docs: { [id: string]: any } = {};
Array.from(map.entries()).forEach(f => (docs[f[0]] = f[1]));
- const docString = JSON.stringify({ id: doc[Id], docs }, replacer);
-
+ const docString = JSON.stringify({ id: clone[Id], docs }, decycle(replacer));
+
+ let generateZIP = (proms: string[]) => {
+ var zip = new JSZip();
+ var count = 0;
+ var zipFilename = 'dashExport.zip';
+
+ proms
+ .filter(url => url.startsWith(window.location.origin))
+ .forEach((url, i) => {
+ var filename = proms[i].replace(window.location.origin + '/', '').replace(/\//g, '%%%');
+ // loading a file and add it in a zip file
+ JSZipUtils.getBinaryContent(url, function (err: any, data: any) {
+ if (err) {
+ throw err; // or handle the error
+ }
+ zip.file(filename, data, { binary: true });
+ count++;
+ if (count == proms.length) {
+ zip.file('doc.json', docString);
+ zip.generateAsync({ type: 'blob' }).then(function (content) {
+ saveAs(content, zipFilename);
+ });
+ }
+ });
+ });
+ };
+ generateZIP(proms);
const zip = new JSZip();
zip.file('doc.json', docString);
@@ -894,11 +1010,12 @@ export namespace Doc {
export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string, retitle = false): Doc {
const copy = new Doc(copyProtoId, true);
+ updateCachedAcls(copy);
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]);
+ const field = key === 'author' ? Doc.CurrentUserEmail : ProxyField.WithoutProxy(() => doc[key]);
if (key === 'proto' && copyProto) {
if (doc[key] instanceof Doc) {
copy[key] = Doc.MakeCopy(doc[key]!, false);
@@ -922,7 +1039,6 @@ export namespace Doc {
}
}
});
- copy.author = Doc.CurrentUserEmail;
if (copyProto) {
Doc.GetProto(copy).context = undefined;
Doc.GetProto(copy).aliases = new List<Doc>([copy]);
@@ -934,6 +1050,7 @@ export namespace Doc {
if (retitle) {
copy.title = incrementTitleCopy(StrCast(copy.title));
}
+ Doc.AddFileOrphan(copy);
return copy;
}
@@ -943,6 +1060,7 @@ export namespace Doc {
if (doc) {
const delegate = new Doc(id, true);
delegate[Initializing] = true;
+ updateCachedAcls(delegate);
delegate.proto = doc;
delegate.author = Doc.CurrentUserEmail;
Object.keys(doc)
@@ -951,6 +1069,7 @@ export namespace Doc {
if (!Doc.IsSystem(doc)) Doc.AddDocToList(doc[DataSym], 'aliases', delegate);
title && (delegate.title = title);
delegate[Initializing] = false;
+ Doc.AddFileOrphan(delegate);
return delegate;
}
return undefined;
@@ -1073,7 +1192,7 @@ export namespace Doc {
BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap();
SearchMatchDoc: ObservableMap<Doc, { searchMatch: number }> = new ObservableMap();
}
- const brushManager = new DocBrush();
+ export const brushManager = new DocBrush();
export class DocData {
@observable _user_doc: Doc = undefined!;
@@ -1094,7 +1213,7 @@ export namespace Doc {
return doc[StrCast(doc.layoutKey, 'layout')];
}
export function LayoutFieldKey(doc: Doc): string {
- return StrCast(Doc.Layout(doc).layout).split("'")[1];
+ return StrCast(Doc.Layout(doc)[StrCast(doc.layoutKey, 'layout')]).split("'")[1]; // bcz: TODO check on this . used to always reference 'layout', now it uses the layout speicfied by the current layoutKey
}
export function NativeAspect(doc: Doc, dataDoc?: Doc, useDim?: boolean) {
return Doc.NativeWidth(doc, dataDoc, useDim) / (Doc.NativeHeight(doc, dataDoc, useDim) || 1);
@@ -1103,9 +1222,10 @@ export namespace Doc {
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;
- return !doc ? 0 : NumCast(doc._nativeHeight, nheight || dheight);
+ if (!doc) return 0;
+ const nheight = (Doc.NativeWidth(doc, dataDoc, useHeight) * doc[HeightSym]()) / doc[WidthSym]();
+ const dheight = NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '-nativeHeight'], useHeight ? doc[HeightSym]() : 0);
+ return NumCast(doc._nativeHeight, nheight || dheight);
}
export function SetNativeWidth(doc: Doc, width: number | undefined, fieldKey?: string) {
doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + '-nativeWidth'] = width;
@@ -1176,7 +1296,7 @@ export namespace Doc {
}
// don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message)
export function IsBrushedDegreeUnmemoized(doc: Doc) {
- if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return DocBrushStatus.unbrushed;
+ if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate || doc.opacity === 0) return DocBrushStatus.unbrushed;
const status = brushManager.BrushedDoc.has(doc) ? DocBrushStatus.selfBrushed : brushManager.BrushedDoc.has(Doc.GetProto(doc)) ? DocBrushStatus.protoBrushed : DocBrushStatus.unbrushed;
if (status === DocBrushStatus.unbrushed) {
const lastBrushed = Array.from(brushManager.BrushedDoc.keys()).lastElement();
@@ -1219,48 +1339,59 @@ export namespace Doc {
}
export function linkFollowUnhighlight() {
- Doc.UnhighlightAll();
+ clearTimeout(UnhighlightTimer);
+ UnhighlightWatchers.forEach(watcher => watcher());
+ UnhighlightWatchers.length = 0;
+ highlightedDocs.forEach(doc => Doc.UnHighlightDoc(doc));
document.removeEventListener('pointerdown', linkFollowUnhighlight);
}
- let _lastDate = 0;
- export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true) {
+ let UnhighlightWatchers: (() => void)[] = [];
+ export let UnhighlightTimer: any;
+ export function AddUnHighlightWatcher(watcher: () => void) {
+ if (UnhighlightTimer) {
+ UnhighlightWatchers.push(watcher);
+ } else watcher();
+ }
+ export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true, presEffect?: Doc) {
linkFollowUnhighlight();
- (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs));
+ (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs, presEffect));
document.removeEventListener('pointerdown', linkFollowUnhighlight);
document.addEventListener('pointerdown', linkFollowUnhighlight);
- const lastDate = (_lastDate = Date.now());
- window.setTimeout(() => _lastDate === lastDate && linkFollowUnhighlight(), 5000);
+ if (UnhighlightTimer) clearTimeout(UnhighlightTimer);
+ UnhighlightTimer = window.setTimeout(() => {
+ linkFollowUnhighlight();
+ UnhighlightTimer = 0;
+ }, 5000);
}
- export class HighlightBrush {
- @observable HighlightedDoc: Map<Doc, boolean> = new Map();
- }
- const highlightManager = new HighlightBrush();
+ export var highlightedDocs = new ObservableSet<Doc>();
export function IsHighlighted(doc: Doc) {
- if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return false;
- return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc));
+ if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate || doc.opacity === 0) return false;
+ return doc[HighlightSym] || Doc.GetProto(doc)[HighlightSym];
}
- export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) {
+ export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true, presEffect?: Doc) {
runInAction(() => {
- highlightManager.HighlightedDoc.set(doc, true);
- dataAndDisplayDocs && highlightManager.HighlightedDoc.set(Doc.GetProto(doc), true);
+ highlightedDocs.add(doc);
+ doc[HighlightSym] = true;
+ doc[AnimationSym] = presEffect;
+ if (dataAndDisplayDocs) {
+ highlightedDocs.add(Doc.GetProto(doc));
+ Doc.GetProto(doc)[HighlightSym] = true;
+ }
});
}
- export function UnHighlightDoc(doc: Doc) {
+ /// if doc is defined, then it is unhighlighted, otherwise all highlighted docs are unhighlighted
+ export function UnHighlightDoc(doc?: Doc) {
runInAction(() => {
- highlightManager.HighlightedDoc.set(doc, false);
- highlightManager.HighlightedDoc.set(Doc.GetProto(doc), false);
+ (doc ? [doc] : Array.from(highlightedDocs)).forEach(doc => {
+ highlightedDocs.delete(doc);
+ highlightedDocs.delete(Doc.GetProto(doc));
+ doc[HighlightSym] = Doc.GetProto(doc)[HighlightSym] = false;
+ doc[AnimationSym] = undefined;
+ });
});
}
- export function UnhighlightAll() {
- const mapEntries = highlightManager.HighlightedDoc.keys();
- let docEntry: IteratorResult<Doc>;
- while (!(docEntry = mapEntries.next()).done) {
- const targetDoc = docEntry.value;
- targetDoc && Doc.UnHighlightDoc(targetDoc);
- }
- }
export function UnBrushAllDocs() {
brushManager.BrushedDoc.clear();
}
@@ -1328,14 +1459,14 @@ 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, fieldPrefix?: string, append: boolean = true) {
if (!container) return;
- const filterField = '_' + (fieldSuffix ? fieldSuffix + '-' : '') + 'docFilters';
+ const filterField = '_' + (fieldPrefix ? fieldPrefix + '-' : '') + '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')) {
+ if (fields[0] === key && (fields[1] === value || modifiers === 'match' || (fields[2] === 'match' && modifiers === 'remove'))) {
if (fields[2] === modifiers && modifiers && fields[1] === value) {
if (toggle) modifiers = 'remove';
else return;
@@ -1373,11 +1504,17 @@ export namespace Doc {
layoutDoc._viewScale = NumCast(layoutDoc._viewScale, 1) * contentScale;
layoutDoc._nativeWidth = undefined;
layoutDoc._nativeHeight = undefined;
+ layoutDoc._forceReflow = undefined;
+ layoutDoc._nativeHeightUnfrozen = undefined;
+ layoutDoc._nativeDimModifiable = undefined;
} else {
layoutDoc._autoHeight = false;
if (!Doc.NativeWidth(layoutDoc)) {
layoutDoc._nativeWidth = NumCast(layoutDoc._width, panelWidth);
layoutDoc._nativeHeight = NumCast(layoutDoc._height, panelHeight);
+ layoutDoc._forceReflow = true;
+ layoutDoc._nativeHeightUnfrozen = true;
+ layoutDoc._nativeDimModifiable = true;
}
}
});
@@ -1389,6 +1526,20 @@ export namespace Doc {
return !curPres ? false : DocListCast(curPres.data).findIndex(val => Doc.AreProtosEqual(val, doc)) !== -1;
}
+ export function styleFromLayoutString(rootDoc: Doc, layoutDoc: Doc, props: any, scale: number) {
+ const style: { [key: string]: any } = {};
+ const divKeys = ['width', 'height', 'fontSize', 'transform', 'left', 'backgroundColor', 'left', 'right', 'top', 'bottom', 'pointerEvents', 'position'];
+ const replacer = (match: any, expr: string, offset: any, string: any) => {
+ // bcz: this executes a script to convert a property expression string: { script } into a value
+ return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ self: rootDoc, this: layoutDoc, scale }).result?.toString() ?? '';
+ };
+ divKeys.map((prop: string) => {
+ const p = props[prop];
+ typeof p === 'string' && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer));
+ });
+ return style;
+ }
+
// prettier-ignore
export function toIcon(doc?: Doc, isOpen?: boolean) {
switch (StrCast(doc?.type)) {
@@ -1424,8 +1575,12 @@ export namespace Doc {
formData.append('remap', 'true');
const response = await fetch(upload, { method: 'POST', body: formData });
const json = await response.json();
+ console.log(json);
if (json !== 'error') {
- const doc = await DocServer.GetRefField(json);
+ await DocServer.GetRefFields(json.docids as string[]);
+ const doc = DocCast(await DocServer.GetRefField(json.id));
+ (await DocListCastAsync(doc?.LINKS))?.forEach(link => LinkManager.Instance.addLink(link));
+ doc.LINKS = undefined;
return doc;
}
}