diff options
| author | Bob Zeleznik <zzzman@gmail.com> | 2019-08-15 22:36:36 -0400 |
|---|---|---|
| committer | Bob Zeleznik <zzzman@gmail.com> | 2019-08-15 22:36:36 -0400 |
| commit | eb8033aea536e817d49379f50530feca13d0c115 (patch) | |
| tree | 3b1ebe6cb38390b9b4485bbe87fb3194c41a06ad /src/client/views/collections | |
| parent | 5da62eef1dbf9509ac7bce73d927d835d98716d4 (diff) | |
| parent | 46386fd73a06e52d606f891c812a9af0598eec79 (diff) | |
Merge branch 'master' into monika_ellie_UI
Diffstat (limited to 'src/client/views/collections')
14 files changed, 768 insertions, 376 deletions
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 6801b94fd..4a296493a 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -22,6 +22,24 @@ export enum CollectionViewType { Masonry } +export namespace CollectionViewType { + + const stringMapping = new Map<string, CollectionViewType>([ + ["invalid", CollectionViewType.Invalid], + ["freeform", CollectionViewType.Freeform], + ["schema", CollectionViewType.Schema], + ["docking", CollectionViewType.Docking], + ["tree", CollectionViewType.Tree], + ["stacking", CollectionViewType.Stacking], + ["masonry", CollectionViewType.Masonry] + ]); + + export const valueOf = (value: string) => { + return stringMapping.get(value.toLowerCase()); + }; + +} + export interface CollectionRenderProps { addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; removeDocument: (document: Doc) => boolean; @@ -65,7 +83,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { active = (): boolean => { var isSelected = this.props.isSelected(); - return isSelected || this._isChildActive || this.props.renderDepth === 0 || BoolCast(this.props.Document.excludeFromLibrary); + return isSelected || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0 || BoolCast(this.props.Document.excludeFromLibrary); } //TODO should this be observable? @@ -81,7 +99,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { addDocument(doc: Doc, allowDuplicates: boolean = false): boolean { var curPage = NumCast(this.props.Document.curPage, -1); Doc.GetProto(doc).page = curPage; - if (curPage >= 0) { + if (this.props.fieldExt) { // bcz: fieldExt !== undefined means this is an overlay layer Doc.GetProto(doc).annotationOn = this.props.Document; } allowDuplicates = true; @@ -108,8 +126,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { let value = Cast(targetDataDoc[targetField], listSpec(Doc), []); let index = value.reduce((p, v, i) => (v instanceof Doc && v[Id] === doc[Id]) ? i : p, -1); PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn => - annotationOn === this.dataDoc.Document && (doc.annotationOn = undefined) - ); + annotationOn === this.dataDoc.Document && (doc.annotationOn = undefined)); if (index !== -1) { value.splice(index, 1); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index f559480ed..7332a490a 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -18,7 +18,6 @@ import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; import { undoBatch, UndoManager } from "../../util/UndoManager"; import { DocumentView } from "../nodes/DocumentView"; -import { CollectionViewType } from './CollectionBaseView'; import "./CollectionDockingView.scss"; import { SubCollectionViewProps } from "./CollectionSubView"; import { ParentDocSelector } from './ParentDocumentSelector'; @@ -390,7 +389,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp const stack = tab.contentItem.parent; // shifts the focus to this tab when another tab is dragged over it tab.element[0].onmouseenter = (e: any) => { - if (!this._isPointerDown) return; + if (!this._isPointerDown || !SelectionManager.GetIsDragging()) return; var activeContentItem = tab.header.parent.getActiveContentItem(); if (tab.contentItem !== activeContentItem) { tab.header.parent.setActiveContentItem(tab.contentItem); @@ -410,10 +409,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp tab.reactComponents = [dragSpan, upDiv]; tab.element.append(dragSpan); tab.element.append(upDiv); - tab.reactionDisposer = reaction(() => [doc.title], - () => { - tab.titleElement[0].textContent = doc.title; - }, { fireImmediately: true }); + tab.reactionDisposer = reaction(() => [doc.title, Doc.IsBrushedDegree(doc)], () => { + tab.titleElement[0].textContent = doc.title, { fireImmediately: true }; + tab.titleElement[0].style.outline = `${["transparent", "white", "white"][Doc.IsBrushedDegree(doc)]} ${["none", "dashed", "solid"][Doc.IsBrushedDegree(doc)]} 1px`; + }); //TODO why can't this just be doc instead of the id? tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; } @@ -421,9 +420,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp tab.titleElement[0].Tab = tab; tab.closeElement.off('click') //unbind the current click handler .click(async function () { - if (tab.reactionDisposer) { - tab.reactionDisposer(); - } + tab.reactionDisposer && tab.reactionDisposer(); let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId); if (doc instanceof Doc) { let theDoc = doc; @@ -511,7 +508,7 @@ interface DockedFrameProps { } @observer export class DockedFrameRenderer extends React.Component<DockedFrameProps> { - _mainCont = React.createRef<HTMLDivElement>(); + _mainCont: HTMLDivElement | undefined = undefined; @observable private _panelWidth = 0; @observable private _panelHeight = 0; @observable private _document: Opt<Doc>; @@ -551,14 +548,15 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { private onActiveContentItemChanged() { if (this.props.glContainer.tab) { this._isActive = this.props.glContainer.tab.isActive; + !this._isActive && this._document && Doc.UnBrushDoc(this._document); // bcz: bad -- trying to simulate a pointer leave event when a new tab is opened up on top of an existing one. } } - panelWidth = () => Math.min(this._panelWidth, Math.max(NumCast(this._document!.width), this.nativeWidth())); - panelHeight = () => Math.min(this._panelHeight, Math.max(NumCast(this._document!.height), NumCast(this._document!.nativeHeight, this._panelHeight))); + 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))); - nativeWidth = () => !BoolCast(this._document!.ignoreAspect) ? NumCast(this._document!.nativeWidth, this._panelWidth) : 0; - nativeHeight = () => !BoolCast(this._document!.ignoreAspect) ? NumCast(this._document!.nativeHeight, this._panelHeight) : 0; + nativeWidth = () => !this._document!.ignoreAspect ? NumCast(this._document!.nativeWidth) || this._panelWidth : 0; + nativeHeight = () => !this._document!.ignoreAspect ? NumCast(this._document!.nativeHeight) || this._panelHeight : 0; contentScaling = () => { const nativeH = this.nativeHeight(); @@ -569,9 +567,9 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } ScreenToLocalTransform = () => { - if (this._mainCont.current && this._mainCont.current.children) { - let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current.children[0].firstChild as HTMLElement); - scale = Utils.GetScreenTransform(this._mainCont.current).scale; + if (this._mainCont && this._mainCont!.children) { + let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.children[0].firstChild as HTMLElement); + scale = Utils.GetScreenTransform(this._mainCont).scale; return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale); } return Transform.Identity(); @@ -616,7 +614,13 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { @computed get content() { return ( - <div className="collectionDockingView-content" ref={this._mainCont} + <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 >); diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx index 70010819a..8eda4d9ee 100644 --- a/src/client/views/collections/CollectionPDFView.tsx +++ b/src/client/views/collections/CollectionPDFView.tsx @@ -1,66 +1,31 @@ -import { action, IReactionDisposer, observable, reaction } from "mobx"; +import { computed } from "mobx"; import { observer } from "mobx-react"; -import { WidthSym, HeightSym } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; -import { NumCast } from "../../../new_fields/Types"; 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"; import React = require("react"); -import { PDFBox } from "../nodes/PDFBox"; @observer export class CollectionPDFView extends React.Component<FieldViewProps> { - private _pdfBox?: PDFBox; - private _reactionDisposer?: IReactionDisposer; - private _buttonTray: React.RefObject<HTMLDivElement>; - - constructor(props: FieldViewProps) { - super(props); - - this._buttonTray = React.createRef(); - } - - componentDidMount() { - this._reactionDisposer = reaction( - () => NumCast(this.props.Document.scrollY), - () => { - this.props.Document.panY = NumCast(this.props.Document.scrollY); - }, - { fireImmediately: true } - ); - } - - componentWillUnmount() { - this._reactionDisposer && this._reactionDisposer(); - } - public static LayoutString(fieldKey: string = "data", fieldExt: string = "annotations") { return FieldView.LayoutString(CollectionPDFView, fieldKey, fieldExt); } - @observable _inThumb = false; - private set curPage(value: number) { this._pdfBox && this._pdfBox.GotoPage(value); } - private get curPage() { return NumCast(this.props.Document.curPage, -1); } - private get numPages() { return NumCast(this.props.Document.numPages); } - @action onPageBack = () => this._pdfBox && this._pdfBox.BackPage(); - @action onPageForward = () => this._pdfBox && this._pdfBox.ForwardPage(); + private _pdfBox?: PDFBox; + private _buttonTray: React.RefObject<HTMLDivElement> = React.createRef(); - nativeWidth = () => NumCast(this.props.Document.nativeWidth); - nativeHeight = () => NumCast(this.props.Document.nativeHeight); - private get uIButtons() { - let ratio = (this.curPage - 1) / this.numPages * 100; + @computed + get uIButtons() { return ( <div className="collectionPdfView-buttonTray" ref={this._buttonTray} key="tray" style={{ height: "100%" }}> - <button className="collectionPdfView-backward" onClick={this.onPageBack}>{"<"}</button> - <button className="collectionPdfView-forward" onClick={this.onPageForward}>{">"}</button> - {/* <div className="collectionPdfView-slider" onPointerDown={this.onThumbDown} style={{ top: 60, left: -20, width: 50, height: `calc(100% - 80px)` }} > - <div className="collectionPdfView-thumb" onPointerDown={this.onThumbDown} style={{ top: `${ratio}%`, width: 50, height: 50 }} /> - </div> */} + <button className="collectionPdfView-backward" onClick={() => this._pdfBox && this._pdfBox.BackPage()}>{"<"}</button> + <button className="collectionPdfView-forward" onClick={() => this._pdfBox && this._pdfBox.ForwardPage()}>{">"}</button> </div> ); } @@ -73,20 +38,16 @@ export class CollectionPDFView extends React.Component<FieldViewProps> { setPdfBox = (pdfBox: PDFBox) => { this._pdfBox = pdfBox; }; - - private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => { - let props = { ...this.props, ...renderProps }; - return ( - <> - <CollectionFreeFormView {...props} setPdfBox={this.setPdfBox} CollectionView={this} chromeCollapsed={true} /> - {renderProps.active() ? this.uIButtons : (null)} - </> - ); + subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => { + return (<> + <CollectionFreeFormView {...this.props} {...renderProps} setPdfBox={this.setPdfBox} CollectionView={this} chromeCollapsed={true} /> + {renderProps.active() ? this.uIButtons : (null)} + </>); } render() { return ( - <CollectionBaseView {...this.props} className={`collectionPdfView-cont${this._inThumb ? "-dragging" : ""}`} onContextMenu={this.onContextMenu}> + <CollectionBaseView {...this.props} className={"collectionPdfView-cont"} onContextMenu={this.onContextMenu}> {this.subView} </CollectionBaseView> ); diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index 01744fb34..e0cedc210 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -22,30 +22,6 @@ overflow: scroll; } - .collectionSchemaView-previewRegion { - position: relative; - background: $light-color; - height: 100%; - - .collectionSchemaView-previewDoc { - height: 100%; - width: 100%; - position: absolute; - } - - .collectionSchemaView-input { - position: absolute; - max-width: 150px; - width: 100%; - bottom: 0px; - } - - .documentView-node:first-child { - position: relative; - background: $light-color; - } - } - .collectionSchemaView-dividerDragger { position: relative; height: 100%; @@ -62,6 +38,30 @@ } } +.collectionSchemaView-previewRegion { + position: relative; + background: $light-color; + height: auto !important; + + .collectionSchemaView-previewDoc { + height: 100%; + width: 100%; + position: absolute; + } + + .collectionSchemaView-input { + position: absolute; + max-width: 150px; + width: 100%; + bottom: 0px; + } + + .documentView-node:first-child { + position: relative; + background: $light-color; + } +} + .ReactTable { width: 100%; background: white; diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 75787c0a8..4537dcc85 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -30,7 +30,7 @@ import { undoBatch } from "../../util/UndoManager"; import { CollectionSchemaHeader, CollectionSchemaAddColumnHeader } from "./CollectionSchemaHeaders"; import { CellProps, CollectionSchemaCell, CollectionSchemaNumberCell, CollectionSchemaStringCell, CollectionSchemaBooleanCell, CollectionSchemaCheckboxCell, CollectionSchemaDocCell } from "./CollectionSchemaCells"; import { MovableColumn, MovableRow } from "./CollectionSchemaMovableTableHOC"; -import { ComputedField } from "../../../new_fields/ScriptField"; +import { ComputedField, ScriptField } from "../../../new_fields/ScriptField"; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; @@ -50,7 +50,7 @@ const columnTypes: Map<string, ColumnType> = new Map([ ["title", ColumnType.String], ["x", ColumnType.Number], ["y", ColumnType.Number], ["width", ColumnType.Number], ["height", ColumnType.Number], ["nativeWidth", ColumnType.Number], ["nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean], - ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["libraryBrush", ColumnType.Boolean], ["zIndex", ColumnType.Number] + ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["zIndex", ColumnType.Number] ]); @observer @@ -303,13 +303,13 @@ export class SchemaTable extends React.Component<SchemaTableProps> { return resized; }, [] as { "id": string, "value": number }[]); } - @computed get sorted(): { "id": string, "desc"?: true }[] { + @computed get sorted(): { id: string, desc: boolean }[] { return this.columns.reduce((sorted, shf) => { if (shf.desc) { sorted.push({ "id": shf.heading, "desc": shf.desc }); } return sorted; - }, [] as { "id": string, "desc"?: true }[]); + }, [] as { id: string, desc: boolean }[]); } @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } @@ -899,6 +899,7 @@ interface CollectionSchemaPreviewProps { height: () => number; showOverlays?: (doc: Doc) => { title?: string, caption?: string }; CollectionView?: CollectionView | CollectionPDFView | CollectionVideoView; + onClick?: ScriptField; getTransform: () => Transform; addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; moveDocument: (document: Doc, target: Doc, addDoc: ((doc: Doc) => boolean)) => boolean; @@ -988,23 +989,24 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre DataDoc={this.props.DataDocument} Document={this.props.Document} fitToBox={this.props.fitToBox} - renderDepth={this.props.renderDepth + 1} - selectOnLoad={false} + onClick={this.props.onClick} showOverlays={this.props.showOverlays} addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} moveDocument={this.props.moveDocument} + whenActiveChanged={this.props.whenActiveChanged} + ContainingCollectionView={this.props.CollectionView} + addDocTab={this.props.addDocTab} + parentActive={this.props.active} ScreenToLocalTransform={this.getTransform} + renderDepth={this.props.renderDepth + 1} + selectOnLoad={false} ContentScaling={this.contentScaling} PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight} - ContainingCollectionView={this.props.CollectionView} focus={emptyFunction} backgroundColor={returnEmptyString} - parentActive={this.props.active} - whenActiveChanged={this.props.whenActiveChanged} bringToFront={emptyFunction} - addDocTab={this.props.addDocTab} zoomToScale={emptyFunction} getScale={returnOne} /> diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 271ad2d58..01d4ea2b6 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -1,10 +1,15 @@ @import "../globalCssVariables"; -.collectionStackingView { +.collectionMasonryView { + display:inline; +} +.collectionStackingView{ + display: flex; +} +.collectionStackingView, .collectionMasonryView{ height: 100%; width: 100%; position: absolute; - display: flex; top: 0; overflow-y: auto; flex-wrap: wrap; @@ -31,14 +36,20 @@ .collectionStackingView-masonrySingle, .collectionStackingView-masonryGrid { width: 100%; - height: 100%; - position: absolute; display: grid; top: 0; left: 0; - width: 100%; + } + .collectionStackingView-masonrySingle { + height: 100%; position: absolute; } + .collectionStackingView-masonryGrid { + margin: auto; + height: max-content; + position: relative; + grid-auto-rows: 0px; + } .collectionStackingView-masonrySingle { width: 100%; @@ -80,12 +91,17 @@ height: 100%; margin: auto; } + + .collectionStackingView-masonrySection { + margin: auto; + } .collectionStackingView-sectionHeader { text-align: center; margin-left: 2px; margin-right: 2px; margin-top: 10px; + background: gray; // overflow: hidden; overflow is visible so the color menu isn't hidden -ftong .editableView-input { diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 4a751c84c..4add7774e 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -9,8 +9,8 @@ import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; -import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { emptyFunction } from "../../../Utils"; +import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from "../../../new_fields/Types"; +import { emptyFunction, Utils, numberRange } from "../../../Utils"; import { DocumentType } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; import { Transform } from "../../util/Transform"; @@ -20,24 +20,35 @@ import { CollectionSchemaPreview } from "./CollectionSchemaView"; import "./CollectionStackingView.scss"; import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewFieldColumn"; import { CollectionSubView } from "./CollectionSubView"; +import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from "../ContextMenuItem"; +import { ScriptBox } from "../ScriptBox"; @observer export class CollectionStackingView extends CollectionSubView(doc => doc) { _masonryGridRef: HTMLDivElement | null = null; _draggerRef = React.createRef<HTMLDivElement>(); _heightDisposer?: IReactionDisposer; + _childLayoutDisposer?: IReactionDisposer; _sectionFilterDisposer?: IReactionDisposer; _docXfs: any[] = []; _columnStart: number = 0; @observable private cursor: CursorProperty = "grab"; - get sectionHeaders() { return Cast(this.props.Document.sectionHeaders, listSpec(SchemaHeaderField)); } + @computed get sectionHeaders() { return Cast(this.props.Document.sectionHeaders, listSpec(SchemaHeaderField)); } + @computed get sectionFilter() { return StrCast(this.props.Document.sectionFilter); } + @computed get filteredChildren() { return this.childDocs.filter(d => !d.isMinimized); } @computed get xMargin() { return NumCast(this.props.Document.xMargin, 2 * this.gridGap); } @computed get yMargin() { return NumCast(this.props.Document.yMargin, 2 * this.gridGap); } @computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); } - @computed get singleColumn() { return BoolCast(this.props.Document.singleColumn, true); } - @computed get columnWidth() { return this.singleColumn ? (this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin) : Math.min(this.props.PanelWidth() - 2 * this.xMargin, NumCast(this.props.Document.columnWidth, 250)); } - @computed get filteredChildren() { return this.childDocs.filter(d => !d.isMinimized); } - @computed get sectionFilter() { return this.singleColumn ? StrCast(this.props.Document.sectionFilter) : ""; } + @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 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)); + } + + childDocHeight(child: Doc) { return this.getDocHeight(Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, child).layout); } get layoutDoc() { // if this document's layout field contains a document (ie, a rendering template), then we will use that @@ -45,12 +56,12 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document; } - get Sections() { - if (!this.sectionFilter) return new Map<SchemaHeaderField, Doc[]>(); + if (!this.sectionFilter || this.sectionHeaders instanceof Promise) return new Map<SchemaHeaderField, Doc[]>(); if (this.sectionHeaders === undefined) { - this.props.Document.sectionHeaders = new List<SchemaHeaderField>(); + setTimeout(() => this.props.Document.sectionHeaders = new List<SchemaHeaderField>(), 0); + return new Map<SchemaHeaderField, Doc[]>(); } const sectionHeaders = this.sectionHeaders!; let fields = new Map<SchemaHeaderField, Doc[]>(sectionHeaders.map(sh => [sh, []] as [SchemaHeaderField, []])); @@ -75,18 +86,26 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } componentDidMount() { - // is there any reason this needs to exist? -syip - this._heightDisposer = reaction(() => [this.yMargin, this.props.Document[WidthSym](), this.gridGap, this.columnWidth, this.childDocs.map(d => [d.height, d.width, d.zoomBasis, d.nativeHeight, d.nativeWidth, d.isMinimized])], - () => { - if (this.singleColumn && BoolCast(this.props.Document.autoHeight)) { - let hgt = this.Sections.size * 50 + this.filteredChildren.reduce((height, d, i) => { - let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d); - return height + this.getDocHeight(pair.layout) + (i === this.filteredChildren.length - 1 ? this.yMargin : this.gridGap); - }, this.yMargin); - (this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc) - .height = hgt * (this.props as any).ContentScaling(); - } - }, { fireImmediately: true }); + this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)], + async (args) => args[1] instanceof Doc && + this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc), undefined))); + + // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported). + this._heightDisposer = reaction(() => { + if (this.isStackingView && BoolCast(this.props.Document.autoHeight)) { + let sectionsList = Array.from(this.Sections.size ? this.Sections.values() : [this.filteredChildren]); + return this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => Math.max(maxHght, + 50 + s.reduce((height, d, i) => height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap), this.yMargin) + ), 0); + } + return -1; + }, + (hgt: number) => { + let doc = hgt === -1 ? undefined : this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; + doc && (doc.height = hgt); + }, + { fireImmediately: true } + ); // reset section headers when a new filter is inputted this._sectionFilterDisposer = reaction( @@ -95,6 +114,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { ); } componentWillUnmount() { + this._childLayoutDisposer && this._childLayoutDisposer(); this._heightDisposer && this._heightDisposer(); this._sectionFilterDisposer && this._sectionFilterDisposer(); } @@ -109,9 +129,12 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } overlays = (doc: Doc) => { - return doc.type === DocumentType.IMG || doc.type === DocumentType.VID ? { title: "title", caption: "caption" } : {}; + return doc.type === DocumentType.IMG || doc.type === DocumentType.VID ? { title: StrCast(this.props.Document.showTitles), caption: StrCast(this.props.Document.showCaptions) } : {}; } + @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } + @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : ScriptCast(this.Document.onChildClick); } + getDisplayDoc(layoutDoc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) { let height = () => this.getDocHeight(layoutDoc); let finalDxf = () => dxf().scale(this.columnWidth / layoutDoc[WidthSym]()); @@ -120,7 +143,8 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { DataDocument={dataDoc} showOverlays={this.overlays} renderDepth={this.props.renderDepth} - fitToBox={true} + fitToBox={this.props.fitToBox} + onClick={layoutDoc.isTemplate ? this.onClickHandler : this.onChildClickHandler} width={width} height={height} getTransform={finalDxf} @@ -135,12 +159,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { previewScript={undefined}> </CollectionSchemaPreview>; } - getDocHeight(d: Doc, columnScale: number = 1) { + getDocHeight(d: Doc) { let nw = NumCast(d.nativeWidth); let nh = NumCast(d.nativeHeight); - if (!BoolCast(d.ignoreAspect) && nw && nh) { + if (!d.ignoreAspect && nw && nh) { let aspect = nw && nh ? nh / nw : 1; - let wid = Math.min(d[WidthSym](), this.columnWidth / columnScale); + 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](); @@ -226,14 +251,14 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { }); } headings = () => Array.from(this.Sections.keys()); - section = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { + sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { let key = this.sectionFilter; let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined; let types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]); if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) { type = types[0]; } - let cols = () => this.singleColumn ? 1 : Math.max(1, Math.min(this.filteredChildren.length, + let cols = () => this.isStackingView ? 1 : Math.max(1, Math.min(this.filteredChildren.length, Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))); return <CollectionStackingViewFieldColumn key={heading ? heading.heading : ""} @@ -249,6 +274,55 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { />; } + getDocTransform(doc: Doc, dref: HTMLDivElement) { + let { scale, translateX, translateY } = Utils.GetScreenTransform(dref); + let outerXf = Utils.GetScreenTransform(this._masonryGridRef!); + let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); + return this.props.ScreenToLocalTransform(). + translate(offset[0], offset[1]). + scale(NumCast(doc.width, 1) / this.columnWidth); + } + masonryChildren(docs: Doc[]) { + this._docXfs.length = 0; + return docs.map((d, i) => { + let dref = React.createRef<HTMLDivElement>(); + let layoutDoc = Doc.expandTemplateLayout(d, this.props.DataDoc); + let width = () => (d.nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth);/// (uniqueHeadings.length + 1); + let height = () => this.getDocHeight(layoutDoc); + let dxf = () => this.getDocTransform(layoutDoc, dref.current!); + let rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); + this._docXfs.push({ dxf: dxf, width: width, height: height }); + return <div className="collectionStackingView-masonryDoc" key={d[Id]} ref={dref} style={{ gridRowEnd: `span ${rowSpan}` }} > + {this.getDisplayDoc(layoutDoc, d, dxf, width)} + </div>; + }); + } + + @observable _headingsHack: number = 1; + sectionMasonry(heading: SchemaHeaderField | undefined, docList: Doc[]) { + let cols = Math.max(1, Math.min(docList.length, + Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))); + return <div key={heading ? heading.heading : "empty"} className="collectionStackingView-masonrySection"> + {!heading ? (null) : + <div key={`${heading.heading}`} className="collectionStackingView-sectionHeader" style={{ background: heading.color }} + onClick={action(() => this._headingsHack++ && heading.setCollapsed(!heading.collapsed))} > + {heading.heading} + </div>} + {this._headingsHack && heading && heading.collapsed ? (null) : + <div key={`${heading}-stack`} className={`collectionStackingView-masonryGrid`} + style={{ + padding: `${this.yMargin}px ${this.xMargin}px`, + width: `${cols * (this.columnWidth + this.gridGap) + 2 * this.xMargin - this.gridGap}px`, + gridGap: this.gridGap, + gridTemplateColumns: numberRange(cols).reduce((list, i) => list + ` ${this.columnWidth}px`, ""), + }}> + {this.masonryChildren(docList)} + {this.columnDragger} + </div> + } + </div>; + } + @action addGroup = (value: string) => { if (value && this.sectionHeaders) { @@ -266,11 +340,22 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } onToggle = (checked: Boolean) => { - this.props.CollectionView.props.Document.chromeSatus = checked ? "collapsed" : "view-mode"; + this.props.CollectionView.props.Document.chromeStatus = checked ? "collapsed" : "view-mode"; + } + + onContextMenu = (e: React.MouseEvent): void => { + // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout + if (!e.isPropagationStopped()) { + let subItems: ContextMenuProps[] = []; + subItems.push({ description: `${this.props.Document.fillColumn ? "Variable Size" : "Autosize"} Column`, event: () => this.props.Document.fillColumn = !this.props.Document.fillColumn, icon: "plus" }); + subItems.push({ description: `${this.props.Document.showTitles ? "Hide Titles" : "Show Titles"}`, event: () => this.props.Document.showTitles = !this.props.Document.showTitles ? "title" : "", icon: "plus" }); + subItems.push({ description: `${this.props.Document.showCaptions ? "Hide Captions" : "Show Captions"}`, event: () => this.props.Document.showCaptions = !this.props.Document.showCaptions ? "caption" : "", icon: "plus" }); + subItems.push({ description: "Edit onChildClick script", icon: "edit", event: () => ScriptBox.EditClickScript(this.props.Document, "onChildClick") }); + ContextMenu.Instance.addItem({ description: "Stacking Options ...", subitems: subItems, icon: "eye" }); + } } render() { - let headings = Array.from(this.Sections.keys()); let editableViewProps = { GetValue: () => "", SetValue: this.addGroup, @@ -278,18 +363,16 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { }; Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey); - // let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); + let sections = (this.sectionFilter ? Array.from(this.Sections.entries()).sort(this.sortFunc) : [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]]); return ( - <div className="collectionStackingView" - ref={this.createRef} onDrop={this.onDrop.bind(this)} onWheel={(e: React.WheelEvent) => e.stopPropagation()} > - {this.sectionFilter ? Array.from(this.Sections.entries()).sort(this.sortFunc). - map((section: [SchemaHeaderField, Doc[]]) => this.section(section[0], section[1])) : - this.section(undefined, this.filteredChildren)} - {(this.sectionFilter && (this.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.CollectionView.props.Document.chromeStatus !== 'disabled')) ? + <div className={this.isStackingView ? "collectionStackingView" : "collectionMasonryView"} + ref={this.createRef} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onWheel={(e: React.WheelEvent) => e.stopPropagation()} > + {sections.map(section => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1]))} + {!this.showAddAGroup ? (null) : <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" - style={{ width: (this.columnWidth / (headings.length + ((this.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? 1 : 0))) - 10, marginTop: 10 }}> + style={{ width: this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}> <EditableView {...editableViewProps} /> - </div> : null} + </div>} {this.props.CollectionView.props.Document.chromeStatus !== 'disabled' ? <Switch onChange={this.onToggle} onClick={this.onToggle} diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index df03da376..74c7ef305 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -1,28 +1,25 @@ import React = require("react"); +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faPalette } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, observable } from "mobx"; import { observer } from "mobx-react"; -import { number } from "prop-types"; import { Doc, WidthSym } from "../../../new_fields/Doc"; -import { CollectionStackingView } from "./CollectionStackingView"; import { Id } from "../../../new_fields/FieldSymbols"; +import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; +import { ScriptField } from "../../../new_fields/ScriptField"; +import { NumCast, StrCast } from "../../../new_fields/Types"; import { Utils } from "../../../Utils"; -import { NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; -import { EditableView } from "../EditableView"; -import { action, observable, computed } from "mobx"; -import { undoBatch } from "../../util/UndoManager"; -import { DragManager } from "../../util/DragManager"; -import { DocumentManager } from "../../util/DocumentManager"; -import { SelectionManager } from "../../util/SelectionManager"; -import "./CollectionStackingView.scss"; import { Docs } from "../../documents/Documents"; -import { SchemaHeaderField, PastelSchemaPalette } from "../../../new_fields/SchemaHeaderField"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { ScriptField } from "../../../new_fields/ScriptField"; +import { DragManager } from "../../util/DragManager"; import { CompileScript } from "../../util/Scripting"; -import { RichTextField } from "../../../new_fields/RichTextField"; +import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; -import { Flyout, anchorPoints } from "../DocumentDecorations"; -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faPalette } from '@fortawesome/free-solid-svg-icons'; +import { undoBatch } from "../../util/UndoManager"; +import { anchorPoints, Flyout } from "../DocumentDecorations"; +import { EditableView } from "../EditableView"; +import { CollectionStackingView } from "./CollectionStackingView"; +import "./CollectionStackingView.scss"; library.add(faPalette); @@ -81,38 +78,17 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC let parent = this.props.parent; parent._docXfs.length = 0; return docs.map((d, i) => { - let headings = this.props.headings(); - let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); let pair = Doc.GetLayoutDataDocPair(parent.props.Document, parent.props.DataDoc, parent.props.fieldKey, d); - let width = () => (d.nativeWidth && !BoolCast(d.ignoreAspect) ? Math.min(pair.layout[WidthSym](), parent.columnWidth / (uniqueHeadings.length + 1)) : parent.columnWidth / (uniqueHeadings.length + 1));/// (uniqueHeadings.length + 1); - let height = () => parent.getDocHeight(pair.layout, uniqueHeadings.length + 1);// / (d.nativeWidth && !BoolCast(d.ignoreAspect) ? uniqueHeadings.length + 1 : 1); + let width = () => Math.min(d.nativeWidth && !d.ignoreAspect && !parent.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, parent.columnWidth / parent.numGroupColumns); + let height = () => parent.getDocHeight(pair.layout); let dref = React.createRef<HTMLDivElement>(); - // if (uniqueHeadings.length > 0) { let dxf = () => this.getDocTransform(pair.layout, dref.current!); this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height }); - // } - // else { - // //have to add the height of all previous single column sections or the doc decorations will be in the wrong place. - // let dxf = () => this.getDocTransform(layoutDoc, i, width()); - // this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height }); - // } - let rowHgtPcnt = height(); let rowSpan = Math.ceil((height() + parent.gridGap) / parent.gridGap); - let style = parent.singleColumn ? { width: width(), margin: "auto", marginTop: i === 0 ? 0 : parent.gridGap, height: `${rowHgtPcnt}` } : { gridRowEnd: `span ${rowSpan}` }; - return <div className={`collectionStackingView-${parent.singleColumn ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} > + let style = parent.isStackingView ? { width: width(), margin: "auto", marginTop: i === 0 ? 0 : parent.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` }; + return <div className={`collectionStackingView-${parent.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} > {this.props.parent.getDisplayDoc(pair.layout, pair.data, dxf, width)} </div>; - // } else { - // let dref = React.createRef<HTMLDivElement>(); - // let dxf = () => this.getDocTransform(layoutDoc, dref.current!); - // this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height }); - // let rowHgtPcnt = height(); - // let rowSpan = Math.ceil((height() + parent.gridGap) / parent.gridGap); - // let divStyle = parent.singleColumn ? { width: width(), marginTop: i === 0 ? 0 : parent.gridGap, height: `${rowHgtPcnt}` } : { gridRowEnd: `span ${rowSpan}` }; - // return <div className="collectionStackingView-masonryDoc" key={d[Id]} ref={dref} style={divStyle} > - // {this.props.parent.getDisplayDoc(layoutDoc, d, dxf, width)} - // </div>; - // } }); } @@ -278,7 +254,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC let headings = this.props.headings(); let heading = this._heading; let style = this.props.parent; - let singleColumn = style.singleColumn; + let singleColumn = style.isStackingView; let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); let evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`; let headerEditableViewProps = { @@ -326,9 +302,9 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC </button>} </div> </div> : (null); - for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth}px `; + for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `; return ( - <div 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.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.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"}`} @@ -348,7 +324,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC </div> {(this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" - style={{ width: style.columnWidth / (uniqueHeadings.length + 1) }}> + style={{ width: style.columnWidth / style.numGroupColumns }}> <EditableView {...newEditableViewProps} /> </div> : null} </div> diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 02b2583cd..4b1fca18a 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -25,9 +25,8 @@ import { CollectionSchemaPreview } from './CollectionSchemaView'; import { CollectionSubView } from "./CollectionSubView"; import "./CollectionTreeView.scss"; import React = require("react"); -import { ComputedField } from '../../../new_fields/ScriptField'; +import { ComputedField, ScriptField } from '../../../new_fields/ScriptField'; import { KeyValueBox } from '../nodes/KeyValueBox'; -import { exportNamedDeclaration } from 'babel-types'; export interface TreeViewProps { @@ -71,8 +70,9 @@ class TreeView extends React.Component<TreeViewProps> { private _header?: React.RefObject<HTMLDivElement> = React.createRef(); private _treedropDisposer?: DragManager.DragDropDisposer; private _dref = React.createRef<HTMLDivElement>(); + get defaultExpandedView() { return this.childDocs ? this.fieldKey : "fields"; } @observable _collapsed: boolean = true; - @computed get treeViewExpandedView() { return StrCast(this.props.document.treeViewExpandedView, "fields"); } + @computed get treeViewExpandedView() { return StrCast(this.props.document.treeViewExpandedView, this.defaultExpandedView); } @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.document.maxEmbedHeight, 300); } @computed get dataDoc() { return this.resolvedDataDoc ? this.resolvedDataDoc : this.props.document; } @computed get fieldKey() { @@ -127,19 +127,19 @@ class TreeView extends React.Component<TreeViewProps> { onPointerDown = (e: React.PointerEvent) => e.stopPropagation(); onPointerEnter = (e: React.PointerEvent): void => { - this.props.active() && (this.props.document.libraryBrush = true); + this.props.active() && Doc.BrushDoc(this.dataDoc); if (e.buttons === 1 && SelectionManager.GetIsDragging()) { this._header!.current!.className = "treeViewItem-header"; document.addEventListener("pointermove", this.onDragMove, true); } } onPointerLeave = (e: React.PointerEvent): void => { - this.props.document.libraryBrush = false; + Doc.UnBrushDoc(this.dataDoc); this._header!.current!.className = "treeViewItem-header"; document.removeEventListener("pointermove", this.onDragMove, true); } onDragMove = (e: PointerEvent): void => { - this.props.document.libraryBrush = false; + Doc.UnBrushDoc(this.dataDoc); let x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); let rect = this._header!.current!.getBoundingClientRect(); let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2); @@ -341,10 +341,12 @@ class TreeView extends React.Component<TreeViewProps> { let headerElements = ( <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView} onPointerDown={action(() => { - this.props.document.treeViewExpandedView = this.treeViewExpandedView === this.fieldKey ? "fields" : - this.treeViewExpandedView === "fields" && this.props.document.layout ? "layout" : - this.treeViewExpandedView === "layout" && this.props.document.links ? "links" : - this.childDocs ? this.fieldKey : "fields"; + if (!this._collapsed) { + this.props.document.treeViewExpandedView = this.treeViewExpandedView === this.fieldKey ? "fields" : + this.treeViewExpandedView === "fields" && this.props.document.layout ? "layout" : + this.treeViewExpandedView === "layout" && this.props.document.links ? "links" : + this.childDocs ? this.fieldKey : "fields"; + } this._collapsed = false; })}> {this.treeViewExpandedView} @@ -357,7 +359,7 @@ class TreeView extends React.Component<TreeViewProps> { return <> <div className="docContainer" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown} style={{ - background: BoolCast(this.props.document.libraryBrush) ? "#06121212" : "0", + background: Doc.IsBrushed(this.props.document) ? "#06121212" : "0", outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined, pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? "all" : "none" }} > @@ -398,21 +400,56 @@ class TreeView extends React.Component<TreeViewProps> { panelWidth: () => number, renderDepth: number ) { - let docList = docs.filter(child => !child.excludeFromLibrary); + let viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField); + if (viewSpecScript) { + let script = viewSpecScript.script; + docs = docs.filter(d => { + let res = script.run({ doc: d }); + if (res.success) { + return res.result; + } + else { + console.log(res.error); + } + }); + } + + let descending = BoolCast(containingCollection.stackingHeadersSortDescending); + docs.sort(function (a, b): 1 | -1 { + let descA = descending ? b : a; + let descB = descending ? a : b; + let first = descA[String(containingCollection.sectionFilter)]; + let second = descB[String(containingCollection.sectionFilter)]; + // TODO find better way to sort how to sort.................. + if (typeof first === 'number' && typeof second === 'number') { + return (first - second) > 0 ? 1 : -1; + } + if (typeof first === 'string' && typeof second === 'string') { + return first > second ? 1 : -1; + } + if (typeof first === 'boolean' && typeof second === 'boolean') { + // if (first === second) { // bugfixing?: otherwise, the list "flickers" because the list is resorted during every load + // return Number(descA.x) > Number(descB.x) ? 1 : -1; + // } + return first > second ? 1 : -1; + } + return descending ? 1 : -1; + }); + let rowWidth = () => panelWidth() - 20; - return docList.map((child, i) => { + return docs.map((child, i) => { let pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, key, child); let indent = i === 0 ? undefined : () => { - if (StrCast(docList[i - 1].layout).indexOf("CollectionView") !== -1) { - let fieldKeysub = StrCast(docList[i - 1].layout).split("fieldKey")[1]; + if (StrCast(docs[i - 1].layout).indexOf("CollectionView") !== -1) { + let fieldKeysub = StrCast(docs[i - 1].layout).split("fieldKey")[1]; let fieldKey = fieldKeysub.split("\"")[1]; - Doc.AddDocToList(docList[i - 1], fieldKey, child); + Doc.AddDocToList(docs[i - 1], fieldKey, child); remove(child); } }; let addDocument = (doc: Doc, relativeTo?: Doc, before?: boolean) => { - return add(doc, relativeTo ? relativeTo : docList[i], before !== undefined ? before : false); + return add(doc, relativeTo ? relativeTo : docs[i], before !== undefined ? before : false); }; let rowHeight = () => { let aspect = NumCast(child.nativeWidth, 0) / NumCast(child.nativeHeight, 0); diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index f59fee985..7e1adaa19 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,6 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEye } from '@fortawesome/free-regular-svg-icons'; -import { faColumns, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons'; +import { faColumns, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree, faCopy } from '@fortawesome/free-solid-svg-icons'; import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from "mobx-react"; import * as React from 'react'; @@ -20,7 +20,7 @@ import { CollectionTreeView } from "./CollectionTreeView"; import { CollectionViewBaseChrome } from './CollectionViewChromes'; export const COLLECTION_BORDER_WIDTH = 2; -library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any); +library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy); @observer export class CollectionView extends React.Component<FieldViewProps> { @@ -30,6 +30,10 @@ export class CollectionView extends React.Component<FieldViewProps> { public static LayoutString(fieldStr: string = "data", fieldExt: string = "") { return FieldView.LayoutString(CollectionView, fieldStr, fieldExt); } + constructor(props: any) { + super(props); + } + componentDidMount = () => { this._reactionDisposer = reaction(() => StrCast(this.props.Document.chromeStatus), () => { @@ -65,7 +69,7 @@ export class CollectionView extends React.Component<FieldViewProps> { @action private collapse = (value: boolean) => { this._collapsed = value; - this.props.Document.chromeStatus = value ? "collapsed" : "visible"; + this.props.Document.chromeStatus = value ? "collapsed" : "enabled"; } private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => { @@ -86,7 +90,7 @@ export class CollectionView extends React.Component<FieldViewProps> { onContextMenu = (e: React.MouseEvent): void => { 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 subItems: ContextMenuProps[] = []; - subItems.push({ description: "Freeform", event: () => this.props.Document.viewType = CollectionViewType.Freeform, icon: "signature" }); + subItems.push({ description: "Freeform", event: () => { this.props.Document.viewType = CollectionViewType.Freeform; delete this.props.Document.usePivotLayout; }, icon: "signature" }); if (CollectionBaseView.InSafeMode()) { ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document.viewType = CollectionViewType.Invalid, icon: "project-diagram" }); } @@ -97,14 +101,16 @@ export class CollectionView extends React.Component<FieldViewProps> { 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 }); break; } } ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems, icon: "eye" }); - ContextMenu.Instance.addItem({ description: "Apply Template", event: () => this.props.addDocTab && this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" }); - ContextMenu.Instance.addItem({ - description: this.props.Document.chromeStatus !== "disabled" ? "Hide Chrome" : "Show Chrome", event: () => this.props.Document.chromeStatus = (this.props.Document.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" - }); + let existing = ContextMenu.Instance.findByDescription("Layout..."); + let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : []; + layoutItems.push({ description: "Create Layout Instance", event: () => this.props.addDocTab && this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" }); + layoutItems.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }); + !existing && ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "hand-point-right" }); } } diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss index 793cb7a8b..a13de6df5 100644 --- a/src/client/views/collections/CollectionViewChromes.scss +++ b/src/client/views/collections/CollectionViewChromes.scss @@ -42,6 +42,14 @@ transform-origin: top left; // margin-top: 10px; } + .collectionViewBaseChrome-template { + margin-left: 10px; + display: grid; + background: rgb(238, 238, 238); + color:grey; + margin-top:auto; + margin-bottom:auto; + } .collectionViewBaseChrome-viewSpecs { margin-left: 10px; @@ -106,17 +114,20 @@ } - .collectionStackingViewChrome-cont { + .collectionStackingViewChrome-cont, + .collectionTreeViewChrome-cont { display: flex; justify-content: space-between; } - .collectionStackingViewChrome-sort { + .collectionStackingViewChrome-sort, + .collectionTreeViewChrome-sort { display: flex; align-items: center; justify-content: space-between; - .collectionStackingViewChrome-sortIcon { + .collectionStackingViewChrome-sortIcon, + .collectionTreeViewChrome-sortIcon { transition: transform .5s; margin-left: 10px; } @@ -127,18 +138,21 @@ } - .collectionStackingViewChrome-sectionFilter-cont { + .collectionStackingViewChrome-sectionFilter-cont, + .collectionTreeViewChrome-sectionFilter-cont { justify-self: right; display: flex; font-size: 75%; letter-spacing: 2px; - .collectionStackingViewChrome-sectionFilter-label { + .collectionStackingViewChrome-sectionFilter-label, + .collectionTreeViewChrome-sectionFilter-label { vertical-align: center; padding: 10px; } - .collectionStackingViewChrome-sectionFilter { + .collectionStackingViewChrome-sectionFilter, + .collectionTreeViewChrome-sectionFilter { color: white; width: 100px; text-align: center; @@ -165,7 +179,8 @@ } } - .collectionStackingViewChrome-sectionFilter:hover { + .collectionStackingViewChrome-sectionFilter:hover, + .collectionTreeViewChrome-sectionFilter:hover { cursor: text; } } diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index 1b2561953..470085932 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -3,7 +3,7 @@ import { CollectionView } from "./CollectionView"; import "./CollectionViewChromes.scss"; import { CollectionViewType } from "./CollectionBaseView"; import { undoBatch } from "../../util/UndoManager"; -import { action, observable, runInAction, computed, IObservable, IObservableValue } from "mobx"; +import { action, observable, runInAction, computed, IObservable, IObservableValue, reaction, autorun } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast } from "../../../new_fields/Doc"; import { DocLike } from "../MetadataEntryMenu"; @@ -21,6 +21,7 @@ import { listSpec } from "../../../new_fields/Schema"; import { List } from "../../../new_fields/List"; import { Id } from "../../../new_fields/FieldSymbols"; import { threadId } from "worker_threads"; +import { DragManager } from "../../util/DragManager"; const datepicker = require('js-datepicker'); interface CollectionViewChromeProps { @@ -37,7 +38,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @observable private _dateWithinValue: string = ""; @observable private _dateValue: Date | string = ""; @observable private _keyRestrictions: [JSX.Element, string][] = []; - @observable private _collapsed: boolean = false; @computed private get filterValue() { return Cast(this.props.CollectionView.props.Document.viewSpecScript, ScriptField); } private _picker: any; @@ -61,7 +61,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro throw new Error("how did you get here, if chrome status is 'disabled' on a collection, a chrome shouldn't even be instantiated!"); } else if (chromeStatus === "collapsed") { - this._collapsed = true; if (this.props.collapse) { this.props.collapse(true); } @@ -163,9 +162,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @action toggleCollapse = () => { - this._collapsed = !this._collapsed; + this.props.CollectionView.props.Document.chromeStatus = this.props.CollectionView.props.Document.chromeStatus === "enabled" ? "collapsed" : "enabled"; if (this.props.collapse) { - this.props.collapse(this._collapsed); + this.props.collapse(this.props.CollectionView.props.Document.chromeStatus !== "enabled"); } } @@ -182,22 +181,80 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Tree: return ( + <CollectionTreeViewChrome + key="collchrome" + CollectionView={this.props.CollectionView} + type={this.props.type} + />); default: return null; } } + private get document() { + return this.props.CollectionView.props.Document; + } + + private get pivotKey() { + return StrCast(this.document.pivotField); + } + + private set pivotKey(value: string) { + this.document.pivotField = value; + } + + @observable private pivotKeyDisplay = this.pivotKey; + getPivotInput = () => { + if (!this.document.usePivotLayout) { + return (null); + } + return (<input className="collectionViewBaseChrome-viewSpecsInput" + placeholder="PIVOT ON..." + value={this.pivotKeyDisplay} + onChange={action((e: React.ChangeEvent<HTMLInputElement>) => this.pivotKeyDisplay = e.currentTarget.value)} + onKeyPress={action((e: React.KeyboardEvent<HTMLInputElement>) => { + let value = e.currentTarget.value; + if (e.which === 13) { + this.pivotKey = value; + this.pivotKeyDisplay = ""; + } + })} />); + } + + private dropDisposer?: DragManager.DragDropDisposer; + protected createDropTarget = (ele: HTMLDivElement) => { + this.dropDisposer && this.dropDisposer(); + if (ele) { + this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + } + } + + @undoBatch + @action + protected drop(e: Event, de: DragManager.DropEvent): boolean { + if (de.data instanceof DragManager.DocumentDragData) { + if (de.data.draggedDocuments.length) { + this.props.CollectionView.props.Document.childLayout = de.data.draggedDocuments[0]; + e.stopPropagation(); + return true; + } + } + return true; + } + render() { + let collapsed = this.props.CollectionView.props.Document.chromeStatus !== "enabled"; return ( - <div className="collectionViewChrome-cont" style={{ top: this._collapsed ? -70 : 0 }}> + <div className="collectionViewChrome-cont" style={{ top: collapsed ? -70 : 0 }}> <div className="collectionViewChrome"> <div className="collectionViewBaseChrome"> <button className="collectionViewBaseChrome-collapse" style={{ - top: this._collapsed ? 70 : 10, - transform: `rotate(${this._collapsed ? 180 : 0}deg) scale(${this._collapsed ? 0.5 : 1}) translate(${this._collapsed ? "-100%, -100%" : "0, 0"})`, - opacity: (this._collapsed && !this.props.CollectionView.props.isSelected()) ? 0 : 0.9, - left: (this._collapsed ? 0 : "unset"), + top: collapsed ? 70 : 10, + transform: `rotate(${collapsed ? 180 : 0}deg) scale(${collapsed ? 0.5 : 1}) translate(${collapsed ? "-100%, -100%" : "0, 0"})`, + opacity: (collapsed && !this.props.CollectionView.props.isSelected()) ? 0 : 0.9, + left: (collapsed ? 0 : "unset"), }} title="Collapse collection chrome" onClick={this.toggleCollapse}> <FontAwesomeIcon icon="caret-up" size="2x" /> @@ -213,12 +270,13 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking View</option> <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry View</option> </select> - <div className="collectionViewBaseChrome-viewSpecs" style={{ display: this._collapsed ? "none" : "grid" }}> + <div className="collectionViewBaseChrome-viewSpecs" style={{ display: collapsed ? "none" : "grid" }}> <input className="collectionViewBaseChrome-viewSpecsInput" placeholder="FILTER DOCUMENTS" value={this.filterValue ? this.filterValue.script.originalScript : ""} onChange={(e) => { }} onPointerDown={this.openViewSpecs} /> + {this.getPivotInput()} <div className="collectionViewBaseChrome-viewSpecsMenu" onPointerDown={this.openViewSpecs} style={{ @@ -260,6 +318,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro </div> </div> </div> + <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} style={{}}> + TEMPLATE + </div> </div> {this.subChrome()} </div> @@ -437,4 +498,109 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh </div > ); } -}
\ No newline at end of file +} + +@observer +export class CollectionTreeViewChrome extends React.Component<CollectionViewChromeProps> { + @observable private _currentKey: string = ""; + @observable private suggestions: string[] = []; + + @computed private get descending() { return BoolCast(this.props.CollectionView.props.Document.stackingHeadersSortDescending); } + @computed get sectionFilter() { return StrCast(this.props.CollectionView.props.Document.sectionFilter); } + + getKeySuggestions = async (value: string): Promise<string[]> => { + value = value.toLowerCase(); + let docs: Doc | Doc[] | Promise<Doc> | Promise<Doc[]> | (() => DocLike) + = () => DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldExt ? this.props.CollectionView.props.fieldExt : this.props.CollectionView.props.fieldKey]); + if (typeof docs === "function") { + docs = docs(); + } + docs = await docs; + if (docs instanceof Doc) { + return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value)); + } else { + const keys = new Set<string>(); + docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); + return Array.from(keys).filter(key => key.toLowerCase().startsWith(value)); + } + } + + @action + onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => { + this._currentKey = newValue; + } + + getSuggestionValue = (suggestion: string) => suggestion; + + renderSuggestion = (suggestion: string) => { + return <p>{suggestion}</p>; + } + + onSuggestionFetch = async ({ value }: { value: string }) => { + const sugg = await this.getKeySuggestions(value); + runInAction(() => { + this.suggestions = sugg; + }); + } + + @action + onSuggestionClear = () => { + this.suggestions = []; + } + + setValue = (value: string) => { + this.props.CollectionView.props.Document.sectionFilter = value; + return true; + } + + @action toggleSort = () => { this.props.CollectionView.props.Document.stackingHeadersSortDescending = !this.props.CollectionView.props.Document.stackingHeadersSortDescending; }; + @action resetValue = () => { this._currentKey = this.sectionFilter; }; + + render() { + return ( + <div className="collectionTreeViewChrome-cont"> + <button className="collectionTreeViewChrome-sort" onClick={this.toggleSort}> + <div className="collectionTreeViewChrome-sortLabel"> + Sort + </div> + <div className="collectionTreeViewChrome-sortIcon" style={{ transform: `rotate(${this.descending ? "180" : "0"}deg)` }}> + <FontAwesomeIcon icon="caret-up" size="2x" color="white" /> + </div> + </button> + <div className="collectionTreeViewChrome-sectionFilter-cont"> + <div className="collectionTreeViewChrome-sectionFilter-label"> + GROUP ITEMS BY: + </div> + <div className="collectionTreeViewChrome-sectionFilter"> + <EditableView + GetValue={() => this.sectionFilter} + autosuggestProps={ + { + resetValue: this.resetValue, + value: this._currentKey, + onChange: this.onKeyChange, + autosuggestProps: { + inputProps: + { + value: this._currentKey, + onChange: this.onKeyChange + }, + getSuggestionValue: this.getSuggestionValue, + suggestions: this.suggestions, + alwaysRenderSuggestions: true, + renderSuggestion: this.renderSuggestion, + onSuggestionsFetchRequested: this.onSuggestionFetch, + onSuggestionsClearRequested: this.onSuggestionClear + } + }} + oneLine + SetValue={this.setValue} + contents={this.sectionFilter ? this.sectionFilter : "N/A"} + /> + </div> + </div> + </div> + ); + } +} + diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index c3e55d825..17111af58 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -50,10 +50,10 @@ export class SelectorContextMenu extends React.Component<SelectorProps> { render() { return ( <> - <p>Contexts:</p> - {this._docs.map(doc => <p><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)} - {this._otherDocs.length ? <hr></hr> : null} - {this._otherDocs.map(doc => <p><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)} + <p key="contexts">Contexts:</p> + {this._docs.map(doc => <p key={doc.col[Id] + doc.target[Id]}><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)} + {this._otherDocs.length ? <hr key="hr" /> : null} + {this._otherDocs.map(doc => <p key="p"><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)} </> ); } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index a08a12426..ed7d9a02e 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,9 +1,9 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faEye } from "@fortawesome/free-regular-svg-icons"; import { faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faPaintBrush, faTable, faUpload, faChalkboard, faBraille } from "@fortawesome/free-solid-svg-icons"; -import { action, computed, observable } from "mobx"; +import { action, computed, observable, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCastAsync, HeightSym, WidthSym } from "../../../../new_fields/Doc"; +import { Doc, DocListCastAsync, HeightSym, WidthSym, DocListCast, FieldResult, Field, Opt } from "../../../../new_fields/Doc"; import { Id } from "../../../../new_fields/FieldSymbols"; import { InkField, StrokeData } from "../../../../new_fields/InkField"; import { createSchema, makeInterface } from "../../../../new_fields/Schema"; @@ -29,8 +29,8 @@ import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView"; import { pageSchema } from "../../nodes/ImageBox"; import { OverlayElementOptions, OverlayView } from "../../OverlayView"; import PDFMenu from "../../pdf/PDFMenu"; -import { ScriptBox } from "../../ScriptBox"; import { CollectionSubView } from "../CollectionSubView"; +import { ScriptBox } from "../../ScriptBox"; import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView"; import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; import "./CollectionFreeFormView.scss"; @@ -48,6 +48,139 @@ export const panZoomSchema = createSchema({ arrangeInit: ScriptField, }); +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 curPage = FieldValue(target.Document.curPage, -1); + + let docViews = target.childDocs.filter(doc => doc instanceof Doc).reduce((prev, doc) => { + var page = NumCast(doc.page, -1); + if ((Math.abs(Math.round(page) - Math.round(curPage)) < 3) || page === -1) { + 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} + {...target.getChildDocumentViewProps(doc)} + />), + bounds: { + x: pos.x, + y: pos.y, + z: pos.z, + width: NumCast(pos.width), + height: NumCast(pos.height) + } + }); + } + } + return prev; + }, elements); + + target.resetSelectOnLoaded(); + + return docViews; + }; + +} + type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof positionSchema, typeof pageSchema]>; const PanZoomDocument = makeInterface(panZoomSchema, positionSchema, pageSchema); @@ -59,6 +192,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private get _pwidth() { return this.props.PanelWidth(); } private get _pheight() { return this.props.PanelHeight(); } private inkKey = "ink"; + private _childLayoutDisposer?: IReactionDisposer; + + componentDidMount() { + this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)], + async (args) => args[1] instanceof Doc && + this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc), undefined))); + } + componentWillUnmount() { + this._childLayoutDisposer && this._childLayoutDisposer(); + } get parentScaling() { return (this.props as any).ContentScaling && this.fitToBox && !this.isAnnotationOverlay ? (this.props as any).ContentScaling() : 1; @@ -352,12 +495,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action onPointerWheel = (e: React.WheelEvent): void => { if (BoolCast(this.props.Document.lockedPosition)) return; - // if (!this.props.active()) { - // return; - // } - if (this.props.Document.type === "pdf") { + if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming + e.stopPropagation(); return; } + let childSelected = this.childDocs.some(doc => { var dv = DocumentManager.Instance.getDocumentView(doc); return dv && SelectionManager.IsSelected(dv) ? true : false; @@ -366,21 +508,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return; } e.stopPropagation(); - const coefficient = 1000; - - if (e.ctrlKey) { - let deltaScale = (1 - (e.deltaY / coefficient)); - let nw = this.nativeWidth * deltaScale; - let nh = this.nativeHeight * deltaScale; - if (nw && nh) { - this.props.Document.nativeWidth = nw; - this.props.Document.nativeHeight = nh; - } - e.stopPropagation(); - e.preventDefault(); - } else { - // if (modes[e.deltaMode] === 'pixels') coefficient = 50; - // else if (modes[e.deltaMode] === 'lines') coefficient = 1000; // This should correspond to line-height?? + + // bcz: this changes the nativewidth/height, but ImageBox will just revert it back to its defaults. need more logic to fix. + // if (e.ctrlKey && this.props.Document.scrollHeight === undefined) { + // let deltaScale = (1 - (e.deltaY / coefficient)); + // let nw = this.nativeWidth * deltaScale; + // let nh = this.nativeHeight * deltaScale; + // if (nw && nh) { + // this.props.Document.nativeWidth = nw; + // this.props.Document.nativeHeight = nh; + // } + // e.preventDefault(); + // } + // else + { let deltaScale = e.deltaY > 0 ? (1 / 1.1) : 1.1; if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) { deltaScale = 1 / this.zoomScaling(); @@ -392,21 +533,19 @@ 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.stopPropagation(); + e.preventDefault(); } } @action setPan(panX: number, panY: number) { - if (BoolCast(this.props.Document.lockedPosition)) return; - this.props.Document.panTransformType = "None"; - var scale = this.getLocalTransform().inverse().Scale; - const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); - const newPanY = Math.min((1 - 1 / scale) * this.nativeHeight, Math.max(0, panY)); - this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX; - this.props.Document.panY = this.isAnnotationOverlay && StrCast(this.props.Document.backgroundLayout).indexOf("PDFBox") === -1 ? newPanY : panY; - if (this.props.Document.scrollY) { - this.props.Document.scrollY = panY - scale * this.props.Document[HeightSym](); + if (!BoolCast(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)); + const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.props.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY)); + this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX; + this.props.Document.panY = this.isAnnotationOverlay ? newPanY : panY; } } @@ -490,16 +629,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { this.Document.scale = scale; } - getScale = () => { - if (this.Document.scale) { - return this.Document.scale; - } - return 1; - } - + getScale = () => this.Document.scale ? this.Document.scale : 1; getChildDocumentViewProps(childDocLayout: Doc): DocumentViewProps { - let self = this; let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, childDocLayout); return { DataDoc: pair.data, @@ -507,6 +639,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { addDocument: this.props.addDocument, removeDocument: this.props.removeDocument, moveDocument: this.props.moveDocument, + onClick: this.props.onClick, ScreenToLocalTransform: pair.layout.z ? this.getTransformOverlay : this.getTransform, renderDepth: this.props.renderDepth + 1, selectOnLoad: pair.layout[Id] === this._selectOnLoaded, @@ -531,6 +664,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { addDocument: this.props.addDocument, removeDocument: this.props.removeDocument, moveDocument: this.props.moveDocument, + onClick: this.props.onClick, ScreenToLocalTransform: this.getTransform, renderDepth: this.props.renderDepth, selectOnLoad: layoutDoc[Id] === this._selectOnLoaded, @@ -558,7 +692,19 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return result.result === undefined ? { x: Cast(doc.x, "number"), y: Cast(doc.y, "number"), z: Cast(doc.z, "number"), width: Cast(doc.width, "number"), height: Cast(doc.height, "number") } : result.result; } - private viewDefToJSX(viewDef: any): { ele: JSX.Element, bounds?: { x: number, y: number, z?: number, width: number, height: number } } | undefined { + 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; + } + + private viewDefToJSX(viewDef: any): Opt<ViewDefResult> { if (viewDef.type === "text") { const text = Cast(viewDef.text, "string"); const x = Cast(viewDef.x, "number"); @@ -587,20 +733,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const script = this.Document.arrangeScript; let state: any = undefined; const docs = this.childDocs; - let elements: { ele: JSX.Element, bounds?: { x: number, y: number, z?: number, width: number, height: number } }[] = []; + let elements: ViewDefResult[] = []; if (initScript) { const initResult = initScript.script.run({ docs, collection: this.Document }); if (initResult.success) { const result = initResult.result; const { state: scriptState, views } = result; state = scriptState; - if (Array.isArray(views)) { - elements = views.reduce<typeof elements>((prev, ele) => { - const jsx = this.viewDefToJSX(ele); - jsx && prev.push(jsx); - return prev; - }, elements); - } + elements = this.viewDefsToJSX(views); } } let docviews = docs.filter(doc => doc instanceof Doc).reduce((prev, doc) => { @@ -622,14 +762,17 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return prev; }, elements); - setTimeout(() => this._selectOnLoaded = "", 600);// bcz: surely there must be a better way .... + this.resetSelectOnLoaded(); return docviews; } + resetSelectOnLoaded = () => setTimeout(() => this._selectOnLoaded = "", 600);// bcz: surely there must be a better way .... + @computed.struct get views() { - return this.elements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); + let source = this.Document.usePivotLayout === true ? PivotView.elements(this) : this.elements; + return source.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); } @computed.struct get overlayViews() { @@ -642,19 +785,45 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); } + fitToContainer = async () => this.props.Document.fitToBox = !this.fitToBox; + + arrangeContents = async () => { + const docs = await DocListCastAsync(this.Document[this.props.fieldKey]); + 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; + } + } + } + }, "arrange contents"); + } + + analyzeStrokes = async () => { + let data = Cast(this.fieldExtensionDoc[this.inkKey], InkField); + if (!data) { + return; + } + let relevantKeys = ["inkAnalysis", "handwriting"]; + CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.fieldExtensionDoc, relevantKeys, data.inkData); + } + onContextMenu = (e: React.MouseEvent) => { let layoutItems: ContextMenuProps[] = []; - layoutItems.push({ - description: `${this.fitToBox ? "Unset" : "Set"} Fit To Container`, - event: async () => this.props.Document.fitToBox = !this.fitToBox, - icon: !this.fitToBox ? "expand-arrows-alt" : "compress-arrows-alt" - }); - 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: this.fitToContainer, icon: !this.fitToBox ? "expand-arrows-alt" : "compress-arrows-alt" }); + 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.props.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: async () => { @@ -669,73 +838,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { event: async () => this.props.Document.clusterOverridesDefaultBackground = !this.props.Document.clusterOverridesDefaultBackground, icon: !this.props.Document.useClusters ? "chalkboard" : "chalkboard" }); - layoutItems.push({ - description: "Arrange contents in grid", - icon: "table", - event: async () => { - const docs = await DocListCastAsync(this.Document[this.props.fieldKey]); - 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; - } - } - } - }, "arrange contents"); - } - }); + layoutItems.push({ description: "Arrange contents in grid", event: this.arrangeContents, icon: "table" }); ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" }); - ContextMenu.Instance.addItem({ - description: "Analyze Strokes", event: async () => { - let data = Cast(this.fieldExtensionDoc[this.inkKey], InkField); - if (!data) { - return; - } - let relevantKeys = ["inkAnalysis", "handwriting"]; - CognitiveServices.Inking.Manager.analyzer(this.fieldExtensionDoc, relevantKeys, data.inkData); - }, icon: "paint-brush" - }); - ContextMenu.Instance.addItem({ - description: "Import document", icon: "upload", event: () => { - const input = document.createElement("input"); - input.type = "file"; - input.accept = ".zip"; - input.onchange = async _e => { - const files = input.files; - if (!files) return; - const file = files[0]; - let formData = new FormData(); - formData.append('file', file); - formData.append('remap', "true"); - const upload = Utils.prepend("/uploadDoc"); - const response = await fetch(upload, { method: "POST", body: formData }); - const json = await response.json(); - if (json === "error") { - return; - } - const doc = await DocServer.GetRefField(json); - if (!doc || !(doc instanceof Doc)) { - return; - } - const [x, y] = this.props.ScreenToLocalTransform().transformPoint(e.pageX, e.pageY); - doc.x = x, doc.y = y; - this.addDocument(doc, false); - }; - input.click(); - } - }); + + let existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers..."); + let analyzers: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : []; + analyzers.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" }); + !existingAnalyze && ContextMenu.Instance.addItem({ description: "Analyzers...", subitems: analyzers, icon: "hand-point-right" }); } |
