import { action, computed, makeObservable, observable } from 'mobx'; import * as React from 'react'; import * as rp from 'request-promise'; import { ClientUtils, DashColor, returnFalse } from '../../../ClientUtils'; import CursorField from '../../../fields/CursorField'; import { Doc, DocListCast, expandedFieldName, GetDocFromUrl, GetHrefFromHTML, Opt, RTFIsFragment, StrListCast } from '../../../fields/Doc'; import { AclPrivate, DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, DateCast, DocCast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; import { Upload } from '../../../server/SharedMediaTypes'; import { DocServer } from '../../DocServer'; import { Networking } from '../../Network'; import { DocUtils } from '../../documents/DocUtils'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { Docs, DocumentOptions } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { dropActionType } from '../../util/DropActionTypes'; import { ImageUtils } from '../../util/Import & Export/ImageUtils'; import { SnappingManager } from '../../util/SnappingManager'; import { UndoManager } from '../../util/UndoManager'; import { ViewBoxBaseComponent } from '../DocComponent'; import { DocumentViewProps } from '../nodes/DocumentContentsView'; import { DocumentView } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere'; import { FlashcardPracticeUI } from './FlashcardPracticeUI'; export enum docSortings { Time = 'time', Type = 'type', Color = 'color', Chat = 'chat', Tag = 'tag', None = '', } export const ChatSortField = 'chat_sortIndex'; export interface CollectionViewProps extends React.PropsWithChildren { isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc) isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently) layoutEngine?: () => string; setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt) => void) => void; ignoreUnrendered?: boolean; // property overrides for child documents childDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox) childDocumentsActive?: () => boolean | undefined; // whether child documents can be dragged if collection can be dragged (eg., in a when a Pile document is in startburst mode) childContentsActive?: () => boolean | undefined; childLayoutFitWidth?: (child: Doc) => boolean; childlayout_showTitle?: () => string; childOpacity?: () => number; childContextMenuItems?: () => { script: ScriptField; label: string }[]; childLayoutTemplate?: () => Doc | undefined; // specify a layout Doc template to use for children of the collection childRejectDrop?: (de: DragManager.DropEvent, subView?: DocumentView) => boolean; // whether a child document can be dropped on this document childHideDecorationTitle?: boolean; childHideResizeHandles?: boolean; childHideDecorations?: boolean; childDragAction?: dropActionType; childXPadding?: number; childYPadding?: number; childLayoutString?: string; childIgnoreNativeSize?: boolean; childClickScript?: ScriptField; childDoubleClickScript?: ScriptField; AddToMap?: (treeViewDoc: Doc, index: number[]) => void; RemFromMap?: (treeViewDoc: Doc, index: number[]) => void; hierarchyIndex?: number[]; // hierarchical index of a document up to the rendering root (primarily used for tree views) } export interface SubCollectionViewProps extends CollectionViewProps { isAnyChildContentActive: () => boolean; } export function CollectionSubView() { class CollectionSubViewInternal extends ViewBoxBaseComponent() { private dropDisposer?: DragManager.DragDropDisposer; private gestureDisposer?: GestureUtils.GestureEventDisposer; protected _mainCont?: HTMLDivElement; constructor(props: X & SubCollectionViewProps) { super(props); makeObservable(this); } @observable _focusFilters: Opt = undefined; // childFilters that are overridden when previewing a link to an anchor which has childFilters set on it @observable _focusRangeFilters: Opt = undefined; // childFiltersByRanges that are overridden when previewing a link to an anchor which has childFiltersByRanges set on it protected createDashEventsTarget = (ele: HTMLDivElement | null) => { this.dropDisposer?.(); this.gestureDisposer?.(); if (ele) { this._mainCont = ele; this.dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc, this.onInternalPreDrop.bind(this)); this.gestureDisposer = GestureUtils.MakeGestureTarget(ele, this.onGesture.bind(this)); } }; protected CreateDropTarget(ele: HTMLDivElement) { // used in schema view this.createDashEventsTarget(ele); } componentWillUnmount() { this.gestureDisposer?.(); } get dataDoc() { return this._props.TemplateDataDocument instanceof Doc && this.Document.isTemplateForField // ? Doc.GetProto(this._props.TemplateDataDocument) : this.Document.rootDocument ? this.Document : this.Document[DocData]; // if the layout document has a rootDocument, then we don't want to get its parent which would be the unexpanded template } get childContainerViewPath() { return this.DocumentView?.().docViewPath; } // this returns whether either the collection is selected, or the template that it is part of is selected rootSelected = () => this._props.isSelected() || BoolCast(this._props.TemplateDataDocument && this._props.rootSelected?.()); // The data field for rendering this collection will be on the this.Document unless we're rendering a template in which case we try to use props.TemplateDataDocument. // When a document has a TemplateDataDoc but it's not a template, then it contains its own rendering data, but needs to pass the TemplateDataDoc through // to its children which may be templates. // If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey' @computed get dataField() { return this.dataDoc[this._props.fieldKey]; // this used to be 'layoutDoc', but then template fields will get ignored since the template is not a proto of the layout. hopefully nothing depending on the previous code. } hasChildDocs = () => this.childLayoutPairs.map(pair => pair.layout); @computed get childLayoutPairs(): { layout: Doc; data: Doc }[] { const lastEle = this.DocumentView?.(); if (!lastEle?.IsInvalid(this.Document)) { const rootTemplate = lastEle && Doc.LayoutDoc(lastEle.rootDoc).isTemplateDoc && Doc.LayoutDoc(lastEle.rootDoc); const templateFieldKey = rootTemplate && [expandedFieldName(rootTemplate), ...this._props.docViewPath() .filter(dv => dv.Document.isTemplateForField) .map(dv => dv.Document.title), ].join('_'); // prettier-ignore return this.childDocs .map(doc => Doc.GetLayoutDataDocPair(this.Document, !this._props.isAnnotationOverlay ? this._props.TemplateDataDocument : undefined, doc, templateFieldKey || "")) .filter(pair => // filter out any documents that have a proto that we don't have permissions to !pair.layout?.hidden && pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate))) .filter(pair => !this._filterFunc?.(pair.layout!)) .map(({ data, layout }) => ({ data: data!, layout: layout! })); // prettier-ignore } return []; } /** * This is the raw, stored list of children on a collection. If you modify this list, the database will be updated */ @computed get childDocList() { return Cast(this.dataField, listSpec(Doc)); } addLinkedDocTab = (docsIn: Doc | Doc[], location: OpenWhere) => { const doc = toList(docsIn).lastElement(); const where = location.split(':')[0]; if (where === OpenWhere.lightbox && (this.childDocList?.includes(doc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(doc))) { if (doc.hidden) doc.hidden = false; if (!location.includes(OpenWhereMod.always)) return true; } return this._props.addDocTab(docsIn, location); }; collectionFilters = () => this._focusFilters ?? StrListCast(this.Document._childFilters); collectionRangeDocFilters = () => this._focusRangeFilters ?? StrListCast(this.Document._childFiltersByRanges); // child filters apply to the descendants of the documents in this collection childDocFilters = () => [...(this._props.childFilters?.().filter(f => ClientUtils.IsRecursiveFilter(f)) || []), ...this.collectionFilters()]; // unrecursive filters apply to the documents in the collection, but no their children. See Utils.noRecursionHack unrecursiveDocFilters = () => [...(this._props.childFilters?.().filter(f => !ClientUtils.IsRecursiveFilter(f)) || [])]; childDocRangeFilters = () => [...(this._props.childFiltersByRanges?.() || []), ...this.collectionRangeDocFilters()]; searchFilterDocs = () => this._props.searchFilterDocs?.() ?? DocListCast(this.Document._searchFilterDocs); @observable docDraggedIndex = -1; @computed.struct get childDocs() { TraceMobx(); let rawdocs: (Doc | Promise)[] = []; if (this.dataField instanceof Doc) { // if collection data is just a document, then promote it to a singleton list; rawdocs = [this.dataField]; } else if (Cast(this.dataField, listSpec(Doc), null)) { // otherwise, if the collection data is a list, then use it. rawdocs = DocListCast(this.dataField); } else if (this.dataField) { // Finally, if it's not a doc or a list and the document is a template, we try to render the root doc. // For example, if an image doc is rendered with a slide template, the template will try to render the data field as a collection. // Since the data field is actually an image, we set the list of documents to the singleton of root document's proto which will be an image. const templateRoot = this._props.TemplateDataDocument; rawdocs = templateRoot && !this._props.isAnnotationOverlay ? [Doc.GetProto(templateRoot)] : []; } const childDocs = this.childSortedDocs( rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this._props.ignoreUnrendered || !d.layout_unrendered)).map(d => d as Doc), this.docDraggedIndex ); const childDocFilters = this.childDocFilters(); const childFiltersByRanges = this.childDocRangeFilters(); const searchDocs = this.searchFilterDocs(); if (this.Document.dontRegisterView || (!childDocFilters.length && !this.unrecursiveDocFilters().length && !childFiltersByRanges.length && !searchDocs.length)) { return childDocs.filter(cd => !cd.cookies); // remove any documents that require a cookie if there are no filters to provide one } const docsforFilter: Doc[] = []; childDocs.forEach(d => { // dragging facets const dragged = this._props.childFilters?.().some(f => f.includes(ClientUtils.noDragDocsFilter)); if (dragged && SnappingManager.CanEmbed && DragManager.docsBeingDragged.includes(d)) return; let notFiltered = d.z || Doc.IsSystem(d) || DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), childFiltersByRanges, this.Document).length > 0; if (notFiltered) { notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, childFiltersByRanges, this.Document).length > 0; const fieldKey = Doc.LayoutDataKey(d); const isAnnotatableDoc = d[fieldKey] instanceof List && !(d[fieldKey] as List)?.some(ele => !(ele instanceof Doc)); const docChildDocs = d[isAnnotatableDoc ? fieldKey + '_annotations' : fieldKey]; const sidebarDocs = isAnnotatableDoc && d[fieldKey + '_sidebar']; if (docChildDocs !== undefined || sidebarDocs !== undefined) { let subDocs = [...DocListCast(docChildDocs), ...DocListCast(sidebarDocs)]; if (subDocs.length > 0) { let newarray: Doc[] = []; notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, childDocFilters, childFiltersByRanges, d).length); while (subDocs.length > 0 && !notFiltered) { newarray = []; // eslint-disable-next-line no-loop-func subDocs.forEach(t => { const docFieldKey = Doc.LayoutDataKey(t); const isSubDocAnnotatable = t[docFieldKey] instanceof List && !(t[docFieldKey] as List)?.some(ele => !(ele instanceof Doc)); notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !childFiltersByRanges.length) || DocUtils.FilterDocs([t], childDocFilters, childFiltersByRanges, d).length)); DocListCast(t[isSubDocAnnotatable ? docFieldKey + '_annotations' : docFieldKey]).forEach(newdoc => newarray.push(newdoc)); isSubDocAnnotatable && DocListCast(t[docFieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc)); }); subDocs = newarray; } } } } notFiltered && docsforFilter.push(d); }); return docsforFilter; } childSortedDocs = (docsIn: Doc[], dragIndex: number) => { const sortType = StrCast(this.Document[this._props.fieldKey + '_sort']) as docSortings; const isDesc = BoolCast(this.Document[this._props.fieldKey + '_sort_reverse']); const docs = docsIn.slice(); sortType && docs.sort((docA, docB) => { const [typeA, typeB] = (() => { switch (sortType) { default: case docSortings.Type: return [StrCast(docA.type), StrCast(docB.type)]; case docSortings.Chat: return [NumCast(docA[ChatSortField], 9999), NumCast(docB[ChatSortField], 9999)]; case docSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(), DateCast(docB.author_date)?.date ?? Date.now()]; case docSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()]; case docSortings.Tag: return [StrListCast(docA.tags).join(""), StrListCast(docB.tags).join("")]; } })(); return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? -1 : 1); }); //prettier-ignore if (dragIndex !== -1) { const draggedDoc = DragManager.docsBeingDragged[0]; const originalIndex = docs.findIndex(doc => doc === draggedDoc); originalIndex !== -1 && docs.splice(originalIndex, 1); draggedDoc && docs.splice(dragIndex, 0, draggedDoc); } return docs; }; @action protected async setCursorPosition(position: [number, number]) { let ind; const doc = this.Document; const id = Doc.UserDoc()[Id]; const email = ClientUtils.CurrentUserEmail(); const pos = { x: position[0], y: position[1] }; if (id && email) { const proto = Doc.GetProto(doc); if (!proto) { return; } // The following conditional detects a recurring bug we've seen on the server if (proto[Id] === Docs.Prototypes.get(DocumentType.COL)[Id]) { alert('COLLECTION PROTO CURSOR ISSUE DETECTED! Check console for more info...'); throw new Error(`AHA! You were trying to set a cursor on a collection's proto, which is the original collection proto! Look at the two previously printed lines for document values!`); } let cursors = Cast(proto.cursors, listSpec(CursorField)); if (!cursors) { proto.cursors = cursors = new List(); } if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.data.metadata.id === id)) > -1) { cursors[ind].setPosition(pos); } else { const entry = new CursorField({ metadata: { id: id, identifier: email, timestamp: Date.now() }, position: pos }); cursors.push(entry); } } } // eslint-disable-next-line @typescript-eslint/no-unused-vars protected onGesture(e: Event, ge: GestureUtils.GestureEvent) {} protected onInternalPreDrop(e: Event, de: DragManager.DropEvent, targetDropAction: dropActionType) { const dragData = de.complete.docDragData; if (dragData) { const sourceDragAction = dragData.dropAction; const sameCollection = !dragData.draggedDocuments.some(d => d.embedContainer !== this.Document); dragData.dropAction = !sameCollection // if doc from another tree ? sourceDragAction || targetDropAction // then use the source's dragAction otherwise the target's : sourceDragAction === dropActionType.inPlace // if source drag is inPlace ? sourceDragAction // keep the doc in place : dropActionType.same; // otherwise use same tree semantics to move within tree e.stopPropagation(); } } addDocument = (doc: Doc | Doc[], annotationKey?: string) => this._props.addDocument?.(doc, annotationKey) || false; removeDocument = (doc: Doc | Doc[], annotationKey?: string) => this._props.removeDocument?.(doc, annotationKey) || false; moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean) => this._props.moveDocument?.(doc, targetCollection, addDocument) || false; protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean { const { docDragData } = de.complete; if (docDragData && !docDragData.draggedDocuments.includes(this.Document) && !this._props.rejectDrop?.(de, this.DocumentView?.())) { let added; const dropAction = docDragData.dropAction || docDragData.userDropAction; const targetDocments = DocListCast(this.dataDoc[this._props.fieldKey]); const someMoved = !dropAction && docDragData.draggedDocuments.some(drag => targetDocments.includes(drag)); if (someMoved) docDragData.droppedDocuments = docDragData.droppedDocuments.map((drop, i) => (targetDocments.includes(docDragData.draggedDocuments[i]) ? docDragData.draggedDocuments[i] : drop)); if ((!dropAction || dropAction === dropActionType.inPlace || dropAction === dropActionType.same || dropAction === dropActionType.move || someMoved) && docDragData.moveDocument) { const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d); const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d); if (movedDocs.length) { const canAdd = (de.embedKey || dropAction || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.Document)) && (dropAction !== dropActionType.inPlace || docDragData.draggedDocuments.every(d => d.embedContainer === this.Document)); const moved = docDragData.moveDocument(movedDocs, this.Document, canAdd ? this.addDocument : returnFalse); added = canAdd || moved ? moved : undefined; } else if (addedDocs.length) { added = this.addDocument(addedDocs); } if (!added && ScriptCast(this.Document.dropConverter)) { ScriptCast(this.Document.dropConverter)?.script.run({ dragData: docDragData }); added = addedDocs.length ? this.addDocument(addedDocs) : true; } } else { ScriptCast(this.Document.dropConverter)?.script.run({ dragData: docDragData }); added = this.addDocument(docDragData.droppedDocuments); !added && alert('You cannot perform this move'); } added === false && !this._props.isAnnotationOverlay && e.preventDefault(); added === true && e.stopPropagation(); return !!added; } if (de.complete.annoDragData) { if (![de.complete.annoDragData.dragDocument.embedContainer, de.complete.annoDragData.dragDocument].includes(this.Document)) { de.complete.annoDragData.dropDocCreator = () => this.getAnchor?.(true) || this.Document; } else { const dropCreator = de.complete.annoDragData.dropDocCreator; de.complete.annoDragData.dropDocCreator = () => { const dropped = dropCreator(this._props.isAnnotationOverlay ? this.Document : undefined); this.addDocument(dropped); return dropped; }; } return true; } return false; } protected async onExternalDrop(e: React.DragEvent, options: DocumentOptions = {}, completed?: (docs: Doc[]) => void) { if (e.ctrlKey) { e.stopPropagation(); // bcz: this is a hack to stop propagation when dropping an image on a text document with shift+ctrl return; } const { dataTransfer } = e; const html = dataTransfer.getData('text/html'); const text = dataTransfer.getData('text/plain'); const uriList = dataTransfer.getData('text/uri-list'); if (text && text.startsWith(' this.addDocument(doc); if (html) { if (RTFIsFragment(html)) { const href = GetHrefFromHTML(html); if (href) { const docId = GetDocFromUrl(href); if (docId) { // prosemirror text containing link to dash document DocServer.GetRefField(docId).then(f => { if (f instanceof Doc) { if (options.x || options.y) { f.x = options.x as number; f.y = options.y as number; } // should be in CollectionFreeFormView f instanceof Doc && addDocument(f); } }); } else { addDocument(Docs.Create.WebDocument(href, { ...options, title: href })); } } else if (text) { addDocument(Docs.Create.TextDocument(text, { ...options, _layout_showTitle: StrCast(Doc.UserDoc().layout_showTitle), _width: 100, _height: 25 })); } return; } if (!html.startsWith(' 1 && tags[1].startsWith('img') ? tags[1] : ''; const cors = img.includes('corsproxy') ? img.match(/http.*corsproxy\//)![0] : ''; img = cors ? img.replace(cors, '') : img; if (img) { const imgSrc = img.split('src="')[1].split('"')[0]; const imgOpts = { ...options, _width: 300 }; if (imgSrc.startsWith('data:image') && imgSrc.includes('base64')) { const result = ((await Networking.PostToServer('/uploadRemoteImage', { sources: [imgSrc] })) as Upload.ImageInformation[]).lastElement(); const newImgSrc = result.accessPaths.agnostic.client.indexOf('dashblobstore') === -1 // ? ClientUtils.prepend(result.accessPaths.agnostic.client) : result.accessPaths.agnostic.client; addDocument(ImageUtils.AssignImgInfo(Docs.Create.ImageDocument(newImgSrc, imgOpts), result)); } else if (imgSrc.startsWith('http')) { const doc = Docs.Create.ImageDocument(imgSrc, imgOpts); addDocument(ImageUtils.AssignImgInfo(doc, await ImageUtils.ExtractImgInfo(doc))); } return; } const path = window.location.origin + '/doc/'; if (text.startsWith(path)) { const docId = text.replace(Doc.globalServerPath(), '').split('?')[0]; DocServer.GetRefField(docId).then(f => { const fDoc = f; if (fDoc instanceof Doc) { if (options.x || options.y) { fDoc.x = options.x as number; fDoc.y = options.y as number; } // should be in CollectionFreeFormView addDocument(fDoc); } }); } else { const srcWeb = DocumentView.Selected().lastElement(); const srcUrl = (srcWeb?.Document.data as WebField)?.url?.href?.match(/https?:\/\/[^/]*/)?.[0]; const reg = new RegExp(ClientUtils.prepend(''), 'g'); const modHtml = srcUrl ? html.replace(reg, srcUrl) : html; const backgroundColor = tags.map(tag => tag.match(/.*(background-color: ?[^;]*)/)?.[1]?.replace(/background-color: ?(.*)/, '$1')).filter(t => t)?.[0]; const htmlDoc = Docs.Create.HtmlDocument(modHtml, { ...options, title: srcUrl ? 'from:' + srcUrl : '-web clip-', _width: 300, _height: 300, backgroundColor }); Doc.GetProto(htmlDoc)['data-text'] = Doc.GetProto(htmlDoc).text = text; addDocument(htmlDoc); if (srcWeb) { const iframe = DocumentView.Selected()[0].ContentDiv?.getElementsByTagName('iframe')?.[0]; const focusNode = iframe?.contentDocument?.getSelection()?.focusNode; if (focusNode) { const anchor = srcWeb?.ComponentView?.getAnchor?.(true); anchor && DocUtils.MakeLink(htmlDoc, anchor, {}); } } } return; } } if (uriList || text) { if ((uriList || text).includes('www.youtube.com/watch') || text.includes('www.youtube.com/embed') || text.includes('www.youtube.com/shorts')) { const batch = UndoManager.StartBatch('youtube upload'); const generatedDocuments: Doc[] = []; this.slowLoadDocuments((uriList || text).split('v=').lastElement().split('&')[0].split('shorts/').lastElement(), options, generatedDocuments, text, completed, addDocument).then(batch.end); return; } // let matches: RegExpExecArray | null; // if ((matches = /(https:\/\/)?docs\.google\.com\/document\/d\/([^\\]+)\/edit/g.exec(text)) !== null) { // const newBox = Docs.Create.TextDocument("", { ...options, _width: 400, _height: 200, title: "Awaiting title from Google Docs..." }); // const proto = newBox.proto!; // const documentId = matches[2]; // proto[GoogleRef] = documentId; // proto.data = "Please select this document and then click on its pull button to load its contents from from Google Docs..."; // proto.backgroundColor = "#eeeeff"; // addDocument(newBox); // return; // } // if ((matches = /(https:\/\/)?photos\.google\.com\/(u\/3\/)?album\/([^\\]+)/g.exec(text)) !== null) { // const albumId = matches[3]; // const mediaItems = await GooglePhotos.Query.AlbumSearch(albumId); // return; // } } if (uriList) { addDocument( Docs.Create.WebDocument(uriList.split('#annotations:')[0], { // clean hypothes.is URLs that reference a specific annotation (eg. https://en.wikipedia.org/wiki/Cartoon#annotations:t7qAeNbCEeqfG5972KR2Ig) ...options, title: uriList.split('#annotations:')[0], _width: 400, _height: 512, _nativeWidth: 850, data_useCors: true, }) ); return; } const { items } = e.dataTransfer; const { length } = items; const files: File[] = []; const generatedDocuments: Doc[] = []; if (!length) { alert('No uploadable content found.'); return; } const batch = UndoManager.StartBatch('collection view drop'); for (let i = 0; i < length; i++) { const item = e.dataTransfer.items[i]; if (item.kind === 'string' && item.type.includes('uri')) { // eslint-disable-next-line no-await-in-loop const stringContents = await new Promise(resolve => { item.getAsString(resolve); }); // eslint-disable-next-line no-await-in-loop const type = (await rp.head(ClientUtils.CorsProxy(stringContents)))['content-type']; if (type) { // eslint-disable-next-line no-await-in-loop const doc = await DocUtils.DocumentFromType(type, ClientUtils.CorsProxy(stringContents), options); doc && generatedDocuments.push(doc); } } if (item.kind === 'file') { const file = item.getAsFile(); file?.type && files.push(file); } } this.slowLoadDocuments(files, options, generatedDocuments, text, completed, addDocument).then(batch.end); } slowLoadDocuments = async (files: File[] | string, options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: ((doc: Doc[]) => void) | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => { // create placeholder docs // inside placeholder docs have some func that let pileUpDoc; if (typeof files === 'string') { const loading = Docs.Create.LoadingDocument(files, options); generatedDocuments.push(loading); Doc.addCurrentlyLoading(loading); DocUtils.uploadYoutubeVideoLoading(files, {}, loading); } else { generatedDocuments.push( ...(await Promise.all( files.map(async file => { if (file.name.endsWith('svg')) { return (await DocUtils.openSVGfile(file, options)) as Doc; } const loading = Docs.Create.LoadingDocument(file, options); Doc.addCurrentlyLoading(loading); DocUtils.uploadFileToDoc(file, {}, loading).then(d => { if (d && d?.type === DocumentType.IMG) { const imgTemplate = DocCast(Doc.UserDoc().defaultImageLayout); if (imgTemplate) { const templateFieldKey = StrCast(imgTemplate.title); d.layout_fieldKey = templateFieldKey; d[templateFieldKey] = imgTemplate; } } }); return loading; }) )) ); } if (generatedDocuments.length) { // Creating a dash document const isFreeformView = this.Document._type_collection === CollectionViewType.Freeform; const set = !isFreeformView ? generatedDocuments : generatedDocuments.length > 1 ? generatedDocuments.map(d => { DocUtils.iconify(d); return d; }) : []; if (completed) completed(set); else if (isFreeformView && generatedDocuments.length > 1) { pileUpDoc = DocUtils.pileup(generatedDocuments, options.x as number, options.y as number)!; addDocument(pileUpDoc); } else { generatedDocuments.forEach(addDocument); } } else if (text && !text.includes('https://')) { addDocument(Docs.Create.TextDocument(text, { ...options, title: text.substring(0, 20), _width: 400, _height: 315 })); } else { alert('Document upload failed - possibly an unsupported file type.'); } }; protected _sideBtnWidth = 35; protected _sideBtnMaxPanelPct = 0.15; @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined; /** * How much the content of the collection is being scaled based on its nesting and its fit-to-width settings */ @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (!this._props.fitWidth?.(this.Document) ? this._props.NativeDimScaling?.()||1: 1); } // prettier-ignore /** * The maximum size a UI widget can be in collection coordinates based on not wanting the widget to visually obscure too much of the collection * This takes the desired screen space size and converts into collection coordinates. It then returns the smaller of the converted * size or a fraction of the collection view. */ @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth * this.contentScaling, (this._props.fitWidth?.(this.Document) && this._props.PanelWidth() > NumCast(this.layoutDoc._width)? 1: this._sideBtnMaxPanelPct) * NumCast(this.layoutDoc.width, 1)); } // prettier-ignore /** * This computes a scale factor for UI elements so that they shrink and grow as the collection does in screen space. * Note, the scale factor does not allow for elements to grow larger than their native screen space size. */ @computed get uiBtnScaling() { return this.maxWidgetSize / this._sideBtnWidth; } // prettier-ignore screenXPadding = (docView?: DocumentView) => { if (!docView) return 0; const diff = this._props.PanelWidth() - docView.PanelWidth(); const xpad1 = this.uiBtnScaling * (this._sideBtnWidth - NumCast(this.layoutDoc.xMargin)) - diff / 2; // this._sideBtnWidth; return xpad1 / (docView.NativeDimScaling?.() || 1); }; filteredChildDocs = () => this.childLayoutPairs.map(pair => pair.layout); childDocsFunc = () => this.childDocs; @action setFilterFunc = (func?: (doc: Doc) => boolean) => { this._filterFunc = func; }; // prettier-ignore public flashCardUI = (curDoc: () => Doc | undefined, docViewProps: () => DocumentViewProps, answered?: (correct: boolean) => void) => { return ( ); }; } return CollectionSubViewInternal; }