import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import * as CSS from 'csstype'; import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { DivHeight, emptyFunction, returnEmptyDoclist, returnNone, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { CollectionViewType } from '../../documents/DocumentTypes'; import { DragManager, dropActionType } from '../../util/DragManager'; import { SettingsManager } from '../../util/SettingsManager'; import { Transform } from '../../util/Transform'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { EditableView } from '../EditableView'; import { LightboxView } from '../LightboxView'; import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView'; import { DocumentView } from '../nodes/DocumentView'; import { FieldViewProps, FocusViewOptions } from '../nodes/FieldView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { StyleProp } from '../StyleProvider'; import { CollectionMasonryViewFieldRow } from './CollectionMasonryViewFieldRow'; import './CollectionStackingView.scss'; import { CollectionStackingViewFieldColumn } from './CollectionStackingViewFieldColumn'; import { CollectionSubView } from './CollectionSubView'; const _global = (window /* browser */ || global) /* node */ as any; export type collectionStackingViewProps = { sortFunc?: (a: Doc, b: Doc) => number; chromeHidden?: boolean; // view type is stacking type_collection?: CollectionViewType; NativeWidth?: () => number; NativeHeight?: () => number; }; @observer export class CollectionStackingView extends CollectionSubView>() { _disposers: { [key: string]: IReactionDisposer } = {}; _masonryGridRef: HTMLDivElement | null = null; // used in a column dragger, likely due for the masonry grid view. We want to use this _draggerRef = React.createRef(); // keeping track of documents. Updated on internal and external drops. What's the difference? _docXfs: { height: () => number; width: () => number; stackedDocTransform: () => Transform }[] = []; // Doesn't look like this field is being used anywhere. Obsolete? _columnStart: number = 0; @observable _refList: any[] = []; // map of node headers to their heights. Used in Masonry @observable _heightMap = new Map(); // Assuming that this is the current css cursor style @observable _cursor: CSS.Property.Cursor = 'ew-resize'; // gets reset whenever we scroll. Not sure what it is @observable _scroll = 0; // used to force the document decoration to update when scrolling // does this mean whether the browser is hidden? Or is chrome something else entirely? @computed get chromeHidden() { return this._props.chromeHidden || BoolCast(this.layoutDoc.chromeHidden); } // it looks like this gets the column headers that Mehek was showing just now @computed get colHeaderData() { return Cast(this.dataDoc['_' + this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null); } // Still not sure what a pivot is, but it appears that we can actually filter docs somehow? @computed get pivotField() { return StrCast(this.layoutDoc._pivotField); } // filteredChildren is what you want to work with. It's the list of things that you're currently displaying @computed get filteredChildren() { const children = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.hidden).map(pair => pair.layout); if (this._props.sortFunc) children.sort(this._props.sortFunc); return children; } // how much margin we give the header @computed get headerMargin() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin); } @computed get xMargin() { return NumCast(this.layoutDoc._xMargin, Math.max(3, 0.05 * this._props.PanelWidth())); } @computed get yMargin() { return this._props.yPadding || NumCast(this.layoutDoc._yMargin, Math.min(5, 0.05 * this._props.PanelWidth())); } @computed get gridGap() { return NumCast(this.layoutDoc._gridGap, 5); } // are we stacking or masonry? @computed get isStackingView() { return (this._props.type_collection ?? this.layoutDoc._type_collection) === CollectionViewType.Stacking; } // this is the number of StackingViewFieldColumns that we have @computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; } // reveals a button to add a group in masonry view @computed get showAddAGroup() { return this.pivotField && !this.chromeHidden; } // columnWidth handles the margin on the left and right side of the documents @computed get columnWidth() { return Math.min(this._props.PanelWidth() - 2 * this.xMargin, this.isStackingView ? Number.MAX_VALUE : this.layoutDoc._columnWidth === -1 ? this._props.PanelWidth() - 2 * this.xMargin : NumCast(this.layoutDoc._columnWidth, 250)); } @computed get NodeWidth() { return this._props.PanelWidth() - this.gridGap; } constructor(props: any) { super(props); makeObservable(this); if (this.colHeaderData === undefined) { // TODO: what is a layout doc? Is it literally how this document is supposed to be layed out? // here we're making an empty list of column headers (again, what Mehek showed us) this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List(); } } // TODO: plj - these are the children children = (docs: Doc[]) => { //TODO: can somebody explain me to what exactly TraceMobX is? TraceMobx(); // appears that we are going to reset the _docXfs. TODO: what is Xfs? this._docXfs.length = 0; this._renderCount < docs.length && setTimeout(action(() => (this._renderCount = Math.min(docs.length, this._renderCount + 5)))); return docs.map((d, i) => { const height = () => this.getDocHeight(d); const width = () => this.getDocWidth(d); const trans = () => this.getDocTransition(d); // assuming we need to get rowSpan because we might be dealing with many columns. Grid gap makes sense if multiple columns const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); // just getting the style const style = this.isStackingView ? { margin: this.Document._stacking_alignCenter ? 'auto' : undefined, transition: trans(), width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { gridRowEnd: `span ${rowSpan}` }; // So we're choosing whether we're going to render a column or a masonry doc return (
{this.getDisplayDoc(d, width, trans, i)}
); }); }; @action setDocHeight = (key: string, sectionHeight: number) => { this._heightMap.set(key, sectionHeight); }; // is sections that all collections inherit? I think this is how we show the masonry/columns //TODO: this seems important get Sections() { // appears that pivot field IS actually for sorting if (!this.pivotField || this.colHeaderData instanceof Promise) return new Map(); if (this.colHeaderData === undefined) { setTimeout(() => (this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List()), 0); return new Map(); } const colHeaderData = Array.from(this.colHeaderData); const fields = new Map(colHeaderData.map(sh => [sh, []] as [SchemaHeaderField, []])); let changed = false; this.filteredChildren.map(d => { const sectionValue = (d[this.pivotField] ? d[this.pivotField] : `NO ${this.pivotField.toUpperCase()} VALUE`) as object; // the next five lines ensures that floating point rounding errors don't create more than one section -syip const parsed = parseInt(sectionValue.toString()); const castedSectionValue = !isNaN(parsed) ? parsed : sectionValue; // look for if header exists already const existingHeader = colHeaderData.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`)); if (existingHeader) { fields.get(existingHeader)!.push(d); } else { const newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`); fields.set(newSchemaHeader, [d]); colHeaderData.push(newSchemaHeader); changed = true; } }); // remove all empty columns if hideHeadings is set // we will want to have something like this, so that we can hide columns and add them back in if (this.layoutDoc._columnsHideIfEmpty) { Array.from(fields.keys()) .filter(key => !fields.get(key)!.length) .map(header => { fields.delete(header); colHeaderData.splice(colHeaderData.indexOf(header), 1); changed = true; }); } changed && setTimeout( action(() => this.colHeaderData?.splice(0, this.colHeaderData.length, ...colHeaderData)), 0 ); return fields; } componentDidMount() { super.componentDidMount?.(); this._props.setContentViewBox?.(this); // reset section headers when a new filter is inputted this._disposers.pivotField = reaction( () => this.pivotField, () => (this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List()) ); this._disposers.autoHeight = reaction( () => this.layoutDoc._layout_autoHeight, layout_autoHeight => layout_autoHeight && this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : this._refList.reduce((p, r) => p + DivHeight(r), 0))) ); this._disposers.refList = reaction( () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !LightboxView.Contains(this.DocumentView?.()) }), ({ refList, autoHeight }) => { this.observer.disconnect(); if (autoHeight) refList.forEach(r => this.observer.observe(r)); }, { fireImmediately: true } ); } componentWillUnmount() { super.componentWillUnmount(); this.observer.disconnect(); Object.keys(this._disposers).forEach(key => this._disposers[key]()); } isAnyChildContentActive = () => this._props.isAnyChildContentActive(); moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => { return this._props.removeDocument?.(doc) && addDocument?.(doc) ? true : false; }; onChildClickHandler = () => this._props.childClickScript || ScriptCast(this.Document.onChildClick); @computed get onChildDoubleClickHandler() { return () => this._props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); } scrollToBottom = () => { smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight, 'ease'); }; // let's dive in and get the actual document we want to drag/move around focusDocument = (doc: Doc, options: FocusViewOptions) => { Doc.BrushDoc(doc); const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]); if (found) { const top = found.getBoundingClientRect().top; const localTop = this.ScreenToLocalBoxXf().transformPoint(0, top); if (Math.floor(localTop[1]) !== 0) { let focusSpeed = options.zoomTime ?? 500; smoothScroll(focusSpeed, this._mainCont!, localTop[1] + this._mainCont!.scrollTop, options.easeFunc); return focusSpeed; } } return undefined; }; styleProvider = (doc: Doc | undefined, props: Opt, property: string) => { if (property === StyleProp.Opacity && doc) { if (this._props.childOpacity) { return this._props.childOpacity(); } if (this.Document._currentFrame !== undefined) { return CollectionFreeFormDocumentView.getValues(doc, NumCast(this.Document._currentFrame))?.opacity; } } return this._props.styleProvider?.(doc, props, property); }; @undoBatch onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => { if (['Enter'].includes(e.key) && e.ctrlKey) { e.stopPropagation?.(); const below = !e.altKey && e.key !== 'Tab'; const layout_fieldKey = StrCast(fieldProps.fieldKey); const newDoc = Doc.MakeCopy(fieldProps.Document, true); const dataField = fieldProps.Document[Doc.LayoutFieldKey(newDoc)]; newDoc[DocData][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List([]) : undefined; if (layout_fieldKey !== 'layout' && fieldProps.Document[layout_fieldKey] instanceof Doc) { newDoc[layout_fieldKey] = fieldProps.Document[layout_fieldKey]; } newDoc[DocData].text = undefined; FormattedTextBox.SetSelectOnLoad(newDoc); return this.addDocument?.(newDoc); } }; isContentActive = () => (this._props.isContentActive() ? true : this._props.isSelected() === false || this._props.isContentActive() === false ? false : undefined); @observable _renderCount = 5; isChildContentActive = () => this._props.isContentActive?.() === false ? false : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive)) ? true : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false ? false : undefined; isChildButtonContentActive = () => (this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false ? false : undefined); @observable docRefs = new ObservableMap(); childFitWidth = (doc: Doc) => Cast(this.Document.childLayoutFitWidth, 'boolean', this._props.childLayoutFitWidth?.(doc) ?? Cast(doc.layout_fitWidth, 'boolean', null)); // this is what renders the document that you see on the screen // called in Children: this actually adds a document to our children list getDisplayDoc(doc: Doc, width: () => number, trans: () => string, count: number) { const dataDoc = doc.isTemplateDoc || doc.isTemplateForField ? this._props.TemplateDataDocument : undefined; const height = () => this.getDocHeight(doc); const panelHeight = () => (this.isStackingView ? height() : Math.min(height(), this._props.PanelHeight())); const panelWidth = () => (this.isStackingView ? width() : this.columnWidth); const stackedDocTransform = () => this.getDocTransform(doc); this._docXfs.push({ stackedDocTransform, width, height }); return count > this._renderCount ? null : ( r?.ContentDiv && this.docRefs.set(doc, r))} Document={doc} TemplateDataDocument={dataDoc} renderDepth={this._props.renderDepth + 1} PanelWidth={panelWidth} PanelHeight={panelHeight} pointerEvents={this.DocumentView?.()._props.onClickScript?.() ? returnNone : undefined} // if the stack has an onClick, then we don't want the contents to be interactive (see CollectionPileView) styleProvider={this.styleProvider} containerViewPath={this.childContainerViewPath} layout_fitWidth={this.childFitWidth} isContentActive={doc.onClick ? this.isChildButtonContentActive : this.isChildContentActive} onKey={this.onKeyDown} DataTransition={trans} onBrowseClickScript={this._props.onBrowseClickScript} isDocumentActive={this.isContentActive} LayoutTemplate={this._props.childLayoutTemplate} LayoutTemplateString={this._props.childLayoutString} NativeWidth={this._props.childIgnoreNativeSize ? returnZero : this._props.childLayoutFitWidth?.(doc) || (this.childFitWidth(doc) && !Doc.NativeWidth(doc)) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox NativeHeight={this._props.childIgnoreNativeSize ? returnZero : this._props.childLayoutFitWidth?.(doc) || (this.childFitWidth(doc) && !Doc.NativeHeight(doc)) ? height : undefined} dontCenter={this._props.childIgnoreNativeSize ? 'xy' : (StrCast(this.layoutDoc.layout_dontCenter) as any)} dontRegisterView={BoolCast(this.layoutDoc.childDontRegisterViews, this._props.dontRegisterView)} // used to be true if DataDoc existed, but template textboxes won't layout_autoHeight resize if dontRegisterView is set, but they need to. rootSelected={this.rootSelected} layout_showTitle={this._props.childlayout_showTitle} dragAction={(this.layoutDoc.childDragAction ?? this._props.childDragAction) as dropActionType} onClickScript={this.onChildClickHandler} onDoubleClickScript={this.onChildDoubleClickHandler} ScreenToLocalTransform={stackedDocTransform} focus={this.focusDocument} childFilters={this.childDocFilters} hideDecorationTitle={this._props.childHideDecorationTitle} hideResizeHandles={this._props.childHideResizeHandles} childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} xPadding={NumCast(this.layoutDoc._childXPadding, this._props.childXPadding)} yPadding={NumCast(this.layoutDoc._childYPadding, this._props.childYPadding)} addDocument={this._props.addDocument} moveDocument={this._props.moveDocument} removeDocument={this._props.removeDocument} contentPointerEvents={StrCast(this.layoutDoc.childContentPointerEvents) as any} whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged} addDocTab={this._props.addDocTab} pinToPres={this._props.pinToPres} /> ); } getDocTransform(doc: Doc) { const dref = this.docRefs.get(doc); this._scroll; // must be referenced for document decorations to update when the text box container is scrolled const { translateX, translateY } = Utils.GetScreenTransform(dref?.ContentDiv); // the document view may center its contents and if so, will prepend that onto the screenToLocalTansform. so we have to subtract that off return new Transform(-translateX + (dref?.centeringX || 0), -translateY + (dref?.centeringY || 0), 1).scale(this.ScreenToLocalBoxXf().Scale); } getDocWidth(d?: Doc) { if (!d) return 0; const childLayoutDoc = Doc.Layout(d, this._props.childLayoutTemplate?.()); const maxWidth = this.columnWidth / this.numGroupColumns; if (!this.layoutDoc._columnsFill && !this.childFitWidth(childLayoutDoc)) { return Math.min(NumCast(d._width), maxWidth); } return maxWidth; } getDocTransition(d?: Doc) { if (!d) return ''; return StrCast(d.dataTransition); } getDocHeight(d?: Doc) { if (!d || d.hidden) return 0; const childLayoutDoc = Doc.Layout(d, this._props.childLayoutTemplate?.()); const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField ? undefined : this._props.TemplateDataDocument; const maxHeight = (lim => (lim === 0 ? this._props.PanelWidth() : lim === -1 ? 10000 : lim))(NumCast(this.layoutDoc.childLimitHeight, -1)); const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!this.childFitWidth(childLayoutDoc) ? NumCast(d._width) : 0); const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!this.childFitWidth(childLayoutDoc) ? NumCast(d._height) : 0); if (nw && nh) { const colWid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); const docWid = this.layoutDoc._columnsFill ? colWid : Math.min(this.getDocWidth(d), colWid); return Math.min(maxHeight, (docWid * nh) / nw); } const childHeight = NumCast(childLayoutDoc._height); const panelHeight = this.childFitWidth(childLayoutDoc) ? Number.MAX_SAFE_INTEGER : this._props.PanelHeight() - 2 * this.yMargin; return Math.min(childHeight, maxHeight, panelHeight); } // This following three functions must be from the view Mehek showed columnDividerDown = (e: React.PointerEvent) => { runInAction(() => (this._cursor = 'grabbing')); const batch = UndoManager.StartBatch('stacking width'); setupMoveUpEvents( this, e, this.onDividerMove, action(() => { this._cursor = 'ew-resize'; batch.end(); }), emptyFunction ); }; @action onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => { this.layoutDoc._columnWidth = Math.max(10, this.columnWidth + delta[0]); return false; }; @computed get columnDragger() { return (
); } @undoBatch onInternalDrop = (e: Event, de: DragManager.DropEvent) => { // Fairly confident that this is where the swapping of nodes in the various arrays happens const where = [de.x, de.y]; // start at -1 until we're sure we want to add it to the column let dropInd = -1; let dropAfter = 0; if (de.complete.docDragData) { // going to re-add the docs to the _docXFs based on position of where we just dropped this._docXfs.map((cd, i) => { const pos = cd .stackedDocTransform() .inverse() .transformPoint(-2 * this.gridGap, -2 * this.gridGap); const pos1 = cd.stackedDocTransform().inverse().transformPoint(cd.width(), cd.height()); if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && (i === this._docXfs.length - 1 || where[1] < pos1[1])) { dropInd = i; const axis = this.isStackingView ? 1 : 0; dropAfter = where[axis] > (pos[axis] + pos1[axis]) / 2 ? 1 : 0; } }); const oldDocs = this.childDocs.length; if (super.onInternalDrop(e, de)) { // check to see if we actually need anything to the new column of nodes (if droppedDocs != empty) const droppedDocs = this.childDocs.slice().filter((d: Doc, ind: number) => ind >= oldDocs); // if the drop operation adds something to the end of the list, then use that as the new document (may be different than what was dropped e.g., in the case of a button which is dropped but which creates say, a note). const newDocs = droppedDocs.length ? droppedDocs : de.complete.docDragData.droppedDocuments; // if nothing was added to the end of the list, then presumably the dropped documents were already in the list, but possibly got reordered so we use them. const docs = this.childDocList; // still figuring out where to add the document if (docs && newDocs.length) { newDocs.forEach(newdoc => docs.indexOf(newdoc) !== -1 && docs.splice(docs.indexOf(newdoc), 1)); const insertInd = dropInd === -1 ? docs.length : dropInd + dropAfter; const offset = newDocs.reduce((off, ndoc) => (this.filteredChildren.find((fdoc, i) => ndoc === fdoc && i < insertInd) ? off + 1 : off), 0); newDocs.filter(ndoc => docs.indexOf(ndoc) !== -1).forEach(ndoc => docs.splice(docs.indexOf(ndoc), 1)); docs.splice(insertInd - offset, 0, ...newDocs); } return true; } } else if (de.complete.linkDragData?.dragDocument.embedContainer === this.Document && de.complete.linkDragData?.linkDragView?.CollectionFreeFormDocumentView) { const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _layout_fitWidth: true, title: 'dropped annotation' }); if (!this._props.addDocument?.(source)) e.preventDefault(); de.complete.linkDocument = DocUtils.MakeLink(source, de.complete.linkDragData.linkSourceGetAnchor(), { link_relationship: 'doc annotation' }); // TODODO this is where in text links get passed e.stopPropagation(); return true; } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) { return this.internalAnchorAnnoDrop(e, de.complete.annoDragData); } e.preventDefault(); return false; }; @undoBatch internalAnchorAnnoDrop(e: Event, annoDragData: DragManager.AnchorAnnoDragData) { const dropCreator = annoDragData.dropDocCreator; annoDragData.dropDocCreator = (annotationOn: Doc | undefined) => { const dropDoc = dropCreator(annotationOn); return dropDoc || this.Document; }; return true; } /// an item from outside of Dash is being dropped onto this stacking view (e.g, a document from the file system) @undoBatch onExternalDrop = async (e: React.DragEvent): Promise => { const where = [e.clientX, e.clientY]; let targInd = -1; this._docXfs.map((cd, i) => { const pos = cd .stackedDocTransform() .inverse() .transformPoint(-2 * this.gridGap, -2 * this.gridGap); const pos1 = cd.stackedDocTransform().inverse().transformPoint(cd.width(), cd.height()); if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) { targInd = i; } }); super.onExternalDrop(e, {}, (docs: Doc[]) => { if (targInd === -1) { this.addDocument(docs); } else { const childDocs = this.childDocList; if (childDocs) { childDocs.splice(targInd, 0, ...docs); } } }); }; headings = () => Array.from(this.Sections); // what a section looks like if we're in stacking view sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { const key = this.pivotField; let type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined = undefined; if (this.pivotField) { const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]); if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) { type = types[0]; } } return ( ); }; // what a section looks like if we're in masonry. Shouldn't actually need to use this. sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[], first: boolean) => { const key = this.pivotField; let type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined = undefined; const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]); if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) { type = types[0]; } const rows = () => (!this.isStackingView ? 1 : Math.max(1, Math.min(docList.length, Math.floor((this._props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap))))); return ( ); }; /// add a new group category (column) to the active set of note categories. (e.g., if the pivot field is 'transportation', groups might be 'car', 'plane', 'bike', etc) @action addGroup = (value: string) => { if (value && this.colHeaderData) { const schemaHdrField = new SchemaHeaderField(value); this.colHeaderData.push(schemaHdrField); return true; } return false; }; sortFunc = (a: [SchemaHeaderField, Doc[]], b: [SchemaHeaderField, Doc[]]): 1 | -1 => { const descending = StrCast(this.layoutDoc._columnsSort) === 'descending'; const firstEntry = descending ? b : a; const secondEntry = descending ? a : b; return firstEntry[0].heading > secondEntry[0].heading ? 1 : -1; }; onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout if (!e.isPropagationStopped()) { const cm = ContextMenu.Instance; const options = cm.findByDescription('Options...'); const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; optionItems.push({ description: `${this.layoutDoc._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => (this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill), icon: 'plus' }); optionItems.push({ description: `${this.layoutDoc._layout_autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight), icon: 'plus' }); optionItems.push({ description: 'Clear All', event: () => (this.dataDoc[this.fieldKey ?? 'data'] = new List([])), icon: 'times' }); !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' }); } }; // @computed get renderedSections() { TraceMobx(); let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]]; if (this.pivotField) { const entries = Array.from(this.Sections.entries()); sections = this.layoutDoc._columnsSort ? entries.sort(this.sortFunc) : entries; } return sections.map((section, i) => (this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1], i === 0))); } return35 = () => 35; @computed get buttonMenu() { const menuDoc = DocCast(this.layoutDoc.layout_headerButton); return !menuDoc ? null : (
); } @computed get nativeWidth() { return this._props.NativeWidth?.() ?? Doc.NativeWidth(this.layoutDoc); } @computed get nativeHeight() { return this._props.NativeHeight?.() ?? Doc.NativeHeight(this.layoutDoc); } @computed get scaling() { return !this.nativeWidth ? 1 : this._props.PanelHeight() / this.nativeHeight; } @computed get backgroundEvents() { return this._props.isContentActive() === false ? 'none' : undefined; } observer = new _global.ResizeObserver(() => this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : this._refList.reduce((p, r) => p + DivHeight(r), 0)))); onPassiveWheel = (e: WheelEvent) => e.stopPropagation(); _oldWheel: any; render() { TraceMobx(); const editableViewProps = { GetValue: () => '', SetValue: this.addGroup, contents: '+ ADD A GROUP', }; const buttonMenu = this.layoutDoc.layout_headerButton; const noviceExplainer = this.layoutDoc.layout_explainer; return ( <> {buttonMenu || noviceExplainer ? (
{buttonMenu ? this.buttonMenu : null} {Doc.noviceMode && noviceExplainer ?
{StrCast(noviceExplainer)}
: null}
) : null}
{ this._masonryGridRef = ele; this.createDashEventsTarget(ele); //so the whole grid is the drop target? this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); this._oldWheel = ele; // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); }} style={{ overflowY: this.isContentActive() ? 'auto' : 'hidden', background: this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor), pointerEvents: (this._props.pointerEvents?.() as any) ?? this.backgroundEvents, }} onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))} onDrop={this.onExternalDrop.bind(this)} onContextMenu={this.onContextMenu} onWheel={e => this.isContentActive() && e.stopPropagation()}> {this.renderedSections} {!this.showAddAGroup ? null : (
)}
); } }