diff options
| author | ab <abdullah_ahmed@brown.edu> | 2019-07-14 23:39:56 -0400 |
|---|---|---|
| committer | ab <abdullah_ahmed@brown.edu> | 2019-07-14 23:39:56 -0400 |
| commit | b7dd805549c5cdb6b583312e5e9637f9f3e0ee93 (patch) | |
| tree | 24217f91f213e57c503d226bae1475acb84c6a1d /src/client/views/collections | |
| parent | baf446a5d65f8e0203eb3c7fb2f43d62a6997daa (diff) | |
| parent | d7c6f0da00d4ed56d28f679d6f7de1002684864a (diff) | |
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web
Diffstat (limited to 'src/client/views/collections')
17 files changed, 728 insertions, 489 deletions
diff --git a/src/client/views/collections/CollectionBaseView.scss b/src/client/views/collections/CollectionBaseView.scss index 1f5acb96a..34bcb705e 100644 --- a/src/client/views/collections/CollectionBaseView.scss +++ b/src/client/views/collections/CollectionBaseView.scss @@ -1,11 +1,12 @@ @import "../globalCssVariables"; #collectionBaseView { border-width: 0; - box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw; border-color: $light-color-secondary; border-style: solid; border-radius: 0 0 $border-radius $border-radius; box-sizing: border-box; border-radius: inherit; pointer-events: all; + width:100%; + height:100%; }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 879898018..eba69b448 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -5,7 +5,7 @@ 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 { BoolCast, Cast, NumCast, PromiseValue } from '../../../new_fields/Types'; +import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from '../../../new_fields/Types'; import { DocumentManager } from '../../util/DocumentManager'; import { SelectionManager } from '../../util/SelectionManager'; import { ContextMenu } from '../ContextMenu'; @@ -64,7 +64,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { active = (): boolean => { var isSelected = this.props.isSelected(); - return isSelected || this._isChildActive || this.props.renderDepth === 0; + return isSelected || this._isChildActive || this.props.renderDepth === 0 || BoolCast(this.props.Document.excludeFromLibrary); } //TODO should this be observable? @@ -74,21 +74,26 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { this.props.whenActiveChanged(isActive); } + @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); } + @action.bound addDocument(doc: Doc, allowDuplicates: boolean = false): boolean { + let self = this; var curPage = NumCast(this.props.Document.curPage, -1); Doc.GetProto(doc).page = curPage; if (curPage >= 0) { Doc.GetProto(doc).annotationOn = this.props.Document; } allowDuplicates = true; - const value = Cast(this.dataDoc[this.dataField], listSpec(Doc)); + 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; + const value = Cast(targetDataDoc[targetField], listSpec(Doc)); if (value !== undefined) { if (allowDuplicates || !value.some(v => v instanceof Doc && v[Id] === doc[Id])) { value.push(doc); } } else { - Doc.GetProto(this.dataDoc)[this.dataField] = new List([doc]); + Doc.GetProto(targetDataDoc)[targetField] = new List([doc]); } return true; } @@ -98,7 +103,9 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { 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 - const value = Cast(this.dataDoc[this.dataField], listSpec(Doc), []); + 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[Id] === doc[Id]) ? i : p, -1); PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn => annotationOn === this.dataDoc.Document && (doc.annotationOn = undefined) @@ -116,7 +123,10 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { @action.bound moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean { - if (Doc.AreProtosEqual(this.dataDoc, targetCollection)) { + let self = this; + let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document; + if (Doc.AreProtosEqual(targetDataDoc, targetCollection)) { + //if (Doc.AreProtosEqual(this.extensionDoc, targetCollection)) { return true; } if (this.removeDocument(doc)) { @@ -135,7 +145,9 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { }; const viewtype = this.collectionViewType; return ( - <div id="collectionBaseView" className={this.props.className || "collectionView-cont"} + <div id="collectionBaseView" + style={{ boxShadow: `#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}> {viewtype !== undefined ? this.props.children(viewtype, props) : (null)} </div> diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index e675e82fc..781bafec0 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,6 +1,6 @@ import 'golden-layout/src/css/goldenlayout-base.css'; import 'golden-layout/src/css/goldenlayout-dark-theme.css'; -import { action, Lambda, observable, reaction } from "mobx"; +import { action, Lambda, observable, reaction, trace, computed } from "mobx"; import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; import Measure from "react-measure"; @@ -24,10 +24,11 @@ import { SubCollectionViewProps } from "./CollectionSubView"; import { ParentDocSelector } from './ParentDocumentSelector'; import React = require("react"); import { MainView } from '../MainView'; -import { LinkManager } from '../../util/LinkManager'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { library } from '@fortawesome/fontawesome-svg-core'; -import { faFile } from '@fortawesome/free-solid-svg-icons'; +import { faFile, faUnlockAlt } from '@fortawesome/free-solid-svg-icons'; +import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; +import { Docs } from '../../documents/Documents'; library.add(faFile); @observer @@ -52,6 +53,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp private _flush: boolean = false; private _ignoreStateChange = ""; private _isPointerDown = false; + private _maximizedSrc: Opt<DocumentView>; constructor(props: SubCollectionViewProps) { super(props); @@ -62,16 +64,36 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } hack: boolean = false; undohack: any = null; - public StartOtherDrag(e: any, dragDocs: Doc[], dragDataDocs?: (Doc | undefined)[]) { - this.hack = true; - this.undohack = UndoManager.StartBatch("goldenDrag"); - dragDocs.map((dragDoc, i) => - this.AddRightSplit(dragDoc, dragDataDocs ? dragDataDocs[i] : dragDoc, true).contentItems[0].tab._dragListener. - onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 })); + public StartOtherDrag(e: any, dragDocs: Doc[], dragDataDocs: (Doc | undefined)[] = []) { + let config: any; + if (dragDocs.length === 1) { + config = CollectionDockingView.makeDocumentConfig(dragDocs[0], dragDataDocs[0]); + } else { + config = { + type: 'row', + content: dragDocs.map((doc, i) => { + CollectionDockingView.makeDocumentConfig(doc, dragDataDocs[i]); + }) + }; + } + const div = document.createElement("div"); + const dragSource = this._goldenLayout.createDragSource(div, config); + dragSource._dragListener.on("dragStop", () => { + dragSource.destroy(); + }); + dragSource._dragListener.onMouseDown(e); + // dragSource.destroy(); + // this.hack = true; + // this.undohack = UndoManager.StartBatch("goldenDrag"); + // dragDocs.map((dragDoc, i) => + // this.AddRightSplit(dragDoc, dragDataDocs[i], true).contentItems[0].tab._dragListener. + // onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 })); } @action - public OpenFullScreen(document: Doc, dataDoc: Doc) { + public OpenFullScreen(docView: DocumentView) { + let document = Doc.MakeAlias(docView.props.Document); + let dataDoc = docView.dataDoc; let newItemStackConfig = { type: 'stack', content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)] @@ -80,10 +102,25 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp this._goldenLayout.root.contentItems[0].addChild(docconfig); docconfig.callDownwards('_$init'); this._goldenLayout._$maximiseItem(docconfig); + this._maximizedSrc = docView; this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); this.stateChanged(); } + public CloseFullScreen = () => { + let target = this._goldenLayout._maximisedItem; + if (target !== null && this._maximizedSrc) { + this._goldenLayout._maximisedItem.remove(); + SelectionManager.SelectDoc(this._maximizedSrc, false); + this._maximizedSrc = undefined; + this.stateChanged(); + } + } + + public HasFullScreen = () => { + return this._goldenLayout._maximisedItem !== null; + } + @undoBatch @action public CloseRightSplit = (document: Doc): boolean => { @@ -167,7 +204,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp return newContentItem; } @action - public AddTab = (stack: any, document: Doc, dataDocument: Doc) => { + public AddTab = (stack: any, document: Doc, dataDocument: Doc | undefined) => { let docs = Cast(this.props.Document.data, listSpec(Doc)); if (docs) { docs.push(document); @@ -223,6 +260,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp // Because this is in a set timeout, if this component unmounts right after mounting, // we will leak a GoldenLayout, because we try to destroy it before we ever create it setTimeout(() => this.setupGoldenLayout(), 1); + this.props.Document.workspaceBrush = true; } this._ignoreStateChange = ""; }, { fireImmediately: true }); @@ -232,6 +270,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } componentWillUnmount: () => void = () => { try { + this.props.Document.workspaceBrush = false; this._goldenLayout.unbind('itemDropped', this.itemDropped); this._goldenLayout.unbind('tabCreated', this.tabCreated); this._goldenLayout.unbind('stackCreated', this.stackCreated); @@ -252,15 +291,17 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp var cur = this._containerRef.current; // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed - this._goldenLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height); + this._goldenLayout && this._goldenLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height); } @action onPointerUp = (e: React.PointerEvent): void => { - this._isPointerDown = false; if (this._flush) { this._flush = false; - setTimeout(() => this.stateChanged(), 10); + setTimeout(() => { + CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig()); + this.stateChanged() + }, 10); } } @action @@ -281,39 +322,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp let tab = (e.target as any).parentElement as HTMLElement; DocServer.GetRefField(docid).then(action(async (sourceDoc: Opt<Field>) => (sourceDoc instanceof Doc) && DragLinksAsDocuments(tab, x, y, sourceDoc))); - } else - if ((className === "lm_title" || className === "lm_tab lm_active") && e.shiftKey) { - e.stopPropagation(); - e.preventDefault(); - let x = e.clientX; - let y = e.clientY; - let docid = (e.target as any).DashDocId; - let datadocid = (e.target as any).DashDataDocId; - let tab = (e.target as any).parentElement as HTMLElement; - let glTab = (e.target as any).Tab; - if (glTab && glTab.contentItem && glTab.contentItem.parent) { - glTab.contentItem.parent.setActiveContentItem(glTab.contentItem); - } - DocServer.GetRefField(docid).then(action(async (f: Opt<Field>) => { - if (f instanceof Doc) { - let dataDoc = (datadocid !== docid) ? await DocServer.GetRefField(datadocid) : f; - DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f], [dataDoc instanceof Doc ? dataDoc : f]), x, y, - { - handlers: { - dragComplete: emptyFunction, - }, - hideSource: false, - withoutShiftDrag: true - }); - } - })); - } + } if (className === "lm_drag_handle" || className === "lm_close" || className === "lm_maximise" || className === "lm_minimise" || className === "lm_close_tab") { this._flush = true; } - if (this.props.active()) { - e.stopPropagation(); - } } @undoBatch @@ -333,6 +345,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } itemDropped = () => { + CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig()); this.stateChanged(); } @@ -348,9 +361,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp if (tab.contentItem.config.fixed) { tab.contentItem.parent.config.fixed = true; } + let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId) as Doc; let dataDoc = await DocServer.GetRefField(tab.contentItem.config.props.dataDocumentId) as Doc; - if (doc instanceof Doc && dataDoc instanceof Doc) { + if (doc instanceof Doc) { let dragSpan = document.createElement("span"); dragSpan.style.position = "relative"; dragSpan.style.bottom = "6px"; @@ -367,7 +381,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } tab.setActive(true); }; - ReactDOM.render(<span onPointerDown={ + ReactDOM.render(<span title="Drag as document" onPointerDown={ e => { e.preventDefault(); e.stopPropagation(); @@ -398,9 +412,16 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp if (doc instanceof Doc) { let theDoc = doc; CollectionDockingView.Instance._removedDocs.push(theDoc); + + const recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc); + if (recent) { + Doc.AddDocToList(recent, "data", doc, undefined, true, true); + } SelectionManager.DeselectAll(); } + CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig()); tab.contentItem.remove(); + CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig()); }); } @@ -415,14 +436,24 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp stackCreated = (stack: any) => { //stack.header.controlsContainer.find('.lm_popout').hide(); + stack.header.element[0].style.backgroundColor = DocServer.Control.isReadOnly() ? "#228540" : undefined; + stack.header.element.on('mousedown', (e: any) => { + if (e.target === stack.header.element[0] && e.button === 1) { + this.AddTab(stack, Docs.Create.FreeformDocument([], { width: this.props.PanelWidth(), height: this.props.PanelHeight(), title: "Untitled Collection" }), undefined); + } + }); stack.header.controlsContainer.find('.lm_close') //get the close icon .off('click') //unbind the current click handler - .click(action(function () { + .click(action(async function () { //if (confirm('really close this?')) { + const recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc); stack.remove(); - stack.contentItems.map(async (contentItem: any) => { + stack.contentItems.forEach(async (contentItem: any) => { let doc = await DocServer.GetRefField(contentItem.config.props.documentId); if (doc instanceof Doc) { + if (recent) { + Doc.AddDocToList(recent, "data", doc, undefined, true, true); + } let theDoc = doc; CollectionDockingView.Instance._removedDocs.push(theDoc); } @@ -439,9 +470,18 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } render() { + if (this.props.renderDepth > 0) { + return <div style={{ width: "100%", height: "100%" }}>Nested workspaces can't be rendered</div>; + } return ( - <div className="collectiondockingview-container" id="menuContainer" - onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} /> + <Measure offset onResize={this.onResize}> + {({ measureRef }) => + <div ref={measureRef}> + <div className="collectiondockingview-container" id="menuContainer" + onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} /> + </div> + } + </Measure> ); } @@ -450,6 +490,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp interface DockedFrameProps { documentId: FieldId; dataDocumentId: FieldId; + glContainer: any; //collectionDockingView: CollectionDockingView } @observer @@ -459,6 +500,9 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { @observable private _panelHeight = 0; @observable private _document: Opt<Doc>; @observable private _dataDoc: Opt<Doc>; + + @observable private _isActive: boolean = false; + get _stack(): any { let parent = (this.props as any).glContainer.parent.parent; if (this._document && this._document.excludeFromLibrary && parent.parent && parent.parent.contentItems.length > 1) { @@ -469,13 +513,32 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { constructor(props: any) { super(props); DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => { - this._dataDoc = this._document = f as Doc; + this._document = f as Doc; if (this.props.dataDocumentId && this.props.documentId !== this.props.dataDocumentId) { DocServer.GetRefField(this.props.dataDocumentId).then(action((f: Opt<Field>) => this._dataDoc = f as Doc)); } })); } + componentDidMount() { + this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged); + this.props.glContainer.on("tab", this.onActiveContentItemChanged); + this.onActiveContentItemChanged(); + } + + componentWillUnmount() { + this.props.glContainer.layoutManager.off("activeContentItemChanged", this.onActiveContentItemChanged); + this.props.glContainer.off("tab", this.onActiveContentItemChanged); + } + + @action.bound + private onActiveContentItemChanged() { + if (this.props.glContainer.tab) { + this._isActive = this.props.glContainer.tab.isActive; + } + } + + nativeWidth = () => NumCast(this._document!.nativeWidth, this._panelWidth); nativeHeight = () => { let nh = NumCast(this._document!.nativeHeight, this._panelHeight); @@ -510,7 +573,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; } - addDocTab = (doc: Doc, dataDoc: Doc, location: string) => { + addDocTab = (doc: Doc, dataDoc: Doc | undefined, location: string) => { if (doc.dockingConfig) { MainView.Instance.openWorkspace(doc); } else if (location === "onRight") { @@ -519,40 +582,51 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc); } } - get content() { - if (!this._document || !this._dataDoc) { + @computed get docView() { + if (!this._document) { + return (null); + } + let resolvedDataDoc = this._document.layout instanceof Doc ? this._document : this._dataDoc; + return <DocumentView key={this._document[Id]} + Document={this._document} + DataDoc={resolvedDataDoc} + bringToFront={emptyFunction} + addDocument={undefined} + removeDocument={undefined} + ContentScaling={this.contentScaling} + PanelWidth={this.nativeWidth} + PanelHeight={this.nativeHeight} + ScreenToLocalTransform={this.ScreenToLocalTransform} + renderDepth={0} + selectOnLoad={false} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + focus={emptyFunction} + addDocTab={this.addDocTab} + ContainingCollectionView={undefined} + zoomToScale={emptyFunction} + getScale={returnOne} /> + } + + @computed get content() { + if (!this._document) { return (null); } return ( <div className="collectionDockingView-content" ref={this._mainCont} style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px) scale(${this.scaleToFitMultiplier})` }}> - <DocumentView key={this._document[Id]} - Document={this._document} - DataDoc={this._dataDoc} - bringToFront={emptyFunction} - addDocument={undefined} - removeDocument={undefined} - ContentScaling={this.contentScaling} - PanelWidth={this.nativeWidth} - PanelHeight={this.nativeHeight} - ScreenToLocalTransform={this.ScreenToLocalTransform} - renderDepth={0} - selectOnLoad={false} - parentActive={returnTrue} - whenActiveChanged={emptyFunction} - focus={emptyFunction} - addDocTab={this.addDocTab} - ContainingCollectionView={undefined} - zoomToScale={emptyFunction} - getScale={returnOne} /> + {this.docView} </div >); } render() { + if (!this._isActive) return null; let theContent = this.content; return !this._document ? (null) : <Measure offset onResize={action((r: any) => { this._panelWidth = r.offset.width; this._panelHeight = r.offset.height; })}> - {({ measureRef }) => <div ref={measureRef}> {theContent} </div>} + {({ measureRef }) => <div ref={measureRef}> + {theContent} + </div>} </Measure>; } -} +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx index 31a73ab36..8ab360984 100644 --- a/src/client/views/collections/CollectionPDFView.tsx +++ b/src/client/views/collections/CollectionPDFView.tsx @@ -30,13 +30,13 @@ export class CollectionPDFView extends React.Component<FieldViewProps> { () => NumCast(this.props.Document.scrollY), () => { // let transform = this.props.ScreenToLocalTransform(); - if (this._buttonTray.current) { - // console.log(this._buttonTray.current.offsetHeight); - // console.log(NumCast(this.props.Document.scrollY)); - let scale = this.nativeWidth() / this.props.Document[WidthSym](); - this.props.Document.panY = NumCast(this.props.Document.scrollY); - // console.log(scale); - } + // if (this._buttonTray.current) { + // console.log(this._buttonTray.current.offsetHeight); + // console.log(NumCast(this.props.Document.scrollY)); + let scale = this.nativeWidth() / this.props.Document[WidthSym](); + this.props.Document.panY = NumCast(this.props.Document.scrollY); + // console.log(scale); + // } // console.log(this.props.Document[HeightSym]()); }, { fireImmediately: true } @@ -47,8 +47,8 @@ export class CollectionPDFView extends React.Component<FieldViewProps> { this._reactionDisposer && this._reactionDisposer(); } - public static LayoutString(fieldKey: string = "data") { - return FieldView.LayoutString(CollectionPDFView, fieldKey); + public static LayoutString(fieldKey: string = "data", fieldExt: string = "annotations") { + return FieldView.LayoutString(CollectionPDFView, fieldKey, fieldExt); } @observable _inThumb = false; diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 5562676e9..f72b1aa07 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -119,8 +119,8 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { let fieldContentView = <FieldView {...props} />; let reference = React.createRef<HTMLDivElement>(); let onItemDown = (e: React.PointerEvent) => { - (this.props.CollectionView.props.isSelected() ? - SetupDrag(reference, () => props.Document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e) : undefined); + (!this.props.CollectionView.props.isSelected() ? undefined : + SetupDrag(reference, () => props.Document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e)); }; let applyToDoc = (doc: Doc, run: (args?: { [name: string]: any }) => any) => { const res = run({ this: doc }); @@ -235,7 +235,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { onPointerDown = (e: React.PointerEvent): void => { if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) { if (this.props.isSelected()) e.stopPropagation(); - else e.preventDefault(); } } @@ -264,7 +263,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { let dbName = StrCast(this.props.Document.title); let res = await Gateway.Instance.PostSchema(csv, dbName); if (self.props.CollectionView.props.addDocument) { - let schemaDoc = await Docs.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document }); + let schemaDoc = await Docs.Create.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document }); if (schemaDoc) { //self.props.CollectionView.props.addDocument(schemaDoc, false); self.props.Document.schemaDoc = schemaDoc; @@ -334,7 +333,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @computed get reactTable() { - trace(); let previewWidth = this.previewWidth() + 2 * this.borderWidth + this.DIVIDER_WIDTH + 1; return <ReactTable style={{ position: "relative", float: "left", width: `calc(100% - ${previewWidth}px` }} data={this.childDocs} page={0} pageSize={this.childDocs.length} showPagination={false} columns={this.tableColumns} @@ -388,7 +386,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } render() { - trace(); return ( <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} onContextMenu={this.onContextMenu} ref={this.createTarget}> @@ -405,8 +402,10 @@ interface CollectionSchemaPreviewProps { DataDocument?: Doc; childDocs?: Doc[]; renderDepth: number; + fitToBox?: boolean; width: () => number; height: () => number; + showOverlays?: (doc: Doc) => { title?: string, caption?: string }; CollectionView?: CollectionView | CollectionPDFView | CollectionVideoView; getTransform: () => Transform; addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; @@ -414,7 +413,7 @@ interface CollectionSchemaPreviewProps { removeDocument: (document: Doc) => boolean; active: () => boolean; whenActiveChanged: (isActive: boolean) => void; - addDocTab: (document: Doc, dataDoc: Doc, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; setPreviewScript: (script: string) => void; previewScript?: string; } @@ -462,18 +461,35 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.props.setPreviewScript(e.currentTarget.value); } + @computed get borderRounding() { + 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 minDim = percent * (nativeDim ? nativeDim : Math.min(this.PanelWidth(), this.PanelHeight())); + return minDim; + } + return undefined; + } render() { let input = this.props.previewScript === undefined ? (null) : <div ref={this.createTarget}><input className="collectionSchemaView-input" value={this.props.previewScript} onChange={this.onPreviewScriptChange} style={{ left: `calc(50% - ${Math.min(75, (this.props.Document ? this.PanelWidth() / 2 : 75))}px)` }} /></div>; return (<div className="collectionSchemaView-previewRegion" style={{ width: this.props.width(), height: "100%" }}> {!this.props.Document || !this.props.width ? (null) : ( - <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.centeringOffset}px, 0px)`, height: "100%" }}> + <div className="collectionSchemaView-previewDoc" + style={{ + transform: `translate(${this.centeringOffset}px, 0px)`, + borderRadius: this.borderRounding, + height: "100%" + }}> <DocumentView DataDoc={this.props.Document.layout instanceof Doc ? this.props.Document : this.props.DataDocument} Document={this.props.Document} + fitToBox={this.props.fitToBox} renderDepth={this.props.renderDepth + 1} selectOnLoad={false} + showOverlays={this.props.showOverlays} addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} moveDocument={this.props.moveDocument} diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 034a09eaa..7e886304d 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -16,7 +16,7 @@ align-items: center; } - .collectionStackingView-masonrySingle, .collectionStackingView-masonryGrid{ + .collectionStackingView-masonrySingle, .collectionStackingView-masonryGrid { width:100%; height:100%; position: absolute; @@ -25,7 +25,17 @@ left: 0; width: 100%; position: absolute; - + } + .collectionStackingView-masonrySingle { + width:100%; + height:100%; + position: absolute; + display:flex; + flex-direction: column; + top: 0; + left: 0; + width: 100%; + position: absolute; } .collectionStackingView-description { @@ -46,6 +56,9 @@ margin-left: -5; } + .collectionStackingView-columnDoc{ + display: inline-block; + } .collectionStackingView-columnDoc, .collectionStackingView-masonryDoc { diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 088a9bae8..fe01103d6 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -1,6 +1,6 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, IReactionDisposer, reaction } from "mobx"; +import { action, computed, IReactionDisposer, reaction, untracked } from "mobx"; import { observer } from "mobx-react"; import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; @@ -10,7 +10,10 @@ import { ContextMenu } from "../ContextMenu"; import { CollectionSchemaPreview } from "./CollectionSchemaView"; import "./CollectionStackingView.scss"; import { CollectionSubView } from "./CollectionSubView"; -import { resolve } from "bluebird"; +import { undoBatch } from "../../util/UndoManager"; +import { DragManager } from "../../util/DragManager"; +import { DocumentType } from "../../documents/Documents"; +import { Transform } from "../../util/Transform"; @observer export class CollectionStackingView extends CollectionSubView(doc => doc) { @@ -18,29 +21,20 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { _draggerRef = React.createRef<HTMLDivElement>(); _heightDisposer?: IReactionDisposer; _gridSize = 1; + _docXfs: any[] = []; @computed get xMargin() { return NumCast(this.props.Document.xMargin, 2 * this.gridGap); } @computed get yMargin() { return NumCast(this.props.Document.yMargin, 2 * this.gridGap); } @computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); } @computed get singleColumn() { return BoolCast(this.props.Document.singleColumn, true); } @computed get columnWidth() { return this.singleColumn ? (this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin) : Math.min(this.props.PanelWidth() - 2 * this.xMargin, NumCast(this.props.Document.columnWidth, 250)); } + @computed get filteredChildren() { return this.childDocs.filter(d => !d.isMinimized); } - singleColDocHeight(d: Doc) { - let nw = NumCast(d.nativeWidth); - let nh = NumCast(d.nativeHeight); - let aspect = nw && nh ? nh / nw : 1; - let wid = Math.min(d[WidthSym](), this.columnWidth); - return (nw && nh) ? wid * aspect : d[HeightSym](); - } componentDidMount() { this._heightDisposer = reaction(() => [this.yMargin, this.gridGap, this.columnWidth, this.childDocs.map(d => [d.height, d.width, d.zoomBasis, d.nativeHeight, d.nativeWidth, d.isMinimized])], - () => { - if (this.singleColumn) { - let children = this.childDocs.filter(d => !d.isMinimized); - this.props.Document.height = children.reduce((height, d, i) => - height + this.singleColDocHeight(d) + (i === children.length - 1 ? this.yMargin : this.gridGap) - , this.yMargin); - } - }, { fireImmediately: true }); + () => this.singleColumn && + (this.props.Document.height = this.filteredChildren.reduce((height, d, i) => + height + this.getDocHeight(d) + (i === this.filteredChildren.length - 1 ? this.yMargin : this.gridGap), this.yMargin)) + , { fireImmediately: true }); } componentWillUnmount() { if (this._heightDisposer) this._heightDisposer(); @@ -48,85 +42,89 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @action moveDocument = (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean): boolean => { - this.props.removeDocument(doc); - addDocument(doc); - return true; - } - getDocTransform(doc: Doc, dref: HTMLDivElement) { - let { scale, translateX, translateY } = Utils.GetScreenTransform(dref); - let outerXf = Utils.GetScreenTransform(this._masonryGridRef!); - let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); - return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]).scale(NumCast(doc.width, 1) / this.columnWidth); + return this.props.removeDocument(doc) && addDocument(doc); } createRef = (ele: HTMLDivElement | null) => { this._masonryGridRef = ele; this.createDropTarget(ele!); } - @computed - get singleColumnChildren() { - let children = this.childDocs.filter(d => !d.isMinimized); - return children.map((d, i) => { - let layoutDoc = Doc.expandTemplateLayout(d, this.props.DataDoc); - let dref = React.createRef<HTMLDivElement>(); - let dxf = () => this.getDocTransform(layoutDoc, dref.current!).scale(this.columnWidth / d[WidthSym]()); - let width = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth; - let height = () => this.singleColDocHeight(layoutDoc); - return <div className="collectionStackingView-columnDoc" - key={d[Id]} - ref={dref} - style={{ width: width(), height: height() }} > - <CollectionSchemaPreview - Document={layoutDoc} - DataDocument={d != this.props.DataDoc ? this.props.DataDoc : undefined} - renderDepth={this.props.renderDepth} - width={width} - height={height} - getTransform={dxf} - CollectionView={this.props.CollectionView} - addDocument={this.props.addDocument} - moveDocument={this.props.moveDocument} - removeDocument={this.props.removeDocument} - active={this.props.active} - whenActiveChanged={this.props.whenActiveChanged} - addDocTab={this.props.addDocTab} - setPreviewScript={emptyFunction} - previewScript={undefined}> - </CollectionSchemaPreview> - </div>; - }); + overlays = (doc: Doc) => { + return doc.type === DocumentType.IMG || doc.type === DocumentType.VID ? { title: "title", caption: "caption" } : {}; + } + + getDisplayDoc(layoutDoc: Doc, d: Doc, dxf: () => Transform) { + let resolvedDataDoc = !this.props.Document.isTemplate && this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined; + let width = () => d.nativeWidth ? Math.min(layoutDoc[WidthSym](), this.columnWidth) : this.columnWidth; + let height = () => this.getDocHeight(layoutDoc); + let finalDxf = () => dxf().scale(this.columnWidth / layoutDoc[WidthSym]()); + return <CollectionSchemaPreview + Document={layoutDoc} + DataDocument={resolvedDataDoc} + showOverlays={this.overlays} + renderDepth={this.props.renderDepth} + width={width} + height={height} + getTransform={finalDxf} + CollectionView={this.props.CollectionView} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + removeDocument={this.props.removeDocument} + active={this.props.active} + whenActiveChanged={this.props.whenActiveChanged} + addDocTab={this.props.addDocTab} + setPreviewScript={emptyFunction} + previewScript={undefined}> + </CollectionSchemaPreview>; + } + getDocHeight(d: Doc) { + let nw = NumCast(d.nativeWidth); + let nh = NumCast(d.nativeHeight); + let aspect = nw && nh ? nh / nw : 1; + let wid = Math.min(d[WidthSym](), this.columnWidth); + return (nw && nh) ? wid * aspect : d[HeightSym](); } + + + offsetTransform(doc: Doc, translateX: number, translateY: number) { + let outerXf = Utils.GetScreenTransform(this._masonryGridRef!); + let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); + return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]).scale(NumCast(doc.width, 1) / this.columnWidth); + } + getDocTransform(doc: Doc, dref: HTMLDivElement) { + let { scale, translateX, translateY } = Utils.GetScreenTransform(dref); + return this.offsetTransform(doc, translateX, translateY); + } + getSingleDocTransform(doc: Doc, ind: number, width: number) { + let localY = this.filteredChildren.reduce((height, d, i) => + height + (i < ind ? this.getDocHeight(Doc.expandTemplateLayout(d, this.props.DataDoc)) + this.gridGap : 0), this.yMargin); + let translate = this.props.ScreenToLocalTransform().inverse().transformPoint((this.props.PanelWidth() - width) / 2, localY); + return this.offsetTransform(doc, translate[0], translate[1]); + } + @computed get children() { - return this.childDocs.filter(d => !d.isMinimized).map((d, i) => { - let aspect = d.nativeHeight ? NumCast(d.nativeWidth) / NumCast(d.nativeHeight) : undefined; - let dref = React.createRef<HTMLDivElement>(); - let dxf = () => this.getDocTransform(d, dref.current!).scale(this.columnWidth / d[WidthSym]()); - let width = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth; - let height = () => aspect ? width() / aspect : d[HeightSym](); - let rowSpan = Math.ceil((height() + this.gridGap) / (this._gridSize + this.gridGap)); - return (<div className="collectionStackingView-masonryDoc" - key={d[Id]} - ref={dref} - style={{ gridRowEnd: `span ${rowSpan}` }} > - <CollectionSchemaPreview - Document={d} - DataDocument={this.props.Document.layout instanceof Doc ? this.props.Document : this.props.DataDoc} - renderDepth={this.props.renderDepth} - CollectionView={this.props.CollectionView} - addDocument={this.props.addDocument} - moveDocument={this.props.moveDocument} - removeDocument={this.props.removeDocument} - getTransform={dxf} - width={width} - height={height} - active={this.props.active} - addDocTab={this.props.addDocTab} - whenActiveChanged={this.props.whenActiveChanged} - setPreviewScript={emptyFunction} - previewScript={undefined}> - </CollectionSchemaPreview> - </div>); + this._docXfs.length = 0; + return this.filteredChildren.map((d, i) => { + let layoutDoc = Doc.expandTemplateLayout(d, this.props.DataDoc); + let width = () => d.nativeWidth ? Math.min(layoutDoc[WidthSym](), this.columnWidth) : this.columnWidth; + let height = () => this.getDocHeight(layoutDoc); + if (this.singleColumn) { + let dxf = () => this.getSingleDocTransform(layoutDoc, i, width()); + let rowHgtPcnt = height() / (this.props.Document[HeightSym]() - 2 * this.yMargin) * 100; + this._docXfs.push({ dxf: dxf, width: width, height: height }); + return <div className="collectionStackingView-columnDoc" key={d[Id]} style={{ width: width(), marginTop: i === 0 ? 0 : this.gridGap, height: `${rowHgtPcnt}%` }} > + {this.getDisplayDoc(layoutDoc, d, dxf)} + </div>; + } else { + let dref = React.createRef<HTMLDivElement>(); + let dxf = () => this.getDocTransform(layoutDoc, dref.current!); + let rowSpan = Math.ceil((height() + this.gridGap) / (this._gridSize + this.gridGap)); + this._docXfs.push({ dxf: dxf, width: width, height: height }); + return <div className="collectionStackingView-masonryDoc" key={d[Id]} ref={dref} style={{ gridRowEnd: `span ${rowSpan}` }} > + {this.getDisplayDoc(layoutDoc, d, dxf)} + </div>; + } }); } @@ -166,13 +164,64 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { }); } } + + @undoBatch + @action + drop = (e: Event, de: DragManager.DropEvent) => { + let targInd = -1; + let where = [de.x, de.y]; + if (de.data instanceof DragManager.DocumentDragData) { + this._docXfs.map((cd, i) => { + let pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); + let pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height()); + if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) { + targInd = i; + } + }); + } + if (super.drop(e, de)) { + let newDoc = de.data.droppedDocuments[0]; + let docs = this.childDocList; + if (docs) { + if (targInd === -1) targInd = docs.length; + else targInd = docs.indexOf(this.filteredChildren[targInd]); + let srcInd = docs.indexOf(newDoc); + docs.splice(srcInd, 1); + docs.splice(targInd > srcInd ? targInd - 1 : targInd, 0, newDoc); + } + } + return false; + } + @undoBatch + @action + onDrop = (e: React.DragEvent): void => { + let where = [e.clientX, e.clientY]; + let targInd = -1; + this._docXfs.map((cd, i) => { + let pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); + let pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height()); + if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) { + targInd = i; + } + }); + super.onDrop(e, {}, () => { + if (targInd !== -1) { + let newDoc = this.childDocs[this.childDocs.length - 1]; + let docs = this.childDocList; + if (docs) { + docs.splice(docs.length - 1, 1); + docs.splice(targInd, 0, newDoc); + } + } + }); + } render() { - let cols = this.singleColumn ? 1 : Math.max(1, Math.min(this.childDocs.filter(d => !d.isMinimized).length, + let cols = this.singleColumn ? 1 : Math.max(1, Math.min(this.filteredChildren.length, Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))); let templatecols = ""; for (let i = 0; i < cols; i++) templatecols += `${this.columnWidth}px `; return ( - <div className="collectionStackingView" ref={this.createRef} onContextMenu={this.onContextMenu} onWheel={(e: React.WheelEvent) => e.stopPropagation()} > + <div className="collectionStackingView" ref={this.createRef} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onWheel={(e: React.WheelEvent) => e.stopPropagation()} > <div className={`collectionStackingView-masonry${this.singleColumn ? "Single" : "Grid"}`} style={{ padding: this.singleColumn ? `${this.yMargin}px ${this.xMargin}px ${this.yMargin}px ${this.xMargin}px` : `${this.yMargin}px ${this.xMargin}px`, @@ -185,7 +234,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { gridAutoRows: this.singleColumn ? undefined : `${this._gridSize}px` }} > - {this.singleColumn ? this.singleColumnChildren : this.children} + {this.children} {this.singleColumn ? (null) : this.columnDragger} </div> </div> diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 3b3bbdbd9..71f1908f0 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -19,6 +19,7 @@ import { CollectionPDFView } from "./CollectionPDFView"; import { CollectionVideoView } from "./CollectionVideoView"; import { CollectionView } from "./CollectionView"; import React = require("react"); +import { MainView } from "../MainView"; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; @@ -47,11 +48,17 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); } + get childDocs() { let self = this; //TODO tfs: This might not be what we want? //This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue) - return DocListCast((BoolCast(this.props.Document.isTemplate) ? this.extensionDoc : this.props.Document)[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey]); + return DocListCast(this.extensionDoc[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey]); + } + get childDocList() { + //TODO tfs: This might not be what we want? + //This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue) + return Cast(this.extensionDoc[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey], listSpec(Doc)); } @action @@ -62,10 +69,17 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { let email = CurrentUserUtils.email; let pos = { x: position[0], y: position[1] }; if (id && email) { - const proto = await doc.proto; + const proto = Doc.GetProto(doc); if (!proto) { return; } + // The following conditional detects a recurring bug we've seen on the server + if (proto[Id] === "collectionProto") { + alert("COLLECTION PROTO CURSOR ISSUE DETECTED! Check console for more info..."); + console.log(doc); + console.log(proto); + throw new Error(`AHA! You were trying to set a cursor on a collection's proto, which is the original collection proto! Look at the two previously printed lines for document values!`); + } let cursors = Cast(proto.cursors, listSpec(CursorField)); if (!cursors) { proto.cursors = cursors = new List<CursorField>(); @@ -89,7 +103,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { } else if (de.data.moveDocument) { let movedDocs = de.data.options === this.props.Document[Id] ? de.data.draggedDocuments : de.data.droppedDocuments; added = movedDocs.reduce((added: boolean, d) => - de.data.moveDocument(d, this.props.Document, this.props.addDocument) || added, false); + de.data.moveDocument(d, /*this.props.DataDoc ? this.props.DataDoc :*/ this.props.Document, this.props.addDocument) || added, false); } else { added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false); } @@ -97,55 +111,15 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { return added; } else if (de.data instanceof DragManager.AnnotationDragData) { + e.stopPropagation(); return this.props.addDocument(de.data.dropDocument); } return false; } - protected async getDocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> { - let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined; - if (type.indexOf("image") !== -1) { - ctor = Docs.ImageDocument; - } - if (type.indexOf("video") !== -1) { - ctor = Docs.VideoDocument; - } - if (type.indexOf("audio") !== -1) { - ctor = Docs.AudioDocument; - } - if (type.indexOf("pdf") !== -1) { - ctor = Docs.PdfDocument; - options.nativeWidth = 1200; - } - if (type.indexOf("excel") !== -1) { - ctor = Docs.DBDocument; - options.dropAction = "copy"; - } - if (type.indexOf("html") !== -1) { - if (path.includes(window.location.hostname)) { - let s = path.split('/'); - let id = s[s.length - 1]; - DocServer.GetRefField(id).then(field => { - if (field instanceof Doc) { - let alias = Doc.MakeAlias(field); - alias.x = options.x || 0; - alias.y = options.y || 0; - alias.width = options.width || 300; - alias.height = options.height || options.width || 300; - this.props.addDocument(alias, false); - } - }); - return undefined; - } - ctor = Docs.WebDocument; - options = { height: options.width, ...options, title: path, nativeWidth: undefined }; - } - return ctor ? ctor(path, options) : undefined; - } - @undoBatch @action - protected onDrop(e: React.DragEvent, options: DocumentOptions): void { + protected onDrop(e: React.DragEvent, options: DocumentOptions, completed?: () => void) { if (e.ctrlKey) { e.stopPropagation(); // bcz: this is a hack to stop propagation when dropping an image on a text document with shift+ctrl return; @@ -171,10 +145,10 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { } }); } else { - this.props.addDocument && this.props.addDocument(Docs.WebDocument(href, options)); + this.props.addDocument && this.props.addDocument(Docs.Create.WebDocument(href, options)); } } else if (text) { - this.props.addDocument && this.props.addDocument(Docs.TextDocument({ ...options, width: 100, height: 25, documentText: "@@@" + text }), false); + this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument({ ...options, width: 100, height: 25, documentText: "@@@" + text }), false); } return; } @@ -184,7 +158,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { let img = tags[0].startsWith("img") ? tags[0] : tags.length > 1 && tags[1].startsWith("img") ? tags[1] : ""; if (img) { let split = img.split("src=\"")[1].split("\"")[0]; - let doc = Docs.ImageDocument(split, { ...options, width: 300 }); + let doc = Docs.Create.ImageDocument(split, { ...options, width: 300 }); this.props.addDocument(doc, false); return; } else { @@ -198,15 +172,15 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { } }); } else { - let htmlDoc = Docs.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text }); + let htmlDoc = Docs.Create.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text }); this.props.addDocument(htmlDoc, false); } return; } } if (text && text.indexOf("www.youtube.com/watch") !== -1) { - const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/"); - this.props.addDocument(Docs.WebDocument(url, { ...options, width: 300, height: 300 })); + const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/");// + "?enablejsapi=1"; + this.props.addDocument(Docs.Create.VideoDocument(url, { ...options, width: 400, height: 315 })); return; } @@ -223,7 +197,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { .then(result => { let type = result["content-type"]; if (type) { - this.getDocumentFromType(type, str, { ...options, width: 300, nativeWidth: 300 }) + Docs.Get.DocumentFromType(type, str, { ...options, width: 300, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300 }) .then(doc => doc && this.props.addDocument(doc, false)); } }); @@ -244,10 +218,9 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { body: formData }).then(async (res: Response) => { (await res.json()).map(action((file: any) => { - let path = window.location.origin + file; - let docPromise = this.getDocumentFromType(type, path, { ...options, nativeWidth: 300, width: 300, title: dropFileName }); - - docPromise.then(doc => doc && this.props.addDocument(doc)); + let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName }; + let path = DocServer.prepend(file); + Docs.Get.DocumentFromType(type, path, full).then(doc => doc && this.props.addDocument(doc)); })); }); promises.push(prom); @@ -255,7 +228,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { } if (promises.length) { - Promise.all(promises).finally(() => batch.end()); + Promise.all(promises).finally(() => { completed && completed(); batch.end(); }); } else { batch.end(); } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 9e0130281..0196fecff 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,15 +1,15 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faAngleRight, faCaretDown, faCaretRight, faCaretSquareDown, faCaretSquareRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; +import { faAngleRight, faCamera, faExpand, faTrash, faBell, faCaretDown, faCaretRight, faCaretSquareDown, faCaretSquareRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable } from "mobx"; +import { action, computed, observable, trace, untracked } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, HeightSym, WidthSym } from '../../../new_fields/Doc'; +import { Doc, DocListCast, HeightSym, WidthSym, Opt } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; import { Document, listSpec } from '../../../new_fields/Schema'; import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types'; import { emptyFunction, Utils } from '../../../Utils'; -import { Docs, DocUtils } from '../../documents/Documents'; +import { Docs, DocUtils, DocumentType } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager"; import { SelectionManager } from '../../util/SelectionManager'; @@ -36,7 +36,7 @@ export interface TreeViewProps { deleteDoc: (doc: Doc) => boolean; moveDocument: DragManager.MoveFunction; dropAction: "alias" | "copy" | undefined; - addDocTab: (doc: Doc, dataDoc: Doc, where: string) => void; + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void; panelWidth: () => number; panelHeight: () => number; addDocument: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean; @@ -50,6 +50,10 @@ export interface TreeViewProps { library.add(faTrashAlt); library.add(faAngleRight); +library.add(faBell); +library.add(faTrash); +library.add(faCamera); +library.add(faExpand); library.add(faCaretDown); library.add(faCaretRight); library.add(faCaretSquareDown); @@ -65,12 +69,14 @@ class TreeView extends React.Component<TreeViewProps> { private _dref = React.createRef<HTMLDivElement>(); @observable __chosenKey: string = ""; @computed get _chosenKey() { return this.__chosenKey ? this.__chosenKey : this.fieldKey; } + @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.document.maxEmbedHeight, 300); } @observable _collapsed: boolean = true; @computed get fieldKey() { - let keys = Array.from(Object.keys(this.resolvedDataDoc)); + let keys = Array.from(Object.keys(this.resolvedDataDoc)); // bcz: Argh -- make untracked to avoid this rerunning whenever 'libraryBrush' is set if (this.resolvedDataDoc.proto instanceof Doc) { - keys.push(...Array.from(Object.keys(this.resolvedDataDoc.proto))); + let arr = Array.from(Object.keys(this.resolvedDataDoc.proto));// bcz: Argh -- make untracked to avoid this rerunning whenever 'libraryBrush' is set + keys.push(...arr); while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1); } let keyList: string[] = []; @@ -97,7 +103,7 @@ class TreeView extends React.Component<TreeViewProps> { } @undoBatch delete = () => this.props.deleteDoc(this.resolvedDataDoc); - @undoBatch openRight = async () => this.props.addDocTab(this.props.document, this.props.document, "onRight"); + @undoBatch openRight = async () => this.props.addDocTab(this.props.document, undefined, "onRight"); onPointerDown = (e: React.PointerEvent) => e.stopPropagation(); onPointerEnter = (e: React.PointerEvent): void => { @@ -108,12 +114,12 @@ class TreeView extends React.Component<TreeViewProps> { } } onPointerLeave = (e: React.PointerEvent): void => { - this.props.document.libraryBrush = false; + this.props.document.libraryBrush = undefined; this._header!.current!.className = "treeViewItem-header"; document.removeEventListener("pointermove", this.onDragMove, true); } onDragMove = (e: PointerEvent): void => { - this.props.document.libraryBrush = false; + this.props.document.libraryBrush = undefined; let x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); let rect = this._header!.current!.getBoundingClientRect(); let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2); @@ -147,32 +153,26 @@ class TreeView extends React.Component<TreeViewProps> { let docList = Cast(this.resolvedDataDoc[this.fieldKey], listSpec(Doc)); let doc = Cast(this.resolvedDataDoc[this.fieldKey], Doc); let isDoc = doc instanceof Doc || docList; - return <div className="bullet" onClick={action(() => this._collapsed = !this._collapsed)}> + let c + return <div className="bullet" onClick={action(() => this._collapsed = !this._collapsed)} style={{ color: StrCast(this.props.document.color, "black"), opacity: 0.4 }}> {<FontAwesomeIcon icon={this._collapsed ? (isDoc ? "caret-square-right" : "caret-right") : (isDoc ? "caret-square-down" : "caret-down")} />} </div>; } - titleClicked = (e: React.MouseEvent) => { - if (this._collapsed) return false; - else { - this.props.document.embed = !BoolCast(this.props.document.embed); - return true; - } - } static loadId = ""; editableView = (key: string, style?: string) => (<EditableView oneLine={true} display={"inline"} editing={this.resolvedDataDoc[Id] === TreeView.loadId} contents={StrCast(this.props.document[key])} - onClick={this.titleClicked} height={36} fontStyle={style} + fontSize={12} GetValue={() => StrCast(this.props.document[key])} SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc)[key] = value) ? true : true} OnFillDown={(value: string) => { Doc.GetProto(this.resolvedDataDoc)[key] = value; - let doc = Docs.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); + let doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); TreeView.loadId = doc[Id]; return this.props.addDocument(doc); }} @@ -186,8 +186,7 @@ class TreeView extends React.Component<TreeViewProps> { } let keyList: string[] = keys.reduce((l, key) => { let listspec = DocListCast(this.resolvedDataDoc[key]); - if (listspec && listspec.length) - return [...l, key]; + if (listspec && listspec.length) return [...l, key]; return l; }, [] as string[]); keys.map(key => Cast(this.resolvedDataDoc[key], Doc) instanceof Doc && keyList.push(key)); @@ -206,7 +205,7 @@ class TreeView extends React.Component<TreeViewProps> { let onItemDown = SetupDrag(reference, () => this.resolvedDataDoc, this.move, this.props.dropAction, this.props.treeViewId, true); let headerElements = ( - <span className="collectionTreeView-keyHeader" key={this._chosenKey} + <span className="collectionTreeView-keyHeader" key={this._chosenKey + "chosen"} onPointerDown={action(() => { let ind = this.keyList.indexOf(this._chosenKey); ind = (ind + 1) % this.keyList.length; @@ -222,7 +221,8 @@ class TreeView extends React.Component<TreeViewProps> { return <> <div className="docContainer" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown} style={{ - background: BoolCast(this.props.document.protoBrush, false) ? "#06123232" : BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0", + background: BoolCast(this.props.document.libraryBrush) ? "#06121212" : "0", + outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined, pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? "all" : "none" }} > @@ -236,22 +236,26 @@ class TreeView extends React.Component<TreeViewProps> { onWorkspaceContextMenu = (e: React.MouseEvent): void => { 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 - ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.resolvedDataDoc)) }); - ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" }); + ContextMenu.Instance.addItem({ description: (BoolCast(this.props.document.embed) ? "Collapse" : "Expand") + " inline", event: () => this.props.document.embed = !BoolCast(this.props.document.embed), icon: "expand" }); if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking) { 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" }); if (DocumentManager.Instance.getDocumentViews(this.resolvedDataDoc).length) { - ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.resolvedDataDoc).map(view => view.props.focus(this.props.document, true)) }); + ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.resolvedDataDoc).map(view => view.props.focus(this.props.document, true)), icon: "camera" }); } - ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)) }); + ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)), icon: "trash-alt" }); } else { - ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.props.document)) }); + ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.resolvedDataDoc)), icon: "caret-square-right" }); + ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.props.document)), icon: "trash-alt" }); } + ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" }); ContextMenu.Instance.displayMenu(e.pageX > 156 ? e.pageX - 156 : 0, e.pageY - 15); e.stopPropagation(); + e.preventDefault(); } } + + @undoBatch treeDrop = (e: Event, de: DragManager.DropEvent) => { let x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); let rect = this._header!.current!.getBoundingClientRect(); @@ -265,6 +269,8 @@ class TreeView extends React.Component<TreeViewProps> { e.stopPropagation(); } 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); if (inside) { let docList = Cast(this.resolvedDataDoc.data, listSpec(Doc)); @@ -272,7 +278,6 @@ class TreeView extends React.Component<TreeViewProps> { addDoc = (doc: Doc) => { docList && docList.push(doc); return true; }; } } - e.stopPropagation(); 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) @@ -297,7 +302,14 @@ class TreeView extends React.Component<TreeViewProps> { let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.props.document, this._chosenKey, doc, addBefore, before); let groups = LinkManager.Instance.getRelatedGroupedLinks(this.props.document); groups.forEach((groupLinkDocs, groupType) => { - let destLinks = groupLinkDocs.map(d => LinkManager.Instance.getOppositeAnchor(d, this.props.document)); + // let destLinks = groupLinkDocs.map(d => LinkManager.Instance.getOppositeAnchor(d, this.props.document)); + let destLinks: Doc[] = []; + groupLinkDocs.forEach((doc) => { + let opp = LinkManager.Instance.getOppositeAnchor(doc, this.props.document); + if (opp) { + destLinks.push(opp); + } + }); ele.push( <div key={"treeviewlink-" + groupType + "subtitle"}> <div className="collectionTreeView-subtitle">{groupType}:</div> @@ -311,13 +323,33 @@ class TreeView extends React.Component<TreeViewProps> { return ele; } + @computed get boundsOfCollectionDocument() { + if (StrCast(this.props.document.type).indexOf(DocumentType.COL) === -1) return undefined; + let layoutDoc = Doc.expandTemplateLayout(this.props.document, this.props.dataDoc); + return Doc.ComputeContentBounds(DocListCast(layoutDoc.data)); + } + 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() - 5)); + return NumCast(this.props.document.nativeWidth) ? Math.min(this.props.document[WidthSym](), this.props.panelWidth() - 5) : this.props.panelWidth() - 5; + } + docHeight = () => { + let bounds = this.boundsOfCollectionDocument; + return Math.min(this.MAX_EMBED_HEIGHT, (() => { + let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth); + if (aspect) return this.docWidth() * aspect; + if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x); + return NumCast(this.props.document.height) ? NumCast(this.props.document.height) : 50; + })()); + } + render() { let contentElement: (JSX.Element | null) = null; let docList = Cast(this.resolvedDataDoc[this._chosenKey], listSpec(Doc)); let remDoc = (doc: Doc) => this.remove(doc, this._chosenKey); let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.resolvedDataDoc, this._chosenKey, doc, addBefore, before); let doc = Cast(this.resolvedDataDoc[this._chosenKey], Doc); - let docWidth = () => NumCast(this.props.document.nativeWidth) ? Math.min(this.props.document[WidthSym](), this.props.panelWidth() - 5) : this.props.panelWidth() - 5; + if (!this._collapsed) { if (!this.props.document.embed) { contentElement = <ul key={this._chosenKey + "more"}> @@ -327,13 +359,14 @@ class TreeView extends React.Component<TreeViewProps> { </ul >; } else { let layoutDoc = Doc.expandTemplateLayout(this.props.document, this.props.dataDoc); - contentElement = <div ref={this._dref} style={{ display: "inline-block", height: layoutDoc[HeightSym]() }} key={this.props.document[Id]}> + contentElement = <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} renderDepth={this.props.renderDepth} - width={docWidth} - height={layoutDoc[HeightSym]} + fitToBox={this.boundsOfCollectionDocument !== undefined} + width={this.docWidth} + height={this.docHeight} getTransform={this.docTransform} CollectionView={undefined} addDocument={emptyFunction as any} @@ -369,7 +402,7 @@ class TreeView extends React.Component<TreeViewProps> { remove: ((doc: Doc) => boolean), move: DragManager.MoveFunction, dropAction: dropActionType, - addDocTab: (doc: Doc, dataDoc: Doc, where: string) => void, + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void, screenToLocalXf: () => Transform, outerXf: () => { translateX: number, translateY: number }, active: () => boolean, @@ -399,7 +432,7 @@ class TreeView extends React.Component<TreeViewProps> { dataDoc={dataDoc} containingCollection={containingCollection} treeViewId={treeViewId} - key={child[Id]} + key={child[Id] + "child " + i} indentDocument={indent} renderDepth={renderDepth} deleteDoc={remove} @@ -444,9 +477,12 @@ export class CollectionTreeView extends CollectionSubView(Document) { } onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout - if (!e.isPropagationStopped() && this.props.Document.excludeFromLibrary) { // excludeFromLibrary means this is the user document + if (!e.isPropagationStopped() && this.props.Document.workspaceLibrary) { // excludeFromLibrary means this is the user document ContextMenu.Instance.addItem({ description: "Create Workspace", event: undoBatch(() => MainView.Instance.createNewWorkspace()) }); ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.remove(this.props.Document)) }); + e.stopPropagation(); + e.preventDefault(); + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); } } @@ -455,14 +491,49 @@ export class CollectionTreeView extends CollectionSubView(Document) { outerXf = () => Utils.GetScreenTransform(this._mainEle!); onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {}); + + @observable static NotifsCol: Opt<Doc>; + + openNotifsCol = () => { + if (CollectionTreeView.NotifsCol && CollectionDockingView.Instance) { + CollectionDockingView.Instance.AddRightSplit(CollectionTreeView.NotifsCol, undefined); + } + } + @computed get notifsButton() { + const length = CollectionTreeView.NotifsCol ? DocListCast(CollectionTreeView.NotifsCol.data).length : 0; + const notifsRef = React.createRef<HTMLDivElement>(); + const dragNotifs = action(() => CollectionTreeView.NotifsCol!); + return <div id="toolbar" key="toolbar"> + <div ref={notifsRef}> + <button className="toolbar-button round-button" title="Notifs" + onClick={this.openNotifsCol} onPointerDown={CollectionTreeView.NotifsCol ? SetupDrag(notifsRef, dragNotifs) : emptyFunction}> + <FontAwesomeIcon icon={faBell} size="sm" /> + </button> + <div className="main-notifs-badge" style={length > 0 ? { "display": "initial" } : { "display": "none" }}> + {length} + </div> + </div> + </div >; + } + @computed get clearButton() { + return <div id="toolbar" key="toolbar"> + <div > + <button className="toolbar-button round-button" title="Notifs" + onClick={undoBatch(action(() => Doc.GetProto(this.props.Document)[this.props.fieldKey] = undefined))}> + <FontAwesomeIcon icon={faTrash} size="sm" /> + </button> + </div> + </div >; + } + + render() { 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); 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" }} + style={{ overflow: "auto", background: StrCast(this.props.Document.backgroundColor, "lightgray") }} onContextMenu={this.onContextMenu} onWheel={(e: React.WheelEvent) => (e.target as any).scrollHeight > (e.target as any).clientHeight && e.stopPropagation()} onDrop={this.onTreeDrop} @@ -475,10 +546,12 @@ export class CollectionTreeView extends CollectionSubView(Document) { SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc).title = value) ? true : true} OnFillDown={(value: string) => { Doc.GetProto(this.props.Document).title = value; - let doc = Docs.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); + let doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); TreeView.loadId = doc[Id]; Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true); }} /> + {this.props.Document.workspaceLibrary ? this.notifsButton : (null)} + {this.props.Document.allowClear ? this.clearButton : (null)} <ul className="no-indent" style={{ width: "max-content" }} > { TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.Document, this.props.DataDoc, this.props.fieldKey, addDoc, this.remove, diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx index c1a6ca44e..f731c4cef 100644 --- a/src/client/views/collections/CollectionVideoView.tsx +++ b/src/client/views/collections/CollectionVideoView.tsx @@ -21,13 +21,13 @@ import { Docs, DocUtils } from "../../documents/Documents"; export class CollectionVideoView extends React.Component<FieldViewProps> { private _videoBox?: VideoBox; - public static LayoutString(fieldKey: string = "data") { - return FieldView.LayoutString(CollectionVideoView, fieldKey); + public static LayoutString(fieldKey: string = "data", fieldExt: string = "annotations") { + return FieldView.LayoutString(CollectionVideoView, fieldKey, fieldExt); } private get uIButtons() { let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale); let curTime = NumCast(this.props.Document.curPage); - return ([ + return (VideoBox._showControls ? [] : [ <div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling}, ${scaling})` }}> <span>{"" + Math.round(curTime)}</span> <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span> @@ -43,7 +43,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { @action onPlayDown = () => { - if (this._videoBox && this._videoBox.player) { + if (this._videoBox) { if (this._videoBox.Playing) { this._videoBox.Pause(); } else { @@ -98,7 +98,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { SearchBox.convertDataUri(dataUrl, filename).then((returnedFilename) => { if (returnedFilename) { let url = DocServer.prepend(returnedFilename); - let imageSummary = Docs.ImageDocument(url, { + let imageSummary = Docs.Create.ImageDocument(url, { x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y), width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-" }); @@ -122,7 +122,6 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { } render() { - trace(); return ( <CollectionBaseView {...this.props} className="collectionVideoView-cont" onContextMenu={this.onContextMenu}> {this.subView} diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 0517c17bf..56750668d 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -27,7 +27,7 @@ library.add(faThList); @observer export class CollectionView extends React.Component<FieldViewProps> { - public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(CollectionView, fieldStr); } + public static LayoutString(fieldStr: string = "data", fieldExt: string = "") { return FieldView.LayoutString(CollectionView, fieldStr, fieldExt); } private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => { let props = { ...this.props, ...renderProps }; @@ -43,7 +43,7 @@ export class CollectionView extends React.Component<FieldViewProps> { return (null); } - get isAnnotationOverlay() { return this.props.fieldKey === "annotations" || this.props.fieldExt === "annotations"; } + 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 @@ -63,7 +63,7 @@ export class CollectionView extends React.Component<FieldViewProps> { otherdoc.height = 50; Doc.GetProto(otherdoc).title = "applied(" + this.props.Document.title + ")"; Doc.GetProto(otherdoc).layout = Doc.MakeDelegate(this.props.Document); - this.props.addDocTab && this.props.addDocTab(otherdoc, otherdoc, "onRight"); + this.props.addDocTab && this.props.addDocTab(otherdoc, undefined, "onRight"); }), icon: "project-diagram" }); } diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index fa7058d5a..c0f489cd8 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -9,7 +9,7 @@ import { CollectionDockingView } from "./CollectionDockingView"; import { NumCast } from "../../../new_fields/Types"; import { CollectionViewType } from "./CollectionBaseView"; -type SelectorProps = { Document: Doc, addDocTab(doc: Doc, dataDoc: Doc, location: string): void }; +type SelectorProps = { Document: Doc, addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void }; @observer export class SelectorContextMenu extends React.Component<SelectorProps> { @observable private _docs: { col: Doc, target: Doc }[] = []; @@ -23,9 +23,9 @@ export class SelectorContextMenu extends React.Component<SelectorProps> { async fetchDocuments() { let aliases = (await SearchUtil.GetAliasesOfDocument(this.props.Document)).filter(doc => doc !== this.props.Document); - const docs = await SearchUtil.Search(`data_l:"${this.props.Document[Id]}"`, true); + const { docs } = await SearchUtil.Search(`data_l:"${this.props.Document[Id]}"`, true); const map: Map<Doc, Doc> = new Map; - const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search(`data_l:"${doc[Id]}"`, true))); + const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search(`data_l:"${doc[Id]}"`, true).then(result => result.docs))); allDocs.forEach((docs, index) => docs.forEach(doc => map.set(doc, aliases[index]))); docs.forEach(doc => map.delete(doc)); runInAction(() => { @@ -43,7 +43,7 @@ export class SelectorContextMenu extends React.Component<SelectorProps> { col.panX = newPanX; col.panY = newPanY; } - this.props.addDocTab(col, col, "inTab"); // bcz: dataDoc? + this.props.addDocTab(col, undefined, "inTab"); // bcz: dataDoc? }; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index ebeb1fcee..2d94f1b8e 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -17,58 +17,56 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP _brushReactionDisposer?: IReactionDisposer; componentDidMount() { - this._brushReactionDisposer = reaction( - () => { - let doclist = DocListCast(this.props.Document[this.props.fieldKey]); - return { doclist: doclist ? doclist : [], xs: doclist.map(d => d.x) }; - }, - () => { - let doclist = DocListCast(this.props.Document[this.props.fieldKey]); - let views = doclist ? doclist.filter(doc => StrCast(doc.backgroundLayout).indexOf("istogram") !== -1) : []; - views.forEach((dstDoc, i) => { - views.forEach((srcDoc, j) => { - let dstTarg = dstDoc; - let srcTarg = srcDoc; - let x1 = NumCast(srcDoc.x); - let x2 = NumCast(dstDoc.x); - let x1w = NumCast(srcDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1); - let x2w = NumCast(dstDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1); - if (x1w < 0 || x2w < 0 || i === j) { } - else { - let findBrush = (field: (Doc | Promise<Doc>)[]) => field.findIndex(brush => { - let bdocs = brush instanceof Doc ? Cast(brush.brushingDocs, listSpec(Doc), []) : undefined; - return bdocs && bdocs.length && ((bdocs[0] === dstTarg && bdocs[1] === srcTarg)) ? true : false; - }); - let brushAction = (field: (Doc | Promise<Doc>)[]) => { - let found = findBrush(field); - if (found !== -1) { - console.log("REMOVE BRUSH " + srcTarg.title + " " + dstTarg.title); - field.splice(found, 1); - } - }; - if (Math.abs(x1 + x1w - x2) < 20) { - let linkDoc: Doc = new Doc(); - linkDoc.title = "Histogram Brush"; - linkDoc.linkDescription = "Brush between " + StrCast(srcTarg.title) + " and " + StrCast(dstTarg.Title); - linkDoc.brushingDocs = new List([dstTarg, srcTarg]); + // this._brushReactionDisposer = reaction( + // () => { + // let doclist = DocListCast(this.props.Document[this.props.fieldKey]); + // return { doclist: doclist ? doclist : [], xs: doclist.map(d => d.x) }; + // }, + // () => { + // let doclist = DocListCast(this.props.Document[this.props.fieldKey]); + // let views = doclist ? doclist.filter(doc => StrCast(doc.backgroundLayout).indexOf("istogram") !== -1) : []; + // views.forEach((dstDoc, i) => { + // views.forEach((srcDoc, j) => { + // let dstTarg = dstDoc; + // let srcTarg = srcDoc; + // let x1 = NumCast(srcDoc.x); + // let x2 = NumCast(dstDoc.x); + // let x1w = NumCast(srcDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1); + // let x2w = NumCast(dstDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1); + // if (x1w < 0 || x2w < 0 || i === j) { } + // else { + // let findBrush = (field: (Doc | Promise<Doc>)[]) => field.findIndex(brush => { + // let bdocs = brush instanceof Doc ? Cast(brush.brushingDocs, listSpec(Doc), []) : undefined; + // return bdocs && bdocs.length && ((bdocs[0] === dstTarg && bdocs[1] === srcTarg)) ? true : false; + // }); + // let brushAction = (field: (Doc | Promise<Doc>)[]) => { + // let found = findBrush(field); + // if (found !== -1) { + // field.splice(found, 1); + // } + // }; + // if (Math.abs(x1 + x1w - x2) < 20) { + // let linkDoc: Doc = new Doc(); + // linkDoc.title = "Histogram Brush"; + // linkDoc.linkDescription = "Brush between " + StrCast(srcTarg.title) + " and " + StrCast(dstTarg.Title); + // linkDoc.brushingDocs = new List([dstTarg, srcTarg]); - brushAction = (field: (Doc | Promise<Doc>)[]) => { - if (findBrush(field) === -1) { - console.log("ADD BRUSH " + srcTarg.title + " " + dstTarg.title); - field.push(linkDoc); - } - }; - } - if (dstTarg.brushingDocs === undefined) dstTarg.brushingDocs = new List<Doc>(); - if (srcTarg.brushingDocs === undefined) srcTarg.brushingDocs = new List<Doc>(); - let dstBrushDocs = Cast(dstTarg.brushingDocs, listSpec(Doc), []); - let srcBrushDocs = Cast(srcTarg.brushingDocs, listSpec(Doc), []); - brushAction(dstBrushDocs); - brushAction(srcBrushDocs); - } - }); - }); - }); + // brushAction = (field: (Doc | Promise<Doc>)[]) => { + // if (findBrush(field) === -1) { + // field.push(linkDoc); + // } + // }; + // } + // if (dstTarg.brushingDocs === undefined) dstTarg.brushingDocs = new List<Doc>(); + // if (srcTarg.brushingDocs === undefined) srcTarg.brushingDocs = new List<Doc>(); + // let dstBrushDocs = Cast(dstTarg.brushingDocs, listSpec(Doc), []); + // let srcBrushDocs = Cast(srcTarg.brushingDocs, listSpec(Doc), []); + // brushAction(dstBrushDocs); + // brushAction(srcBrushDocs); + // } + // }); + // }); + // }); } componentWillUnmount() { if (this._brushReactionDisposer) { @@ -115,19 +113,16 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP }); return drawnPairs; }, [] as { a: Doc, b: Doc, l: Doc[] }[]); - return connections.map(c => { - let x = c.l.reduce((p, l) => p + l[Id], ""); - return <CollectionFreeFormLinkView key={x} A={c.a} B={c.b} LinkDocs={c.l} - removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />; - }); + 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} />); } render() { return ( <div className="collectionfreeformlinksview-container"> - <svg className="collectionfreeformlinksview-svgCanvas"> + {/* <svg className="collectionfreeformlinksview-svgCanvas"> {this.uniqueConnections} - </svg> + </svg> */} {this.props.children} </div> ); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index ccf261c95..00407d39a 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -36,9 +36,9 @@ // linear-gradient(to bottom, $light-color-secondary 1px, transparent 1px); // background-size: 30px 30px; // } - box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw; + opacity: 0.99; border: 0px solid $light-color-secondary; - border-radius: $border-radius; + border-radius: inherit; box-sizing: border-box; position: absolute; @@ -52,46 +52,6 @@ height: 100%; } - -.collectionfreeformview-overlay { - .collectionfreeformview>.jsx-parser { - position: inherit; - height: 100%; - } - - >.jsx-parser { - position: absolute; - z-index: 0; - } - - .formattedTextBox-cont { - background: $light-color-secondary; - overflow: visible; - } - - opacity: 0.99; - border: 0px solid transparent; - border-radius: $border-radius; - box-sizing: border-box; - position:absolute; - z-index: -1; - - .marqueeView { - overflow: hidden; - } - - top: 0; - left: 0; - width: 100%; - height: 100%; - - .collectionfreeformview { - .formattedTextBox-cont { - background: yellow; - } - } -} - // selection border...? .border { border-style: solid; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index bbec33ba3..19e280444 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,6 +1,6 @@ import { action, computed } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCastAsync, HeightSym, WidthSym } from "../../../../new_fields/Doc"; +import { Doc, DocListCastAsync, HeightSym, WidthSym, DocListCast } from "../../../../new_fields/Doc"; import { Id } from "../../../../new_fields/FieldSymbols"; import { InkField, StrokeData } from "../../../../new_fields/InkField"; import { createSchema, makeInterface } from "../../../../new_fields/Schema"; @@ -11,7 +11,7 @@ import { DragManager } from "../../../util/DragManager"; import { HistoryUtil } from "../../../util/History"; import { SelectionManager } from "../../../util/SelectionManager"; import { Transform } from "../../../util/Transform"; -import { undoBatch } from "../../../util/UndoManager"; +import { undoBatch, UndoManager } from "../../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"; import { ContextMenu } from "../../ContextMenu"; import { InkingCanvas } from "../../InkingCanvas"; @@ -27,12 +27,18 @@ import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); import v5 = require("uuid/v5"); +import { ScriptField } from "../../../../new_fields/ScriptField"; +import { OverlayView, OverlayElementOptions } from "../../OverlayView"; +import { ScriptBox } from "../../ScriptBox"; +import { CompileScript } from "../../../util/Scripting"; export const panZoomSchema = createSchema({ panX: "number", panY: "number", - scale: "number" + scale: "number", + arrangeScript: ScriptField, + arrangeInit: ScriptField, }); type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof positionSchema, typeof pageSchema]>; @@ -46,13 +52,22 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private get _pwidth() { return this.props.PanelWidth(); } private get _pheight() { return this.props.PanelHeight(); } + @computed get contentBounds() { + let bounds = this.props.fitToBox && !NumCast(this.nativeWidth) ? Doc.ComputeContentBounds(DocListCast(this.props.Document.data)) : undefined; + return { + panX: bounds ? (bounds.x + bounds.r) / 2 : this.Document.panX || 0, + panY: bounds ? (bounds.y + bounds.b) / 2 : this.Document.panY || 0, + scale: bounds ? Math.min(this.props.PanelHeight() / (bounds.b - bounds.y), this.props.PanelWidth() / (bounds.r - bounds.x)) : this.Document.scale || 1 + }; + } + @computed get nativeWidth() { return this.Document.nativeWidth || 0; } @computed get nativeHeight() { return this.Document.nativeHeight || 0; } - public get isAnnotationOverlay() { return this.props.fieldKey === "annotations" || this.props.fieldExt === "annotations"; } + public get isAnnotationOverlay() { return this.props.fieldExt ? true : false; } // fieldExt will be "" or "annotation". should maybe generalize this, or make it more specific (ie, 'annotation' instead of 'fieldExt') private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } - private panX = () => this.Document.panX || 0; - private panY = () => this.Document.panY || 0; - private zoomScaling = () => this.Document.scale || 1; + private panX = () => this.contentBounds.panX; + private panY = () => this.contentBounds.panY; + private zoomScaling = () => this.contentBounds.scale; private centeringShiftX = () => !this.nativeWidth ? this._pwidth / 2 : 0; // shift so pan position is at center of window for non-overlay collections private centeringShiftY = () => !this.nativeHeight ? this._pheight / 2 : 0;// shift so pan position is at center of window for non-overlay collections private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform()); @@ -80,6 +95,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { }); } + @computed get fieldExtensionDoc() { + return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true"); + } + + @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { @@ -87,10 +107,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { if (de.data instanceof DragManager.DocumentDragData) { if (de.data.droppedDocuments.length) { let dragDoc = de.data.droppedDocuments[0]; - let zoom = NumCast(dragDoc.zoomBasis, 1); let [xp, yp] = this.getTransform().transformPoint(de.x, de.y); - let x = xp - de.data.xOffset / zoom; - let y = yp - de.data.yOffset / zoom; + let x = xp - de.data.xOffset; + let y = yp - de.data.yOffset; let dropX = NumCast(de.data.droppedDocuments[0].x); let dropY = NumCast(de.data.droppedDocuments[0].y); de.data.droppedDocuments.forEach(d => { @@ -111,10 +130,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { else if (de.data instanceof DragManager.AnnotationDragData) { if (de.data.dropDocument) { let dragDoc = de.data.dropDocument; - let zoom = NumCast(dragDoc.zoomBasis, 1); let [xp, yp] = this.getTransform().transformPoint(de.x, de.y); - let x = xp - de.data.xOffset / zoom; - let y = yp - de.data.yOffset / zoom; + let x = xp - de.data.xOffset; + let y = yp - de.data.yOffset; let dropX = NumCast(de.data.dropDocument.x); let dropY = NumCast(de.data.dropDocument.y); dragDoc.x = x + NumCast(dragDoc.x) - dropX; @@ -153,18 +171,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { if (!this.isAnnotationOverlay) { PDFMenu.Instance.fadeOut(true); let minx = docs.length ? NumCast(docs[0].x) : 0; - let maxx = docs.length ? NumCast(docs[0].width) / NumCast(docs[0].zoomBasis, 1) + minx : minx; + let maxx = docs.length ? NumCast(docs[0].width) + minx : minx; let miny = docs.length ? NumCast(docs[0].y) : 0; - let maxy = docs.length ? NumCast(docs[0].height) / NumCast(docs[0].zoomBasis, 1) + miny : miny; + let maxy = docs.length ? NumCast(docs[0].height) + miny : miny; let ranges = docs.filter(doc => doc).reduce((range, doc) => { let x = NumCast(doc.x); - let xe = x + NumCast(doc.width) / NumCast(doc.zoomBasis, 1); + let xe = x + NumCast(doc.width); let y = NumCast(doc.y); - let ye = y + NumCast(doc.height) / NumCast(doc.zoomBasis, 1); + let ye = y + NumCast(doc.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.extensionDoc.ink, InkField); + let ink = Cast(this.fieldExtensionDoc.ink, InkField); if (ink && ink.inkData) { ink.inkData.forEach((value: StrokeData, key: string) => { let bounds = InkingCanvas.StrokeRect(value); @@ -192,9 +210,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action onPointerWheel = (e: React.WheelEvent): void => { + if (BoolCast(this.props.Document.lockedPosition)) return; // if (!this.props.active()) { // return; // } + if (this.props.Document.type === "pdf") { + return; + } let childSelected = this.childDocs.some(doc => { var dv = DocumentManager.Instance.getDocumentView(doc); return dv && SelectionManager.IsSelected(dv) ? true : false; @@ -235,21 +257,17 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action setPan(panX: number, panY: number) { - + if (BoolCast(this.props.Document.lockedPosition)) return; this.props.Document.panTransformType = "None"; var scale = this.getLocalTransform().inverse().Scale; const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); const newPanY = Math.min((1 - 1 / scale) * this.nativeHeight, Math.max(0, panY)); - // this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX; - // this.props.Document.panY = this.isAnnotationOverlay ? newPanY : panY; - this.props.Document.panX = panX; + this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX; + this.props.Document.panY = this.isAnnotationOverlay ? newPanY : panY; + // this.props.Document.panX = panX; + // this.props.Document.panY = panY; if (this.props.Document.scrollY) { this.props.Document.scrollY = panY; - this.props.Document.panY = panY; - } - else { - - this.props.Document.panY = panY; } } @@ -277,6 +295,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const panY = this.Document.panY; const id = this.Document[Id]; const state = HistoryUtil.getState(); + state.initializers = state.initializers || {}; // TODO This technically isn't correct if type !== "doc", as // currently nothing is done, but we should probably push a new state if (state.type === "doc" && panX !== undefined && panY !== undefined) { @@ -293,10 +312,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } } SelectionManager.DeselectAll(); - const newPanX = NumCast(doc.x) + NumCast(doc.width) / NumCast(doc.zoomBasis, 1) / 2; - const newPanY = NumCast(doc.y) + NumCast(doc.height) / NumCast(doc.zoomBasis, 1) / 2; + const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2; + const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2; const newState = HistoryUtil.getState(); - newState.initializers[id] = { panX: newPanX, panY: newPanY }; + (newState.initializers || (newState.initializers = {}))[id] = { panX: newPanX, panY: newPanY }; HistoryUtil.pushState(newState); this.setPan(newPanX, newPanY); @@ -337,10 +356,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { getChildDocumentViewProps(childDocLayout: Doc): DocumentViewProps { - let resolvedDataDoc = this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined; + let self = this; + let resolvedDataDoc = !this.props.Document.isTemplate && this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined; let layoutDoc = Doc.expandTemplateLayout(childDocLayout, resolvedDataDoc); return { - DataDoc: resolvedDataDoc !== layoutDoc && resolvedDataDoc ? resolvedDataDoc : layoutDoc, + DataDoc: resolvedDataDoc !== layoutDoc && resolvedDataDoc ? resolvedDataDoc : undefined, Document: layoutDoc, addDocument: this.props.addDocument, removeDocument: this.props.removeDocument, @@ -385,16 +405,36 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { }; } + getCalculatedPositions(script: ScriptField, params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): { x?: number, y?: number, width?: number, height?: number, state?: any } { + const result = script.script.run(params); + if (!result.success) { + return {}; + } + return result.result === undefined ? {} : result.result; + } + @computed.struct get views() { let curPage = FieldValue(this.Document.curPage, -1); - let docviews = this.childDocs.reduce((prev, doc) => { + const initScript = this.Document.arrangeInit; + const script = this.Document.arrangeScript; + let state: any = undefined; + const docs = this.childDocs; + if (initScript) { + const initResult = initScript.script.run({ docs, collection: this.Document }); + if (initResult.success) { + state = initResult.result; + } + } + let docviews = docs.reduce((prev, doc) => { if (!(doc instanceof Doc)) return prev; var page = NumCast(doc.page, -1); if (Math.round(page) === Math.round(curPage) || page === -1) { let minim = BoolCast(doc.isMinimized, false); if (minim === undefined || !minim) { - prev.push(<CollectionFreeFormDocumentView key={doc[Id]} {...this.getChildDocumentViewProps(doc)} />); + const pos = script ? this.getCalculatedPositions(script, { doc, index: prev.length, collection: this.Document, docs, state }) : {}; + state = pos.state === undefined ? state : pos.state; + prev.push(<CollectionFreeFormDocumentView key={doc[Id]} x={pos.x} y={pos.y} width={pos.width} height={pos.height} {...this.getChildDocumentViewProps(doc)} />); } } return prev; @@ -415,24 +455,57 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { description: "Arrange contents in grid", event: async () => { const docs = await DocListCastAsync(this.Document[this.props.fieldKey]); - if (docs) { - let startX = this.Document.panX || 0; - let x = startX; - let y = this.Document.panY || 0; - let i = 0; - const width = Math.max(...docs.map(doc => NumCast(doc.width))); - const height = Math.max(...docs.map(doc => NumCast(doc.height))); - for (const doc of docs) { - doc.x = x; - doc.y = y; - x += width + 20; - if (++i === 6) { - i = 0; - x = startX; - y += height + 20; + UndoManager.RunInBatch(() => { + if (docs) { + let startX = this.Document.panX || 0; + let x = startX; + let y = this.Document.panY || 0; + let i = 0; + const width = Math.max(...docs.map(doc => NumCast(doc.width))); + const height = Math.max(...docs.map(doc => NumCast(doc.height))); + for (const doc of docs) { + doc.x = x; + doc.y = y; + x += width + 20; + if (++i === 6) { + i = 0; + x = startX; + y += height + 20; + } } } - } + }, "arrange contents"); + } + }); + ContextMenu.Instance.addItem({ + description: "Add freeform arrangement", + event: () => { + let addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => { + let overlayDisposer: () => void = emptyFunction; + const script = this.Document[key]; + let originalText: string | undefined = undefined; + if (script) originalText = script.script.originalScript; + // tslint:disable-next-line: no-unnecessary-callback-wrapper + let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => { + const script = CompileScript(text, { + params, + requiredType, + typecheck: false + }); + if (!script.compiled) { + onError(script.errors.map(error => error.messageText).join("\n")); + return; + } + const docs = DocListCast(this.Document[this.props.fieldKey]); + docs.map(d => d.transition = "transform 1s"); + this.Document[key] = new ScriptField(script); + overlayDisposer(); + setTimeout(() => docs.map(d => d.transition = undefined), 1200); + }} />; + overlayDisposer = OverlayView.Instance.addElement(scriptingBox, options); + }; + addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300 }, { collection: "Doc", docs: "Doc[]" }, undefined); + addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300 }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}"); } }); } @@ -442,21 +515,19 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { ...this.views ] render() { - const containerName = `collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`; const easing = () => this.props.Document.panTransformType === "Ease"; - if (this.props.fieldExt) Doc.UpdateDocumentExtensionForField(this.extensionDoc, this.props.fieldKey); + + Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey); return ( - <div className={containerName} ref={this.createDropTarget} onWheel={this.onPointerWheel} - style={{ borderRadius: "inherit" }} + <div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel} onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} onContextMenu={this.onContextMenu}> <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} isSelected={this.props.isSelected} addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}> <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} easing={easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> - <CollectionFreeFormLinksView {...this.props} key="freeformLinks"> - <InkingCanvas getScreenTransform={this.getTransform} Document={this.extensionDoc} inkFieldKey={this.props.fieldExt ? "ink" : this.props.fieldKey + "_ink"} > + <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.fieldExtensionDoc} inkFieldKey={"ink"} > {this.childViews} </InkingCanvas> </CollectionFreeFormLinksView> diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss index 6e8ec8662..9fc2e44fb 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss @@ -21,6 +21,6 @@ white-space:nowrap; } .marquee-legend::after { - content: "Press: c (collection), s (summary), r (replace) or Delete" + content: "Press: c (collection), s (summary), or Delete" } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 2a78cda2f..a4a6881f8 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,7 +1,7 @@ import * as htmlToImage from "html-to-image"; import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../../../new_fields/Doc"; +import { Doc, FieldResult } from "../../../../new_fields/Doc"; import { Id } from "../../../../new_fields/FieldSymbols"; import { InkField, StrokeData } from "../../../../new_fields/InkField"; import { List } from "../../../../new_fields/List"; @@ -44,14 +44,12 @@ export class MarqueeView extends React.Component<MarqueeViewProps> _commandExecuted = false; @action - cleanupInteractions = (all: boolean = false, rem_keydown: boolean = true) => { + cleanupInteractions = (all: boolean = false) => { if (all) { document.removeEventListener("pointerup", this.onPointerUp, true); document.removeEventListener("pointermove", this.onPointerMove, true); } - if (rem_keydown) { - document.removeEventListener("keydown", this.marqueeCommand, true); - } + document.removeEventListener("keydown", this.marqueeCommand, true); this._visible = false; } @@ -78,7 +76,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> } ns.map(line => { let indent = line.search(/\S|$/); - let newBox = Docs.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line }); + let newBox = Docs.Create.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line }); this.props.addDocument(newBox, false); y += 40 * this.props.getTransform().Scale; }); @@ -88,13 +86,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps> navigator.clipboard.readText().then(text => { let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); if (ns.length === 1 && text.startsWith("http")) { - this.props.addDocument(Docs.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }), false);// paste an image from its URL in the paste buffer + this.props.addDocument(Docs.Create.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }), false);// paste an image from its URL in the paste buffer } else { this.pasteTable(ns, x, y); } }); } else if (!e.ctrlKey) { - let newBox = Docs.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" }); + let newBox = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" }); newBox.proto!.autoHeight = true; this.props.addLiveTextDocument(newBox); } @@ -136,7 +134,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> doc.width = 200; docList.push(doc); } - let newCol = Docs.SchemaDocument([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 }); + let newCol = Docs.Create.SchemaDocument([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 }); this.props.addDocument(newCol, false); } @@ -191,12 +189,9 @@ export class MarqueeView extends React.Component<MarqueeViewProps> SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document); } this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]); - mselect.length ? this.cleanupInteractions(true, false) : this.cleanupInteractions(true); - } - else { - //console.log("invisible"); - this.cleanupInteractions(true); } + //console.log("invisible"); + this.cleanupInteractions(true); if (e.altKey) { e.preventDefault(); @@ -229,6 +224,18 @@ export class MarqueeView extends React.Component<MarqueeViewProps> return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) }; } + get ink() { + let container = this.props.container.Document; + let containerKey = this.props.container.props.fieldKey; + return Cast(container[containerKey + "_ink"], InkField); + } + + set ink(value: InkField | undefined) { + let container = Doc.GetProto(this.props.container.Document); + let containerKey = this.props.container.props.fieldKey; + container[containerKey + "_ink"] = value; + } + @undoBatch @action marqueeCommand = async (e: KeyboardEvent) => { @@ -240,15 +247,14 @@ export class MarqueeView extends React.Component<MarqueeViewProps> e.stopPropagation(); (e as any).propagationIsStopped = true; this.marqueeSelect().map(d => this.props.removeDocument(d)); - let ink = Cast(this.props.container.props.Document.ink, InkField); - if (ink) { - this.marqueeInkDelete(ink.inkData); + if (this.ink) { + this.marqueeInkDelete(this.ink.inkData); } SelectionManager.DeselectAll(); this.cleanupInteractions(false); e.stopPropagation(); } - if (e.key === "c" || e.key === "s" || e.key === "S" || e.key === "e" || e.key === "p") { + if (e.key === "c" || e.key === "s" || e.key === "S") { this._commandExecuted = true; e.stopPropagation(); e.preventDefault(); @@ -264,21 +270,18 @@ export class MarqueeView extends React.Component<MarqueeViewProps> return d; }); } - let ink = Cast(this.props.container.props.Document.ink, InkField); - let inkData = ink ? ink.inkData : undefined; - let zoomBasis = NumCast(this.props.container.props.Document.scale, 1); - let newCollection = Docs.FreeformDocument(selected, { + let inkData = this.ink ? this.ink.inkData : undefined; + let newCollection = Docs.Create.FreeformDocument(selected, { x: bounds.left, y: bounds.top, panX: 0, panY: 0, - borderRounding: e.key === "e" ? -1 : undefined, backgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white", width: bounds.width, height: bounds.height, - ink: inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined, - title: e.key === "s" || e.key === "S" ? "-summary-" : e.key === "p" ? "-summary-" : "a nested collection", + title: e.key === "s" || e.key === "S" ? "-summary-" : "a nested collection", }); + newCollection.data_ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined; this.marqueeInkDelete(inkData); if (e.key === "s") { @@ -289,14 +292,14 @@ export class MarqueeView extends React.Component<MarqueeViewProps> d.page = -1; return d; }); - let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); + let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); newCollection.proto!.summaryDoc = summary; selected = [newCollection]; newCollection.x = bounds.left + bounds.width; summary.proto!.subBulletDocs = new List<Doc>(selected); //summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight" summary.templates = new List<string>([Templates.Bullet.Layout]); - let container = Docs.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, title: "-summary-" }); + let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, title: "-summary-" }); container.viewType = CollectionViewType.Stacking; this.props.addLiveTextDocument(container); // }); @@ -308,7 +311,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> d.page = -1; return d; }); - let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); + let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); newCollection.proto!.summaryDoc = summary; selected = [newCollection]; newCollection.x = bounds.left + bounds.width; @@ -355,7 +358,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> let idata = new Map(); ink.forEach((value: StrokeData, key: string, map: any) => !InkingCanvas.IntersectStrokeRect(value, this.Bounds) && idata.set(key, value)); - Doc.SetOnPrototype(this.props.container.props.Document, "ink", new InkField(idata)); + this.ink = new InkField(idata); } } |
