diff options
| author | Stanley Yip <stanley_yip@brown.edu> | 2019-10-29 19:13:25 -0400 |
|---|---|---|
| committer | Stanley Yip <stanley_yip@brown.edu> | 2019-10-29 19:13:25 -0400 |
| commit | 525e18727edbdaba578e1ace748ddfd9573a65a4 (patch) | |
| tree | ff109c875a4d89708d8a9eaabff97971e08842e3 /src/client/views/collections | |
| parent | b7353705ee06292e570c9847d72287190f3f42ed (diff) | |
| parent | 24031b32402f19932d293bdc3eb483235cff820a (diff) | |
Merge branch 'interaction_stanley' of https://github.com/browngraphicslab/Dash-Web into interaction_stanley
Diffstat (limited to 'src/client/views/collections')
24 files changed, 596 insertions, 803 deletions
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx deleted file mode 100644 index 46d2582bd..000000000 --- a/src/client/views/collections/CollectionBaseView.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import { action, computed, observable } from 'mobx'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { Doc, DocListCast } from '../../../new_fields/Doc'; -import { Id } from '../../../new_fields/FieldSymbols'; -import { List } from '../../../new_fields/List'; -import { listSpec } from '../../../new_fields/Schema'; -import { BoolCast, Cast, NumCast, PromiseValue, StrCast, FieldValue } from '../../../new_fields/Types'; -import { DocumentManager } from '../../util/DocumentManager'; -import { SelectionManager } from '../../util/SelectionManager'; -import { ContextMenu } from '../ContextMenu'; -import { FieldViewProps } from '../nodes/FieldView'; -import './CollectionBaseView.scss'; -import { DateField } from '../../../new_fields/DateField'; -import { ImageField } from '../../../new_fields/URLField'; -import { Touchable } from '../Touchable'; - -export enum CollectionViewType { - Invalid, - Freeform, - Schema, - Docking, - Tree, - Stacking, - Masonry, - Pivot, - Linear, - Staff -} - -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], - ["pivot", CollectionViewType.Pivot], - ["linear", CollectionViewType.Linear] - ]); - - export const valueOf = (value: string) => { - return stringMapping.get(value.toLowerCase()); - }; - -} - -export interface CollectionRenderProps { - addDocument: (document: Doc) => boolean; - removeDocument: (document: Doc) => boolean; - moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; - active: () => boolean; - whenActiveChanged: (isActive: boolean) => void; -} - -export interface CollectionViewProps extends FieldViewProps { - onContextMenu?: (e: React.MouseEvent) => void; - children: (type: CollectionViewType, props: CollectionRenderProps) => JSX.Element | JSX.Element[] | null | (JSX.Element | null)[]; - className?: string; - contentRef?: React.Ref<HTMLDivElement>; -} - -@observer -export class CollectionBaseView extends Touchable<CollectionViewProps> { - @observable private static _safeMode = false; - static InSafeMode() { return this._safeMode; } - static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; } - get collectionViewType(): CollectionViewType | undefined { - let Document = this.props.Document; - let viewField = Cast(Document.viewType, "number"); - if (CollectionBaseView._safeMode) { - if (viewField === CollectionViewType.Freeform) { - return CollectionViewType.Tree; - } - if (viewField === CollectionViewType.Invalid) { - return CollectionViewType.Freeform; - } - } - if (viewField !== undefined) { - return viewField; - } else { - return CollectionViewType.Invalid; - } - } - - @computed get dataDoc() { return Doc.fieldExtensionDoc(this.props.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); } - @computed get dataField() { return this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; } - - active = (): boolean => { - var isSelected = this.props.isSelected(); - return isSelected || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0; - } - - //TODO should this be observable? - private _isChildActive = false; - whenActiveChanged = (isActive: boolean) => { - this._isChildActive = isActive; - this.props.whenActiveChanged(isActive); - } - - @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); } - - @action.bound - addDocument(doc: Doc): boolean { - if (this.props.fieldExt) { // bcz: fieldExt !== undefined means this is an overlay layer - Doc.GetProto(doc).annotationOn = this.props.Document; - } - let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document; - let targetField = (this.props.fieldExt || this.props.Document.isTemplate) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; - Doc.AddDocToList(targetDataDoc, targetField, doc); - Doc.GetProto(doc).lastOpened = new DateField; - return true; - } - - @action.bound - removeDocument(doc: Doc): boolean { - let docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView); - docView && SelectionManager.DeselectDoc(docView); - //TODO This won't create the field if it doesn't already exist - let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document; - let targetField = (this.props.fieldExt || this.props.Document.isTemplate) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; - let value = Cast(targetDataDoc[targetField], listSpec(Doc), []); - let index = value.reduce((p, v, i) => (v instanceof Doc && v === doc) ? i : p, -1); - index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1); - PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn => { - if (Doc.AreProtosEqual(annotationOn, FieldValue(Cast(this.dataDoc.extendsDoc, Doc)))) { - Doc.GetProto(doc).annotationOn = undefined; - } - }); - - if (index !== -1) { - value.splice(index, 1); - - // SelectionManager.DeselectAll() - ContextMenu.Instance.clearItems(); - return true; - } - return false; - } - - // this is called with the document that was dragged and the collection to move it into. - // if the target collection is the same as this collection, then the move will be allowed. - // otherwise, the document being moved must be able to be removed from its container before - // moving it into the target. - @action.bound - moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean { - if (Doc.AreProtosEqual(this.props.Document, targetCollection)) { - return true; - } - return this.removeDocument(doc) ? addDocument(doc) : false; - } - - showIsTagged = () => { - const children = DocListCast(this.props.Document.data); - const imageProtos = children.filter(doc => Cast(doc.data, ImageField)).map(Doc.GetProto); - const allTagged = imageProtos.length > 0 && imageProtos.every(image => image.googlePhotosTags); - if (allTagged) { - return ( - <img - id={"google-tags"} - src={"/assets/google_tags.png"} - /> - ); - } - return (null); - } - - render() { - const props: CollectionRenderProps = { - addDocument: this.addDocument, - removeDocument: this.removeDocument, - moveDocument: this.moveDocument, - active: this.active, - whenActiveChanged: this.whenActiveChanged, - }; - const viewtype = this.collectionViewType; - return ( - <div id="collectionBaseView" - style={{ - pointerEvents: this.props.Document.isBackground ? "none" : "all", - boxShadow: this.props.Document.isBackground || viewtype === CollectionViewType.Linear ? undefined : `#9c9396 ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}` - }} - className={this.props.className || "collectionView-cont"} - onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}> - {this.showIsTagged()} - {viewtype !== undefined ? this.props.children(viewtype, props) : (null)} - </div> - ); - } - -} diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index b4ab8d550..bcdc9c97e 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -31,6 +31,10 @@ touch-action: none; } + .lm_content { + background: white; + } + .lm_controls>li { opacity: 0.6; transform: scale(1.2); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 5e00fda36..937a4949e 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -617,19 +617,20 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } } - panelWidth = () => this._document && this._document.maxWidth ? Math.min(Math.max(NumCast(this._document.width), NumCast(this._document.nativeWidth)), this._panelWidth) : this._panelWidth; + get layoutDoc() { return this._document && Doc.Layout(this._document);} + panelWidth = () => this.layoutDoc && this.layoutDoc.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc.width), NumCast(this.layoutDoc.nativeWidth)), this._panelWidth) : this._panelWidth; panelHeight = () => this._panelHeight; - 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; + nativeWidth = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!.fitWidth ? NumCast(this.layoutDoc!.nativeWidth) || this._panelWidth : 0; + nativeHeight = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!.fitWidth ? NumCast(this.layoutDoc!.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); + if (this.layoutDoc!.type === DocumentType.PDF) { + if ((this.layoutDoc && this.layoutDoc.fitWidth) || + this._panelHeight / NumCast(this.layoutDoc!.nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!.nativeWidth)) { + return this._panelWidth / NumCast(this.layoutDoc!.nativeWidth); } else { - return this._panelHeight / NumCast(this._document!.nativeHeight); + return this._panelHeight / NumCast(this.layoutDoc!.nativeHeight); } } const nativeH = this.nativeHeight(); @@ -647,7 +648,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } return Transform.Identity(); } - get previewPanelCenteringOffset() { return this.nativeWidth() && !this._document!.ignoreAspect ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; } + get previewPanelCenteringOffset() { return this.nativeWidth() && !this.layoutDoc!.ignoreAspect ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; } addDocTab = (doc: Doc, dataDoc: Opt<Doc>, location: string) => { SelectionManager.DeselectAll(); @@ -692,11 +693,11 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } render() { - return (!this._isActive || !this._document) ? (null) : + return (!this._isActive || !this.layoutDoc) ? (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%" + height: this.layoutDoc && this.layoutDoc.fitWidth ? undefined : "100%" }}> {this.docView} </div >); diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index 1709b9c99..52ebfafd3 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -4,12 +4,12 @@ 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 { Doc, WidthSym } from "../../../new_fields/Doc"; -import { Id } from "../../../new_fields/FieldSymbols"; +import Measure from "react-measure"; +import { Doc } from "../../../new_fields/Doc"; import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { ScriptField } from "../../../new_fields/ScriptField"; -import { NumCast, StrCast } from "../../../new_fields/Types"; -import { Utils, numberRange } from "../../../Utils"; +import { StrCast } from "../../../new_fields/Types"; +import { numberRange } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; import { CompileScript } from "../../util/Scripting"; @@ -20,7 +20,7 @@ import { anchorPoints, Flyout } from "../DocumentDecorations"; import { EditableView } from "../EditableView"; import { CollectionStackingView } from "./CollectionStackingView"; import "./CollectionStackingView.scss"; -import Measure from "react-measure"; +import { undo } from "prosemirror-history"; library.add(faPalette); @@ -41,27 +41,28 @@ interface CMVFieldRowProps { export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowProps> { @observable private _background = "inherit"; @observable private _createAliasSelected: boolean = false; + @observable private _collapsed: boolean = false; + @observable private _headingsHack: number = 1; + @observable private _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading; + @observable private _color = this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; - private _dropRef: HTMLDivElement | null = null; - private dropDisposer?: DragManager.DragDropDisposer; + private _dropDisposer?: DragManager.DragDropDisposer; private _headerRef: React.RefObject<HTMLDivElement> = React.createRef(); private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 }; private _contRef: React.RefObject<HTMLDivElement> = React.createRef(); private _sensitivity: number = 16; + private _counter: number = 0; - @observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading; - @observable _color = this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; createRowDropRef = (ele: HTMLDivElement | null) => { - this._dropRef = ele; - this.dropDisposer && this.dropDisposer(); + this._dropDisposer && this._dropDisposer(); if (ele) { - this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.rowDrop.bind(this) } }); + this._dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.rowDrop.bind(this) } }); } } getTrueHeight = () => { - if (this.collapsed) { + if (this._collapsed) { this.props.setDocHeight(this._heading, 20); } else { let rawHeight = this._contRef.current!.getBoundingClientRect().height + 15; //+ 15 accounts for the group header @@ -75,14 +76,11 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr rowDrop = action((e: Event, de: DragManager.DropEvent) => { this._createAliasSelected = false; if (de.data instanceof DragManager.DocumentDragData) { + (this.props.parent.Document.dropConverter instanceof ScriptField) && + this.props.parent.Document.dropConverter.script.run({ dragData: de.data }); let key = StrCast(this.props.parent.props.Document.sectionFilter); let castedValue = this.getValue(this._heading); - if (castedValue) { - de.data.droppedDocuments.forEach(d => d[key] = castedValue); - } - else { - de.data.droppedDocuments.forEach(d => d[key] = undefined); - } + de.data.droppedDocuments.forEach(d => d[key] = castedValue); this.props.parent.drop(e, de); e.stopPropagation(); } @@ -90,15 +88,9 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr getValue = (value: string): any => { let parsed = parseInt(value); - if (!isNaN(parsed)) { - return parsed; - } - if (value.toLowerCase().indexOf("true") > -1) { - return true; - } - if (value.toLowerCase().indexOf("false") > -1) { - return false; - } + if (!isNaN(parsed)) return parsed; + if (value.toLowerCase().indexOf("true") > -1) return true; + if (value.toLowerCase().indexOf("false") > -1) return false; return value; } @@ -132,12 +124,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr } } - @action - pointerEnteredRow = () => { - if (SelectionManager.GetIsDragging()) { - this._background = "#b4b4b4"; - } - } + pointerEnteredRow = action(() => SelectionManager.GetIsDragging() && (this._background = "#b4b4b4")); @action pointerLeaveRow = () => { @@ -155,8 +142,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr return this.props.parent.props.addDocument(newDoc); } - @action - deleteRow = () => { + deleteRow = undoBatch(action(() => { this._createAliasSelected = false; let key = StrCast(this.props.parent.props.Document.sectionFilter); this.props.docList.forEach(d => d[key] = undefined); @@ -164,7 +150,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr let index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject); this.props.parent.sectionHeaders.splice(index, 1); } - } + })); @action collapseSection = () => { @@ -206,6 +192,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr document.removeEventListener("pointerup", this.pointerUp); } + @action headerDown = (e: React.PointerEvent<HTMLDivElement>) => { e.stopPropagation(); e.preventDefault(); @@ -252,48 +239,31 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr ); } - @action - toggleAlias = () => { - this._createAliasSelected = true; - } + toggleAlias = action(() => this._createAliasSelected = true); + toggleVisibility = action(() => this._collapsed = !this._collapsed); renderMenu = () => { let selected = this._createAliasSelected; - return ( - <div className="collectionStackingView-optionPicker"> - <div className="optionOptions"> - <div className={"optionPicker" + (selected === true ? " active" : "")} onClick={this.toggleAlias}>Create Alias</div> - </div> + return (<div className="collectionStackingView-optionPicker"> + <div className="optionOptions"> + <div className={"optionPicker" + (selected === true ? " active" : "")} onClick={this.toggleAlias}>Create Alias</div> + <div className={"optionPicker" + (selected === true ? " active" : "")} onClick={this.deleteRow}>Delete</div> </div> - ); + </div>); } - @observable private collapsed: boolean = false; - - private toggleVisibility = action(() => { - this.collapsed = !this.collapsed; - }); - - @observable _headingsHack: number = 1; - handleResize = (size: any) => { - this.counter += 1; - if (this.counter !== 1) { + if (++this._counter !== 1) { this.getTrueHeight(); } } - private counter: number = 0; render() { - let cols = this.props.rows(); let rows = Math.max(1, Math.min(this.props.docList.length, Math.floor((this.props.parent.props.PanelWidth() - 2 * this.props.parent.xMargin) / (this.props.parent.columnWidth + this.props.parent.gridGap)))); let key = StrCast(this.props.parent.props.Document.sectionFilter); - let templatecols = ""; - let headings = this.props.headings(); let heading = this._heading; let style = this.props.parent; - 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 = { GetValue: () => evContents, @@ -314,45 +284,46 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr toggle: this.toggleVisibility, color: this._color }; - let headingView = this.props.headingObject ? - <div className="collectionStackingView-sectionHeader" ref={this._headerRef} > - <div className={"collectionStackingView-collapseBar" + (this.props.headingObject.collapsed === true ? " active" : "")} onClick={this.collapseSection}></div> - <div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown} - title={evContents === `NO ${key.toUpperCase()} VALUE` ? - `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""} - style={{ - width: "100%", - background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "lightgrey", - color: "grey" - }}> - {<EditableView {...headerEditableViewProps} />} - {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : - <div className="collectionStackingView-sectionColor"> - <Flyout anchorPoint={anchorPoints.CENTER_RIGHT} content={this.renderColorPicker()}> - <button className="collectionStackingView-sectionColorButton"> - <FontAwesomeIcon icon="palette" size="lg" /> - </button> - </ Flyout > - </div> - } - {evContents === `NO ${key.toUpperCase()} VALUE` ? - (null) : - <button className="collectionStackingView-sectionDelete" onClick={this.deleteRow}> - <FontAwesomeIcon icon="trash" size="lg" /> - </button>} - {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : - <div className="collectionStackingView-sectionOptions"> - <Flyout anchorPoint={anchorPoints.TOP_RIGHT} content={this.renderMenu()}> - <button className="collectionStackingView-sectionOptionButton"> - <FontAwesomeIcon icon="ellipsis-v" size="lg"></FontAwesomeIcon> - </button> - </Flyout> - </div> - } - </div> - </div > : (null); + let headingView = this.props.parent.props.Document.miniHeaders ? + <div className="collectionStackingView-miniHeader" style={{ width: "100%" }}> + {<EditableView {...headerEditableViewProps} />} + </div> : + this.props.headingObject ? + <div className="collectionStackingView-sectionHeader" ref={this._headerRef} > + <div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown} + title={evContents === `NO ${key.toUpperCase()} VALUE` ? + `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""} + style={{ + width: "100%", + background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "lightgrey", + color: "grey" + }}> + {<EditableView {...headerEditableViewProps} />} + {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : + <div className="collectionStackingView-sectionColor"> + <Flyout anchorPoint={anchorPoints.CENTER_RIGHT} content={this.renderColorPicker()}> + <button className="collectionStackingView-sectionColorButton"> + <FontAwesomeIcon icon="palette" size="lg" /> + </button> + </ Flyout > + </div> + } + <button className="collectionStackingView-sectionDelete" onClick={this.collapseSection}> + <FontAwesomeIcon icon={this._collapsed ? "chevron-down" : "chevron-up"} size="lg" /> + </button> + {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : + <div className="collectionStackingView-sectionOptions"> + <Flyout anchorPoint={anchorPoints.TOP_RIGHT} content={this.renderMenu()}> + <button className="collectionStackingView-sectionOptionButton"> + <FontAwesomeIcon icon="ellipsis-v" size="lg" /> + </button> + </Flyout> + </div> + } + </div> + </div > : (null); const background = this._background; //to account for observables in Measure - const collapsed = this.collapsed; + const collapsed = this._collapsed; let chromeStatus = this.props.parent.props.Document.chromeStatus; return ( <Measure offset onResize={this.handleResize}> @@ -367,7 +338,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr > {headingView} {collapsed ? (null) : - < div > + < div style={{ position: "relative" }}> <div key={`${heading}-stack`} className={`collectionStackingView-masonryGrid`} ref={this._contRef} style={{ diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 79c032723..54a36f691 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -144,7 +144,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> { Document: this.props.rowProps.original, DataDoc: this.props.rowProps.original, fieldKey: this.props.rowProps.column.id as string, - fieldExt: "", ruleProvider: undefined, ContainingCollectionView: this.props.CollectionView, ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index e0cedc210..6a9392253 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -40,7 +40,6 @@ .collectionSchemaView-previewRegion { position: relative; - background: $light-color; height: auto !important; .collectionSchemaView-previewDoc { diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 3218f630a..34f642f80 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -158,6 +158,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { <CollectionSchemaPreview Document={layoutDoc} DataDocument={this.previewDocument !== this.props.DataDoc ? this.props.DataDoc : undefined} + fieldKey={this.props.fieldKey} childDocs={this.childDocs} renderDepth={this.props.renderDepth} ruleProvider={this.props.Document.isRuleProvider && layoutDoc && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider} @@ -226,7 +227,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } render() { - Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey); return ( <div className="collectionSchemaView-container" style={{ height: "100%", marginTop: "0", }}> <div className="collectionSchemaView-tableContainer" onPointerDown={this.onPointerDown} onWheel={this.onWheel} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createTarget}> @@ -898,6 +898,7 @@ interface CollectionSchemaPreviewProps { childDocs?: Doc[]; renderDepth: number; fitToBox?: boolean; + fieldKey: string; PanelWidth: () => number; PanelHeight: () => number; ruleProvider: Doc | undefined; @@ -922,8 +923,9 @@ interface CollectionSchemaPreviewProps { export class CollectionSchemaPreview extends React.Component<CollectionSchemaPreviewProps>{ private dropDisposer?: DragManager.DragDropDisposer; _mainCont?: HTMLDivElement; - private get nativeWidth() { return NumCast(this.props.Document!.nativeWidth, this.props.PanelWidth()); } - private get nativeHeight() { return NumCast(this.props.Document!.nativeHeight, this.props.PanelHeight()); } + private get layoutDoc() { return this.props.Document && Doc.Layout(this.props.Document); } + private get nativeWidth() { return NumCast(this.layoutDoc!.nativeWidth, this.props.PanelWidth()); } + private get nativeHeight() { return NumCast(this.layoutDoc!.nativeHeight, this.props.PanelHeight()); } private contentScaling = () => { let wscale = this.props.PanelWidth() / (this.nativeWidth ? this.nativeWidth : this.props.PanelWidth()); if (wscale * this.nativeHeight > this.props.PanelHeight()) { @@ -947,10 +949,8 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre if (de.data instanceof DragManager.DocumentDragData) { this.props.childDocs && this.props.childDocs.map(otherdoc => { let target = Doc.GetProto(otherdoc); - let layoutNative = Doc.MakeTitled("layoutNative"); - layoutNative.layout = ComputedField.MakeFunction("this.image_data[0]"); - target.layoutNative = layoutNative; - target.layoutCUstom = target.layout = Doc.MakeDelegate(de.data.draggedDocuments[0]); + target.layout = ComputedField.MakeFunction("this.image_data[0]"); + target.layoutCustom = Doc.MakeDelegate(de.data.draggedDocuments[0]); }); e.stopPropagation(); } @@ -968,7 +968,7 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre let br = StrCast(this.props.Document!.borderRounding); if (br.endsWith("%")) { let percent = Number(br.substr(0, br.length - 1)) / 100; - let nativeDim = Math.min(NumCast(this.props.Document!.nativeWidth), NumCast(this.props.Document!.nativeHeight)); + let nativeDim = Math.min(NumCast(this.layoutDoc!.nativeWidth), NumCast(this.layoutDoc!.nativeHeight)); let minDim = percent * (nativeDim ? nativeDim : Math.min(this.PanelWidth(), this.PanelHeight())); return minDim; } diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index b31f0b8e3..29178b909 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -21,6 +21,10 @@ overflow-y: auto; flex-wrap: wrap; transition: top .5s; + >div { + position: relative; + display: block; + } .collectionSchemaView-previewDoc { height: 100%; @@ -100,6 +104,7 @@ grid-column-end: span 1; height: 100%; margin: auto; + display: inline-grid; } .collectionStackingView-masonrySection { @@ -119,7 +124,38 @@ background: red; } } - + .collectionStackingView-miniHeader { + width: 100%; + .editableView-container-editing-oneLine { + min-height: 20px; + display: flex; + align-items: center; + flex-direction: row; + } + span::before , span::after{ + content: ""; + width: 50%; + border-top: dashed gray 1px; + position: relative; + display: inline-block; + } + span::before { + margin-right: 10px; + } + span::after{ + margin-left: 10px; + } + span { + position: relative; + text-align: center; + white-space: nowrap; + overflow: visible; + width: 100%; + display: flex; + color:gray; + align-items: center; + } + } .collectionStackingView-sectionHeader { text-align: center; margin-left: 2px; @@ -221,7 +257,6 @@ } .collectionStackingView-optionPicker { - width: 78px; .optionOptions { display: inline; @@ -229,10 +264,10 @@ .optionPicker { cursor: pointer; - width: 20px; height: 20px; border-radius: 10px; margin: 3px; + width:max-content; &.active { color: red; diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 90fc9c3e0..7f0165ad4 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -44,7 +44,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.ContainingCollectionDoc && (this.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.ContainingCollectionDoc.chromeStatus !== 'disabled')); } + @computed get showAddAGroup() { return (this.sectionFilter && (this.props.Document.chromeStatus !== 'view-mode' && this.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)); @@ -57,15 +57,16 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { this._docXfs.length = 0; return docs.map((d, i) => { let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d); - let width = () => Math.min(d.nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns); - let height = () => this.getDocHeight(pair.layout); + let layoutDoc = pair.layout ? Doc.Layout(pair.layout) : d; + let width = () => Math.min(layoutDoc.nativeWidth && !layoutDoc.ignoreAspect && !this.props.Document.fillColumn ? layoutDoc[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns); + let height = () => this.getDocHeight(layoutDoc); let dref = React.createRef<HTMLDivElement>(); - let dxf = () => this.getDocTransform(pair.layout!, dref.current!); + let dxf = () => this.getDocTransform(layoutDoc, dref.current!); this._docXfs.push({ dxf: dxf, width: width, height: height }); let rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); let style = this.isStackingView ? { width: width(), margin: "auto", marginTop: i === 0 ? 0 : this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` }; return <div className={`collectionStackingView-${this.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} > - {this.getDisplayDoc(pair.layout as Doc, pair.data, dxf, width)} + {this.getDisplayDoc(pair.layout || d, pair.data, dxf, width)} </div>; }); } @@ -74,12 +75,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { this._heightMap.set(key, sectionHeight); } - get layoutDoc() { - // if this document's layout field contains a document (ie, a rendering template), then we will use that - // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string. - return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document; - } - get Sections() { if (!this.sectionFilter || this.sectionHeaders instanceof Promise) return new Map<SchemaHeaderField, Doc[]>(); @@ -110,23 +105,30 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } componentDidMount() { - // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported). + super.componentDidMount(); this._heightDisposer = reaction(() => { - if (BoolCast(this.props.Document.autoHeight)) { + if (this.props.Document.autoHeight) { let sectionsList = Array.from(this.Sections.size ? this.Sections.values() : [this.filteredChildren]); if (this.isStackingView) { - return this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => Math.max(maxHght, - (this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap), this.yMargin)), 0); + let res = this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => { + let r1 = Math.max(maxHght, + (this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => { + let val = height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap); + return val; + }, this.yMargin)); + return r1; + }, 0); + return res; } else { let sum = Array.from(this._heightMap.values()).reduce((acc: number, curr: number) => acc += curr, 0); - return this.props.ContentScaling() * (sum + (this.Sections.size ? 85 : -15)); + return this.props.ContentScaling() * (sum + (this.Sections.size ? (this.props.Document.miniHeaders ? 20 : 85) : -15)); } } 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); + doc && hgt > 0 && (Doc.Layout(doc).height = hgt); }, { fireImmediately: true } ); @@ -138,6 +140,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { ); } componentWillUnmount() { + super.componentWillUnmount(); this._heightDisposer && this._heightDisposer(); this._sectionFilterDisposer && this._sectionFilterDisposer(); } @@ -158,17 +161,19 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } @computed get onClickHandler() { return ScriptCast(this.Document.onChildClick); } - getDisplayDoc(layoutDoc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) { - let height = () => this.getDocHeight(layoutDoc); + getDisplayDoc(doc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) { + let layoutDoc = Doc.Layout(doc); + let height = () => this.getDocHeight(doc); let finalDxf = () => dxf().scale(this.columnWidth / layoutDoc[WidthSym]()); return <CollectionSchemaPreview - Document={layoutDoc} + Document={doc} DataDocument={dataDoc} + fieldKey={this.props.fieldKey} showOverlays={this.overlays} renderDepth={this.props.renderDepth} ruleProvider={this.props.Document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider} fitToBox={this.props.fitToBox} - onClick={layoutDoc.isTemplate ? this.onClickHandler : this.onChildClickHandler} + onClick={layoutDoc.isTemplateDoc ? this.onClickHandler : this.onChildClickHandler} PanelWidth={width} PanelHeight={height} getTransform={finalDxf} @@ -188,15 +193,17 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } getDocHeight(d?: Doc) { if (!d) return 0; - let nw = NumCast(d.nativeWidth); - let nh = NumCast(d.nativeHeight); + let layoutDoc = Doc.Layout(d); + let nw = NumCast(layoutDoc.nativeWidth); + let nh = NumCast(layoutDoc.nativeHeight); let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); - if (!d.ignoreAspect && !d.fitWidth && nw && nh) { + if (!layoutDoc.ignoreAspect && !layoutDoc.fitWidth && nw && nh) { let aspect = nw && nh ? nh / nw : 1; - if (!(d.nativeWidth && !d.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(d[WidthSym](), wid); + if (!(d.nativeWidth && !layoutDoc.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid); return wid * aspect; } - return d.fitWidth ? !d.nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin : Math.min(wid * NumCast(d.scrollHeight, NumCast(d.nativeHeight)) / NumCast(d.nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : d[HeightSym](); + return layoutDoc.fitWidth ? !layoutDoc.nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin : + Math.min(wid * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc.nativeHeight)) / NumCast(layoutDoc.nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym](); } columnDividerDown = (e: React.PointerEvent) => { @@ -212,7 +219,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { let dragPos = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; let delta = dragPos - this._columnStart; this._columnStart = dragPos; - this.layoutDoc.columnWidth = this.columnWidth + delta; + this.layoutDoc.columnWidth = Math.max(10, this.columnWidth + delta); } @action @@ -356,7 +363,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } onToggle = (checked: Boolean) => { - this.props.ContainingCollectionDoc && (this.props.ContainingCollectionDoc.chromeStatus = checked ? "collapsed" : "view-mode"); + this.props.Document.chromeStatus = checked ? "collapsed" : "view-mode"; } onContextMenu = (e: React.MouseEvent): void => { @@ -381,12 +388,12 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { SetValue: this.addGroup, contents: "+ ADD A GROUP" }; - Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey); let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]]; if (this.sectionFilter) { let entries = Array.from(this.Sections.entries()); sections = entries.sort(this.sortFunc); } + console.log("NUM = " + this.numGroupColumns); return ( <div className="collectionStackingMasonry-cont" > <div className={this.isStackingView ? "collectionStackingView" : "collectionMasonryView"} diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 7e54b0f29..b9d334b10 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -2,7 +2,7 @@ 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, trace } from "mobx"; +import { action, observable, trace, runInAction } from "mobx"; import { observer } from "mobx-react"; import { Doc, WidthSym } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; @@ -204,7 +204,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC document.removeEventListener("pointerup", this.pointerUp); document.addEventListener("pointerup", this.pointerUp); } - this._createAliasSelected = false; + runInAction(() => this._createAliasSelected = false); } renderColorPicker = () => { @@ -295,7 +295,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC style={{ width: (style.columnWidth) / ((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) + ((this.props.parent.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.Document.chromeStatus !== 'disabled') ? 1 : 0)) || 1) }}> <div className={"collectionStackingView-collapseBar" + (this.props.headingObject.collapsed === true ? " active" : "")} onClick={this.collapseSection}></div> {/* the default bucket (no key value) has a tooltip that describes what it is. diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index fdbe5339d..d7e9494a3 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -6,7 +6,7 @@ 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 { Cast } from "../../../new_fields/Types"; +import { Cast, StrCast } from "../../../new_fields/Types"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { RouteStore } from "../../../server/RouteStore"; import { Utils } from "../../../Utils"; @@ -33,12 +33,15 @@ export interface CollectionViewProps extends FieldViewProps { VisibleHeight?: () => number; chromeCollapsed: boolean; setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; + fieldKey: string; } export interface SubCollectionViewProps extends CollectionViewProps { CollectionView: Opt<CollectionView>; ruleProvider: Doc | undefined; children?: never | (() => JSX.Element[]) | React.ReactNode; + isAnnotationOverlay?: boolean; + annotationsKey: string; } export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { @@ -57,23 +60,31 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { 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)))); + async (args) => { + if (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), "layoutFromParent")); + } + else if (!(args[1] instanceof Promise)) { + this.childDocs.filter(d => !d.isTemplateField).map(async doc => doc.layoutKey === "layoutFromParent" && (doc.layoutKey = "layout")); + } + }); } componentWillUnmount() { this._childLayoutDisposer && this._childLayoutDisposer(); } - // The data field for rendeing this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc. + @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); } + @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); } + + // The data field for rendering this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc. // When a document has a DataDoc but it's not a template, then it contains its own rendering data, but needs to pass the DataDoc through // to its children which may be templates. - // The name of the data field comes from fieldExt if it's an extension, or fieldKey otherwise. + // If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey' @computed get dataField() { - return Doc.fieldExtensionDoc(this.props.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt)[this.props.fieldExt || this.props.fieldKey]; + return this.props.annotationsKey ? (this.extensionDoc ? this.extensionDoc[this.props.annotationsKey] : undefined) : this.dataDoc[this.props.fieldKey]; } - get childLayoutPairs() { return this.childDocs.map(cd => Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, cd)).filter(pair => pair.layout).map(pair => ({ layout: pair.layout!, data: pair.data! })); } @@ -121,11 +132,12 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { @undoBatch @action protected drop(e: Event, de: DragManager.DropEvent): boolean { + (this.props.Document.dropConverter instanceof ScriptField) && + this.props.Document.dropConverter.script.run({ dragData: de.data }); if (de.data instanceof DragManager.DocumentDragData && !de.data.applyAsTemplate) { if (de.mods === "AltKey" && de.data.draggedDocuments.length) { this.childDocs.map(doc => - Doc.ApplyTemplateTo(de.data.draggedDocuments[0], doc) - ); + Doc.ApplyTemplateTo(de.data.draggedDocuments[0], doc, "layoutFromParent")); e.stopPropagation(); return true; } @@ -133,11 +145,10 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { if (de.data.dropAction || de.data.userDropAction) { added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false); } else if (de.data.moveDocument) { - let movedDocs = de.data.draggedDocuments;// de.data.options === this.props.Document[Id] ? de.data.draggedDocuments : de.data.droppedDocuments; - // note that it's possible the drag function might create a drop document that's not the same as the - // original dragged document. So we explicitly call addDocument() with a droppedDocument and + let movedDocs = de.data.draggedDocuments; added = movedDocs.reduce((added: boolean, d, i) => - de.data.moveDocument(d, this.props.Document, (doc: Doc) => this.props.addDocument(de.data.droppedDocuments[i])) || added, false); + de.data.droppedDocuments[i] !== d ? this.props.addDocument(de.data.droppedDocuments[i]) : + de.data.moveDocument(d, this.props.Document, this.props.addDocument) || added, false); } else { added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false); } diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index ca0c321b7..7d0c900a6 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -10,12 +10,12 @@ width:100%; position: relative; top:0; - padding-top: 20px; padding-left: 10px; padding-right: 10px; background: $light-color-secondary; font-size: 13px; overflow: auto; + cursor: default; ul { list-style: none; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index abaa9662c..47c355fc8 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -9,7 +9,7 @@ import { List } from '../../../new_fields/List'; import { Document, listSpec } from '../../../new_fields/Schema'; import { ComputedField, ScriptField } from '../../../new_fields/ScriptField'; import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types'; -import { emptyFunction, Utils } from '../../../Utils'; +import { emptyFunction, Utils, returnFalse } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from '../../util/DocumentManager'; @@ -23,7 +23,7 @@ import { EditableView } from "../EditableView"; import { MainView } from '../MainView'; import { KeyValueBox } from '../nodes/KeyValueBox'; import { Templates } from '../Templates'; -import { CollectionViewType } from './CollectionBaseView'; +import { CollectionViewType } from './CollectionView'; import { CollectionSchemaPreview } from './CollectionSchemaView'; import { CollectionSubView } from "./CollectionSubView"; import "./CollectionTreeView.scss"; @@ -82,56 +82,46 @@ 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 : this.props.document.defaultExpandedView ? StrCast(this.props.document.defaultExpandedView) : ""; } + get defaultExpandedView() { return this.childDocs ? this.fieldKey : StrCast(this.props.document.defaultExpandedView, "fields"); } @observable _overrideTreeViewOpen = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state - @computed get treeViewOpen() { return (BoolCast(this.props.document.treeViewOpen) && !this.props.preventTreeViewOpen) || this._overrideTreeViewOpen; } set treeViewOpen(c: boolean) { if (this.props.preventTreeViewOpen) this._overrideTreeViewOpen = c; else this.props.document.treeViewOpen = c; } + @computed get treeViewOpen() { return (BoolCast(this.props.document.treeViewOpen) && !this.props.preventTreeViewOpen) || this._overrideTreeViewOpen; } @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 dataDoc() { return this.templateDataDoc ? this.templateDataDoc : this.props.document; } @computed get fieldKey() { - let splits = StrCast(this.props.document.layout).split("fieldKey={\""); + let splits = StrCast(Doc.LayoutField(this.props.document)).split("fieldKey={\""); return splits.length > 1 ? splits[1].split("\"")[0] : "data"; } - @computed get childDocs() { - let layout = this.props.document.layout instanceof Doc ? this.props.document.layout : undefined; - return (this.props.dataDoc ? Cast(this.props.dataDoc[this.fieldKey], listSpec(Doc)) : undefined) || - (layout ? Cast(layout[this.fieldKey], listSpec(Doc)) : undefined) || - Cast(this.props.document[this.fieldKey], listSpec(Doc)); - } - @computed get childLinks() { - let layout = this.props.document.layout instanceof Doc ? this.props.document.layout : undefined; - return (this.props.dataDoc ? Cast(this.props.dataDoc.links, listSpec(Doc)) : undefined) || - (layout instanceof Doc ? Cast(layout.links, listSpec(Doc)) : undefined) || - Cast(this.props.document.links, listSpec(Doc)); + childDocList(field: string) { + let layout = Doc.LayoutField(this.props.document) instanceof Doc ? Doc.LayoutField(this.props.document) as Doc : undefined; + return ((this.props.dataDoc ? Cast(this.props.dataDoc[field], listSpec(Doc)) : undefined) || + (layout ? Cast(layout[field], listSpec(Doc)) : undefined) || + Cast(this.props.document[field], listSpec(Doc))) as Doc[]; } - @computed get resolvedDataDoc() { - if (this.props.dataDoc === undefined && this.props.document.layout instanceof Doc) { - // if there is no dataDoc (ie, we're not rendering a template layout), but this document - // has a template layout document, then we will render the template layout but use - // this document as the data document for the layout. + @computed get childDocs() { return this.childDocList(this.fieldKey); } + @computed get childLinks() { return this.childDocList("links"); } + @computed get templateDataDoc() { + if (this.props.dataDoc === undefined && Doc.LayoutField(this.props.document) !== "string") { + // if there is no dataDoc (ie, we're not rendering a template layout), but this document has a layout document (not a layout string), + // then we render the layout document as a template and use this document as the data context for the template layout. return this.props.document; } return this.props.dataDoc; } @computed get boundsOfCollectionDocument() { return StrCast(this.props.document.type).indexOf(DocumentType.COL) === -1 ? undefined : - Doc.ComputeContentBounds(DocListCast(this.props.document.data)); + Doc.ComputeContentBounds(DocListCast(this.props.document[this.fieldKey])); } - @undoBatch delete = () => this.props.deleteDoc(this.dataDoc); - @undoBatch openRight = () => this.props.addDocTab(this.props.document, undefined, "onRight"); + @undoBatch delete = () => this.props.deleteDoc(this.props.document); + @undoBatch openRight = () => this.props.addDocTab(this.props.document, this.templateDataDoc, "onRight"); @undoBatch indent = () => this.props.addDocument(this.props.document) && this.delete(); @undoBatch move = (doc: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => { return this.props.document !== target && this.props.deleteDoc(doc) && addDoc(doc); } - @undoBatch @action remove = (document: Document, key: string): boolean => { - let children = Cast(this.dataDoc[key], listSpec(Doc), []); - if (children.indexOf(document) !== -1) { - children.splice(children.indexOf(document), 1); - return true; - } - return false; + @undoBatch @action remove = (document: Document, key: string) => { + return Doc.RemoveDocFromList(this.dataDoc, key, document); } protected createTreeDropTarget = (ele: HTMLDivElement) => { @@ -175,9 +165,9 @@ class TreeView extends React.Component<TreeViewProps> { fontStyle={style} fontSize={12} GetValue={() => StrCast(this.props.document[key])} - SetValue={undoBatch((value: string) => (Doc.GetProto(this.dataDoc)[key] = value) ? true : true)} + SetValue={undoBatch((value: string) => Doc.SetInPlace(this.props.document, key, value, false) || true)} OnFillDown={undoBatch((value: string) => { - Doc.GetProto(this.dataDoc)[key] = value; + Doc.SetInPlace(this.props.document, key, value, false); let doc = this.props.document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.layoutCustom)) : undefined; if (!doc) doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); TreeView.loadId = doc[Id]; @@ -190,10 +180,10 @@ class TreeView extends React.Component<TreeViewProps> { if (!e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking && this.props.document !== CurrentUserUtils.UserDocument.workspaces) { ContextMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.document), icon: "tv" }); - ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "inTab"), icon: "folder" }); - ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "onRight"), icon: "caret-square-right" }); + ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.templateDataDoc, "inTab"), icon: "folder" }); + ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.templateDataDoc, "onRight"), icon: "caret-square-right" }); if (DocumentManager.Instance.getDocumentViews(this.dataDoc).length) { - ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.dataDoc).map(view => view.props.focus(this.props.document, true)), icon: "camera" }); + ContextMenu.Instance.addItem({ description: "Focus", event: () => (view => view && view.props.focus(this.props.document, true))(DocumentManager.Instance.getFirstDocumentView(this.dataDoc)), icon: "camera" }); } ContextMenu.Instance.addItem({ description: "Delete Item", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" }); } else { @@ -225,19 +215,16 @@ class TreeView extends React.Component<TreeViewProps> { if (de.data instanceof DragManager.DocumentDragData) { e.stopPropagation(); if (de.data.draggedDocuments[0] === this.props.document) return true; - let addDoc = (doc: Doc) => this.props.addDocument(doc, this.resolvedDataDoc, before); + let addDoc = (doc: Doc) => this.props.addDocument(doc, undefined, before); if (inside) { - let docList = Cast(this.dataDoc.data, listSpec(Doc)); - if (docList !== undefined) { - addDoc = (doc: Doc) => { docList && docList.push(doc); return true; }; - } + addDoc = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) || addDoc(doc); } let movedDocs = (de.data.options === this.props.treeViewId ? de.data.draggedDocuments : de.data.droppedDocuments); return (de.data.dropAction || de.data.userDropAction) ? - de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before) || added, false) - : (de.data.moveDocument) ? - movedDocs.reduce((added: boolean, d) => de.data.moveDocument(d, this.resolvedDataDoc, addDoc) || added, false) - : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before), false); + de.data.droppedDocuments.reduce((added, d) => this.props.addDocument(d, undefined, before) || added, false) + : de.data.moveDocument ? + movedDocs.reduce((added, d) => de.data.moveDocument(d, undefined, addDoc) || added, false) + : de.data.droppedDocuments.reduce((added, d) => this.props.addDocument(d, undefined, before), false); } return false; } @@ -250,20 +237,22 @@ class TreeView extends React.Component<TreeViewProps> { return finalXf; } docWidth = () => { - let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth); - if (aspect) return Math.min(this.props.document[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20)); - return NumCast(this.props.document.nativeWidth) ? Math.min(this.props.document[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20; + let layoutDoc = Doc.Layout(this.props.document); + let aspect = NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth); + if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20)); + return NumCast(layoutDoc.nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20; } docHeight = () => { + let layoutDoc = Doc.Layout(this.props.document); let bounds = this.boundsOfCollectionDocument; return Math.min(this.MAX_EMBED_HEIGHT, (() => { - let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth); + let aspect = NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth, 1); if (aspect) return this.docWidth() * aspect; if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x); - return this.props.document.fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection.height) : - Math.min(this.docWidth() * NumCast(this.props.document.scrollHeight, NumCast(this.props.document.nativeHeight)) / NumCast(this.props.document.nativeWidth, + return layoutDoc.fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection.height) : + Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc.nativeHeight)) / NumCast(layoutDoc.nativeWidth, NumCast(this.props.containingCollection.height)))) : - NumCast(this.props.document.height) ? NumCast(this.props.document.height) : 50; + NumCast(layoutDoc.height) ? NumCast(layoutDoc.height) : 50; })()); } @@ -312,22 +301,23 @@ class TreeView extends React.Component<TreeViewProps> { let docs = expandKey === "links" ? this.childLinks : this.childDocs; return <ul key={expandKey + "more"}> {!docs ? (null) : - TreeView.GetChildElements(docs as Doc[], this.props.treeViewId, this.props.document.layout as Doc, - this.resolvedDataDoc, expandKey, addDoc, remDoc, this.move, + TreeView.GetChildElements(docs, this.props.treeViewId, Doc.Layout(this.props.document), + this.templateDataDoc, expandKey, addDoc, remDoc, this.move, this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth, this.props.showHeaderFields, this.props.preventTreeViewOpen, [...this.props.renderedIds, this.props.document[Id]])} </ul >; } else if (this.treeViewExpandedView === "fields") { return <ul><div ref={this._dref} style={{ display: "inline-block" }} key={this.props.document[Id] + this.props.document.title}> - {this.dataDoc ? this.expandedField(this.dataDoc) : (null)} + {this.expandedField(this.props.document)} </div></ul>; } else { - let layoutDoc = this.props.document; + let layoutDoc = Doc.Layout(this.props.document); return <div ref={this._dref} style={{ display: "inline-block", height: this.docHeight() }} key={this.props.document[Id] + this.props.document.title}> <CollectionSchemaPreview Document={layoutDoc} - DataDocument={this.resolvedDataDoc} + DataDocument={this.templateDataDoc} + fieldKey={this.fieldKey} renderDepth={this.props.renderDepth} showOverlays={this.noOverlays} ruleProvider={this.props.document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.document : this.props.ruleProvider} @@ -337,15 +327,14 @@ class TreeView extends React.Component<TreeViewProps> { getTransform={this.docTransform} CollectionDoc={this.props.containingCollection} CollectionView={undefined} - addDocument={emptyFunction as any} + addDocument={returnFalse} moveDocument={this.props.moveDocument} - removeDocument={emptyFunction as any} + removeDocument={returnFalse} active={this.props.active} - whenActiveChanged={emptyFunction as any} + whenActiveChanged={emptyFunction} addDocTab={this.props.addDocTab} pinToPres={this.props.pinToPres} - setPreviewScript={emptyFunction}> - </CollectionSchemaPreview> + setPreviewScript={emptyFunction} /> </div>; } } @@ -369,7 +358,7 @@ class TreeView extends React.Component<TreeViewProps> { onPointerDown={action(() => { if (this.treeViewOpen) { this.props.document.treeViewExpandedView = this.treeViewExpandedView === this.fieldKey ? "fields" : - this.treeViewExpandedView === "fields" && this.props.document.layout ? "layout" : + this.treeViewExpandedView === "fields" && Doc.Layout(this.props.document) ? "layout" : this.treeViewExpandedView === "layout" && this.props.document.links ? "links" : this.childDocs ? this.fieldKey : "fields"; } @@ -461,7 +450,7 @@ class TreeView extends React.Component<TreeViewProps> { let rowWidth = () => panelWidth() - 20; return docs.map((child, i) => { - let pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, key, child); + const pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, key, child); if (!pair.layout || pair.data instanceof Promise) { return (null); } @@ -480,9 +469,10 @@ class TreeView extends React.Component<TreeViewProps> { let addDocument = (doc: Doc, relativeTo?: Doc, before?: boolean) => { return add(doc, relativeTo ? relativeTo : docs[i], before !== undefined ? before : false); }; + const childLayout = Doc.Layout(pair.layout); let rowHeight = () => { - let aspect = NumCast(child.nativeWidth, 0) / NumCast(child.nativeHeight, 0); - return aspect ? Math.min(child[WidthSym](), rowWidth()) / aspect : child[HeightSym](); + let aspect = NumCast(childLayout.nativeWidth, 0) / NumCast(childLayout.nativeHeight, 0); + return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym](); }; return <TreeView document={pair.layout} @@ -517,7 +507,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { private treedropDisposer?: DragManager.DragDropDisposer; private _mainEle?: HTMLDivElement; - @computed get resolvedDataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; } + @computed get dataDoc() { return this.props.DataDoc || this.props.Document; } protected createTreeDropTarget = (ele: HTMLDivElement) => { this.treedropDisposer && this.treedropDisposer(); @@ -527,6 +517,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { } componentWillUnmount() { + super.componentWillUnmount(); this.treedropDisposer && this.treedropDisposer(); } @@ -566,26 +557,25 @@ export class CollectionTreeView extends CollectionSubView(Document) { } render() { - Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey); let dropAction = StrCast(this.props.Document.dropAction) as dropActionType; let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, false); let moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc); return !this.childDocs ? (null) : ( <div id="body" className="collectionTreeView-dropTarget" - style={{ overflow: "auto", background: StrCast(this.props.Document.backgroundColor, "lightgray") }} + style={{ overflow: "auto", background: StrCast(this.props.Document.backgroundColor, "lightgray"), paddingTop: `${NumCast(this.props.Document.yMargin, 20)}px` }} onContextMenu={this.onContextMenu} onWheel={(e: React.WheelEvent) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()} onDrop={this.onTreeDrop} ref={this.createTreeDropTarget}> <EditableView - contents={this.resolvedDataDoc.title} + contents={this.dataDoc.title} display={"block"} maxHeight={72} height={"auto"} - GetValue={() => StrCast(this.resolvedDataDoc.title)} - SetValue={undoBatch((value: string) => (Doc.GetProto(this.resolvedDataDoc).title = value) ? true : true)} + GetValue={() => StrCast(this.dataDoc.title)} + SetValue={undoBatch((value: string) => Doc.SetInPlace(this.dataDoc, "title", value, false) || true)} OnFillDown={undoBatch((value: string) => { - Doc.GetProto(this.props.Document).title = value; + Doc.SetInPlace(this.dataDoc, "title", value, false); let doc = this.props.Document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.layoutCustom)) : undefined; if (!doc) doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); TreeView.loadId = doc[Id]; @@ -596,7 +586,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { { TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.Document, this.props.DataDoc, this.props.fieldKey, addDoc, this.remove, moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, - this.outerXf, this.props.active, this.props.PanelWidth, this.props.renderDepth, () => this.props.Document.chromeStatus !== "disabled", + this.outerXf, this.props.active, this.props.PanelWidth, this.props.renderDepth, () => !this.props.Document.hideHeaderFields, BoolCast(this.props.Document.preventTreeViewOpen), []) } </ul> diff --git a/src/client/views/collections/CollectionBaseView.scss b/src/client/views/collections/CollectionView.scss index aff965469..e4187e4d6 100644 --- a/src/client/views/collections/CollectionBaseView.scss +++ b/src/client/views/collections/CollectionView.scss @@ -1,6 +1,6 @@ @import "../globalCssVariables"; -#collectionBaseView { +.collectionView { border-width: 0; border-color: $light-color-secondary; border-style: solid; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index db9b002cf..4e3c7986d 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -5,12 +5,9 @@ import { action, IReactionDisposer, observable, reaction, runInAction } from 'mo import { observer } from "mobx-react"; import * as React from 'react'; import { Id } from '../../../new_fields/FieldSymbols'; -import { StrCast } from '../../../new_fields/Types'; +import { StrCast, BoolCast, Cast } from '../../../new_fields/Types'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; import { ContextMenu } from "../ContextMenu"; -import { ContextMenuProps } from '../ContextMenuItem'; -import { FieldView, FieldViewProps } from '../nodes/FieldView'; -import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from './CollectionBaseView'; import { CollectionDockingView } from "./CollectionDockingView"; import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines'; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; @@ -21,20 +18,82 @@ import { CollectionViewBaseChrome } from './CollectionViewChromes'; import { ImageUtils } from '../../util/Import & Export/ImageUtils'; import { CollectionLinearView } from '../CollectionLinearView'; import { CollectionStaffView } from './CollectionStaffView'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { ImageField } from '../../../new_fields/URLField'; +import { DocListCast } from '../../../new_fields/Doc'; +import Lightbox from 'react-image-lightbox-with-rotate'; +import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app export const COLLECTION_BORDER_WIDTH = 2; - +import { DateField } from '../../../new_fields/DateField'; +import { Doc, } from '../../../new_fields/Doc'; +import { listSpec } from '../../../new_fields/Schema'; +import { DocumentManager } from '../../util/DocumentManager'; +import { SelectionManager } from '../../util/SelectionManager'; +import './CollectionView.scss'; +import { FieldViewProps, FieldView } from '../nodes/FieldView'; +import { Touchable } from '../Touchable'; library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy); +export enum CollectionViewType { + Invalid, + Freeform, + Schema, + Docking, + Tree, + Stacking, + Masonry, + Pivot, + Linear, + Staff +} + +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], + ["pivot", CollectionViewType.Pivot], + ["linear", CollectionViewType.Linear] + ]); + + export const valueOf = (value: string) => stringMapping.get(value.toLowerCase()); +} + +export interface CollectionRenderProps { + addDocument: (document: Doc) => boolean; + removeDocument: (document: Doc) => boolean; + moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; + active: () => boolean; + whenActiveChanged: (isActive: boolean) => void; +} + @observer -export class CollectionView extends React.Component<FieldViewProps> { - @observable private _collapsed = true; +export class CollectionView extends Touchable<FieldViewProps> { + public static LayoutString(fieldStr: string) { return FieldView.LayoutString(CollectionView, fieldStr); } private _reactionDisposer: IReactionDisposer | undefined; + private _isChildActive = false; //TODO should this be observable? + @observable private _isLightboxOpen = false; + @observable private _curLightboxImg = 0; + @observable private _collapsed = true; + @observable private static _safeMode = false; + public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; } - public static LayoutString(fieldStr: string = "data", fieldExt: string = "") { return FieldView.LayoutString(CollectionView, fieldStr, fieldExt); } - - constructor(props: any) { - super(props); + get collectionViewType(): CollectionViewType | undefined { + let viewField = Cast(this.props.Document.viewType, "number"); + if (CollectionView._safeMode) { + if (viewField === CollectionViewType.Freeform) { + return CollectionViewType.Tree; + } + if (viewField === CollectionViewType.Invalid) { + return CollectionViewType.Freeform; + } + } + return viewField === undefined ? CollectionViewType.Invalid : viewField; } componentDidMount = () => { @@ -49,33 +108,74 @@ export class CollectionView extends React.Component<FieldViewProps> { }); } - componentWillUnmount = () => { - this._reactionDisposer && this._reactionDisposer(); + componentWillUnmount = () => this._reactionDisposer && this._reactionDisposer(); + + // bcz: Argh? What's the height of the collection chromes?? + chromeHeight = () => (this.props.ChromeHeight ? this.props.ChromeHeight() : 0) + (this.props.Document.chromeStatus === "enabled" ? -60 : 0); + + active = () => this.props.isSelected() || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0; + + whenActiveChanged = (isActive: boolean) => { this.props.whenActiveChanged(this._isChildActive = isActive); }; + + @action.bound + addDocument(doc: Doc): boolean { + let targetDataDoc = Doc.GetProto(this.props.Document); + Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc); + let extension = Doc.fieldExtensionDoc(targetDataDoc, this.props.fieldKey); // set metadata about the field being rendered (ie, the set of documents) on an extension field for that field + extension && (extension.lastModified = new DateField(new Date(Date.now()))); + Doc.GetProto(doc).lastOpened = new DateField; + return true; + } + + @action.bound + removeDocument(doc: Doc): boolean { + let docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView); + docView && SelectionManager.DeselectDoc(docView); + let value = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []); + let index = value.reduce((p, v, i) => (v instanceof Doc && v === doc) ? i : p, -1); + index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1); + + ContextMenu.Instance.clearItems(); + if (index !== -1) { + value.splice(index, 1); + return true; + } + return false; + } + + // this is called with the document that was dragged and the collection to move it into. + // if the target collection is the same as this collection, then the move will be allowed. + // otherwise, the document being moved must be able to be removed from its container before + // moving it into the target. + @action.bound + moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean { + if (Doc.AreProtosEqual(this.props.Document, targetCollection)) { + return true; + } + return this.removeDocument(doc) ? addDocument(doc) : false; } - // bcz: Argh? What's the height of the collection chomes?? - chromeHeight = () => { - return (this.props.ChromeHeight ? this.props.ChromeHeight() : 0) + (this.props.Document.chromeStatus === "enabled" ? -60 : 0); + showIsTagged = () => { + const children = DocListCast(this.props.Document[this.props.fieldKey]); + const imageProtos = children.filter(doc => Cast(doc.data, ImageField)).map(Doc.GetProto); + const allTagged = imageProtos.length > 0 && imageProtos.every(image => image.googlePhotosTags); + return !allTagged ? (null) : <img id={"google-tags"} src={"/assets/google_tags.png"} />; } private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => { - let props = { ...this.props, ...renderProps }; - switch (this.isAnnotationOverlay ? CollectionViewType.Freeform : type) { - case CollectionViewType.Schema: return (<CollectionSchemaView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); - // currently cant think of a reason for collection docking view to have a chrome. mind may change if we ever have nested docking views -syip - case CollectionViewType.Docking: return (<CollectionDockingView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); - case CollectionViewType.Tree: return (<CollectionTreeView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); - case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); } - case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); } - case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); } + let props = { ...this.props, ...renderProps, chromeCollapsed: this._collapsed, ChromeHeight: this.chromeHeight, CollectionView: this, annotationsKey: "" }; + switch (type) { + case CollectionViewType.Schema: return (<CollectionSchemaView key="collview" {...props} />); + case CollectionViewType.Docking: return (<CollectionDockingView key="collview" {...props} />); + case CollectionViewType.Tree: return (<CollectionTreeView key="collview" {...props} />); case CollectionViewType.Staff: return (<CollectionStaffView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />) - case CollectionViewType.Linear: { return (<CollectionLinearView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); } + case CollectionViewType.Linear: { return (<CollectionLinearView key="collview" {...props} />); } + case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView key="collview" {...props} />); } + case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView key="collview" {...props} />); } + case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView key="collview" {...props} />); } case CollectionViewType.Freeform: - default: - this.props.Document.freeformLayoutEngine = undefined; - return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); + default: { this.props.Document.freeformLayoutEngine = undefined; return (<CollectionFreeFormView key="collview" {...props} />); } } - return (null); } @action @@ -86,23 +186,18 @@ export class CollectionView extends React.Component<FieldViewProps> { private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => { // currently cant think of a reason for collection docking view to have a chrome. mind may change if we ever have nested docking views -syip - if (this.isAnnotationOverlay || this.props.Document.chromeStatus === "disabled" || type === CollectionViewType.Docking) { - return [(null), this.SubViewHelper(type, renderProps)]; - } - return [ - <CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />, - this.SubViewHelper(type, renderProps) - ]; + let chrome = this.props.Document.chromeStatus === "disabled" || type === CollectionViewType.Docking ? (null) : + <CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />; + return [chrome, this.SubViewHelper(type, renderProps)]; } - get isAnnotationOverlay() { return this.props.fieldExt ? true : false; } 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 + if (!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 : []; + let subItems = existingVm && "subitems" in existingVm ? existingVm.subitems : []; subItems.push({ description: "Freeform", event: () => { this.props.Document.viewType = CollectionViewType.Freeform; }, icon: "signature" }); - if (CollectionBaseView.InSafeMode()) { + if (CollectionView._safeMode) { ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document.viewType = CollectionViewType.Invalid, icon: "project-diagram" }); } subItems.push({ description: "Schema", event: () => this.props.Document.viewType = CollectionViewType.Schema, icon: "th-list" }); @@ -123,21 +218,43 @@ export class CollectionView extends React.Component<FieldViewProps> { break; } } + subItems.push({ description: "lightbox", event: action(() => this._isLightboxOpen = true), icon: "eye" }); !existingVm && ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems, icon: "eye" }); let existing = ContextMenu.Instance.findByDescription("Layout..."); - let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : []; + let layoutItems = existing && "subitems" in existing ? existing.subitems : []; 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" }); ContextMenu.Instance.addItem({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) }); } } + lightbox = (images: string[]) => { + return !this._isLightboxOpen ? (null) : (<Lightbox key="lightbox" + mainSrc={images[this._curLightboxImg]} + nextSrc={images[(this._curLightboxImg + 1) % images.length]} + prevSrc={images[(this._curLightboxImg + images.length - 1) % images.length]} + onCloseRequest={action(() => this._isLightboxOpen = false)} + onMovePrevRequest={action(() => this._curLightboxImg = (this._curLightboxImg + images.length - 1) % images.length)} + onMoveNextRequest={action(() => this._curLightboxImg = (this._curLightboxImg + 1) % images.length)} />); + } render() { - return ( - <CollectionBaseView {...this.props} onContextMenu={this.onContextMenu}> - {this.SubView} - </CollectionBaseView> - ); + const props: CollectionRenderProps = { + addDocument: this.addDocument, + removeDocument: this.removeDocument, + moveDocument: this.moveDocument, + active: this.active, + whenActiveChanged: this.whenActiveChanged, + }; + return (<div className={"collectionView"} + style={{ + pointerEvents: this.props.Document.isBackground ? "none" : "all", + boxShadow: this.props.Document.isBackground || this.collectionViewType === CollectionViewType.Linear ? undefined : `#9c9396 ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}` + }} + onContextMenu={this.onContextMenu}> + {this.showIsTagged()} + {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)} + {this.lightbox(DocListCast(this.props.Document[this.props.fieldKey]).filter(d => d.type === DocumentType.IMG).map(d => Cast(d.data, ImageField) ? Cast(d.data, ImageField)!.url.href : ""))} + </div>); } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index 3a66c05f4..cfc6c2a3f 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -14,7 +14,7 @@ import { undoBatch } from "../../util/UndoManager"; import { EditableView } from "../EditableView"; import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss"; import { DocLike } from "../MetadataEntryMenu"; -import { CollectionViewType } from "./CollectionBaseView"; +import { CollectionViewType } from "./CollectionView"; import { CollectionView } from "./CollectionView"; import "./CollectionViewChromes.scss"; import * as Autosuggest from 'react-autosuggest'; @@ -40,9 +40,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro //(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\) _templateCommand = { - title: "set template", script: "this.target.childLayout = this.source ? this.source[0] : undefined", params: ["target", "source"], + title: "set template", script: "setChildLayout(this.target, this.source && this.source.length ? this.source[0]:undefined)", params: ["target", "source"], initialize: emptyFunction, - immediate: (draggedDocs: Doc[]) => this.props.CollectionView.props.Document.childLayout = draggedDocs.length ? draggedDocs[0] : undefined + immediate: (draggedDocs: Doc[]) => Doc.setChildLayout(this.props.CollectionView.props.Document, draggedDocs.length ? draggedDocs[0] : undefined) }; _contentCommand = { // title: "set content", script: "getProto(this.target).data = aliasDocs(this.source.map(async p => await p));", params: ["target", "source"], // bcz: doesn't look like we can do async stuff in scripting... @@ -480,12 +480,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView 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; + let docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]); if (docs instanceof Doc) { return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value)); } else { @@ -591,19 +586,9 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh if (textwrappedRows.length) { this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>([]); } else { - 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) { - let allRows = [docs[Id]]; - this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows); - } else { - let allRows = docs.map(doc => doc[Id]); - this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows); - } + let docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]); + let allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]); + this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows); } } @@ -638,63 +623,14 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh @observer export class CollectionTreeViewChrome extends React.Component<CollectionViewChromeProps> { - @observable private _currentKey: string = ""; - @observable private suggestions: string[] = []; @computed private get descending() { return Cast(this.props.CollectionView.props.Document.sortAscending, "boolean", null); } - @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 = () => { if (this.props.CollectionView.props.Document.sortAscending) this.props.CollectionView.props.Document.sortAscending = undefined; else if (this.props.CollectionView.props.Document.sortAscending === undefined) this.props.CollectionView.props.Document.sortAscending = false; else this.props.CollectionView.props.Document.sortAscending = true; } - @action resetValue = () => { this._currentKey = this.sectionFilter; }; render() { return ( diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index 7f2913214..8b6fa330c 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -7,7 +7,7 @@ import { Id } from "../../../new_fields/FieldSymbols"; import { SearchUtil } from "../../util/SearchUtil"; import { CollectionDockingView } from "./CollectionDockingView"; import { NumCast } from "../../../new_fields/Types"; -import { CollectionViewType } from "./CollectionBaseView"; +import { CollectionViewType } from "./CollectionView"; import { DocumentButtonBar } from "../DocumentButtonBar"; import { DocumentManager } from "../../util/DocumentManager"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index 886692172..48d330674 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -63,11 +63,12 @@ export function computePivotLayout(pivotDoc: Doc, childDocs: Doc[], childPairs: fontSize: NumCast(pivotDoc.pivotFontSize, 10) }); for (const doc of val) { + let layoutDoc = Doc.Layout(doc); docMap.set(doc, { x: x + xCount * pivotAxisWidth * 1.25, y: -y, width: pivotAxisWidth, - height: doc.nativeWidth ? (NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth + height: layoutDoc.nativeWidth ? (NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth }); xCount++; if (xCount >= numCols) { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss index cfd18ad35..75af11537 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss @@ -1,20 +1,18 @@ .collectionfreeformlinkview-linkLine { stroke: black; - transform: translate(10000px,10000px); opacity: 0.8; pointer-events: all; stroke-width: 3px; + transition: opacity 0.5s ease-in; } .collectionfreeformlinkview-linkCircle { stroke: rgb(0,0,0); opacity: 0.5; - transform: translate(10000px,10000px); pointer-events: all; cursor: pointer; } .collectionfreeformlinkview-linkText { stroke: rgb(0,0,0); opacity: 0.5; - transform: translate(10000px,10000px); pointer-events: all; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index fe92eed10..837413842 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -1,63 +1,51 @@ import { observer } from "mobx-react"; -import { Doc, HeightSym, WidthSym } from "../../../../new_fields/Doc"; -import { BoolCast, NumCast, StrCast } from "../../../../new_fields/Types"; -import { InkingControl } from "../../InkingControl"; +import { Doc } from "../../../../new_fields/Doc"; +import { Utils } from '../../../../Utils'; +import { DocumentView } from "../../nodes/DocumentView"; import "./CollectionFreeFormLinkView.scss"; import React = require("react"); import v5 = require("uuid/v5"); +import { DocumentType } from "../../../documents/DocumentTypes"; +import { observable, action } from "mobx"; export interface CollectionFreeFormLinkViewProps { - A: Doc; - B: Doc; + A: DocumentView; + B: DocumentView; LinkDocs: Doc[]; - addDocument: (document: Doc) => boolean; - removeDocument: (document: Doc) => boolean; } @observer export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> { - - onPointerDown = (e: React.PointerEvent) => { - if (e.button === 0 && !InkingControl.Instance.selectedTool) { - let a = this.props.A; - let b = this.props.B; - let x1 = NumCast(a.x) + (BoolCast(a.isMinimized) ? 5 : a[WidthSym]() / 2); - let y1 = NumCast(a.y) + (BoolCast(a.isMinimized) ? 5 : a[HeightSym]() / 2); - let x2 = NumCast(b.x) + (BoolCast(b.isMinimized) ? 5 : b[WidthSym]() / 2); - let y2 = NumCast(b.y) + (BoolCast(b.isMinimized) ? 5 : b[HeightSym]() / 2); - // this.props.LinkDocs.map(l => { - // let width = l[WidthSym](); - // l.x = (x1 + x2) / 2 - width / 2; - // l.y = (y1 + y2) / 2 + 10; - // if (!this.props.removeDocument(l)) this.props.addDocument(l, false); - // }); - e.stopPropagation(); - e.preventDefault(); - } + @observable _alive: number = 0; + @observable _opacity: number = 1; + @action + componentDidMount() { + this._alive = 1; + setTimeout(this.rerender, 50); + setTimeout(action(() => this._opacity = 0.05), 50); + } + @action + componentWillUnmount() { + this._alive = 0; } + rerender = action(() => { + if (this._alive) { + setTimeout(this.rerender, 50); + this._alive++; + } + }); + render() { - // let l = this.props.LinkDocs; - let a = this.props.A; - let b = this.props.B; - let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / 2); - let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / 2); - let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / 2); - let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / 2); - let text = ""; - // let first = this.props.LinkDocs[0]; - // if (this.props.LinkDocs.length === 1) text += first.title + (first.linkDescription ? "(" + StrCast(first.linkDescription) + ")" : ""); - // else text = "-multiple-"; - return ( - <> - <line key="linkLine" className="collectionfreeformlinkview-linkLine" - x1={`${x1}`} y1={`${y1}`} - x2={`${x2}`} y2={`${y2}`} /> - {/* <circle key="linkCircle" className="collectionfreeformlinkview-linkCircle" - cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={8} onPointerDown={this.onPointerDown} /> */} - <text key="linkText" textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${(x1 + x2) / 2}`} y={`${(y1 + y2) / 2}`}> - {text} - </text> - </> - ); + let y = this._alive; + let acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : []; + let bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : []; + let a = (acont.length ? acont[0] : this.props.A.ContentDiv!).getBoundingClientRect(); + let b = (bcont.length ? bcont[0] : this.props.B.ContentDiv!).getBoundingClientRect(); + let pt1 = Utils.getNearestPointInPerimeter(a.left, a.top, a.width, a.height, b.left + b.width / 2, b.top + b.height / 2); + let pt2 = Utils.getNearestPointInPerimeter(b.left, b.top, b.width, b.height, a.left + a.width / 2, a.top + a.height / 2); + return (<line key="linkLine" className="collectionfreeformlinkview-linkLine" + style={{ opacity: this._opacity }} + x1={`${pt1[0]}`} y1={`${pt1[1]}`} + x2={`${pt2[0]}`} y2={`${pt2[1]}`} />); } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss index 30e158603..cb5cef29c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss @@ -1,10 +1,9 @@ .collectionfreeformlinksview-svgCanvas{ - transform: translate(-10000px,-10000px); position: absolute; top: 0; left: 0; - width: 20000px; - height: 20000px; + width: 100%; + height: 100%; pointer-events: none; } .collectionfreeformlinksview-container { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index a81f5315a..e9191c176 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -1,19 +1,18 @@ -import { computed, IReactionDisposer, reaction } from "mobx"; +import { computed, IReactionDisposer } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast } from "../../../../new_fields/Doc"; +import { Doc } from "../../../../new_fields/Doc"; import { Id } from "../../../../new_fields/FieldSymbols"; -import { List } from "../../../../new_fields/List"; -import { listSpec } from "../../../../new_fields/Schema"; -import { Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types"; import { DocumentManager } from "../../../util/DocumentManager"; import { DocumentView } from "../../nodes/DocumentView"; -import { CollectionViewProps } from "../CollectionSubView"; import "./CollectionFreeFormLinksView.scss"; import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView"; import React = require("react"); +import { Utils } from "../../../../Utils"; +import { SelectionManager } from "../../../util/SelectionManager"; +import { DocumentType } from "../../../documents/DocumentTypes"; @observer -export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> { +export class CollectionFreeFormLinksView extends React.Component { _brushReactionDisposer?: IReactionDisposer; componentDidMount() { @@ -69,59 +68,33 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP // }); } componentWillUnmount() { - if (this._brushReactionDisposer) { - this._brushReactionDisposer(); - } + this._brushReactionDisposer && this._brushReactionDisposer(); } - documentAnchors(view: DocumentView) { - let equalViews = [view]; - let containerDoc = FieldValue(Cast(view.props.Document.annotationOn, Doc)); - if (containerDoc) { - equalViews = DocumentManager.Instance.getDocumentViews(containerDoc.proto!); - } - if (view.props.ContainingCollectionDoc) { - let collid = view.props.ContainingCollectionDoc[Id]; - DocListCast(this.props.Document[this.props.fieldKey]). - filter(child => - child[Id] === collid).map(view => - DocumentManager.Instance.getDocumentViews(view).map(view => - equalViews.push(view))); - } - return equalViews.filter(sv => sv.props.ContainingCollectionDoc === this.props.Document); - } - @computed get uniqueConnections() { let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => { - let srcViews = this.documentAnchors(connection.a); - let targetViews = this.documentAnchors(connection.b); - - let possiblePairs: { a: Doc, b: Doc, }[] = []; - srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document }))); - possiblePairs.map(possiblePair => { - if (!drawnPairs.reduce((found, drawnPair) => { - let match1 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.a) && Doc.AreProtosEqual(possiblePair.b, drawnPair.b)); - let match2 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.b) && Doc.AreProtosEqual(possiblePair.b, drawnPair.a)); - let match = match1 || match2; - if (match && !drawnPair.l.reduce((found, link) => found || link[Id] === connection.l[Id], false)) { - drawnPair.l.push(connection.l); - } - return match || found; - }, false)) { - drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] }); + if (!drawnPairs.reduce((found, drawnPair) => { + let match1 = (connection.a === drawnPair.a && connection.b === drawnPair.b); + let match2 = (connection.a === drawnPair.b && connection.b === drawnPair.a); + let match = match1 || match2; + if (match && !drawnPair.l.reduce((found, link) => found || link[Id] === connection.l[Id], false)) { + drawnPair.l.push(connection.l); } - }); + return match || found; + }, false)) { + drawnPairs.push({ a: connection.a, b: connection.b, l: [connection.l] }); + } return drawnPairs; - }, [] as { a: Doc, b: Doc, l: Doc[] }[]); - return connections.map(c => <CollectionFreeFormLinkView key={c.l.reduce((p, l) => p + l[Id], "")} A={c.a} B={c.b} LinkDocs={c.l} - removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />); + }, [] as { a: DocumentView, b: DocumentView, l: Doc[] }[]); + return connections.filter(c => c.a.props.Document.type === DocumentType.LINK) // get rid of the filter to show links to documents in addition to document anchors + .map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />); } render() { return ( <div className="collectionfreeformlinksview-container"> <svg className="collectionfreeformlinksview-svgCanvas"> - {this.uniqueConnections} + {SelectionManager.GetIsDragging() ? (null) : this.uniqueConnections} </svg> {this.props.children} </div> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index c24e52aba..5e2973368 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -10,7 +10,7 @@ import { createSchema, makeInterface } from "../../../../new_fields/Schema"; import { ScriptField } from "../../../../new_fields/ScriptField"; 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 { aggregateBounds, emptyFunction, intersectRect, returnOne, Utils } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { DocServer } from "../../../DocServer"; import { Docs } from "../../../documents/Documents"; @@ -25,21 +25,20 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss" import { ContextMenu } from "../../ContextMenu"; import { ContextMenuProps } from "../../ContextMenuItem"; import { InkingCanvas } from "../../InkingCanvas"; -import { CollectionFreeFormDocumentView, positionSchema } from "../../nodes/CollectionFreeFormDocumentView"; -import { DocumentContentsView } from "../../nodes/DocumentContentsView"; -import { documentSchema, DocumentViewProps } from "../../nodes/DocumentView"; +import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; +import { DocumentViewProps } from "../../nodes/DocumentView"; import { FormattedTextBox } from "../../nodes/FormattedTextBox"; import { pageSchema } from "../../nodes/ImageBox"; -import PDFMenu from "../../pdf/PDFMenu"; import { CollectionSubView } from "../CollectionSubView"; import { computePivotLayout, ViewDefResult } from "./CollectionFreeFormLayoutEngines"; -import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView"; import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); import { InteractionUtils } from "../../../util/InteractionUtils"; import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; +import PDFMenu from "../../pdf/PDFMenu"; +import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); @@ -53,6 +52,11 @@ export const panZoomSchema = createSchema({ isRuleProvider: "boolean", fitToBox: "boolean", panTransformType: "string", + scrollHeight: "number", + fitX: "number", + fitY: "number", + fitW: "number", + fitH: "number" }); type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof documentSchema, typeof positionSchema, typeof pageSchema]>; @@ -69,9 +73,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @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 nativeWidth() { return this.Document.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 isAnnotationOverlay() { return this.props.isAnnotationOverlay; } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } private easing = () => this.props.Document.panTransformType === "Ease"; private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document.panX || 0; @@ -114,10 +118,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { 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); - } - @action onDrop = (e: React.DragEvent): Promise<void> => { var pt = this.getTransform().transformPoint(e.pageX, e.pageY); @@ -134,21 +134,23 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { if (super.drop(e, de)) { if (de.data instanceof DragManager.DocumentDragData) { if (de.data.droppedDocuments.length) { - let z = NumCast(de.data.droppedDocuments[0].z); + let firstDoc = de.data.droppedDocuments[0]; + let z = NumCast(firstDoc.z); let x = (z ? xpo : xp) - de.data.offset[0]; 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); + let dropX = NumCast(firstDoc.x); + let dropY = NumCast(firstDoc.y); de.data.droppedDocuments.forEach(action((d: Doc) => { + let layoutDoc = Doc.Layout(d); d.x = x + NumCast(d.x) - dropX; d.y = y + NumCast(d.y) - dropY; - if (!NumCast(d.width)) { - d.width = 300; + if (!NumCast(layoutDoc.width)) { + layoutDoc.width = 300; } - if (!NumCast(d.height)) { - let nw = NumCast(d.nativeWidth); - let nh = NumCast(d.nativeHeight); - d.height = nw && nh ? nh / nw * NumCast(d.width) : 300; + if (!NumCast(layoutDoc.height)) { + let nw = NumCast(layoutDoc.nativeWidth); + let nh = NumCast(layoutDoc.nativeHeight); + layoutDoc.height = nw && nh ? nh / nw * NumCast(layoutDoc.width) : 300; } this.bringToFront(d); })); @@ -161,12 +163,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let dragDoc = de.data.dropDocument; let x = xp - de.data.offset[0]; let y = yp - de.data.offset[1]; - let dropX = NumCast(de.data.dropDocument.x); - let dropY = NumCast(de.data.dropDocument.y); + let dropX = NumCast(dragDoc.x); + let dropY = NumCast(dragDoc.y); dragDoc.x = x + NumCast(dragDoc.x) - dropX; dragDoc.y = y + NumCast(dragDoc.y) - dropY; - de.data.targetContext = this.props.Document; - dragDoc.targetContext = this.props.Document; + de.data.targetContext = this.props.Document; // dropped a PDF annotation, so we need to set the targetContext on the dragData which the PDF view uses at the end of the drop operation this.bringToFront(dragDoc); } } @@ -176,11 +177,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { pickCluster(probe: number[]) { return this.childLayoutPairs.map(pair => pair.layout).reduce((cluster, cd) => { + let layoutDoc = Doc.Layout(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; - return !cd.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ? + let cw = NumCast(layoutDoc.width) + 2 * this._clusterDistance; + let ch = NumCast(layoutDoc.height) + 2 * this._clusterDistance; + return !layoutDoc.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); } @@ -188,14 +190,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { 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); - - // hacky way to get a list of DocumentViews in the current view given a list of Documents in the current view - let prevSelected = SelectionManager.SelectedDocuments(); - this.selectDocuments(eles, []); - let clusterDocs = SelectionManager.SelectedDocuments(); - SelectionManager.DeselectAll(); - prevSelected.map(dv => SelectionManager.SelectDoc(dv, true)); - + let clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.CollectionView)!); let de = new DragManager.DocumentDragData(eles); de.moveDocument = this.props.moveDocument; const [left, top] = clusterDocs[0].props.ScreenToLocalTransform().scale(clusterDocs[0].props.ContentScaling()).inverse().transformPoint(0, 0); @@ -272,7 +267,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { onPointerDown = (e: React.PointerEvent): void => { if (e.nativeEvent.cancelBubble) return; 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()) { + if (e.button === 0 && !e.shiftKey && !e.altKey && !e.ctrlKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); @@ -306,14 +301,15 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let miny = docs.length ? NumCast(docs[0].y) : 0; let maxy = docs.length ? NumCast(docs[0].height) + miny : miny; let ranges = docs.filter(doc => doc).reduce((range, doc) => { + let layoutDoc = Doc.Layout(doc); let x = NumCast(doc.x); - let xe = x + NumCast(doc.width); + let xe = x + NumCast(layoutDoc.width); let y = NumCast(doc.y); - let ye = y + NumCast(doc.height); + let ye = y + NumCast(layoutDoc.height); return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]], [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]]; }, [[minx, maxx], [miny, maxy]]); - let ink = Cast(this.fieldExtensionDoc.ink, InkField); + let ink = this.extensionDoc && Cast(this.extensionDoc.ink, InkField); if (ink && ink.inkData) { ink.inkData.forEach((value: StrokeData, key: string) => { let bounds = InkingCanvas.StrokeRect(value); @@ -461,14 +457,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } @action - setPan(panX: number, panY: number) { - if (!this.props.Document.lockedPosition || this.props.Document.inOverlay) { - this.props.Document.panTransformType = "None"; + setPan(panX: number, panY: number, panType: string = "None") { + if (!this.Document.lockedPosition || this.Document.inOverlay) { + this.Document.panTransformType = panType; 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; + const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY)); + this.Document.panX = this.isAnnotationOverlay ? newPanX : panX; + this.Document.panY = this.isAnnotationOverlay ? newPanY : panY; } } @@ -513,19 +509,19 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { 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; + let layoutdoc = Doc.Layout(doc); + const newPanX = NumCast(doc.x) + NumCast(layoutdoc.width) / 2; + const newPanY = NumCast(doc.y) + NumCast(layoutdoc.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.setPan(newPanX, newPanY, "Ease"); Doc.BrushDoc(this.props.Document); this.props.focus(this.props.Document); - willZoom && this.setScaleToZoom(doc, scale); + willZoom && this.setScaleToZoom(layoutdoc, scale); afterFocus && setTimeout(() => { if (afterFocus && afterFocus()) { @@ -554,6 +550,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { ...this.props, DataDoc: childData, Document: childLayout, + layoutKey: undefined, ruleProvider: this.Document.isRuleProvider && childLayout.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider, //bcz: hack! - currently ruleProviders apply to documents in nested colleciton, not direct children of themselves onClick: undefined, // this.props.onClick, // bcz: check this out -- I don't think we want to inherit click handlers, or we at least need a way to ignore them ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform, @@ -571,30 +568,15 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { getScale: this.getScale }; } - getDocumentViewProps(layoutDoc: Doc): DocumentViewProps { - return { - ...this.props, - ScreenToLocalTransform: this.getTransform, - PanelWidth: layoutDoc[WidthSym], - PanelHeight: layoutDoc[HeightSym], - ContentScaling: returnOne, - ContainingCollectionView: this.props.ContainingCollectionView, - focus: this.focusDocument, - backgroundColor: returnEmptyString, - parentActive: this.props.active, - bringToFront: this.bringToFront, - zoomToScale: this.zoomToScale, - getScale: this.getScale - }; - } getCalculatedPositions(params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): { x?: number, y?: number, z?: number, width?: number, height?: number, transition?: string, state?: any } { const script = this.Document.arrangeScript; const result = script && script.script.run(params, console.log); + const layoutDoc = Doc.Layout(params.doc); if (result && result.success) { return { ...result, transition: "transform 1s" }; } - return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(params.doc.width, "number"), height: Cast(params.doc.height, "number") }; + return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(layoutDoc.width, "number"), height: Cast(layoutDoc.height, "number") }; } viewDefsToJSX = (views: any[]) => { @@ -711,7 +693,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { // find rule colorations when rule providing is turned on by looking at each document to see if it has a coloring -- if so, use it's color as the rule for its associated heading. this.Document.isRuleProvider && this.childLayoutPairs.map(pair => // iterate over the children of a displayed document (or if the displayed document is a template, iterate over the children of that template) - DocListCast(pair.layout.layout instanceof Doc ? pair.layout.layout.data : pair.layout.data).map(heading => { + DocListCast(Doc.Layout(pair.layout).data).map(heading => { let headingPair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, heading); let headingLayout = headingPair.layout && (pair.layout.data_ext instanceof Doc) && (pair.layout.data_ext[`Layout[${headingPair.layout[Id]}]`] as Doc) || headingPair.layout; if (headingLayout && NumCast(headingLayout.heading) > 0 && headingLayout.backgroundColor !== headingLayout.defaultBackgroundColor) { @@ -722,16 +704,17 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } analyzeStrokes = async () => { - let data = Cast(this.fieldExtensionDoc.ink, InkField); - if (data) { - CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.fieldExtensionDoc, ["inkAnalysis", "handwriting"], data.inkData); + const extensionDoc = this.extensionDoc; + let data = extensionDoc && Cast(extensionDoc.ink, InkField); + if (data && extensionDoc) { + CognitiveServices.Inking.Appliers.ConcatenateHandwriting(extensionDoc, ["inkAnalysis", "handwriting"], data.inkData); } } onContextMenu = (e: React.MouseEvent) => { let layoutItems: ContextMenuProps[] = []; - if (this.childDocs.some(d => BoolCast(d.isTemplate))) { + if (this.childDocs.some(d => BoolCast(d.isTemplateDoc))) { layoutItems.push({ description: "Template Layout Instance", event: () => this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" }); } layoutItems.push({ description: "reset view", event: () => { this.props.Document.panX = this.props.Document.panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" }); @@ -785,58 +768,35 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private childViews = () => { let children = typeof this.props.children === "function" ? (this.props.children as any)() as JSX.Element[] : []; return [ - <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />, ...children, ...this.views, ]; } render() { // update the actual dimensions of the collection so that they can inquired (e.g., by a minimap) - 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. + this.Document.fitX = this.contentBounds && this.contentBounds.x; + this.Document.fitY = this.contentBounds && this.contentBounds.y; + this.Document.fitW = this.contentBounds && (this.contentBounds.r - this.contentBounds.x); + this.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y); + // if isAnnotationOverlay 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); - return ( + return !this.extensionDoc ? (null) : <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() }} + style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }} onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onTouchStart={this.onTouchStart}> - <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} isSelected={this.props.isSelected} - addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox} setPreviewCursor={this.props.setPreviewCursor} - getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> + <MarqueeView {...this.props} extensionDoc={this.extensionDoc} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument} + addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} 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={"ink"} > + {!this.extensionDoc ? (null) : + <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.extensionDoc} inkFieldKey={"ink"} > {this.childViews} - </InkingCanvas> - </CollectionFreeFormLinksView> + </InkingCanvas>} <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" /> </CollectionFreeFormViewPannableContents> </MarqueeView> {this.overlayViews} - <CollectionFreeFormOverlayView {...this.props} {...this.getDocumentViewProps(this.props.Document)} /> - </div> - ); - } -} - -@observer -class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> { - render() { - 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 }> { - render() { - return !this.props.Document.backgroundLayout ? (null) : - (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"} - renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />); + </div>; } } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index cd9ac7ecc..414238ccd 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -15,29 +15,30 @@ import { Transform } from "../../../util/Transform"; import { undoBatch } from "../../../util/UndoManager"; import { InkingCanvas } from "../../InkingCanvas"; import { PreviewCursor } from "../../PreviewCursor"; -import { CollectionViewType } from "../CollectionBaseView"; +import { CollectionViewType } from "../CollectionView"; import { CollectionFreeFormView } from "./CollectionFreeFormView"; import "./MarqueeView.scss"; import React = require("react"); import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; import InkSelectDecorations from "../../InkSelectDecorations"; +import { SubCollectionViewProps } from "../CollectionSubView"; interface MarqueeViewProps { getContainerTransform: () => Transform; getTransform: () => Transform; - container: CollectionFreeFormView; addDocument: (doc: Doc) => boolean; activeDocuments: () => Doc[]; selectDocuments: (docs: Doc[], ink: Map<any, any>[]) => void; removeDocument: (doc: Doc) => boolean; addLiveTextDocument: (doc: Doc) => void; isSelected: () => boolean; - isAnnotationOverlay: boolean; + extensionDoc: Doc; + isAnnotationOverlay?: boolean; setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; } @observer -export class MarqueeView extends React.Component<MarqueeViewProps> +export class MarqueeView extends React.Component<SubCollectionViewProps & MarqueeViewProps> { private _mainCont = React.createRef<HTMLDivElement>(); @observable _lastX: number = 0; @@ -191,13 +192,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], []); + if (!this.props.active()) this.props.selectDocuments([this.props.Document], []); if (this._visible) { let mselect = this.marqueeSelect(); if (!e.shiftKey) { - SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document); + SelectionManager.DeselectAll(mselect.length ? undefined : this.props.Document); } - this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document], + this.props.selectDocuments(mselect.length ? mselect : [this.props.Document], this.ink ? [this.marqueeInkSelect(this.ink.inkData)] : []); } if (!this._commandExecuted && (Math.abs(this.Bounds.height * this.Bounds.width) > 100)) { @@ -266,13 +267,11 @@ export class MarqueeView extends React.Component<MarqueeViewProps> } get ink() { // ink will be stored on the extension doc for the field (fieldKey) where the container's data is stored. - let cprops = this.props.container.props; - return Cast(Doc.fieldExtensionDoc(cprops.Document, cprops.fieldKey).ink, InkField); + return this.props.extensionDoc && Cast(this.props.extensionDoc.ink, InkField); } set ink(value: InkField | undefined) { - let cprops = this.props.container.props; - Doc.fieldExtensionDoc(cprops.Document, cprops.fieldKey).ink = value; + this.props.extensionDoc && (this.props.extensionDoc.ink = value); } @action @@ -301,12 +300,12 @@ export class MarqueeView extends React.Component<MarqueeViewProps> let bounds = this.Bounds; let defaultPalette = ["rgb(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)", "rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",]; - let colorPalette = Cast(this.props.container.props.Document.colorPalette, listSpec("string")); - if (!colorPalette) this.props.container.props.Document.colorPalette = new List<string>(defaultPalette); - let palette = Array.from(Cast(this.props.container.props.Document.colorPalette, listSpec("string")) as string[]); + let colorPalette = Cast(this.props.Document.colorPalette, listSpec("string")); + if (!colorPalette) this.props.Document.colorPalette = new List<string>(defaultPalette); + let palette = Array.from(Cast(this.props.Document.colorPalette, listSpec("string")) as string[]); let usedPaletted = new Map<string, number>(); - [...this.props.activeDocuments(), this.props.container.props.Document].map(child => { - let bg = StrCast(child.layout instanceof Doc ? child.layout.backgroundColor : child.backgroundColor); + [...this.props.activeDocuments(), this.props.Document].map(child => { + let bg = StrCast(Doc.Layout(child).backgroundColor); if (palette.indexOf(bg) !== -1) { palette.splice(palette.indexOf(bg), 1); if (usedPaletted.get(bg)) usedPaletted.set(bg, usedPaletted.get(bg)! + 1); @@ -461,20 +460,22 @@ export class MarqueeView extends React.Component<MarqueeViewProps> let selRect = this.Bounds; let selection: Doc[] = []; this.props.activeDocuments().filter(doc => !doc.isBackground && doc.z === undefined).map(doc => { + let layoutDoc = Doc.Layout(doc); var x = NumCast(doc.x); var y = NumCast(doc.y); - var w = NumCast(doc.width); - var h = NumCast(doc.height); + var w = NumCast(layoutDoc.width); + var h = NumCast(layoutDoc.height); if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) { selection.push(doc); } }); if (!selection.length && selectBackgrounds) { this.props.activeDocuments().filter(doc => doc.z === undefined).map(doc => { + let layoutDoc = Doc.Layout(doc); var x = NumCast(doc.x); var y = NumCast(doc.y); - var w = NumCast(doc.width); - var h = NumCast(doc.height); + var w = NumCast(layoutDoc.width); + var h = NumCast(layoutDoc.height); if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) { selection.push(doc); } @@ -487,10 +488,11 @@ export class MarqueeView extends React.Component<MarqueeViewProps> let size = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); let otherBounds = { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) }; this.props.activeDocuments().filter(doc => doc.z !== undefined).map(doc => { + let layoutDoc = Doc.Layout(doc); var x = NumCast(doc.x); var y = NumCast(doc.y); - var w = NumCast(doc.width); - var h = NumCast(doc.height); + var w = NumCast(layoutDoc.width); + var h = NumCast(layoutDoc.height); if (this.intersectRect({ left: x, top: y, width: w, height: h }, otherBounds)) { selection.push(doc); } @@ -501,26 +503,22 @@ export class MarqueeView extends React.Component<MarqueeViewProps> @computed get marqueeDiv() { + let p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0]; let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); /** * @RE - The commented out span below * This contains the "C for collection, ..." text on marquees. * Commented out by syip2 when the marquee menu was added. */ - return <div className="marquee" style={{ width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}`, zIndex: 2000 }} > + return <div className="marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}`, zIndex: 2000 }} > {/* <span className="marquee-legend" /> */} </div>; } render() { - let p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0]; - return <div className="marqueeView" onScroll={(e) => e.currentTarget.scrollLeft = 0} style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}> - <div style={{ position: "relative", transform: `translate(${p[0]}px, ${p[1]}px)` }} onScroll={(e) => e.currentTarget.scrollLeft = 0} > - {this._visible ? this.marqueeDiv : null} - <div ref={this._mainCont} style={{ transform: `translate(${-p[0]}px, ${-p[1]}px)` }} > - {this.props.children} - </div> - </div> + return <div className="marqueeView" onScroll={(e) => e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0} style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}> + {this._visible ? this.marqueeDiv : null} + {this.props.children} </div>; } }
\ No newline at end of file |
