diff options
| author | Stanley Yip <stanley_yip@brown.edu> | 2020-01-17 15:45:05 -0500 |
|---|---|---|
| committer | Stanley Yip <stanley_yip@brown.edu> | 2020-01-17 15:45:05 -0500 |
| commit | 8667498929fce14295658f89c8787a1a9b1ce468 (patch) | |
| tree | c3d03768bf2b8d23e438479c67141fae43c63e1e /src/client/views/collections/CollectionMulticolumnView.tsx | |
| parent | 4b0a056f1afaf20dea4be64c7b238748d99ad12e (diff) | |
| parent | 9620d149a2051bc9b42a037b1c65b61deebd11fa (diff) | |
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web into pen
Diffstat (limited to 'src/client/views/collections/CollectionMulticolumnView.tsx')
| -rw-r--r-- | src/client/views/collections/CollectionMulticolumnView.tsx | 298 |
1 files changed, 298 insertions, 0 deletions
diff --git a/src/client/views/collections/CollectionMulticolumnView.tsx b/src/client/views/collections/CollectionMulticolumnView.tsx new file mode 100644 index 000000000..955e87f13 --- /dev/null +++ b/src/client/views/collections/CollectionMulticolumnView.tsx @@ -0,0 +1,298 @@ +import { observer } from 'mobx-react'; +import { makeInterface } from '../../../new_fields/Schema'; +import { documentSchema } from '../../../new_fields/documentSchemas'; +import { CollectionSubView } from './CollectionSubView'; +import * as React from "react"; +import { Doc } from '../../../new_fields/Doc'; +import { NumCast, StrCast, BoolCast } from '../../../new_fields/Types'; +import { ContentFittingDocumentView } from './../nodes/ContentFittingDocumentView'; +import { Utils } from '../../../Utils'; +import "./collectionMulticolumnView.scss"; +import { computed, trace } from 'mobx'; +import { Transform } from '../../util/Transform'; + +type MulticolumnDocument = makeInterface<[typeof documentSchema]>; +const MulticolumnDocument = makeInterface(documentSchema); + +interface Unresolved { + target: Doc; + magnitude: number; + unit: string; +} + +interface Resolved { + target: Doc; + pixels: number; +} + +interface LayoutData { + unresolved: Unresolved[]; + numFixed: number; + numRatio: number; + starSum: number; +} + +const resolvedUnits = ["*", "px"]; +const resizerWidth = 2; +const resizerOpacity = 0.4; + +@observer +export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocument) { + + @computed + private get ratioDefinedDocs() { + return this.childLayoutPairs.map(({ layout }) => layout).filter(({ widthUnit }) => StrCast(widthUnit) === "*"); + } + + @computed + private get resolvedLayoutInformation(): LayoutData { + const unresolved: Unresolved[] = []; + let starSum = 0, numFixed = 0, numRatio = 0; + + for (const { layout } of this.childLayoutPairs) { + const unit = StrCast(layout.widthUnit); + const magnitude = NumCast(layout.widthMagnitude); + if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) { + if (unit === "*") { + starSum += magnitude; + numRatio++; + } else { + numFixed++; + } + unresolved.push({ target: layout, magnitude, unit }); + } + // otherwise, the particular configuration entry is ignored and the remaining + // space is allocated as if the document were absent from the configuration list + } + + setTimeout(() => { + const minimum = Math.min(...this.ratioDefinedDocs.map(({ widthMagnitude }) => NumCast(widthMagnitude))); + this.ratioDefinedDocs.forEach(layout => layout.widthMagnitude = NumCast(layout.widthMagnitude) / minimum); + }); + + return { unresolved, numRatio, numFixed, 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?.unresolved.reduce( + (sum, { magnitude, unit }) => sum + (unit === "px" ? magnitude : 0), 0); + } + + /** + * This 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?.unresolved.length; + if (layoutInfoLen > 0 && this.totalFixedAllocation !== undefined) { + return this.props.PanelWidth() - (this.totalFixedAllocation + resizerWidth * (layoutInfoLen - 1)); + } + } + + /** + * This 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 columnUnitLength(): number | undefined { + if (this.resolvedLayoutInformation && this.totalRatioAllocation !== undefined) { + return this.totalRatioAllocation / this.resolvedLayoutInformation.starSum; + } + } + + private getColumnUnitLength = () => this.columnUnitLength; + + private lookupPixels = (layout: Doc): number => { + const columnUnitLength = this.columnUnitLength; + if (columnUnitLength === undefined) { + return 0; // we're still waiting on promises to resolve + } + let width = NumCast(layout.widthMagnitude); + if (StrCast(layout.widthUnit) === "*") { + width *= columnUnitLength; + } + return width; + } + + private lookupIndividualTransform = (layout: Doc) => { + const columnUnitLength = this.columnUnitLength; + if (columnUnitLength === 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) { + const shift = offset; + return this.props.ScreenToLocalTransform().translate(-shift, 0); + } + offset += this.lookupPixels(candidate) + resizerWidth; + } + return Transform.Identity(); // type coersion, this case should never be hit + } + + @computed + private get contents(): JSX.Element[] | null { + trace(); + const { childLayoutPairs } = this; + const { Document, PanelHeight } = this.props; + const collector: JSX.Element[] = []; + for (let i = 0; i < childLayoutPairs.length; i++) { + const { layout } = childLayoutPairs[i]; + collector.push( + <div className={"document-wrapper"}> + <ContentFittingDocumentView + {...this.props} + key={Utils.GenerateGuid()} + Document={layout} + DataDocument={layout.resolvedDataDoc as Doc} + PanelWidth={() => this.lookupPixels(layout)} + PanelHeight={() => PanelHeight() - (BoolCast(Document.showWidthLabels) ? 20 : 0)} + getTransform={() => this.lookupIndividualTransform(layout)} + /> + <WidthLabel + layout={layout} + collectionDoc={Document} + /> + </div>, + <ResizeBar + width={resizerWidth} + key={Utils.GenerateGuid()} + columnUnitLength={this.getColumnUnitLength} + toLeft={layout} + toRight={childLayoutPairs[i + 1]?.layout} + /> + ); + } + collector.pop(); // removes the final extraneous resize bar + return collector; + } + + render(): JSX.Element { + return ( + <div + className={"collectionMulticolumnView_contents"} + ref={this.createDropTarget} + > + {this.contents} + </div> + ); + } + +} + +interface SpacerProps { + width: number; + columnUnitLength(): number | undefined; + toLeft?: Doc; + toRight?: Doc; +} + +interface WidthLabelProps { + layout: Doc; + collectionDoc: Doc; + decimals?: number; +} + +@observer +class WidthLabel extends React.Component<WidthLabelProps> { + + @computed + private get contents() { + const { layout, decimals } = this.props; + const magnitude = NumCast(layout.widthMagnitude).toFixed(decimals ?? 3); + const unit = StrCast(layout.widthUnit); + return <span className={"display"}>{magnitude} {unit}</span>; + } + + render() { + return BoolCast(this.props.collectionDoc.showWidthLabels) ? this.contents : (null); + } + +} + +@observer +class ResizeBar extends React.Component<SpacerProps> { + + private registerResizing = (e: React.PointerEvent<HTMLDivElement>) => { + e.stopPropagation(); + e.preventDefault(); + window.removeEventListener("pointermove", this.onPointerMove); + window.removeEventListener("pointerup", this.onPointerUp); + window.addEventListener("pointermove", this.onPointerMove); + window.addEventListener("pointerup", this.onPointerUp); + } + + private onPointerMove = ({ movementX }: PointerEvent) => { + const { toLeft, toRight, columnUnitLength } = this.props; + const target = movementX > 0 ? toRight : toLeft; + let scale = columnUnitLength(); + if (target && scale) { + const { widthUnit, widthMagnitude } = target; + scale = widthUnit === "*" ? scale : 1; + target.widthMagnitude = NumCast(widthMagnitude) - Math.abs(movementX) / scale; + } + } + + private get isActivated() { + const { toLeft, toRight } = this.props; + if (toLeft && toRight) { + if (StrCast(toLeft.widthUnit) === "px" && StrCast(toRight.widthUnit) === "px") { + return false; + } + return true; + } else if (toLeft) { + if (StrCast(toLeft.widthUnit) === "px") { + return false; + } + return true; + } else if (toRight) { + if (StrCast(toRight.widthUnit) === "px") { + return false; + } + return true; + } + return false; + } + + private onPointerUp = () => { + window.removeEventListener("pointermove", this.onPointerMove); + window.removeEventListener("pointerup", this.onPointerUp); + } + + render() { + return ( + <div + className={"resizer"} + style={{ + width: this.props.width, + opacity: this.isActivated ? resizerOpacity : 0 + }} + onPointerDown={this.registerResizing} + /> + ); + } + +}
\ No newline at end of file |
