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 (