diff options
Diffstat (limited to 'src/client/views/collections/collectionMulticolumn')
8 files changed, 560 insertions, 62 deletions
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss index f57ba438a..0c74b8ddb 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss @@ -7,6 +7,7 @@ .document-wrapper { display: flex; flex-direction: column; + width: 100%; .label-wrapper { display: flex; @@ -17,13 +18,13 @@ } - .resizer { + .multiColumnResizer { cursor: ew-resize; transition: 0.5s opacity ease; display: flex; flex-direction: column; - .internal { + .multiColumnResizer-hdl { width: 100%; height: 100%; transition: 0.5s background-color ease; diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index 70e56183c..7d8de0db4 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -1,17 +1,18 @@ +import { action, computed } from 'mobx'; import { observer } from 'mobx-react'; -import { makeInterface } from '../../../../new_fields/Schema'; -import { documentSchema } from '../../../../new_fields/documentSchemas'; -import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView'; import * as React from "react"; import { Doc } from '../../../../new_fields/Doc'; -import { NumCast, StrCast, BoolCast } from '../../../../new_fields/Types'; +import { documentSchema } from '../../../../new_fields/documentSchemas'; +import { makeInterface } from '../../../../new_fields/Schema'; +import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../new_fields/Types'; +import { DragManager } from '../../../util/DragManager'; +import { Transform } from '../../../util/Transform'; +import { undoBatch } from '../../../util/UndoManager'; import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView'; -import { Utils } from '../../../../Utils'; +import { CollectionSubView } from '../CollectionSubView'; import "./collectionMulticolumnView.scss"; -import { computed, trace, observable, action } from 'mobx'; -import { Transform } from '../../../util/Transform'; -import WidthLabel from './MulticolumnWidthLabel'; import ResizeBar from './MulticolumnResizer'; +import WidthLabel from './MulticolumnWidthLabel'; type MulticolumnDocument = makeInterface<[typeof documentSchema]>; const MulticolumnDocument = makeInterface(documentSchema); @@ -26,13 +27,13 @@ interface LayoutData { starSum: number; } -export const WidthUnit = { +export const DimUnit = { Pixel: "px", Ratio: "*" }; -const resolvedUnits = Object.values(WidthUnit); -const resizerWidth = 4; +const resolvedUnits = Object.values(DimUnit); +const resizerWidth = 8; @observer export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocument) { @@ -43,12 +44,12 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu */ @computed private get ratioDefinedDocs() { - return this.childLayoutPairs.map(({ layout }) => layout).filter(({ widthUnit }) => StrCast(widthUnit) === WidthUnit.Ratio); + return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout.dimUnit, "*") === DimUnit.Ratio); } /** - * This loops through all childLayoutPairs and extracts the values for widthUnit - * and widthMagnitude, ignoring any that are malformed. Additionally, it then + * This loops through all childLayoutPairs and extracts the values for dimUnit + * and dimMagnitude, 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) @@ -58,11 +59,11 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu private get resolvedLayoutInformation(): LayoutData { let starSum = 0; const widthSpecifiers: WidthSpecifier[] = []; - this.childLayoutPairs.map(({ layout: { widthUnit, widthMagnitude } }) => { - const unit = StrCast(widthUnit); - const magnitude = NumCast(widthMagnitude); + this.childLayoutPairs.map(pair => { + const unit = StrCast(pair.layout.dimUnit, "*"); + const magnitude = NumCast(pair.layout.dimMagnitude, 1); if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) { - (unit === WidthUnit.Ratio) && (starSum += magnitude); + (unit === DimUnit.Ratio) && (starSum += magnitude); widthSpecifiers.push({ magnitude, unit }); } /** @@ -80,9 +81,9 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu setTimeout(() => { const { ratioDefinedDocs } = this; if (this.childLayoutPairs.length) { - const minimum = Math.min(...ratioDefinedDocs.map(({ widthMagnitude }) => NumCast(widthMagnitude))); + const minimum = Math.min(...ratioDefinedDocs.map(doc => NumCast(doc.dimMagnitude, 1))); if (minimum !== 0) { - ratioDefinedDocs.forEach(layout => layout.widthMagnitude = NumCast(layout.widthMagnitude) / minimum); + ratioDefinedDocs.forEach(layout => layout.dimMagnitude = NumCast(layout.dimMagnitude, 1) / minimum, 1); } } }); @@ -101,7 +102,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu @computed private get totalFixedAllocation(): number | undefined { return this.resolvedLayoutInformation?.widthSpecifiers.reduce( - (sum, { magnitude, unit }) => sum + (unit === WidthUnit.Pixel ? magnitude : 0), 0); + (sum, { magnitude, unit }) => sum + (unit === DimUnit.Pixel ? magnitude : 0), 0); } /** @@ -117,7 +118,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu private get totalRatioAllocation(): number | undefined { const layoutInfoLen = this.resolvedLayoutInformation.widthSpecifiers.length; if (layoutInfoLen > 0 && this.totalFixedAllocation !== undefined) { - return this.props.PanelWidth() - (this.totalFixedAllocation + resizerWidth * (layoutInfoLen - 1)); + return this.props.PanelWidth() - (this.totalFixedAllocation + resizerWidth * (layoutInfoLen - 1)) - 2 * NumCast(this.props.Document._xMargin); } } @@ -158,8 +159,8 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu if (columnUnitLength === undefined) { return 0; // we're still waiting on promises to resolve } - let width = NumCast(layout.widthMagnitude); - if (StrCast(layout.widthUnit) === WidthUnit.Ratio) { + let width = NumCast(layout.dimMagnitude, 1); + if (StrCast(layout.dimUnit, "*") === DimUnit.Ratio) { width *= columnUnitLength; } return width; @@ -186,6 +187,34 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu return Transform.Identity(); // type coersion, this case should never be hit } + @undoBatch + @action + drop = (e: Event, de: DragManager.DropEvent) => { + if (super.drop(e, de)) { + de.complete.docDragData?.droppedDocuments.forEach(action((d: Doc) => { + d.dimUnit = "*"; + d.dimMagnitude = 1; + })); + } + return false; + } + + + @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } + + getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) { + return <ContentFittingDocumentView + {...this.props} + Document={layout} + DataDocument={layout.resolvedDataDoc as Doc} + CollectionDoc={this.props.Document} + PanelWidth={width} + PanelHeight={height} + getTransform={dxf} + onClick={this.onChildClickHandler} + renderDepth={this.props.renderDepth + 1} + /> + } /** * @returns the resolved list of rendered child documents, displayed * at their resolved pixel widths, each separated by a resizer. @@ -197,19 +226,14 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu const collector: JSX.Element[] = []; for (let i = 0; i < childLayoutPairs.length; i++) { const { layout } = childLayoutPairs[i]; + const dxf = () => this.lookupIndividualTransform(layout).translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin)); + const width = () => this.lookupPixels(layout); + const height = () => PanelHeight() - 2 * NumCast(Document._yMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0); collector.push( - <div - className={"document-wrapper"} - key={Utils.GenerateGuid()} - > - <ContentFittingDocumentView - {...this.props} - Document={layout} - DataDocument={layout.resolvedDataDoc as Doc} - PanelWidth={() => this.lookupPixels(layout)} - PanelHeight={() => PanelHeight() - (BoolCast(Document.showWidthLabels) ? 20 : 0)} - getTransform={() => this.lookupIndividualTransform(layout)} - /> + <div className={"document-wrapper"} + key={"wrapper" + i} + style={{ width: width() }} > + {this.getDisplayDoc(layout, dxf, width, height)} <WidthLabel layout={layout} collectionDoc={Document} @@ -217,7 +241,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu </div>, <ResizeBar width={resizerWidth} - key={Utils.GenerateGuid()} + key={"resizer" + i} columnUnitLength={this.getColumnUnitLength} toLeft={layout} toRight={childLayoutPairs[i + 1]?.layout} @@ -230,10 +254,11 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu render(): JSX.Element { return ( - <div - className={"collectionMulticolumnView_contents"} - ref={this.createDropTarget} - > + <div className={"collectionMulticolumnView_contents"} + style={{ + marginLeft: NumCast(this.props.Document._xMargin), marginRight: NumCast(this.props.Document._xMargin), + marginTop: NumCast(this.props.Document._yMargin), marginBottom: NumCast(this.props.Document._yMargin) + }} ref={this.createDashEventsTarget}> {this.contents} </div> ); diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss new file mode 100644 index 000000000..64f607680 --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss @@ -0,0 +1,35 @@ +.collectionMultirowView_contents { + display: flex; + width: 100%; + height: 100%; + overflow: hidden; + flex-direction: column; + + .document-wrapper { + display: flex; + flex-direction: row; + height: 100%; + + .label-wrapper { + display: flex; + flex-direction: row; + justify-content: center; + height: 20px; + } + + } + + .multiRowResizer { + cursor: ew-resize; + transition: 0.5s opacity ease; + display: flex; + flex-direction: row; + + .multiRowResizer-hdl { + width: 100%; + height: 100%; + transition: 0.5s background-color ease; + } + } + +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx new file mode 100644 index 000000000..ff7c4998f --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -0,0 +1,269 @@ +import { observer } from 'mobx-react'; +import { makeInterface } from '../../../../new_fields/Schema'; +import { documentSchema } from '../../../../new_fields/documentSchemas'; +import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView'; +import * as React from "react"; +import { Doc } from '../../../../new_fields/Doc'; +import { NumCast, StrCast, BoolCast, ScriptCast } from '../../../../new_fields/Types'; +import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView'; +import { Utils } from '../../../../Utils'; +import "./collectionMultirowView.scss"; +import { computed, trace, observable, action } from 'mobx'; +import { Transform } from '../../../util/Transform'; +import HeightLabel from './MultirowHeightLabel'; +import ResizeBar from './MultirowResizer'; +import { undoBatch } from '../../../util/UndoManager'; +import { DragManager } from '../../../util/DragManager'; + +type MultirowDocument = makeInterface<[typeof documentSchema]>; +const MultirowDocument = makeInterface(documentSchema); + +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(MultirowDocument) { + + /** + * @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); + } + + /** + * 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, 1); + 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.props.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, 1); + 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.props.ScreenToLocalTransform().translate(0, -offset); + } + offset += this.lookupPixels(candidate) + resizerHeight; + } + return Transform.Identity(); // type coersion, this case should never be hit + } + + @undoBatch + @action + drop = (e: Event, de: DragManager.DropEvent) => { + if (super.drop(e, de)) { + de.complete.docDragData?.droppedDocuments.forEach(action((d: Doc) => { + d.dimUnit = "*"; + d.dimMagnitude = 1; + })); + } + return false; + } + + + @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } + + getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) { + return <ContentFittingDocumentView + {...this.props} + Document={layout} + DataDocument={layout.resolvedDataDoc as Doc} + CollectionDoc={this.props.Document} + PanelWidth={width} + PanelHeight={height} + getTransform={dxf} + onClick={this.onChildClickHandler} + renderDepth={this.props.renderDepth + 1} + /> + } + /** + * @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 { Document, PanelWidth } = this.props; + const collector: JSX.Element[] = []; + for (let i = 0; i < childLayoutPairs.length; i++) { + const { layout } = childLayoutPairs[i]; + const dxf = () => this.lookupIndividualTransform(layout).translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin)); + const height = () => this.lookupPixels(layout); + const width = () => PanelWidth() - 2 * NumCast(Document._xMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0); + collector.push( + <div + className={"document-wrapper"} + key={"wrapper" + i} + > + {this.getDisplayDoc(layout, dxf, width, height)} + <HeightLabel + layout={layout} + collectionDoc={Document} + /> + </div>, + <ResizeBar + height={resizerHeight} + key={"resizer" + i} + columnUnitLength={this.getRowUnitLength} + toTop={layout} + toBottom={childLayoutPairs[i + 1]?.layout} + /> + ); + } + collector.pop(); // removes the final extraneous resize bar + return collector; + } + + render(): JSX.Element { + return ( + <div className={"collectionMultirowView_contents"} + style={{ + marginLeft: NumCast(this.props.Document._xMargin), marginRight: NumCast(this.props.Document._xMargin), + marginTop: NumCast(this.props.Document._yMargin), marginBottom: NumCast(this.props.Document._yMargin) + }} ref={this.createDashEventsTarget}> + {this.contents} + </div> + ); + } + +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx index 11e210958..6b89402e6 100644 --- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx +++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; import { observable, action } from "mobx"; import { Doc } from "../../../../new_fields/Doc"; import { NumCast, StrCast } from "../../../../new_fields/Types"; -import { WidthUnit } from "./CollectionMulticolumnView"; +import { DimUnit } from "./CollectionMulticolumnView"; interface ResizerProps { width: number; @@ -46,14 +46,12 @@ export default class ResizeBar extends React.Component<ResizerProps> { const unitLength = columnUnitLength(); if (unitLength) { if (toNarrow) { - const { widthUnit, widthMagnitude } = toNarrow; - const scale = widthUnit === WidthUnit.Ratio ? unitLength : 1; - toNarrow.widthMagnitude = NumCast(widthMagnitude) - Math.abs(movementX) / scale; + const scale = StrCast(toNarrow.dimUnit, "*") === DimUnit.Ratio ? unitLength : 1; + toNarrow.dimMagnitude = Math.max(0.05, NumCast(toNarrow.dimMagnitude, 1) - Math.abs(movementX) / scale); } if (this.resizeMode === ResizeMode.Pinned && toWiden) { - const { widthUnit, widthMagnitude } = toWiden; - const scale = widthUnit === WidthUnit.Ratio ? unitLength : 1; - toWiden.widthMagnitude = NumCast(widthMagnitude) + Math.abs(movementX) / scale; + const scale = StrCast(toWiden.dimUnit, "*") === DimUnit.Ratio ? unitLength : 1; + toWiden.dimMagnitude = Math.max(0.05, NumCast(toWiden.dimMagnitude, 1) + Math.abs(movementX) / scale); } } } @@ -61,17 +59,17 @@ export default class ResizeBar extends React.Component<ResizerProps> { private get isActivated() { const { toLeft, toRight } = this.props; if (toLeft && toRight) { - if (StrCast(toLeft.widthUnit) === WidthUnit.Pixel && StrCast(toRight.widthUnit) === WidthUnit.Pixel) { + if (StrCast(toLeft.dimUnit, "*") === DimUnit.Pixel && StrCast(toRight.dimUnit, "*") === DimUnit.Pixel) { return false; } return true; } else if (toLeft) { - if (StrCast(toLeft.widthUnit) === WidthUnit.Pixel) { + if (StrCast(toLeft.dimUnit, "*") === DimUnit.Pixel) { return false; } return true; } else if (toRight) { - if (StrCast(toRight.widthUnit) === WidthUnit.Pixel) { + if (StrCast(toRight.dimUnit, "*") === DimUnit.Pixel) { return false; } return true; @@ -91,7 +89,7 @@ export default class ResizeBar extends React.Component<ResizerProps> { render() { return ( <div - className={"resizer"} + className={"multiColumnResizer"} style={{ width: this.props.width, opacity: this.isActivated && this.isHoverActive ? resizerOpacity : 0 @@ -100,12 +98,12 @@ export default class ResizeBar extends React.Component<ResizerProps> { onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))} > <div - className={"internal"} + className={"multiColumnResizer-hdl"} onPointerDown={e => this.registerResizing(e, ResizeMode.Pinned)} style={{ backgroundColor: this.resizeMode }} /> <div - className={"internal"} + className={"multiColumnResizer-hdl"} onPointerDown={e => this.registerResizing(e, ResizeMode.Global)} style={{ backgroundColor: this.resizeMode }} /> diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx index b394fed62..5b2054428 100644 --- a/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx +++ b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx @@ -4,7 +4,7 @@ import { computed } from "mobx"; import { Doc } from "../../../../new_fields/Doc"; import { NumCast, StrCast, BoolCast } from "../../../../new_fields/Types"; import { EditableView } from "../../EditableView"; -import { WidthUnit } from "./CollectionMulticolumnView"; +import { DimUnit } from "./CollectionMulticolumnView"; interface WidthLabelProps { layout: Doc; @@ -18,8 +18,8 @@ export default class WidthLabel extends React.Component<WidthLabelProps> { @computed private get contents() { const { layout, decimals } = this.props; - const getUnit = () => StrCast(layout.widthUnit); - const getMagnitude = () => String(+NumCast(layout.widthMagnitude).toFixed(decimals ?? 3)); + const getUnit = () => StrCast(layout.dimUnit); + const getMagnitude = () => String(+NumCast(layout.dimMagnitude).toFixed(decimals ?? 3)); return ( <div className={"label-wrapper"}> <EditableView @@ -27,7 +27,7 @@ export default class WidthLabel extends React.Component<WidthLabelProps> { SetValue={value => { const converted = Number(value); if (!isNaN(converted) && converted > 0) { - layout.widthMagnitude = converted; + layout.dimMagnitude = converted; return true; } return false; @@ -37,8 +37,8 @@ export default class WidthLabel extends React.Component<WidthLabelProps> { <EditableView GetValue={getUnit} SetValue={value => { - if (Object.values(WidthUnit).includes(value)) { - layout.widthUnit = value; + if (Object.values(DimUnit).includes(value)) { + layout.dimUnit = value; return true; } return false; diff --git a/src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx b/src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx new file mode 100644 index 000000000..899577fd5 --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; +import { observer } from "mobx-react"; +import { computed } from "mobx"; +import { Doc } from "../../../../new_fields/Doc"; +import { NumCast, StrCast, BoolCast } from "../../../../new_fields/Types"; +import { EditableView } from "../../EditableView"; +import { DimUnit } from "./CollectionMultirowView"; + +interface HeightLabelProps { + layout: Doc; + collectionDoc: Doc; + decimals?: number; +} + +@observer +export default class HeightLabel extends React.Component<HeightLabelProps> { + + @computed + private get contents() { + const { layout, decimals } = this.props; + const getUnit = () => StrCast(layout.dimUnit); + const getMagnitude = () => String(+NumCast(layout.dimMagnitude).toFixed(decimals ?? 3)); + return ( + <div className={"label-wrapper"}> + <EditableView + GetValue={getMagnitude} + SetValue={value => { + const converted = Number(value); + if (!isNaN(converted) && converted > 0) { + layout.dimMagnitude = converted; + return true; + } + return false; + }} + contents={getMagnitude()} + /> + <EditableView + GetValue={getUnit} + SetValue={value => { + if (Object.values(DimUnit).includes(value)) { + layout.dimUnit = value; + return true; + } + return false; + }} + contents={getUnit()} + /> + </div> + ); + } + + render() { + return BoolCast(this.props.collectionDoc.showHeightLabels) ? this.contents : (null); + } + +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx new file mode 100644 index 000000000..d00939b26 --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx @@ -0,0 +1,114 @@ +import * as React from "react"; +import { observer } from "mobx-react"; +import { observable, action } from "mobx"; +import { Doc } from "../../../../new_fields/Doc"; +import { NumCast, StrCast } from "../../../../new_fields/Types"; +import { DimUnit } from "./CollectionMultirowView"; + +interface ResizerProps { + height: number; + columnUnitLength(): number | undefined; + toTop?: Doc; + toBottom?: Doc; +} + +enum ResizeMode { + Global = "blue", + Pinned = "red", + Undefined = "black" +} + +const resizerOpacity = 1; + +@observer +export default class ResizeBar extends React.Component<ResizerProps> { + @observable private isHoverActive = false; + @observable private isResizingActive = false; + @observable private resizeMode = ResizeMode.Undefined; + + @action + private registerResizing = (e: React.PointerEvent<HTMLDivElement>, mode: ResizeMode) => { + e.stopPropagation(); + e.preventDefault(); + this.resizeMode = mode; + window.removeEventListener("pointermove", this.onPointerMove); + window.removeEventListener("pointerup", this.onPointerUp); + window.addEventListener("pointermove", this.onPointerMove); + window.addEventListener("pointerup", this.onPointerUp); + this.isResizingActive = true; + } + + private onPointerMove = ({ movementY }: PointerEvent) => { + const { toTop: toTop, toBottom: toBottom, columnUnitLength } = this.props; + const movingDown = movementY > 0; + const toNarrow = movingDown ? toBottom : toTop; + const toWiden = movingDown ? toTop : toBottom; + const unitLength = columnUnitLength(); + if (unitLength) { + if (toNarrow) { + const scale = StrCast(toNarrow.dimUnit, "*") === DimUnit.Ratio ? unitLength : 1; + toNarrow.dimMagnitude = Math.max(0.05, NumCast(toNarrow.dimMagnitude, 1) - Math.abs(movementY) / scale); + } + if (this.resizeMode === ResizeMode.Pinned && toWiden) { + const scale = StrCast(toWiden.dimUnit, "*") === DimUnit.Ratio ? unitLength : 1; + toWiden.dimMagnitude = Math.max(0.05, NumCast(toWiden.dimMagnitude, 1) + Math.abs(movementY) / scale); + } + } + } + + private get isActivated() { + const { toTop, toBottom } = this.props; + if (toTop && toBottom) { + if (StrCast(toTop.dimUnit, "*") === DimUnit.Pixel && StrCast(toBottom.dimUnit, "*") === DimUnit.Pixel) { + return false; + } + return true; + } else if (toTop) { + if (StrCast(toTop.dimUnit, "*") === DimUnit.Pixel) { + return false; + } + return true; + } else if (toBottom) { + if (StrCast(toBottom.dimUnit, "*") === DimUnit.Pixel) { + return false; + } + return true; + } + return false; + } + + @action + private onPointerUp = () => { + this.resizeMode = ResizeMode.Undefined; + this.isResizingActive = false; + this.isHoverActive = false; + window.removeEventListener("pointermove", this.onPointerMove); + window.removeEventListener("pointerup", this.onPointerUp); + } + + render() { + return ( + <div + className={"multiRowResizer"} + style={{ + height: this.props.height, + opacity: this.isActivated && this.isHoverActive ? resizerOpacity : 0 + }} + onPointerEnter={action(() => this.isHoverActive = true)} + onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))} + > + <div + className={"multiRowResizer-hdl"} + onPointerDown={e => this.registerResizing(e, ResizeMode.Pinned)} + style={{ backgroundColor: this.resizeMode }} + /> + <div + className={"multiRowResizer-hdl"} + onPointerDown={e => this.registerResizing(e, ResizeMode.Global)} + style={{ backgroundColor: this.resizeMode }} + /> + </div> + ); + } + +}
\ No newline at end of file |
