/* eslint-disable no-use-before-define */ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { saveAs } from 'file-saver'; import * as JSZip from 'jszip'; import { action, runInAction } from 'mobx'; import { ClientUtils, DashColor } from '../../ClientUtils'; import * as JSZipUtils from '../../JSZipUtils'; import { decycle } from '../../decycler/decycler'; import { DateField } from '../../fields/DateField'; import { Doc, DocListCast, Field, FieldResult, FieldType, LinkedTo, Opt, StrListCast } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { InkData, InkDataFieldName, InkField } from '../../fields/InkField'; import { List, ListFieldName } from '../../fields/List'; import { ProxyField } from '../../fields/Proxy'; import { RichTextField } from '../../fields/RichTextField'; import { ComputedField, ScriptField } from '../../fields/ScriptField'; import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../fields/Types'; import { AudioField, CsvField, ImageField, PdfField, VideoField, WebField } from '../../fields/URLField'; import { SharingPermissions } from '../../fields/util'; import { Upload } from '../../server/SharedMediaTypes'; import { DocServer } from '../DocServer'; import { Networking } from '../Network'; import { LinkManager } from '../util/LinkManager'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; import { SerializationHelper } from '../util/SerializationHelper'; import { UndoManager, undoable } from '../util/UndoManager'; import { ContextMenu } from '../views/ContextMenu'; import { ContextMenuProps } from '../views/ContextMenuItem'; import { LinkDescriptionPopup } from '../views/nodes/LinkDescriptionPopup'; import { OpenWhere } from '../views/nodes/OpenWhere'; import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox'; import { DocumentType } from './DocumentTypes'; import { Docs, DocumentOptions } from './Documents'; import { DocumentView } from '../views/nodes/DocumentView'; import { INode, parse } from 'svgson'; import { SVGToBezier, SVGType } from '../util/bezierFit'; import { SmartDrawHandler } from '../views/smartdraw/SmartDrawHandler'; import { PointData } from '../../pen-gestures/GestureTypes'; export namespace DocUtils { function HasFunctionFilter(val: string) { if (val.includes(ClientUtils.isTransparentFunctionHack)) return (d: Doc, color: string) => !d.disableMixBlend && color !== '' && DashColor(color).alpha() !== 1; // add other function filters here... return undefined; } function matchFieldValue(doc: Doc, key: string, valueIn: unknown): boolean { let value = valueIn; const hasFunctionFilter = HasFunctionFilter(value as string); if (hasFunctionFilter) { return hasFunctionFilter(doc, StrCast(doc[key])); } if (key === LinkedTo) { // links are not a field value, so handled here. value is an expression of form ([field=]idToDoc("...")) const allLinks = Doc.Links(doc); const matchLink = (val: string, anchor: Doc) => { const linkedToExp = (val ?? '').split('='); if (linkedToExp.length === 1) return Field.toScriptString(anchor) === val; return DocCast(anchor[linkedToExp[0]]) && Field.toScriptString(DocCast(anchor[linkedToExp[0]])!) === linkedToExp[1]; }; // prettier-ignore return (value === Doc.FilterNone && !allLinks.length) || (value === Doc.FilterAny && !!allLinks.length) || (allLinks.some(link => (DocCast(link.link_anchor_1) && matchLink(value as string, DocCast(link.link_anchor_1)!)) || (DocCast(link.link_anchor_2) && matchLink(value as string, DocCast(link.link_anchor_2)!)) )); } if (typeof value === 'string') { value = value.replace(`,${ClientUtils.noRecursionHack}`, ''); } const fieldVal = doc[key]; // prettier-ignore if ((value === Doc.FilterAny && fieldVal !== undefined) || (value === Doc.FilterNone && fieldVal === undefined)) { return true; } const vals = StrListCast(fieldVal); // list typing is very imperfect. casting to a string list doesn't mean that the entries will actually be strings if (vals.length) { return vals.some(v => typeof v === 'string' && v === (value as string)); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring } return Field.toString(fieldVal as FieldType) === (value as string); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring } /** * @param docs * @param childFilters * @param childFiltersByRanges * @param parentCollection * Given a list of docs and childFilters, @returns the list of Docs that match those filters */ export function FilterDocs(childDocs: Doc[], childFilters: string[], childFiltersByRanges: string[], parentCollection?: Doc) { if (!childFilters?.length && !childFiltersByRanges?.length) { return childDocs.filter(d => !d.cookies); // remove documents that need a cookie if there are no filters to provide one } const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields childFilters.forEach(filter => { const fields = filter.split(Doc.FilterSep); const key = fields[0]; const value = fields[1]; const modifiers = fields[2]; if (!filterFacets[key]) { filterFacets[key] = {}; } filterFacets[key][value] = modifiers; }); const filteredDocs = childFilters.length ? childDocs.filter(d => { if (d.z) return true; // if the document needs a cookie but no filter provides the cookie, then the document does not pass the filter if (d.cookies && (!filterFacets.cookies || !Object.keys(filterFacets.cookies).some(key => d.cookies === key))) { return false; } const facetKeys = Object.keys(filterFacets).filter(fkey => fkey !== 'cookies' && fkey !== ClientUtils.noDragDocsFilter.split(Doc.FilterSep)[0]); for (const facetKey of facetKeys) { const facet = filterFacets[facetKey]; // facets that match some value in the field of the document (e.g. some text field) const matches = Object.keys(facet).filter(value => value !== 'cookies' && facet[value] === 'match'); // facets that have a check next to them const checks = Object.keys(facet).filter(value => facet[value] === 'check'); // metadata facets that exist const exists = Object.keys(facet).filter(value => facet[value] === 'exists'); // facets that unset metadata (a hack for making cookies work) const unsets = Object.keys(facet).filter(value => facet[value] === 'unset'); // facets that specify that a field must not match a specific value const xs = Object.keys(facet).filter(value => facet[value] === 'x'); if (!unsets.length && !exists.length && !xs.length && !checks.length && !matches.length) return true; const failsNotEqualFacets = !xs.length ? false : xs.some(value => matchFieldValue(d, facetKey, value)); const satisfiesCheckFacets = !checks.length ? true : checks.some(value => matchFieldValue(d, facetKey, value)); const satisfiesExistsFacets = !exists.length ? true : facetKey !== LinkedTo ? d[facetKey] !== undefined : Doc.Links(d).length; const satisfiesUnsetsFacets = !unsets.length ? true : d[facetKey] === undefined; const satisfiesMatchFacets = !matches.length ? true : matches.some(value => { if (facetKey.startsWith('*')) { // fields starting with a '*' are used to match families of related fields. ie, *modificationDate will match text_modificationDate, data_modificationDate, etc const allKeys = Array.from(Object.keys(d)); allKeys.push(...Object.keys(Doc.GetProto(d))); const keys = allKeys.filter(key => key.includes(facetKey.substring(1))); return keys.some(key => Field.toString(d[key] as FieldType).includes(value)); } return Field.toString(d[facetKey] as FieldType).includes(value); }); // if we're ORing them together, the default return is false, and we return true for a doc if it satisfies any one set of criteria if (parentCollection?.childFilters_boolean === 'OR') { if (satisfiesUnsetsFacets && satisfiesExistsFacets && satisfiesCheckFacets && !failsNotEqualFacets && satisfiesMatchFacets) return true; } // if we're ANDing them together, the default return is true, and we return false for a doc if it doesn't satisfy any set of criteria else if (!satisfiesUnsetsFacets || !satisfiesExistsFacets || !satisfiesCheckFacets || failsNotEqualFacets || (matches.length && !satisfiesMatchFacets)) return false; } return parentCollection?.childFilters_boolean !== 'OR'; }) : childDocs; const rangeFilteredDocs = filteredDocs.filter(d => { for (let i = 0; i < childFiltersByRanges.length; i += 3) { const key = childFiltersByRanges[i]; const min = Number(childFiltersByRanges[i + 1]); const max = Number(childFiltersByRanges[i + 2]); const val = typeof d[key] === 'string' ? (Number(StrCast(d[key])).toString() === StrCast(d[key]) ? Number(StrCast(d[key])) : undefined) : Cast(d[key], 'number', null); if (val === undefined) { // console.log("Should 'undefined' pass range filter or not?") } else if (val < min || val > max) return false; } return true; }); return rangeFilteredDocs; } export function MakeLink(source: Doc, target: Doc, linkSettings: { layout_isSvg?: boolean; link_relationship?: string; link_description?: string }, id?: string, showPopup?: number[]) { if (!linkSettings.link_relationship) linkSettings.link_relationship = target.type === DocumentType.RTF ? 'Commentary:Comments On' : 'link'; if (target.doc === Doc.UserDoc()) return undefined; const makeLink = action((linkDoc: Doc, showAt?: number[]) => { if (showAt) { LinkManager.Instance.currentLink = linkDoc; TaskCompletionBox.textDisplayed = 'Link Created'; TaskCompletionBox.popupX = showAt[0]; TaskCompletionBox.popupY = showAt[1] - 33; TaskCompletionBox.taskCompleted = true; LinkDescriptionPopup.Instance.popupX = showAt[0]; LinkDescriptionPopup.Instance.popupY = showAt[1]; LinkDescriptionPopup.Instance.display = true; const rect = document.body.getBoundingClientRect(); if (LinkDescriptionPopup.Instance.popupX + 200 > rect.width) { LinkDescriptionPopup.Instance.popupX -= 190; TaskCompletionBox.popupX -= 40; } if (LinkDescriptionPopup.Instance.popupY + 100 > rect.height) { LinkDescriptionPopup.Instance.popupY -= 40; TaskCompletionBox.popupY -= 40; } setTimeout( action(() => { TaskCompletionBox.taskCompleted = false; }), 2500 ); } return linkDoc; }); const a = source.layout_unrendered ? 'link_anchor_1?.annotationOn' : 'link_anchor_1'; const b = target.layout_unrendered ? 'link_anchor_2?.annotationOn' : 'link_anchor_2'; return makeLink( Docs.Create.LinkDocument( source, target, { acl_Guest: SharingPermissions.Augment, _acl_Guest: SharingPermissions.Augment, title: ComputedField.MakeFunction('generateLinkTitle(this)') as unknown as string, // title can accept functions even though type says it can't link_anchor_1_useSmallAnchor: source.useSmallAnchor ? true : undefined, link_anchor_2_useSmallAnchor: target.useSmallAnchor ? true : undefined, link_relationship: linkSettings.link_relationship, link_description: linkSettings.link_description, layout_isSvg: linkSettings.layout_isSvg, x: ComputedField.MakeFunction(`((this.${a}?.x||0)+(this.${b}?.x||0))/2`) as unknown as number, // x can accept functions even though type says it can't y: ComputedField.MakeFunction(`((this.${a}?.y||0)+(this.${b}?.y||0))/2`) as unknown as number, // y can accept functions even though type says it can't link_autoMoveAnchors: true, _lockedPosition: true, _layout_showCaption: '', // removed since they conflict with showing a link with a LinkBox (ie, line, not comparison box) _layout_showTitle: '', // _layout_showCaption: 'link_description', // _layout_showTitle: 'link_relationship', }, id ), showPopup ); } export function AssignScripts(doc: Doc, scripts?: { [key: string]: string | undefined }, funcs?: { [key: string]: string | undefined }) { scripts && Object.keys(scripts).forEach(key => { const script = scripts[key] as string; if (ScriptCast(doc[key])?.script.originalScript !== scripts[key] && script) { const additionalItems: { [key: string]: unknown } = {}; script.match(/_[a-zA-Z]*_/)?.forEach(match => (additionalItems[match] = 'any')); (key.startsWith('_') ? doc : Doc.GetProto(doc))[key] = ScriptField.MakeScript(script, { ...additionalItems, this: Doc.name, dragData: Doc.DocDragDataName, value: 'any', _readOnly_: 'boolean', scriptContext: 'any', documentView: Doc.name, heading: Doc.name, checked: 'boolean', containingTreeView: Doc.name, altKey: 'boolean', ctrlKey: 'boolean', shiftKey: 'boolean', }); } }); funcs && Object.keys(funcs) .filter(key => !key.endsWith('-setter')) .forEach(key => { const cfield = ComputedField.DisableCompute(() => FieldValue(doc[key])); const func = funcs[key]; if (ScriptCast(cfield)?.script.originalScript !== func) { const setFunc = Cast(funcs[key + '-setter'], 'string', null); (key.startsWith('_') ? doc : Doc.GetProto(doc))[key] = func ? ComputedField.MakeFunction(func, { dragData: Doc.DocDragDataName }, { _readOnly_: true }, setFunc) : undefined; } }); return doc; } export function AssignOpts(doc: Doc | undefined, reqdOpts: DocumentOptions, items?: Doc[]) { if (doc) { const compareValues = (val1: unknown, val2: unknown) => { if (val1 instanceof List && val2 instanceof List && val1.length === val2.length) { return !val1.some(v => !val2.includes(v)) || !val2.some(v => val1.includes(v)); } return val1 === val2; }; Object.entries(reqdOpts).forEach(([key, val]) => { const targetDoc = key.startsWith('_') ? doc : Doc.GetProto(doc as Doc); if (!Object.getOwnPropertyNames(targetDoc).includes(key.replace(/^_/, '')) || !compareValues(val, targetDoc[key])) { targetDoc[key] = val as FieldType; } }); items?.forEach(item => !DocListCast(doc.data).includes(item) && Doc.AddDocToList(Doc.GetProto(doc), 'data', item)); items && DocListCast(doc.data).forEach(item => Doc.IsSystem(item) && !items.includes(item) && Doc.RemoveDocFromList(Doc.GetProto(doc), 'data', item)); } return doc; } export function AssignDocField(doc: Doc, field: string, creator: (reqdOpts: DocumentOptions, items?: Doc[]) => Doc, reqdOpts: DocumentOptions, items?: Doc[], scripts?: { [key: string]: string }, funcs?: { [key: string]: string }) { return DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts, items) ?? (doc[field] = creator(reqdOpts, items)), scripts, funcs); } /** * * @param type the type of file. * @param path the path to the file. * @param options the document options. * @param overwriteDoc the placeholder loading doc. * @returns */ export async function DocumentFromType(type: string, path: string, options: DocumentOptions, overwriteDoc?: Doc): Promise> { let ctor: ((path: string, options: DocumentOptions, overwriteDoc?: Doc) => Doc | Promise) | undefined; if (type.indexOf('image') !== -1) { ctor = Docs.Create.ImageDocument; if (!options._width) options._width = 300; } if (type.indexOf('video') !== -1) { ctor = Docs.Create.VideoDocument; if (!options._width) options._width = 600; if (!options._height) options._height = ((options._width as number) * 2) / 3; } if (type.indexOf('audio') !== -1) { ctor = Docs.Create.AudioDocument; } if (type.indexOf('pdf') !== -1) { ctor = Docs.Create.PdfDocument; if (!options._width) options._width = 400; if (!options._height) options._height = ((options._width as number) * 1200) / 927; } if (type.indexOf('csv') !== -1) { ctor = Docs.Create.DataVizDocument; if (!options._width) options._width = 400; if (!options._height) options._height = ((options._width as number) * 1200) / 927; } // TODO:al+glr // if (type.indexOf("map") !== -1) { // ctor = Docs.Create.MapDocument; // if (!options._width) options._width = 800; // if (!options._height) options._height = (options._width as number) * 3 / 4; // } if (type.indexOf('html') !== -1) { if (path.includes(window.location.hostname)) { const s = path.split('/'); const id = s[s.length - 1]; return DocServer.GetRefField(id)?.then(field => { if (field instanceof Doc) { const embedding = Doc.MakeEmbedding(field); embedding.x = (options.x as number) || 0; embedding.y = (options.y as number) || 0; embedding._width = (options._width as number) || 300; embedding._height = (options._height as number) || (options._width as number) || 300; return embedding; } return undefined; }); } ctor = Docs.Create.WebDocument; // eslint-disable-next-line no-param-reassign options = { ...options, _width: 400, _height: 512, title: path }; } return ctor ? ctor(path, overwriteDoc ? { ...options, title: StrCast(overwriteDoc.title, path) } : options, overwriteDoc) : undefined; } /** * Adds items to the doc creator (':') context menu for creating each document type * @param docTextAdder * @param docAdder * @param x * @param y * @param simpleMenu * @param pivotField * @param pivotValue */ export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false, pivotField?: string, pivotValue?: string | number | boolean): void { const foo = DocListCast(DocListCast(Doc.MyTools?.data)[0]?.data).concat(...DocListCast(DocListCast(Doc.MyTools?.data)[1]?.data)); const documentList: ContextMenuProps[] = foo .filter(btnDoc => !btnDoc.hidden) .map(btnDoc => DocCast(btnDoc?.dragFactory)) .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc.title) .map(doc => doc!) .map(dragDoc => ({ description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''), event: undoable(() => { const newDoc = (dragDoc.isTemplateDoc ? DocUtils.delegateDragFactory : DocUtils.copyDragFactory)(dragDoc); if (newDoc) { newDoc._author = ClientUtils.CurrentUserEmail(); newDoc.x = x; newDoc.y = y; newDoc.$backgroundColor = Doc.UserDoc().textBackgroundColor; DocumentView.SetSelectOnLoad(newDoc); if (pivotField) { newDoc[pivotField] = pivotValue; } docAdder?.(newDoc); } }, StrCast(dragDoc.title)), icon: Doc.toIcon(dragDoc), })) as ContextMenuProps[]; documentList.push({ description: ':Smart Drawing', event: e => DocumentView.Selected() .lastElement() .ComponentView?.showSmartDraw?.(e?.x || 0, e?.y || 0), icon: 'file', }); ContextMenu.Instance.addItem({ description: 'Create document', subitems: documentList, icon: 'file', }); !simpleMenu && ContextMenu.Instance.addItem({ description: 'Styled Notes', subitems: DocListCast((Doc.UserDoc().template_notes as Doc).data).map(note => ({ description: ':' + StrCast(note.title), event: undoable(() => { const textDoc = Docs.Create.TextDocument('', { _width: 200, x, y, _layout_autoHeight: note._layout_autoHeight !== false, title: StrCast(note.title) + '#' + (note.embeddingCount = NumCast(note.embeddingCount) + 1), }); textDoc.layout_fieldKey = 'layout_' + note.title; textDoc[textDoc.layout_fieldKey] = note; if (pivotField) { textDoc[pivotField] = pivotValue; } docTextAdder(textDoc); }, 'create quick note'), icon: StrCast(note.icon) as IconProp, })) as ContextMenuProps[], icon: 'sticky-note', }); const userDocList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[1]?.data) .filter(btnDoc => !btnDoc.hidden) .map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)) .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc !== Doc.UserDoc().emptyNote && doc.title) .map(doc => doc!) .map(dragDoc => ({ description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''), event: undoable(() => { const newDoc = DocUtils.delegateDragFactory(dragDoc); if (newDoc) { newDoc.author = ClientUtils.CurrentUserEmail(); newDoc.x = x; newDoc.y = y; DocumentView.SetSelectOnLoad(newDoc); if (pivotField) { newDoc[pivotField] = pivotValue; } docAdder?.(newDoc); } }, StrCast(dragDoc.title)), icon: Doc.toIcon(dragDoc), })) as ContextMenuProps[]; ContextMenu.Instance.addItem({ description: 'User Templates', subitems: userDocList, icon: 'file', }); } // applies a custom template to a document. the template is identified by it's short name (e.g, slideView not layout_slideView) /** * Applies a template to a Doc and logs the action with the UndoManager * If the template already exists and has been registered, it can be specified by it's signature name (e.g., 'icon' not 'layout_icon'). * Alternatively, the signature can be omitted and the template can be provided. * @param doc the Doc to apply the template to. * @param creator a function that will create the template if it doesn't exist * @param templateSignature the signature name for a template that has already been created and registered on the userDoc. (can be "" if template is provide) * @param template the template to use (optional if templateSignature is provided) * @returns doc */ export function makeCustomViewClicked(doc: Doc, creator: Opt<(documents: Array, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = 'custom', template?: Doc) { const batch = UndoManager.StartBatch('makeCustomViewClicked'); createCustomView(doc, creator, templateSignature || StrCast(template?.title), template); batch.end(); return doc; } export function findTemplate(templateName: string, doc: Doc) { let docLayoutTemplate: Opt; const iconViews = DocListCast(Cast(Doc.UserDoc().template_icons, Doc, null)?.data); const templBtns = DocListCast(Cast(Doc.UserDoc().template_buttons, Doc, null)?.data); const noteTypes = DocListCast(Cast(Doc.UserDoc().template_notes, Doc, null)?.data); const userTypes = DocListCast(Cast(Doc.UserDoc().template_user, Doc, null)?.data); const clickFuncs = DocListCast(Cast(Doc.UserDoc().template_clickFuncs, Doc, null)?.data); const allTemplates = iconViews .concat(templBtns) .concat(noteTypes) .concat(userTypes) .concat(clickFuncs) .map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc) .filter(d => d.isTemplateDoc); // bcz: this is hacky -- want to have different templates be applied depending on the "type" of a document. but type is not reliable and there could be other types of template searches so this should be generalized // first try to find a template that matches the specific document type (). otherwise, fallback to a general match on !docLayoutTemplate && allTemplates.forEach(tempDoc => { const templateType = StrCast(doc[templateName + '_fieldKey'] || doc.type); StrCast(tempDoc.title) === templateName + (templateType[0].toUpperCase() + templateType.slice(1)) && (docLayoutTemplate = tempDoc); }); !docLayoutTemplate && allTemplates.forEach(tempDoc => { StrCast(tempDoc.title) === templateName && (docLayoutTemplate = tempDoc); }); return docLayoutTemplate; } export function createCustomView(doc: Doc, creator: Opt<(documents: Array, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = 'custom', docLayoutTemplate?: Doc) { const templateName = templateSignature.replace(/\(.*\)/, ''); doc.layout_fieldKey = 'layout_' + (templateSignature || (docLayoutTemplate?.title ?? '')); // eslint-disable-next-line no-param-reassign docLayoutTemplate = docLayoutTemplate || findTemplate(templateName, doc); const customName = 'layout_' + templateSignature; const _width = NumCast(doc._width); const _height = NumCast(doc._height); const options = { title: 'data', backgroundColor: StrCast(doc.backgroundColor), _layout_autoHeight: true, _width, x: -_width / 2, y: -_height / 2, _layout_showSidebar: false }; if (docLayoutTemplate) { if (docLayoutTemplate !== doc[customName]) { Doc.ApplyTemplateTo(docLayoutTemplate, doc, customName, undefined); } } else { const fieldTemplate = (() => { if (doc.data instanceof RichTextField || typeof doc.data === 'string') return Docs.Create.TextDocument('', options); if (doc.data instanceof PdfField) return Docs.Create.PdfDocument('http://www.msn.com', options); if (doc.data instanceof VideoField) return Docs.Create.VideoDocument('http://www.cs.brown.edu', options); if (doc.data instanceof AudioField) return Docs.Create.AudioDocument('http://www.cs.brown.edu', options); if (doc.data instanceof ImageField) return Docs.Create.ImageDocument('http://www.cs.brown.edu', options); })(); const docTemplate = creator?.(fieldTemplate ? [fieldTemplate] : [], { title: customName + '(' + doc.title + ')', isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) }); fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, docTemplate ? Doc.GetProto(docTemplate) : docTemplate); docTemplate && Doc.ApplyTemplateTo(docTemplate, doc, customName, undefined); } } export function makeCustomView(doc: Doc, custom: boolean, layout: string) { Doc.setNativeView(doc); if (custom) { makeCustomViewClicked(doc, Docs.Create.StackingDocument, layout, undefined); } } export function iconify(doc: Doc) { const layoutFieldKey = Cast(doc.layout_fieldKey, 'string', null); DocUtils.makeCustomViewClicked(doc, Docs.Create.StackingDocument, 'icon', undefined); if (layoutFieldKey && layoutFieldKey !== 'layout' && layoutFieldKey !== 'layout_icon') doc.deiconifyLayout = layoutFieldKey.replace('layout_', ''); } export function pileup(docList: Doc[], x?: number, y?: number, size: number = 55, create: boolean = true) { runInAction(() => { docList.forEach((doc, i) => { const d = doc; DocUtils.iconify(d); d.x = Math.cos((Math.PI * 2 * i) / docList.length) * size - size; d.y = Math.sin((Math.PI * 2 * i) / docList.length) * size - size; d._timecodeToShow = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection }); }); if (create) { const newCollection = Docs.Create.PileDocument(docList, { title: 'pileup', _freeform_noZoom: true, x: (x || 0) - size, y: (y || 0) - size, _width: size * 2, _height: size * 2, dragWhenActive: true, _layout_fitWidth: false }); newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - size; newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - size; newCollection._width = newCollection._height = size * 2; return newCollection; } return undefined; } export function makeIntoPortal(doc: Doc, layoutDoc: Doc, allLinks: Doc[]) { const portalLink = allLinks.find(d => d.link_anchor_1 === doc && d.link_relationship === 'portal to:portal from'); if (!portalLink) { DocUtils.MakeLink( doc, Docs.Create.FreeformDocument([], { _width: NumCast(layoutDoc._width) + 10, _height: Math.max(NumCast(layoutDoc._height), NumCast(layoutDoc._width) + 10), isLightbox: true, _layout_fitWidth: true, title: StrCast(doc.title) + ' [Portal]', }), { link_relationship: 'portal to:portal from' } ); } doc.followLinkLocation = OpenWhere.lightbox; doc.onClick = FollowLinkScript(); } export function LeavePushpin(doc: Doc, annotationField: string) { if (doc.followLinkToggle) return undefined; const context = Cast(doc.embedContainer, Doc, null) ?? Cast(doc.annotationOn, Doc, null); const hasContextAnchor = Doc.Links(doc).some(l => (l.link_anchor_2 === doc && Cast(l.link_anchor_1, Doc, null)?.annotationOn === context) || (l.link_anchor_1 === doc && Cast(l.link_anchor_2, Doc, null)?.annotationOn === context)); if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) { const pushpin = Docs.Create.FontIconDocument({ title: '', annotationOn: Cast(doc.annotationOn, Doc, null), followLinkToggle: true, icon: 'map-pin', x: Cast(doc.x, 'number', null), y: Cast(doc.y, 'number', null), backgroundColor: '#ACCEF7', layout_hideAllLinks: true, _width: 15, _height: 15, _xMargin: 0, onClick: FollowLinkScript(), _timecodeToShow: Cast(doc._timecodeToShow, 'number', null), }); Doc.AddDocToList(context, annotationField, pushpin); DocUtils.MakeLink(pushpin, doc, { link_relationship: 'pushpin' }, ''); doc._timecodeToShow = undefined; return pushpin; } return undefined; } // /** // * // * @param dms Degree Minute Second format exif gps data // * @param ref ref that determines negativity of decimal coordinates // * @returns a decimal format of gps latitude / longitude // */ // function getDecimalfromDMS(dms?: number[], ref?: string) { // if (dms && ref) { // let degrees = dms[0] / dms[1]; // let minutes = dms[2] / dms[3] / 60.0; // let seconds = dms[4] / dms[5] / 3600.0; // if (['S', 'W'].includes(ref)) { // degrees = -degrees; minutes = -minutes; seconds = -seconds // } // return (degrees + minutes + seconds).toFixed(5); // } // } function ConvertDMSToDD(degrees: number, minutes: number, seconds: number, direction: string) { let dd = degrees + minutes / 60 + seconds / (60 * 60); if (direction === 'S' || direction === 'W') { dd *= -1; } // Don't do anything for N or E return dd; } export function assignUploadInfo(result: Upload.FileInformation, protoIn: Doc) { const proto = protoIn; if (Upload.isTextInformation(result)) { proto.text = result.rawText; } if (Upload.isVideoInformation(result)) { proto.data_duration = result.duration; } if (Upload.isImageInformation(result)) { const maxNativeDim = Math.max(result.nativeHeight, result.nativeWidth); const exifRotation = StrCast(result.exifData?.data?.Orientation).toLowerCase(); proto.data_nativeOrientation = result.exifData?.data?.image?.Orientation ?? (exifRotation.includes('rotate 90') || exifRotation.includes('rotate 270') ? 5 : undefined); proto.data_nativeWidth = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim; proto.data_nativeHeight = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); if (NumCast(proto.data_nativeOrientation) >= 5) { proto.data_nativeHeight = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim; proto.data_nativeWidth = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); } proto.data_exif = JSON.stringify(result.exifData?.data); proto.data_contentSize = result.contentSize; // exif gps data coordinates are stored in DMS (Degrees Minutes Seconds), the following operation converts that to decimal coordinates const latitude = result.exifData?.data?.GPSLatitude; const latitudeDirection = result.exifData?.data?.GPSLatitudeRef; const longitude = result.exifData?.data?.GPSLongitude; const longitudeDirection = result.exifData?.data?.GPSLongitudeRef; if (latitude !== undefined && longitude !== undefined && latitudeDirection !== undefined && longitudeDirection !== undefined) { proto.latitude = ConvertDMSToDD(latitude[0], latitude[1], latitude[2], latitudeDirection); proto.longitude = ConvertDMSToDD(longitude[0], longitude[1], longitude[2], longitudeDirection); } } } async function processFileupload(generatedDocuments: Doc[], name: string, type: string, result: Error | Upload.FileInformation, options: DocumentOptions, overwriteDoc?: Doc) { if (result instanceof Error) { alert(`Upload failed: ${result.message}`); return; } const full = { ...options, _width: 400, title: name }; const pathname = result.accessPaths.agnostic.client; const doc = await DocUtils.DocumentFromType(type, pathname, full, overwriteDoc); if (doc) { DocUtils.assignUploadInfo(result, Doc.GetProto(doc)); overwriteDoc && Doc.removeCurrentlyLoading(overwriteDoc); generatedDocuments.push(doc); } return doc; } export function GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, annotationOn?: Doc, backgroundColor?: string) { const defaultTextTemplate = DocCast(Doc.UserDoc().defaultTextLayout); const tbox = StrCast(Doc.UserDoc().fontFamily) === 'Math' ? Docs.Create.EquationDocument('', { // annotationOn, backgroundColor: backgroundColor ?? StrCast(Doc.UserDoc().textBackgroundColor), borderColor: Doc.UserDoc().borderColor as string, borderWidth: Doc.UserDoc().borderWidth as number, x, y, title, text_fontColor: StrCast(Doc.UserDoc().fontColor), _width: 50, _height: 50, _yMargin: 10, _xMargin: 10, nativeWidth: 40, nativeHeight: 40, }) : (defaultTextTemplate?.type === DocumentType.JOURNAL ? Docs.Create.DailyJournalDocument : Docs.Create.TextDocument)('', { annotationOn, backgroundColor, x, y, title, ...(defaultTextTemplate ? {} // if the new doc will inherit from a template, don't set any layout fields since that would block the inheritance : { _width: width || BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 * 6 : 200, _height: BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 : 35, _layout_autoHeight: true, backgroundColor: backgroundColor ?? StrCast(Doc.UserDoc().textBackgroundColor), borderColor: Doc.UserDoc().borderColor as string, borderWidth: Doc.UserDoc().borderWidth as number, text_centered: BoolCast(Doc.UserDoc().textCentered), text_fitBox: BoolCast(Doc.UserDoc().fitBox), text_align: StrCast(Doc.UserDoc().textAlign), text_fontColor: StrCast(Doc.UserDoc().fontColor), text_fontFamily: StrCast(Doc.UserDoc().fontFamily), text_fontWeight: StrCast(Doc.UserDoc().fontWeight), text_fontStyle: StrCast(Doc.UserDoc().fontStyle), text_fontDecoration: StrCast(Doc.UserDoc().fontDecoration), }), }); if (defaultTextTemplate) { tbox.layout_fieldKey = 'layout_' + StrCast(defaultTextTemplate.title); Doc.GetProto(tbox)[StrCast(tbox.layout_fieldKey)] = defaultTextTemplate; // set the text doc's layout to render with the text template tbox.$proto = defaultTextTemplate; // and also set the text doc to inherit from the template (this allows the template to specify default field values) } return tbox; } export function uploadYoutubeVideoLoading(videoId: string, options: DocumentOptions, overwriteDoc?: Doc) { const generatedDocuments: Doc[] = []; Networking.UploadYoutubeToServer(videoId, overwriteDoc?.[Id]).then(upfiles => { const { source: { newFilename, mimetype }, result, } = upfiles.lastElement(); if (result instanceof Error) { if (overwriteDoc) { overwriteDoc.isLoading = false; overwriteDoc.loadingError = result.message; Doc.removeCurrentlyLoading(overwriteDoc); } } else newFilename && processFileupload(generatedDocuments, newFilename, mimetype ?? '', result, options, overwriteDoc); }); } /** * uploadFilesToDocs will take in an array of Files, and creates documents for the * new files. * * @param files an array of files that will be uploaded * @param options options to use while uploading * @returns */ export async function uploadFilesToDocs(files: File[], options: DocumentOptions) { const generatedDocuments: Doc[] = []; // These files do not have overwriteDocs, so we do not set the guid and let the client generate one. const fileNoGuidPairs: Networking.FileGuidPair[] = files.map(file => ({ file })); const upfiles = await Networking.UploadFilesToServer(fileNoGuidPairs); upfiles.forEach(({ source: { newFilename, mimetype }, result }) => { newFilename && mimetype && processFileupload(generatedDocuments, newFilename, mimetype, result, options); }); return generatedDocuments; } export async function openSVGfile(file: File, options: DocumentOptions) { const reader = new FileReader(); const scale = 1; const startPoint = { X: (options.x as number) ?? 0, Y: (options.y as number) ?? 0 }; const buffer = await new Promise((res, rej) => { reader.onload = event => { const fileContent = event.target?.result; // Process the file content here console.log(fileContent); typeof fileContent === 'string' ? res(fileContent) : rej(); }; reader.readAsText(file); }); const svg = buffer.match(/]*>([\s\S]*?)<\/svg>/g); if (svg) { const svgObject = await parse(svg[0]); const strokeData: [InkData, string, string][] = []; const tl = { X: Number.MAX_SAFE_INTEGER, Y: Number.MAX_SAFE_INTEGER }; let last: PointData = { X: 0, Y: 0 }; const processStroke = (child: INode) => { child.attributes.d .split(/[\n]?M/) .slice(1) .map((d, ind) => { const convertedBezier: InkData = SVGToBezier(child.name as SVGType, { ...child, d: '\nM' + d } as unknown as Record, last); last = convertedBezier.lastElement(); convertedBezier.forEach(point => { if (point.X < tl.X) tl.X = point.X; if (point.Y < tl.Y) tl.Y = point.Y; }); strokeData.push([convertedBezier, child.attributes.stroke || 'black', ind === 0 ? child.attributes.fill : child.attributes.fill === 'none' ? child.attributes.fill : DashColor(child.attributes.fill).negate().toString()]); }); }; const processNode = (parent: INode) => { if (parent.children.length) parent.children.forEach(processNode); else if (parent.type !== 'text') processStroke(parent); }; processNode(svgObject); const mapStroke = (pd: PointData): PointData => ({ X: startPoint.X + (pd.X - tl.X) * scale, Y: startPoint.Y + (pd.Y - tl.Y) * scale }); return SmartDrawHandler.CreateDrawingDoc( strokeData.map(sdata => [sdata[0].map(mapStroke), sdata[1], sdata[2]] as [PointData[], string, string]), { autoColor: true }, '', undefined ); } } export function uploadFileToDoc(file: File, options: DocumentOptions, overwriteDoc: Doc) { const generatedDocuments: Doc[] = []; // Since this file has an overwriteDoc, we can set the client tracking guid to the overwriteDoc's guid. return Networking.UploadFilesToServer([{ file, guid: overwriteDoc[Id] }]).then(upfiles => { const { source: { newFilename, mimetype }, result, } = upfiles.lastElement() ?? { source: { newFilename: '', mimetype: '' }, result: new Error('upload failed') }; if (result instanceof Error) { if (overwriteDoc) { overwriteDoc.loadingError = result.message; Doc.removeCurrentlyLoading(overwriteDoc); } return undefined; } return newFilename && mimetype ? processFileupload(generatedDocuments, newFilename, mimetype, result, options, overwriteDoc) : undefined; }); } // copies the specified drag factory document export function copyDragFactory(dragFactory: Doc) { if (!dragFactory) return undefined; const ndoc = dragFactory.isTemplateDoc ? Doc.ApplyTemplate(dragFactory) : Doc.MakeCopy(dragFactory, true); if (ndoc && dragFactory.dragFactory_count !== undefined) { dragFactory.dragFactory_count = NumCast(dragFactory.dragFactory_count) + 1; Doc.SetInPlace(ndoc, 'title', ndoc.title + ' ' + NumCast(dragFactory.dragFactory_count).toString(), true); } return ndoc; } export function delegateDragFactory(dragFactory: Doc) { const ndoc = Doc.MakeDelegateWithProto(dragFactory); if (ndoc && dragFactory.dragFactory_count !== undefined) { dragFactory.dragFactory_count = NumCast(dragFactory.dragFactory_count) + 1; Doc.GetProto(ndoc).title = ndoc.title; } return ndoc; } export function Zip(doc: Doc, zipFilename = 'dashExport.zip') { const { clone, map, linkMap } = Doc.MakeClone(doc); const proms = new Set(); function replacer(key: string, value: { url: string; [key: string]: unknown }) { if (key && ['branchOf', 'cloneOf', 'cursors'].includes(key)) return undefined; if (value?.__type === 'image') { const extension = value.url.replace(/.*\./, ''); proms.add(value.url.replace('.' + extension, '_o.' + extension)); return SerializationHelper.Serialize(new ImageField(value.url)); } if (value?.__type === 'pdf') { proms.add(value.url); return SerializationHelper.Serialize(new PdfField(value.url)); } if (value?.__type === 'audio') { proms.add(value.url); return SerializationHelper.Serialize(new AudioField(value.url)); } if (value?.__type === 'video') { proms.add(value.url); return SerializationHelper.Serialize(new VideoField(value.url)); } if ( value instanceof Doc || value instanceof ScriptField || value instanceof RichTextField || value instanceof InkField || value instanceof CsvField || value instanceof WebField || value instanceof DateField || value instanceof ProxyField || value instanceof ComputedField ) { return SerializationHelper.Serialize(value); } if (value instanceof Array && key !== ListFieldName && key !== InkDataFieldName) return { fields: value, __type: 'list' }; return value; } const docs: { [id: string]: unknown } = {}; const links: { [id: string]: unknown } = {}; Array.from(map.entries()).forEach(f => { docs[f[0]] = f[1]; }); Array.from(linkMap.entries()).forEach(l => { links[l[0]] = l[1]; }); const jsonDocs = JSON.stringify({ id: clone[Id], docs, links }, decycle(replacer)); const zip = new JSZip(); let count = 0; const promArr = Array.from(proms) .filter(url => url?.startsWith('/files')) .map(url => url.replace('/', '')); // window.location.origin)); console.log(promArr.length); if (!promArr.length) { zip.file('docs.json', jsonDocs); zip.generateAsync({ type: 'blob' }).then(content => saveAs(content, zipFilename)); } else promArr.forEach((url, i) => { // loading a file and add it in a zip file JSZipUtils.getBinaryContent(window.location.origin + '/' + url, (err: unknown, data: unknown) => { if (err) throw err; // or handle the error // // Generate a directory within the Zip file structure // const assets = zip.folder("assets"); // assets.file(filename, data, {binary: true}); const assetPathOnServer = promArr[i].replace(window.location.origin + '/', '').replace(/\//g, '%%%'); zip.file(assetPathOnServer, data as string, { binary: true }); console.log(' => ' + url); if (++count === promArr.length) { zip.file('docs.json', jsonDocs); zip.generateAsync({ type: 'blob' }).then(content => saveAs(content, zipFilename)); // const a = document.createElement("a"); // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`); // a.href = url; // a.download = `DocExport-${this.props.Document[Id]}.zip`; // a.click(); } }); }); } } export function FollowLinkScript() { return ScriptField.MakeScript('return followLink(this,altKey)', { altKey: 'boolean' }); } export function IsFollowLinkScript(field: FieldResult) { return ScriptCast(field)?.script.originalScript.includes('return followLink('); } ScriptingGlobals.add('Docs', Docs); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function copyDragFactory(dragFactory: Doc, asDelegate?: boolean) { return dragFactory instanceof Doc ? (asDelegate ? DocUtils.delegateDragFactory(dragFactory) : DocUtils.copyDragFactory(dragFactory)) : dragFactory; }); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function makeDelegate(proto: Doc) { const d = Docs.Create.DelegateDocument(proto, { title: 'child of ' + proto.title }); return d; }); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function generateLinkTitle(link: Doc) { const linkAnchor1title = link.link_anchor_1 && link.link_anchor_1 !== link ? Cast(link.link_anchor_1, Doc, null)?.title : ''; const linkAnchor2title = link.link_anchor_2 && link.link_anchor_2 !== link ? Cast(link.link_anchor_2, Doc, null)?.title : ''; const relation = link.link_relationship || 'to'; return `${linkAnchor1title} (${relation}) ${linkAnchor2title}`; });