From ecd6d50cf6dd89f7d860096037efee81230eb535 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 7 Jun 2019 19:42:52 -0400
Subject: from last

---
 .../views/collections/CollectionTreeView.tsx       | 188 +++++++++++++++------
 1 file changed, 137 insertions(+), 51 deletions(-)

(limited to 'src/client/views/collections/CollectionTreeView.tsx')

diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 9fb1da716..8dc10bcd1 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -6,7 +6,7 @@ import { observer } from "mobx-react";
 import { Doc, DocListCast } from '../../../new_fields/Doc';
 import { Id } from '../../../new_fields/FieldSymbols';
 import { Document, listSpec } from '../../../new_fields/Schema';
-import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types';
+import { BoolCast, Cast, NumCast, StrCast, PromiseValue } from '../../../new_fields/Types';
 import { Docs } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager";
@@ -19,6 +19,7 @@ import { CollectionDockingView } from './CollectionDockingView';
 import { CollectionSubView } from "./CollectionSubView";
 import "./CollectionTreeView.scss";
 import React = require("react");
+import { Transform } from '../../util/Transform';
 
 
 export interface TreeViewProps {
@@ -27,6 +28,10 @@ export interface TreeViewProps {
     moveDocument: DragManager.MoveFunction;
     dropAction: "alias" | "copy" | undefined;
     addDocTab: (doc: Doc, where: string) => void;
+    addDocument: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean;
+    ScreenToLocalTransform: () => Transform;
+    treeViewId: string;
+    parentKey: string;
 }
 
 export enum BulletType {
@@ -45,23 +50,49 @@ library.add(faCaretRight);
  * Component that takes in a document prop and a boolean whether it's collapsed or not.
  */
 class TreeView extends React.Component<TreeViewProps> {
+    private _header?: React.RefObject<HTMLDivElement> = React.createRef();
+    private treedropDisposer?: DragManager.DragDropDisposer;
+    protected createTreeDropTarget = (ele: HTMLDivElement) => {
+        this.treedropDisposer && this.treedropDisposer();
+        if (ele) {
+            this.treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.treeDrop.bind(this) } });
+        }
+    }
 
+    @observable _isOver: boolean = false;
     @observable _collapsed: boolean = true;
 
     @undoBatch delete = () => this.props.deleteDoc(this.props.document);
+    @undoBatch openRight = async () => this.props.addDocTab(this.props.document, "openRight");
+
+    @action onMouseEnter = () => { this._isOver = true; }
+    @action onMouseLeave = () => { this._isOver = false; }
 
-    @undoBatch openRight = async () => {
-        if (this.props.document.dockingConfig) {
-            MainView.Instance.openWorkspace(this.props.document);
-        } else {
-            this.props.addDocTab(this.props.document, "openRight");
+    onPointerEnter = (e: React.PointerEvent): void => {
+        this.props.document.libraryBrush = true;
+        if (e.buttons === 1) {
+            this._header!.current!.className = "treeViewItem-header";
+            document.addEventListener("pointermove", this.onDragMove, true);
         }
     }
-
-    get children() {
-        return Cast(this.props.document.data, listSpec(Doc), []); // bcz: needed?    .filter(doc => FieldValue(doc));
+    onPointerLeave = (e: React.PointerEvent): void => {
+        this.props.document.libraryBrush = false;
+        this._header!.current!.className = "treeViewItem-header";
+        document.removeEventListener("pointermove", this.onDragMove, true);
+    }
+    onDragMove = (e: PointerEvent): void => {
+        this.props.document.libraryBrush = false;
+        let x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY);
+        let rect = this._header!.current!.getBoundingClientRect();
+        let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
+        let before = x[1] < bounds[1];
+        let inside = x[0] > bounds[0] + 75 || (!before && this._bulletType === BulletType.Collapsible);
+        this._header!.current!.className = "treeViewItem-header"
+        if (inside && this._bulletType != BulletType.List) this._header!.current!.className = "treeViewItem-header-inside";
+        else if (before) this._header!.current!.className = "treeViewItem-header-above";
+        else if (!before) this._header!.current!.className = "treeViewItem-header-below";
+        e.stopPropagation();
     }
-
     onPointerDown = (e: React.PointerEvent) => {
         e.stopPropagation();
     }
@@ -69,19 +100,17 @@ class TreeView extends React.Component<TreeViewProps> {
     @action
     remove = (document: Document, key: string) => {
         let children = Cast(this.props.document[key], listSpec(Doc), []);
-        if (children) {
-            children.splice(children.indexOf(document), 1);
-        }
+        children.indexOf(document) !== -1 && children.splice(children.indexOf(document), 1);
     }
 
     @action
-    move: DragManager.MoveFunction = (document, target, addDoc) => {
-        if (this.props.document === target) {
-            return true;
+    move: DragManager.MoveFunction = (document: Doc, target: Doc, addDoc) => {
+        if (this.props.document !== target) {
+            //TODO This should check if it was removed
+            this.props.deleteDoc(document);
+            return addDoc(document);
         }
-        //TODO This should check if it was removed
-        this.remove(document, "data");
-        return addDoc(document);
+        return true;
     }
 
     renderBullet(type: BulletType) {
@@ -93,22 +122,12 @@ class TreeView extends React.Component<TreeViewProps> {
         }
         return <div className="bullet" onClick={onClicked}>{bullet ? <FontAwesomeIcon icon={bullet} /> : ""} </div>;
     }
-
-    @action
-    onMouseEnter = () => {
-        this._isOver = true;
-    }
-    @observable _isOver: boolean = false;
-    @action
-    onMouseLeave = () => {
-        this._isOver = false;
-    }
     /**
      * Renders the EditableView title element for placement into the tree.
      */
     renderTitle() {
         let reference = React.createRef<HTMLDivElement>();
-        let onItemDown = SetupDrag(reference, () => this.props.document, this.props.moveDocument, this.props.dropAction);
+        let onItemDown = SetupDrag(reference, () => this.props.document, this.move, this.props.dropAction, this.props.treeViewId, true);
         let editableView = (titleString: string) =>
             (<EditableView
                 oneLine={!this._isOver ? true : false}
@@ -129,9 +148,9 @@ class TreeView extends React.Component<TreeViewProps> {
                 {/* <FontAwesomeIcon icon="angle-right" size="lg" /> */}
             </div>);
         return (
-            <div className="docContainer" ref={reference} onPointerDown={onItemDown} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}
+            <div className="docContainer" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}
                 style={{ background: BoolCast(this.props.document.protoBrush, false) ? "#06123232" : BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0" }}
-                onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
+            >
                 {editableView(StrCast(this.props.document.title))}
                 {openRight}
                 {/* {<div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div>} */}
@@ -152,14 +171,51 @@ class TreeView extends React.Component<TreeViewProps> {
             } else {
                 ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.props.document)) });
             }
-            ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
+            ContextMenu.Instance.displayMenu(e.pageX - 156, e.pageY - 15);
+            e.stopPropagation();
+        }
+    }
+    treeDrop = (e: Event, de: DragManager.DropEvent) => {
+        let x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
+        let rect = this._header!.current!.getBoundingClientRect();
+        let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
+        let before = x[1] < bounds[1];
+        let inside = x[0] > bounds[0] + 75 || (!before && this._bulletType === BulletType.Collapsible);
+        if (de.data instanceof DragManager.DocumentDragData) {
+            let addDoc = (doc: Doc) => this.props.addDocument(doc, this.props.document, before);
+            if (inside) {
+                let docList = Cast(this.props.document.data, listSpec(Doc));
+                if (docList !== undefined) {
+                    addDoc = (doc: Doc) => { docList && docList.push(doc); return true; }
+                }
+            }
+            let added = false;
+            if (de.data.dropAction || de.data.userDropAction) {
+                added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.props.document, before) || added, false);
+            } else if (de.data.moveDocument) {
+                let movedDocs = de.data.options === this.props.treeViewId ? de.data.draggedDocuments : de.data.droppedDocuments;
+                added = movedDocs.reduce((added: boolean, d) =>
+                    de.data.moveDocument(d, this.props.document, addDoc) || added, false);
+            } else {
+                added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.props.document, before), false);
+            }
             e.stopPropagation();
+            return added;
         }
+        return false;
     }
 
-    onPointerEnter = (e: React.PointerEvent): void => { this.props.document.libraryBrush = true; };
-    onPointerLeave = (e: React.PointerEvent): void => { this.props.document.libraryBrush = false; };
+    public static AddDocToList(target: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean) {
+        let list = Cast(target[key], listSpec(Doc));
+        if (list) {
+            let ind = relativeTo ? list.indexOf(relativeTo) : -1;
+            if (ind === -1) list.push(doc);
+            else list.splice(before ? ind : ind + 1, 0, doc);
+        }
+        return true;
+    }
 
+    _bulletType: BulletType = BulletType.List;
     render() {
         let bulletType = BulletType.List;
         let contentElement: (JSX.Element | null)[] = [];
@@ -170,16 +226,17 @@ class TreeView extends React.Component<TreeViewProps> {
         }
         keys.map(key => {
             let docList = DocListCast(this.props.document[key]);
+            let remDoc = (doc: Doc) => this.remove(doc, key);
+            let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => TreeView.AddDocToList(this.props.document, key, doc, addBefore, before);
             let doc = Cast(this.props.document[key], Doc);
             if (doc instanceof Doc || docList.length) {
                 if (!this._collapsed) {
                     bulletType = BulletType.Collapsible;
-                    let spacing = (key === "data") ? 0 : -10;
                     contentElement.push(<ul key={key + "more"}>
                         {(key === "data") ? (null) :
                             <span className="collectionTreeView-keyHeader" style={{ display: "block", marginTop: "7px" }} key={key}>{key}</span>}
-                        <div style={{ display: "block", marginTop: `${spacing}px` }}>
-                            {TreeView.GetChildElements(doc instanceof Doc ? [doc] : docList, key !== "data", (doc: Doc) => this.remove(doc, key), this.move, this.props.dropAction, this.props.addDocTab)}
+                        <div style={{ display: "block" }}>
+                            {TreeView.GetChildElements(doc instanceof Doc ? [doc] : docList, this.props.treeViewId, key, addDoc, remDoc, this.move, this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform)}
                         </div>
                     </ul >);
                 } else {
@@ -187,29 +244,52 @@ class TreeView extends React.Component<TreeViewProps> {
                 }
             }
         });
+        this._bulletType = bulletType;
         return <div className="treeViewItem-container"
+            ref={this.createTreeDropTarget}
             onContextMenu={this.onWorkspaceContextMenu}>
             <li className="collection-child">
-                {this.renderBullet(bulletType)}
-                {this.renderTitle()}
+                <div className="treeViewItem-header" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
+                    {this.renderBullet(bulletType)}
+                    {this.renderTitle()}
+                </div>
                 {contentElement}
             </li>
         </div>;
     }
-    public static GetChildElements(docs: Doc[], allowMinimized: boolean, remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType, addDocTab: (doc: Doc, where: string) => void) {
-        return docs.filter(child => !child.excludeFromLibrary && (allowMinimized || !child.isMinimized)).map(child =>
-            <TreeView document={child} key={child[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} addDocTab={addDocTab} />);
+    public static GetChildElements(
+        docs: Doc[],
+        treeViewId: string,
+        key: string,
+        add: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean,
+        remove: ((doc: Doc) => void),
+        move: DragManager.MoveFunction,
+        dropAction: dropActionType,
+        addDocTab: (doc: Doc, where: string) => void,
+        screenToLocalXf: () => Transform
+    ) {
+        return docs.filter(child => !child.excludeFromLibrary && (key !== "data" || !child.isMinimized)).map(child =>
+            <TreeView document={child} treeViewId={treeViewId} key={child[Id]} deleteDoc={remove} addDocument={add} moveDocument={move}
+                dropAction={dropAction} addDocTab={addDocTab} ScreenToLocalTransform={screenToLocalXf} parentKey={key} />);
     }
 }
 
 @observer
 export class CollectionTreeView extends CollectionSubView(Document) {
+    private treedropDisposer?: DragManager.DragDropDisposer;
+    protected createTreeDropTarget = (ele: HTMLDivElement) => {
+        if (this.treedropDisposer) {
+            this.treedropDisposer();
+        }
+        if (ele) {
+            this.treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+        }
+    }
+
     @action
     remove = (document: Document) => {
-        let children = Cast(this.props.Document.data, listSpec(Doc), []);
-        if (children) {
-            children.splice(children.indexOf(document), 1);
-        }
+        let children = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []);
+        children.indexOf(document) !== -1 && children.splice(children.indexOf(document), 1);
     }
     onContextMenu = (e: React.MouseEvent): void => {
         // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
@@ -218,20 +298,26 @@ export class CollectionTreeView extends CollectionSubView(Document) {
             ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.remove(this.props.Document)) });
         }
     }
+
+    onTreeDrop = (e: React.DragEvent) => {
+        this.onDrop(e, {});
+    }
     render() {
-        trace();
-        let dropAction = StrCast(this.props.Document.dropAction, "alias") as dropActionType;
+        let dropAction = Cast(this.props.Document.dropAction, "string") as dropActionType;
         if (!this.childDocs) {
             return (null);
         }
-        let childElements = TreeView.GetChildElements(this.childDocs, false, this.remove, this.props.moveDocument, dropAction, this.props.addDocTab);
+        let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => TreeView.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before);
+        let moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc);
+        let childElements = TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.fieldKey, addDoc, this.remove, moveDoc, dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform);
 
         return (
             <div id="body" className="collectionTreeView-dropTarget"
                 style={{ borderRadius: "inherit" }}
                 onContextMenu={this.onContextMenu}
                 onWheel={(e: React.WheelEvent) => this.props.isSelected() && e.stopPropagation()}
-                onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}>
+                onDrop={this.onTreeDrop}
+                ref={this.createTreeDropTarget}>
                 <div className="coll-title">
                     <EditableView
                         contents={this.props.Document.title}
-- 
cgit v1.2.3-70-g09d2

