aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/views/CollectionMulticolumnView.scss11
-rw-r--r--src/client/views/CollectionMulticolumnView.tsx171
-rw-r--r--src/server/RouteManager.ts4
3 files changed, 155 insertions, 31 deletions
diff --git a/src/client/views/CollectionMulticolumnView.scss b/src/client/views/CollectionMulticolumnView.scss
index 84e80da4a..1c2389809 100644
--- a/src/client/views/CollectionMulticolumnView.scss
+++ b/src/client/views/CollectionMulticolumnView.scss
@@ -1,7 +1,14 @@
-.collectionMulticolumnView_outer,
.collectionMulticolumnView_contents {
+ display: flex;
width: 100%;
height: 100%;
overflow: hidden;
-}
+ .spacer {
+ width: 2px;
+ background: black;
+ cursor: ew-resize;
+ opacity: 0.2;
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/CollectionMulticolumnView.tsx b/src/client/views/CollectionMulticolumnView.tsx
index 3231c0da1..ded2aa9df 100644
--- a/src/client/views/CollectionMulticolumnView.tsx
+++ b/src/client/views/CollectionMulticolumnView.tsx
@@ -11,10 +11,24 @@ import { ContentFittingDocumentView } from './nodes/ContentFittingDocumentView';
import { Utils } from '../../Utils';
import { Transform } from '../util/Transform';
import "./collectionMulticolumnView.scss";
+import { computed } from 'mobx';
type MulticolumnDocument = makeInterface<[typeof documentSchema]>;
const MulticolumnDocument = makeInterface(documentSchema);
+interface LayoutUnit {
+ config: Doc;
+ target: Doc;
+}
+
+interface Fixed extends LayoutUnit {
+ pixels: number;
+}
+
+interface Proportional extends LayoutUnit {
+ ratio: number;
+}
+
@observer
export default class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocument) {
private _dropDisposer?: DragManager.DragDropDisposer;
@@ -41,37 +55,140 @@ export default class CollectionMulticolumnView extends CollectionSubView(Multico
public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); }
+ @computed
+ private get layoutInformation() {
+ const fixed: Fixed[] = [];
+ const proportional: Proportional[] = [];
+ let ratioSum = 0;
+ for (const config of this.configuration) {
+ const { columnWidth, target } = config;
+ if (!(target instanceof Doc)) {
+ // we're still waiting on promises, so it's not worth rendering anything yet
+ return (null);
+ }
+ const widthSpecifier = Cast(columnWidth, "number");
+ let matches: RegExpExecArray | null;
+ if (widthSpecifier !== undefined) {
+ // we've gotten a number, referring to a pixel value
+ fixed.push({ config, target, pixels: widthSpecifier });
+ } else if ((matches = /^(\d+(\.\d+)?)\*/.exec(StrCast(columnWidth))) !== null) {
+ // we've gotten a proportional measure, like 1.8*
+ const ratio = Number(matches[1]);
+ ratioSum += ratio;
+ proportional.push({ config, target, ratio });
+ }
+ // otherwise, the particular configuration entry is ignored and the remaining
+ // space is allocated as if the document were absent from the configuration list
+ }
+ return { fixed, proportional, ratioSum };
+ }
+
+ @computed private get totalFixedPool() {
+ return this.layoutInformation?.fixed.reduce((sum, unit) => sum + unit.pixels, 0);
+ }
+
+ @computed private get totalProportionalPool() {
+ const { totalFixedPool } = this;
+ return totalFixedPool !== undefined ? this.props.PanelWidth() - totalFixedPool : undefined;
+ }
+
+ @computed private get columnUnitLength() {
+ const layout = this.layoutInformation;
+ const { totalProportionalPool } = this;
+ if (layout !== null && totalProportionalPool !== undefined) {
+ const { ratioSum, proportional } = layout;
+ return (totalProportionalPool - 2 * (proportional.length - 1)) / ratioSum;
+ }
+ return undefined;
+ }
+
+ @computed
+ private get contents(): JSX.Element[] | null {
+ const layout = this.layoutInformation;
+ if (layout === null) {
+ return (null);
+ }
+ const { fixed, proportional } = layout;
+ const { columnUnitLength } = this;
+ if (columnUnitLength === undefined) {
+ return (null);
+ }
+ const { GenerateGuid } = Utils;
+ const toView = ({ target, pixels }: Fixed) =>
+ <ContentFittingDocumentView
+ {...this.props}
+ key={GenerateGuid()}
+ Document={target}
+ DataDocument={undefined}
+ PanelWidth={() => pixels}
+ getTransform={this.props.ScreenToLocalTransform}
+ />;
+ const collector: JSX.Element[] = fixed.map(toView);
+ const resolvedColumns = proportional.map(({ target, ratio, config }) => ({ target, pixels: ratio * columnUnitLength, config }));
+ for (let i = 0; i < resolvedColumns.length; i++) {
+ collector.push(toView(resolvedColumns[i]));
+ collector.push(
+ <MulticolumnSpacer
+ key={GenerateGuid()}
+ columnBaseUnit={columnUnitLength}
+ toLeft={resolvedColumns[i].config}
+ toRight={resolvedColumns[i + 1]?.config}
+ />
+ );
+ }
+ collector.pop();
+ return collector;
+ }
+
render() {
- const { PanelWidth } = this.props;
return (
- <div className={"collectionMulticolumnView_outer"}>
- <div className={"collectionMulticolumnView_contents"}>
- {this.configuration.map(config => {
- const { target, columnWidth } = config;
- if (target instanceof Doc) {
- let computedWidth: number = 0;
- const widthSpecifier = Cast(columnWidth, "number");
- let matches: RegExpExecArray | null;
- if (widthSpecifier !== undefined) {
- computedWidth = widthSpecifier;
- } else if ((matches = /([\d.]+)\%/.exec(StrCast(columnWidth))) !== null) {
- computedWidth = Number(matches[1]) / 100 * PanelWidth();
- }
- return (!computedWidth ? (null) :
- <ContentFittingDocumentView
- {...this.props}
- Document={target}
- DataDocument={undefined}
- PanelWidth={() => computedWidth}
- getTransform={this.props.ScreenToLocalTransform}
- />
- );
- }
- return (null);
- })}
- </div>
+ <div className={"collectionMulticolumnView_contents"}>
+ {this.contents}
</div>
);
}
+}
+
+interface SpacerProps {
+ columnBaseUnit: number;
+ toLeft?: Doc;
+ toRight?: Doc;
+}
+
+class MulticolumnSpacer 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, columnBaseUnit } = this.props;
+ const target = movementX > 0 ? toRight : toLeft;
+ if (target) {
+ let widthSpecifier = Number(StrCast(target.columnWidth).replace("*", ""));
+ widthSpecifier -= Math.abs(movementX) / columnBaseUnit;
+ target.columnWidth = `${widthSpecifier}*`;
+ }
+ }
+
+ private onPointerUp = () => {
+ window.removeEventListener("pointermove", this.onPointerMove);
+ window.removeEventListener("pointerup", this.onPointerUp);
+ }
+
+ render() {
+ return (
+ <div
+ className={"spacer"}
+ onPointerDown={this.registerResizing}
+ />
+ );
+ }
+
} \ No newline at end of file
diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts
index f9ffdaa80..b07aef74d 100644
--- a/src/server/RouteManager.ts
+++ b/src/server/RouteManager.ts
@@ -68,7 +68,7 @@ export default class RouteManager {
console.log('please remove all duplicate routes before continuing');
}
if (malformedCount) {
- console.log(`please ensure all routes adhere to ^\/$|^\/[A-Za-z]+(\/\:[A-Za-z?]+)*$`);
+ console.log(`please ensure all routes adhere to ^\/$|^\/[A-Za-z]+(\/\:[A-Za-z?_]+)*$`);
}
process.exit(1);
} else {
@@ -132,7 +132,7 @@ export default class RouteManager {
} else {
route = subscriber.build;
}
- if (!/^\/$|^\/[A-Za-z]+(\/\:[A-Za-z?]+)*$/g.test(route)) {
+ if (!/^\/$|^\/[A-Za-z]+(\/\:[A-Za-z?_]+)*$/g.test(route)) {
this.failedRegistrations.push({
reason: RegistrationError.Malformed,
route