aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections
diff options
context:
space:
mode:
authorbob <bcz@cs.brown.edu>2020-02-07 13:58:33 -0500
committerbob <bcz@cs.brown.edu>2020-02-07 13:58:33 -0500
commitea5e85e30b83b40135c83025d4f1be1ed188b9b7 (patch)
tree6e28ccee1b6cdcdca2ad5ecb5d49365d0cd28b13 /src/client/views/collections
parentccedb9302632fcdbc75292b90942a34b98bebcee (diff)
parent688f54be8be328d733e05b0781aa8908305e14fa (diff)
Merge branch 'master' into fixinglayoutsyms
Diffstat (limited to 'src/client/views/collections')
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx5
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx68
-rw-r--r--src/client/views/collections/CollectionStackingView.scss1
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx24
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx2
-rw-r--r--src/client/views/collections/CollectionSubView.tsx30
-rw-r--r--src/client/views/collections/CollectionTimeView.scss8
-rw-r--r--src/client/views/collections/CollectionTimeView.tsx152
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx47
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx191
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx61
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss5
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx81
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss5
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx58
-rw-r--r--src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx22
-rw-r--r--src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx22
17 files changed, 480 insertions, 302 deletions
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 0933d5924..00edf71dd 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -13,9 +13,6 @@ import { faCaretLeft, faCaretRight } from '@fortawesome/free-solid-svg-icons';
import { Doc } from '../../../new_fields/Doc';
import { FormattedTextBox } from '../nodes/FormattedTextBox';
-
-
-
type CarouselDocument = makeInterface<[typeof documentSchema,]>;
const CarouselDocument = makeInterface(documentSchema);
@@ -74,7 +71,7 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument)
</>;
}
render() {
- return <div className="collectionCarouselView-outer">
+ return <div className="collectionCarouselView-outer" ref={this.createDashEventsTarget}>
{this.content}
{this.buttons}
</div>;
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index e691788ad..cb413b3e3 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -177,35 +177,30 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
}
@undoBatch
@action
- public static ReplaceRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]): boolean {
- if (!CollectionDockingView.Instance) return false; const instance = CollectionDockingView.Instance;
- const newItemStackConfig = {
- type: 'stack',
- content: [CollectionDockingView.makeDocumentConfig(document, dataDoc, undefined, libraryPath)]
- };
-
- const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout);
-
+ public static ReplaceRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[], addToSplit?: boolean): boolean {
+ if (!CollectionDockingView.Instance) return false;
+ const instance = CollectionDockingView.Instance;
let retVal = false;
if (instance._goldenLayout.root.contentItems[0].isRow) {
retVal = Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" &&
- DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)?.Document.isDisplayPanle) {
- child.contentItems[0].remove();
- child.addChild(newContentItem, undefined, true);
+ DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)?.Document.isDisplayPanel) {
+ const newItemStackConfig = CollectionDockingView.makeDocumentConfig(document, dataDoc, undefined, libraryPath);
+ child.addChild(newItemStackConfig, undefined);
+ !addToSplit && child.contentItems[0].remove();
instance.layoutChanged(document);
return true;
- } else {
- Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => {
- if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)?.Document.isDisplayPanel) {
- child.contentItems[j].remove();
- child.addChild(newContentItem, undefined, true);
- return true;
- }
- return false;
- });
}
- return false;
+ return Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => {
+ if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)?.Document.isDisplayPanel) {
+ const newItemStackConfig = CollectionDockingView.makeDocumentConfig(document, dataDoc, undefined, libraryPath);
+ child.addChild(newItemStackConfig, undefined);
+ !addToSplit && child.contentItems[j].remove();
+ instance.layoutChanged(document);
+ return true;
+ }
+ return false;
+ });
});
}
if (retVal) {
@@ -255,9 +250,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
//
@undoBatch
@action
- public static UseRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]) {
+ public static UseRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[], shiftKey?: boolean) {
document.isDisplayPanel = true;
- if (!CollectionDockingView.ReplaceRightSplit(document, dataDoc, libraryPath)) {
+ if (shiftKey || !CollectionDockingView.ReplaceRightSplit(document, dataDoc, libraryPath, shiftKey)) {
CollectionDockingView.AddRightSplit(document, dataDoc, libraryPath);
}
}
@@ -473,7 +468,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
onPointerDown={e => {
e.preventDefault();
e.stopPropagation();
- DragManager.StartDocumentDrag([dragSpan], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY);
+ const dragData = new DragManager.DocumentDragData([doc]);
+ dragData.dropAction = doc.dropAction === "alias" ? "alias" : doc.dropAction === "copy" ? "copy" : undefined;
+ DragManager.StartDocumentDrag([dragSpan], dragData, e.clientX, e.clientY);
}}>
<FontAwesomeIcon icon="file" size="lg" />
</span>, dragSpan);
@@ -640,7 +637,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
**/
@undoBatch
@action
- public PinDoc(doc: Doc) {
+ public static PinDoc(doc: Doc) {
//add this new doc to props.Document
const curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc;
if (curPres) {
@@ -648,10 +645,23 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
pinDoc.presentationTargetDoc = doc;
Doc.AddDocToList(curPres, "data", pinDoc);
if (!DocumentManager.Instance.getDocumentView(curPres)) {
- this.addDocTab(curPres, undefined, "onRight");
+ CollectionDockingView.AddRightSplit(curPres, undefined);
}
}
}
+ /**
+ * Adds a document to the presentation view
+ **/
+ @undoBatch
+ @action
+ public static UnpinDoc(doc: Doc) {
+ //add this new doc to props.Document
+ const curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc;
+ if (curPres) {
+ const ind = DocListCast(curPres.data).findIndex((val) => Doc.AreProtosEqual(val, doc));
+ ind !== -1 && Doc.RemoveDocFromList(curPres, "data", DocListCast(curPres.data)[ind]);
+ }
+ }
componentDidMount() {
const observer = new _global.ResizeObserver(action((entries: any) => {
@@ -748,7 +758,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
focus={emptyFunction}
backgroundColor={returnEmptyString}
addDocTab={this.addDocTab}
- pinToPres={this.PinDoc}
+ pinToPres={DockedFrameRenderer.PinDoc}
ContainingCollectionView={undefined}
ContainingCollectionDoc={undefined}
zoomToScale={emptyFunction}
@@ -768,4 +778,4 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
}
Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddRightSplit(doc, undefined); });
-Scripting.addGlobal(function useRightSplit(doc: any) { CollectionDockingView.UseRightSplit(doc, undefined); });
+Scripting.addGlobal(function useRightSplit(doc: any, shiftKey?: boolean) { CollectionDockingView.UseRightSplit(doc, undefined, undefined, shiftKey); });
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 843c743db..293dc5414 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -19,6 +19,7 @@
position: absolute;
top: 0;
overflow-y: auto;
+ overflow-x: hidden;
flex-wrap: wrap;
transition: top .5s;
>div {
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 91c7ca76e..9e8600193 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -11,7 +11,6 @@ import { listSpec } from "../../../new_fields/Schema";
import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from "../../../new_fields/Types";
import { emptyFunction, Utils } from "../../../Utils";
-import { DocumentType } from "../../documents/DocumentTypes";
import { DragManager } from "../../util/DragManager";
import { Transform } from "../../util/Transform";
import { undoBatch } from "../../util/UndoManager";
@@ -41,7 +40,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
@computed get sectionFilter() { return StrCast(this.props.Document.sectionFilter); }
@computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.isMinimized).map(pair => pair.layout); }
@computed get xMargin() { return NumCast(this.props.Document._xMargin, 2 * this.gridGap); }
- @computed get yMargin() { return Math.max(this.props.Document.showTitle && !this.props.Document.showTitleHover ? 30 : 0, NumCast(this.props.Document._yMargin, 2 * this.gridGap)); }
+ @computed get yMargin() { return Math.max(this.props.Document.showTitle && !this.props.Document.showTitleHover ? 30 : 0, NumCast(this.props.Document._yMargin, 0)); } // 2 * this.gridGap)); }
@computed get gridGap() { return NumCast(this.props.Document._gridGap, 10); }
@computed get isStackingView() { return BoolCast(this.props.Document.singleColumn, true); }
@computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; }
@@ -52,11 +51,11 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
@computed get NodeWidth() { return this.props.PanelWidth() - this.gridGap; }
- children(docs: Doc[]) {
+ children(docs: Doc[], columns?: number) {
this._docXfs.length = 0;
return docs.map((d, i) => {
- const width = () => Math.min(d._nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
const height = () => this.getDocHeight(d);
+ const width = () => this.widthScale * Math.min(d._nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
const dref = React.createRef<HTMLDivElement>();
const dxf = () => this.getDocTransform(d, dref.current!);
this._docXfs.push({ dxf: dxf, width: width, height: height });
@@ -377,6 +376,16 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
return sections.map(section => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1]));
}
+ @computed get contentScale() {
+ const heightExtra = this.heightPercent > 1 ? this.heightPercent : 1;
+ return Math.max(this.props.Document[WidthSym]() / this.props.PanelWidth(), heightExtra * this.props.Document[HeightSym]() / this.props.PanelHeight());
+ }
+ @computed get widthScale() {
+ return this.heightPercent < 1 ? Math.max(1, this.contentScale) : 1;
+ }
+ @computed get heightPercent() {
+ return this.props.PanelHeight() / this.layoutDoc[HeightSym]();
+ }
render() {
TraceMobx();
const editableViewProps = {
@@ -390,9 +399,10 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
ref={this.createRef}
style={{
overflowY: this.props.active() ? "auto" : "hidden",
- transform: `scale(${Math.min(1, this.props.PanelHeight() / this.layoutDoc[HeightSym]())})`,
- height: `${Math.max(100, 100 * 1 / Math.min(this.props.PanelWidth() / this.layoutDoc[WidthSym](), this.props.PanelHeight() / this.layoutDoc[HeightSym]()))}%`,
- transformOrigin: "top"
+ transform: `scale(${Math.min(1, this.heightPercent)})`,
+ height: `${100 * Math.max(1, this.contentScale)}%`,
+ width: `${100 * this.widthScale}%`,
+ transformOrigin: "top left",
}}
onScroll={action((e: React.UIEvent<HTMLDivElement>) => this._scroll = e.currentTarget.scrollTop)}
onDrop={this.onDrop.bind(this)}
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index b81b1f31d..21982f1ca 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -407,7 +407,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
gridTemplateColumns: singleColumn ? undefined : templatecols,
gridAutoRows: singleColumn ? undefined : "0px"
}}>
- {this.props.parent.children(this.props.docList)}
+ {this.props.parent.children(this.props.docList, uniqueHeadings.length)}
{singleColumn ? (null) : this.props.parent.columnDragger}
</div>
{(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ?
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 9cdd48089..e0e99d635 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -43,6 +43,7 @@ export interface SubCollectionViewProps extends CollectionViewProps {
children?: never | (() => JSX.Element[]) | React.ReactNode;
isAnnotationOverlay?: boolean;
annotationsKey: string;
+ layoutEngine?: () => string;
}
export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
@@ -110,34 +111,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
get childDocs() {
const docs = DocListCast(this.dataField);
const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
- const viewedDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
- const docFilters = Cast(this.props.Document._docFilter, listSpec("string"), []);
- const clusters: { [key: string]: { [value: string]: string } } = {};
- for (let i = 0; i < docFilters.length; i += 3) {
- const [key, value, modifiers] = docFilters.slice(i, i + 3);
- const cluster = clusters[key];
- if (!cluster) {
- const child: { [value: string]: string } = {};
- child[value] = modifiers;
- clusters[key] = child;
- } else {
- cluster[value] = modifiers;
- }
- }
- const filteredDocs = docFilters.length ? viewedDocs.filter(d => {
- for (const key of Object.keys(clusters)) {
- const cluster = clusters[key];
- const satisfiesFacet = Object.keys(cluster).some(inner => {
- const modifier = cluster[inner];
- return (modifier === "x") !== Doc.matchFieldValue(d, key, inner);
- });
- if (!satisfiesFacet) {
- return false;
- }
- }
- return true;
- }) : viewedDocs;
- return filteredDocs;
+ return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
}
@action
diff --git a/src/client/views/collections/CollectionTimeView.scss b/src/client/views/collections/CollectionTimeView.scss
index 3aac2bc75..a5ce73a92 100644
--- a/src/client/views/collections/CollectionTimeView.scss
+++ b/src/client/views/collections/CollectionTimeView.scss
@@ -5,9 +5,17 @@
height: 100%;
width: 100%;
overflow: hidden;
+ .collectionTimeView-backBtn {
+ background: green;
+ display: inline;
+ margin-right: 20px;
+ }
.collectionFreeform-customText {
text-align: left;
}
+ .collectionFreeform-customDiv {
+ position: absolute;
+ }
.collectionTimeView-thumb {
position: absolute;
width: 30px;
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 253dfa890..0c1f93829 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -1,29 +1,34 @@
import { faEdit } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, IReactionDisposer, observable } from "mobx";
+import { action, computed, observable, trace } from "mobx";
import { observer } from "mobx-react";
import { Set } from "typescript-collections";
-import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Doc, DocListCast, Field } from "../../../new_fields/Doc";
import { List } from "../../../new_fields/List";
+import { RichTextField } from "../../../new_fields/RichTextField";
import { listSpec } from "../../../new_fields/Schema";
import { ComputedField, ScriptField } from "../../../new_fields/ScriptField";
-import { Cast, StrCast, NumCast } from "../../../new_fields/Types";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
import { Docs } from "../../documents/Documents";
+import { Scripting } from "../../util/Scripting";
+import { ContextMenu } from "../ContextMenu";
+import { ContextMenuProps } from "../ContextMenuItem";
import { EditableView } from "../EditableView";
import { anchorPoints, Flyout } from "../TemplateMenu";
+import { ViewDefBounds } from "./collectionFreeForm/CollectionFreeFormLayoutEngines";
import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
-import "./CollectionTimeView.scss";
import { CollectionSubView } from "./CollectionSubView";
-import { CollectionTreeView } from "./CollectionTreeView";
+import "./CollectionTimeView.scss";
import React = require("react");
-import { ContextMenu } from "../ContextMenu";
-import { ContextMenuProps } from "../ContextMenuItem";
-import { RichTextField } from "../../../new_fields/RichTextField";
+import { CollectionTreeView } from "./CollectionTreeView";
+import { ObjectField } from "../../../new_fields/ObjectField";
@observer
export class CollectionTimeView extends CollectionSubView(doc => doc) {
+ _changing = false;
+ @observable _layoutEngine = "pivot";
+
componentDidMount() {
- this.props.Document._freeformLayoutEngine = "timeline";
const childDetailed = this.props.Document.childDetailed; // bcz: needs to be here to make sure the childDetailed layout template has been loaded when the first item is clicked;
if (!this.props.Document._facetCollection) {
const facetCollection = Docs.Create.TreeDocument([], { title: "facetFilters", _yMargin: 0, treeViewHideTitle: true });
@@ -31,19 +36,24 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
this.props.Document.excludeFields = new List<string>(["_facetCollection", "_docFilter"]);
const scriptText = "setDocFilter(containingTreeView.target, heading, this.title, checked)";
- const childText = "const alias = getAlias(this); Doc.ApplyTemplateTo(containingCollection.childDetailed, alias, 'layout_detailed'); useRightSplit(alias); ";
+ const childText = "const alias = getAlias(this); Doc.ApplyTemplateTo(containingCollection.childDetailed, alias, 'layout_detailView'); alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); ";
facetCollection.onCheckedClick = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "boolean", checked: "boolean", containingTreeView: Doc.name });
- this.props.Document.onChildClick = ScriptField.MakeScript(childText, { this: Doc.name, heading: "boolean", containingCollection: Doc.name });
+ this.props.Document.onChildClick = ScriptField.MakeScript(childText, { this: Doc.name, heading: "boolean", containingCollection: Doc.name, shiftKey: "boolean" });
this.props.Document._facetCollection = facetCollection;
this.props.Document._fitToBox = true;
}
+ if (!this.props.Document.onViewDefClick) {
+ this.props.Document.onViewDefDivClick = ScriptField.MakeScript("pivotColumnClick(this,payload)", { payload: "any" })
+ }
}
+
bodyPanelWidth = () => this.props.PanelWidth() - this._facetWidth;
getTransform = () => this.props.ScreenToLocalTransform().translate(-this._facetWidth, 0);
@computed get _allFacets() {
const facets = new Set<string>();
this.childDocs.forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key)));
+ Doc.AreProtosEqual(this.dataDoc, this.props.Document) && this.childDocs.forEach(child => Object.keys(child).forEach(key => facets.add(key)));
return facets.toArray();
}
@@ -106,14 +116,13 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
const docItems: ContextMenuProps[] = [];
const keySet: Set<string> = new Set();
- this.childLayoutPairs.map(pair =>
- Array.from(Object.keys(Doc.GetProto(pair.layout))).filter(fieldKey =>
- pair.layout[fieldKey] instanceof RichTextField ||
- typeof (pair.layout[fieldKey]) === "number" ||
- typeof (pair.layout[fieldKey]) === "string").map(fieldKey => keySet.add(fieldKey)));
+ this.childLayoutPairs.map(pair => this._allFacets.filter(fieldKey =>
+ pair.layout[fieldKey] instanceof RichTextField ||
+ typeof (pair.layout[fieldKey]) === "number" ||
+ typeof (pair.layout[fieldKey]) === "string").map(fieldKey => keySet.add(fieldKey)));
keySet.toArray().map(fieldKey =>
- docItems.push({ description: ":" + fieldKey, event: () => this.props.Document.pivotField = fieldKey, icon: "compress-arrows-alt" }));
- docItems.push({ description: ":(null)", event: () => this.props.Document.pivotField = undefined, icon: "compress-arrows-alt" })
+ docItems.push({ description: ":" + fieldKey, event: () => this.props.Document._pivotField = fieldKey, icon: "compress-arrows-alt" }));
+ docItems.push({ description: ":(null)", event: () => this.props.Document._pivotField = undefined, icon: "compress-arrows-alt" })
ContextMenu.Instance.addItem({ description: "Pivot Fields ...", subitems: docItems, icon: "eye" });
const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y);
ContextMenu.Instance.displayMenu(x, y, ":");
@@ -190,14 +199,14 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
document.removeEventListener("pointermove", this.onMidUp);
}
+ layoutEngine = () => this._layoutEngine;
@computed get contents() {
return <div className="collectionTimeView-innards" key="timeline" style={{ width: this.bodyPanelWidth() }}>
- <CollectionFreeFormView {...this.props} ScreenToLocalTransform={this.getTransform} PanelWidth={this.bodyPanelWidth} />
+ <CollectionFreeFormView {...this.props} layoutEngine={this.layoutEngine} ScreenToLocalTransform={this.getTransform} PanelWidth={this.bodyPanelWidth} />
</div>;
}
-
- _changing = false;
- render() {
+ @computed get filterView() {
+ trace();
const facetCollection = Cast(this.props.Document?._facetCollection, Doc, null);
const flyout = (
<div className="collectionTimeView-flyout" style={{ width: `${this._facetWidth}` }}>
@@ -208,68 +217,98 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
</label>)}
</div>
);
+ return <div className="collectionTimeView-treeView" style={{ width: `${this._facetWidth}px`, overflow: this._facetWidth < 15 ? "hidden" : undefined }}>
+ <div className="collectionTimeView-addFacet" style={{ width: `${this._facetWidth}px` }} onPointerDown={e => e.stopPropagation()}>
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}>
+ <div className="collectionTimeView-button">
+ <span className="collectionTimeView-span">Facet Filters</span>
+ <FontAwesomeIcon icon={faEdit} size={"lg"} />
+ </div>
+ </Flyout>
+ </div>
+ <div className="collectionTimeView-tree" key="tree">
+ <CollectionTreeView {...this.props} Document={facetCollection} />
+ </div>
+ </div>;
+ }
+
+ public static SyncTimelineToPresentation(doc: Doc) {
+ const fieldKey = Doc.LayoutFieldKey(doc);
+ doc[fieldKey + "-timelineCur"] = ComputedField.MakeFunction("(curPresentationItem()[this._pivotField || 'year'] || 0)");
+ }
+ specificMenu = (e: React.MouseEvent) => {
+ const layoutItems: ContextMenuProps[] = [];
+ const doc = this.props.Document;
+
+ layoutItems.push({ description: "Force Timeline", event: () => { doc._forceRenderEngine = "timeline" }, icon: "compress-arrows-alt" });
+ layoutItems.push({ description: "Force Pivot", event: () => { doc._forceRenderEngine = "pivot" }, icon: "compress-arrows-alt" });
+ layoutItems.push({ description: "Auto Time/Pivot layout", event: () => { doc._forceRenderEngine = undefined }, icon: "compress-arrows-alt" });
+ layoutItems.push({ description: "Sync with presentation", event: () => CollectionTimeView.SyncTimelineToPresentation(doc), icon: "compress-arrows-alt" });
+
+ ContextMenu.Instance.addItem({ description: "Pivot/Time Options ...", subitems: layoutItems, icon: "eye" });
+ }
+
+ render() {
const newEditableViewProps = {
GetValue: () => "",
SetValue: (value: any) => {
if (value?.length) {
- this.props.Document.pivotField = value;
+ this.props.Document._pivotField = value;
return true;
}
return false;
},
showMenuOnLoad: true,
- contents: ":" + StrCast(this.props.Document.pivotField),
+ contents: ":" + StrCast(this.props.Document._pivotField),
toggle: this.toggleVisibility,
color: "#f1efeb" // this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
};
let nonNumbers = 0;
this.childDocs.map(doc => {
- const num = NumCast(doc[StrCast(this.props.Document.pivotField)], Number(StrCast(doc[StrCast(this.props.Document.pivotField)])));
+ const num = NumCast(doc[StrCast(this.props.Document._pivotField)], Number(StrCast(doc[StrCast(this.props.Document._pivotField)])));
if (Number.isNaN(num)) {
nonNumbers++;
}
});
- const doTimeline = nonNumbers / this.childDocs.length < 0.1;
- if (doTimeline !== (this.props.Document._freeformLayoutEngine === "timeline")) {
+ const forceLayout = StrCast(this.props.Document._forceRenderEngine);
+ const doTimeline = forceLayout ? (forceLayout === "timeline") : nonNumbers / this.childDocs.length < 0.1 && this.props.PanelWidth() / this.props.PanelHeight() > 6;
+ if (doTimeline !== (this._layoutEngine === "timeline")) {
if (!this._changing) {
this._changing = true;
- setTimeout(() => {
- if (nonNumbers / this.childDocs.length > 0.1) {
- this.childDocs.map(child => child.isMinimized = false);
- this.props.Document._freeformLayoutEngine = "pivot";
- } else {
- this.props.Document._freeformLayoutEngine = "timeline";
- }
+ setTimeout(action(() => {
+ this._layoutEngine = doTimeline ? "timeline" : "pivot";
this._changing = false;
- }, 0);
+ }), 0);
}
- return (null);
}
+
+ const facetCollection = Cast(this.props.Document?._facetCollection, Doc, null);
return !facetCollection ? (null) :
- <div className={"collectionTimeView" + (doTimeline ? "" : "-pivot")} style={{ height: `calc(100% - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}>
+ <div className={"collectionTimeView" + (doTimeline ? "" : "-pivot")} onContextMenu={this.specificMenu}
+ style={{ height: `calc(100% - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}>
<div className={"pivotKeyEntry"}>
- <EditableView {...newEditableViewProps} menuCallback={this.menuCallback} />
+ <button className="collectionTimeView-backBtn" style={{ width: 50, height: 20, background: "green" }}
+ onClick={action(() => {
+ let pfilterIndex = NumCast(this.props.Document._pfilterIndex);
+ if (pfilterIndex > 0) {
+ this.props.Document._docFilter = ObjectField.MakeCopy(this.props.Document["_pfilter" + --pfilterIndex] as ObjectField);
+ this.props.Document._pfilterIndex = pfilterIndex;
+ } else {
+ this.props.Document._docFilter = new List([]);
+ }
+ })}>
+ back
+ </button>
+ <EditableView {...newEditableViewProps} display={"inline"} menuCallback={this.menuCallback} />
</div>
{!this.props.isSelected() || this.props.PanelHeight() < 100 ? (null) :
<div className="collectionTimeView-dragger" key="dragger" onPointerDown={this.onPointerDown} style={{ transform: `translate(${this._facetWidth}px, 0px)` }} >
<span title="library View Dragger" style={{ width: "5px", position: "absolute", top: "0" }} />
</div>
}
- <div className="collectionTimeView-treeView" style={{ width: `${this._facetWidth}px`, overflow: this._facetWidth < 15 ? "hidden" : undefined }}>
- <div className="collectionTimeView-addFacet" style={{ width: `${this._facetWidth}px` }} onPointerDown={e => e.stopPropagation()}>
- <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}>
- <div className="collectionTimeView-button">
- <span className="collectionTimeView-span">Facet Filters</span>
- <FontAwesomeIcon icon={faEdit} size={"lg"} />
- </div>
- </Flyout>
- </div>
- <div className="collectionTimeView-tree" key="tree">
- <CollectionTreeView {...this.props} Document={facetCollection} />
- </div>
- </div>
+ {this.filterView}
{this.contents}
{!this.props.isSelected() || !doTimeline ? (null) : <>
<div className="collectionTimeView-thumb-min collectionTimeView-thumb" key="min" onPointerDown={this.onMinDown} />
@@ -278,4 +317,13 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
</>}
</div>;
}
-} \ No newline at end of file
+}
+
+Scripting.addGlobal(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBounds) {
+ let pfilterIndex = NumCast(pivotDoc._pfilterIndex);
+ pivotDoc["_pfilter" + pfilterIndex] = ObjectField.MakeCopy(pivotDoc._docFilter as ObjectField);
+ pivotDoc._pfilterIndex = ++pfilterIndex;
+ pivotDoc._docFilter = new List();
+ (bounds.payload as string[]).map(filterVal =>
+ Doc.setDocFilter(pivotDoc, StrCast(pivotDoc._pivotField), filterVal, "check"));
+}); \ No newline at end of file
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 631870866..09f6a96d7 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -626,6 +626,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
const layoutItems: ContextMenuProps[] = [];
layoutItems.push({ description: (this.props.Document.preventTreeViewOpen ? "Persist" : "Abandon") + "Treeview State", event: () => this.props.Document.preventTreeViewOpen = !this.props.Document.preventTreeViewOpen, icon: "paint-brush" });
layoutItems.push({ description: (this.props.Document.hideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.props.Document.hideHeaderFields = !this.props.Document.hideHeaderFields, icon: "paint-brush" });
+ layoutItems.push({ description: (this.props.Document.treeViewHideTitle ? "Show" : "Hide") + " Title", event: () => this.props.Document.treeViewHideTitle = !this.props.Document.treeViewHideTitle, icon: "paint-brush" });
ContextMenu.Instance.addItem({ description: "Treeview Options ...", subitems: layoutItems, icon: "eye" });
}
ContextMenu.Instance.addItem({
@@ -639,28 +640,46 @@ export class CollectionTreeView extends CollectionSubView(Document) {
d.captions = undefined;
});
});
- const { TextDocument, ImageDocument, CarouselDocument } = Docs.Create;
+ const { TextDocument, ImageDocument, CarouselDocument, TreeDocument } = Docs.Create;
const { Document } = this.props;
const fallbackImg = "http://www.cs.brown.edu/~bcz/face.gif";
- const detailedTemplate = `{ "doc": { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "short_description" } } ] }, { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "year" } } ] }, { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "company" } } ] } ] }, "selection":{"type":"text","anchor":1,"head":1},"storedMarks":[] }`;
+ const detailedTemplate = `{ "doc": { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "year" } } ] }, { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "company" } } ] } ] }, "selection":{"type":"text","anchor":1,"head":1},"storedMarks":[] }`;
const textDoc = TextDocument("", { title: "details", _autoHeight: true });
- const detailedLayout = Docs.Create.StackingDocument([
+ const detailView = Docs.Create.StackingDocument([
CarouselDocument([], { title: "data", _height: 350, _itemIndex: 0, backgroundColor: "#9b9b9b3F" }),
textDoc,
- ], { _chromeStatus: "disabled", title: "detailed layout stack" });
- textDoc.data = new RichTextField(detailedTemplate, "short_description year company");
- detailedLayout.isTemplateDoc = makeTemplate(detailedLayout);
+ TextDocument("", { title: "short_description", _autoHeight: true }),
+ TreeDocument([], { title: "narratives", _height: 75, treeViewHideTitle: true })
+ ], { _chromeStatus: "disabled", _width: 300, _height: 300, _autoHeight: true, title: "detailView" });
+ textDoc.data = new RichTextField(detailedTemplate, "year company");
+ detailView.isTemplateDoc = makeTemplate(detailView);
- const cardLayout = ImageDocument(fallbackImg, { title: "cardLayout", isTemplateDoc: true, isTemplateForField: "hero", }); // this acts like a template doc and a template field ... a little weird, but seems to work?
- cardLayout.proto!.layout = ImageBox.LayoutString("hero");
- cardLayout.showTitle = "title";
- cardLayout.showTitleHover = "titlehover";
- Document.childLayout = cardLayout;
- Document.childDetailed = detailedLayout;
+ const heroView = ImageDocument(fallbackImg, { title: "heroView", isTemplateDoc: true, isTemplateForField: "hero", }); // this acts like a template doc and a template field ... a little weird, but seems to work?
+ heroView.proto!.layout = ImageBox.LayoutString("hero");
+ heroView.showTitle = "title";
+ heroView.showTitleHover = "titlehover";
+
+ Doc.AddDocToList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data",
+ Docs.Create.FontIconDocument({
+ _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias", onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
+ dragFactory: heroView, removeDropProperties: new List<string>(["dropAction"]), title: "hero view", icon: "portrait"
+ }));
+
+ Doc.AddDocToList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data",
+ Docs.Create.FontIconDocument({
+ _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias", onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
+ dragFactory: detailView, removeDropProperties: new List<string>(["dropAction"]), title: "detail view", icon: "file-alt"
+ }));
+
+
+ Document.childLayout = heroView;
+ Document.childDetailed = detailView;
Document._viewType = CollectionViewType.Time;
- Document.pivotField = "company";
+ Document._forceActive = true;
+ Document._pivotField = "company";
+ Document.childDropAction = "alias";
}
});
const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
@@ -684,7 +703,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
}
render() {
- const dropAction = StrCast(this.props.Document._dropAction) as dropActionType;
+ const dropAction = StrCast(this.props.Document.dropAction) as dropActionType;
const addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, false);
const moveDoc = (d: Doc, target: Doc | undefined, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc);
return !this.childDocs ? (null) : (
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 63bcc68e5..95f7794bb 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -1,29 +1,18 @@
-import { Doc, Field, FieldResult, WidthSym, HeightSym } from "../../../../new_fields/Doc";
-import { NumCast, StrCast, Cast, DateCast, BoolCast } from "../../../../new_fields/Types";
+import { Doc, Field, FieldResult } from "../../../../new_fields/Doc";
+import { NumCast, StrCast, Cast } from "../../../../new_fields/Types";
import { ScriptBox } from "../../ScriptBox";
import { CompileScript } from "../../../util/Scripting";
import { ScriptField } from "../../../../new_fields/ScriptField";
import { OverlayView, OverlayElementOptions } from "../../OverlayView";
import { emptyFunction, aggregateBounds } from "../../../../Utils";
import React = require("react");
-import { ObservableMap, runInAction } from "mobx";
import { Id, ToString } from "../../../../new_fields/FieldSymbols";
import { ObjectField } from "../../../../new_fields/ObjectField";
import { RefField } from "../../../../new_fields/RefField";
-interface PivotData {
- type: string;
- text: string;
- x: number;
- y: number;
- zIndex?: number;
- width?: number;
- height?: number;
- fontSize: number;
- color?: string;
-}
-
export interface ViewDefBounds {
+ type: string;
+ text?: string;
x: number;
y: number;
z?: number;
@@ -31,7 +20,10 @@ export interface ViewDefBounds {
width?: number;
height?: number;
transition?: string;
+ fontSize?: number;
highlight?: boolean;
+ color?: string;
+ payload: any;
}
export interface PoolData {
@@ -44,7 +36,6 @@ export interface PoolData {
color?: string,
transition?: string,
highlight?: boolean,
- state?: any
}
export interface ViewDefResult {
@@ -57,49 +48,99 @@ function toLabel(target: FieldResult<Field>) {
return target[ToString]();
}
return String(target);
+}/**
+ * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
+ *
+ * @param {String} text The text to be rendered.
+ * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
+ *
+ * @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
+ */
+function getTextWidth(text: string, font: string): number {
+ // re-use canvas object for better performance
+ var canvas = (getTextWidth as any).canvas || ((getTextWidth as any).canvas = document.createElement("canvas"));
+ var context = canvas.getContext("2d");
+ context.font = font;
+ var metrics = context.measureText(text);
+ return metrics.width;
+}
+
+interface pivotColumn {
+ docs: Doc[],
+ filters: string[]
}
+
export function computePivotLayout(
poolData: Map<string, PoolData>,
pivotDoc: Doc,
childDocs: Doc[],
+ filterDocs: Doc[],
childPairs: { layout: Doc, data?: Doc }[],
panelDim: number[],
- viewDefsToJSX: (views: any) => ViewDefResult[]
+ viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[]
) {
- console.log("PIVOT " + pivotDoc[HeightSym]());
const fieldKey = "data";
- const pivotColumnGroups = new Map<FieldResult<Field>, Doc[]>();
- const fontSize = NumCast(pivotDoc[fieldKey + "-timelineFontSize"], panelDim[1] > 58 ? 20 : Math.max(7, panelDim[1] / 3));
+ const pivotColumnGroups = new Map<FieldResult<Field>, pivotColumn>();
- let maxInColumn = 1;
- const pivotFieldKey = toLabel(pivotDoc.pivotField);
- for (const doc of childDocs) {
+ const pivotFieldKey = toLabel(pivotDoc._pivotField);
+ for (const doc of filterDocs) {
const val = Field.toString(doc[pivotFieldKey] as Field);
if (val) {
- !pivotColumnGroups.get(val) && pivotColumnGroups.set(val, []);
- pivotColumnGroups.get(val)!.push(doc);
- maxInColumn = Math.max(maxInColumn, pivotColumnGroups.get(val)?.length || 0);
+ !pivotColumnGroups.get(val) && pivotColumnGroups.set(val, { docs: [], filters: [val] });
+ pivotColumnGroups.get(val)!.docs.push(doc);
}
}
+ if (pivotColumnGroups.size > 10) {
+ const arrayofKeys = Array.from(pivotColumnGroups.keys());
+ const sortedKeys = arrayofKeys.sort();
+ const clusterSize = Math.ceil(pivotColumnGroups.size / 10);
+ const numClusters = Math.ceil(sortedKeys.length / clusterSize);
+ for (let i = 0; i < numClusters; i++) {
+ for (let j = i * clusterSize + 1; j < Math.min(sortedKeys.length, (i + 1) * clusterSize); j++) {
+ const curgrp = pivotColumnGroups.get(sortedKeys[i * clusterSize])!;
+ const newgrp = pivotColumnGroups.get(sortedKeys[j])!;
+ curgrp.docs.push(...newgrp.docs);
+ curgrp.filters.push(...newgrp.filters);
+ pivotColumnGroups.delete(sortedKeys[j]);
+ }
+ }
+ }
+ const fontSize = NumCast(pivotDoc[fieldKey + "-timelineFontSize"], panelDim[1] > 58 ? 20 : Math.max(7, panelDim[1] / 3));
+ const desc = `${fontSize}px ${getComputedStyle(document.body).fontFamily}`;
+ const textlen = Array.from(pivotColumnGroups.keys()).map(c => getTextWidth(toLabel(c), desc)).reduce((p, c) => Math.max(p, c), 0 as number);
+ const max_text = Math.min(Math.ceil(textlen / 120) * 28, panelDim[1] / 2);
+ let maxInColumn = Array.from(pivotColumnGroups.values()).reduce((p, s) => Math.max(p, s.docs.length), 1);
const colWidth = panelDim[0] / pivotColumnGroups.size;
- const colHeight = panelDim[1];
- const pivotAxisWidth = Math.sqrt(colWidth * colHeight / maxInColumn);
- const numCols = Math.max(Math.round(colWidth / pivotAxisWidth), 1);
+ const colHeight = panelDim[1] - max_text;
+ let numCols = 0;
+ let bestArea = 0;
+ let pivotAxisWidth = 0;
+ for (let i = 1; i < 10; i++) {
+ const numInCol = Math.ceil(maxInColumn / i);
+ const hd = colHeight / numInCol;
+ const wd = colWidth / i;
+ const dim = Math.min(hd, wd);
+ if (dim > bestArea) {
+ bestArea = dim;
+ numCols = i;
+ pivotAxisWidth = dim;
+ }
+ }
const docMap = new Map<Doc, ViewDefBounds>();
- const groupNames: PivotData[] = [];;
+ const groupNames: ViewDefBounds[] = [];
const expander = 1.05;
const gap = .15;
let x = 0;
- let max_text = 60;
- pivotColumnGroups.forEach((val, key) => {
+ const sortedPivotKeys = Array.from(pivotColumnGroups.keys()).sort();
+ sortedPivotKeys.forEach(key => {
+ const val = pivotColumnGroups.get(key)!;
let y = 0;
let xCount = 0;
const text = toLabel(key);
- max_text = Math.max(max_text, Math.min(500, text.length));
groupNames.push({
type: "text",
text,
@@ -107,9 +148,10 @@ export function computePivotLayout(
y: pivotAxisWidth,
width: pivotAxisWidth * expander * numCols,
height: max_text,
- fontSize
+ fontSize,
+ payload: val
});
- for (const doc of val) {
+ for (const doc of val.docs) {
const layoutDoc = Doc.Layout(doc);
let wid = pivotAxisWidth;
let hgt = layoutDoc._nativeWidth ? (NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth)) * pivotAxisWidth : pivotAxisWidth;
@@ -118,10 +160,12 @@ export function computePivotLayout(
wid = layoutDoc._nativeHeight ? (NumCast(layoutDoc._nativeWidth) / NumCast(layoutDoc._nativeHeight)) * pivotAxisWidth : pivotAxisWidth;
}
docMap.set(doc, {
- x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2 + (val.length < numCols ? (numCols - val.length) * pivotAxisWidth / 2 : 0),
+ type: "doc",
+ x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2 + (val.docs.length < numCols ? (numCols - val.docs.length) * pivotAxisWidth / 2 : 0),
y: -y + (pivotAxisWidth - hgt) / 2,
width: wid,
- height: hgt
+ height: hgt,
+ payload: undefined
});
xCount++;
if (xCount >= numCols) {
@@ -132,24 +176,32 @@ export function computePivotLayout(
x += pivotAxisWidth * (numCols * expander + gap);
});
- return normalizeResults(panelDim, max_text, childPairs, docMap, poolData, viewDefsToJSX, groupNames, 0, []);
+ const maxColHeight = pivotAxisWidth * expander * Math.ceil(maxInColumn / numCols);
+ const dividers = sortedPivotKeys.map((key, i) =>
+ ({ type: "div", color: "lightGray", x: i * pivotAxisWidth * (numCols * expander + gap), y: -maxColHeight + pivotAxisWidth, width: pivotAxisWidth * numCols * expander, height: maxColHeight, payload: pivotColumnGroups.get(key)!.filters }));
+ groupNames.push(...dividers);
+ return normalizeResults(panelDim, max_text, childPairs, docMap, poolData, viewDefsToJSX, groupNames, 0, [], childDocs.filter(c => !filterDocs.includes(c)));
}
+function toNumber(val: FieldResult<Field>) {
+ return val === undefined ? undefined : NumCast(val, Number(StrCast(val)));
+}
export function computeTimelineLayout(
poolData: Map<string, PoolData>,
pivotDoc: Doc,
childDocs: Doc[],
+ filterDocs: Doc[],
childPairs: { layout: Doc, data?: Doc }[],
panelDim: number[],
- viewDefsToJSX: (views: any) => ViewDefResult[]
+ viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[]
) {
const fieldKey = "data";
const pivotDateGroups = new Map<number, Doc[]>();
const docMap = new Map<Doc, ViewDefBounds>();
- const groupNames: PivotData[] = [];
- const timelineFieldKey = Field.toString(pivotDoc.pivotField as Field);
- const curTime = Cast(pivotDoc[fieldKey + "-timelineCur"], "number", null);
+ const groupNames: ViewDefBounds[] = [];
+ const timelineFieldKey = Field.toString(pivotDoc._pivotField as Field);
+ const curTime = toNumber(pivotDoc[fieldKey + "-timelineCur"]);
const curTimeSpan = Cast(pivotDoc[fieldKey + "-timelineSpan"], "number", null);
const minTimeReq = curTime === undefined ? Cast(pivotDoc[fieldKey + "-timelineMinReq"], "number", null) : curTimeSpan && (curTime - curTimeSpan);
const maxTimeReq = curTime === undefined ? Cast(pivotDoc[fieldKey + "-timelineMaxReq"], "number", null) : curTimeSpan && (curTime + curTimeSpan);
@@ -161,12 +213,10 @@ export function computeTimelineLayout(
}
let minTime = Number.MAX_VALUE;
- let maxTime = Number.MIN_VALUE;
- childDocs.map(doc => {
+ let maxTime = -Number.MAX_VALUE;
+ filterDocs.map(doc => {
const num = NumCast(doc[timelineFieldKey], Number(StrCast(doc[timelineFieldKey])));
- if (Number.isNaN(num) || (minTimeReq && num < minTimeReq) || (maxTimeReq && num > maxTimeReq)) {
- doc.isMinimized = true;
- } else {
+ if (!(Number.isNaN(num) || (minTimeReq && num < minTimeReq) || (maxTimeReq && num > maxTimeReq))) {
!pivotDateGroups.get(num) && pivotDateGroups.set(num, []);
pivotDateGroups.get(num)!.push(doc);
minTime = Math.min(num, minTime);
@@ -180,8 +230,14 @@ export function computeTimelineLayout(
minTime = curTime - (maxTime - curTime);
}
}
- pivotDoc[fieldKey + "-timelineMin"] = minTime = minTimeReq ? Math.min(minTimeReq, minTime) : minTime;
- pivotDoc[fieldKey + "-timelineMax"] = maxTime = maxTimeReq ? Math.max(maxTimeReq, maxTime) : maxTime;
+ setTimeout(() => {
+ pivotDoc[fieldKey + "-timelineMin"] = minTime = minTimeReq ? Math.min(minTimeReq, minTime) : minTime;
+ pivotDoc[fieldKey + "-timelineMax"] = maxTime = maxTimeReq ? Math.max(maxTimeReq, maxTime) : maxTime;
+ }, 0);
+
+ if (maxTime === minTime) {
+ maxTime = minTime + 1;
+ }
const arrayofKeys = Array.from(pivotDateGroups.keys());
const sortedKeys = arrayofKeys.sort((n1, n2) => n1 - n2);
@@ -190,10 +246,10 @@ export function computeTimelineLayout(
let prevKey = Math.floor(minTime);
if (sortedKeys.length && scaling * (sortedKeys[0] - prevKey) > 25) {
- groupNames.push({ type: "text", text: prevKey.toString(), x: x, y: 0, height: fontHeight, fontSize });
+ groupNames.push({ type: "text", text: prevKey.toString(), x: x, y: 0, height: fontHeight, fontSize, payload: undefined });
}
if (!sortedKeys.length && curTime !== undefined) {
- groupNames.push({ type: "text", text: curTime.toString(), x: (curTime - minTime) * scaling, zIndex: 1000, color: "orange", y: 0, height: fontHeight, fontSize });
+ groupNames.push({ type: "text", text: curTime.toString(), x: (curTime - minTime) * scaling, zIndex: 1000, color: "orange", y: 0, height: fontHeight, fontSize, payload: undefined });
}
const pivotAxisWidth = NumCast(pivotDoc.pivotTimeWidth, panelDim[1] / 2.5);
@@ -201,26 +257,27 @@ export function computeTimelineLayout(
let zind = 0;
sortedKeys.forEach(key => {
if (curTime !== undefined && curTime > prevKey && curTime <= key) {
- groupNames.push({ type: "text", text: curTime.toString(), x: (curTime - minTime) * scaling, y: 0, zIndex: 1000, color: "orange", height: fontHeight, fontSize });
+ groupNames.push({ type: "text", text: curTime.toString(), x: (curTime - minTime) * scaling, y: 0, zIndex: 1000, color: "orange", height: fontHeight, fontSize, payload: key });
}
const keyDocs = pivotDateGroups.get(key)!;
- keyDocs.forEach(d => d.isMinimized = false);
x += scaling * (key - prevKey);
const stack = findStack(x, stacking);
prevKey = key;
- !stack && (curTime === undefined || Math.abs(x - (curTime - minTime) * scaling) > pivotAxisWidth) && groupNames.push({ type: "text", text: key.toString(), x: x, y: stack * 25, height: fontHeight, fontSize });
+ if (!stack && (curTime === undefined || Math.abs(x - (curTime - minTime) * scaling) > pivotAxisWidth)) {
+ groupNames.push({ type: "text", text: key.toString(), x: x, y: stack * 25, height: fontHeight, fontSize, payload: undefined });
+ }
layoutDocsAtTime(keyDocs, key);
});
- if (sortedKeys.length && curTime > sortedKeys[sortedKeys.length - 1]) {
+ if (sortedKeys.length && curTime !== undefined && curTime > sortedKeys[sortedKeys.length - 1]) {
x = (curTime - minTime) * scaling;
- groupNames.push({ type: "text", text: curTime.toString(), x: x, y: 0, zIndex: 1000, color: "orange", height: fontHeight, fontSize });
+ groupNames.push({ type: "text", text: curTime.toString(), x: x, y: 0, zIndex: 1000, color: "orange", height: fontHeight, fontSize, payload: undefined });
}
if (Math.ceil(maxTime - minTime) * scaling > x + 25) {
- groupNames.push({ type: "text", text: Math.ceil(maxTime).toString(), x: Math.ceil(maxTime - minTime) * scaling, y: 0, height: fontHeight, fontSize });
+ groupNames.push({ type: "text", text: Math.ceil(maxTime).toString(), x: Math.ceil(maxTime - minTime) * scaling, y: 0, height: fontHeight, fontSize, payload: undefined });
}
- const divider = { type: "div", color: "black", x: 0, y: 0, width: panelDim[0], height: 1 } as any;
- return normalizeResults(panelDim, fontHeight, childPairs, docMap, poolData, viewDefsToJSX, groupNames, (maxTime - minTime) * scaling, [divider]);
+ const divider = { type: "div", color: "black", x: 0, y: 0, width: panelDim[0], height: 1, payload: undefined };
+ return normalizeResults(panelDim, fontHeight, childPairs, docMap, poolData, viewDefsToJSX, groupNames, (maxTime - minTime) * scaling, [divider], childDocs.filter(c => !filterDocs.includes(c)));
function layoutDocsAtTime(keyDocs: Doc[], key: number) {
keyDocs.forEach(doc => {
@@ -233,8 +290,9 @@ export function computeTimelineLayout(
wid = layoutDoc._nativeHeight ? (NumCast(layoutDoc._nativeWidth) / NumCast(layoutDoc._nativeHeight)) * pivotAxisWidth : pivotAxisWidth;
}
docMap.set(doc, {
+ type: "doc",
x: x, y: -Math.sqrt(stack) * pivotAxisWidth / 2 - pivotAxisWidth + (pivotAxisWidth - hgt) / 2,
- zIndex: (curTime === key ? 1000 : zind++), highlight: curTime === key, width: wid / (Math.max(stack, 1)), height: hgt
+ zIndex: (curTime === key ? 1000 : zind++), highlight: curTime === key, width: wid / (Math.max(stack, 1)), height: hgt, payload: undefined
});
stacking[stack] = x + pivotAxisWidth;
});
@@ -242,17 +300,18 @@ export function computeTimelineLayout(
}
function normalizeResults(panelDim: number[], fontHeight: number, childPairs: { data?: Doc, layout: Doc }[], docMap: Map<Doc, ViewDefBounds>,
- poolData: Map<string, PoolData>, viewDefsToJSX: (views: any) => ViewDefResult[], groupNames: PivotData[], minWidth: number, extras: PivotData[]) {
+ poolData: Map<string, PoolData>, viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], groupNames: ViewDefBounds[], minWidth: number, extras: ViewDefBounds[],
+ extraDocs: Doc[]) {
- const grpEles = groupNames.map(gn => ({ x: gn.x, y: gn.y, height: gn.height }) as PivotData);
- const docEles = childPairs.filter(d => !d.layout.isMinimized).map(pair => docMap.get(pair.layout) as PivotData);
+ const grpEles = groupNames.map(gn => ({ x: gn.x, y: gn.y, width: gn.width, height: gn.height }) as ViewDefBounds);
+ const docEles = childPairs.filter(d => docMap.get(d.layout)).map(pair => docMap.get(pair.layout) as ViewDefBounds);
const aggBounds = aggregateBounds(docEles.concat(grpEles), 0, 0);
aggBounds.r = Math.max(minWidth, aggBounds.r - aggBounds.x);
const wscale = panelDim[0] / (aggBounds.r - aggBounds.x);
let scale = wscale * (aggBounds.b - aggBounds.y) > panelDim[1] ? (panelDim[1]) / (aggBounds.b - aggBounds.y) : wscale;
if (Number.isNaN(scale)) scale = 1;
- childPairs.filter(d => !d.layout.isMinimized).map(pair => {
+ childPairs.filter(d => docMap.get(d.layout)).map(pair => {
const newPosRaw = docMap.get(pair.layout);
if (newPosRaw) {
const newPos = {
@@ -267,6 +326,7 @@ function normalizeResults(panelDim: number[], fontHeight: number, childPairs: {
poolData.set(pair.layout[Id], { transition: "transform 1s", ...newPos });
}
});
+ extraDocs.map(ed => poolData.set(ed[Id], { x: 0, y: 0, zIndex: -99 }));
return {
elements: viewDefsToJSX(extras.concat(groupNames.map(gname => ({
@@ -277,7 +337,8 @@ function normalizeResults(panelDim: number[], fontHeight: number, childPairs: {
color: gname.color,
width: gname.width === undefined ? undefined : gname.width * scale,
height: Math.max(fontHeight, (gname.height || 0) * scale),
- fontSize: gname.fontSize
+ fontSize: gname.fontSize,
+ payload: gname.payload
}))))
};
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 63544a637..2518a4a55 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -7,7 +7,7 @@ import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync, Field } f
import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas";
import { Id } from "../../../../new_fields/FieldSymbols";
import { InkTool, InkField, InkData } from "../../../../new_fields/InkField";
-import { createSchema, makeInterface } from "../../../../new_fields/Schema";
+import { createSchema, makeInterface, listSpec } from "../../../../new_fields/Schema";
import { ScriptField } from "../../../../new_fields/ScriptField";
import { BoolCast, Cast, DateCast, NumCast, StrCast, ScriptCast } from "../../../../new_fields/Types";
import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
@@ -32,7 +32,7 @@ import { FormattedTextBox } from "../../nodes/FormattedTextBox";
import { pageSchema } from "../../nodes/ImageBox";
import PDFMenu from "../../pdf/PDFMenu";
import { CollectionSubView } from "../CollectionSubView";
-import { computePivotLayout, ViewDefResult, computeTimelineLayout, PoolData } from "./CollectionFreeFormLayoutEngines";
+import { computePivotLayout, ViewDefResult, computeTimelineLayout, PoolData, ViewDefBounds } from "./CollectionFreeFormLayoutEngines";
import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
import "./CollectionFreeFormView.scss";
import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
@@ -126,6 +126,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@undoBatch
@action
drop = (e: Event, de: DragManager.DropEvent) => {
+ if (this.props.Document.isBackground) return false;
const xf = this.getTransform();
const xfo = this.getTransformOverlay();
const [xp, yp] = xf.transformPoint(de.x, de.y);
@@ -434,13 +435,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
let y = this.Document._panY || 0;
const docs = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.isMinimized).map(pair => pair.layout);
const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
- if (!this.isAnnotationOverlay && docs.length) {
+ if (!this.isAnnotationOverlay && docs.length && this.childDataProvider(docs[0])) {
PDFMenu.Instance.fadeOut(true);
const minx = this.childDataProvider(docs[0]).x;//docs.length ? NumCast(docs[0].x) : 0;
const miny = this.childDataProvider(docs[0]).y;//docs.length ? NumCast(docs[0].y) : 0;
const maxx = this.childDataProvider(docs[0]).width + minx;//docs.length ? NumCast(docs[0].width) + minx : minx;
const maxy = this.childDataProvider(docs[0]).height + miny;//docs.length ? NumCast(docs[0].height) + miny : miny;
- const ranges = docs.filter(doc => doc).reduce((range, doc) => {
+ const ranges = docs.filter(doc => doc).filter(doc => this.childDataProvider(doc)).reduce((range, doc) => {
const x = this.childDataProvider(doc).x;//NumCast(doc.x);
const y = this.childDataProvider(doc).y;//NumCast(doc.y);
const xe = this.childDataProvider(doc).width + x;//x + NumCast(layoutDoc.width);
@@ -747,11 +748,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
};
}
- viewDefsToJSX = (views: any[]) => {
+ viewDefsToJSX = (views: ViewDefBounds[]) => {
return !Array.isArray(views) ? [] : views.filter(ele => this.viewDefToJSX(ele)).map(ele => this.viewDefToJSX(ele)!);
}
- private viewDefToJSX(viewDef: any): Opt<ViewDefResult> {
+ onViewDefDivClick = (e: React.MouseEvent, payload: any) => {
+ (this.props.Document.onViewDefDivClick as ScriptField)?.script.run({ this: this.props.Document, payload });
+ }
+ private viewDefToJSX(viewDef: ViewDefBounds): Opt<ViewDefResult> {
const x = Cast(viewDef.x, "number");
const y = Cast(viewDef.y, "number");
const z = Cast(viewDef.z, "number");
@@ -769,15 +773,15 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
style={{ width, height, color, fontSize, transform: `translate(${x}px, ${y}px)` }}>
{text}
</div>,
- bounds: { x: x!, y: y!, z, zIndex, width, height }
+ bounds: viewDef
};
} else if (viewDef.type === "div") {
const backgroundColor = Cast(viewDef.color, "string");
return [x, y].some(val => val === undefined) ? undefined :
{
- ele: <div className="collectionFreeform-customDiv" key={"div" + x + y + z}
+ ele: <div className="collectionFreeform-customDiv" title={viewDef.payload?.join(" ")} key={"div" + x + y + z} onClick={e => this.onViewDefDivClick(e, viewDef)}
style={{ width, height, backgroundColor, transform: `translate(${x}px, ${y}px)` }} />,
- bounds: { x: x!, y: y!, z, zIndex, width, height }
+ bounds: viewDef
};
}
}
@@ -790,12 +794,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}.bind(this));
doTimelineLayout(poolData: Map<string, any>) {
- return computeTimelineLayout(poolData, this.props.Document, this.childDocs,
+ return computeTimelineLayout(poolData, this.props.Document, this.childDocs, this.filterDocs,
this.childLayoutPairs, [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX);
}
doPivotLayout(poolData: Map<string, any>) {
- return computePivotLayout(poolData, this.props.Document, this.childDocs,
+ return computePivotLayout(poolData, this.props.Document, this.childDocs, this.filterDocs,
this.childLayoutPairs, [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX);
}
@@ -808,7 +812,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => {
const pos = this.getCalculatedPositions({ doc: pair.layout, index: i, collection: this.Document, docs: layoutDocs, state });
- state = pos.state === undefined ? state : pos.state;
poolData.set(pair.layout[Id], pos);
});
return { elements: elements };
@@ -816,12 +819,42 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@computed get doInternalLayoutComputation() {
const newPool = new Map<string, any>();
- switch (this.Document._freeformLayoutEngine) {
+ switch (this.props.layoutEngine?.()) {
case "timeline": return { newPool, computedElementData: this.doTimelineLayout(newPool) };
case "pivot": return { newPool, computedElementData: this.doPivotLayout(newPool) };
}
return { newPool, computedElementData: this.doFreeformLayout(newPool) };
}
+
+ @computed get filterDocs() {
+ const docFilters = Cast(this.props.Document._docFilter, listSpec("string"), []);
+ const clusters: { [key: string]: { [value: string]: string } } = {};
+ for (let i = 0; i < docFilters.length; i += 3) {
+ const [key, value, modifiers] = docFilters.slice(i, i + 3);
+ const cluster = clusters[key];
+ if (!cluster) {
+ const child: { [value: string]: string } = {};
+ child[value] = modifiers;
+ clusters[key] = child;
+ } else {
+ cluster[value] = modifiers;
+ }
+ }
+ const filteredDocs = docFilters.length ? this.childDocs.filter(d => {
+ for (const key of Object.keys(clusters)) {
+ const cluster = clusters[key];
+ const satisfiesFacet = Object.keys(cluster).some(inner => {
+ const modifier = cluster[inner];
+ return (modifier === "x") !== Doc.matchFieldValue(d, key, inner);
+ });
+ if (!satisfiesFacet) {
+ return false;
+ }
+ }
+ return true;
+ }) : this.childDocs;
+ return filteredDocs;
+ }
get doLayoutComputation() {
const { newPool, computedElementData } = this.doInternalLayoutComputation;
runInAction(() =>
@@ -839,7 +872,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} {...this.getChildDocumentViewProps(pair.layout, pair.data)}
dataProvider={this.childDataProvider}
jitterRotation={NumCast(this.props.Document.jitterRotation)}
- fitToBox={this.props.fitToBox || this.Document._freeformLayoutEngine !== undefined} />,
+ fitToBox={this.props.fitToBox || this.props.layoutEngine !== undefined} />,
bounds: this.childDataProvider(pair.layout)
}));
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 65862f34f..7d8de0db4 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -1,19 +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, ScriptCast } 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 { undoBatch } from '../../../util/UndoManager';
-import { DragManager } from '../../../util/DragManager';
+import WidthLabel from './MulticolumnWidthLabel';
type MulticolumnDocument = makeInterface<[typeof documentSchema]>;
const MulticolumnDocument = makeInterface(documentSchema);
@@ -34,7 +33,7 @@ export const DimUnit = {
};
const resolvedUnits = Object.values(DimUnit);
-const resizerWidth = 4;
+const resizerWidth = 8;
@observer
export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocument) {
@@ -45,7 +44,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
*/
@computed
private get ratioDefinedDocs() {
- return this.childLayoutPairs.map(({ layout }) => layout).filter(({ dimUnit }) => StrCast(dimUnit) === DimUnit.Ratio);
+ return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout.dimUnit, "*") === DimUnit.Ratio);
}
/**
@@ -60,9 +59,9 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
private get resolvedLayoutInformation(): LayoutData {
let starSum = 0;
const widthSpecifiers: WidthSpecifier[] = [];
- this.childLayoutPairs.map(({ layout: { dimUnit, dimMagnitude } }) => {
- const unit = StrCast(dimUnit);
- const magnitude = NumCast(dimMagnitude);
+ 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);
widthSpecifiers.push({ magnitude, unit });
@@ -82,9 +81,9 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
setTimeout(() => {
const { ratioDefinedDocs } = this;
if (this.childLayoutPairs.length) {
- const minimum = Math.min(...ratioDefinedDocs.map(({ dimMagnitude }) => NumCast(dimMagnitude)));
+ const minimum = Math.min(...ratioDefinedDocs.map(doc => NumCast(doc.dimMagnitude, 1)));
if (minimum !== 0) {
- ratioDefinedDocs.forEach(layout => layout.dimMagnitude = NumCast(layout.dimMagnitude) / minimum);
+ ratioDefinedDocs.forEach(layout => layout.dimMagnitude = NumCast(layout.dimMagnitude, 1) / minimum, 1);
}
}
});
@@ -119,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);
}
}
@@ -160,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.dimMagnitude);
- if (StrCast(layout.dimUnit) === DimUnit.Ratio) {
+ let width = NumCast(layout.dimMagnitude, 1);
+ if (StrCast(layout.dimUnit, "*") === DimUnit.Ratio) {
width *= columnUnitLength;
}
return width;
@@ -203,6 +202,19 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
@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.
@@ -214,21 +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}
- CollectionDoc={this.props.Document}
- PanelWidth={() => this.lookupPixels(layout)}
- PanelHeight={() => PanelHeight() - (BoolCast(Document.showWidthLabels) ? 20 : 0)}
- getTransform={() => this.lookupIndividualTransform(layout)}
- onClick={this.onChildClickHandler}
- />
+ <div className={"document-wrapper"}
+ key={"wrapper" + i}
+ style={{ width: width() }} >
+ {this.getDisplayDoc(layout, dxf, width, height)}
<WidthLabel
layout={layout}
collectionDoc={Document}
@@ -236,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}
@@ -249,7 +254,11 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
render(): JSX.Element {
return (
- <div className={"collectionMulticolumnView_contents"} ref={this.createDashEventsTarget}>
+ <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
index ef4b4a19c..64f607680 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss
@@ -8,6 +8,7 @@
.document-wrapper {
display: flex;
flex-direction: row;
+ height: 100%;
.label-wrapper {
display: flex;
@@ -18,13 +19,13 @@
}
- .resizer {
+ .multiRowResizer {
cursor: ew-resize;
transition: 0.5s opacity ease;
display: flex;
flex-direction: row;
- .internal {
+ .multiRowResizer-hdl {
width: 100%;
height: 100%;
transition: 0.5s background-color ease;
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index aa440b677..ff7c4998f 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -34,7 +34,7 @@ export const DimUnit = {
};
const resolvedUnits = Object.values(DimUnit);
-const resizerHeight = 4;
+const resizerHeight = 8;
@observer
export class CollectionMultirowView extends CollectionSubView(MultirowDocument) {
@@ -45,7 +45,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
*/
@computed
private get ratioDefinedDocs() {
- return this.childLayoutPairs.map(({ layout }) => layout).filter(({ dimUnit }) => StrCast(dimUnit) === DimUnit.Ratio);
+ return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout.dimUnit, "*") === DimUnit.Ratio);
}
/**
@@ -60,9 +60,9 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
private get resolvedLayoutInformation(): LayoutData {
let starSum = 0;
const heightSpecifiers: HeightSpecifier[] = [];
- this.childLayoutPairs.map(({ layout: { dimUnit, dimMagnitude } }) => {
- const unit = StrCast(dimUnit);
- const magnitude = NumCast(dimMagnitude);
+ 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 });
@@ -82,9 +82,9 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
setTimeout(() => {
const { ratioDefinedDocs } = this;
if (this.childLayoutPairs.length) {
- const minimum = Math.min(...ratioDefinedDocs.map(({ dimMagnitude }) => NumCast(dimMagnitude)));
+ const minimum = Math.min(...ratioDefinedDocs.map(layout => NumCast(layout.dimMagnitude, 1)));
if (minimum !== 0) {
- ratioDefinedDocs.forEach(layout => layout.dimMagnitude = NumCast(layout.dimMagnitude) / minimum);
+ ratioDefinedDocs.forEach(layout => layout.dimMagnitude = NumCast(layout.dimMagnitude, 1) / minimum);
}
}
});
@@ -119,7 +119,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
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));
+ return this.props.PanelHeight() - (this.totalFixedAllocation + resizerHeight * (layoutInfoLen - 1)) - 2 * NumCast(this.props.Document._yMargin);
}
}
@@ -160,8 +160,8 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
if (rowUnitLength === undefined) {
return 0; // we're still waiting on promises to resolve
}
- let height = NumCast(layout.dimMagnitude);
- if (StrCast(layout.dimUnit) === DimUnit.Ratio) {
+ let height = NumCast(layout.dimMagnitude, 1);
+ if (StrCast(layout.dimUnit, "*") === DimUnit.Ratio) {
height *= rowUnitLength;
}
return height;
@@ -203,6 +203,19 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
@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.
@@ -214,22 +227,15 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
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={Utils.GenerateGuid()}
+ key={"wrapper" + i}
>
- <ContentFittingDocumentView
- {...this.props}
- Document={layout}
- DataDocument={layout.resolvedDataDoc as Doc}
- CollectionDoc={this.props.Document}
- PanelHeight={() => this.lookupPixels(layout)}
- PanelWidth={() => PanelWidth() - (BoolCast(Document.showHeightLabels) ? 20 : 0)}
- getTransform={() => this.lookupIndividualTransform(layout)}
- onClick={this.onChildClickHandler}
- renderDepth={this.props.renderDepth + 1}
- />
+ {this.getDisplayDoc(layout, dxf, width, height)}
<HeightLabel
layout={layout}
collectionDoc={Document}
@@ -237,7 +243,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
</div>,
<ResizeBar
height={resizerHeight}
- key={Utils.GenerateGuid()}
+ key={"resizer" + i}
columnUnitLength={this.getRowUnitLength}
toTop={layout}
toBottom={childLayoutPairs[i + 1]?.layout}
@@ -250,7 +256,11 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
render(): JSX.Element {
return (
- <div className={"collectionMultirowView_contents"} ref={this.createDashEventsTarget}>
+ <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>
);
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
index 46c39d817..6b89402e6 100644
--- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
@@ -46,14 +46,12 @@ export default class ResizeBar extends React.Component<ResizerProps> {
const unitLength = columnUnitLength();
if (unitLength) {
if (toNarrow) {
- const { dimUnit, dimMagnitude } = toNarrow;
- const scale = dimUnit === DimUnit.Ratio ? unitLength : 1;
- toNarrow.dimMagnitude = NumCast(dimMagnitude) - 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 { dimUnit, dimMagnitude } = toWiden;
- const scale = dimUnit === DimUnit.Ratio ? unitLength : 1;
- toWiden.dimMagnitude = NumCast(dimMagnitude) + 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.dimUnit) === DimUnit.Pixel && StrCast(toRight.dimUnit) === DimUnit.Pixel) {
+ if (StrCast(toLeft.dimUnit, "*") === DimUnit.Pixel && StrCast(toRight.dimUnit, "*") === DimUnit.Pixel) {
return false;
}
return true;
} else if (toLeft) {
- if (StrCast(toLeft.dimUnit) === DimUnit.Pixel) {
+ if (StrCast(toLeft.dimUnit, "*") === DimUnit.Pixel) {
return false;
}
return true;
} else if (toRight) {
- if (StrCast(toRight.dimUnit) === DimUnit.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/MultirowResizer.tsx b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
index 4f58f3fa8..d00939b26 100644
--- a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
@@ -46,14 +46,12 @@ export default class ResizeBar extends React.Component<ResizerProps> {
const unitLength = columnUnitLength();
if (unitLength) {
if (toNarrow) {
- const { dimUnit, dimMagnitude } = toNarrow;
- const scale = dimUnit === DimUnit.Ratio ? unitLength : 1;
- toNarrow.dimMagnitude = NumCast(dimMagnitude) - Math.abs(movementY) / scale;
+ 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 { dimUnit, dimMagnitude } = toWiden;
- const scale = dimUnit === DimUnit.Ratio ? unitLength : 1;
- toWiden.dimMagnitude = NumCast(dimMagnitude) + Math.abs(movementY) / scale;
+ const scale = StrCast(toWiden.dimUnit, "*") === DimUnit.Ratio ? unitLength : 1;
+ toWiden.dimMagnitude = Math.max(0.05, NumCast(toWiden.dimMagnitude, 1) + Math.abs(movementY) / scale);
}
}
}
@@ -61,17 +59,17 @@ export default class ResizeBar extends React.Component<ResizerProps> {
private get isActivated() {
const { toTop, toBottom } = this.props;
if (toTop && toBottom) {
- if (StrCast(toTop.dimUnit) === DimUnit.Pixel && StrCast(toBottom.dimUnit) === DimUnit.Pixel) {
+ if (StrCast(toTop.dimUnit, "*") === DimUnit.Pixel && StrCast(toBottom.dimUnit, "*") === DimUnit.Pixel) {
return false;
}
return true;
} else if (toTop) {
- if (StrCast(toTop.dimUnit) === DimUnit.Pixel) {
+ if (StrCast(toTop.dimUnit, "*") === DimUnit.Pixel) {
return false;
}
return true;
} else if (toBottom) {
- if (StrCast(toBottom.dimUnit) === DimUnit.Pixel) {
+ if (StrCast(toBottom.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={"multiRowResizer"}
style={{
height: this.props.height,
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={"multiRowResizer-hdl"}
onPointerDown={e => this.registerResizing(e, ResizeMode.Pinned)}
style={{ backgroundColor: this.resizeMode }}
/>
<div
- className={"internal"}
+ className={"multiRowResizer-hdl"}
onPointerDown={e => this.registerResizing(e, ResizeMode.Global)}
style={{ backgroundColor: this.resizeMode }}
/>