diff options
| author | bobzel <zzzman@gmail.com> | 2021-01-11 11:33:46 -0500 |
|---|---|---|
| committer | bobzel <zzzman@gmail.com> | 2021-01-11 11:33:46 -0500 |
| commit | 6c1857193aa51ccfec081aaa4372de164e152355 (patch) | |
| tree | 8ae584d0c0a262df63153fec097f32ab9ef80441 /src/client/views/collections/TreeView.tsx | |
| parent | 768ab508529ab2b7f43a44592e5dc2c9f15a68a5 (diff) | |
| parent | e80e0e3938f1f7c5f740553eb5cb7b152f2598e8 (diff) | |
Merge branch 'master' into ink_edits
Diffstat (limited to 'src/client/views/collections/TreeView.tsx')
| -rw-r--r-- | src/client/views/collections/TreeView.tsx | 476 |
1 files changed, 271 insertions, 205 deletions
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 925eb4be6..a61a9e43c 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -1,6 +1,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; +import { TREE_BULLET_WIDTH } from '../globalCssVariables.scss'; import { DataSym, Doc, DocListCast, DocListCastOrNull, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; @@ -9,7 +10,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, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, simulateMouseClick, Utils } from '../../../Utils'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, returnZero, simulateMouseClick, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from "../../documents/DocumentTypes"; import { CurrentUserUtils } from '../../util/CurrentUserUtils'; @@ -20,15 +21,17 @@ import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { EditableView } from "../EditableView"; -import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; -import { DocumentView } from '../nodes/DocumentView'; +import { DocumentView, DocumentViewProps, StyleProviderFunc } from '../nodes/DocumentView'; +import { FieldViewProps } from '../nodes/FieldView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; import { KeyValueBox } from '../nodes/KeyValueBox'; +import { StyleProp, testDocProps } from '../StyleProvider'; import { CollectionTreeView } from './CollectionTreeView'; import { CollectionView, CollectionViewType } from './CollectionView'; import "./TreeView.scss"; import React = require("react"); +import { SliderBox } from '../nodes/SliderBox'; export interface TreeViewProps { document: Doc; @@ -39,17 +42,16 @@ export interface TreeViewProps { removeDoc: ((doc: Doc | Doc[]) => boolean) | undefined; moveDocument: DragManager.MoveFunction; dropAction: dropActionType; - addDocTab: (doc: Doc, where: string, libraryPath?: Doc[]) => boolean; + addDocTab: (doc: Doc, where: string) => boolean; pinToPres: (document: Doc) => void; panelWidth: () => number; panelHeight: () => number; - ChromeHeight: undefined | (() => number); addDocument: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean; indentDocument?: () => void; outdentDocument?: () => void; ScreenToLocalTransform: () => Transform; dontRegisterView?: boolean; - backgroundColor?: (doc: Opt<Doc>, renderDepth: number) => string | undefined; + styleProvider?: StyleProviderFunc | undefined; outerXf: () => { translateX: number, translateY: number }; treeView: CollectionTreeView; parentKey: string; @@ -59,11 +61,13 @@ export interface TreeViewProps { renderedIds: string[]; // list of document ids rendered used to avoid unending expansion of items in a cycle onCheckedClick?: () => ScriptField; onChildClick?: () => ScriptField; - ignoreFields?: string[]; + skipFields?: string[]; firstLevel: boolean; whenActiveChanged: (isActive: boolean) => void; } +const treeBulletWidth = function () { return Number(TREE_BULLET_WIDTH.replace("px", "")); }; + @observer /** * Renders a treeView of a collection of documents @@ -78,12 +82,13 @@ export class TreeView extends React.Component<TreeViewProps> { private _openScript: (() => ScriptField) | undefined; private _header?: React.RefObject<HTMLDivElement> = React.createRef(); private _treedropDisposer?: DragManager.DragDropDisposer; - private _dref = React.createRef<HTMLDivElement>(); private _tref = React.createRef<HTMLDivElement>(); private _docRef = React.createRef<DocumentView>(); private _uniqueId = Utils.GenerateGuid(); private _editMaxWidth: number | string = 0; + + @observable _dref: DocumentView | undefined | null; @computed get doc() { TraceMobx(); return this.props.document; } get noviceMode() { return BoolCast(Doc.UserDoc().noviceMode, false); } get displayName() { return "TreeView(" + this.props.document.title + ")"; } // this makes mobx trace() statements more descriptive @@ -111,9 +116,8 @@ export class TreeView extends React.Component<TreeViewProps> { @computed get childDocs() { TraceMobx(); return this.childDocList(this.fieldKey); } @computed get childLinks() { return this.childDocList("links"); } @computed get childAnnos() { return this.childDocList(this.fieldKey + "-annotations"); } - @computed get boundsOfCollectionDocument() { - return StrCast(this.props.document.type).indexOf(DocumentType.COL) === -1 || !DocListCast(this.props.document[this.fieldKey]).length ? undefined : - Doc.ComputeContentBounds(DocListCast(this.props.document[this.fieldKey])); + @computed get isCollectionDoc() { + return !StrCast(this.props.document.type).includes(DocumentType.COL) || !DocListCast(this.props.document[this.fieldKey]).length ? false : true; } @undoBatch openRight = () => this.props.addDocTab(this.doc, "add:right"); @@ -185,8 +189,12 @@ export class TreeView extends React.Component<TreeViewProps> { } public static makeTextBullet() { - const bullet = Docs.Create.TextDocument("-text-", { title: "-title-", "sidebarColor": "transparent", "sidebarViewType": CollectionViewType.Freeform, forceActive: true, _viewType: CollectionViewType.Tree, hideLinkButton: true, _showSidebar: true, treeViewOutlineMode: true, x: 0, y: 0, _xMargin: 0, _yMargin: 0, _autoHeight: true, _singleLine: true, _backgroundColor: "transparent", _width: 1000, _height: 10 }); - Doc.GetProto(bullet).layout = CollectionView.LayoutString("data"); + const bullet = Docs.Create.TextDocument("-text-", { + layout: CollectionView.LayoutString("data"), + title: "-title-", "sidebarColor": "transparent", "sidebarViewType": CollectionViewType.Freeform, + _viewType: CollectionViewType.Tree, hideLinkButton: true, _showSidebar: true, treeViewOutlineMode: true, + x: 0, y: 0, _xMargin: 0, _yMargin: 0, _autoHeight: true, _singleLine: true, _backgroundColor: "transparent", _width: 1000, _height: 10 + }); Doc.GetProto(bullet).title = ComputedField.MakeFunction('self.text?.Text'); Doc.GetProto(bullet).data = new List<Doc>([]); Doc.SetInPlace(bullet, "editTitle", "*", false); @@ -269,31 +277,35 @@ export class TreeView extends React.Component<TreeViewProps> { return false; } - refTransform = (ref: HTMLDivElement) => { + refTransform = (ref: HTMLDivElement | undefined | null) => { + if (!ref) return this.props.ScreenToLocalTransform(); const { scale, translateX, translateY } = Utils.GetScreenTransform(ref); const outerXf = this.props.outerXf(); const offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]); } - docTransform = () => this.refTransform(this._dref.current!); - getTransform = () => this.refTransform(this._tref.current!); + docTransform = () => this.refTransform(this._dref?.ContentRef?.current); + getTransform = () => this.refTransform(this._tref.current); docWidth = () => { const layoutDoc = this.layoutDoc; const aspect = Doc.NativeAspect(layoutDoc); - if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT * aspect, this.props.panelWidth() - 20)); - return Doc.NativeWidth(layoutDoc) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20; + 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]()); } docHeight = () => { const layoutDoc = this.layoutDoc; - const bounds = this.boundsOfCollectionDocument; return Math.max(70, Math.min(this.MAX_EMBED_HEIGHT, (() => { const aspect = Doc.NativeAspect(layoutDoc); if (aspect) return this.docWidth() / (aspect || 1); - if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x); - return layoutDoc._fitWidth ? (!Doc.NativeHeight(this.doc) ? NumCast(this.props.containingCollection._height) : - Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, Doc.NativeHeight(layoutDoc)) / - (Doc.NativeWidth(layoutDoc) || NumCast(this.props.containingCollection._height)) - )) : (layoutDoc[HeightSym]() || 50); + return layoutDoc._fitWidth ? + (!Doc.NativeHeight(this.doc) ? + NumCast(this.props.containingCollection._height) + : + Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, Doc.NativeHeight(layoutDoc)) / (Doc.NativeWidth(layoutDoc) || NumCast(this.props.containingCollection._height)) + )) + : + (layoutDoc[HeightSym]() || 50); })())); } @@ -304,7 +316,7 @@ export class TreeView extends React.Component<TreeViewProps> { doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)); for (const key of Object.keys(ids).slice().sort()) { - if (this.props.ignoreFields?.includes(key) || key === "title" || key === "treeViewOpen") continue; + if (this.props.skipFields?.includes(key) || key === "title" || key === "treeViewOpen") continue; const contents = doc[key]; let contentElement: (JSX.Element | null)[] | JSX.Element = []; @@ -318,9 +330,9 @@ export class TreeView extends React.Component<TreeViewProps> { const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true); contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] : DocListCast(contents), this.props.treeView, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, - this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, - this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, - [...this.props.renderedIds, doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields, false, this.props.whenActiveChanged, this.props.dontRegisterView); + this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.titleStyleProvider, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, + this.props.panelWidth, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, + [...this.props.renderedIds, doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.skipFields, false, this.props.whenActiveChanged, this.props.dontRegisterView); } else { contentElement = <EditableView key="editableView" contents={contents !== undefined ? Field.toString(contents as Field) : "null"} @@ -347,9 +359,21 @@ export class TreeView extends React.Component<TreeViewProps> { return rows; } - rtfWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.props.panelWidth() - 20); + rtfWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.props.panelWidth() - treeBulletWidth()); 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](), 20); + rtfOutlineHeight = () => Math.max(this.layoutDoc?.[HeightSym](), treeBulletWidth()); + expandPanelHeight = () => { + if (this.layoutDoc._fitWidth) return this.docHeight(); + const aspect = this.layoutDoc[WidthSym]() / this.layoutDoc[HeightSym](); + const docAspect = this.docWidth() / this.docHeight(); + return (docAspect < aspect) ? this.docWidth() / aspect : this.docHeight(); + } + expandPanelWidth = () => { + if (this.layoutDoc._fitWidth) return this.docWidth(); + const aspect = this.layoutDoc[WidthSym]() / this.layoutDoc[HeightSym](); + const docAspect = this.docWidth() / this.docHeight(); + return (docAspect > aspect) ? this.docHeight() * aspect : this.docWidth(); + } @computed get renderContent() { TraceMobx(); @@ -357,67 +381,46 @@ export class TreeView extends React.Component<TreeViewProps> { if (["links", "annotations", this.fieldKey].includes(expandKey)) { const remDoc = (doc: Doc | Doc[]) => this.remove(doc, expandKey); const localAdd = (doc: Doc, addBefore?: Doc, before?: boolean) => { + // if there's a sort ordering specified that can be modified on drop (eg, zorder can be modified, alphabetical can't), + // then the modification would be done here + const ordering = StrCast(this.doc[this.fieldKey + "-sortCriteria"]); + if (ordering === "zorder") { + const docs = TreeView.sortDocs(this.childDocs || ([] as Doc[]), ordering); + doc.zIndex = addBefore ? (before ? NumCast(addBefore.zIndex) - 0.5 : NumCast(addBefore.zIndex) + 0.5) : 1000; + docs.push(doc); + docs.sort((a, b) => NumCast(a.zIndex) > NumCast(b.zIndex) ? 1 : -1); + docs.forEach((d, i) => d.zIndex = i); + } const added = Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, false, true); added && (doc.context = this.doc.context); return added; }; const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true); const docs = expandKey === "links" ? this.childLinks : expandKey === "annotations" ? this.childAnnos : this.childDocs; - const sortKey = `${this.fieldKey}-sortAscending`; + const sortKey = `${this.fieldKey}-sortCriteria`; return <ul key={expandKey + "more"} className={this.doc.treeViewHideTitle ? "no-indent" : ""} onClick={(e) => { - !this.outlineMode && (this.doc[sortKey] = (this.doc[sortKey] ? false : (this.doc[sortKey] === false ? undefined : true))); + !this.outlineMode && (this.doc[sortKey] = + (this.doc[sortKey] === "ascending" ? "descending" : + (this.doc[sortKey] === "descending" ? "zorder" : + (this.doc[sortKey] === "zorder" ? undefined : + "ascending")))); e.stopPropagation(); }}> {!docs ? (null) : TreeView.GetChildElements(docs, this.props.treeView, this.layoutDoc, this.dataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, - StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, - this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, - [...this.props.renderedIds, this.doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields, false, this.props.whenActiveChanged, this.props.dontRegisterView)} + StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.props.pinToPres, this.titleStyleProvider, this.props.ScreenToLocalTransform, + this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, + [...this.props.renderedIds, this.doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.skipFields, false, this.props.whenActiveChanged, this.props.dontRegisterView)} </ul >; } else if (this.treeViewExpandedView === "fields") { - return <ul key={this.doc[Id] + this.doc.title}><div ref={this._dref} style={{ display: "inline-block" }} > - {this.expandedField} - </div></ul>; - } else { - const layoutDoc = this.layoutDoc; - const panelHeight = StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfHeight : this.docHeight; - const panelWidth = StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfWidth : this.docWidth; - return <div ref={this._dref} style={{ display: "inline-block", height: panelHeight() }} key={this.doc[Id]}> - <ContentFittingDocumentView - Document={this.doc} - DataDoc={undefined} - LibraryPath={emptyPath} - dontRegisterView={this.props.dontRegisterView} - renderDepth={this.props.renderDepth + 1} - rootSelected={returnTrue} - treeViewDoc={undefined} - backgroundColor={this.props.backgroundColor} - fitToBox={this.boundsOfCollectionDocument !== undefined} - FreezeDimensions={true} - NativeWidth={layoutDoc.type === DocumentType.RTF ? this.rtfWidth : undefined} - NativeHeight={layoutDoc.type === DocumentType.RTF ? this.rtfHeight : undefined} - PanelWidth={panelWidth} - PanelHeight={panelHeight} - focus={returnFalse} - ScreenToLocalTransform={this.docTransform} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - ContainingCollectionDoc={this.props.containingCollection} - ContainingCollectionView={undefined} - addDocument={returnFalse} - moveDocument={this.move} - removeDocument={this.props.removeDoc} - parentActive={this.props.active} - whenActiveChanged={this.props.whenActiveChanged} - addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} - bringToFront={returnFalse} - ContentScaling={returnOne} - /> - </div>; + return <ul key={this.doc[Id] + this.doc.title}> + <div style={{ display: "inline-block" }} > + {this.expandedField} + </div> + </ul>; } + return <ul>{this.renderEmbeddedDocument(false)}</ul>; } get onCheckedClick() { return this.doc.type === DocumentType.COL ? undefined : this.props.onCheckedClick?.() ?? ScriptCast(this.doc.onCheckedClick); } @@ -439,21 +442,32 @@ export class TreeView extends React.Component<TreeViewProps> { @computed get renderBullet() { TraceMobx(); + const iconType = Doc.toIcon(this.doc); const checked = this.onCheckedClick ? (this.doc.treeViewChecked ?? "unchecked") : undefined; - return <div className={`bullet${this.outlineMode ? "-outline" : ""}`} title={this.childDocs?.length ? `click to see ${this.childDocs?.length} items` : "view fields"} + return <div className={`bullet${this.outlineMode ? "-outline" : ""}`} key={"bullet"} + title={this.childDocs?.length ? `click to see ${this.childDocs?.length} items` : "view fields"} onClick={this.bulletClick} - style={this.outlineMode ? { opacity: NumCast(this.doc.opacity, 1) } : { + style={this.outlineMode ? { opacity: this.titleStyleProvider?.(this.doc, this.props.treeView.props, StyleProp.Opacity) } : { color: StrCast(this.doc.color, checked === "unchecked" ? "white" : "inherit"), opacity: checked === "unchecked" ? undefined : 0.4 }}> - {this.outlineMode && !(this.doc.text as RichTextField)?.Text ? (null) : - <FontAwesomeIcon icon={this.outlineMode ? [this.childDocs?.length && !this.treeViewOpen ? "fas" : "far", "circle"] : - checked === "check" ? "check" : - (checked === "x" ? "times" : checked === "unchecked" ? "square" : - !this.treeViewOpen ? (this.childDocs?.length ? "caret-square-right" : "caret-right") : - (this.childDocs?.length ? "caret-square-down" : "caret-down"))} />} + {this.outlineMode ? + !(this.doc.text as RichTextField)?.Text ? (null) : + <FontAwesomeIcon size="sm" icon={[this.childDocs?.length && !this.treeViewOpen ? "fas" : "far", "circle"]} /> : + <div className="treeView-bulletIcons" > + <div className={`treeView-${this.onCheckedClick ? "checkIcon" : "expandIcon"}`}> + <FontAwesomeIcon size="sm" icon={ + checked === "check" ? "check" : + (checked === "x" ? "times" : checked === "unchecked" ? "square" : + !this.treeViewOpen ? "caret-right" : + "caret-down")} /> + </div> + {this.onCheckedClick ? (null) : <FontAwesomeIcon icon={iconType} />} + </div> + } </div>; } + @computed get showTitleEditorControl() { return ["*", this._uniqueId, this.props.treeView._uniqueId].includes(Doc.GetT(this.doc, "editTitle", "string", true) || ""); } @computed get headerElements() { return (Doc.IsSystem(this.doc) && Doc.UserDoc().noviceMode) || this.props.treeViewHideHeaderFields() ? (null) : @@ -478,9 +492,50 @@ export class TreeView extends React.Component<TreeViewProps> { showContextMenu = (e: React.MouseEvent) => simulateMouseClick(this._docRef.current?.ContentDiv, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30); contextMenuItems = () => Doc.IsSystem(this.doc) ? [] : [{ script: ScriptField.MakeFunction(`openOnRight(self)`)!, label: "Open" }, { script: ScriptField.MakeFunction(`DocFocus(self)`)!, label: "Focus" }]; - truncateTitleWidth = () => NumCast(this.props.treeView.props.Document.treeViewTruncateTitleWidth, 0); + truncateTitleWidth = () => NumCast(this.props.treeView.props.Document.treeViewTruncateTitleWidth, this.props.panelWidth()); onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.treeChildClick)); onChildDoubleClick = () => (!this.outlineMode && this._openScript?.()) || ScriptCast(this.doc.treeChildDoubleClick); + + refocus = () => this.props.treeView.props.focus(this.props.treeView.props.Document); + ignoreEvent = (e: any) => { + if (this.props.active(true)) { + e.stopPropagation(); + e.preventDefault(); + } + } + titleStyleProvider = (doc: (Doc | undefined), props: Opt<DocumentViewProps | FieldViewProps>, property: string): any => { + if (!doc || doc !== this.doc) return this.props?.treeView?.props.styleProvider?.(doc, props, property); // properties are inherited from the CollectionTreeView, not the hierarchical parent in the treeView + + switch (property.split(":")[0]) { + case StyleProp.Opacity: return this.outlineMode ? undefined : 1; + case StyleProp.BackgroundColor: return StrCast(doc._backgroundColor, StrCast(doc.backgroundColor)); + case StyleProp.DocContents: return testDocProps(props) && !props?.treeViewDoc ? (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), + }}> + {StrCast(doc?.title)} + </div>; + case StyleProp.Decorations: return (null); + } + } + embeddedStyleProvider = (doc: (Doc | undefined), props: Opt<DocumentViewProps | FieldViewProps>, property: string): any => { + if (property.startsWith(StyleProp.Decorations)) return (null); + return this.props?.treeView?.props.styleProvider?.(doc, props, property); // properties are inherited from the CollectionTreeView, not the hierarchical parent in the treeView + } + onKeyDown = (e: React.KeyboardEvent) => { + if (this.doc.treeViewHideHeader || this.outlineMode) { + e.stopPropagation(); + e.preventDefault(); + switch (e.key) { + case "Tab": setTimeout(() => RichTextMenu.Instance.TextView?.EditorView?.focus(), 150); + return UndoManager.RunInBatch(() => e.shiftKey ? this.props.outdentDocument?.() : this.props.indentDocument?.(), "tab"); + case "Backspace": return !(this.doc.text as RichTextField)?.Text && this.props.removeDoc?.(this.doc); + case "Enter": return UndoManager.RunInBatch(this.makeTextCollection, "bullet"); + } + } + } + /** * Renders the EditableView title element for placement into the tree. */ @@ -488,12 +543,12 @@ export class TreeView extends React.Component<TreeViewProps> { get renderTitle() { TraceMobx(); const view = this.showTitleEditorControl ? this.editableView("title") : - <DocumentView + <DocumentView key="title" ref={this._docRef} Document={this.doc} DataDoc={undefined} + styleProvider={this.titleStyleProvider} treeViewDoc={this.props.treeView.props.Document} - LibraryPath={emptyPath} addDocument={undefined} addDocTab={this.props.addDocTab} rootSelected={returnTrue} @@ -504,22 +559,23 @@ export class TreeView extends React.Component<TreeViewProps> { moveDocument={this.move} removeDocument={this.props.removeDoc} ScreenToLocalTransform={this.getTransform} - ContentScaling={returnOne} + NativeHeight={returnZero} + NativeWidth={returnZero} PanelWidth={this.truncateTitleWidth} - PanelHeight={returnZero} + PanelHeight={() => 18} contextMenuItems={this.contextMenuItems} - opacity={this.outlineMode ? undefined : returnOne} renderDepth={1} focus={returnTrue} parentActive={returnTrue} whenActiveChanged={this.props.whenActiveChanged} bringToFront={emptyFunction} + cantBrush={this.props.treeView.props.cantBrush} dontRegisterView={BoolCast(this.props.treeView.props.Document.dontRegisterChildViews)} docFilters={returnEmptyFilter} docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} - ContainingCollectionDoc={this.props.containingCollection} + ContainingCollectionDoc={this.props.treeView.props.Document} />; return <> <div className={`docContainer${Doc.IsSystem(this.props.document) ? "-system" : ""}`} ref={this._tref} title="click to edit title. Double Click or Drag to Open" @@ -531,16 +587,92 @@ export class TreeView extends React.Component<TreeViewProps> { }} > {view} </div > - {this.headerElements} + <div className={"right-buttons-container"}> + {this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.Decorations + (Doc.IsSystem(this.props.containingCollection) ? ":afterHeader" : ""))} {/* hide and lock buttons */} + {this.headerElements} + </div> </>; } - refocus = () => this.props.treeView.props.focus(this.props.treeView.props.Document); + renderBulletHeader = (contents: JSX.Element) => { + return <> + <div className={`treeView-header` + (this._editMaxWidth ? "-editing" : "")} key="titleheader" + ref={this._header} + style={{ maxWidth: this._editMaxWidth }} + onClick={this.ignoreEvent} + onPointerDown={this.ignoreEvent} + onPointerEnter={this.onPointerEnter} + onPointerLeave={this.onPointerLeave}> + {contents} + </div> + {this.renderBorder} + </>; + } + + // renders the text version of a document as the header (e.g., useful for Slide views where the "") + @computed get renderTitleAsHeader() { + return <> + {this.renderBullet} + {this.renderTitle} + </>; + } + + renderEmbeddedDocument = (asText: boolean) => { + const layout = StrCast(Doc.LayoutField(this.layoutDoc)); + const isExpandable = layout.includes(FormattedTextBox.name) || layout.includes(SliderBox.name); + const panelWidth = asText || isExpandable ? this.rtfWidth : this.expandPanelWidth; + const panelHeight = asText ? this.rtfOutlineHeight : isExpandable ? this.rtfHeight : this.expandPanelHeight; + return <DocumentView key={this.doc[Id]} ref={action((r: DocumentView | null) => this._dref = r)} + Document={this.doc} + DataDoc={undefined} + PanelWidth={panelWidth} + PanelHeight={panelHeight} + NativeWidth={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfWidth : undefined} + NativeHeight={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfHeight : undefined} + fitContentsToDoc={true} + hideTitle={asText} + LayoutTemplateString={asText ? FormattedTextBox.LayoutString("text") : undefined} + focus={asText ? this.refocus : returnFalse} + dontRegisterView={asText ? undefined : this.props.dontRegisterView} + ScreenToLocalTransform={this.docTransform} + renderDepth={this.props.renderDepth + 1} + rootSelected={returnTrue} + styleProvider={asText ? this.titleStyleProvider : this.embeddedStyleProvider} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionDoc={this.props.containingCollection} + ContainingCollectionView={undefined} + addDocument={this.props.addDocument} + moveDocument={this.move} + removeDocument={this.props.removeDoc} + parentActive={this.props.active} + whenActiveChanged={this.props.whenActiveChanged} + addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} + cantBrush={this.props.treeView.props.cantBrush} + bringToFront={returnFalse} + />; + } + + @computed get renderDocumentAsHeader() { + return <> + {this.renderBullet} + {this.renderEmbeddedDocument(true)} + </>; + } + + @computed get renderBorder() { + const sorting = this.doc[`${this.fieldKey}-sortCriteria`]; + return <div className={`treeView-border${this.outlineMode ? "outline" : ""}`} + style={{ borderColor: sorting === undefined ? undefined : sorting === "ascending" ? "crimson" : sorting === "descending" ? "blue" : "green" }}> + {!this.treeViewOpen ? (null) : this.renderContent} + </div>; + } render() { TraceMobx(); - if (this.props.renderedIds.indexOf(this.doc[Id]) !== -1) return null; - const sorting = this.doc[`${this.fieldKey}-sortAscending`]; + if (this.props.renderedIds.indexOf(this.doc[Id]) !== -1) return "<" + this.doc.title + ">"; if (this.showTitleEditorControl) { // find containing CollectionTreeView and set our maximum width so the containing tree view won't have to scroll let par: any = this._header?.current; while (par && par.className !== "collectionTreeView-dropTarget") par = par.parentNode; @@ -551,89 +683,47 @@ export class TreeView extends React.Component<TreeViewProps> { } } else this._editMaxWidth = ""; - const selected = SelectionManager.IsSelected(DocumentManager.Instance.getFirstDocumentView(this.doc)); - return this.doc.treeViewHideHeader || this.outlineMode ? - !StrCast(Doc.LayoutField(this.doc)).includes("CollectionView") ? - this.renderContent - : <div className={`treeView-container${selected ? "-active" : ""}`} ref={this.createTreeDropTarget} onPointerDown={e => this.props.active(true) && SelectionManager.DeselectAll()} - onKeyDown={e => { - e.stopPropagation(); - e.preventDefault(); - switch (e.key) { - case "Backspace": return !(this.doc.text as RichTextField)?.Text && this.props.removeDoc?.(this.doc); - case "Enter": return UndoManager.RunInBatch(() => this.makeTextCollection(), "bullet"); - case "Tab": setTimeout(() => RichTextMenu.Instance.TextView?.EditorView?.focus(), 150); - return UndoManager.RunInBatch(() => e.shiftKey ? this.props.outdentDocument?.() : this.props.indentDocument?.(), "tab"); - } - }} > - <div className={`treeView-header` + (this._editMaxWidth ? "-editing" : "")} ref={this._header} style={{ alignItems: this.outlineMode ? "center" : undefined, maxWidth: this._editMaxWidth }} - onClick={e => { if (this.props.active(true)) { e.stopPropagation(); e.preventDefault(); } }} - onPointerDown={e => { if (this.props.active(true)) { e.stopPropagation(); e.preventDefault(); } }} - onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> - {this.renderBullet} - <div ref={this._dref} style={{ display: "inline-block", height: this.rtfOutlineHeight() }} key={this.doc[Id]}> - <ContentFittingDocumentView - Document={this.doc} - DataDoc={undefined} - LayoutTemplateString={FormattedTextBox.LayoutString("text")} - LibraryPath={emptyPath} - renderDepth={this.props.renderDepth + 1} - rootSelected={returnTrue} - treeViewDoc={undefined} - backgroundColor={this.props.backgroundColor} - fitToBox={this.boundsOfCollectionDocument !== undefined} - PanelWidth={this.rtfWidth} - PanelHeight={this.rtfOutlineHeight} - focus={this.refocus} - ScreenToLocalTransform={this.docTransform} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - ContainingCollectionDoc={this.props.containingCollection} - ContainingCollectionView={undefined} - addDocument={this.props.addDocument} - moveDocument={this.move} - removeDocument={this.props.removeDoc} - parentActive={this.props.active} - whenActiveChanged={this.props.whenActiveChanged} - addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} - bringToFront={returnFalse} - ContentScaling={returnOne} - /> - </div> - </div> - <div className={`treeView-border${this.outlineMode ? "outline" : ""}`} style={{ borderColor: sorting === undefined ? undefined : sorting ? "crimson" : "blue" }}> - {!this.treeViewOpen ? (null) : this.renderContent} - </div> - </div> : - <div className="treeView-container" ref={this.createTreeDropTarget} onPointerDown={e => this.props.active(true) && SelectionManager.DeselectAll()}> - <li className="collection-child"> - <div className={`treeView-header` + (this._editMaxWidth ? "-editing" : "")} ref={this._header} style={{ maxWidth: this._editMaxWidth }} onClick={e => { - if (this.props.active(true)) { - e.stopPropagation(); - e.preventDefault(); - SelectionManager.DeselectAll(); - } - }} - onPointerDown={e => { - if (this.props.active(true)) { - e.stopPropagation(); - e.preventDefault(); - } - }} - onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> - {this.renderBullet} - {this.renderTitle} - </div> - <div className={`treeView-border${this.outlineMode ? "outline" : ""}`} style={{ borderColor: sorting === undefined ? undefined : sorting ? "crimson" : "blue" }}> - {!this.treeViewOpen ? (null) : this.renderContent} - </div> - </li> - </div>; + const hideTitle = this.doc.treeViewHideHeader || this.outlineMode; + return <div className={`treeView-container${this._dref?.contentsActive() ? "-active" : ""}`} + ref={this.createTreeDropTarget} + onPointerDown={e => this.props.active(true) && SelectionManager.DeselectAll()} + onKeyDown={this.onKeyDown}> + <li className="collection-child"> + {hideTitle && this.doc.type !== DocumentType.RTF ? + this.renderEmbeddedDocument(false) : + this.renderBulletHeader(hideTitle ? this.renderDocumentAsHeader : this.renderTitleAsHeader)} + </li> + </div>; } + public static sortDocs(childDocs: Doc[], criterion: string | undefined) { + const docs = childDocs.slice(); + if (criterion) { + const sortAlphaNum = (a: string, b: string): 0 | 1 | -1 => { + const reN = /[0-9]*$/; + const aA = a.replace(reN, ""); // get rid of trailing numbers + const bA = b.replace(reN, ""); + if (aA === bA) { // if header string matches, then compare numbers numerically + const aN = parseInt(a.match(reN)![0], 10); + const bN = parseInt(b.match(reN)![0], 10); + return aN === bN ? 0 : aN > bN ? 1 : -1; + } else { + return aA > bA ? 1 : -1; + } + }; + docs.sort(function (d1, d2): 0 | 1 | -1 { + const a = (criterion === "ascending" ? d2 : d1); + const b = (criterion === "ascending" ? d1 : d2); + const first = a[criterion === "zorder" ? "zIndex" : "title"]; + const second = b[criterion === "zorder" ? "zIndex" : "title"]; + if (typeof first === 'number' && typeof second === 'number') return (first - second) > 0 ? 1 : -1; + if (typeof first === 'string' && typeof second === 'string') return sortAlphaNum(first, second); + return criterion ? 1 : -1; + }); + } + return docs; + } public static GetChildElements( childDocs: Doc[], @@ -649,19 +739,18 @@ export class TreeView extends React.Component<TreeViewProps> { dropAction: dropActionType, addDocTab: (doc: Doc, where: string) => boolean, pinToPres: (document: Doc) => void, - backgroundColor: undefined | ((document: Opt<Doc>, renderDepth: number) => string | undefined), + styleProvider: undefined | StyleProviderFunc, screenToLocalXf: () => Transform, outerXf: () => { translateX: number, translateY: number }, active: (outsideReaction?: boolean) => boolean, panelWidth: () => number, - ChromeHeight: undefined | (() => number), renderDepth: number, treeViewHideHeaderFields: () => boolean, treeViewPreventOpen: boolean, renderedIds: string[], onCheckedClick: undefined | (() => ScriptField), onChildClick: undefined | (() => ScriptField), - ignoreFields: string[] | undefined, + skipFields: string[] | undefined, firstLevel: boolean, whenActiveChanged: (isActive: boolean) => void, dontRegisterView: boolean | undefined) { @@ -670,31 +759,9 @@ export class TreeView extends React.Component<TreeViewProps> { childDocs = childDocs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result); } - const docs = childDocs.slice(); - const ascending = containingCollection?.[key + "-sortAscending"]; - if (ascending !== undefined) { - const sortAlphaNum = (a: string, b: string): 0 | 1 | -1 => { - const reN = /[0-9]*$/; - const aA = a.replace(reN, ""); // get rid of trailing numbers - const bA = b.replace(reN, ""); - if (aA === bA) { // if header string matches, then compare numbers numerically - const aN = parseInt(a.match(reN)![0], 10); - const bN = parseInt(b.match(reN)![0], 10); - return aN === bN ? 0 : aN > bN ? 1 : -1; - } else { - return aA > bA ? 1 : -1; - } - }; - docs.sort(function (a, b): 0 | 1 | -1 { - const first = (ascending ? b : a).title; - const second = (ascending ? a : b).title; - if (typeof first === 'number' && typeof second === 'number') return (first - second) > 0 ? 1 : -1; - if (typeof first === 'string' && typeof second === 'string') return sortAlphaNum(first, second); - return ascending ? 1 : -1; - }); - } + const docs = TreeView.sortDocs(childDocs, StrCast(containingCollection?.[key + "-sortCriteria"])); - const rowWidth = () => panelWidth() - 20; + const rowWidth = () => panelWidth() - treeBulletWidth(); return docs.filter(child => child instanceof Doc).map((child, i) => { const pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, child); if (!pair.layout || pair.data instanceof Promise) { @@ -744,10 +811,9 @@ export class TreeView extends React.Component<TreeViewProps> { renderDepth={renderDepth} removeDoc={StrCast(containingCollection.freezeChildren).includes("remove") ? undefined : remove} addDocument={addDocument} - backgroundColor={backgroundColor} + styleProvider={styleProvider} panelWidth={rowWidth} panelHeight={rowHeight} - ChromeHeight={ChromeHeight} dontRegisterView={dontRegisterView} moveDocument={move} dropAction={dropAction} @@ -760,7 +826,7 @@ export class TreeView extends React.Component<TreeViewProps> { treeViewHideHeaderFields={treeViewHideHeaderFields} treeViewPreventOpen={treeViewPreventOpen} renderedIds={renderedIds} - ignoreFields={ignoreFields} + skipFields={skipFields} firstLevel={firstLevel} whenActiveChanged={whenActiveChanged} />; }); |
