diff options
| author | andrewdkim <adkim414@gmail.com> | 2019-10-02 15:59:56 -0400 |
|---|---|---|
| committer | andrewdkim <adkim414@gmail.com> | 2019-10-02 15:59:56 -0400 |
| commit | 00416cdb70aa8dd9698972ab0df8ca0a6c8575f9 (patch) | |
| tree | fb446dbdf8ff37d58aaa92019ae3edf72409900b /src/client/views/collections | |
| parent | 2f09822358dba784ec26d5707423b4025096ee45 (diff) | |
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web into animationtimeline_two
Diffstat (limited to 'src/client/views/collections')
15 files changed, 415 insertions, 577 deletions
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 56d12bd84..0168c466f 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -20,7 +20,8 @@ export enum CollectionViewType { Docking, Tree, Stacking, - Masonry + Masonry, + Pivot, } export namespace CollectionViewType { @@ -32,7 +33,8 @@ export namespace CollectionViewType { ["docking", CollectionViewType.Docking], ["tree", CollectionViewType.Tree], ["stacking", CollectionViewType.Stacking], - ["masonry", CollectionViewType.Masonry] + ["masonry", CollectionViewType.Masonry], + ["pivot", CollectionViewType.Pivot] ]); export const valueOf = (value: string) => { diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index 0e7e0afa7..6f5abd05b 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -1,8 +1,5 @@ @import "../../views/globalCssVariables.scss"; -.collectiondockingview-content { - height: 100%; -} .lm_active .messageCounter{ color:white; background: #999999; @@ -21,7 +18,7 @@ .collectiondockingview-container { width: 100%; - height: 100%; + height:100%; border-style: solid; border-width: $COLLECTION_BORDER_WIDTH; position: absolute; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 8fcba99e3..b047e77a8 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -9,7 +9,7 @@ import * as ReactDOM from 'react-dom'; import Measure from "react-measure"; import * as GoldenLayout from "../../../client/goldenLayout"; import { DateField } from '../../../new_fields/DateField'; -import { Doc, DocListCast, Field, Opt } from "../../../new_fields/Doc"; +import { Doc, DocListCast, Field, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc"; import { Id } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; import { FieldId } from "../../../new_fields/RefField"; @@ -30,7 +30,9 @@ import "./CollectionDockingView.scss"; import { SubCollectionViewProps } from "./CollectionSubView"; import React = require("react"); import { ButtonSelector } from './ParentDocumentSelector'; +import { DocumentType } from '../../documents/DocumentTypes'; library.add(faFile); +const _global = (window /* browser */ || global /* node */) as any; @observer export class CollectionDockingView extends React.Component<SubCollectionViewProps> { @@ -533,12 +535,11 @@ interface DockedFrameProps { } @observer export class DockedFrameRenderer extends React.Component<DockedFrameProps> { - _mainCont: HTMLDivElement | undefined = undefined; + _mainCont: HTMLDivElement | null = null; @observable private _panelWidth = 0; @observable private _panelHeight = 0; @observable private _document: Opt<Doc>; @observable private _dataDoc: Opt<Doc>; - @observable private _isActive: boolean = false; get _stack(): any { @@ -576,6 +577,13 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } componentDidMount() { + let observer = new _global.ResizeObserver(action((entries: any) => { + for (let entry of entries) { + this._panelWidth = entry.contentRect.width; + this._panelHeight = entry.contentRect.height; + } + })); + observer.observe(this.props.glContainer._element[0]); this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged); this.props.glContainer.on("tab", this.onActiveContentItemChanged); this.onActiveContentItemChanged(); @@ -594,13 +602,21 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } } - panelWidth = () => this._document!.ignoreAspect ? this._panelWidth : Math.min(this._panelWidth, Math.max(NumCast(this._document!.width), this.nativeWidth())); - panelHeight = () => this._document!.ignoreAspect ? this._panelHeight : Math.min(this._panelHeight, Math.max(NumCast(this._document!.height), NumCast(this._document!.nativeHeight, this._panelHeight))); + panelWidth = () => this._document!.ignoreAspect || this._document!.fitWidth ? this._panelWidth : Math.min(this._panelWidth, Math.max(NumCast(this._document!.width), this.nativeWidth())); + panelHeight = () => this._document!.ignoreAspect || this._document!.fitWidth ? this._panelHeight : Math.min(this._panelHeight, Math.max(NumCast(this._document!.height), this.nativeHeight())); - nativeWidth = () => !this._document!.ignoreAspect ? NumCast(this._document!.nativeWidth) || this._panelWidth : 0; - nativeHeight = () => !this._document!.ignoreAspect ? NumCast(this._document!.nativeHeight) || this._panelHeight : 0; + nativeWidth = () => !this._document!.ignoreAspect && !this._document!.fitWidth ? NumCast(this._document!.nativeWidth) || this._panelWidth : 0; + nativeHeight = () => !this._document!.ignoreAspect && !this._document!.fitWidth ? NumCast(this._document!.nativeHeight) || this._panelHeight : 0; contentScaling = () => { + if (this._document!.type === DocumentType.PDF) { + if ((this._document && this._document.fitWidth) || + this._panelHeight / NumCast(this._document!.nativeHeight) > this._panelWidth / NumCast(this._document!.nativeWidth)) { + return this._panelWidth / NumCast(this._document!.nativeWidth); + } else { + return this._panelHeight / NumCast(this._document!.nativeHeight); + } + } const nativeH = this.nativeHeight(); const nativeW = this.nativeWidth(); if (!nativeW || !nativeH) return 1; @@ -619,6 +635,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { get previewPanelCenteringOffset() { return this.nativeWidth() && !BoolCast(this._document!.ignoreAspect) ? (this._panelWidth - this.nativeWidth() / this.ScreenToLocalTransform().Scale) / 2 : 0; } addDocTab = (doc: Doc, dataDoc: Opt<Doc>, location: string) => { + SelectionManager.DeselectAll(); if (doc.dockingConfig) { MainView.Instance.openWorkspace(doc); return true; @@ -630,13 +647,10 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { return CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc); } } - @computed get docView() { - if (!this._document) { - return (null); - } - let resolvedDataDoc = this._document.layout instanceof Doc ? this._document : this._dataDoc; - return <DocumentView key={this._document[Id]} - Document={this._document} + docView(document: Doc) { + let resolvedDataDoc = document.layout instanceof Doc ? document : this._dataDoc; + return <DocumentView key={document[Id]} + Document={document} DataDoc={resolvedDataDoc} bringToFront={emptyFunction} addDocument={undefined} @@ -659,28 +673,14 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { getScale={returnOne} />; } - @computed get content() { - return ( - <div className="collectionDockingView-content" ref={action((ref: HTMLDivElement) => { - this._mainCont = ref; - if (ref) { - this._panelWidth = Number(getComputedStyle(ref).width!.replace("px", "")); - this._panelHeight = Number(getComputedStyle(ref).height!.replace("px", "")); - } - })} - style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}> - {this.docView} - </div >); - } - render() { - if (!this._isActive || !this._document) return null; - let theContent = this.content; - return !this._document ? (null) : - <Measure offset onResize={action((r: any) => { this._panelWidth = r.offset.width; this._panelHeight = r.offset.height; })}> - {({ measureRef }) => <div ref={measureRef}> - {theContent} - </div>} - </Measure>; + return (!this._isActive || !this._document) ? (null) : + (<div className="collectionDockingView-content" ref={ref => this._mainCont = ref} + style={{ + transform: `translate(${this.previewPanelCenteringOffset}px, 0px)`, + height: this._document && this._document.fitWidth ? undefined : "100%" + }}> + {this.docView(this._document)} + </div >); } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionPDFView.scss b/src/client/views/collections/CollectionPDFView.scss index 50201bae8..62ec8a5be 100644 --- a/src/client/views/collections/CollectionPDFView.scss +++ b/src/client/views/collections/CollectionPDFView.scss @@ -1,26 +1,4 @@ -.collectionPdfView-buttonTray { - top: 15px; - left: 20px; - position: relative; - transform-origin: left top; - position: absolute; -} -.collectionPdfView-thumb { - width: 25px; - height: 25px; - transform-origin: left top; - position: absolute; - background: darkgray; -} - -.collectionPdfView-slider { - width: 25px; - height: 25px; - transform-origin: left top; - position: absolute; - background: lightgray; -} .collectionPdfView-cont { width: 100%; @@ -29,28 +7,5 @@ top: 0; left: 0; z-index: -1; + overflow: hidden !important; } - -.collectionPdfView-cont-dragging { - span { - user-select: none; - } -} - -.collectionPdfView-backward { - color: white; - font-size: 24px; - top: 0px; - left: 0px; - position: absolute; - background-color: rgba(50, 50, 50, 0.2); -} - -.collectionPdfView-forward { - color: white; - font-size: 24px; - top: 0px; - left: 45px; - position: absolute; - background-color: rgba(50, 50, 50, 0.2); -}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx index 8eda4d9ee..cc8142ec0 100644 --- a/src/client/views/collections/CollectionPDFView.tsx +++ b/src/client/views/collections/CollectionPDFView.tsx @@ -1,10 +1,9 @@ -import { computed } from "mobx"; +import { trace } from "mobx"; import { observer } from "mobx-react"; import { Id } from "../../../new_fields/FieldSymbols"; import { emptyFunction } from "../../../Utils"; import { ContextMenu } from "../ContextMenu"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; -import { PDFBox } from "../nodes/PDFBox"; import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from "./CollectionBaseView"; import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; import "./CollectionPDFView.scss"; @@ -17,35 +16,18 @@ export class CollectionPDFView extends React.Component<FieldViewProps> { return FieldView.LayoutString(CollectionPDFView, fieldKey, fieldExt); } - private _pdfBox?: PDFBox; - private _buttonTray: React.RefObject<HTMLDivElement> = React.createRef(); - - @computed - get uIButtons() { - return ( - <div className="collectionPdfView-buttonTray" ref={this._buttonTray} key="tray" style={{ height: "100%" }}> - <button className="collectionPdfView-backward" onClick={() => this._pdfBox && this._pdfBox.BackPage()}>{"<"}</button> - <button className="collectionPdfView-forward" onClick={() => this._pdfBox && this._pdfBox.ForwardPage()}>{">"}</button> - </div> - ); - } - onContextMenu = (e: React.MouseEvent): void => { if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 ContextMenu.Instance.addItem({ description: "PDFOptions", event: emptyFunction, icon: "file-pdf" }); } } - setPdfBox = (pdfBox: PDFBox) => { this._pdfBox = pdfBox; }; - subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => { - return (<> - <CollectionFreeFormView {...this.props} {...renderProps} setPdfBox={this.setPdfBox} CollectionView={this} chromeCollapsed={true} /> - {renderProps.active() ? this.uIButtons : (null)} - </>); + return (<CollectionFreeFormView {...this.props} {...renderProps} CollectionView={this} chromeCollapsed={true} />); } render() { + trace(); return ( <CollectionBaseView {...this.props} className={"collectionPdfView-cont"} onContextMenu={this.onContextMenu}> {this.subView} diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 4dac27e60..179e44266 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -34,7 +34,7 @@ export interface CellProps { row: number; col: number; rowProps: CellInfo; - CollectionView: CollectionView | CollectionPDFView | CollectionVideoView; + CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; ContainingCollection: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; Document: Doc; fieldKey: string; @@ -151,7 +151,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { fieldExt: "", ruleProvider: undefined, ContainingCollectionView: this.props.CollectionView, - ContainingCollectionDoc: this.props.CollectionView.props.Document, + ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, isSelected: returnFalse, select: emptyFunction, renderDepth: this.props.renderDepth + 1, @@ -301,7 +301,7 @@ export class CollectionSchemaCheckboxCell extends CollectionSchemaCell { render() { let reference = React.createRef<HTMLDivElement>(); let onItemDown = (e: React.PointerEvent) => { - (!this.props.CollectionView.props.isSelected() ? undefined : + (!this.props.CollectionView || !this.props.CollectionView.props.isSelected() ? undefined : SetupDrag(reference, () => this._document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e)); }; return ( diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 7bd2a1971..8d931f812 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -246,7 +246,7 @@ export interface SchemaTableProps { PanelHeight: () => number; PanelWidth: () => number; childDocs?: Doc[]; - CollectionView: CollectionView | CollectionPDFView | CollectionVideoView; + CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; ContainingCollectionDoc: Opt<Doc>; fieldKey: string; @@ -804,7 +804,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { csv.substring(0, csv.length - 1); let dbName = StrCast(this.props.Document.title); let res = await Gateway.Instance.PostSchema(csv, dbName); - if (self.props.CollectionView.props.addDocument) { + if (self.props.CollectionView && self.props.CollectionView.props.addDocument) { let schemaDoc = await Docs.Create.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document }); if (schemaDoc) { //self.props.CollectionView.props.addDocument(schemaDoc, false); diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index ccf131797..45de0fefa 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -42,7 +42,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @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; } - @computed get showAddAGroup() { return (this.sectionFilter && (this.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.CollectionView.props.Document.chromeStatus !== 'disabled')); } + @computed get showAddAGroup() { return (this.sectionFilter && this.props.ContainingCollectionDoc && (this.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.ContainingCollectionDoc.chromeStatus !== 'disabled')); } @computed get columnWidth() { return Math.min(this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin, this.isStackingView ? Number.MAX_VALUE : NumCast(this.props.Document.columnWidth, 250)); @@ -160,13 +160,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { if (!d) return 0; let nw = NumCast(d.nativeWidth); let nh = NumCast(d.nativeHeight); - if (!d.ignoreAspect && nw && nh) { + if (!d.ignoreAspect && !d.fitWidth && nw && nh) { let aspect = nw && nh ? nh / nw : 1; let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); if (!(d.nativeWidth && !d.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(d[WidthSym](), wid); return wid * aspect; } - return d[HeightSym](); + return d.fitWidth ? Math.min(this.props.PanelHeight() - 2 * this.yMargin, d[HeightSym]()) : d[HeightSym](); } columnDividerDown = (e: React.PointerEvent) => { @@ -347,7 +347,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } onToggle = (checked: Boolean) => { - this.props.CollectionView.props.Document.chromeStatus = checked ? "collapsed" : "view-mode"; + this.props.ContainingCollectionDoc && (this.props.ContainingCollectionDoc.chromeStatus = checked ? "collapsed" : "view-mode"); } onContextMenu = (e: React.MouseEvent): void => { @@ -391,10 +391,10 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { style={{ width: this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}> <EditableView {...editableViewProps} /> </div>} - {this.props.CollectionView.props.Document.chromeStatus !== 'disabled' ? <Switch + {this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.chromeStatus !== 'disabled' ? <Switch onChange={this.onToggle} onClick={this.onToggle} - defaultChecked={this.props.CollectionView.props.Document.chromeStatus !== 'view-mode'} + defaultChecked={this.props.ContainingCollectionDoc.chromeStatus !== 'view-mode'} checkedChildren="edit" unCheckedChildren="view" /> : null} diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index b3b7b40dd..240adf428 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -266,7 +266,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC style={{ width: (style.columnWidth) / ((uniqueHeadings.length + - ((this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? 1 : 0)) || 1) + ((this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'disabled') ? 1 : 0)) || 1) }}> {/* the default bucket (no key value) has a tooltip that describes what it is. Further, it does not have a color and cannot be deleted. */} @@ -297,7 +297,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC </div> : (null); for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `; return ( - <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }} + <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }} ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}> {headingView} <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`} @@ -315,7 +315,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC {this.children(this.props.docList)} {singleColumn ? (null) : this.props.parent.columnDragger} </div> - {(this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? + {(this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'disabled') ? <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" style={{ width: style.columnWidth / style.numGroupColumns }}> <EditableView {...newEditableViewProps} /> diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index c11dd6150..069269b06 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,12 +1,12 @@ import { action, computed, IReactionDisposer, reaction } from "mobx"; import * as rp from 'request-promise'; import CursorField from "../../../new_fields/CursorField"; -import { Doc, DocListCast } from "../../../new_fields/Doc"; +import { Doc, DocListCast, Opt } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { ScriptField } from "../../../new_fields/ScriptField"; -import { BoolCast, Cast } from "../../../new_fields/Types"; +import { Cast } from "../../../new_fields/Types"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { RouteStore } from "../../../server/RouteStore"; import { Utils } from "../../../Utils"; @@ -30,10 +30,11 @@ export interface CollectionViewProps extends FieldViewProps { PanelWidth: () => number; PanelHeight: () => number; chromeCollapsed: boolean; + setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; } export interface SubCollectionViewProps extends CollectionViewProps { - CollectionView: CollectionView | CollectionPDFView | CollectionVideoView; + CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; ruleProvider: Doc | undefined; } diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 5f4742834..d3072ff1e 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -18,6 +18,7 @@ import { CollectionSchemaView } from "./CollectionSchemaView"; import { CollectionStackingView } from './CollectionStackingView'; import { CollectionTreeView } from "./CollectionTreeView"; import { CollectionViewBaseChrome } from './CollectionViewChromes'; +import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines'; export const COLLECTION_BORDER_WIDTH = 2; library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy); @@ -59,8 +60,10 @@ export class CollectionView extends React.Component<FieldViewProps> { case CollectionViewType.Tree: return (<CollectionTreeView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); } case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); } + case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); } case CollectionViewType.Freeform: default: + this.props.Document.freeformLayoutEngine = undefined; return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); } return (null); @@ -89,7 +92,7 @@ export class CollectionView extends React.Component<FieldViewProps> { if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 let existingVm = ContextMenu.Instance.findByDescription("View Modes..."); let subItems: ContextMenuProps[] = existingVm && "subitems" in existingVm ? existingVm.subitems : []; - subItems.push({ description: "Freeform", event: () => { this.props.Document.viewType = CollectionViewType.Freeform; delete this.props.Document.usePivotLayout; }, icon: "signature" }); + subItems.push({ description: "Freeform", event: () => { this.props.Document.viewType = CollectionViewType.Freeform; }, icon: "signature" }); if (CollectionBaseView.InSafeMode()) { ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document.viewType = CollectionViewType.Invalid, icon: "project-diagram" }); } @@ -103,10 +106,10 @@ export class CollectionView extends React.Component<FieldViewProps> { }, icon: "ellipsis-v" }); subItems.push({ description: "Masonry", event: () => this.props.Document.viewType = CollectionViewType.Masonry, icon: "columns" }); + subItems.push({ description: "Pivot", event: () => this.props.Document.viewType = CollectionViewType.Pivot, icon: "columns" }); switch (this.props.Document.viewType) { case CollectionViewType.Freeform: { - subItems.push({ description: "Custom", icon: "fingerprint", event: CollectionFreeFormView.AddCustomLayout(this.props.Document, this.props.fieldKey) }); - subItems.push({ description: "Pivot", icon: "copy", event: () => this.props.Document.usePivotLayout = true }); + subItems.push({ description: "Custom", icon: "fingerprint", event: AddCustomFreeFormLayout(this.props.Document, this.props.fieldKey) }); break; } } diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index 20786f690..cefa9eebc 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -260,7 +260,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @observable private pivotKeyDisplay = this.pivotKey; getPivotInput = () => { - if (!this.document.usePivotLayout) { + if (StrCast(this.document.freeformLayoutEngine) !== "pivot") { return (null); } return (<input className="collectionViewBaseChrome-viewSpecsInput" @@ -396,6 +396,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="4">Tree View</option> <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking View</option> <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry View</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="7">Pivot View</option> </select> <div className="collectionViewBaseChrome-viewSpecs" style={{ display: collapsed ? "none" : "grid" }}> <input className="collectionViewBaseChrome-viewSpecsInput" diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx new file mode 100644 index 000000000..886692172 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -0,0 +1,117 @@ +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 } from "../../../../Utils"; +import React = require("react"); + +interface PivotData { + type: string; + text: string; + x: number; + y: number; + width: number; + height: number; + fontSize: number; +} + +export interface ViewDefBounds { + x: number; + y: number; + z?: number; + width: number; + height: number; + transition?: string; +} + +export interface ViewDefResult { + ele: JSX.Element; + bounds?: ViewDefBounds; +} + +export function computePivotLayout(pivotDoc: Doc, childDocs: Doc[], childPairs: { layout: Doc, data?: Doc }[], viewDefsToJSX: (views: any) => ViewDefResult[]) { + let layoutPoolData: Map<{ layout: Doc, data?: Doc }, any> = new Map(); + const pivotAxisWidth = NumCast(pivotDoc.pivotWidth, 200); + const pivotColumnGroups = new Map<FieldResult<Field>, Doc[]>(); + + for (const doc of childDocs) { + const val = doc[StrCast(pivotDoc.pivotField, "title")]; + if (val) { + !pivotColumnGroups.get(val) && pivotColumnGroups.set(val, []); + pivotColumnGroups.get(val)!.push(doc); + } + } + + const minSize = Array.from(pivotColumnGroups.entries()).reduce((min, pair) => Math.min(min, pair[1].length), Infinity); + const numCols = NumCast(pivotDoc.pivotNumColumns, Math.ceil(Math.sqrt(minSize))); + const docMap = new Map<Doc, ViewDefBounds>(); + const groupNames: PivotData[] = []; + + let x = 0; + pivotColumnGroups.forEach((val, key) => { + let y = 0; + let xCount = 0; + groupNames.push({ + type: "text", + text: String(key), + x, + y: pivotAxisWidth + 50, + width: pivotAxisWidth * 1.25 * numCols, + height: 100, + fontSize: NumCast(pivotDoc.pivotFontSize, 10) + }); + for (const doc of val) { + docMap.set(doc, { + x: x + xCount * pivotAxisWidth * 1.25, + y: -y, + width: pivotAxisWidth, + height: doc.nativeWidth ? (NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth + }); + xCount++; + if (xCount >= numCols) { + xCount = 0; + y += pivotAxisWidth * 1.25; + } + } + x += pivotAxisWidth * 1.25 * (numCols + 1); + }); + + childPairs.map(pair => { + let defaultPosition = { + x: NumCast(pair.layout.x), + y: NumCast(pair.layout.y), + z: NumCast(pair.layout.z), + width: NumCast(pair.layout.width), + height: NumCast(pair.layout.height) + }; + const pos = docMap.get(pair.layout) || defaultPosition; + layoutPoolData.set(pair, { transition: "transform 1s", ...pos }); + }); + return { map: layoutPoolData, elements: viewDefsToJSX(groupNames) }; +} + +export function AddCustomFreeFormLayout(doc: Doc, dataKey: string): () => void { + return () => { + let addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => { + let overlayDisposer: () => void = emptyFunction; // filled in below after we have a reference to the scriptingBox + const scriptField = Cast(doc[key], ScriptField); + let scriptingBox = <ScriptBox initialText={scriptField && scriptField.script.originalScript} + // tslint:disable-next-line: no-unnecessary-callback-wrapper + onCancel={() => overlayDisposer()} // don't get rid of the function wrapper-- we don't want to use the current value of overlayDiposer, but the one set below + onSave={(text, onError) => { + const script = CompileScript(text, { params, requiredType, typecheck: false }); + if (!script.compiled) { + onError(script.errors.map(error => error.messageText).join("\n")); + } else { + doc[key] = new ScriptField(script); + overlayDisposer(); + } + }} />; + overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, options); + }; + addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300, title: "Layout Initialization" }, { collection: "Doc", docs: "Doc[]" }, undefined); + addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300, title: "Layout Script" }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}"); + }; +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 5157d0c75..c6e8d7cf7 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,22 +1,23 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faEye } from "@fortawesome/free-regular-svg-icons"; import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons"; -import { action, computed, IReactionDisposer, observable, reaction, trace } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCastAsync, Field, FieldResult, HeightSym, Opt, WidthSym, DocListCast } from "../../../../new_fields/Doc"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc"; import { Id } from "../../../../new_fields/FieldSymbols"; import { InkField, StrokeData } from "../../../../new_fields/InkField"; import { createSchema, makeInterface } from "../../../../new_fields/Schema"; import { ScriptField } from "../../../../new_fields/ScriptField"; -import { BoolCast, Cast, FieldValue, NumCast, StrCast, PromiseValue, DateCast } from "../../../../new_fields/Types"; -import { emptyFunction, returnEmptyString, returnOne, Utils } from "../../../../Utils"; +import { BoolCast, Cast, DateCast, NumCast, StrCast } from "../../../../new_fields/Types"; +import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; +import { aggregateBounds, emptyFunction, intersectRect, returnEmptyString, returnOne, Utils } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; +import { DocServer } from "../../../DocServer"; import { Docs } from "../../../documents/Documents"; import { DocumentType } from "../../../documents/DocumentTypes"; import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager } from "../../../util/DragManager"; import { HistoryUtil } from "../../../util/History"; -import { CompileScript } from "../../../util/Scripting"; import { SelectionManager } from "../../../util/SelectionManager"; import { Transform } from "../../../util/Transform"; import { undoBatch, UndoManager } from "../../../util/UndoManager"; @@ -26,12 +27,12 @@ import { ContextMenuProps } from "../../ContextMenuItem"; import { InkingCanvas } from "../../InkingCanvas"; import { CollectionFreeFormDocumentView, positionSchema } from "../../nodes/CollectionFreeFormDocumentView"; import { DocumentContentsView } from "../../nodes/DocumentContentsView"; -import { DocumentViewProps, documentSchema } from "../../nodes/DocumentView"; +import { documentSchema, DocumentViewProps } from "../../nodes/DocumentView"; +import { FormattedTextBox } from "../../nodes/FormattedTextBox"; import { pageSchema } from "../../nodes/ImageBox"; -import { OverlayElementOptions, OverlayView } from "../../OverlayView"; import PDFMenu from "../../pdf/PDFMenu"; -import { ScriptBox } from "../../ScriptBox"; import { CollectionSubView } from "../CollectionSubView"; +import { computePivotLayout, ViewDefResult } from "./CollectionFreeFormLayoutEngines"; import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView"; import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; import "./CollectionFreeFormView.scss"; @@ -40,9 +41,6 @@ import React = require("react"); import v5 = require("uuid/v5"); import { Timeline } from "../../animationtimeline/Timeline"; import { number } from "prop-types"; -import { DocServer } from "../../../DocServer"; -import { FormattedTextBox } from "../../nodes/FormattedTextBox"; -import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard); @@ -54,132 +52,10 @@ export const panZoomSchema = createSchema({ arrangeInit: ScriptField, useClusters: "boolean", isRuleProvider: "boolean", - fitToBox: "boolean" + fitToBox: "boolean", + panTransformType: "string", }); -export interface ViewDefBounds { - x: number; - y: number; - z?: number; - width: number; - height: number; -} - -export interface ViewDefResult { - ele: JSX.Element; - bounds?: ViewDefBounds; -} - -export namespace PivotView { - - export interface PivotData { - type: string; - text: string; - x: number; - y: number; - width: number; - height: number; - fontSize: number; - } - - export const elements = (target: CollectionFreeFormView) => { - let collection = target.Document; - const field = StrCast(collection.pivotField) || "title"; - const width = NumCast(collection.pivotWidth) || 200; - const groups = new Map<FieldResult<Field>, Doc[]>(); - - for (const doc of target.childDocs) { - const val = doc[field]; - if (val === undefined) continue; - - const l = groups.get(val); - if (l) { - l.push(doc); - } else { - groups.set(val, [doc]); - } - } - - let minSize = Infinity; - - groups.forEach((val, key) => minSize = Math.min(minSize, val.length)); - - const numCols = NumCast(collection.pivotNumColumns) || Math.ceil(Math.sqrt(minSize)); - const fontSize = NumCast(collection.pivotFontSize); - - const docMap = new Map<Doc, ViewDefBounds>(); - const groupNames: PivotData[] = []; - - let x = 0; - groups.forEach((val, key) => { - let y = 0; - let xCount = 0; - groupNames.push({ - type: "text", - text: String(key), - x, - y: width + 50, - width: width * 1.25 * numCols, - height: 100, fontSize: fontSize - }); - for (const doc of val) { - docMap.set(doc, { - x: x + xCount * width * 1.25, - y: -y, - width, - height: width - }); - xCount++; - if (xCount >= numCols) { - xCount = 0; - y += width * 1.25; - } - } - x += width * 1.25 * (numCols + 1); - }); - - let elements = target.viewDefsToJSX(groupNames); - let docViews = target.childDocs.reduce((prev, doc) => { - let minim = BoolCast(doc.isMinimized); - if (minim === undefined || !minim) { - let defaultPosition = (): ViewDefBounds => { - return { - x: NumCast(doc.x), - y: NumCast(doc.y), - z: NumCast(doc.z), - width: NumCast(doc.width), - height: NumCast(doc.height) - }; - }; - const pos = docMap.get(doc) || defaultPosition(); - prev.push({ - ele: <CollectionFreeFormDocumentView - key={doc[Id]} - x={pos.x} - y={pos.y} - width={pos.width} - height={pos.height} - transition={"transform 1s"} - jitterRotation={NumCast(target.props.Document.jitterRotation)} - {...target.getChildDocumentViewProps(doc)} - />, - bounds: { - x: pos.x, - y: pos.y, - z: pos.z, - width: NumCast(pos.width), - height: NumCast(pos.height) - } - }); - } - return prev; - }, elements); - - return docViews; - }; - -} - type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof documentSchema, typeof positionSchema, typeof pageSchema]>; const PanZoomDocument = makeInterface(panZoomSchema, documentSchema, positionSchema, pageSchema); @@ -187,50 +63,26 @@ const PanZoomDocument = makeInterface(panZoomSchema, documentSchema, positionSch export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private _lastX: number = 0; private _lastY: number = 0; - private get _pwidth() { return this.props.PanelWidth(); } - private get _pheight() { return this.props.PanelHeight(); } - private _timelineRef = React.createRef<Timeline>(); - private get parentScaling() { - return (this.props as any).ContentScaling && this.fitToBox && !this.isAnnotationOverlay ? (this.props as any).ContentScaling() : 1; - } - - ComputeContentBounds(boundsList: { x: number, y: number, width: number, height: number }[]) { - let bounds = boundsList.reduce((bounds, b) => { - var [sptX, sptY] = [b.x, b.y]; - let [bptX, bptY] = [sptX + NumCast(b.width, 1), sptY + NumCast(b.height, 1)]; - return { - x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y), - r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b) - }; - }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: -Number.MAX_VALUE }); - return bounds; - } - - @computed get actualContentBounds() { - return this.fitToBox && !this.isAnnotationOverlay ? this.ComputeContentBounds(this.elements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)) : undefined; - } - - @computed get contentBounds() { - let bounds = this.actualContentBounds; - let res = { - panX: bounds ? (bounds.x + bounds.r) / 2 : this.Document.panX || 0, - panY: bounds ? (bounds.y + bounds.b) / 2 : this.Document.panY || 0, - scale: (bounds ? Math.min(this.props.PanelHeight() / (bounds.b - bounds.y), this.props.PanelWidth() / (bounds.r - bounds.x)) : this.Document.scale || 1) / this.parentScaling - }; - if (res.scale === 0) res.scale = 1; - return res; - } - - @computed get fitToBox() { return this.props.fitToBox || this.Document.fitToBox; } - @computed get nativeWidth() { return this.fitToBox ? 0 : this.Document.nativeWidth || 0; } - @computed get nativeHeight() { return this.fitToBox ? 0 : this.Document.nativeHeight || 0; } - public get isAnnotationOverlay() { return this.props.fieldExt ? true : false; } // fieldExt will be "" or "annotation". should maybe generalize this, or make it more specific (ie, 'annotation' instead of 'fieldExt') + private _clusterDistance: number = 75; + private _hitCluster = false; + @observable _clusterSets: (Doc[])[] = []; + @observable _timelineRef = React.createRef<Timeline>(); + + @computed get fitToContent() { return (this.props.fitToBox || this.Document.fitToBox) && !this.isAnnotationOverlay; } + @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; } + @computed get contentBounds() { return aggregateBounds(this.elements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)); } + @computed get nativeWidth() { return this.fitToContent ? 0 : this.Document.nativeWidth || 0; } + @computed get nativeHeight() { return this.fitToContent ? 0 : this.Document.nativeHeight || 0; } + private get isAnnotationOverlay() { return this.props.fieldExt ? true : false; } // fieldExt will be "" or "annotation". should maybe generalize this, or make it more specific (ie, 'annotation' instead of 'fieldExt') private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } - private panX = () => this.contentBounds.panX; - private panY = () => this.contentBounds.panY; - private zoomScaling = () => this.contentBounds.scale; - private centeringShiftX = () => !this.nativeWidth && !this.isAnnotationOverlay ? this._pwidth / 2 / this.parentScaling : 0; // shift so pan position is at center of window for non-overlay collections - private centeringShiftY = () => !this.nativeHeight && !this.isAnnotationOverlay ? this._pheight / 2 / this.parentScaling : 0;// shift so pan position is at center of window for non-overlay collections + private easing = () => this.props.Document.panTransformType === "Ease"; + private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document.panX || 0; + private panY = () => this.fitToContent ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document.panY || 0; + private zoomScaling = () => (1 / this.parentScaling) * (this.fitToContent ? + Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) : + this.Document.scale || 1); + private centeringShiftX = () => !this.nativeWidth && !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling : 0; // shift so pan position is at center of window for non-overlay collections + private centeringShiftY = () => !this.nativeHeight && !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling : 0;// shift so pan position is at center of window for non-overlay collections private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform()); private getTransformOverlay = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1); private getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth); @@ -248,47 +100,31 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { this.addDocument(newBox, false); } private addDocument = (newBox: Doc, allowDuplicates: boolean) => { - this.props.addDocument(newBox, false); - this.bringToFront(newBox); - this.updateCluster(newBox); - return true; + let added = this.props.addDocument(newBox, false); + added && this.bringToFront(newBox); + added && this.updateCluster(newBox); + return added; } private selectDocuments = (docs: Doc[]) => { SelectionManager.DeselectAll(); - docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).filter(dv => dv).map(dv => - SelectionManager.SelectDoc(dv!, true)); + docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).map(dv => dv && SelectionManager.SelectDoc(dv, true)); } + public isCurrent(doc: Doc) { return !this.props.Document.isMinimized && (Math.abs(NumCast(doc.page, -1) - NumCast(this.Document.curPage, -1)) < 1.5 || NumCast(doc.page, -1) === -1); } + public getActiveDocuments = () => { - const curPage = FieldValue(this.Document.curPage, -1); - return this.childLayoutPairs.filter(pair => { - var page = NumCast(pair.layout!.page, -1); - return page === curPage || page === -1; - }).map(pair => pair.layout); + return this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); } @computed get fieldExtensionDoc() { return Doc.fieldExtensionDoc(this.props.DataDoc || this.props.Document, this.props.fieldKey); } - intersectRect(r1: { left: number, top: number, width: number, height: number }, - r2: { left: number, top: number, width: number, height: number }) { - return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top); - } - _clusterDistance = 75; - boundsOverlap(doc: Doc, doc2: Doc) { - var x2 = NumCast(doc2.x) - this._clusterDistance; - var y2 = NumCast(doc2.y) - this._clusterDistance; - var w2 = NumCast(doc2.width) + this._clusterDistance; - var h2 = NumCast(doc2.height) + this._clusterDistance; - var x = NumCast(doc.x) - this._clusterDistance; - var y = NumCast(doc.y) - this._clusterDistance; - var w = NumCast(doc.width) + this._clusterDistance; - var h = NumCast(doc.height) + this._clusterDistance; - if (doc.z === doc2.z && this.intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 })) { - return true; - } - return false; + @action + onDrop = (e: React.DragEvent): void => { + var pt = this.getTransform().transformPoint(e.pageX, e.pageY); + super.onDrop(e, { x: pt[0], y: pt[1] }); } + @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { @@ -304,7 +140,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let y = (z ? ypo : yp) - de.data.offset[1]; let dropX = NumCast(de.data.droppedDocuments[0].x); let dropY = NumCast(de.data.droppedDocuments[0].y); - de.data.droppedDocuments.forEach(d => { + de.data.droppedDocuments.forEach(action((d: Doc) => { d.x = x + NumCast(d.x) - dropX; d.y = y + NumCast(d.y) - dropY; if (!NumCast(d.width)) { @@ -316,7 +152,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { d.height = nw && nh ? nh / nw * NumCast(d.width) : 300; } this.bringToFront(d); - }); + })); de.data.droppedDocuments.length === 1 && this.updateCluster(de.data.droppedDocuments[0]); } @@ -339,18 +175,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return false; } - tryDragCluster(e: PointerEvent) { - let probe = this.getTransform().transformPoint(e.clientX, e.clientY); - let cluster = this.childLayoutPairs.map(pair => pair.layout).reduce((cluster, cd) => { + pickCluster(probe: number[]) { + return this.childLayoutPairs.map(pair => pair.layout).reduce((cluster, cd) => { let cx = NumCast(cd.x) - this._clusterDistance; let cy = NumCast(cd.y) - this._clusterDistance; let cw = NumCast(cd.width) + 2 * this._clusterDistance; let ch = NumCast(cd.height) + 2 * this._clusterDistance; - if (!cd.z && this.intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 })) { - return NumCast(cd.cluster); - } - return cluster; + return !cd.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ? + NumCast(cd.cluster) : cluster; }, -1); + } + tryDragCluster(e: PointerEvent) { + let cluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)); if (cluster !== -1) { let eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => NumCast(cd.cluster) === cluster); @@ -375,36 +211,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return false; } - @observable sets: (Doc[])[] = []; @undoBatch - @action updateClusters(useClusters: boolean) { - this.Document.useClusters = useClusters; - this.sets.length = 0; - this.childLayoutPairs.map(pair => pair.layout).map(c => { - let included = []; - for (let i = 0; i < this.sets.length; i++) { - for (let member of this.sets[i]) { - if (this.boundsOverlap(c, member)) { - included.push(i); - break; - } - } - } - if (included.length === 0) { - this.sets.push([c]); - } else if (included.length === 1) { - this.sets[included[0]].push(c); - } else { - this.sets[included[0]].push(c); - for (let s = 1; s < included.length; s++) { - this.sets[included[0]].push(...this.sets[included[s]]); - this.sets[included[s]].length = 0; - } - } - }); - this.sets.map((set, i) => set.map(member => member.cluster = i)); + this.props.Document.useClusters = useClusters; + this._clusterSets.length = 0; + this.childLayoutPairs.map(pair => pair.layout).map(c => this.updateCluster(c)); } @undoBatch @@ -412,28 +224,28 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { updateCluster(doc: Doc) { let childLayouts = this.childLayoutPairs.map(pair => pair.layout); if (this.props.Document.useClusters) { - this.sets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1)); + this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1)); let preferredInd = NumCast(doc.cluster); doc.cluster = -1; - this.sets.map((set, i) => set.map(member => { - if (doc.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && this.boundsOverlap(doc, member)) { + this._clusterSets.map((set, i) => set.map(member => { + if (doc.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) { doc.cluster = i; } })); - if (doc.cluster === -1 && preferredInd !== -1 && (!this.sets[preferredInd] || !this.sets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { + if (doc.cluster === -1 && preferredInd !== -1 && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { doc.cluster = preferredInd; } - this.sets.map((set, i) => { + this._clusterSets.map((set, i) => { if (doc.cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) { doc.cluster = i; } }); if (doc.cluster === -1) { - doc.cluster = this.sets.length; - this.sets.push([doc]); + doc.cluster = this._clusterSets.length; + this._clusterSets.push([doc]); } else { - for (let i = this.sets.length; i <= doc.cluster; i++) !this.sets[i] && this.sets.push([]); - this.sets[doc.cluster].push(doc); + for (let i = this._clusterSets.length; i <= doc.cluster; i++) !this._clusterSets[i] && this._clusterSets.push([]); + this._clusterSets[doc.cluster].push(doc); } } } @@ -442,13 +254,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let clusterColor = ""; let cluster = NumCast(doc.cluster); if (this.Document.useClusters) { - if (this.sets.length <= cluster) { + if (this._clusterSets.length <= cluster) { setTimeout(() => this.updateCluster(doc), 0); } else { // choose a cluster color from a palette let colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"]; clusterColor = colors[cluster % colors.length]; - let set = this.sets.length > cluster ? this.sets[cluster].filter(s => s.backgroundColor && (s.backgroundColor !== s.defaultBackgroundColor)) : undefined; + let set = this._clusterSets[cluster] && this._clusterSets[cluster].filter(s => s.backgroundColor && (s.backgroundColor !== s.defaultBackgroundColor)); // override the cluster color with an explicitly set color on a non-background document. then override that with an explicitly set color on a background document set && set.filter(s => !s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); set && set.filter(s => s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); @@ -459,6 +271,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action onPointerDown = (e: React.PointerEvent): void => { + this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false; if (e.button === 0 && !e.shiftKey && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); @@ -476,8 +289,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action onPointerMove = (e: PointerEvent): void => { - if (!e.cancelBubble) { - if (this.props.Document.useClusters && this.tryDragCluster(e)) { + if (!e.cancelBubble && !this.isAnnotationOverlay) { + if (this._hitCluster && this.tryDragCluster(e)) { e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); document.removeEventListener("pointermove", this.onPointerMove); @@ -512,8 +325,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } let cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1; - let panelDim = this.props.ScreenToLocalTransform().transformDirection(this._pwidth / this.zoomScaling() * cscale, - this._pheight / this.zoomScaling() * cscale); + let panelDim = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth() / this.zoomScaling() * cscale, + this.props.PanelHeight() / this.zoomScaling() * cscale); if (ranges[0][0] - dx > (this.panX() + panelDim[0] / 2)) x = ranges[0][1] + panelDim[0] / 2; if (ranges[0][1] - dx < (this.panX() - panelDim[0] / 2)) x = ranges[0][0] - panelDim[0] / 2; if (ranges[1][0] - dy > (this.panY() + panelDim[1] / 2)) y = ranges[1][1] + panelDim[1] / 2; @@ -529,7 +342,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action onPointerWheel = (e: React.WheelEvent): void => { - if (BoolCast(this.props.Document.lockedPosition)) return; + if (this.props.Document.lockedPosition || this.isAnnotationOverlay) return; if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming e.stopPropagation(); } @@ -546,13 +359,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40); this.props.Document.scale = Math.abs(safeScale); this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale); - e.preventDefault(); } } @action setPan(panX: number, panY: number) { - if (!BoolCast(this.props.Document.lockedPosition)) { + if (!this.props.Document.lockedPosition) { this.props.Document.panTransformType = "None"; var scale = this.getLocalTransform().inverse().Scale; const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); @@ -562,12 +374,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } } - @action - onDrop = (e: React.DragEvent): void => { - var pt = this.getTransform().transformPoint(e.pageX, e.pageY); - super.onDrop(e, { x: pt[0], y: pt[1] }); - } - bringToFront = (doc: Doc, sendToBack?: boolean) => { if (sendToBack || doc.isBackground) { doc.zIndex = 0; @@ -599,54 +405,45 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } } SelectionManager.DeselectAll(); - const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2; - const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2; - const newState = HistoryUtil.getState(); - newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY }; - HistoryUtil.pushState(newState); - - let px = this.Document.panX; - let py = this.Document.panY; - let s = this.Document.scale; - this.setPan(newPanX, newPanY); - - this.props.Document.panTransformType = "Ease"; - this.props.focus(this.props.Document); - if (willZoom) { - this.setScaleToZoom(doc, scale); + if (this.props.Document.scrollHeight) { + let annotOn = Cast(doc.annotationOn, Doc) as Doc; + let offset = annotOn && (NumCast(annotOn.height) / 2); + this.props.Document.scrollY = NumCast(doc.y) - offset; + } else { + const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2; + const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2; + const newState = HistoryUtil.getState(); + newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY }; + HistoryUtil.pushState(newState); + + let savedState = { px: this.Document.panX, py: this.Document.panY, s: this.Document.scale, pt: this.Document.panTransformType }; + + this.setPan(newPanX, newPanY); + this.Document.panTransformType = "Ease"; + this.props.focus(this.props.Document); + willZoom && this.setScaleToZoom(doc, scale); + + afterFocus && setTimeout(() => { + if (afterFocus && afterFocus()) { + this.Document.panX = savedState.px; + this.Document.panY = savedState.py; + this.Document.scale = savedState.s; + this.Document.panTransformType = savedState.pt; + } + }, 1000); } - console.log("Focused " + this.Document.title + " " + s); - afterFocus && setTimeout(() => { - if (afterFocus && afterFocus()) { - console.log("UnFocused " + this.Document.title + " " + s); - this.Document.panX = px; - this.Document.panY = py; - this.Document.scale = s; - } - }, 1000); + } setScaleToZoom = (doc: Doc, scale: number = 0.5) => { - let p = this.props; - let PanelHeight = p.PanelHeight(); - let panelWidth = p.PanelWidth(); - - let docHeight = NumCast(doc.height); - let docWidth = NumCast(doc.width); - let targetHeight = scale * PanelHeight; - let targetWidth = scale * panelWidth; - - let maxScaleX: number = targetWidth / docWidth; - let maxScaleY: number = targetHeight / docHeight; - let maxApplicableScale = Math.min(maxScaleX, maxScaleY); - this.Document.scale = maxApplicableScale; + this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc.width), this.props.PanelHeight() / NumCast(doc.height)); } zoomToScale = (scale: number) => { this.Document.scale = scale; } - getScale = () => this.Document.scale ? this.Document.scale : 1; + getScale = () => this.Document.scale || 1; getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps { return { @@ -663,7 +460,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { PanelHeight: childLayout[HeightSym], ContentScaling: returnOne, ContainingCollectionView: this.props.CollectionView, - ContainingCollectionDoc: this.props.CollectionView.props.Document, + ContainingCollectionDoc: this.props.Document, focus: this.focusDocument, backgroundColor: this.getClusterColor, parentActive: this.props.active, @@ -690,7 +487,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { PanelHeight: layoutDoc[HeightSym], ContentScaling: returnOne, ContainingCollectionView: this.props.CollectionView, - ContainingCollectionDoc: this.props.CollectionView.props.Document, + ContainingCollectionDoc: this.props.ContainingCollectionDoc, focus: this.focusDocument, backgroundColor: returnEmptyString, parentActive: this.props.active, @@ -713,110 +510,109 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } viewDefsToJSX = (views: any[]) => { - let elements: ViewDefResult[] = []; - if (Array.isArray(views)) { - elements = views.reduce<typeof elements>((prev, ele) => { - const jsx = this.viewDefToJSX(ele); - jsx && prev.push(jsx); - return prev; - }, elements); - } - return elements; + return !Array.isArray(views) ? [] : views.filter(ele => this.viewDefToJSX(ele)).map(ele => this.viewDefToJSX(ele)!); } private viewDefToJSX(viewDef: any): Opt<ViewDefResult> { if (viewDef.type === "text") { - const text = Cast(viewDef.text, "string"); + const text = Cast(viewDef.text, "string"); // don't use NumCast, StrCast, etc since we want to test for undefined below const x = Cast(viewDef.x, "number"); const y = Cast(viewDef.y, "number"); const z = Cast(viewDef.z, "number"); const width = Cast(viewDef.width, "number"); const height = Cast(viewDef.height, "number"); const fontSize = Cast(viewDef.fontSize, "number"); - if ([text, x, y, width, height].some(val => val === undefined)) { - return undefined; - } - - return { - ele: <div className="collectionFreeform-customText" style={{ - transform: `translate(${x}px, ${y}px)`, - width, height, fontSize - }}>{text}</div>, bounds: { x: x!, y: y!, z: z, width: width!, height: height! } - }; + return [text, x, y, width, height].some(val => val === undefined) ? undefined : + { + ele: <div className="collectionFreeform-customText" style={{ width, height, fontSize, transform: `translate(${x}px, ${y}px)` }}> + {text} + </div>, + bounds: { x: x!, y: y!, z: z, width: width!, height: height! } + }; } } - @computed.struct - get elements() { - if (this.Document.usePivotLayout) return PivotView.elements(this); - let curPage = FieldValue(this.Document.curPage, -1); - const initScript = this.Document.arrangeInit; - let state: any = undefined; - let pairs = this.childLayoutPairs; - let elements: ViewDefResult[] = []; - if (initScript) { - const initResult = initScript.script.run({ docs: pairs.map(pair => pair.layout), collection: this.Document }, console.log); - if (initResult.success) { - const result = initResult.result; - const { state: scriptState, views } = result; - state = scriptState; - elements = this.viewDefsToJSX(views); - } + lookupLayout = (doc: Doc, dataDoc?: Doc) => { + let data: any = undefined; + let computedElementData: { map: Map<{ layout: Doc, data?: Doc | undefined }, any>, elements: ViewDefResult[] }; + switch (this.Document.freeformLayoutEngine) { + case "pivot": computedElementData = this.doPivotLayout; break; + default: computedElementData = this.doFreeformLayout; break; } - let docviews = pairs.reduce((prev, pair) => { - var page = NumCast(pair.layout.page, -1); - if (!pair.layout.isMinimized && ((Math.abs(Math.round(page) - Math.round(curPage)) < 3) || page === -1)) { - const pos = this.getCalculatedPositions({ doc: pair.layout, index: prev.length, collection: this.Document, docs: pairs.map(pair => pair.layout), state }); - state = pos.state === undefined ? state : pos.state; - prev.push({ - ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} - ruleProvider={this.Document.isRuleProvider ? this.props.Document : this.props.ruleProvider} - jitterRotation={NumCast(this.props.Document.jitterRotation)} - transition={pos.transition} x={pos.x} y={pos.y} width={pos.width} height={pos.height} - {...this.getChildDocumentViewProps(pair.layout, pair.data)} />, - bounds: { x: pos.x || 0, y: pos.y || 0, z: pos.z, width: pos.width || 0, height: pos.height || 0 } - }); + computedElementData.map.forEach((value: any, key: { layout: Doc, data?: Doc }) => { + if (key.layout === doc && key.data === dataDoc) { + data = value; } - // } - return prev; - }, elements); + }); + return data && { x: data.x, y: data.y, z: data.z, width: data.width, height: data.height, transition: data.transition }; + } - return docviews; + @computed + get doPivotLayout() { + return computePivotLayout(this.props.Document, this.childDocs, + this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)), this.viewDefsToJSX); } - @computed.struct - get views() { - return this.elements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); + @computed + get doFreeformLayout() { + let layoutPoolData: Map<{ layout: Doc, data?: Doc }, any> = new Map(); + let layoutDocs = this.childLayoutPairs.map(pair => pair.layout); + const initResult = this.Document.arrangeInit && this.Document.arrangeInit.script.run({ docs: layoutDocs, collection: this.Document }, console.log); + let state = initResult && initResult.success ? initResult.result.scriptState : undefined; + let elements = initResult && initResult.success ? this.viewDefsToJSX(initResult.result.views) : []; + + 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; + layoutPoolData.set(pair, pos); + }); + return { map: layoutPoolData, elements: elements }; } - @computed.struct - get overlayViews() { - return this.elements.filter(ele => ele.bounds && ele.bounds.z).map(ele => ele.ele); + + @computed + get doLayoutComputation() { + let computedElementData: { map: Map<{ layout: Doc, data?: Doc | undefined }, any>, elements: ViewDefResult[] }; + switch (this.Document.freeformLayoutEngine) { + case "pivot": computedElementData = this.doPivotLayout; break; + default: computedElementData = this.doFreeformLayout; break; + } + this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).forEach(pair => + computedElementData.elements.push({ + ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} dataProvider={this.lookupLayout} + ruleProvider={this.Document.isRuleProvider ? this.props.Document : this.props.ruleProvider} + jitterRotation={NumCast(this.props.Document.jitterRotation)} {...this.getChildDocumentViewProps(pair.layout, pair.data)} />, + bounds: this.lookupLayout(pair.layout, pair.data) + })); + + return computedElementData; } + @computed.struct get elements() { return this.doLayoutComputation.elements; } + @computed.struct get views() { return this.elements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); } + @computed.struct get overlayViews() { return this.elements.filter(ele => ele.bounds && ele.bounds.z).map(ele => ele.ele); } + @action onCursorMove = (e: React.PointerEvent) => { super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); } - arrangeContents = async () => { - const docs = await DocListCastAsync(this.Document[this.props.fieldKey]); + layoutDocsInGrid = () => { UndoManager.RunInBatch(() => { - if (docs) { - let startX = this.Document.panX || 0; - let x = startX; - let y = this.Document.panY || 0; - let i = 0; - const width = Math.max(...docs.map(doc => NumCast(doc.width))); - const height = Math.max(...docs.map(doc => NumCast(doc.height))); - for (const doc of docs) { - doc.x = x; - doc.y = y; - x += width + 20; - if (++i === 6) { - i = 0; - x = startX; - y += height + 20; - } + const docs = DocListCast(this.Document[this.props.fieldKey]); + let startX = this.Document.panX || 0; + let x = startX; + let y = this.Document.panY || 0; + let i = 0; + const width = Math.max(...docs.map(doc => NumCast(doc.width))); + const height = Math.max(...docs.map(doc => NumCast(doc.height))); + for (const doc of docs) { + doc.x = x; + doc.y = y; + x += width + 20; + if (++i === 6) { + i = 0; + x = startX; + y += height + 20; } } }, "arrange contents"); @@ -852,10 +648,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } this._timelineRef.current!.timelineContextMenu(e.nativeEvent); layoutItems.push({ description: "reset view", event: () => { this.props.Document.panX = this.props.Document.panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" }); - layoutItems.push({ description: `${this.fitToBox ? "Unset" : "Set"} Fit To Container`, event: async () => this.Document.fitToBox = !this.fitToBox, icon: !this.fitToBox ? "expand-arrows-alt" : "compress-arrows-alt" }); + layoutItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: async () => this.Document.fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); layoutItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }); layoutItems.push({ description: `${this.Document.isRuleProvider ? "Stop Auto Format" : "Auto Format"}`, event: this.autoFormat, icon: "chalkboard" }); - layoutItems.push({ description: "Arrange contents in grid", event: this.arrangeContents, icon: "table" }); + layoutItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }); layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" }); layoutItems.push({ description: "Jitter Rotation", event: action(() => this.props.Document.jitterRotation = 10), icon: "paint-brush" }); layoutItems.push({ @@ -910,54 +706,26 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />, ...this.views ] - - public static AddCustomLayout(doc: Doc, dataKey: string): () => void { - return () => { - let addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => { - let overlayDisposer: () => void = emptyFunction; - const script = Cast(doc[key], ScriptField); - let originalText: string | undefined = undefined; - if (script) originalText = script.script.originalScript; - // tslint:disable-next-line: no-unnecessary-callback-wrapper - let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => { - const script = CompileScript(text, { - params, - requiredType, - typecheck: false - }); - if (!script.compiled) { - onError(script.errors.map(error => error.messageText).join("\n")); - return; - } - doc[key] = new ScriptField(script); - overlayDisposer(); - }} />; - overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, options); - }; - addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300, title: "Layout Initialization" }, { collection: "Doc", docs: "Doc[]" }, undefined); - addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300, title: "Layout Script" }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}"); - }; - } render() { // update the actual dimensions of the collection so that they can inquired (e.g., by a minimap) - this.props.Document.fitX = this.actualContentBounds && this.actualContentBounds.x; - this.props.Document.fitY = this.actualContentBounds && this.actualContentBounds.y; - this.props.Document.fitW = this.actualContentBounds && (this.actualContentBounds.r - this.actualContentBounds.x); - this.props.Document.fitH = this.actualContentBounds && (this.actualContentBounds.b - this.actualContentBounds.y); + this.props.Document.fitX = this.contentBounds && this.contentBounds.x; + this.props.Document.fitY = this.contentBounds && this.contentBounds.y; + this.props.Document.fitW = this.contentBounds && (this.contentBounds.r - this.contentBounds.x); + this.props.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y); // if fieldExt is set, then children will be stored in the extension document for the fieldKey. // otherwise, they are stored in fieldKey. All annotations to this document are stored in the extension document Doc.UpdateDocumentExtensionForField(this.props.DataDoc || this.props.Document, this.props.fieldKey); - const easing = () => this.props.Document.panTransformType === "Ease"; return ( <div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel} + style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (NumCast(this.props.Document.scrollHeight) ? NumCast(this.props.Document.scrollHeight) : "100%") : this.props.PanelHeight() }} onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu}> <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} isSelected={this.props.isSelected} - addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox} - getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}> + addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox} setPreviewCursor={this.props.setPreviewCursor} + getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} - easing={easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> + easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> <CollectionFreeFormLinksView {...this.props} key="freeformLinks"> - <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.fieldExtensionDoc} inkFieldKey={this._inkKey} > + <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.fieldExtensionDoc} inkFieldKey={"ink"} > {this.childViews} </InkingCanvas> </CollectionFreeFormLinksView> @@ -974,23 +742,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @observer class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> { - @computed get overlayView() { - return (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"} - renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />); - } render() { - return this.overlayView; + return <DocumentContentsView {...this.props} layoutKey={"overlayLayout"} + renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />; } } @observer class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> { - @computed get backgroundView() { - return (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"} - renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />); - } render() { - return this.props.Document.backgroundLayout ? this.backgroundView : (null); + return !this.props.Document.backgroundLayout ? (null) : + (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"} + renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />); } } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index bbea4a555..82193aefa 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -30,6 +30,8 @@ interface MarqueeViewProps { removeDocument: (doc: Doc) => boolean; addLiveTextDocument: (doc: Doc) => void; isSelected: () => boolean; + isAnnotationOverlay: boolean; + setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; } @observer @@ -43,6 +45,10 @@ export class MarqueeView extends React.Component<MarqueeViewProps> @observable _visible: boolean = false; _commandExecuted = false; + componentDidMount() { + this.props.setPreviewCursor && this.props.setPreviewCursor(this.setPreviewCursor); + } + @action cleanupInteractions = (all: boolean = false) => { if (all) { @@ -145,15 +151,10 @@ export class MarqueeView extends React.Component<MarqueeViewProps> } @action onPointerDown = (e: React.PointerEvent): void => { - this._downX = this._lastX = e.pageX; - this._downY = this._lastY = e.pageY; - this._commandExecuted = false; - PreviewCursor.Visible = false; - this.cleanupInteractions(true); + this._downX = this._lastX = e.clientX; + this._downY = this._lastY = e.clientY; if (e.button === 2 || (e.button === 0 && e.altKey)) { - document.addEventListener("pointermove", this.onPointerMove, true); - document.addEventListener("pointerup", this.onPointerUp, true); - document.addEventListener("keydown", this.marqueeCommand, true); + this.setPreviewCursor(e.clientX, e.clientY, true); if (e.altKey) { //e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors. e.preventDefault(); @@ -176,6 +177,8 @@ export class MarqueeView extends React.Component<MarqueeViewProps> e.stopPropagation(); e.preventDefault(); } + } else { + this.cleanupInteractions(true); // stop listening for events if another lower-level handle (e.g. another Marquee) has stopPropagated this } if (e.altKey) { e.preventDefault(); @@ -185,16 +188,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps> @action onPointerUp = (e: PointerEvent): void => { if (!this.props.container.props.active()) this.props.selectDocuments([this.props.container.props.Document]); - // console.log("pointer up!"); if (this._visible) { - // console.log("visible"); let mselect = this.marqueeSelect(); if (!e.shiftKey) { SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document); } this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]); } - //console.log("invisible"); this.cleanupInteractions(true); if (e.altKey) { @@ -202,11 +202,28 @@ export class MarqueeView extends React.Component<MarqueeViewProps> } } + setPreviewCursor = (x: number, y: number, drag: boolean) => { + if (drag) { + this._downX = this._lastX = x; + this._downY = this._lastY = y; + this._commandExecuted = false; + PreviewCursor.Visible = false; + this.cleanupInteractions(true); + document.addEventListener("pointermove", this.onPointerMove, true); + document.addEventListener("pointerup", this.onPointerUp, true); + document.addEventListener("keydown", this.marqueeCommand, true); + } else { + this._downX = x; + this._downY = y; + PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument); + } + } + @action onClick = (e: React.MouseEvent): void => { if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { - PreviewCursor.Show(e.clientX, e.clientY, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument); + this.setPreviewCursor(e.clientX, e.clientY, false); // let the DocumentView stopPropagation of this event when it selects this document } else { // why do we get a click event when the cursor have moved a big distance? // let's cut it off here so no one else has to deal with it. @@ -297,8 +314,8 @@ export class MarqueeView extends React.Component<MarqueeViewProps> y: bounds.top, panX: 0, panY: 0, - backgroundColor: this.props.container.isAnnotationOverlay ? undefined : chosenColor, - defaultBackgroundColor: this.props.container.isAnnotationOverlay ? undefined : chosenColor, + backgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor, + defaultBackgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor, width: bounds.width, height: bounds.height, title: "a nested collection", |
