import { action, computed, makeObservable, observable } from 'mobx'; import * as React from 'react'; import * as rp from 'request-promise'; import { Utils, returnFalse } from '../../../Utils'; import CursorField from '../../../fields/CursorField'; import { Doc, DocListCast, Field, Opt, 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 { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; import { DocServer } from '../../DocServer'; import { Networking } from '../../Network'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { ViewBoxBaseComponent } from '../DocComponent'; import { DocUtils, Docs, DocumentOptions } from '../../documents/Documents'; import { DragManager, dropActionType } from '../../util/DragManager'; import { ImageUtils } from '../../util/Import & Export/ImageUtils'; import { SelectionManager } from '../../util/SelectionManager'; import { SnappingManager } from '../../util/SnappingManager'; import { UndoManager, undoBatch } from '../../util/UndoManager'; import { LoadingBox } from '../nodes/LoadingBox'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { CollectionView, CollectionViewProps } from './CollectionView'; export interface SubCollectionViewProps extends CollectionViewProps { isAnyChildContentActive: () => boolean; } export function CollectionSubView(moreProps?: X) { class CollectionSubView extends ViewBoxBaseComponent() { private dropDisposer?: DragManager.DragDropDisposer; private gestureDisposer?: GestureUtils.GestureEventDisposer; protected _mainCont?: HTMLDivElement; constructor(props: any) { 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.resolvedDataDoc ? this.Document : Doc.GetProto(this.Document); // if the layout document has a resolvedDataDoc, 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. } @computed get childLayoutPairs(): { layout: Doc; data: Doc }[] { const { Document, TemplateDataDocument } = this._props; const validPairs = this.childDocs .map(doc => Doc.GetLayoutDataDocPair(Document, !this._props.isAnnotationOverlay ? TemplateDataDocument : undefined, doc)) .filter(pair => { // filter out any documents that have a proto that we don't have permissions to return !pair.layout?.hidden && pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate)); }); return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types } @computed get childDocList() { return Cast(this.dataField, listSpec(Doc)); } collectionFilters = () => this._focusFilters ?? StrListCast(this.Document._childFilters); collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.Document._childFiltersByRanges, listSpec('string'), []); // child filters apply to the descendants of the documents in this collection childDocFilters = () => [...(this._props.childFilters?.().filter(f => Utils.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 => !Utils.IsRecursiveFilter(f)) || [])]; childDocRangeFilters = () => [...(this._props.childFiltersByRanges?.() || []), ...this.collectionRangeDocFilters()]; searchFilterDocs = () => this._props.searchFilterDocs?.() ?? DocListCast(this.Document._searchFilterDocs); @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 = Cast(this.dataField, listSpec(Doc), null); } 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 = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this._props.ignoreUnrendered || !d.layout_unrendered)).map(d => d as Doc); 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(Utils.noDragDocsFilter)); if (dragged && SnappingManager.CanEmbed && DragManager.docsBeingDragged.includes(d)) return false; 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.LayoutFieldKey(d); const annos = !Field.toString(Doc.LayoutField(d) as Field).includes(CollectionView.name); const data = d[annos ? fieldKey + '_annotations' : fieldKey]; const side = annos && d[fieldKey + '_sidebar']; if (data !== undefined || side !== undefined) { let subDocs = [...DocListCast(data), ...DocListCast(side)]; if (subDocs.length > 0) { let newarray: Doc[] = []; notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, childDocFilters, childFiltersByRanges, d).length); while (subDocs.length > 0 && !notFiltered) { newarray = []; subDocs.forEach(t => { const fieldKey = Doc.LayoutFieldKey(t); const annos = !Field.toString(Doc.LayoutField(t) as Field).includes(CollectionView.name); notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !childFiltersByRanges.length) || DocUtils.FilterDocs([t], childDocFilters, childFiltersByRanges, d).length)); DocListCast(t[annos ? fieldKey + '_annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc)); annos && DocListCast(t[fieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc)); }); subDocs = newarray; } } } } notFiltered && docsforFilter.push(d); }); return docsforFilter; } @action protected async setCursorPosition(position: [number, number]) { let ind; const doc = this.Document; const id = Doc.UserDoc()[Id]; const email = Doc.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); } } } @undoBatch 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._props.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, annotationKey?: string) => this._props.moveDocument?.(doc, targetCollection, addDocument) || false; protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean { const docDragData = de.complete.docDragData; if (docDragData) { let added = undefined; 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 ? true : false; } else if (de.complete.annoDragData) { 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; } @undoBatch 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 (FormattedTextBox.IsFragment(html)) { const href = FormattedTextBox.GetHref(html); if (href) { const docId = FormattedTextBox.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] })).lastElement(); const newImgSrc = result.accessPaths.agnostic.client.indexOf('dashblobstore') === -1 // ? Utils.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; } else { const path = window.location.origin + '/doc/'; if (text.startsWith(path)) { const docId = text.replace(Doc.globalServerPath(), '').split('?')[0]; 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 { const srcWeb = SelectionManager.Views.lastElement(); const srcUrl = (srcWeb?.Document.data as WebField)?.url?.href?.match(/https?:\/\/[^/]*/)?.[0]; const reg = new RegExp(Utils.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 = SelectionManager.Views[0].ContentDiv?.getElementsByTagName('iframe')?.[0]; const focusNode = iframe?.contentDocument?.getSelection()?.focusNode as any; 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')) { const stringContents = await new Promise(resolve => item.getAsString(resolve)); const type = (await rp.head(Utils.CorsProxy(stringContents)))['content-type']; if (type) { const doc = await DocUtils.DocumentFromType(type, Utils.CorsProxy(stringContents), options); doc && generatedDocuments.push(doc); } } if (item.kind === 'file') { const file = item.getAsFile(); file?.type && files.push(file); file?.type === 'application/json' && Utils.readUploadedFileAsText(file).then(result => { const json = JSON.parse(result as string); addDocument( Docs.Create.TreeDocument( json['rectangular-puzzle'].crossword.clues[0].clue.map((c: any) => { const label = Docs.Create.LabelDocument({ title: c['#text'], _width: 120, _height: 20 }); const proto = Doc.GetProto(label); proto._width = 120; proto._height = 20; return proto; }), { _width: 150, _height: 600, title: 'across', backgroundColor: 'white', _createDocOnCR: true } ) ); }); } } 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 = undefined; if (typeof files === 'string') { const loading = Docs.Create.LoadingDocument(files, options); generatedDocuments.push(loading); LoadingBox.addCurrentlyLoading(loading); DocUtils.uploadYoutubeVideoLoading(files, {}, loading); } else { generatedDocuments.push( ...files.map(file => { const loading = Docs.Create.LoadingDocument(file, options); LoadingBox.addCurrentlyLoading(loading); DocUtils.uploadFileToDoc(file, {}, loading); 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.'); } } }; } return CollectionSubView; }