import { action, computed, observable } from 'mobx';
import ReactLoading from 'react-loading';
import * as rp from 'request-promise';
import CursorField from '../../../fields/CursorField';
import { AclPrivate, Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
import { Cast, ScriptCast, StrCast } from '../../../fields/Types';
import { WebField } from '../../../fields/URLField';
import { GestureUtils } from '../../../pen-gestures/GestureUtils';
import { returnFalse, Utils } from '../../../Utils';
import { DocServer } from '../../DocServer';
import { Networking } from '../../Network';
import { ImageUtils } from '../../util/Import & Export/ImageUtils';
import { InteractionUtils } from '../../util/InteractionUtils';
import { undoBatch, UndoManager } from '../../util/UndoManager';
import { DocComponent } from '../DocComponent';
import React = require('react');

export interface SubCollectionViewProps extends CollectionViewProps {
    CollectionView: Opt<CollectionView>;
    isAnyChildContentActive: () => boolean;
}

export function CollectionSubView<X>(moreProps?: X) {
    class CollectionSubView extends DocComponent<X & SubCollectionViewProps>() {
        private dropDisposer?: DragManager.DragDropDisposer;
        private gestureDisposer?: GestureUtils.GestureEventDisposer;
        protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
        protected _mainCont?: HTMLDivElement;
        @observable _focusFilters: Opt<string[]>; // docFilters that are overridden when previewing a link to an anchor which has docFilters set on it
        @observable _focusRangeFilters: Opt<string[]>; // docRangeFilters that are overridden when previewing a link to an anchor which has docRangeFilters set on it
        protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
            //used for stacking and masonry view
            this.dropDisposer?.();
            this.gestureDisposer?.();
            this._multiTouchDisposer?.();
            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));
                this._multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(ele, this.onTouchStart.bind(this));
            }
        };
        protected CreateDropTarget(ele: HTMLDivElement) {
            //used in schema view
            this.createDashEventsTarget(ele);
        }

        componentWillUnmount() {
            this.gestureDisposer?.();
            this._multiTouchDisposer?.();
        }

        @computed get dataDoc() {
            return this.props.DataDoc instanceof Doc && this.props.Document.isTemplateForField ? Doc.GetProto(this.props.DataDoc) : this.props.Document.resolvedDataDoc ? this.props.Document : Doc.GetProto(this.props.Document); // if the layout document has a resolvedDataDoc, then we don't want to get its parent which would be the unexpanded template
        }

        rootSelected = (outsideReaction?: boolean) => {
            return this.props.isSelected(outsideReaction) || (this.rootDoc && this.props.rootSelected(outsideReaction));
        };

        // The data field for rendering this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc.
        // When a document has a DataDoc but it's not a template, then it contains its own rendering data, but needs to pass the DataDoc 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() {
            if (this.layoutDoc[this.props.fieldKey]) return this.layoutDoc[this.props.fieldKey];
            // sets the dataDoc's data field to an empty list if the data field is undefined - prevents issues with addonly
            // setTimeout changes it outside of the @computed section
            !this.dataDoc[this.props.fieldKey] && setTimeout(() => !this.dataDoc[this.props.fieldKey] && (this.dataDoc[this.props.fieldKey] = new List<Doc>()));
            return this.dataDoc[this.props.fieldKey];
        }

        get childLayoutPairs(): { layout: Doc; data: Doc }[] {
            const { Document, DataDoc } = this.props;
            const validPairs = this.childDocs
                .map(doc => Doc.GetLayoutDataDocPair(Document, !this.props.isAnnotationOverlay ? DataDoc : undefined, doc))
                .filter(pair => {
                    // filter out any documents that have a proto that we don't have permissions to
                    return 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
        }
        get childDocList() {
            return Cast(this.dataField, listSpec(Doc));
        }
        collectionFilters = () => this._focusFilters ?? StrListCast(this.props.Document._docFilters);
        collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.props.Document._docRangeFilters, listSpec('string'), []);
        // child filters apply to the descendants of the documents in this collection
        childDocFilters = () => [...(this.props.docFilters?.().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.docFilters?.().filter(f => !Utils.IsRecursiveFilter(f)) || [])];
        childDocRangeFilters = () => [...(this.props.docRangeFilters?.() || []), ...this.collectionRangeDocFilters()];
        searchFilterDocs = () => this.props.searchFilterDocs?.() ?? DocListCast(this.props.Document._searchFilterDocs);
        @computed.struct get childDocs() {
            TraceMobx();
            let rawdocs: (Doc | Promise<Doc>)[] = [];
            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 {
                // 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 rootDoc = Cast(this.props.Document.rootDocument, Doc, null);
                rawdocs = rootDoc && !this.props.isAnnotationOverlay ? [Doc.GetProto(rootDoc)] : [];
            }

            const docs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this.props.ignoreUnrendered || !d.unrendered)).map(d => d as Doc);
            const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
            const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;

            const childDocFilters = this.childDocFilters();
            const docRangeFilters = this.childDocRangeFilters();
            const searchDocs = this.searchFilterDocs();
            if (this.props.Document.dontRegisterView || (!childDocFilters.length && !this.unrecursiveDocFilters().length && !docRangeFilters.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.docFilters?.().some(f => f.includes(Utils.noDragsDocFilter));
                if (dragged && DragManager.docsBeingDragged.includes(d)) return false;
                let notFiltered = d.z || Doc.IsSystem(d) || DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), docRangeFilters, viewSpecScript, this.props.Document).length > 0;
                if (notFiltered) {
                    notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, docRangeFilters, viewSpecScript, this.props.Document).length > 0;
                    const fieldKey = Doc.LayoutFieldKey(d);
                    const annos = !Field.toString(Doc.LayoutField(d) as Field).includes('CollectionView');
                    const data = d[annos ? fieldKey + '-annotations' : fieldKey];
                    if (data !== undefined) {
                        let subDocs = DocListCast(data);
                        if (subDocs.length > 0) {
                            let newarray: Doc[] = [];
                            notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, childDocFilters, docRangeFilters, viewSpecScript, 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');
                                    notFiltered =
                                        notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !docRangeFilters.length) || DocUtils.FilterDocs([t], childDocFilters, docRangeFilters, viewSpecScript, d).length));
                                    DocListCast(t[annos ? fieldKey + '-annotations' : fieldKey]).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.props.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<CursorField>();
                }
                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, targetAction: dropActionType) {
            if (de.complete.docDragData) {
                // if targetDropAction is, say 'alias', but we're just dragging within a collection, we want to ignore the targetAction.
                // otherwise, the targetAction should become the actual action (which can still be overridden by the userDropAction -eg, shift/ctrl keys)
                if (targetAction && !de.complete.docDragData.draggedDocuments.some(d => d.context === this.props.Document && this.childDocs.includes(d))) {
                    de.complete.docDragData.dropAction = targetAction;
                }
                e.stopPropagation();
            }
        }

        addDocument = (doc: Doc | Doc[]) => this.props.addDocument?.(doc) || false;

        @action
        protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean {
            const docDragData = de.complete.docDragData;
            if (docDragData) {
                let added = false;
                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 === 'same' || dropAction === '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 = this.props.Document._viewType === CollectionViewType.Pile || de.embedKey || this.props.Document.allowOverlayDrop || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document);
                        added = docDragData.moveDocument(movedDocs, this.props.Document, canAdd ? this.addDocument : returnFalse);
                    } else {
                        ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData });
                        added = addedDocs.length ? this.addDocument(addedDocs) : true;
                    }
                    added && e.stopPropagation();
                    return added;
                } else {
                    ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData });
                    added = this.addDocument(docDragData.droppedDocuments);
                }
                !added && alert('You cannot perform this move');
                e.stopPropagation();
                return added;
            } else if (de.complete.annoDragData) {
                const dropCreator = de.complete.annoDragData.dropDocCreator;
                de.complete.annoDragData.dropDocCreator = () => {
                    const dropped = dropCreator(this.props.isAnnotationOverlay ? this.rootDoc : undefined);
                    this.addDocument(dropped);
                    return dropped;
                };
                return true;
            }
            return false;
        }

        @undoBatch
        @action
        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('<div')) {
                return;
            }

            e.stopPropagation();
            e.preventDefault();

            const addDocument = (doc: Doc | Doc[]) => 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, _showTitle: StrCast(Doc.UserDoc().showTitle), _width: 100, _height: 25 }));
                    }
                    return;
                }
                if (!html.startsWith('<a')) {
                    const tags = html.split('<');
                    if (tags[0] === '') tags.splice(0, 1);
                    let img = tags[0].startsWith('img') ? tags[0] : tags.length > 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 split = img.split('src="')[1].split('"')[0];
                        let source = split;
                        if (split.startsWith('data:image') && split.includes('base64')) {
                            const [{ accessPaths }] = await Networking.PostToServer('/uploadRemoteImage', { sources: [split] });
                            source = Utils.prepend(accessPaths.agnostic.client);
                        }
                        if (source.startsWith('http')) {
                            const doc = Docs.Create.ImageDocument(source, { ...options, _width: 300 });
                            ImageUtils.ExtractExif(doc);
                            addDocument(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({ doc: htmlDoc }, { doc: anchor });
                                }
                            }
                        }
                        return;
                    }
                }
            }

            if (uriList || text) {
                if ((uriList || text).includes('www.youtube.com/watch') || text.includes('www.youtube.com/embed')) {
                    const batch = UndoManager.StartBatch('youtube upload');
                    const generatedDocuments: Doc[] = [];
                    this.slowLoadDocuments((uriList || text).split('v=')[1].split('&')[0], options, generatedDocuments, text, completed, e.clientX, e.clientY, 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) {
                // const existingWebDoc = await Hypothesis.findWebDoc(uriList);
                // if (existingWebDoc) {
                //     const alias = Doc.MakeAlias(existingWebDoc);
                //     alias.x = options.x;
                //     alias.y = options.y;
                //     alias._nativeWidth = 850;
                //     alias._height = 512;
                //     alias._width = 400;
                //     addDocument(alias);
                // } else
                {
                    const newDoc = 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,
                        useCors: true,
                    });
                    addDocument(newDoc);
                }
                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<string>(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', _singleLine: true }
                                )
                            );
                        });
                }
            }
            this.slowLoadDocuments(files, options, generatedDocuments, text, completed, e.clientX, e.clientY, addDocument).then(batch.end);
        }

        slowLoadDocuments = async (
            files: File[] | string,
            options: DocumentOptions,
            generatedDocuments: Doc[],
            text: string,
            completed: ((doc: Doc[]) => void) | undefined,
            clientX: number,
            clientY: number,
            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);
                Doc.addCurrentlyLoading(loading);
                DocUtils.uploadYoutubeVideoLoading(files, {}, loading);
            } else {
                generatedDocuments.push(
                    ...files.map(file => {
                        const loading = Docs.Create.LoadingDocument(file, options);
                        Doc.addCurrentlyLoading(loading);
                        DocUtils.uploadFileToDoc(file, {}, loading);
                        return loading;
                    })
                );
            }
            if (generatedDocuments.length) {
                // Creating a dash document
                const isFreeformView = this.props.Document._viewType === 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;
}

import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
import { Docs, DocumentOptions, DocUtils } from '../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
import { DragManager, dropActionType } from '../../util/DragManager';
import { SelectionManager } from '../../util/SelectionManager';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { CollectionView, CollectionViewProps } from './CollectionView';
