diff options
Diffstat (limited to 'src/client/views/collections')
22 files changed, 1837 insertions, 1239 deletions
diff --git a/src/client/views/collections/CollectionBaseView.scss b/src/client/views/collections/CollectionBaseView.scss new file mode 100644 index 000000000..1f5acb96a --- /dev/null +++ b/src/client/views/collections/CollectionBaseView.scss @@ -0,0 +1,11 @@ +@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; +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 096c65092..e9a693f55 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -1,14 +1,16 @@ import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { ContextMenu } from '../ContextMenu'; -import { FieldViewProps } from '../nodes/FieldView'; -import { Cast, FieldValue, PromiseValue, NumCast } from '../../../new_fields/Types'; -import { Doc, FieldResult, Opt, DocListCast } from '../../../new_fields/Doc'; -import { listSpec } from '../../../new_fields/Schema'; +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 { DocumentManager } from '../../util/DocumentManager'; import { SelectionManager } from '../../util/SelectionManager'; -import { Id } from '../../../new_fields/FieldSymbols'; +import { ContextMenu } from '../ContextMenu'; +import { FieldViewProps } from '../nodes/FieldView'; +import './CollectionBaseView.scss'; export enum CollectionViewType { Invalid, @@ -34,7 +36,6 @@ export interface CollectionViewProps extends FieldViewProps { contentRef?: React.Ref<HTMLDivElement>; } - @observer export class CollectionBaseView extends React.Component<CollectionViewProps> { @observable private static _safeMode = false; @@ -58,10 +59,12 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { } } + @computed get dataDoc() { return Doc.resolvedFieldDataDoc(BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc ? this.props.DataDoc : this.props.Document : this.props.Document, this.props.fieldKey, this.props.fieldExt); } + @computed get dataField() { return this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; } + active = (): boolean => { var isSelected = this.props.isSelected(); - var topMost = this.props.isTopMost; - return isSelected || this._isChildActive || topMost; + return isSelected || this._isChildActive || this.props.renderDepth === 0 || BoolCast(this.props.Document.excludeFromLibrary); } //TODO should this be observable? @@ -71,85 +74,35 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { this.props.whenActiveChanged(isActive); } - createsCycle(documentToAdd: Doc, containerDocument: Doc): boolean { - if (!(documentToAdd instanceof Doc)) { - return false; - } - let data = DocListCast(documentToAdd.data); - for (const doc of data) { - if (this.createsCycle(doc, containerDocument)) { - return true; - } - } - let annots = DocListCast(documentToAdd.annotations); - for (const annot of annots) { - if (this.createsCycle(annot, containerDocument)) { - return true; - } - } - for (let containerProto: Opt<Doc> = containerDocument; containerProto !== undefined; containerProto = FieldValue(containerProto.proto)) { - if (containerProto[Id] === documentToAdd[Id]) { - return true; - } - } - return false; - } - @computed get isAnnotationOverlay() { return this.props.fieldKey === "annotations"; } - @action.bound addDocument(doc: Doc, allowDuplicates: boolean = false): boolean { - let props = this.props; - var curPage = NumCast(props.Document.curPage, -1); - Doc.SetOnPrototype(doc, "page", curPage); + var curPage = NumCast(this.props.Document.curPage, -1); + Doc.GetProto(doc).page = curPage; if (curPage >= 0) { - Doc.SetOnPrototype(doc, "annotationOn", props.Document); + Doc.GetProto(doc).annotationOn = this.props.Document; } - if (!this.createsCycle(doc, props.Document)) { - //TODO This won't create the field if it doesn't already exist - const childDocs = Cast(props.Document[props.fieldKey], listSpec(Doc)); - let alreadyAdded = true; - if (childDocs !== undefined) { - // if this is not the first document added to the collection - if (allowDuplicates || !childDocs.some(v => v instanceof Doc && v[Id] === doc[Id])) { - alreadyAdded = false; - childDocs.push(doc); - } - // if we're here, we've tried to add a duplicate - } else { - // if we are the first, set up a new list for this and all - // future child documents stored in the associated collection document at the fieldKey (likely .data) - // passed in via props - alreadyAdded = false; - Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new List([doc])); - } - // set the ZoomBasis only if hasn't already been set -- bcz: maybe set/resetting the ZoomBasis should be a parameter to addDocument? - if (!alreadyAdded && (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid)) { - let zoom = NumCast(this.props.Document.scale, 1); - // Doc.GetProto(doc).zoomBasis = zoom; + allowDuplicates = true; + const value = Cast(this.dataDoc[this.dataField], 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]); } return true; } @action.bound removeDocument(doc: Doc): boolean { - SelectionManager.DeselectAll(); - const props = this.props; + 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(props.Document[props.fieldKey], listSpec(Doc), []); - let index = -1; - for (let i = 0; i < value.length; i++) { - let v = value[i]; - if (v instanceof Doc && v[Id] === doc[Id]) { - index = i; - break; - } - } - PromiseValue(Cast(doc.annotationOn, Doc)).then((annotationOn) => { - if (annotationOn === props.Document) { - doc.annotationOn = undefined; - } - }); + const value = Cast(this.dataDoc[this.dataField], 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) + ); if (index !== -1) { value.splice(index, 1); @@ -163,11 +116,10 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { @action.bound moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean { - if (this.props.Document === targetCollection) { + if (Doc.AreProtosEqual(this.dataDoc, targetCollection)) { return true; } if (this.removeDocument(doc)) { - SelectionManager.DeselectAll(); return addDocument(doc); } return false; @@ -183,12 +135,11 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { }; const viewtype = this.collectionViewType; return ( - <div className={this.props.className || "collectionView-cont"} - style={{ borderRadius: "inherit", pointerEvents: "all" }} + <div id="collectionBaseView" className={this.props.className || "collectionView-cont"} onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}> {viewtype !== undefined ? this.props.children(viewtype, props) : (null)} </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 5270a4624..f44ab50c7 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,122 +1,39 @@ import 'golden-layout/src/css/goldenlayout-base.css'; import 'golden-layout/src/css/goldenlayout-dark-theme.css'; -import { action, observable, reaction, Lambda, IReactionDisposer } from "mobx"; +import { action, Lambda, observable, reaction } from "mobx"; import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; -import Measure, { ContentRect } from "react-measure"; +import Measure from "react-measure"; import * as GoldenLayout from "../../../client/goldenLayout"; -import { Doc, Field, Opt, DocListCast } from "../../../new_fields/Doc"; +import { Doc, DocListCast, Field, Opt } from "../../../new_fields/Doc"; +import { Id } from '../../../new_fields/FieldSymbols'; +import { FieldId } from "../../../new_fields/RefField"; import { listSpec } from "../../../new_fields/Schema"; -import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { emptyFunction, returnTrue, Utils } from "../../../Utils"; +import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; +import { emptyFunction, returnTrue, Utils, returnOne } from "../../../Utils"; import { DocServer } from "../../DocServer"; +import { DocumentManager } from '../../util/DocumentManager'; import { DragLinksAsDocuments, DragManager } from "../../util/DragManager"; +import { SelectionManager } from '../../util/SelectionManager'; +import { Transform } from '../../util/Transform'; import { undoBatch, UndoManager } from "../../util/UndoManager"; +import { DocumentView } from "../nodes/DocumentView"; +import { CollectionViewType } from './CollectionBaseView'; import "./CollectionDockingView.scss"; import { SubCollectionViewProps } from "./CollectionSubView"; -import React = require("react"); import { ParentDocSelector } from './ParentDocumentSelector'; -import { DocumentManager } from '../../util/DocumentManager'; -import { Id } from '../../../new_fields/FieldSymbols'; -import { DockedFrameRenderer } from './DockedFrameRenderer'; +import React = require("react"); +import { MainView } from '../MainView'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faFile } from '@fortawesome/free-solid-svg-icons'; +import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; +library.add(faFile); @observer export class CollectionDockingView extends React.Component<SubCollectionViewProps> { - public static TopLevel: CollectionDockingView; - private _goldenLayout: any = null; - private _containerRef = React.createRef<HTMLDivElement>(); - reactionDisposer?: IReactionDisposer; - _removedDocs: Doc[] = []; - private _flush: boolean = false; - private _ignoreStateChange = ""; - private _isPointerDown = false; - hack: boolean = false; - undohack: any = null; - - constructor(props: SubCollectionViewProps) { - super(props); - CollectionDockingView.TopLevel = this; - (window as any).React = React; - (window as any).ReactDOM = ReactDOM; - } - - componentDidMount: () => void = () => { - if (this._containerRef.current) { - this.reactionDisposer = reaction( - () => StrCast(this.props.Document.dockingConfig), - () => { - if (!this._goldenLayout || this._ignoreStateChange !== this.retrieveConfiguration()) { - // 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._ignoreStateChange = ""; - }, { fireImmediately: true }); - - // window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window - } - } - - componentWillUnmount: () => void = () => { - try { - this._goldenLayout.unbind('itemDropped', this.itemDropped); - this._goldenLayout.unbind('tabCreated', this.tabCreated); - this._goldenLayout.unbind('stackCreated', this.stackCreated); - this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed); - } catch (e) { - console.log("Unable to unbind Golden Layout event listener...", e); - } - if (this._goldenLayout) this._goldenLayout.destroy(); - this._goldenLayout = null; - - if (this.reactionDisposer) { - this.reactionDisposer(); - } - } - - setupGoldenLayout() { - var config = StrCast(this.props.Document.dockingConfig); - if (config) { - if (!this._goldenLayout) { - this.initializeConfiguration(config); - } - else { - if (config === this.retrieveConfiguration()) { - return; - } - try { - this._goldenLayout.unbind('itemDropped', this.itemDropped); - this._goldenLayout.unbind('tabCreated', this.tabCreated); - this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed); - this._goldenLayout.unbind('stackCreated', this.stackCreated); - } catch (e) { } - this._goldenLayout.destroy(); - this.initializeConfiguration(config); - } - this._goldenLayout.on('itemDropped', this.itemDropped); - this._goldenLayout.on('tabCreated', this.tabCreated); - this._goldenLayout.on('tabDestroyed', this.tabDestroyed); - this._goldenLayout.on('stackCreated', this.stackCreated); - this._goldenLayout.registerComponent('DocumentFrameRenderer', DockedFrameRenderer); - this._goldenLayout.container = this._containerRef.current; - if (this._goldenLayout.config.maximisedItemId === '__glMaximised') { - try { - this._goldenLayout.config.root.getItemsById(this._goldenLayout.config.maximisedItemId)[0].toggleMaximise(); - } catch (e) { - this._goldenLayout.config.maximisedItemId = null; - } - } - this._goldenLayout.init(); - } - } - - private makeDocConfig = (document: Doc, width?: number) => { - const config = CollectionDockingView.makeDocumentConfig(document, width); - (config.props as any).parent = this; - return config; - } - - public static makeDocumentConfig(document: Doc, width?: number) { + public static Instance: CollectionDockingView; + public static makeDocumentConfig(document: Doc, dataDoc: Doc | undefined, width?: number) { return { type: 'react-component', component: 'DocumentFrameRenderer', @@ -124,83 +41,147 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp width: width, props: { documentId: document[Id], + dataDocumentId: dataDoc ? dataDoc[Id] : "" + //collectionDockingView: CollectionDockingView.Instance } }; } - initializeConfiguration = (configText: string) => { - let configuration: any = JSON.parse(configText); - this.injectParentProp(configuration.content); - this._goldenLayout = new GoldenLayout(configuration); - } + private _goldenLayout: any = null; + private _containerRef = React.createRef<HTMLDivElement>(); + private _flush: boolean = false; + private _ignoreStateChange = ""; + private _isPointerDown = false; + private _maximizedSrc: Opt<DocumentView>; - retrieveConfiguration = () => { - let configuration: any = this._goldenLayout.toConfig(); - this.injectParentProp(configuration.content, true); - return JSON.stringify(configuration); + constructor(props: SubCollectionViewProps) { + super(props); + if (props.addDocTab === emptyFunction) CollectionDockingView.Instance = this; + //Why is this here? + (window as any).React = React; + (window as any).ReactDOM = ReactDOM; } - - injectParentProp = (contentArray: any[], reverse: boolean = false) => { - if (!contentArray || contentArray.length === 0) return; - contentArray.forEach(member => { - let baseCase = Object.keys(member).includes("props"); - if (!baseCase) { - this.injectParentProp(member.content, reverse); - } else { - reverse ? delete member.props.parent : member.props.parent = this; - } + hack: boolean = false; + undohack: any = null; + 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(); }); - } - - public StartOtherDrag(dragDocs: Doc[], e: any) { - this.hack = true; - this.undohack = UndoManager.StartBatch("goldenDrag"); - dragDocs.map(dragDoc => - CollectionDockingView.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener. - onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 })); + 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 static OpenFullScreen(document: Doc, dockingView: CollectionDockingView = CollectionDockingView.TopLevel) { - dockingView.openFullScreen(document); - } - - private openFullScreen = (document: Doc) => { + public OpenFullScreen(docView: DocumentView) { + let document = Doc.MakeAlias(docView.props.Document); + let dataDoc = docView.dataDoc; let newItemStackConfig = { type: 'stack', - content: [this.makeDocConfig(document)] + content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)] }; var docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout); this._goldenLayout.root.contentItems[0].addChild(docconfig); docconfig.callDownwards('_$init'); this._goldenLayout._$maximiseItem(docconfig); - this._ignoreStateChange = this.retrieveConfiguration(); + 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 => { + let retVal = false; + if (this._goldenLayout.root.contentItems[0].isRow) { + retVal = Array.from(this._goldenLayout.root.contentItems[0].contentItems).some((child: any) => { + if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" && + Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)) { + child.contentItems[0].remove(); + this.layoutChanged(document); + return true; + } else { + Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => { + if (Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) { + child.contentItems[j].remove(); + child.config.activeItemIndex = Math.max(child.contentItems.length - 1, 0); + let docs = Cast(this.props.Document.data, listSpec(Doc)); + docs && docs.indexOf(document) !== -1 && docs.splice(docs.indexOf(document), 1); + return true; + } + return false; + }); + } + return false; + }); + } + if (retVal) { + this.stateChanged(); + } + return retVal; + } + + @action + layoutChanged(removed?: Doc) { + this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]); + this._goldenLayout.emit('stateChanged'); + this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); + if (removed) CollectionDockingView.Instance._removedDocs.push(removed); this.stateChanged(); } // // Creates a vertical split on the right side of the docking view, and then adds the Document to that split // - public static AddRightSplit = (document: Doc, minimize: boolean = false, dockingView: CollectionDockingView = CollectionDockingView.TopLevel) => { - return dockingView.addRightSplit(document, minimize); - } - - private addRightSplit(document: Doc, minimize = false) { + @action + public AddRightSplit = (document: Doc, dataDoc: Doc | undefined, minimize: boolean = false) => { let docs = Cast(this.props.Document.data, listSpec(Doc)); if (docs) { docs.push(document); } let newItemStackConfig = { type: 'stack', - content: [this.makeDocConfig(document)] + content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)] }; var newContentItem = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout); - if (this._goldenLayout.root.contentItems[0].isRow) { + if (this._goldenLayout.root.contentItems.length === 0) { + this._goldenLayout.root.addChild(newContentItem); + } else if (this._goldenLayout.root.contentItems[0].isRow) { this._goldenLayout.root.contentItems[0].addChild(newContentItem); - } - else { + } else { var collayout = this._goldenLayout.root.contentItems[0]; var newRow = collayout.layoutManager.createContentItem({ type: "row" }, this._goldenLayout); collayout.parent.replaceChild(collayout, newRow); @@ -221,75 +202,99 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp return newContentItem; } - - public static AddTab = (stack: any, document: Doc, dockingView: CollectionDockingView = CollectionDockingView.TopLevel) => { - dockingView.addTab(stack, document); - } - - private addTab = (stack: any, document: Doc) => { + @action + public AddTab = (stack: any, document: Doc, dataDocument: Doc | undefined) => { let docs = Cast(this.props.Document.data, listSpec(Doc)); if (docs) { docs.push(document); } - let docContentConfig = this.makeDocConfig(document); + let docContentConfig = CollectionDockingView.makeDocumentConfig(document, dataDocument); var newContentItem = stack.layoutManager.createContentItem(docContentConfig, this._goldenLayout); stack.addChild(newContentItem.contentItems[0], undefined); this.layoutChanged(); } - @undoBatch - @action - public static CloseRightSplit = (document: Doc, dockingView: CollectionDockingView = CollectionDockingView.TopLevel): boolean => { - let retVal = false; - if (dockingView._goldenLayout.root.contentItems[0].isRow) { - retVal = Array.from(dockingView._goldenLayout.root.contentItems[0].contentItems).some((child: any) => { - if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" && - Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)) { - child.contentItems[0].remove(); - dockingView.layoutChanged(document); - return true; - } else { - Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => { - if (Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) { - child.contentItems[j].remove(); - child.config.activeItemIndex = Math.max(child.contentItems.length - 1, 0); - let docs = Cast(dockingView.props.Document.data, listSpec(Doc)); - docs && docs.indexOf(document) !== -1 && docs.splice(docs.indexOf(document), 1); - return true; - } - return false; - }); + setupGoldenLayout() { + var config = StrCast(this.props.Document.dockingConfig); + if (config) { + if (!this._goldenLayout) { + this._goldenLayout = new GoldenLayout(JSON.parse(config)); + } + else { + if (config === JSON.stringify(this._goldenLayout.toConfig())) { + return; } - return false; - }); - } - if (retVal) { - dockingView.stateChanged(); + try { + this._goldenLayout.unbind('itemDropped', this.itemDropped); + this._goldenLayout.unbind('tabCreated', this.tabCreated); + this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed); + this._goldenLayout.unbind('stackCreated', this.stackCreated); + } catch (e) { } + this._goldenLayout.destroy(); + this._goldenLayout = new GoldenLayout(JSON.parse(config)); + } + this._goldenLayout.on('itemDropped', this.itemDropped); + this._goldenLayout.on('tabCreated', this.tabCreated); + this._goldenLayout.on('tabDestroyed', this.tabDestroyed); + this._goldenLayout.on('stackCreated', this.stackCreated); + this._goldenLayout.registerComponent('DocumentFrameRenderer', DockedFrameRenderer); + this._goldenLayout.container = this._containerRef.current; + if (this._goldenLayout.config.maximisedItemId === '__glMaximised') { + try { + this._goldenLayout.config.root.getItemsById(this._goldenLayout.config.maximisedItemId)[0].toggleMaximise(); + } catch (e) { + this._goldenLayout.config.maximisedItemId = null; + } + } + this._goldenLayout.init(); } - return retVal; } + reactionDisposer?: Lambda; + componentDidMount: () => void = () => { + if (this._containerRef.current) { + this.reactionDisposer = reaction( + () => StrCast(this.props.Document.dockingConfig), + () => { + if (!this._goldenLayout || this._ignoreStateChange !== JSON.stringify(this._goldenLayout.toConfig())) { + // 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 }); - @action - layoutChanged(removed?: Doc) { - this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]); - this._goldenLayout.emit('stateChanged'); - this._ignoreStateChange = this.retrieveConfiguration(); - if (removed) CollectionDockingView.TopLevel._removedDocs.push(removed); - this.stateChanged(); + window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window + } } + 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); + this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed); + } catch (e) { + } + if (this._goldenLayout) this._goldenLayout.destroy(); + this._goldenLayout = null; + window.removeEventListener('resize', this.onResize); + + if (this.reactionDisposer) { + this.reactionDisposer(); + } + } @action - onResize = (size: ContentRect) => { + onResize = (event: any) => { + 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); - if (this._goldenLayout) { - this._goldenLayout.updateSize(size.offset!.width, size.offset!.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); @@ -298,6 +303,11 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp @action onPointerDown = (e: React.PointerEvent): void => { this._isPointerDown = true; + let onPointerUp = action(() => { + window.removeEventListener("pointerup", onPointerUp); + this._isPointerDown = false; + }); + window.addEventListener("pointerup", onPointerUp); var className = (e.target as any).className; if (className === "messageCounter") { e.stopPropagation(); @@ -306,48 +316,22 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp let y = e.clientY; let docid = (e.target as any).DashDocId; let tab = (e.target as any).parentElement as HTMLElement; - DocServer.getRefField(docid).then(action(async (sourceDoc: Opt<Field>) => + 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 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((f: Opt<Field>) => { - if (f instanceof Doc) { - DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f]), x, y, - { - handlers: { - dragComplete: emptyFunction, - }, - hideSource: false - }); - } - })); - } + } 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 stateChanged = () => { - let docs = Cast(CollectionDockingView.TopLevel.props.Document.data, listSpec(Doc)); - CollectionDockingView.TopLevel._removedDocs.map(theDoc => + let docs = Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc)); + CollectionDockingView.Instance._removedDocs.map(theDoc => docs && docs.indexOf(theDoc) !== -1 && docs.splice(docs.indexOf(theDoc), 1)); - CollectionDockingView.TopLevel._removedDocs.length = 0; - var json = this.retrieveConfiguration(); + CollectionDockingView.Instance._removedDocs.length = 0; + var json = JSON.stringify(this._goldenLayout.toConfig()); this.props.Document.dockingConfig = json; if (this.undohack && !this.hack) { this.undohack.end(); @@ -356,52 +340,61 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp this.hack = false; } - private itemDropped = () => { + itemDropped = () => { this.stateChanged(); } - private htmlToElement(html: string) { + htmlToElement(html: string) { var template = document.createElement('template'); html = html.trim(); // Never return a text node of whitespace as the result template.innerHTML = html; return template.content.firstChild; } - private tabCreated = async (tab: any) => { + tabCreated = async (tab: any) => { if (tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") { if (tab.contentItem.config.fixed) { tab.contentItem.parent.config.fixed = true; } - DocServer.getRefField(tab.contentItem.config.props.documentId).then(async doc => { - if (doc instanceof Doc) { - let counter: any = this.htmlToElement(`<span class="messageCounter">0</div>`); - tab.element.append(counter); - let upDiv = document.createElement("span"); - const stack = tab.contentItem.parent; - // shifts the focus to this tab when another tab is dragged over it - tab.element[0].onmouseenter = (e: any) => { - if (!this._isPointerDown) return; - var activeContentItem = tab.header.parent.getActiveContentItem(); - if (tab.contentItem !== activeContentItem) { - tab.header.parent.setActiveContentItem(tab.contentItem); - } - tab.setActive(true); - }; - tab.header.element[0].ondrop = (e: any) => { - console.log("DROPPPP THE BASS!", e); - }; - ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, location) => CollectionDockingView.AddTab(stack, doc)} />, upDiv); - tab.reactComponents = [upDiv]; - tab.element.append(upDiv); - counter.DashDocId = tab.contentItem.config.props.documentId; - tab.reactionDisposer = reaction(() => [doc.linkedFromDocs, doc.LinkedToDocs, doc.title], - () => { - counter.innerHTML = DocListCast(doc.linkedFromDocs).length + DocListCast(doc.linkedToDocs).length; - tab.titleElement[0].textContent = doc.title; - }, { fireImmediately: true }); - tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; - } - }); + 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) { + let dragSpan = document.createElement("span"); + dragSpan.style.position = "relative"; + dragSpan.style.bottom = "6px"; + dragSpan.style.paddingLeft = "4px"; + dragSpan.style.paddingRight = "2px"; + let upDiv = document.createElement("span"); + const stack = tab.contentItem.parent; + // shifts the focus to this tab when another tab is dragged over it + tab.element[0].onmouseenter = (e: any) => { + if (!this._isPointerDown) return; + var activeContentItem = tab.header.parent.getActiveContentItem(); + if (tab.contentItem !== activeContentItem) { + tab.header.parent.setActiveContentItem(tab.contentItem); + } + tab.setActive(true); + }; + ReactDOM.render(<span title="Drag as document" onPointerDown={ + e => { + e.preventDefault(); + e.stopPropagation(); + DragManager.StartDocumentDrag([dragSpan], new DragManager.DocumentDragData([doc], [dataDoc]), e.clientX, e.clientY, { + handlers: { dragComplete: emptyFunction }, + hideSource: false + }); + }}><FontAwesomeIcon icon="file" size="lg" /></span>, dragSpan); + ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={doc => CollectionDockingView.Instance.AddTab(stack, doc, dataDoc)} />, upDiv); + tab.reactComponents = [dragSpan, upDiv]; + tab.element.append(dragSpan); + tab.element.append(upDiv); + tab.reactionDisposer = reaction(() => [doc.title], + () => { + tab.titleElement[0].textContent = doc.title; + }, { fireImmediately: true }); + //TODO why can't this just be doc instead of the id? + tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; + } } tab.titleElement[0].Tab = tab; tab.closeElement.off('click') //unbind the current click handler @@ -409,35 +402,41 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp if (tab.reactionDisposer) { tab.reactionDisposer(); } - let doc = await DocServer.getRefField(tab.contentItem.config.props.documentId); + let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId); if (doc instanceof Doc) { let theDoc = doc; - CollectionDockingView.TopLevel._removedDocs.push(theDoc); + CollectionDockingView.Instance._removedDocs.push(theDoc); + if (CurrentUserUtils.UserDocument.recentlyClosed instanceof Doc) { + Doc.AddDocToList(CurrentUserUtils.UserDocument.recentlyClosed, "data", doc, undefined, true, true); + } + SelectionManager.DeselectAll(); } tab.contentItem.remove(); }); } - private tabDestroyed = (tab: any) => { + tabDestroyed = (tab: any) => { if (tab.reactComponents) { for (const ele of tab.reactComponents) { ReactDOM.unmountComponentAtNode(ele); } } } + _removedDocs: Doc[] = []; - private stackCreated = (stack: any) => { + stackCreated = (stack: any) => { //stack.header.controlsContainer.find('.lm_popout').hide(); + stack.header.element[0].style.backgroundColor = DocServer.Control.isReadOnly() ? "#228540" : undefined; stack.header.controlsContainer.find('.lm_close') //get the close icon .off('click') //unbind the current click handler .click(action(function () { //if (confirm('really close this?')) { stack.remove(); stack.contentItems.map(async (contentItem: any) => { - let doc = await DocServer.getRefField(contentItem.config.props.documentId); + let doc = await DocServer.GetRefField(contentItem.config.props.documentId); if (doc instanceof Doc) { let theDoc = doc; - CollectionDockingView.TopLevel._removedDocs.push(theDoc); + CollectionDockingView.Instance._removedDocs.push(theDoc); } }); //} @@ -452,16 +451,130 @@ 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 ( - <Measure onResize={this.onResize} offset> - {({ measureRef }) => ( - <div ref={measureRef} style={{ width: "100%", height: "100%" }}> + <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> ); } +} + +interface DockedFrameProps { + documentId: FieldId; + dataDocumentId: FieldId; + //collectionDockingView: CollectionDockingView +} +@observer +export class DockedFrameRenderer extends React.Component<DockedFrameProps> { + _mainCont = React.createRef<HTMLDivElement>(); + @observable private _panelWidth = 0; + @observable private _panelHeight = 0; + @observable private _document: Opt<Doc>; + @observable private _dataDoc: Opt<Doc>; + 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) { + return parent.parent.contentItems[1]; + } + return parent; + } + constructor(props: any) { + super(props); + DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => { + 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)); + } + })); + } + + nativeWidth = () => NumCast(this._document!.nativeWidth, this._panelWidth); + nativeHeight = () => { + let nh = NumCast(this._document!.nativeHeight, this._panelHeight); + let res = BoolCast(this._document!.ignoreAspect) ? this._panelHeight : nh; + return res; + } + contentScaling = () => { + const nativeH = this.nativeHeight(); + const nativeW = this.nativeWidth(); + let wscale = this._panelWidth / nativeW; + return wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale; + } + + ScreenToLocalTransform = () => { + if (this._mainCont.current && this._mainCont.current.children) { + let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current.children[0].firstChild as HTMLElement); + scale = Utils.GetScreenTransform(this._mainCont.current).scale; + return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale); + } + return Transform.Identity(); + } + get scaleToFitMultiplier() { + let docWidth = NumCast(this._document!.width); + let docHeight = NumCast(this._document!.height); + if (NumCast(this._document!.nativeWidth) || !docWidth || !this._panelWidth || !this._panelHeight) return 1; + if (StrCast(this._document!.layout).indexOf("Collection") === -1 || + !BoolCast(this._document!.fitToContents, false) || + NumCast(this._document!.viewType) !== CollectionViewType.Freeform) return 1; + let scaling = Math.max(1, this._panelWidth / docWidth * docHeight > this._panelHeight ? + this._panelHeight / docHeight : this._panelWidth / docWidth); + return scaling; + } + get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; } + + addDocTab = (doc: Doc, dataDoc: Doc | undefined, location: string) => { + if (doc.dockingConfig) { + MainView.Instance.openWorkspace(doc); + } else if (location === "onRight") { + CollectionDockingView.Instance.AddRightSplit(doc, dataDoc); + } else { + CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc); + } + } + get content() { + if (!this._document) { + return (null); + } + let resolvedDataDoc = this._document.layout instanceof Doc ? this._document : this._dataDoc; + 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={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} /> + </div >); + } + + render() { + let theContent = this.content; + return !this._document ? (null) : + <Measure offset onResize={action((r: any) => { this._panelWidth = r.offset.width; this._panelHeight = r.offset.height; })}> + {({ measureRef }) => <div ref={measureRef}> {theContent} </div>} + </Measure>; + } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx index 5e51437a4..31a73ab36 100644 --- a/src/client/views/collections/CollectionPDFView.tsx +++ b/src/client/views/collections/CollectionPDFView.tsx @@ -1,60 +1,74 @@ -import { action, observable } from "mobx"; +import { action, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; +import { WidthSym } from "../../../new_fields/Doc"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { NumCast } from "../../../new_fields/Types"; +import { emptyFunction } from "../../../Utils"; import { ContextMenu } from "../ContextMenu"; +import { FieldView, FieldViewProps } from "../nodes/FieldView"; +import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from "./CollectionBaseView"; +import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; import "./CollectionPDFView.scss"; import React = require("react"); -import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; -import { FieldView, FieldViewProps } from "../nodes/FieldView"; -import { CollectionRenderProps, CollectionBaseView, CollectionViewType } from "./CollectionBaseView"; -import { emptyFunction } from "../../../Utils"; -import { NumCast } from "../../../new_fields/Types"; -import { Id } from "../../../new_fields/FieldSymbols"; +import { PDFBox } from "../nodes/PDFBox"; @observer export class CollectionPDFView extends React.Component<FieldViewProps> { + private _pdfBox?: PDFBox; + private _reactionDisposer?: IReactionDisposer; + private _buttonTray: React.RefObject<HTMLDivElement>; + + constructor(props: FieldViewProps) { + super(props); + + this._buttonTray = React.createRef(); + } + + componentDidMount() { + this._reactionDisposer = reaction( + () => NumCast(this.props.Document.scrollY), + () => { + // 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); + } + // console.log(this.props.Document[HeightSym]()); + }, + { fireImmediately: true } + ); + } + + componentWillUnmount() { + this._reactionDisposer && this._reactionDisposer(); + } public static LayoutString(fieldKey: string = "data") { return FieldView.LayoutString(CollectionPDFView, fieldKey); } @observable _inThumb = false; - private set curPage(value: number) { this.props.Document.curPage = value; } + private set curPage(value: number) { this._pdfBox && this._pdfBox.GotoPage(value); } private get curPage() { return NumCast(this.props.Document.curPage, -1); } private get numPages() { return NumCast(this.props.Document.numPages); } - @action onPageBack = () => this.curPage > 1 ? (this.props.Document.curPage = this.curPage - 1) : -1; - @action onPageForward = () => this.curPage < this.numPages ? (this.props.Document.curPage = this.curPage + 1) : -1; + @action onPageBack = () => this._pdfBox && this._pdfBox.BackPage(); + @action onPageForward = () => this._pdfBox && this._pdfBox.ForwardPage(); - @action - onThumbDown = (e: React.PointerEvent) => { - document.addEventListener("pointermove", this.onThumbMove, false); - document.addEventListener("pointerup", this.onThumbUp, false); - e.stopPropagation(); - this._inThumb = true; - } - @action - onThumbMove = (e: PointerEvent) => { - let pso = (e.clientY - (e as any).target.parentElement.getBoundingClientRect().top) / (e as any).target.parentElement.getBoundingClientRect().height; - this.curPage = Math.trunc(Math.min(this.numPages, pso * this.numPages + 1)); - e.stopPropagation(); - } - @action - onThumbUp = (e: PointerEvent) => { - this._inThumb = false; - document.removeEventListener("pointermove", this.onThumbMove); - document.removeEventListener("pointerup", this.onThumbUp); - } nativeWidth = () => NumCast(this.props.Document.nativeWidth); nativeHeight = () => NumCast(this.props.Document.nativeHeight); private get uIButtons() { let ratio = (this.curPage - 1) / this.numPages * 100; return ( - <div className="collectionPdfView-buttonTray" key="tray" style={{ height: "100%" }}> + <div className="collectionPdfView-buttonTray" ref={this._buttonTray} key="tray" style={{ height: "100%" }}> <button className="collectionPdfView-backward" onClick={this.onPageBack}>{"<"}</button> <button className="collectionPdfView-forward" onClick={this.onPageForward}>{">"}</button> - <div className="collectionPdfView-slider" onPointerDown={this.onThumbDown} style={{ top: 60, left: -20, width: 50, height: `calc(100% - 80px)` }} > + {/* <div className="collectionPdfView-slider" onPointerDown={this.onThumbDown} style={{ top: 60, left: -20, width: 50, height: `calc(100% - 80px)` }} > <div className="collectionPdfView-thumb" onPointerDown={this.onThumbDown} style={{ top: `${ratio}%`, width: 50, height: 50 }} /> - </div> + </div> */} </div> ); } @@ -65,12 +79,15 @@ export class CollectionPDFView extends React.Component<FieldViewProps> { } } + setPdfBox = (pdfBox: PDFBox) => { this._pdfBox = pdfBox; }; + + private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => { let props = { ...this.props, ...renderProps }; return ( <> - <CollectionFreeFormView {...props} CollectionView={this} /> - {this.props.isSelected() ? this.uIButtons : (null)} + <CollectionFreeFormView {...props} setPdfBox={this.setPdfBox} CollectionView={this} /> + {renderProps.active() ? this.uIButtons : (null)} </> ); } diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 477879b79..cf39e26a8 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -2,36 +2,35 @@ import React = require("react"); import { library } from '@fortawesome/fontawesome-svg-core'; import { faCog, faPlus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, untracked, runInAction, trace } from "mobx"; +import { action, computed, observable, trace, untracked } from "mobx"; import { observer } from "mobx-react"; import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table"; -import { MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss'; import "react-table/react-table.css"; -import { emptyFunction, returnFalse, returnZero } from "../../../Utils"; -import { SetupDrag } from "../../util/DragManager"; +import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils"; +import { Doc, DocListCast, DocListCastAsync, Field } from "../../../new_fields/Doc"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { List } from "../../../new_fields/List"; +import { listSpec } from "../../../new_fields/Schema"; +import { Cast, FieldValue, NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; +import { Docs } from "../../documents/Documents"; +import { Gateway } from "../../northstar/manager/Gateway"; +import { SetupDrag, DragManager } from "../../util/DragManager"; import { CompileScript } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; -import { COLLECTION_BORDER_WIDTH } from "../../views/globalCssVariables.scss"; +import { COLLECTION_BORDER_WIDTH, MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss'; +import { ContextMenu } from "../ContextMenu"; import { anchorPoints, Flyout } from "../DocumentDecorations"; import '../DocumentDecorations.scss'; import { EditableView } from "../EditableView"; import { DocumentView } from "../nodes/DocumentView"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; +import { CollectionPDFView } from "./CollectionPDFView"; import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; -import { Opt, Field, Doc, DocListCastAsync, DocListCast } from "../../../new_fields/Doc"; -import { Cast, FieldValue, NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; -import { listSpec } from "../../../new_fields/Schema"; -import { List } from "../../../new_fields/List"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { Gateway } from "../../northstar/manager/Gateway"; -import { Docs } from "../../documents/Documents"; -import { ContextMenu } from "../ContextMenu"; -import { CollectionView } from "./CollectionView"; -import { CollectionPDFView } from "./CollectionPDFView"; import { CollectionVideoView } from "./CollectionVideoView"; -import { SelectionManager } from "../../util/SelectionManager"; +import { CollectionView } from "./CollectionView"; import { undoBatch } from "../../util/UndoManager"; +import { timesSeries } from "async"; library.add(faCog); @@ -90,8 +89,9 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { let columnDocs = DocListCast(schemaDoc.data); if (columnDocs) { let ddoc = columnDocs.find(doc => doc.title === columnName); - if (ddoc) + if (ddoc) { return ddoc; + } } } return this.props.Document; @@ -100,11 +100,13 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { renderCell = (rowProps: CellInfo) => { let props: FieldViewProps = { Document: rowProps.original, + DataDoc: rowProps.original, fieldKey: rowProps.column.id as string, + fieldExt: "", ContainingCollectionView: this.props.CollectionView, isSelected: returnFalse, select: emptyFunction, - isTopMost: false, + renderDepth: this.props.renderDepth + 1, selectOnLoad: false, ScreenToLocalTransform: Transform.Identity, focus: emptyFunction, @@ -116,9 +118,10 @@ 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); + let onItemDown = (e: React.PointerEvent) => { + (!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 }); if (!res.success) return false; @@ -232,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(); } } @@ -282,13 +284,13 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @computed get previewDocument(): Doc | undefined { - const children = DocListCast(this.props.Document[this.props.fieldKey]); - const selected = children.length > this._selectedIndex ? FieldValue(children[this._selectedIndex]) : undefined; - return selected ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined; + const selected = this.childDocs.length > this._selectedIndex ? this.childDocs[this._selectedIndex] : undefined; + let pdc = selected ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined; + return pdc; } getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate( - - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth); + - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth) get documentKeysCheckList() { @@ -331,13 +333,12 @@ 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} column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }} getTrProps={this.getTrProps} - /> + />; } @computed @@ -346,23 +347,38 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />; } + @computed get previewPanel() { - trace(); - return <CollectionSchemaPreview - Document={this.previewDocument} - width={this.previewWidth} - height={this.previewHeight} - getTransform={this.getPreviewTransform} - CollectionView={this.props.CollectionView} - addDocument={this.props.addDocument} - removeDocument={this.props.removeDocument} - active={this.props.active} - whenActiveChanged={this.props.whenActiveChanged} - addDocTab={this.props.addDocTab} - setPreviewScript={this.setPreviewScript} - previewScript={this.previewScript} - /> + // let layoutDoc = this.previewDocument; + // let resolvedDataDoc = (layoutDoc !== this.props.DataDoc) ? this.props.DataDoc : undefined; + // if (layoutDoc && !(Cast(layoutDoc.layout, Doc) instanceof Doc) && + // resolvedDataDoc && resolvedDataDoc !== layoutDoc) { + // // ... so change the layout to be an expanded view of the template layout. This allows the view override the template's properties and be referenceable as its own document. + // layoutDoc = Doc.expandTemplateLayout(layoutDoc, resolvedDataDoc); + // } + + let layoutDoc = this.previewDocument ? Doc.expandTemplateLayout(this.previewDocument, this.props.DataDoc) : undefined; + return <div ref={this.createTarget}> + <CollectionSchemaPreview + Document={layoutDoc} + DataDocument={this.previewDocument !== this.props.DataDoc ? this.props.DataDoc : undefined} + childDocs={this.childDocs} + renderDepth={this.props.renderDepth} + width={this.previewWidth} + height={this.previewHeight} + getTransform={this.getPreviewTransform} + CollectionView={this.props.CollectionView} + moveDocument={this.props.moveDocument} + addDocument={this.props.addDocument} + removeDocument={this.props.removeDocument} + active={this.props.active} + whenActiveChanged={this.props.whenActiveChanged} + addDocTab={this.props.addDocTab} + setPreviewScript={this.setPreviewScript} + previewScript={this.previewScript} + /> + </div>; } @action setPreviewScript = (script: string) => { @@ -370,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}> @@ -384,21 +399,28 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } interface CollectionSchemaPreviewProps { Document?: Doc; + DataDocument?: Doc; + childDocs?: Doc[]; + fitToBox?: () => number[]; + renderDepth: number; width: () => number; height: () => number; - CollectionView: CollectionView | CollectionPDFView | CollectionVideoView; + CollectionView?: CollectionView | CollectionPDFView | CollectionVideoView; getTransform: () => Transform; addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; + moveDocument: (document: Doc, target: Doc, addDoc: ((doc: Doc) => boolean)) => boolean; removeDocument: (document: Doc) => boolean; active: () => boolean; whenActiveChanged: (isActive: boolean) => void; - addDocTab: (document: Doc, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; setPreviewScript: (script: string) => void; previewScript?: string; } @observer export class CollectionSchemaPreview extends React.Component<CollectionSchemaPreviewProps>{ + private dropDisposer?: DragManager.DragDropDisposer; + _mainCont?: HTMLDivElement; private get nativeWidth() { return NumCast(this.props.Document!.nativeWidth, this.props.width()); } private get nativeHeight() { return NumCast(this.props.Document!.nativeHeight, this.props.height()); } private contentScaling = () => { @@ -408,49 +430,79 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre } return wscale; } - private PanelWidth = () => this.nativeWidth * this.contentScaling(); - private PanelHeight = () => this.nativeHeight * this.contentScaling(); - private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, 0).scale(1 / this.contentScaling()) - get centeringOffset() { return (this.props.width() - this.nativeWidth * this.contentScaling()) / 2; } - @action - onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this.props.setPreviewScript(e.currentTarget.value); + protected createDropTarget = (ele: HTMLDivElement) => { } + private createTarget = (ele: HTMLDivElement) => { + this._mainCont = ele; + this.dropDisposer && this.dropDisposer(); + if (ele) { + this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + } + } + @undoBatch @action - public collapseToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => { - SelectionManager.DeselectAll(); - if (expandedDocs) { - let isMinimized: boolean | undefined; - expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => { - if (isMinimized === undefined) { - isMinimized = BoolCast(maximizedDoc.isMinimized, false); - } - maximizedDoc.isMinimized = !isMinimized; + drop = (e: Event, de: DragManager.DropEvent) => { + if (de.data instanceof DragManager.DocumentDragData) { + let docDrag = de.data; + this.props.childDocs && this.props.childDocs.map(otherdoc => { + Doc.GetProto(otherdoc).layout = Doc.MakeDelegate(docDrag.draggedDocuments[0]); }); + e.stopPropagation(); } + return true; + } + private PanelWidth = () => this.nativeWidth ? this.nativeWidth * this.contentScaling() : this.props.width(); + private PanelHeight = () => this.nativeHeight ? this.nativeHeight * this.contentScaling() : this.props.height(); + private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, 0).scale(1 / this.contentScaling()); + get centeringOffset() { return this.nativeWidth ? (this.props.width() - this.nativeWidth * this.contentScaling()) / 2 : 0; } + @action + 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() { - trace(); - console.log(this.props.Document); let input = this.props.previewScript === undefined ? (null) : - <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)` }} />; - return (<div className="collectionSchemaView-previewRegion" style={{ width: this.props.width() }}> + <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)` }}> - <DocumentView Document={this.props.Document} isTopMost={false} selectOnLoad={false} - addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} + <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} + addDocument={this.props.addDocument} + removeDocument={this.props.removeDocument} + moveDocument={this.props.moveDocument} ScreenToLocalTransform={this.getTransform} ContentScaling={this.contentScaling} - PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight} + PanelWidth={this.PanelWidth} + PanelHeight={this.PanelHeight} ContainingCollectionView={this.props.CollectionView} focus={emptyFunction} parentActive={this.props.active} whenActiveChanged={this.props.whenActiveChanged} bringToFront={emptyFunction} addDocTab={this.props.addDocTab} - collapseToPoint={this.collapseToPoint} + zoomToScale={emptyFunction} + getScale={returnOne} /> </div>)} {input} diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 4d84aaaa9..bc733f152 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -1,19 +1,6 @@ @import "../globalCssVariables"; - .collectionStackingView { - top: 0; - left: 0; - display: flex; - flex-direction: column; - width: 100%; - position: absolute; overflow-y: auto; - 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; .collectionStackingView-docView-container { width: 45%; @@ -29,13 +16,26 @@ align-items: center; } - .collectionStackingView-masonrySingle, .collectionStackingView-masonryGrid{ + .collectionStackingView-masonrySingle, .collectionStackingView-masonryGrid { width:100%; height:100%; position: absolute; - } - .collectionStackingView-masonryGrid { display:grid; + top: 0; + 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 { @@ -48,4 +48,24 @@ background: $dark-color; color: $light-color; } + + .collectionStackingView-columnDragger { + width: 15; + height: 15; + position: absolute; + margin-left: -5; + } + + + .collectionStackingView-columnDoc, + .collectionStackingView-masonryDoc { + margin-left: auto; + margin-right: auto; + } + + .collectionStackingView-masonryDoc { + transform-origin: top left; + grid-column-end: span 1; + height: 100%; + } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index da7ea50c6..416c536f6 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -1,40 +1,44 @@ import React = require("react"); -import { action, computed, IReactionDisposer, reaction } from "mobx"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +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"; -import { BoolCast, NumCast } from "../../../new_fields/Types"; -import { emptyFunction, returnOne, Utils } from "../../../Utils"; -import { SelectionManager } from "../../util/SelectionManager"; -import { undoBatch } from "../../util/UndoManager"; -import { DocumentView } from "../nodes/DocumentView"; +import { BoolCast, NumCast, Cast } from "../../../new_fields/Types"; +import { emptyFunction, Utils } from "../../../Utils"; +import { ContextMenu } from "../ContextMenu"; import { CollectionSchemaPreview } from "./CollectionSchemaView"; import "./CollectionStackingView.scss"; import { CollectionSubView } from "./CollectionSubView"; +import { undoBatch } from "../../util/UndoManager"; +import { DragManager } from "../../util/DragManager"; @observer export class CollectionStackingView extends CollectionSubView(doc => doc) { _masonryGridRef: HTMLDivElement | null = null; + _draggerRef = React.createRef<HTMLDivElement>(); _heightDisposer?: IReactionDisposer; - get gridGap() { return 10; } - get gridSize() { return 20; } - get singleColumn() { return BoolCast(this.props.Document.singleColumn, true); } - get columnWidth() { return this.singleColumn ? this.props.PanelWidth() - 4 * this.gridGap : NumCast(this.props.Document.columnWidth, 250); } + _gridSize = 1; + @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.childDocs.map(d => [d.height, d.width, d.zoomBasis, d.nativeHeight, d.nativeWidth, d.isMinimized]), this.columnWidth, this.props.PanelHeight()], - () => { - if (this.singleColumn) { - this.props.Document.height = this.childDocs.filter(d => !d.isMinimized).reduce((height, d) => { - let hgt = d[HeightSym](); - let wid = d[WidthSym](); - let nw = NumCast(d.nativeWidth); - let nh = NumCast(d.nativeHeight); - if (nw && nh) hgt = nh / nw * Math.min(this.columnWidth, wid); - return height + hgt + 2 * this.gridGap; - }, this.gridGap * 2); - } - }, { fireImmediately: true }); + 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])], + () => this.singleColumn && + (this.props.Document.height = this.filteredChildren.reduce((height, d, i) => + height + this.singleColDocHeight(d) + (i === this.filteredChildren.length - 1 ? this.yMargin : this.gridGap), this.yMargin)) + , { fireImmediately: true }); } componentWillUnmount() { if (this._heightDisposer) this._heightDisposer(); @@ -42,136 +46,203 @@ 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; + return this.props.removeDocument(doc) && addDocument(doc); } - getDocTransform(doc: Doc, dref: HTMLDivElement) { - let { scale, translateX, translateY } = Utils.GetScreenTransform(dref); + getSingleDocTransform(doc: Doc, ind: number, width: number) { + let localY = this.filteredChildren.reduce((height, d, i) => + height + (i < ind ? this.singleColDocHeight(d) + this.gridGap : 0), this.yMargin); + let translate = this.props.ScreenToLocalTransform().inverse().transformPoint((this.props.PanelWidth() - width) / 2, localY); let outerXf = Utils.GetScreenTransform(this._masonryGridRef!); - let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); + let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translate[0], outerXf.translateY - translate[1]); return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]).scale(NumCast(doc.width, 1) / this.columnWidth); } createRef = (ele: HTMLDivElement | null) => { this._masonryGridRef = ele; this.createDropTarget(ele!); } - @undoBatch - @action - public collapseToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => { - SelectionManager.DeselectAll(); - if (expandedDocs) { - let isMinimized: boolean | undefined; - expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => { - if (isMinimized === undefined) { - isMinimized = BoolCast(maximizedDoc.isMinimized, false); - } - maximizedDoc.isMinimized = !isMinimized; - }); - } - } @computed get singleColumnChildren() { - return this.childDocs.filter(d => !d.isMinimized).map((d, i) => { - let dref = React.createRef<HTMLDivElement>(); - let script = undefined; - let colWidth = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth; - let margin = colWidth() < this.columnWidth ? "auto" : undefined; - let rowHeight = () => { - let hgt = d[HeightSym](); - let nw = NumCast(d.nativeWidth); - let nh = NumCast(d.nativeHeight); - if (nw && nh) hgt = nh / nw * colWidth(); - return hgt; - } - let dxf = () => this.getDocTransform(d, dref.current!).scale(this.columnWidth / d[WidthSym]()); - return <div className="collectionStackingView-masonryDoc" + return this.filteredChildren.map((d, i) => { + let layoutDoc = Doc.expandTemplateLayout(d, this.props.DataDoc); + let width = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth; + let height = () => this.singleColDocHeight(layoutDoc); + let dxf = () => this.getSingleDocTransform(layoutDoc, i, width()).scale(this.columnWidth / d[WidthSym]()); + let gap = i === 0 ? 0 : this.gridGap; + return <div className="collectionStackingView-columnDoc" key={d[Id]} - ref={dref} - style={{ marginTop: `${i ? 2 * this.gridGap : 0}px`, width: colWidth(), height: rowHeight(), marginLeft: margin, marginRight: margin }} > + style={{ width: width(), display: "inline-block", marginTop: gap, height: `${height() / (this.props.Document[HeightSym]() - 2 * this.yMargin) * 100}%` }} > <CollectionSchemaPreview - Document={d} - width={colWidth} - height={rowHeight} + 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={script}> + previewScript={undefined}> </CollectionSchemaPreview> </div>; }); } + 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); + } + docXfs: any[] = []; @computed get children() { - return this.childDocs.filter(d => !d.isMinimized).map(d => { + this.docXfs.length = 0; + return this.filteredChildren.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!); - let colSpan = Math.ceil(Math.min(d[WidthSym](), this.columnWidth + this.gridGap) / (this.gridSize + this.gridGap)); - let rowSpan = Math.ceil((this.columnWidth / d[WidthSym]() * d[HeightSym]() + this.gridGap) / (this.gridSize + this.gridGap)); - let childFocus = (doc: Doc) => { - doc.libraryBrush = true; - this.props.focus(this.props.Document); // just focus on this collection, not the underlying document because the API doesn't support adding an offset to focus on and we can't pan zoom our contents to be centered. - } + 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)); + this.docXfs.push({ dxf: dxf, width: width, height: height }); return (<div className="collectionStackingView-masonryDoc" key={d[Id]} ref={dref} - style={{ - width: NumCast(d.nativeWidth, d[WidthSym]()), - height: NumCast(d.nativeHeight, d[HeightSym]()), - transformOrigin: "top left", - gridRowEnd: `span ${rowSpan}`, - gridColumnEnd: `span ${colSpan}`, - transform: `scale(${this.columnWidth / NumCast(d.nativeWidth, d[WidthSym]())}, ${this.columnWidth / NumCast(d.nativeWidth, d[WidthSym]())})` - }} > - <DocumentView key={d[Id]} Document={d} + 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} - moveDocument={this.moveDocument} - ContainingCollectionView={this.props.CollectionView} - isTopMost={false} - ScreenToLocalTransform={dxf} - focus={childFocus} - ContentScaling={returnOne} - PanelWidth={d[WidthSym]} - PanelHeight={d[HeightSym]} - selectOnLoad={false} - parentActive={this.props.active} + getTransform={dxf} + width={width} + height={height} + active={this.props.active} addDocTab={this.props.addDocTab} - bringToFront={emptyFunction} whenActiveChanged={this.props.whenActiveChanged} - collapseToPoint={this.collapseToPoint} - /> + setPreviewScript={emptyFunction} + previewScript={undefined}> + </CollectionSchemaPreview> </div>); - }) + }); + } + + _columnStart: number = 0; + columnDividerDown = (e: React.PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + document.addEventListener("pointermove", this.onDividerMove); + document.addEventListener('pointerup', this.onDividerUp); + this._columnStart = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; + } + @action + onDividerMove = (e: PointerEvent): void => { + let dragPos = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; + let delta = dragPos - this._columnStart; + this._columnStart = dragPos; + + this.props.Document.columnWidth = this.columnWidth + delta; + } + + @action + onDividerUp = (e: PointerEvent): void => { + document.removeEventListener("pointermove", this.onDividerMove); + document.removeEventListener('pointerup', this.onDividerUp); + } + + @computed get columnDragger() { + return <div className="collectionStackingView-columnDragger" onPointerDown={this.columnDividerDown} ref={this._draggerRef} style={{ left: `${this.columnWidth + this.xMargin}px` }} > + <FontAwesomeIcon icon={"caret-down"} /> + </div>; + } + onContextMenu = (e: React.MouseEvent): void => { + if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 + ContextMenu.Instance.addItem({ + description: "Toggle multi-column", + event: () => this.props.Document.singleColumn = !BoolCast(this.props.Document.singleColumn, true), icon: "file-pdf" + }); + } + } + + @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 leftMargin = 2 * this.gridGap; - let topMargin = 2 * this.gridGap; - let itemCols = Math.ceil(this.columnWidth / (this.gridSize + this.gridGap)); - let cells = Math.floor((this.props.PanelWidth() - leftMargin) / (itemCols * (this.gridSize + this.gridGap))); + 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" style={{ height: "100%" }} - ref={this.createRef} 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: `${topMargin}px 0px 0px ${leftMargin}px`, - width: this.singleColumn ? "100%" : `${cells * itemCols * (this.gridSize + this.gridGap) + leftMargin}`, + padding: this.singleColumn ? `${this.yMargin}px ${this.xMargin}px ${this.yMargin}px ${this.xMargin}px` : `${this.yMargin}px ${this.xMargin}px`, + margin: "auto", + width: this.singleColumn ? undefined : `${cols * (this.columnWidth + this.gridGap) + 2 * this.xMargin - this.gridGap}px`, height: "100%", - overflow: "hidden", - marginRight: "auto", position: "relative", gridGap: this.gridGap, - gridTemplateColumns: this.singleColumn ? undefined : `repeat(auto-fill, minmax(${this.gridSize}px,1fr))`, - gridAutoRows: this.singleColumn ? undefined : `${this.gridSize}px` + gridTemplateColumns: this.singleColumn ? undefined : templatecols, + gridAutoRows: this.singleColumn ? undefined : `${this._gridSize}px` }} > {this.singleColumn ? this.singleColumnChildren : 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 36e276d13..a7614b605 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,23 +1,25 @@ -import { action, runInAction } from "mobx"; -import React = require("react"); -import { undoBatch, UndoManager } from "../../util/UndoManager"; -import { DragManager } from "../../util/DragManager"; -import { Docs, DocumentOptions } from "../../documents/Documents"; -import { RouteStore } from "../../../server/RouteStore"; +import { action, computed } from "mobx"; +import * as rp from 'request-promise'; +import CursorField from "../../../new_fields/CursorField"; +import { Doc, DocListCast, Opt } from "../../../new_fields/Doc"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { List } from "../../../new_fields/List"; +import { listSpec } from "../../../new_fields/Schema"; +import { BoolCast, Cast } from "../../../new_fields/Types"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; +import { RouteStore } from "../../../server/RouteStore"; +import { DocServer } from "../../DocServer"; +import { Docs, DocumentOptions } from "../../documents/Documents"; +import { DragManager } from "../../util/DragManager"; +import { undoBatch, UndoManager } from "../../util/UndoManager"; +import { DocComponent } from "../DocComponent"; import { FieldViewProps } from "../nodes/FieldView"; -import * as rp from 'request-promise'; -import { CollectionView } from "./CollectionView"; +import { FormattedTextBox } from "../nodes/FormattedTextBox"; import { CollectionPDFView } from "./CollectionPDFView"; import { CollectionVideoView } from "./CollectionVideoView"; -import { Doc, Opt, FieldResult, DocListCast } from "../../../new_fields/Doc"; -import { DocComponent } from "../DocComponent"; -import { listSpec } from "../../../new_fields/Schema"; -import { Cast, PromiseValue, FieldValue, ListSpec } from "../../../new_fields/Types"; -import { List } from "../../../new_fields/List"; -import { DocServer } from "../../DocServer"; -import CursorField from "../../../new_fields/CursorField"; -import { DocumentManager } from "../../util/DocumentManager"; +import { CollectionView } from "./CollectionView"; +import React = require("react"); +import { MainView } from "../MainView"; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; @@ -35,9 +37,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) { private dropDisposer?: DragManager.DragDropDisposer; protected createDropTarget = (ele: HTMLDivElement) => { - if (this.dropDisposer) { - this.dropDisposer(); - } + this.dropDisposer && this.dropDisposer(); if (ele) { this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); } @@ -46,10 +46,18 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { this.createDropTarget(ele); } + @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]); + } + 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 DocListCast(this.props.Document[this.props.fieldKey]); + return Cast((BoolCast(this.props.Document.isTemplate) ? this.extensionDoc : this.props.Document)[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey], listSpec(Doc)); } @action @@ -60,10 +68,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>(); @@ -81,79 +96,29 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { @action protected drop(e: Event, de: DragManager.DropEvent): boolean { if (de.data instanceof DragManager.DocumentDragData) { - if (de.data.dropAction || de.data.userDropAction) { - ["width", "height", "curPage"].map(key => - de.data.draggedDocuments.map((draggedDocument: Doc, i: number) => - PromiseValue(Cast(draggedDocument[key], "number")).then(f => f && (de.data.droppedDocuments[i][key] = f)))); - } let added = false; if (de.data.dropAction || de.data.userDropAction) { - added = de.data.droppedDocuments.reduce((added: boolean, d) => { - let moved = this.props.addDocument(d); - return moved || added; - }, false); + added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false); } else if (de.data.moveDocument) { - const move = de.data.moveDocument; - added = de.data.droppedDocuments.reduce((added: boolean, d) => { - let moved = move(d, this.props.Document, this.props.addDocument); - return moved || added; - }, false); + 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); } else { - added = de.data.droppedDocuments.reduce((added: boolean, d) => { - let moved = this.props.addDocument(d); - return moved || added; - }, false); + added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false); } e.stopPropagation(); return added; } - 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.Create.ImageDocument; - } - if (type.indexOf("video") !== -1) { - ctor = Docs.Create.VideoDocument; - } - if (type.indexOf("audio") !== -1) { - ctor = Docs.Create.AudioDocument; - } - if (type.indexOf("pdf") !== -1) { - ctor = Docs.Create.PdfDocument; - options.nativeWidth = 1200; - } - if (type.indexOf("excel") !== -1) { - ctor = Docs.Create.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.Create.WebDocument; - options = { height: options.width, ...options, title: path, nativeWidth: undefined }; + else if (de.data instanceof DragManager.AnnotationDragData) { + e.stopPropagation(); + return this.props.addDocument(de.data.dropDocument); } - return ctor ? ctor(path, options) : undefined; + return false; } @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; @@ -167,17 +132,50 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { e.stopPropagation(); e.preventDefault(); - if (html && html.indexOf(document.location.origin)) { // prosemirror text containing link to dash document - let start = html.indexOf(window.location.origin); - let path = html.substr(start, html.length - start); - let docid = path.substr(0, path.indexOf("\">")).replace(DocServer.Util.prepend("/doc/"), "").split("?")[0]; - DocServer.getRefField(docid).then(f => (f instanceof Doc) && this.props.addDocument(f, false)); + if (html && FormattedTextBox.IsFragment(html)) { + let href = FormattedTextBox.GetHref(html); + if (href) { + let docid = FormattedTextBox.GetDocFromUrl(href); + if (docid) { // prosemirror text containing link to dash document + DocServer.GetRefField(docid).then(f => { + if (f instanceof Doc) { + if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView + (f instanceof Doc) && this.props.addDocument(f, false); + } + }); + } else { + this.props.addDocument && this.props.addDocument(Docs.Create.WebDocument(href, options)); + } + } else if (text) { + this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument({ ...options, width: 100, height: 25, documentText: "@@@" + text }), false); + } return; } - if (html && html.indexOf("<img") !== 0 && !html.startsWith("<a")) { - let htmlDoc = Docs.Create.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text }); - this.props.addDocument(htmlDoc, false); - return; + if (html && !html.startsWith("<a")) { + let tags = html.split("<"); + if (tags[0] === "") tags.splice(0, 1); + 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.Create.ImageDocument(split, { ...options, width: 300 }); + this.props.addDocument(doc, false); + return; + } else { + let path = window.location.origin + "/doc/"; + if (text.startsWith(path)) { + let docid = text.replace(DocServer.prepend("/doc/"), "").split("?")[0]; + DocServer.GetRefField(docid).then(f => { + if (f instanceof Doc) { + if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView + (f instanceof Doc) && this.props.addDocument(f, false); + } + }); + } else { + 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/"); @@ -194,11 +192,11 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { if (item.kind === "string" && item.type.indexOf("uri") !== -1) { let str: string; let prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve)) - .then(action((s: string) => rp.head(DocServer.Util.prepend(RouteStore.corsProxy + "/" + (str = s))))) + .then(action((s: string) => rp.head(DocServer.prepend(RouteStore.corsProxy + "/" + (str = s))))) .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: 300 }) .then(doc => doc && this.props.addDocument(doc, false)); } }); @@ -219,10 +217,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: 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); @@ -230,7 +227,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.scss b/src/client/views/collections/CollectionTreeView.scss index 458030b28..5205f4313 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -4,60 +4,33 @@ border-width: $COLLECTION_BORDER_WIDTH; border-color: transparent; border-style: solid; - border-radius: $border-radius; + border-radius: inherit; box-sizing: border-box; height: 100%; - padding: 20px; + padding-top: 20px; padding-left: 10px; padding-right: 0px; background: $light-color-secondary; font-size: 13px; - overflow: scroll; + overflow: auto; ul { list-style: none; padding-left: 20px; } - li { - margin: 5px 0; - } - .no-indent { padding-left: 0; } .bullet { - float: left; position: relative; width: 15px; - display: block; color: $intermediate-color; - margin-top: 3px; + margin-top: 4px; transform: scale(1.3, 1.3); } - - .docContainer { - margin-left: 10px; - display: block; - // width:100%;//width: max-content; - } - - .docContainer:hover { - .treeViewItem-openRight { - display: inline-block; - height:13px; - // display: inline; - svg { - display:block; - padding:0px; - margin: 0px; - } - } - } - - .editableView-container { font-weight: bold; } @@ -70,31 +43,72 @@ display: inline; } - .treeViewItem-openRight { - margin-left: 5px; - display: none; - } - - .docContainer:hover { - .delete-button { - display: inline; - // width: auto; - } - } - - .coll-title { - width: max-content; + .editableView-input, .editableView-container-editing { display: block; + text-overflow: ellipsis; font-size: 24px; + white-space: nowrap; } - - .collection-child { - margin-top: 10px; - margin-bottom: 10px; - } - +} +.collectionTreeView-keyHeader { + font-style: italic; + font-size: 8pt; + margin-left: 3px; + display:none; + background: lightgray; +} + +.collectionTreeView-subtitle { + font-style: italic; + font-size: 8pt; + color: $intermediate-color; +} + +.docContainer { + position: relative; + text-overflow: ellipsis; + white-space: pre-wrap; + overflow: hidden; + // width:100%;//width: max-content; + +} +.treeViewItem-openRight { + display: none; +} + +.treeViewItem-border { + display:inherit; + border-left: dashed 1px #00000042; +} + +.treeViewItem-header:hover { .collectionTreeView-keyHeader { - font-style: italic; - font-size: 8pt; + display:inherit; + } + .treeViewItem-openRight { + display: inline-block; + height:13px; + margin-top:2px; + margin-left: 5px; + // display: inline; + svg { + display:block; + padding:0px; + margin: 0px; + } } +} + +.treeViewItem-header { + border: transparent 1px solid; + display:flex; +} +.treeViewItem-header-above { + border-top: black 1px solid; +} +.treeViewItem-header-below { + border-bottom: black 1px solid; +} +.treeViewItem-header-inside { + border: black 1px solid; }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 424042fee..ef340d770 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,251 +1,558 @@ -import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faCaretDown, faCaretRight, faTrashAlt, faAngleRight } from '@fortawesome/free-solid-svg-icons'; +import { library } from '@fortawesome/fontawesome-svg-core'; +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, observable, trace } from "mobx"; +import { action, computed, observable, trace } from "mobx"; import { observer } from "mobx-react"; -import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager"; -import { EditableView } from "../EditableView"; -import { CollectionSubView } from "./CollectionSubView"; -import "./CollectionTreeView.scss"; -import React = require("react"); -import { Document, listSpec } from '../../../new_fields/Schema'; -import { Cast, StrCast, BoolCast, FieldValue, NumCast } from '../../../new_fields/Types'; -import { Doc, DocListCast } from '../../../new_fields/Doc'; +import { Doc, DocListCast, HeightSym, WidthSym, Opt } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; -import { ContextMenu } from '../ContextMenu'; -import { undoBatch } from '../../util/UndoManager'; -import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; -import { CollectionDockingView } from './CollectionDockingView'; +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, DocTypes } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; -import { Docs } from '../../documents/Documents'; +import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager"; +import { SelectionManager } from '../../util/SelectionManager'; +import { Transform } from '../../util/Transform'; +import { undoBatch } from '../../util/UndoManager'; +import { ContextMenu } from '../ContextMenu'; +import { EditableView } from "../EditableView"; import { MainView } from '../MainView'; +import { Templates } from '../Templates'; import { CollectionViewType } from './CollectionBaseView'; +import { CollectionDockingView } from './CollectionDockingView'; +import { CollectionSchemaPreview } from './CollectionSchemaView'; +import { CollectionSubView } from "./CollectionSubView"; +import "./CollectionTreeView.scss"; +import React = require("react"); +import { LinkManager } from '../../util/LinkManager'; export interface TreeViewProps { document: Doc; - deleteDoc: (doc: Doc) => void; + dataDoc?: Doc; + containingCollection: Doc; + renderDepth: number; + deleteDoc: (doc: Doc) => boolean; moveDocument: DragManager.MoveFunction; dropAction: "alias" | "copy" | undefined; - addDocTab: (doc: Doc, where: string) => void; -} - -export enum BulletType { - Collapsed, - Collapsible, - List + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void; + panelWidth: () => number; + panelHeight: () => number; + addDocument: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean; + indentDocument?: () => void; + ScreenToLocalTransform: () => Transform; + outerXf: () => { translateX: number, translateY: number }; + treeViewId: string; + parentKey: string; + active: () => boolean; } 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); +library.add(faCaretSquareRight); @observer /** * Component that takes in a document prop and a boolean whether it's collapsed or not. */ class TreeView extends React.Component<TreeViewProps> { - + private _header?: React.RefObject<HTMLDivElement> = React.createRef(); + private _treedropDisposer?: DragManager.DragDropDisposer; + 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; - @undoBatch delete = () => this.props.deleteDoc(this.props.document); - - @undoBatch openRight = async () => { - if (this.props.document.dockingConfig) { - MainView.Instance.openWorkspace(this.props.document); - } else { - this.props.addDocTab(this.props.document, "openRight"); + @computed get fieldKey() { + let keys = Array.from(Object.keys(this.resolvedDataDoc)); + if (this.resolvedDataDoc.proto instanceof Doc) { + keys.push(...Array.from(Object.keys(this.resolvedDataDoc.proto))); + while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1); + } + let keyList: string[] = []; + keys.map(key => { + let docList = Cast(this.resolvedDataDoc[key], listSpec(Doc)); + if (docList && docList.length > 0) { + keyList.push(key); + } + }); + let layout = StrCast(this.props.document.layout); + if (layout.indexOf("fieldKey={\"") !== -1) { + return layout.split("fieldKey={\"")[1].split("\"")[0]; } + return keyList.length ? keyList[0] : "data"; } - get children() { - return Cast(this.props.document.data, listSpec(Doc), []); // bcz: needed? .filter(doc => FieldValue(doc)); + @computed get resolvedDataDoc() { return BoolCast(this.props.document.isTemplate) && this.props.dataDoc ? this.props.dataDoc : this.props.document; } + + protected createTreeDropTarget = (ele: HTMLDivElement) => { + this._treedropDisposer && this._treedropDisposer(); + if (ele) { + this._treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.treeDrop.bind(this) } }); + } } - onPointerDown = (e: React.PointerEvent) => { + @undoBatch delete = () => this.props.deleteDoc(this.resolvedDataDoc); + @undoBatch openRight = async () => this.props.addDocTab(this.props.document, undefined, "onRight"); + + onPointerDown = (e: React.PointerEvent) => e.stopPropagation(); + onPointerEnter = (e: React.PointerEvent): void => { + this.props.active() && (this.props.document.libraryBrush = true); + if (e.buttons === 1 && SelectionManager.GetIsDragging()) { + this._header!.current!.className = "treeViewItem-header"; + document.addEventListener("pointermove", this.onDragMove, true); + } + } + onPointerLeave = (e: React.PointerEvent): void => { + this.props.document.libraryBrush = false; + this._header!.current!.className = "treeViewItem-header"; + document.removeEventListener("pointermove", this.onDragMove, true); + } + onDragMove = (e: PointerEvent): void => { + this.props.document.libraryBrush = false; + 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); + let before = x[1] < bounds[1]; + let inside = x[0] > bounds[0] + 75 || (!before && !this._collapsed); + this._header!.current!.className = "treeViewItem-header"; + if (inside) this._header!.current!.className += " treeViewItem-header-inside"; + else if (before) this._header!.current!.className += " treeViewItem-header-above"; + else if (!before) this._header!.current!.className += " treeViewItem-header-below"; e.stopPropagation(); } @action - remove = (document: Document, key: string) => { - let children = Cast(this.props.document[key], listSpec(Doc), []); - if (children) { + remove = (document: Document, key: string): boolean => { + let children = Cast(this.resolvedDataDoc[key], listSpec(Doc), []); + if (children.indexOf(document) !== -1) { children.splice(children.indexOf(document), 1); + return true; } + return false; } @action - move: DragManager.MoveFunction = (document, target, addDoc) => { - if (this.props.document === target) { - return true; - } - //TODO This should check if it was removed - this.remove(document, "data"); - return addDoc(document); + move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => { + return this.props.document !== target && this.props.deleteDoc(doc) && addDoc(doc); } + @action + indent = () => this.props.addDocument(this.props.document) && this.delete() - renderBullet(type: BulletType) { - let onClicked = action(() => this._collapsed = !this._collapsed); - let bullet: IconProp | undefined = undefined; - switch (type) { - case BulletType.Collapsed: bullet = "caret-right"; break; - case BulletType.Collapsible: bullet = "caret-down"; break; - } - return <div className="bullet" onClick={onClicked}>{bullet ? <FontAwesomeIcon icon={bullet} /> : ""} </div>; + renderBullet() { + 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)}> + {<FontAwesomeIcon icon={this._collapsed ? (isDoc ? "caret-square-right" : "caret-right") : (isDoc ? "caret-square-down" : "caret-down")} />} + </div>; } - @action - onMouseEnter = () => { - this._isOver = true; - } - @observable _isOver: boolean = false; - @action - onMouseLeave = () => { - this._isOver = false; + static loadId = ""; + editableView = (key: string, style?: string) => (<EditableView + oneLine={true} + display={"inline"} + editing={this.resolvedDataDoc[Id] === TreeView.loadId} + contents={StrCast(this.props.document[key])} + 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.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); + }} + OnTab={() => this.props.indentDocument && this.props.indentDocument()} + />) + + @computed get keyList() { + let keys = Array.from(Object.keys(this.resolvedDataDoc)); + if (this.resolvedDataDoc.proto instanceof Doc) { + keys.push(...Array.from(Object.keys(this.resolvedDataDoc.proto))); + } + let keyList: string[] = keys.reduce((l, key) => { + let listspec = DocListCast(this.resolvedDataDoc[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)); + if (LinkManager.Instance.getAllRelatedLinks(this.props.document).length > 0) keyList.push("links"); + if (keyList.indexOf(this.fieldKey) !== -1) { + keyList.splice(keyList.indexOf(this.fieldKey), 1); + } + keyList.splice(0, 0, this.fieldKey); + return keyList.filter((item, index) => keyList.indexOf(item) >= index); } /** * Renders the EditableView title element for placement into the tree. */ renderTitle() { let reference = React.createRef<HTMLDivElement>(); - let onItemDown = SetupDrag(reference, () => this.props.document, this.props.moveDocument, this.props.dropAction); - let editableView = (titleString: string) => - (<EditableView - oneLine={!this._isOver ? true : false} - display={"inline"} - contents={titleString} - height={36} - GetValue={() => StrCast(this.props.document.title)} - SetValue={(value: string) => { - let target = this.props.document.proto ? this.props.document.proto : this.props.document; - target.title = value; - return true; - }} - />); - let dataDocs = CollectionDockingView.TopLevel ? Cast(CollectionDockingView.TopLevel.props.Document.data, listSpec(Doc), []) : []; - let openRight = dataDocs && dataDocs.indexOf(this.props.document) !== -1 ? (null) : ( + let onItemDown = SetupDrag(reference, () => this.resolvedDataDoc, this.move, this.props.dropAction, this.props.treeViewId, true); + + let headerElements = ( + <span className="collectionTreeView-keyHeader" key={this._chosenKey} + onPointerDown={action(() => { + let ind = this.keyList.indexOf(this._chosenKey); + ind = (ind + 1) % this.keyList.length; + this.__chosenKey = this.keyList[ind]; + })} > + {this._chosenKey} + </span>); + let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document[this.fieldKey], listSpec(Doc), []) : []; + let openRight = dataDocs && dataDocs.indexOf(this.resolvedDataDoc) !== -1 ? (null) : ( <div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}> <FontAwesomeIcon icon="angle-right" size="lg" /> - {/* <FontAwesomeIcon icon="angle-right" size="lg" /> */} </div>); - return ( - <div className="docContainer" ref={reference} onPointerDown={onItemDown} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} - style={{ background: BoolCast(this.props.document.protoBrush, false) ? "#06123232" : BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0" }} - onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> - {editableView(StrCast(this.props.document.title))} - {openRight} + return <> + <div className="docContainer" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown} + style={{ + background: BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0", + outline: BoolCast(this.props.document.workspaceBrush, false) ? "dashed 1px #06123232" : undefined, + pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? "all" : "none" + }} + > + {this.editableView("title")} {/* {<div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div>} */} - </div >); + </div > + {headerElements} + {openRight} + </>; } 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.props.document)) }); - ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }), "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, "inTab"), icon: "folder" }); - ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, "onRight"), icon: "caret-square-right" }); - if (DocumentManager.Instance.getDocumentViews(this.props.document).length) { - ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document)) }); + 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)), 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.displayMenu(e.pageX - 15, e.pageY - 15); + 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(); } } - onPointerEnter = (e: React.PointerEvent): void => { this.props.document.libraryBrush = true; }; - onPointerLeave = (e: React.PointerEvent): void => { this.props.document.libraryBrush = false; }; - - render() { - let bulletType = BulletType.List; - let contentElement: (JSX.Element | null)[] = []; - let keys = Array.from(Object.keys(this.props.document)); - if (this.props.document.proto instanceof Doc) { - keys.push(...Array.from(Object.keys(this.props.document.proto))); - while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1); + @undoBatch + treeDrop = (e: Event, de: DragManager.DropEvent) => { + let x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); + let rect = this._header!.current!.getBoundingClientRect(); + let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2); + let before = x[1] < bounds[1]; + let inside = x[0] > bounds[0] + 75 || (!before && !this._collapsed); + if (de.data instanceof DragManager.LinkDragData) { + let sourceDoc = de.data.linkSourceDocument; + let destDoc = this.props.document; + DocUtils.MakeLink(sourceDoc, destDoc); + e.stopPropagation(); } - keys.map(key => { - let docList = DocListCast(this.props.document[key]); - let doc = Cast(this.props.document[key], Doc); - if (doc instanceof Doc || docList.length) { - if (!this._collapsed) { - bulletType = BulletType.Collapsible; - let spacing = (key === "data") ? 0 : -10; - contentElement.push(<ul key={key + "more"}> - {(key === "data") ? (null) : - <span className="collectionTreeView-keyHeader" style={{ display: "block", marginTop: "7px" }} key={key}>{key}</span>} - <div style={{ display: "block", marginTop: `${spacing}px` }}> - {TreeView.GetChildElements(doc instanceof Doc ? [doc] : docList, key !== "data", (doc: Doc) => this.remove(doc, key), this.move, this.props.dropAction, this.props.addDocTab)} - </div> - </ul >); - } else { - bulletType = BulletType.Collapsed; + 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)); + if (docList !== undefined) { + addDoc = (doc: Doc) => { docList && docList.push(doc); return true; }; } } + let movedDocs = (de.data.options === this.props.treeViewId ? de.data.draggedDocuments : de.data.droppedDocuments); + return (de.data.dropAction || de.data.userDropAction) ? + de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before) || added, false) + : (de.data.moveDocument) ? + movedDocs.reduce((added: boolean, d) => de.data.moveDocument(d, this.resolvedDataDoc, addDoc) || added, false) + : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before), false); + } + return false; + } + + docTransform = () => { + let { scale, translateX, translateY } = Utils.GetScreenTransform(this._dref.current!); + let outerXf = this.props.outerXf(); + let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); + let finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1]); + return finalXf; + } + + renderLinks = () => { + let ele: JSX.Element[] = []; + let remDoc = (doc: Doc) => this.remove(doc, this._chosenKey); + 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)); + ele.push( + <div key={"treeviewlink-" + groupType + "subtitle"}> + <div className="collectionTreeView-subtitle">{groupType}:</div> + { + TreeView.GetChildElements(destLinks, this.props.treeViewId, this.props.document, this.props.dataDoc, "treeviewlink-" + groupType, addDoc, remDoc, this.move, + this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth) + } + </div> + ); }); - return <div className="treeViewItem-container" - onContextMenu={this.onWorkspaceContextMenu}> + return ele; + } + + @computed get docBounds() { + if (StrCast(this.props.document.type).indexOf(DocTypes.COL) === -1) return undefined; + let layoutDoc = Doc.expandTemplateLayout(this.props.document, this.props.dataDoc); + return Doc.ComputeContentBounds(layoutDoc); + } + 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.docBounds; + 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; + })()); + } + fitToBox = () => { + let bounds = this.docBounds!; + return [(bounds.x + bounds.r) / 2, (bounds.y + bounds.b) / 2, Math.min(this.docHeight() / (bounds.b - bounds.y), this.docWidth() / (bounds.r - bounds.x))]; + } + + 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); + + if (!this._collapsed) { + if (!this.props.document.embed) { + contentElement = <ul key={this._chosenKey + "more"}> + {this._chosenKey === "links" ? this.renderLinks() : + TreeView.GetChildElements(doc instanceof Doc ? [doc] : DocListCast(docList), this.props.treeViewId, this.props.document, this.props.dataDoc, this._chosenKey, addDoc, remDoc, this.move, + this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth)} + </ul >; + } else { + let layoutDoc = Doc.expandTemplateLayout(this.props.document, this.props.dataDoc); + contentElement = <div ref={this._dref} style={{ display: "inline-block", height: this.docHeight() }} key={this.props.document[Id]}> + <CollectionSchemaPreview + Document={layoutDoc} + DataDocument={this.resolvedDataDoc} + renderDepth={this.props.renderDepth} + fitToBox={this.docBounds && !NumCast(this.props.document.nativeWidth) ? this.fitToBox : undefined} + width={this.docWidth} + height={this.docHeight} + getTransform={this.docTransform} + CollectionView={undefined} + addDocument={emptyFunction as any} + moveDocument={this.props.moveDocument} + removeDocument={emptyFunction as any} + active={this.props.active} + whenActiveChanged={emptyFunction as any} + addDocTab={this.props.addDocTab} + setPreviewScript={emptyFunction}> + </CollectionSchemaPreview> + </div>; + } + } + return <div className="treeViewItem-container" ref={this.createTreeDropTarget} onContextMenu={this.onWorkspaceContextMenu}> <li className="collection-child"> - {this.renderBullet(bulletType)} - {this.renderTitle()} - {contentElement} + <div className="treeViewItem-header" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> + {this.renderBullet()} + {this.renderTitle()} + </div> + <div className="treeViewItem-border"> + {contentElement} + </div> </li> </div>; } - public static GetChildElements(docs: Doc[], allowMinimized: boolean, remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType, addDocTab: (doc: Doc, where: string) => void) { - return docs.filter(child => !child.excludeFromLibrary && (allowMinimized || !child.isMinimized)).map(child => - <TreeView document={child} key={child[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} addDocTab={addDocTab} />); + public static GetChildElements( + docs: Doc[], + treeViewId: string, + containingCollection: Doc, + dataDoc: Doc | undefined, + key: string, + add: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean, + remove: ((doc: Doc) => boolean), + move: DragManager.MoveFunction, + dropAction: dropActionType, + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void, + screenToLocalXf: () => Transform, + outerXf: () => { translateX: number, translateY: number }, + active: () => boolean, + panelWidth: () => number, + renderDepth: number + ) { + let docList = docs.filter(child => !child.excludeFromLibrary); + let rowWidth = () => panelWidth() - 20; + return docList.map((child, i) => { + let indent = i === 0 ? undefined : () => { + if (StrCast(docList[i - 1].layout).indexOf("CollectionView") !== -1) { + let fieldKeysub = StrCast(docList[i - 1].layout).split("fieldKey")[1]; + let fieldKey = fieldKeysub.split("\"")[1]; + Doc.AddDocToList(docList[i - 1], fieldKey, child); + remove(child); + } + }; + let addDocument = (doc: Doc, relativeTo?: Doc, before?: boolean) => { + return add(doc, relativeTo ? relativeTo : docList[i], before !== undefined ? before : false); + }; + let rowHeight = () => { + let aspect = NumCast(child.nativeWidth, 0) / NumCast(child.nativeHeight, 0); + return aspect ? Math.min(child[WidthSym](), rowWidth()) / aspect : child[HeightSym](); + }; + return <TreeView + document={child} + dataDoc={dataDoc} + containingCollection={containingCollection} + treeViewId={treeViewId} + key={child[Id]} + indentDocument={indent} + renderDepth={renderDepth} + deleteDoc={remove} + addDocument={addDocument} + panelWidth={rowWidth} + panelHeight={rowHeight} + moveDocument={move} + dropAction={dropAction} + addDocTab={addDocTab} + ScreenToLocalTransform={screenToLocalXf} + outerXf={outerXf} + parentKey={key} + active={active} />; + }); } } @observer export class CollectionTreeView extends CollectionSubView(Document) { + private treedropDisposer?: DragManager.DragDropDisposer; + private _mainEle?: HTMLDivElement; + + protected createTreeDropTarget = (ele: HTMLDivElement) => { + this.treedropDisposer && this.treedropDisposer(); + if (this._mainEle = ele) { + this.treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + } + } + + componentWillUnmount() { + this.treedropDisposer && this.treedropDisposer(); + } + @action - remove = (document: Document) => { - let children = Cast(this.props.Document.data, listSpec(Doc), []); - if (children) { + remove = (document: Document): boolean => { + let children = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []); + if (children.indexOf(document) !== -1) { children.splice(children.indexOf(document), 1); + return true; } + return false; } 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); } } - render() { - let dropAction = StrCast(this.props.Document.dropAction, "alias") as dropActionType; - if (!this.childDocs) { - return (null); + + @computed get resolvedDataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.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); } - let childElements = TreeView.GetChildElements(this.childDocs, false, this.remove, this.props.moveDocument, dropAction, this.props.addDocTab); + } + @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 >; + } + - return ( + 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={{ borderRadius: "inherit" }} + style={{ overflow: "auto" }} onContextMenu={this.onContextMenu} - onWheel={(e: React.WheelEvent) => this.props.isSelected() && e.stopPropagation()} - onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}> - <div className="coll-title"> - <EditableView - contents={this.props.Document.title} - display={"inline"} - height={72} - GetValue={() => StrCast(this.props.Document.title)} - SetValue={(value: string) => { - let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document; - target.title = value; - return true; - }} /> - </div> - <ul className="no-indent"> - {childElements} + onWheel={(e: React.WheelEvent) => (e.target as any).scrollHeight > (e.target as any).clientHeight && e.stopPropagation()} + onDrop={this.onTreeDrop} + ref={this.createTreeDropTarget}> + <EditableView + contents={this.resolvedDataDoc.title} + display={"block"} + height={72} + GetValue={() => StrCast(this.resolvedDataDoc.title)} + SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc).title = value) ? true : true} + OnFillDown={(value: string) => { + Doc.GetProto(this.props.Document).title = value; + 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, + moveDoc, dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.outerXf, this.props.active, this.props.PanelWidth, this.props.renderDepth) + } </ul> </div > ); diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx index ccbac9915..446f104d0 100644 --- a/src/client/views/collections/CollectionVideoView.tsx +++ b/src/client/views/collections/CollectionVideoView.tsx @@ -12,7 +12,7 @@ import { Id } from "../../../new_fields/FieldSymbols"; import { VideoBox } from "../nodes/VideoBox"; import { NumCast, Cast, StrCast } from "../../../new_fields/Types"; import { VideoField } from "../../../new_fields/URLField"; -import { SearchBox } from "../SearchBox"; +import { SearchBox } from "../search/SearchBox"; import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from "../../documents/Documents"; @@ -97,7 +97,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, ""); SearchBox.convertDataUri(dataUrl, filename).then((returnedFilename) => { if (returnedFilename) { - let url = DocServer.Util.prepend(returnedFilename); + let url = DocServer.prepend(returnedFilename); 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 68eefab4c..e500e5c70 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -2,8 +2,10 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons'; import { observer } from "mobx-react"; import * as React from 'react'; +import { Doc } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; +import { Docs } from '../../documents/Documents'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from '../ContextMenuItem'; @@ -29,7 +31,7 @@ export class CollectionView extends React.Component<FieldViewProps> { private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => { let props = { ...this.props, ...renderProps }; - switch (type) { + switch (this.isAnnotationOverlay ? CollectionViewType.Freeform : type) { case CollectionViewType.Schema: return (<CollectionSchemaView {...props} CollectionView={this} />); case CollectionViewType.Docking: return (<CollectionDockingView {...props} CollectionView={this} />); case CollectionViewType.Tree: return (<CollectionTreeView {...props} CollectionView={this} />); @@ -41,7 +43,7 @@ export class CollectionView extends React.Component<FieldViewProps> { return (null); } - get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } // bcz: ? Why do we need to compare Id's? + get isAnnotationOverlay() { return this.props.fieldKey === "annotations" || this.props.fieldExt === "annotations"; } 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 @@ -54,6 +56,16 @@ export class CollectionView extends React.Component<FieldViewProps> { subItems.push({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree), icon: "tree" }); subItems.push({ description: "Stacking", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Stacking), icon: "th-list" }); ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems }); + ContextMenu.Instance.addItem({ + description: "Apply Template", event: undoBatch(() => { + let otherdoc = new Doc(); + otherdoc.width = 100; + 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, undefined, "onRight"); + }), icon: "project-diagram" + }); } } diff --git a/src/client/views/collections/DockedFrameRenderer.tsx b/src/client/views/collections/DockedFrameRenderer.tsx deleted file mode 100644 index 1e7c5661b..000000000 --- a/src/client/views/collections/DockedFrameRenderer.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import 'golden-layout/src/css/goldenlayout-base.css'; -import 'golden-layout/src/css/goldenlayout-dark-theme.css'; -import { action, observable, reaction, Lambda, IReactionDisposer } from "mobx"; -import { observer } from "mobx-react"; -import Measure, { ContentRect } from "react-measure"; -import { Doc, Field, Opt, DocListCast } from "../../../new_fields/Doc"; -import { FieldId } from "../../../new_fields/RefField"; -import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { emptyFunction, returnTrue, Utils } from "../../../Utils"; -import { DocServer } from "../../DocServer"; -import { Transform } from '../../util/Transform'; -import { DocumentView } from "../nodes/DocumentView"; -import "./CollectionDockingView.scss"; -import { SubCollectionViewProps } from "./CollectionSubView"; -import React = require("react"); -import { CollectionViewType } from './CollectionBaseView'; -import { Id } from '../../../new_fields/FieldSymbols'; -import { CollectionDockingView } from './CollectionDockingView'; - -interface DockedFrameProps { - documentId: FieldId; - glContainer: any; - glEventHub: any; - parent: CollectionDockingView; -} - -@observer -export class DockedFrameRenderer extends React.Component<DockedFrameProps> { - _mainCont = React.createRef<HTMLDivElement>(); - @observable private _panelWidth = 0; - @observable private _panelHeight = 0; - @observable private _document: Opt<Doc>; - private get parentProps(): SubCollectionViewProps { - return this.props.parent.props; - } - - get _stack(): any { - let parent = this.props.glContainer.parent.parent; - if (this._document && this._document.excludeFromLibrary && parent.parent && parent.parent.contentItems.length > 1) - return parent.parent.contentItems[1]; - return parent; - } - constructor(props: any) { - super(props); - DocServer.getRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc)); - } - - nativeWidth = () => NumCast(this._document!.nativeWidth, this._panelWidth); - nativeHeight = () => NumCast(this._document!.nativeHeight, this._panelHeight); - contentScaling = () => { - const nativeH = this.nativeHeight(); - const nativeW = this.nativeWidth(); - let wscale = this._panelWidth / nativeW; - return wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale; - } - - ScreenToLocalTransform = () => { - if (this._mainCont.current && this._mainCont.current.children) { - let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current.children[0].firstChild as HTMLElement); - scale = Utils.GetScreenTransform(this._mainCont.current).scale; - return this.parentProps.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale); - } - return Transform.Identity(); - } - get scaleToFitMultiplier() { - let docWidth = NumCast(this._document!.width); - let docHeight = NumCast(this._document!.height); - if (NumCast(this._document!.nativeWidth) || !docWidth || !this._panelWidth || !this._panelHeight) return 1; - if (StrCast(this._document!.layout).indexOf("Collection") === -1 || - NumCast(this._document!.viewType) !== CollectionViewType.Freeform) return 1; - let scaling = Math.max(1, this._panelWidth / docWidth * docHeight > this._panelHeight ? - this._panelHeight / docHeight : this._panelWidth / docWidth); - return scaling; - } - get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; } - - addDocTab = (doc: Doc, location: string) => { - if (location === "onRight") { - CollectionDockingView.AddRightSplit(doc); - } else { - CollectionDockingView.AddTab(this._stack, doc); - } - } - 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}, ${this.scaleToFitMultiplier})` }}> - <DocumentView key={this._document[Id]} Document={this._document} - bringToFront={emptyFunction} - addDocument={undefined} - removeDocument={undefined} - ContentScaling={this.contentScaling} - PanelWidth={this.nativeWidth} - PanelHeight={this.nativeHeight} - ScreenToLocalTransform={this.ScreenToLocalTransform} - isTopMost={true} - selectOnLoad={false} - parentActive={returnTrue} - whenActiveChanged={emptyFunction} - focus={emptyFunction} - addDocTab={this.addDocTab} - ContainingCollectionView={undefined} /> - </div >); - } - - render() { - let theContent = this.content; - return !this._document ? (null) : - <Measure offset onResize={action((r: any) => { this._panelWidth = r.offset.width; this._panelHeight = r.offset.height; })}> - {({ measureRef }) => <div ref={measureRef}> {theContent} </div>} - </Measure>; - } -}
\ No newline at end of file diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index 95183bd2c..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, 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,14 +23,14 @@ 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(() => { - this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.TopLevel.props.Document)).map(doc => ({ col: doc, target: this.props.Document })); - this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.TopLevel.props.Document)).map(([col, target]) => ({ col, target })); + this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: this.props.Document })); + this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target })); }); } @@ -43,7 +43,7 @@ export class SelectorContextMenu extends React.Component<SelectorProps> { col.panX = newPanX; col.panY = newPanY; } - this.props.addDocTab(col, "inTab"); + this.props.addDocTab(col, undefined, "inTab"); // bcz: dataDoc? }; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss index 7a0fd2b31..fc5212edd 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss @@ -9,6 +9,7 @@ opacity: 0.5; transform: translate(10000px,10000px); pointer-events: all; + cursor: pointer; } .collectionfreeformlinkview-linkText { stroke: rgb(0,0,0); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 301b769af..b546d1b78 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -1,11 +1,10 @@ import { observer } from "mobx-react"; -import { Utils } from "../../../../Utils"; +import { Doc, HeightSym, WidthSym } from "../../../../new_fields/Doc"; +import { BoolCast, NumCast, StrCast } from "../../../../new_fields/Types"; +import { InkingControl } from "../../InkingControl"; import "./CollectionFreeFormLinkView.scss"; import React = require("react"); import v5 = require("uuid/v5"); -import { StrCast, NumCast, BoolCast } from "../../../../new_fields/Types"; -import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc"; -import { InkingControl } from "../../InkingControl"; export interface CollectionFreeFormLinkViewProps { A: Doc; @@ -26,18 +25,18 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : a[HeightSym]() / 2); let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : b[WidthSym]() / 2); let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : b[HeightSym]() / 2); - this.props.LinkDocs.map(l => { - let width = l[WidthSym](); - l.x = (x1 + x2) / 2 - width / 2; - l.y = (y1 + y2) / 2 + 10; - if (!this.props.removeDocument(l)) this.props.addDocument(l, false); - }); + // this.props.LinkDocs.map(l => { + // let width = l[WidthSym](); + // l.x = (x1 + x2) / 2 - width / 2; + // l.y = (y1 + y2) / 2 + 10; + // if (!this.props.removeDocument(l)) this.props.addDocument(l, false); + // }); e.stopPropagation(); e.preventDefault(); } } render() { - let l = this.props.LinkDocs; + // let l = this.props.LinkDocs; let a = this.props.A; let b = this.props.B; let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / NumCast(a.zoomBasis, 1) / 2); @@ -45,12 +44,13 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / NumCast(b.zoomBasis, 1) / 2); let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / NumCast(b.zoomBasis, 1) / 2); let text = ""; - this.props.LinkDocs.map(l => text += StrCast(l.title) + "(" + StrCast(l.linkDescription) + "), "); - text = text.substr(0, text.length - 2); + // let first = this.props.LinkDocs[0]; + // if (this.props.LinkDocs.length === 1) text += first.title + (first.linkDescription ? "(" + StrCast(first.linkDescription) + ")" : ""); + // else text = "-multiple-"; return ( <> <line key="linkLine" className="collectionfreeformlinkview-linkLine" - style={{ strokeWidth: `${2 * l.length / 2}` }} + style={{ strokeWidth: `${2 * 1 / 2}` }} x1={`${x1}`} y1={`${y1}`} x2={`${x2}`} y2={`${y2}`} /> {/* <circle key="linkCircle" className="collectionfreeformlinkview-linkCircle" diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index a43c5f241..2d94f1b8e 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -1,75 +1,72 @@ -import { computed, IReactionDisposer, reaction, trace } from "mobx"; +import { computed, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; -import { Utils } from "../../../../Utils"; +import { Doc, DocListCast } from "../../../../new_fields/Doc"; +import { Id } from "../../../../new_fields/FieldSymbols"; +import { List } from "../../../../new_fields/List"; +import { listSpec } from "../../../../new_fields/Schema"; +import { Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types"; import { DocumentManager } from "../../../util/DocumentManager"; import { DocumentView } from "../../nodes/DocumentView"; import { CollectionViewProps } from "../CollectionSubView"; import "./CollectionFreeFormLinksView.scss"; import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView"; import React = require("react"); -import { Doc, DocListCastAsync, DocListCast } from "../../../../new_fields/Doc"; -import { Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types"; -import { listSpec } from "../../../../new_fields/Schema"; -import { List } from "../../../../new_fields/List"; -import { Id } from "../../../../new_fields/FieldSymbols"; @observer export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> { _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) { @@ -98,6 +95,7 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => { let srcViews = this.documentAnchors(connection.a); let targetViews = this.documentAnchors(connection.b); + let possiblePairs: { a: Doc, b: Doc, }[] = []; srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document }))); possiblePairs.map(possiblePair => { @@ -110,25 +108,21 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP } return match || found; }, false)) { - console.log("A" + possiblePair.a[Id] + " B" + possiblePair.b[Id] + " L" + connection.l[Id]); - drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] }) + drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] }); } }); 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/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx index 2838b7905..3193f5624 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx @@ -1,15 +1,13 @@ -import { computed } from "mobx"; import { observer } from "mobx-react"; +import * as mobxUtils from 'mobx-utils'; +import CursorField from "../../../../new_fields/CursorField"; +import { listSpec } from "../../../../new_fields/Schema"; +import { Cast } from "../../../../new_fields/Types"; +import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; import { CollectionViewProps } from "../CollectionSubView"; import "./CollectionFreeFormView.scss"; import React = require("react"); import v5 = require("uuid/v5"); -import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; -import CursorField from "../../../../new_fields/CursorField"; -import { List } from "../../../../new_fields/List"; -import { Cast } from "../../../../new_fields/Types"; -import { listSpec } from "../../../../new_fields/Schema"; -import * as mobxUtils from 'mobx-utils'; @observer export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index e10ba9d7e..ccf261c95 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -1,7 +1,7 @@ @import "../../globalCssVariables"; .collectionfreeformview-ease { - position: absolute; + position: inherit; top: 0; left: 0; width: 100%; @@ -25,8 +25,9 @@ height: 100%; width: 100%; } + >.jsx-parser { - z-index:0; + z-index: 0; } //nested freeform views @@ -40,24 +41,27 @@ border-radius: $border-radius; box-sizing: border-box; position: absolute; + .marqueeView { overflow: hidden; } + top: 0; left: 0; width: 100%; height: 100%; } - + .collectionfreeformview-overlay { .collectionfreeformview>.jsx-parser { position: inherit; height: 100%; } - + >.jsx-parser { - z-index:0; + position: absolute; + z-index: 0; } .formattedTextBox-cont { @@ -71,9 +75,11 @@ box-sizing: border-box; position:absolute; z-index: -1; + .marqueeView { overflow: hidden; } + top: 0; left: 0; width: 100%; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index cd613e6ab..1777c287c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,16 +1,25 @@ -import { action, computed, trace } from "mobx"; +import { action, computed } from "mobx"; import { observer } from "mobx-react"; -import { emptyFunction, returnFalse, returnOne } from "../../../../Utils"; +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"; +import { BoolCast, Cast, FieldValue, NumCast } from "../../../../new_fields/Types"; +import { emptyFunction, returnOne } from "../../../../Utils"; import { DocumentManager } from "../../../util/DocumentManager"; 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 { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"; +import { ContextMenu } from "../../ContextMenu"; import { InkingCanvas } from "../../InkingCanvas"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; import { DocumentContentsView } from "../../nodes/DocumentContentsView"; import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView"; +import { pageSchema } from "../../nodes/ImageBox"; +import PDFMenu from "../../pdf/PDFMenu"; import { CollectionSubView } from "../CollectionSubView"; import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView"; import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; @@ -18,19 +27,18 @@ import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); import v5 = require("uuid/v5"); -import { createSchema, makeInterface, listSpec } from "../../../../new_fields/Schema"; -import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc"; -import { FieldValue, Cast, NumCast, BoolCast } from "../../../../new_fields/Types"; -import { pageSchema } from "../../nodes/ImageBox"; -import { InkField, StrokeData } from "../../../../new_fields/InkField"; -import { HistoryUtil } from "../../../util/History"; -import { Id } from "../../../../new_fields/FieldSymbols"; -import { DocServer } from "../../../DocServer"; +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,11 +54,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @computed get nativeWidth() { return this.Document.nativeWidth || 0; } @computed get nativeHeight() { return this.Document.nativeHeight || 0; } - public get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } + public get isAnnotationOverlay() { return this.props.fieldKey === "annotations" || this.props.fieldExt === "annotations"; } 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.props.fitToBox ? this.props.fitToBox()[0] : this.Document.panX || 0; + private panY = () => this.props.fitToBox ? this.props.fitToBox()[1] : this.Document.panY || 0; + private zoomScaling = () => this.props.fitToBox ? this.props.fitToBox()[2] : this.Document.scale || 1; 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()); @@ -81,31 +89,45 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { - if (super.drop(e, de) && 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 dropX = NumCast(de.data.droppedDocuments[0].x); - let dropY = NumCast(de.data.droppedDocuments[0].y); - de.data.droppedDocuments.forEach(d => { - d.x = x + NumCast(d.x) - dropX; - d.y = y + NumCast(d.y) - dropY; - if (!NumCast(d.width)) { - d.width = 300; - } - if (!NumCast(d.height)) { - let nw = NumCast(d.nativeWidth); - let nh = NumCast(d.nativeHeight); - d.height = nw && nh ? nh / nw * NumCast(d.width) : 300; - } - this.bringToFront(d); - }); - SelectionManager.ReselectAll(); + if (super.drop(e, de)) { + 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 dropX = NumCast(de.data.droppedDocuments[0].x); + let dropY = NumCast(de.data.droppedDocuments[0].y); + de.data.droppedDocuments.forEach(d => { + d.x = x + NumCast(d.x) - dropX; + d.y = y + NumCast(d.y) - dropY; + if (!NumCast(d.width)) { + d.width = 300; + } + if (!NumCast(d.height)) { + let nw = NumCast(d.nativeWidth); + let nh = NumCast(d.nativeHeight); + d.height = nw && nh ? nh / nw * NumCast(d.width) : 300; + } + this.bringToFront(d); + }); + } + } + 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 dropX = NumCast(de.data.dropDocument.x); + let dropY = NumCast(de.data.dropDocument.y); + dragDoc.x = x + NumCast(dragDoc.x) - dropX; + dragDoc.y = y + NumCast(dragDoc.y) - dropY; + this.bringToFront(dragDoc); + } } - return true; } return false; } @@ -135,6 +157,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let docs = this.childDocs || []; let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); 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 miny = docs.length ? NumCast(docs[0].y) : 0; @@ -147,7 +170,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { 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.props.Document.ink, InkField); + let ink = Cast(this.extensionDoc.ink, InkField); if (ink && ink.inkData) { ink.inkData.forEach((value: StrokeData, key: string) => { let bounds = InkingCanvas.StrokeRect(value); @@ -175,14 +198,18 @@ 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; }); - if (!this.props.isSelected() && !childSelected && !this.props.isTopMost) { + if (!this.props.isSelected() && !childSelected && this.props.renderDepth > 0) { return; } e.stopPropagation(); @@ -201,7 +228,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } else { // if (modes[e.deltaMode] === 'pixels') coefficient = 50; // else if (modes[e.deltaMode] === 'lines') coefficient = 1000; // This should correspond to line-height?? - let deltaScale = (1 - (e.deltaY / coefficient)); + let deltaScale = e.deltaY > 0 ? (1 / 1.1) : 1.1; if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) { deltaScale = 1 / this.zoomScaling(); } @@ -218,35 +245,24 @@ 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.panY = panY; + if (this.props.Document.scrollY) { + this.props.Document.scrollY = panY; + } } @action onDrop = (e: React.DragEvent): void => { var pt = this.getTransform().transformPoint(e.pageX, e.pageY); - let html = e.dataTransfer.getData("text/html"); - if (html && html.indexOf(document.location.origin)) { // prosemirror text containing link to dash document - e.stopPropagation(); - e.preventDefault(); - let start = html.indexOf(window.location.origin); - let path = html.substr(start, html.length - start); - let docid = path.substr(0, path.indexOf("\">")).replace(DocServer.Util.prepend("/doc/"), "").split("?")[0]; - DocServer.getRefField(docid).then(f => { - if (f instanceof Doc) { - f.x = pt[0]; - f.y = pt[1]; - (f instanceof Doc) && this.props.addDocument(f, false); - } - }); - return; - } else { - super.onDrop(e, { x: pt[0], y: pt[1] }); - } + super.onDrop(e, { x: pt[0], y: pt[1] }); } onDragOver = (): void => { @@ -262,7 +278,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { doc.zIndex = docs.length + 1; } - focusDocument = (doc: Doc) => { + focusDocument = (doc: Doc, willZoom: boolean) => { const panX = this.Document.panX; const panY = this.Document.panY; const id = this.Document[Id]; @@ -289,22 +305,80 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { newState.initializers[id] = { panX: newPanX, panY: newPanY }; HistoryUtil.pushState(newState); this.setPan(newPanX, newPanY); + this.props.Document.panTransformType = "Ease"; this.props.focus(this.props.Document); + if (willZoom) { + this.setScaleToZoom(doc); + } + + } + + setScaleToZoom = (doc: Doc) => { + let p = this.props; + let PanelHeight = p.PanelHeight(); + let panelWidth = p.PanelWidth(); + + let docHeight = NumCast(doc.height); + let docWidth = NumCast(doc.width); + let targetHeight = 0.5 * PanelHeight; + let targetWidth = 0.5 * panelWidth; + + let maxScaleX: number = targetWidth / docWidth; + let maxScaleY: number = targetHeight / docHeight; + let maxApplicableScale = Math.min(maxScaleX, maxScaleY); + this.Document.scale = maxApplicableScale; } + zoomToScale = (scale: number) => { + this.Document.scale = scale; + } - getDocumentViewProps(document: Doc): DocumentViewProps { + getScale = () => { + if (this.Document.scale) { + return this.Document.scale; + } + return 1; + } + + + getChildDocumentViewProps(childDocLayout: Doc): DocumentViewProps { + let resolvedDataDoc = this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined; + let layoutDoc = Doc.expandTemplateLayout(childDocLayout, resolvedDataDoc); + return { + DataDoc: resolvedDataDoc !== layoutDoc && resolvedDataDoc ? resolvedDataDoc : undefined, + Document: layoutDoc, + addDocument: this.props.addDocument, + removeDocument: this.props.removeDocument, + moveDocument: this.props.moveDocument, + ScreenToLocalTransform: this.getTransform, + renderDepth: this.props.renderDepth + 1, + selectOnLoad: layoutDoc[Id] === this._selectOnLoaded, + PanelWidth: layoutDoc[WidthSym], + PanelHeight: layoutDoc[HeightSym], + ContentScaling: returnOne, + ContainingCollectionView: this.props.CollectionView, + focus: this.focusDocument, + parentActive: this.props.active, + whenActiveChanged: this.props.whenActiveChanged, + bringToFront: this.bringToFront, + addDocTab: this.props.addDocTab, + zoomToScale: this.zoomToScale, + getScale: this.getScale + }; + } + getDocumentViewProps(layoutDoc: Doc): DocumentViewProps { return { - Document: document, + DataDoc: this.props.DataDoc, + Document: this.props.Document, addDocument: this.props.addDocument, removeDocument: this.props.removeDocument, moveDocument: this.props.moveDocument, ScreenToLocalTransform: this.getTransform, - isTopMost: false, - selectOnLoad: document[Id] === this._selectOnLoaded, - PanelWidth: document[WidthSym], - PanelHeight: document[HeightSym], + renderDepth: this.props.renderDepth + 1, + selectOnLoad: layoutDoc[Id] === this._selectOnLoaded, + PanelWidth: layoutDoc[WidthSym], + PanelHeight: layoutDoc[HeightSym], ContentScaling: returnOne, ContainingCollectionView: this.props.CollectionView, focus: this.focusDocument, @@ -312,19 +386,41 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { whenActiveChanged: this.props.whenActiveChanged, bringToFront: this.bringToFront, addDocTab: this.props.addDocTab, + zoomToScale: this.zoomToScale, + getScale: this.getScale }; } + 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.getDocumentViewProps(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; @@ -340,17 +436,76 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); } + onContextMenu = () => { + ContextMenu.Instance.addItem({ + 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; + } + } + } + } + }); + 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; + 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}"); + } + }); + } + private childViews = () => [ <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />, ...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); return ( <div className={containerName} ref={this.createDropTarget} onWheel={this.onPointerWheel} style={{ borderRadius: "inherit" }} - onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} > + 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}> @@ -358,14 +513,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { easing={easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> <CollectionFreeFormLinksView {...this.props} key="freeformLinks"> - <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} > + <InkingCanvas getScreenTransform={this.getTransform} Document={this.extensionDoc} inkFieldKey={this.props.fieldExt ? "ink" : this.props.fieldKey + "_ink"} > {this.childViews} </InkingCanvas> </CollectionFreeFormLinksView> <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" /> </CollectionFreeFormViewPannableContents> </MarqueeView> - <CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} {...this.props} /> + <CollectionFreeFormOverlayView {...this.props} {...this.getDocumentViewProps(this.props.Document)} /> </div> ); } @@ -375,7 +530,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> { @computed get overlayView() { return (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"} - isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />); + renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />); } render() { return this.overlayView; @@ -385,8 +540,9 @@ class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & @observer class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> { @computed get backgroundView() { + let props = this.props; return (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"} - isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />); + renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />); } render() { return this.props.Document.backgroundLayout ? this.backgroundView : (null); 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 07a58ed64..a4a6881f8 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,27 +1,24 @@ import * as htmlToImage from "html-to-image"; -import { action, computed, observable, trace } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; +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"; +import { Cast, NumCast } from "../../../../new_fields/Types"; +import { Utils } from "../../../../Utils"; +import { DocServer } from "../../../DocServer"; import { Docs } from "../../../documents/Documents"; import { SelectionManager } from "../../../util/SelectionManager"; import { Transform } from "../../../util/Transform"; -import { undoBatch, UndoManager } from "../../../util/UndoManager"; +import { undoBatch } from "../../../util/UndoManager"; import { InkingCanvas } from "../../InkingCanvas"; import { PreviewCursor } from "../../PreviewCursor"; +import { Templates } from "../../Templates"; +import { CollectionViewType } from "../CollectionBaseView"; import { CollectionFreeFormView } from "./CollectionFreeFormView"; import "./MarqueeView.scss"; import React = require("react"); -import { Utils } from "../../../../Utils"; -import { Doc } from "../../../../new_fields/Doc"; -import { NumCast, Cast } from "../../../../new_fields/Types"; -import { InkField, StrokeData } from "../../../../new_fields/InkField"; -import { List } from "../../../../new_fields/List"; -import { ImageField } from "../../../../new_fields/URLField"; -import { Template, Templates } from "../../Templates"; -import { SearchBox } from "../../SearchBox"; -import { DocServer } from "../../../DocServer"; -import { Id } from "../../../../new_fields/FieldSymbols"; -import { CollectionView } from "../CollectionView"; -import { CollectionViewType } from "../CollectionBaseView"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -94,8 +91,9 @@ export class MarqueeView extends React.Component<MarqueeViewProps> this.pasteTable(ns, x, y); } }); - } else { + } else if (!e.ctrlKey) { let newBox = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" }); + newBox.proto!.autoHeight = true; this.props.addLiveTextDocument(newBox); } e.stopPropagation(); @@ -147,6 +145,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> this._downY = this._lastY = e.pageY; this._commandExecuted = false; PreviewCursor.Visible = false; + this.cleanupInteractions(true); if (e.button === 2 || (e.button === 0 && e.altKey)) { if (!this.props.container.props.active()) this.props.selectDocuments([this.props.container.props.Document]); document.addEventListener("pointermove", this.onPointerMove, true); @@ -182,14 +181,18 @@ export class MarqueeView extends React.Component<MarqueeViewProps> @action onPointerUp = (e: PointerEvent): void => { + // console.log("pointer up!"); if (this._visible) { + // console.log("visible"); let mselect = this.marqueeSelect(); if (!e.shiftKey) { SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document); } this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]); } + //console.log("invisible"); this.cleanupInteractions(true); + if (e.altKey) { e.preventDefault(); } @@ -221,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) => { @@ -232,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(); @@ -256,23 +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 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", - scale: zoomBasis, - width: bounds.width * zoomBasis, - height: bounds.height * zoomBasis, - ink: inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined, - title: e.key === "s" || e.key === "S" ? "-summary-" : e.key === "p" ? "-summary-" : "a nested collection", + width: bounds.width, + height: bounds.height, + title: e.key === "s" || e.key === "S" ? "-summary-" : "a nested collection", }); - newCollection.zoomBasis = zoomBasis; + newCollection.data_ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined; this.marqueeInkDelete(inkData); if (e.key === "s") { @@ -295,39 +304,25 @@ export class MarqueeView extends React.Component<MarqueeViewProps> this.props.addLiveTextDocument(container); // }); } else if (e.key === "S") { - await htmlToImage.toPng(this._mainCont.current!, { width: bounds.width * zoomBasis, height: bounds.height * zoomBasis, quality: 0.2 }).then((dataUrl) => { - selected.map(d => { - this.props.removeDocument(d); - d.x = NumCast(d.x) - bounds.left - bounds.width / 2; - d.y = NumCast(d.y) - bounds.top - bounds.height / 2; - d.page = -1; - return d; - }); - let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); - SearchBox.convertDataUri(dataUrl, "icon" + summary[Id] + "_image").then((returnedFilename) => { - if (returnedFilename) { - let url = DocServer.Util.prepend(returnedFilename); - let imageSummary = Docs.Create.ImageDocument(url, { - x: bounds.left, y: bounds.top + 100 / zoomBasis, - width: 150, height: bounds.height / bounds.width * 150, title: "-summary image-" - }); - summary.imageSummary = imageSummary; - this.props.addDocument(imageSummary, false); - } - }) - newCollection.proto!.summaryDoc = summary; - selected = [newCollection]; - newCollection.x = bounds.left + bounds.width; - //this.props.addDocument(newCollection, false); - summary.proto!.summarizedDocs = new List<Doc>(selected); - summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight" - - this.props.addLiveTextDocument(summary); + selected.map(d => { + this.props.removeDocument(d); + d.x = NumCast(d.x) - bounds.left - bounds.width / 2; + d.y = NumCast(d.y) - bounds.top - bounds.height / 2; + d.page = -1; + return d; }); + 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; + //this.props.addDocument(newCollection, false); + summary.proto!.summarizedDocs = new List<Doc>(selected); + summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight" + + this.props.addLiveTextDocument(summary); } else { this.props.addDocument(newCollection, false); - SelectionManager.DeselectAll(); this.props.selectDocuments([newCollection]); } this.cleanupInteractions(false); @@ -363,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); } } |
