diff options
Diffstat (limited to 'src/client/views/collections/CollectionTreeView.tsx')
-rw-r--r-- | src/client/views/collections/CollectionTreeView.tsx | 621 |
1 files changed, 466 insertions, 155 deletions
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index c80bd8fce..c212cc97c 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,251 +1,562 @@ -import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faCaretDown, faCaretRight, faTrashAlt, faAngleRight } from '@fortawesome/free-solid-svg-icons'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faAngleRight, faCamera, faExpand, faTrash, faBell, faCaretDown, faCaretRight, faCaretSquareDown, faCaretSquareRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, observable, trace } from "mobx"; +import { action, computed, observable, trace, untracked } from "mobx"; import { observer } from "mobx-react"; -import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager"; -import { EditableView } from "../EditableView"; -import { CollectionSubView } from "./CollectionSubView"; -import "./CollectionTreeView.scss"; -import React = require("react"); -import { Document, listSpec } from '../../../new_fields/Schema'; -import { Cast, StrCast, BoolCast, FieldValue, NumCast } from '../../../new_fields/Types'; -import { Doc, DocListCast } from '../../../new_fields/Doc'; +import { Doc, DocListCast, HeightSym, WidthSym, Opt } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; -import { ContextMenu } from '../ContextMenu'; -import { undoBatch } from '../../util/UndoManager'; -import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; -import { CollectionDockingView } from './CollectionDockingView'; +import { List } from '../../../new_fields/List'; +import { Document, listSpec } from '../../../new_fields/Schema'; +import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types'; +import { emptyFunction, Utils } from '../../../Utils'; +import { Docs, DocUtils, DocumentType } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; -import { Docs } from '../../documents/Documents'; +import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager"; +import { SelectionManager } from '../../util/SelectionManager'; +import { Transform } from '../../util/Transform'; +import { undoBatch } from '../../util/UndoManager'; +import { ContextMenu } from '../ContextMenu'; +import { EditableView } from "../EditableView"; import { MainView } from '../MainView'; +import { Templates } from '../Templates'; import { CollectionViewType } from './CollectionBaseView'; +import { CollectionDockingView } from './CollectionDockingView'; +import { CollectionSchemaPreview } from './CollectionSchemaView'; +import { CollectionSubView } from "./CollectionSubView"; +import "./CollectionTreeView.scss"; +import React = require("react"); +import { LinkManager } from '../../util/LinkManager'; export interface TreeViewProps { document: Doc; - deleteDoc: (doc: Doc) => void; + dataDoc?: Doc; + containingCollection: Doc; + renderDepth: number; + deleteDoc: (doc: Doc) => boolean; moveDocument: DragManager.MoveFunction; dropAction: "alias" | "copy" | undefined; - addDocTab: (doc: Doc, where: string) => void; -} - -export enum BulletType { - Collapsed, - Collapsible, - List + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void; + panelWidth: () => number; + panelHeight: () => number; + addDocument: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean; + indentDocument?: () => void; + ScreenToLocalTransform: () => Transform; + outerXf: () => { translateX: number, translateY: number }; + treeViewId: string; + parentKey: string; + active: () => boolean; } library.add(faTrashAlt); library.add(faAngleRight); +library.add(faBell); +library.add(faTrash); +library.add(faCamera); +library.add(faExpand); library.add(faCaretDown); library.add(faCaretRight); +library.add(faCaretSquareDown); +library.add(faCaretSquareRight); @observer /** * 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; + private _dref = React.createRef<HTMLDivElement>(); + @observable __chosenKey: string = ""; + @computed get _chosenKey() { return this.__chosenKey ? this.__chosenKey : this.fieldKey; } + @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.document.maxEmbedHeight, 300); } @observable _collapsed: boolean = true; - @undoBatch delete = () => this.props.deleteDoc(this.props.document); - - @undoBatch openRight = async () => { - if (this.props.document.dockingConfig) { - MainView.Instance.openWorkspace(this.props.document); - } else { - this.props.addDocTab(this.props.document, "openRight"); + @computed get fieldKey() { + let keys = Array.from(Object.keys(this.resolvedDataDoc)); // bcz: Argh -- make untracked to avoid this rerunning whenever 'libraryBrush' is set + if (this.resolvedDataDoc.proto instanceof Doc) { + let arr = Array.from(Object.keys(this.resolvedDataDoc.proto));// bcz: Argh -- make untracked to avoid this rerunning whenever 'libraryBrush' is set + keys.push(...arr); + while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1); + } + let keyList: string[] = []; + keys.map(key => { + let docList = Cast(this.resolvedDataDoc[key], listSpec(Doc)); + if (docList && docList.length > 0) { + keyList.push(key); + } + }); + let layout = StrCast(this.props.document.layout); + if (layout.indexOf("fieldKey={\"") !== -1) { + return layout.split("fieldKey={\"")[1].split("\"")[0]; } + return keyList.length ? keyList[0] : "data"; } - get children() { - return Cast(this.props.document.data, listSpec(Doc), []); // bcz: needed? .filter(doc => FieldValue(doc)); + @computed get resolvedDataDoc() { return BoolCast(this.props.document.isTemplate) && this.props.dataDoc ? this.props.dataDoc : this.props.document; } + + protected createTreeDropTarget = (ele: HTMLDivElement) => { + this._treedropDisposer && this._treedropDisposer(); + if (ele) { + this._treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.treeDrop.bind(this) } }); + } } - onPointerDown = (e: React.PointerEvent) => { + @undoBatch delete = () => this.props.deleteDoc(this.resolvedDataDoc); + @undoBatch openRight = async () => this.props.addDocTab(this.props.document, undefined, "onRight"); + + onPointerDown = (e: React.PointerEvent) => e.stopPropagation(); + onPointerEnter = (e: React.PointerEvent): void => { + this.props.active() && (this.props.document.libraryBrush = true); + if (e.buttons === 1 && SelectionManager.GetIsDragging()) { + this._header!.current!.className = "treeViewItem-header"; + document.addEventListener("pointermove", this.onDragMove, true); + } + } + onPointerLeave = (e: React.PointerEvent): void => { + this.props.document.libraryBrush = undefined; + this._header!.current!.className = "treeViewItem-header"; + document.removeEventListener("pointermove", this.onDragMove, true); + } + onDragMove = (e: PointerEvent): void => { + this.props.document.libraryBrush = undefined; + 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._collapsed); + this._header!.current!.className = "treeViewItem-header"; + if (inside) 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(); } @action - remove = (document: Document, key: string) => { - let children = Cast(this.props.document[key], listSpec(Doc), []); - if (children) { + remove = (document: Document, key: string): boolean => { + let children = Cast(this.resolvedDataDoc[key], listSpec(Doc), []); + if (children.indexOf(document) !== -1) { children.splice(children.indexOf(document), 1); + return true; } + return false; } @action - move: DragManager.MoveFunction = (document, target, addDoc) => { - if (this.props.document === target) { - return true; - } - //TODO This should check if it was removed - this.remove(document, "data"); - return addDoc(document); + move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => { + return this.props.document !== target && this.props.deleteDoc(doc) && addDoc(doc); } + @action + indent = () => this.props.addDocument(this.props.document) && this.delete() - renderBullet(type: BulletType) { - let onClicked = action(() => this._collapsed = !this._collapsed); - let bullet: IconProp | undefined = undefined; - switch (type) { - case BulletType.Collapsed: bullet = "caret-right"; break; - case BulletType.Collapsible: bullet = "caret-down"; break; - } - return <div className="bullet" onClick={onClicked}>{bullet ? <FontAwesomeIcon icon={bullet} /> : ""} </div>; + renderBullet() { + let docList = Cast(this.resolvedDataDoc[this.fieldKey], listSpec(Doc)); + let doc = Cast(this.resolvedDataDoc[this.fieldKey], Doc); + let isDoc = doc instanceof Doc || docList; + let c; + return <div className="bullet" onClick={action(() => this._collapsed = !this._collapsed)} style={{ color: StrCast(this.props.document.color, "black"), opacity: 0.4 }}> + {<FontAwesomeIcon icon={this._collapsed ? (isDoc ? "caret-square-right" : "caret-right") : (isDoc ? "caret-square-down" : "caret-down")} />} + </div>; } - @action - onMouseEnter = () => { - this._isOver = true; - } - @observable _isOver: boolean = false; - @action - onMouseLeave = () => { - this._isOver = false; + static loadId = ""; + editableView = (key: string, style?: string) => (<EditableView + oneLine={true} + display={"inline"} + editing={this.resolvedDataDoc[Id] === TreeView.loadId} + contents={StrCast(this.props.document[key])} + height={36} + fontStyle={style} + fontSize={12} + GetValue={() => StrCast(this.props.document[key])} + SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc)[key] = value) ? true : true} + OnFillDown={(value: string) => { + Doc.GetProto(this.resolvedDataDoc)[key] = value; + let doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); + TreeView.loadId = doc[Id]; + return this.props.addDocument(doc); + }} + OnTab={() => this.props.indentDocument && this.props.indentDocument()} + />) + + @computed get keyList() { + let keys = Array.from(Object.keys(this.resolvedDataDoc)); + if (this.resolvedDataDoc.proto instanceof Doc) { + keys.push(...Array.from(Object.keys(this.resolvedDataDoc.proto))); + } + let keyList: string[] = keys.reduce((l, key) => { + let listspec = DocListCast(this.resolvedDataDoc[key]); + if (listspec && listspec.length) return [...l, key]; + return l; + }, [] as string[]); + keys.map(key => Cast(this.resolvedDataDoc[key], Doc) instanceof Doc && keyList.push(key)); + if (LinkManager.Instance.getAllRelatedLinks(this.props.document).length > 0) keyList.push("links"); + if (keyList.indexOf(this.fieldKey) !== -1) { + keyList.splice(keyList.indexOf(this.fieldKey), 1); + } + keyList.splice(0, 0, this.fieldKey); + return keyList.filter((item, index) => keyList.indexOf(item) >= index); } /** * 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 editableView = (titleString: string) => - (<EditableView - oneLine={!this._isOver ? true : false} - display={"inline-block"} - contents={titleString} - height={36} - GetValue={() => StrCast(this.props.document.title)} - SetValue={(value: string) => { - let target = this.props.document.proto ? this.props.document.proto : this.props.document; - target.title = value; - return true; - }} - />); - let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc), []) : []; - let openRight = dataDocs && dataDocs.indexOf(this.props.document) !== -1 ? (null) : ( + let onItemDown = SetupDrag(reference, () => this.resolvedDataDoc, this.move, this.props.dropAction, this.props.treeViewId, true); + + let headerElements = ( + <span className="collectionTreeView-keyHeader" key={this._chosenKey + "chosen"} + onPointerDown={action(() => { + let ind = this.keyList.indexOf(this._chosenKey); + ind = (ind + 1) % this.keyList.length; + this.__chosenKey = this.keyList[ind]; + })} > + {this._chosenKey} + </span>); + let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document[this.fieldKey], listSpec(Doc), []) : []; + let openRight = dataDocs && dataDocs.indexOf(this.resolvedDataDoc) !== -1 ? (null) : ( <div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}> <FontAwesomeIcon icon="angle-right" size="lg" /> - {/* <FontAwesomeIcon icon="angle-right" size="lg" /> */} </div>); - return ( - <div className="docContainer" 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} + return <> + <div className="docContainer" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown} + style={{ + background: BoolCast(this.props.document.libraryBrush) ? "#06121212" : "0", + outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined, + pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? "all" : "none" + }} + > + {this.editableView("title")} {/* {<div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div>} */} - </div >); + </div > + {headerElements} + {openRight} + </>; } onWorkspaceContextMenu = (e: React.MouseEvent): void => { if (!e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.props.document)) }); - ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.KVPDocument(this.props.document, { width: 300, height: 300 }), "onRight"), icon: "layer-group" }); + ContextMenu.Instance.addItem({ description: (BoolCast(this.props.document.embed) ? "Collapse" : "Expand") + " inline", event: () => this.props.document.embed = !BoolCast(this.props.document.embed), icon: "expand" }); if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking) { - ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, "inTab"), icon: "folder" }); - ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, "onRight"), icon: "caret-square-right" }); - if (DocumentManager.Instance.getDocumentViews(this.props.document).length) { - ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document, false)) }); + ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "inTab"), icon: "folder" }); + ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "onRight"), icon: "caret-square-right" }); + if (DocumentManager.Instance.getDocumentViews(this.resolvedDataDoc).length) { + ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.resolvedDataDoc).map(view => view.props.focus(this.props.document, true)), icon: "camera" }); } - ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)) }); + ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)), icon: "trash-alt" }); } else { - ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.props.document)) }); + ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.resolvedDataDoc)), icon: "caret-square-right" }); + ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.props.document)), icon: "trash-alt" }); } - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); + ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" }); + ContextMenu.Instance.displayMenu(e.pageX > 156 ? e.pageX - 156 : 0, e.pageY - 15); e.stopPropagation(); + e.preventDefault(); } } - onPointerEnter = (e: React.PointerEvent): void => { this.props.document.libraryBrush = true; }; - onPointerLeave = (e: React.PointerEvent): void => { this.props.document.libraryBrush = false; }; - - render() { - let bulletType = BulletType.List; - let contentElement: (JSX.Element | null)[] = []; - let keys = Array.from(Object.keys(this.props.document)); - if (this.props.document.proto instanceof Doc) { - keys.push(...Array.from(Object.keys(this.props.document.proto))); - while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1); + @undoBatch + 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._collapsed); + if (de.data instanceof DragManager.LinkDragData) { + let sourceDoc = de.data.linkSourceDocument; + let destDoc = this.props.document; + DocUtils.MakeLink(sourceDoc, destDoc); + e.stopPropagation(); } - keys.map(key => { - let docList = DocListCast(this.props.document[key]); - 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> - </ul >); - } else { - bulletType = BulletType.Collapsed; + if (de.data instanceof DragManager.DocumentDragData) { + e.stopPropagation(); + if (de.data.draggedDocuments[0] === this.props.document) return true; + let addDoc = (doc: Doc) => this.props.addDocument(doc, this.resolvedDataDoc, before); + if (inside) { + let docList = Cast(this.resolvedDataDoc.data, listSpec(Doc)); + if (docList !== undefined) { + addDoc = (doc: Doc) => { docList && docList.push(doc); return true; }; } } + let movedDocs = (de.data.options === this.props.treeViewId ? de.data.draggedDocuments : de.data.droppedDocuments); + return (de.data.dropAction || de.data.userDropAction) ? + de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before) || added, false) + : (de.data.moveDocument) ? + movedDocs.reduce((added: boolean, d) => de.data.moveDocument(d, this.resolvedDataDoc, addDoc) || added, false) + : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before), false); + } + return false; + } + + docTransform = () => { + let { scale, translateX, translateY } = Utils.GetScreenTransform(this._dref.current!); + let outerXf = this.props.outerXf(); + let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); + let finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1]); + return finalXf; + } + + renderLinks = () => { + let ele: JSX.Element[] = []; + let remDoc = (doc: Doc) => this.remove(doc, this._chosenKey); + let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.props.document, this._chosenKey, doc, addBefore, before); + let groups = LinkManager.Instance.getRelatedGroupedLinks(this.props.document); + groups.forEach((groupLinkDocs, groupType) => { + // let destLinks = groupLinkDocs.map(d => LinkManager.Instance.getOppositeAnchor(d, this.props.document)); + let destLinks: Doc[] = []; + groupLinkDocs.forEach((doc) => { + let opp = LinkManager.Instance.getOppositeAnchor(doc, this.props.document); + if (opp) { + destLinks.push(opp); + } + }); + ele.push( + <div key={"treeviewlink-" + groupType + "subtitle"}> + <div className="collectionTreeView-subtitle">{groupType}:</div> + { + TreeView.GetChildElements(destLinks, this.props.treeViewId, this.props.document, this.props.dataDoc, "treeviewlink-" + groupType, addDoc, remDoc, this.move, + this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth) + } + </div> + ); }); - return <div className="treeViewItem-container" - onContextMenu={this.onWorkspaceContextMenu}> + return ele; + } + + @computed get boundsOfCollectionDocument() { + if (StrCast(this.props.document.type).indexOf(DocumentType.COL) === -1) return undefined; + let layoutDoc = Doc.expandTemplateLayout(this.props.document, this.props.dataDoc); + return Doc.ComputeContentBounds(DocListCast(layoutDoc.data)); + } + docWidth = () => { + let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth); + if (aspect) return Math.min(this.props.document[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 5)); + return NumCast(this.props.document.nativeWidth) ? Math.min(this.props.document[WidthSym](), this.props.panelWidth() - 5) : this.props.panelWidth() - 5; + } + docHeight = () => { + let bounds = this.boundsOfCollectionDocument; + return Math.min(this.MAX_EMBED_HEIGHT, (() => { + let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth); + if (aspect) return this.docWidth() * aspect; + if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x); + return NumCast(this.props.document.height) ? NumCast(this.props.document.height) : 50; + })()); + } + + render() { + let contentElement: (JSX.Element | null) = null; + let docList = Cast(this.resolvedDataDoc[this._chosenKey], listSpec(Doc)); + let remDoc = (doc: Doc) => this.remove(doc, this._chosenKey); + let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.resolvedDataDoc, this._chosenKey, doc, addBefore, before); + let doc = Cast(this.resolvedDataDoc[this._chosenKey], Doc); + + if (!this._collapsed) { + if (!this.props.document.embed) { + contentElement = <ul key={this._chosenKey + "more"}> + {this._chosenKey === "links" ? this.renderLinks() : + TreeView.GetChildElements(doc instanceof Doc ? [doc] : DocListCast(docList), this.props.treeViewId, this.props.document, this.props.dataDoc, this._chosenKey, addDoc, remDoc, this.move, + this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth)} + </ul >; + } else { + let layoutDoc = Doc.expandTemplateLayout(this.props.document, this.props.dataDoc); + contentElement = <div ref={this._dref} style={{ display: "inline-block", height: this.docHeight() }} key={this.props.document[Id] + this.props.document.title}> + <CollectionSchemaPreview + Document={layoutDoc} + DataDocument={this.resolvedDataDoc} + renderDepth={this.props.renderDepth} + fitToBox={this.boundsOfCollectionDocument !== undefined} + width={this.docWidth} + height={this.docHeight} + getTransform={this.docTransform} + CollectionView={undefined} + addDocument={emptyFunction as any} + moveDocument={this.props.moveDocument} + removeDocument={emptyFunction as any} + active={this.props.active} + whenActiveChanged={emptyFunction as any} + addDocTab={this.props.addDocTab} + setPreviewScript={emptyFunction}> + </CollectionSchemaPreview> + </div>; + } + } + return <div className="treeViewItem-container" ref={this.createTreeDropTarget} onContextMenu={this.onWorkspaceContextMenu}> <li className="collection-child"> - {this.renderBullet(bulletType)} - {this.renderTitle()} - {contentElement} + <div className="treeViewItem-header" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> + {this.renderBullet()} + {this.renderTitle()} + </div> + <div className="treeViewItem-border"> + {contentElement} + </div> </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, + containingCollection: Doc, + dataDoc: Doc | undefined, + key: string, + add: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean, + remove: ((doc: Doc) => boolean), + move: DragManager.MoveFunction, + dropAction: dropActionType, + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void, + screenToLocalXf: () => Transform, + outerXf: () => { translateX: number, translateY: number }, + active: () => boolean, + panelWidth: () => number, + renderDepth: number + ) { + let docList = docs.filter(child => !child.excludeFromLibrary); + let rowWidth = () => panelWidth() - 20; + return docList.map((child, i) => { + let indent = i === 0 ? undefined : () => { + if (StrCast(docList[i - 1].layout).indexOf("CollectionView") !== -1) { + let fieldKeysub = StrCast(docList[i - 1].layout).split("fieldKey")[1]; + let fieldKey = fieldKeysub.split("\"")[1]; + Doc.AddDocToList(docList[i - 1], fieldKey, child); + remove(child); + } + }; + let addDocument = (doc: Doc, relativeTo?: Doc, before?: boolean) => { + return add(doc, relativeTo ? relativeTo : docList[i], before !== undefined ? before : false); + }; + let rowHeight = () => { + let aspect = NumCast(child.nativeWidth, 0) / NumCast(child.nativeHeight, 0); + return aspect ? Math.min(child[WidthSym](), rowWidth()) / aspect : child[HeightSym](); + }; + return <TreeView + document={child} + dataDoc={dataDoc} + containingCollection={containingCollection} + treeViewId={treeViewId} + key={child[Id] + "child " + i} + indentDocument={indent} + renderDepth={renderDepth} + deleteDoc={remove} + addDocument={addDocument} + panelWidth={rowWidth} + panelHeight={rowHeight} + moveDocument={move} + dropAction={dropAction} + addDocTab={addDocTab} + ScreenToLocalTransform={screenToLocalXf} + outerXf={outerXf} + parentKey={key} + active={active} />; + }); } } @observer export class CollectionTreeView extends CollectionSubView(Document) { + private treedropDisposer?: DragManager.DragDropDisposer; + private _mainEle?: HTMLDivElement; + + protected createTreeDropTarget = (ele: HTMLDivElement) => { + this.treedropDisposer && this.treedropDisposer(); + if (this._mainEle = ele) { + this.treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + } + } + + componentWillUnmount() { + this.treedropDisposer && this.treedropDisposer(); + } + @action - remove = (document: Document) => { - let children = Cast(this.props.Document.data, listSpec(Doc), []); - if (children) { + remove = (document: Document): boolean => { + let children = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []); + if (children.indexOf(document) !== -1) { children.splice(children.indexOf(document), 1); + return true; } + return false; } 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 - if (!e.isPropagationStopped() && this.props.Document.excludeFromLibrary) { // excludeFromLibrary means this is the user document + if (!e.isPropagationStopped() && this.props.Document.workspaceLibrary) { // excludeFromLibrary means this is the user document ContextMenu.Instance.addItem({ description: "Create Workspace", event: undoBatch(() => MainView.Instance.createNewWorkspace()) }); ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.remove(this.props.Document)) }); + e.stopPropagation(); + e.preventDefault(); + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); } } - render() { - let dropAction = StrCast(this.props.Document.dropAction, "alias") as dropActionType; - if (!this.childDocs) { - return (null); + + @computed get resolvedDataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; } + + outerXf = () => Utils.GetScreenTransform(this._mainEle!); + onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {}); + + + @observable static NotifsCol: Opt<Doc>; + + openNotifsCol = () => { + if (CollectionTreeView.NotifsCol && CollectionDockingView.Instance) { + CollectionDockingView.Instance.AddRightSplit(CollectionTreeView.NotifsCol, undefined); } - let childElements = TreeView.GetChildElements(this.childDocs, false, this.remove, this.props.moveDocument, dropAction, this.props.addDocTab); + } + @computed get notifsButton() { + const length = CollectionTreeView.NotifsCol ? DocListCast(CollectionTreeView.NotifsCol.data).length : 0; + const notifsRef = React.createRef<HTMLDivElement>(); + const dragNotifs = action(() => CollectionTreeView.NotifsCol!); + return <div id="toolbar" key="toolbar"> + <div ref={notifsRef}> + <button className="toolbar-button round-button" title="Notifs" + onClick={this.openNotifsCol} onPointerDown={CollectionTreeView.NotifsCol ? SetupDrag(notifsRef, dragNotifs) : emptyFunction}> + <FontAwesomeIcon icon={faBell} size="sm" /> + </button> + <div className="main-notifs-badge" style={length > 0 ? { "display": "initial" } : { "display": "none" }}> + {length} + </div> + </div> + </div >; + } + @computed get clearButton() { + return <div id="toolbar" key="toolbar"> + <div > + <button className="toolbar-button round-button" title="Notifs" + onClick={undoBatch(action(() => Doc.GetProto(this.props.Document)[this.props.fieldKey] = undefined))}> + <FontAwesomeIcon icon={faTrash} size="sm" /> + </button> + </div> + </div >; + } - return ( + + render() { + let dropAction = StrCast(this.props.Document.dropAction) as dropActionType; + let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.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); + return !this.childDocs ? (null) : ( <div id="body" className="collectionTreeView-dropTarget" - style={{ borderRadius: "inherit" }} + style={{ overflow: "auto", background: StrCast(this.props.Document.backgroundColor, "lightgray") }} onContextMenu={this.onContextMenu} - onWheel={(e: React.WheelEvent) => this.props.isSelected() && e.stopPropagation()} - onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}> - <div className="coll-title"> - <EditableView - contents={this.props.Document.title} - display={"inline"} - height={72} - GetValue={() => StrCast(this.props.Document.title)} - SetValue={(value: string) => { - let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document; - target.title = value; - return true; - }} /> - </div> - <ul className="no-indent"> - {childElements} + onWheel={(e: React.WheelEvent) => (e.target as any).scrollHeight > (e.target as any).clientHeight && e.stopPropagation()} + onDrop={this.onTreeDrop} + ref={this.createTreeDropTarget}> + <EditableView + contents={this.resolvedDataDoc.title} + display={"block"} + height={72} + GetValue={() => StrCast(this.resolvedDataDoc.title)} + SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc).title = value) ? true : true} + OnFillDown={(value: string) => { + Doc.GetProto(this.props.Document).title = value; + let doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); + TreeView.loadId = doc[Id]; + Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true); + }} /> + {this.props.Document.workspaceLibrary ? this.notifsButton : (null)} + {this.props.Document.allowClear ? this.clearButton : (null)} + <ul className="no-indent" style={{ width: "max-content" }} > + { + TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.Document, this.props.DataDoc, this.props.fieldKey, addDoc, this.remove, + moveDoc, dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.outerXf, this.props.active, this.props.PanelWidth, this.props.renderDepth) + } </ul> </div > ); |