aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections/TreeView.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/collections/TreeView.tsx')
-rw-r--r--src/client/views/collections/TreeView.tsx100
1 files changed, 69 insertions, 31 deletions
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 2e98fb508..86fd21677 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -1,7 +1,7 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
-import { DataSym, Doc, DocListCast, DocListCastOrNull, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { DataSym, Doc, DocListCast, DocListCastOrNull, Field, HeightSym, Opt, WidthSym, StrListCast } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { RichTextField } from '../../../fields/RichTextField';
@@ -9,7 +9,7 @@ import { listSpec } from '../../../fields/Schema';
import { ComputedField, ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, simulateMouseClick, Utils } from '../../../Utils';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, simulateMouseClick, Utils, returnOne } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from "../../documents/DocumentTypes";
import { CurrentUserUtils } from '../../util/CurrentUserUtils';
@@ -20,7 +20,7 @@ import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
import { undoBatch, UndoManager } from '../../util/UndoManager';
import { EditableView } from "../EditableView";
-import { TREE_BULLET_WIDTH } from '../globalCssVariables.scss';
+import { TREE_BULLET_WIDTH } from '../global/globalCssVariables.scss';
import { DocumentView, DocumentViewProps, StyleProviderFunc, DocumentViewInternal } from '../nodes/DocumentView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
@@ -54,6 +54,7 @@ export interface TreeViewProps {
indentDocument?: (editTitle: boolean) => void;
outdentDocument?: (editTitle: boolean) => void;
ScreenToLocalTransform: () => Transform;
+ contextMenuItems: { script: ScriptField, filter: ScriptField, label: string }[];
dontRegisterView?: boolean;
styleProvider?: StyleProviderFunc | undefined;
treeViewHideHeaderFields: () => boolean;
@@ -99,13 +100,14 @@ export class TreeView extends React.Component<TreeViewProps> {
@observable _dref: DocumentView | undefined | null;
get displayName() { return "TreeView(" + this.props.document.title + ")"; } // this makes mobx trace() statements more descriptive
get defaultExpandedView() {
- return this.props.treeView.fileSysMode ? (this.doc.isFolder ? this.fieldKey : "aliases") :
- this.props.treeView.outlineMode || this.childDocs ? this.fieldKey : Doc.UserDoc().noviceMode ? "layout" : StrCast(this.props.treeView.doc.treeViewExpandedView, "fields");
+ return this.doc.viewType === CollectionViewType.Docking ? this.fieldKey :
+ this.props.treeView.fileSysMode ? (this.doc.isFolder ? this.fieldKey : "layout") :
+ this.props.treeView.outlineMode || this.childDocs ? this.fieldKey : Doc.UserDoc().noviceMode ? "layout" : StrCast(this.props.treeView.doc.treeViewExpandedView, "fields");
}
@computed get doc() { return this.props.document; }
@computed get treeViewOpen() { return (!this.treeViewOpenIsTransient && Doc.GetT(this.doc, "treeViewOpen", "boolean", true)) || this._transientOpenState; }
- @computed get treeViewExpandedView() { return StrCast(this.doc.treeViewExpandedView, this.defaultExpandedView); }
+ @computed get treeViewExpandedView() { return this.validExpandViewTypes.includes(StrCast(this.doc.treeViewExpandedView)) ? StrCast(this.doc.treeViewExpandedView) : this.defaultExpandedView; }
@computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.containerCollection.maxEmbedHeight, 200); }
@computed get dataDoc() { return this.doc[DataSym]; }
@computed get layoutDoc() { return Doc.Layout(this.doc); }
@@ -151,7 +153,10 @@ export class TreeView extends React.Component<TreeViewProps> {
this.treeViewOpen = !this.treeViewOpen;
} else {
// choose an appropriate alias or make one. --- choose the first alias that (1) user owns, (2) has no context field ... otherwise make a new alias
+ // this.props.addDocTab(CurrentUserUtils.ActiveDashboard.isShared ? Doc.MakeAlias(this.props.document) : this.props.document, "add:right");
+ // choose an appropriate alias or make one -- -- choose the first alias that (1) the user owns, (2) has no context field - if I own it and someone else does not have it open,, otherwise create an alias
this.props.addDocTab(this.props.document, "add:right");
+
}
}
constructor(props: any) {
@@ -261,15 +266,19 @@ export class TreeView extends React.Component<TreeViewProps> {
if (docDragData) {
e.stopPropagation();
if (docDragData.draggedDocuments[0] === this.doc) return true;
- const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before);
- const canAdd = !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes("add") || docDragData.treeViewDoc === this.props.treeView.props.Document;
- const localAdd = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) && ((doc.context = this.doc.context) || true) ? true : false;
- const addDoc = !inside ? parentAddDoc :
- (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc), true as boolean);
- const move = (!docDragData.dropAction || docDragData.dropAction === "proto" || docDragData.dropAction === "move" || docDragData.dropAction === "same") && docDragData.moveDocument;
- if (canAdd) {
- UndoManager.RunInTempBatch(() => docDragData.droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (docDragData.dropAction === "proto" ? addDoc(d) : false) : addDoc(d)) || added, false));
- }
+ this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document);
+ }
+ }
+
+ dropDocuments(droppedDocuments: Doc[], before: boolean, inside: number | boolean, dropAction: dropActionType, moveDocument: DragManager.MoveFunction | undefined, forceAdd: boolean) {
+ const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before);
+ const canAdd = !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes("add") || forceAdd;
+ const localAdd = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) && ((doc.context = this.doc.context) || true) ? true : false;
+ const addDoc = !inside ? parentAddDoc :
+ (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc), true as boolean);
+ const move = (!dropAction || dropAction === "proto" || dropAction === "move" || dropAction === "same") && moveDocument;
+ if (canAdd) {
+ UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === "proto" ? addDoc(d) : false) : addDoc(d)) || added, false));
}
}
@@ -287,7 +296,7 @@ export class TreeView extends React.Component<TreeViewProps> {
const aspect = Doc.NativeAspect(layoutDoc);
if (layoutDoc._fitWidth) return Math.min(this.props.panelWidth() - treeBulletWidth(), layoutDoc[WidthSym]());
if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT * aspect, this.props.panelWidth() - treeBulletWidth()));
- return Math.min(this.props.panelWidth() - treeBulletWidth(), Doc.NativeWidth(layoutDoc) ? layoutDoc[WidthSym]() : this.layoutDoc[WidthSym]());
+ return Math.min((this.props.panelWidth() - treeBulletWidth()) / (this.props.treeView.props.scaling?.() || 1), Doc.NativeWidth(layoutDoc) ? layoutDoc[WidthSym]() : this.layoutDoc[WidthSym]());
}
docHeight = () => {
const layoutDoc = this.layoutDoc;
@@ -329,7 +338,7 @@ export class TreeView extends React.Component<TreeViewProps> {
this.props.dropAction, this.props.addDocTab, this.titleStyleProvider, this.props.ScreenToLocalTransform, this.props.isContentActive,
this.props.panelWidth, this.props.renderDepth, this.props.treeViewHideHeaderFields,
[...this.props.renderedIds, doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.skipFields, false, this.props.whenChildContentsActiveChanged,
- this.props.dontRegisterView, emptyFunction, emptyFunction);
+ this.props.dontRegisterView, emptyFunction, emptyFunction, this.childContextMenuItems());
} else {
contentElement = <EditableView key="editableView"
contents={contents !== undefined ? Field.toString(contents as Field) : "null"}
@@ -356,7 +365,7 @@ export class TreeView extends React.Component<TreeViewProps> {
return rows;
}
- rtfWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.props.panelWidth() - treeBulletWidth());
+ rtfWidth = () => Math.min(this.layoutDoc?.[WidthSym](), (this.props.panelWidth() - treeBulletWidth())) / (this.props.treeView.props.scaling?.() || 1);
rtfHeight = () => this.rtfWidth() <= this.layoutDoc?.[WidthSym]() ? Math.min(this.layoutDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT;
rtfOutlineHeight = () => Math.max(this.layoutDoc?.[HeightSym](), treeBulletWidth());
expandPanelHeight = () => {
@@ -411,7 +420,7 @@ export class TreeView extends React.Component<TreeViewProps> {
StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.titleStyleProvider, this.props.ScreenToLocalTransform,
this.props.isContentActive, this.props.panelWidth, this.props.renderDepth, this.props.treeViewHideHeaderFields,
[...this.props.renderedIds, this.doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.skipFields, false, this.props.whenChildContentsActiveChanged,
- this.props.dontRegisterView, emptyFunction, emptyFunction)}
+ this.props.dontRegisterView, emptyFunction, emptyFunction, this.childContextMenuItems())}
</ul >;
} else if (this.treeViewExpandedView === "fields") {
return <ul key={this.doc[Id] + this.doc.title}>
@@ -468,16 +477,20 @@ export class TreeView extends React.Component<TreeViewProps> {
</div>;
}
+ @computed get validExpandViewTypes() {
+ if (this.doc.viewType === CollectionViewType.Docking) return [this.fieldKey];
+ const annos = () => DocListCast(this.doc[this.fieldKey + "-annotations"]).length ? "annotations" : "";
+ const links = () => DocListCast(this.doc.links).length ? "links" : "";
+ const data = () => this.childDocs && !this.props.treeView.dashboardMode ? this.fieldKey : "";
+ const aliases = () => this.props.treeView.dashboardMode ? "" : "aliases";
+ const fields = () => Doc.UserDoc().noviceMode ? "" : "fields";
+ return [data(), "layout", ...(this.props.treeView.fileSysMode ? [aliases(), links(), annos()] : []), fields()].filter(m => m);
+ }
@action
expandNextviewType = () => {
if (this.treeViewOpen && !this.doc.isFolder && !this.props.treeView.outlineMode && !this.doc.treeViewExpandedViewLock) {
- const next = (modes: any[]) => modes[(modes.indexOf(StrCast(this.doc.treeViewExpandedView)) + 1) % modes.length];
- const annos = () => DocListCast(this.doc[this.fieldKey + "-annotations"]).length ? "annotations" : "";
- const links = () => DocListCast(this.doc.links).length ? "links" : "";
- const children = () => this.childDocs ? this.fieldKey : "";
- this.doc.treeViewExpandedView = next(this.props.treeView.fileSysMode ?
- (Doc.UserDoc().noviceMode ? ["layout", "aliases"] : ["layout", "aliases", "fields"]) :
- (Doc.UserDoc().noviceMode ? [children(), "layout"] : [children(), "fields", "layout", links(), annos()]).filter(mode => mode));
+ const next = (modes: any[]) => modes[(modes.indexOf(StrCast(this.treeViewExpandedView)) + 1) % modes.length];
+ this.doc.treeViewExpandedView = next(this.validExpandViewTypes);
}
this.treeViewOpen = true;
}
@@ -500,13 +513,22 @@ export class TreeView extends React.Component<TreeViewProps> {
}
contextMenuItems = () => {
const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: "any" })!, label: "New Folder" };
- return this.doc.isFolder ? [makeFolder] :
+ const openAlias = { script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, label: "Open Alias" };
+ const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, label: "Focus or Open" };
+ return [...this.props.contextMenuItems.filter(mi => !mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result), ... (this.doc.isFolder ? [makeFolder] :
Doc.IsSystem(this.doc) ? [] :
this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc) ?
- [{ script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, label: "Open Alias" }, makeFolder] :
- [{ script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, label: "Focus or Open" }];
+ [openAlias, makeFolder] :
+ this.doc.viewType === CollectionViewType.Docking ? [] :
+ [openAlias, focusDoc])];
+ }
+ childContextMenuItems = () => {
+ const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []);
+ const customFilters = Cast(this.doc.childContextMenuFilters, listSpec(ScriptField), []);
+ return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], label }));
}
onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.treeChildClick));
+
onChildDoubleClick = () => (!this.props.treeView.outlineMode && this._openScript?.()) || ScriptCast(this.doc.treeChildDoubleClick);
refocus = () => this.props.treeView.props.focus(this.props.treeView.props.Document);
@@ -522,7 +544,7 @@ export class TreeView extends React.Component<TreeViewProps> {
switch (property.split(":")[0]) {
case StyleProp.Opacity: return this.props.treeView.outlineMode ? undefined : 1;
case StyleProp.BackgroundColor: return this.selected ? "#7089bb" : StrCast(doc._backgroundColor, StrCast(doc.backgroundColor));
- case StyleProp.DocContents: return testDocProps(props) && !props?.treeViewDoc ? (null) :
+ case StyleProp.DocContents: return this.props.treeView.outlineMode ? (null) :
<div className="treeView-label" style={{ // just render a title for a tree view label (identified by treeViewDoc being set in 'props')
maxWidth: props?.PanelWidth() || undefined,
background: props?.styleProvider?.(doc, props, StyleProp.BackgroundColor),
@@ -624,6 +646,7 @@ export class TreeView extends React.Component<TreeViewProps> {
searchFilterDocs={returnEmptyDoclist}
ContainingCollectionView={undefined}
ContainingCollectionDoc={this.props.treeView.props.Document}
+ ContentScaling={returnOne}
/>;
const buttons = this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.Decorations + (Doc.IsSystem(this.props.containerCollection) ? ":afterHeader" : ""));
@@ -680,10 +703,12 @@ export class TreeView extends React.Component<TreeViewProps> {
hideDecorationTitle={this.props.treeView.outlineMode}
hideResizeHandles={this.props.treeView.outlineMode}
focus={this.refocus}
+ ContentScaling={returnOne}
hideLinkButton={BoolCast(this.props.treeView.props.Document.childHideLinkButton)}
dontRegisterView={BoolCast(this.props.treeView.props.Document.childDontRegisterViews, this.props.dontRegisterView)}
ScreenToLocalTransform={this.docTransform}
renderDepth={this.props.renderDepth + 1}
+ treeViewDoc={this.props.treeView?.props.Document}
rootSelected={returnTrue}
layerProvider={returnTrue}
docViewPath={this.props.treeView.props.docViewPath}
@@ -727,12 +752,23 @@ export class TreeView extends React.Component<TreeViewProps> {
</div>;
}
+ onTreeDrop = (de: React.DragEvent) => {
+ const pt = [de.clientX, de.clientY];
+ const rect = this._header.current!.getBoundingClientRect();
+ const before = pt[1] < rect.top + rect.height / 2;
+ const inside = this.props.treeView.fileSysMode && !this.doc.isFolder ? false : pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && this.childDocList.length);
+
+ const docs = this.props.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, "copy", undefined, false));
+ }
+
render() {
TraceMobx();
const hideTitle = this.doc.treeViewHideHeader || this.props.treeView.outlineMode;
return this.props.renderedIds.indexOf(this.doc[Id]) !== -1 ? "<" + this.doc.title + ">" : // just print the title of documents we've previously rendered in this hierarchical path to avoid cycles
<div className={`treeView-container${this.props.isContentActive() ? "-active" : ""}`}
ref={this.createTreeDropTarget}
+
+ onDrop={this.onTreeDrop}
//onPointerDown={e => this.props.isContentActive(true) && SelectionManager.DeselectAll()} // bcz: this breaks entering a text filter in a filterBox since it deselects the filter's target document
onKeyDown={this.onKeyDown}>
<li className="collection-child">
@@ -798,7 +834,8 @@ export class TreeView extends React.Component<TreeViewProps> {
whenChildContentsActiveChanged: (isActive: boolean) => void,
dontRegisterView: boolean | undefined,
observerHeight: (ref: any) => void,
- unobserveHeight: (ref: any) => void
+ unobserveHeight: (ref: any) => void,
+ contextMenuItems: ({ script: ScriptField, filter: ScriptField, label: string }[])
) {
const viewSpecScript = Cast(conainerCollection.viewSpecScript, ScriptField);
if (viewSpecScript) {
@@ -863,6 +900,7 @@ export class TreeView extends React.Component<TreeViewProps> {
parentTreeView={parentTreeView}
observeHeight={observerHeight}
unobserveHeight={unobserveHeight}
+ contextMenuItems={contextMenuItems}
/>;
});
}