import { action, computed, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../../../fields/Doc'; import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { DragManager, dropActionType } from '../../../util/DragManager'; import { Transform } from '../../../util/Transform'; import { undoBatch } from '../../../util/UndoManager'; import { DocumentView } from '../../nodes/DocumentView'; import { CollectionSubView } from '../CollectionSubView'; import './CollectionMultirowView.scss'; import HeightLabel from './MultirowHeightLabel'; import ResizeBar from './MultirowResizer'; interface HeightSpecifier { magnitude: number; unit: string; } interface LayoutData { heightSpecifiers: HeightSpecifier[]; starSum: number; } export const DimUnit = { Pixel: 'px', Ratio: '*', }; const resolvedUnits = Object.values(DimUnit); const resizerHeight = 8; @observer export class CollectionMultirowView extends CollectionSubView() { constructor(props: any) { super(props); makeObservable(this); } /** * @returns the list of layout documents whose width unit is * *, denoting that it will be displayed with a ratio, not fixed pixel, value */ @computed private get ratioDefinedDocs() { return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout._dimUnit, '*') === DimUnit.Ratio); } @computed private get minimumDim() { const ratioDocs = this.ratioDefinedDocs.filter(layout => layout._dimMagnitude); return ratioDocs.length ? Math.min(...ratioDocs.map(layout => NumCast(layout._dimMagnitude))) : 1; } /** * This loops through all childLayoutPairs and extracts the values for _dimUnit * and _dimUnit, ignoring any that are malformed. Additionally, it then * normalizes the ratio values so that one * value is always 1, with the remaining * values proportionate to that easily readable metric. * @returns the list of the resolved width specifiers (unit and magnitude pairs) * as well as the sum of the * coefficients, i.e. the ratio magnitudes */ @computed private get resolvedLayoutInformation(): LayoutData { let starSum = 0; const heightSpecifiers: HeightSpecifier[] = []; this.childLayoutPairs.map(pair => { const unit = StrCast(pair.layout._dimUnit, '*'); const magnitude = NumCast(pair.layout._dimMagnitude, this.minimumDim); if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) { unit === DimUnit.Ratio && (starSum += magnitude); heightSpecifiers.push({ magnitude, unit }); } /** * Otherwise, the child document is ignored and the remaining * space is allocated as if the document were absent from the child list */ }); /** * Here, since these values are all relative, adjustments during resizing or * manual updating can, though their ratios remain the same, cause the values * themselves to drift toward zero. Thus, whenever we change any of the values, * we normalize everything (dividing by the smallest magnitude). */ // setTimeout(() => { // const { ratioDefinedDocs } = this; // if (this.childLayoutPairs.length) { // const minimum = Math.min(...ratioDefinedDocs.map(layout => NumCast(layout._dimMagnitude, 1))); // if (minimum !== 0) { // ratioDefinedDocs.forEach(layout => layout._dimMagnitude = NumCast(layout._dimMagnitude, 1) / minimum); // } // } // }); return { heightSpecifiers, starSum }; } /** * This returns the total quantity, in pixels, that this * view needs to reserve for child documents that have * (with higher priority) requested a fixed pixel width. * * If the underlying resolvedLayoutInformation returns null * because we're waiting on promises to resolve, this value will be undefined as well. */ @computed private get totalFixedAllocation(): number | undefined { return this.resolvedLayoutInformation?.heightSpecifiers.reduce((sum, { magnitude, unit }) => sum + (unit === DimUnit.Pixel ? magnitude : 0), 0); } /** * @returns the total quantity, in pixels, that this * view needs to reserve for child documents that have * (with lower priority) requested a certain relative proportion of the * remaining pixel width not allocated for fixed widths. * * If the underlying totalFixedAllocation returns undefined * because we're waiting indirectly on promises to resolve, this value will be undefined as well. */ @computed private get totalRatioAllocation(): number | undefined { const layoutInfoLen = this.resolvedLayoutInformation.heightSpecifiers.length; if (layoutInfoLen > 0 && this.totalFixedAllocation !== undefined) { return this._props.PanelHeight() - (this.totalFixedAllocation + resizerHeight * (layoutInfoLen - 1)) - 2 * NumCast(this.Document._yMargin); } } /** * @returns the total quantity, in pixels, that * 1* (relative / star unit) is worth. For example, * if the configuration has three documents, with, respectively, * widths of 2*, 2* and 1*, and the panel width returns 1000px, * this accessor returns 1000 / (2 + 2 + 1), or 200px. * Elsewhere, this is then multiplied by each relative-width * document's (potentially decimal) * count to compute its actual width (400px, 400px and 200px). * * If the underlying totalRatioAllocation or this.resolveLayoutInformation return undefined * because we're waiting indirectly on promises to resolve, this value will be undefined as well. */ @computed private get rowUnitLength(): number | undefined { if (this.resolvedLayoutInformation && this.totalRatioAllocation !== undefined) { return this.totalRatioAllocation / this.resolvedLayoutInformation.starSum; } } /** * This wrapper function exists to prevent mobx from * needlessly rerendering the internal ContentFittingDocumentViews */ private getRowUnitLength = () => this.rowUnitLength; /** * @param layout the document whose transform we'd like to compute * Given a layout document, this function * returns the resolved width it has requested, in pixels. * @returns the stored row width if already in pixels, * or the ratio width evaluated to a pixel value */ private lookupPixels = (layout: Doc): number => { const rowUnitLength = this.rowUnitLength; if (rowUnitLength === undefined) { return 0; // we're still waiting on promises to resolve } let height = NumCast(layout._dimMagnitude, this.minimumDim); if (StrCast(layout._dimUnit, '*') === DimUnit.Ratio) { height *= rowUnitLength; } return height; }; /** * @returns the transform that will correctly place * the document decorations box, shifted to the right by * the sum of all the resolved row widths of the * documents before the target. */ private lookupIndividualTransform = (layout: Doc) => { const rowUnitLength = this.rowUnitLength; if (rowUnitLength === undefined) { return Transform.Identity(); // we're still waiting on promises to resolve } let offset = 0; for (const { layout: candidate } of this.childLayoutPairs) { if (candidate === layout) { return this.ScreenToLocalBoxXf().translate(0, -offset / (this._props.NativeDimScaling?.() || 1)); } offset += this.lookupPixels(candidate) + resizerHeight; } return Transform.Identity(); // type coersion, this case should never be hit }; @undoBatch onInternalDrop = (e: Event, de: DragManager.DropEvent) => { let dropInd = -1; if (de.complete.docDragData && this._mainCont) { let curInd = -1; de.complete.docDragData?.droppedDocuments.forEach(d => (curInd = this.childDocs.indexOf(d))); Array.from(this._mainCont.children).forEach((child, index) => { const brect = child.getBoundingClientRect(); if (brect.y < de.y && brect.y + brect.height > de.y) { if (curInd !== -1 && curInd === Math.floor(index / 2)) { dropInd = curInd; } else if (child.className === 'multiColumnResizer') { dropInd = Math.floor(index / 2); } else { dropInd = Math.ceil(index / 2 + (de.y - brect.y > brect.height / 2 ? 0 : -1)); } } }); if (super.onInternalDrop(e, de)) { de.complete.docDragData?.droppedDocuments.forEach( action((d: Doc) => { d._dimUnit = '*'; d._dimMagnitude = 1; if (dropInd !== curInd || dropInd === -1) { if (this.childDocs.includes(d)) { if (dropInd > this.childDocs.indexOf(d)) dropInd--; } Doc.RemoveDocFromList(this.dataDoc, this._props.fieldKey, d); Doc.AddDocToList(this.dataDoc, this._props.fieldKey, d, DocListCast(this.dataDoc[this._props.fieldKey])[dropInd], undefined, dropInd === -1); } }) ); return true; } } return false; }; onChildClickHandler = () => ScriptCast(this.Document.onChildClick); onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick); isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive(); isChildContentActive = () => { const childDocsActive = this._props.childDocumentsActive?.() ?? this.Document.childDocumentsActive; return this._props.isContentActive?.() === false || childDocsActive === false ? false // : this._props.isDocumentActive?.() && childDocsActive ? true : undefined; }; getDisplayDoc = (layout: Doc) => { const height = () => this.lookupPixels(layout); const width = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc._xMargin) - (BoolCast(this.layoutDoc.showWidthLabels) ? 20 : 0); const dxf = () => this.lookupIndividualTransform(layout) .translate(-NumCast(this.layoutDoc._xMargin), -NumCast(this.layoutDoc._yMargin)) .scale(this._props.NativeDimScaling?.() || 1); const shouldNotScale = () => this._props.fitContentsToBox?.() || BoolCast(layout.freeform_fitContentsToBox); return ( ); }; /** * @returns the resolved list of rendered child documents, displayed * at their resolved pixel widths, each separated by a resizer. */ @computed private get contents(): JSX.Element[] | null { const { childLayoutPairs } = this; const collector: JSX.Element[] = []; for (let i = 0; i < childLayoutPairs.length; i++) { const { layout } = childLayoutPairs[i]; collector.push(
{this.getDisplayDoc(layout)}
, ); } collector.pop(); // removes the final extraneous resize bar return collector; } render() { return (
{this.contents}
); } }