import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc'; import { DocData, Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction, returnAll, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnOne, returnTrue, returnZero } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType } from '../../util/DragManager'; import { SelectionManager } from '../../util/SelectionManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { EditableView } from '../EditableView'; import { DocumentView } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { StyleProp } from '../StyleProvider'; import { CollectionFreeFormView } from './collectionFreeForm'; import { CollectionSubView } from './CollectionSubView'; import './CollectionTreeView.scss'; import { TreeView } from './TreeView'; import React = require('react'); const _global = (window /* browser */ || global) /* node */ as any; export type collectionTreeViewProps = { treeViewExpandedView?: 'fields' | 'layout' | 'links' | 'data'; treeViewOpen?: boolean; treeViewHideTitle?: boolean; treeViewHideHeaderFields?: boolean; treeViewSkipFields?: string[]; // prevents specific fields from being displayed (see LinkBox) onCheckedClick?: () => ScriptField; onChildClick?: () => ScriptField; // TODO: [AL] add these fields AddToMap?: (treeViewDoc: Doc, index: number[]) => void; RemFromMap?: (treeViewDoc: Doc, index: number[]) => void; hierarchyIndex?: number[]; }; export enum TreeViewType { outline = 'outline', fileSystem = 'fileSystem', default = 'default', } @observer export class CollectionTreeView extends CollectionSubView>() { private _treedropDisposer?: DragManager.DragDropDisposer; private _mainEle?: HTMLDivElement; private _titleRef?: HTMLDivElement | HTMLInputElement | null; private _disposers: { [name: string]: IReactionDisposer } = {}; private _isDisposing = false; // notes that instance is in process of being disposed private refList: Set = new Set(); // list of tree view items to monitor for height changes private observer: any; // observer for monitoring tree view items. @computed get doc() { return this.props.Document; } @computed get dataDoc() { return this.props.DataDoc || this.doc; } @computed get treeViewtruncateTitleWidth() { return NumCast(this.doc.treeView_TruncateTitleWidth, this.panelWidth()); } @computed get treeChildren() { TraceMobx(); return this.props.childDocuments || this.childDocs; } @computed get outlineMode() { return this.doc.treeView_Type === TreeViewType.outline; } @computed get fileSysMode() { return this.doc.treeView_Type === TreeViewType.fileSystem; } @computed get dashboardMode() { return this.doc === Doc.MyDashboards; } @observable _titleHeight = 0; // height of the title bar MainEle = () => this._mainEle; // these should stay in synch with counterparts in DocComponent.ts ViewBoxAnnotatableComponent @observable _isAnyChildContentActive = false; whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive))); isContentActive = (outsideReaction?: boolean) => (this._isAnyChildContentActive ? true : this.props.isContentActive() ? true : false); componentWillUnmount() { this._isDisposing = true; super.componentWillUnmount(); this._treedropDisposer?.(); Object.values(this._disposers).forEach(disposer => disposer?.()); } shrinkWrap = () => {}; // placeholder to allow setContentView to work componentDidMount() { //this.props.setContentView?.(this); this._disposers.autoheight = reaction( () => this.rootDoc.layout_autoHeight, auto => auto && this.computeHeight(), { fireImmediately: true } ); } computeHeight = () => { if (!this._isDisposing) { const titleHeight = !this._titleRef ? this.marginTop() : Number(getComputedStyle(this._titleRef).height.replace('px', '')); const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace('px', '')), this.marginBot()) + 6; this.layoutDoc._layout_autoHeightMargins = bodyHeight; !this.props.dontRegisterView && this.props.setHeight?.(bodyHeight + titleHeight); } }; unobserveHeight = (ref: any) => { this.refList.delete(ref); this.rootDoc.layout_autoHeight && this.computeHeight(); }; observeHeight = (ref: any) => { if (ref) { this.refList.add(ref); this.observer = new _global.ResizeObserver( action((entries: any) => { if (this.rootDoc.layout_autoHeight && ref && this.refList.size && !SnappingManager.GetIsDragging()) { this.computeHeight(); } }) ); this.rootDoc.layout_autoHeight && this.computeHeight(); this.observer.observe(ref); } }; protected createTreeDropTarget = (ele: HTMLDivElement) => { this._treedropDisposer?.(); if ((this._mainEle = ele)) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this)); }; protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent) => { const dropAction = this.layoutDoc.dropAction as dropActionType; const dragData = de.complete.docDragData; if (dragData) { const sameTree = Doc.AreProtosEqual(dragData.treeViewDoc, this.rootDoc) ? true : false; const isAlreadyInTree = () => sameTree || dragData.draggedDocuments.some(d => d.embedContainer === this.doc && this.childDocs.includes(d)); if (isAlreadyInTree() !== sameTree) { console.log('WHAAAT'); } dragData.dropAction = dropAction && !isAlreadyInTree() ? dropAction : sameTree && dragData.dropAction !== 'inSame' ? 'same' : dragData.dropAction; e.stopPropagation(); } }; screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this._headerHeight); @action remove = (doc: Doc | Doc[]): boolean => { const docs = doc instanceof Doc ? [doc] : doc; const targetDataDoc = this.doc[DocData]; const value = DocListCast(targetDataDoc[this.props.fieldKey]); const result = value.filter(v => !docs.includes(v)); if ((doc instanceof Doc ? [doc] : doc).some(doc => SelectionManager.Views().some(dv => Doc.AreProtosEqual(dv.rootDoc, doc)))) SelectionManager.DeselectAll(); if (result.length !== value.length && doc instanceof Doc) { const ind = DocListCast(targetDataDoc[this.props.fieldKey]).indexOf(doc); const prev = ind && DocListCast(targetDataDoc[this.props.fieldKey])[ind - 1]; this.props.removeDocument?.(doc); if (ind > 0 && prev) { FormattedTextBox.SelectOnLoad = prev[Id]; DocumentManager.Instance.getDocumentView(prev, this.props.DocumentView?.())?.select(false); } return true; } return false; }; @action addDoc = (docs: Doc | Doc[], relativeTo: Opt, before?: boolean): boolean => { const doAddDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => { const res = flg && Doc.AddDocToList(this.doc[DocData], this.props.fieldKey, doc, relativeTo, before); res && Doc.SetContainer(doc, this.props.Document); return res; }, true); if (this.doc.resolvedDataDoc instanceof Promise) return false; return relativeTo === undefined ? this.props.addDocument?.(docs) || false : doAddDoc(docs); }; 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 (!Doc.noviceMode) { const layoutItems: ContextMenuProps[] = []; layoutItems.push({ description: 'Make tree state ' + (this.doc.treeView_OpenIsTransient ? 'persistent' : 'transient'), event: () => (this.doc.treeView_OpenIsTransient = !this.doc.treeView_OpenIsTransient), icon: 'paint-brush' }); layoutItems.push({ description: (this.doc.treeView_HideHeaderFields ? 'Show' : 'Hide') + ' Header Fields', event: () => (this.doc.treeView_HideHeaderFields = !this.doc.treeView_HideHeaderFields), icon: 'paint-brush' }); layoutItems.push({ description: (this.doc.treeView_HideTitle ? 'Show' : 'Hide') + ' Title', event: () => (this.doc.treeView_HideTitle = !this.doc.treeView_HideTitle), icon: 'paint-brush' }); ContextMenu.Instance.addItem({ description: 'Options...', subitems: layoutItems, icon: 'eye' }); const existingOnClick = ContextMenu.Instance.findByDescription('OnClick...'); const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : []; onClicks.push({ description: 'Edit onChecked Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.doc, undefined, 'onCheckedClick'), 'edit onCheckedClick'), icon: 'edit' }); !existingOnClick && ContextMenu.Instance.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); } }; onTreeDrop = (e: React.DragEvent, addDocs?: (docs: Doc[]) => void) => this.onExternalDrop(e, {}, addDocs); @undoBatch makeTextCollection = (childDocs: Doc[]) => this.addDoc(TreeView.makeTextBullet(), childDocs.length ? childDocs[0] : undefined, true); get editableTitle() { return ( StrCast(this.dataDoc.title)} SetValue={undoBatch((value: string, shift: boolean, enter: boolean) => { if (enter && this.props.Document.treeView_Type === TreeViewType.outline) this.makeTextCollection(this.treeChildren); this.dataDoc.title = value; return true; })} /> ); } onKey = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => { if (this.outlineMode && e.key === 'Enter') { e.stopPropagation(); this.makeTextCollection(this.treeChildren); return true; } }; get documentTitle() { return ( ); } childContextMenuItems = () => { const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []); const customFilters = Cast(this.doc.childContextMenuFilters, listSpec(ScriptField), []); const icons = StrListCast(this.doc.childContextMenuIcons); return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], icon: icons[i], label })); }; headerFields = () => this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeView_HideHeaderFields); @observable _renderCount = 1; @computed get treeViewElements() { TraceMobx(); const dragAction = StrCast(this.doc.childDragAction) as dropActionType; const addDoc = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before); const moveDoc = (d: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument?.(d, target, addDoc) || false; if (this._renderCount < this.treeChildren.length) setTimeout(action(() => (this._renderCount = Math.min(this.treeChildren.length, this._renderCount + 20)))); return TreeView.GetChildElements( this.treeChildren, this, this, this.doc, this.props.DataDoc, undefined, undefined, addDoc, this.remove, moveDoc, dragAction, this.props.addDocTab, this.props.styleProvider, this.screenToLocalTransform, this.isContentActive, this.panelWidth, this.props.renderDepth, this.headerFields, [], this.props.onCheckedClick, this.onChildClick, this.props.treeViewSkipFields, true, this.whenChildContentsActiveChanged, this.props.dontRegisterView || Cast(this.props.Document.childDontRegisterViews, 'boolean', null), this.observeHeight, this.unobserveHeight, this.childContextMenuItems(), //TODO: [AL] add these this.props.AddToMap, this.props.RemFromMap, this.props.hierarchyIndex, this._renderCount ); } @computed get titleBar() { return this.dataDoc === null ? null : (
(this._titleRef = r) && (this._titleHeight = r.getBoundingClientRect().height * this.props.ScreenToLocalTransform().Scale))} key={this.doc[Id]} style={!this.outlineMode ? { marginLeft: this.marginX(), paddingTop: this.marginTop() } : {}}> {this.outlineMode ? this.documentTitle : this.editableTitle}
); } @computed get noviceExplainer() { return !Doc.noviceMode || !this.rootDoc.layout_explainer ? null :
{StrCast(this.rootDoc.layout_explainer)}
; } return35 = () => 35; @computed get buttonMenu() { const menuDoc = Cast(this.rootDoc.layout_headerButton, Doc, null); // To create a multibutton menu add a CollectionLinearView return !menuDoc ? null : (
); } @computed get nativeWidth() { return Doc.NativeWidth(this.Document, undefined, true); } @computed get nativeHeight() { return Doc.NativeHeight(this.Document, undefined, true); } /// scale factor for tree view so that it will fit within it's panel bounds @computed get nativeDimScaling() { const nw = this.nativeWidth; const nh = this.nativeHeight; const hscale = nh ? this.props.PanelHeight() / nh : 1; const wscale = nw ? this.props.PanelWidth() / nw : 1; return wscale < hscale ? wscale : hscale; } marginX = () => NumCast(this.doc._xMargin); marginTop = () => NumCast(this.doc._yMargin); marginBot = () => NumCast(this.doc._yMargin); documentTitleWidth = () => Math.min(this.layoutDoc?.[Width](), this.panelWidth()); documentTitleHeight = () => (this.layoutDoc?.[Height]() || 0) - NumCast(this.layoutDoc.layout_autoHeightMargins); truncateTitleWidth = () => this.treeViewtruncateTitleWidth; onChildClick = () => this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick); panelWidth = () => Math.max(0, this.props.PanelWidth() - 2 * this.marginX() * (this.props.NativeDimScaling?.() || 1)); addAnnotationDocument = (doc: Doc | Doc[]) => this.addDocument(doc, `${this.props.fieldKey}_annotations`) || false; remAnnotationDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, `${this.props.fieldKey}_annotations`) || false; moveAnnotationDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[], annotationKey?: string) => boolean) => this.moveDocument(doc, targetCollection, addDocument, `${this.props.fieldKey}_annotations`) || false; @observable _headerHeight = 0; @computed get content() { const background = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor); const color = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.Color); const pointerEvents = () => (this.props.isContentActive() === false ? 'none' : undefined); const titleBar = this.props.treeViewHideTitle || this.doc.treeView_HideTitle ? null : this.titleBar; return (
{!this.buttonMenu && !this.noviceExplainer ? null : (
r && (this._headerHeight = Number(getComputedStyle(r).height.replace(/px/, ''))))}> {this.buttonMenu} {this.noviceExplainer}
)}
!this.doc.treeView_HasOverlay && r && this.createTreeDropTarget(r)} style={{ ...(!titleBar ? { marginLeft: this.marginX(), paddingTop: this.marginTop() } : {}), color: color(), overflow: 'auto', width: '100%', height: '100%', }}> {titleBar}
e.stopPropagation()} onClick={e => (!this.layoutDoc.forceActive ? this.props.select(false) : SelectionManager.DeselectAll())} onDrop={this.onTreeDrop}>
    {this.treeViewElements}
); } render() { TraceMobx(); const scale = this.props.NativeDimScaling?.() || 1; return (
{!(this.doc instanceof Doc) || !this.treeChildren ? null : this.doc.treeView_HasOverlay ? ( {this.content} ) : ( this.content )}
); } }