diff options
Diffstat (limited to 'src/client/views/collections')
23 files changed, 785 insertions, 269 deletions
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 84ffbac36..734669893 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -7,8 +7,8 @@ import { Cast, FieldValue, PromiseValue, NumCast } from '../../../new_fields/Typ import { Doc, FieldResult, Opt, DocListCast } from '../../../new_fields/Doc'; import { listSpec } from '../../../new_fields/Schema'; import { List } from '../../../new_fields/List'; -import { Id } from '../../../new_fields/RefField'; import { SelectionManager } from '../../util/SelectionManager'; +import { Id } from '../../../new_fields/FieldSymbols'; export enum CollectionViewType { Invalid, @@ -16,6 +16,7 @@ export enum CollectionViewType { Schema, Docking, Tree, + Stacking } export interface CollectionRenderProps { @@ -119,7 +120,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { // 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.SetOnPrototype(doc, "zoomBasis", zoom); + // Doc.GetProto(doc).zoomBasis = zoom; } } return true; @@ -127,6 +128,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { @action.bound removeDocument(doc: Doc): boolean { + SelectionManager.DeselectAll(); const props = this.props; //TODO This won't create the field if it doesn't already exist const value = Cast(props.Document[props.fieldKey], listSpec(Doc), []); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 06b262d79..dcc1bd95d 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -6,7 +6,7 @@ import * as ReactDOM from 'react-dom'; import Measure from "react-measure"; import * as GoldenLayout from "../../../client/goldenLayout"; import { Doc, Field, Opt, DocListCast } from "../../../new_fields/Doc"; -import { FieldId, Id } from "../../../new_fields/RefField"; +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"; @@ -21,6 +21,8 @@ import React = require("react"); import { ParentDocSelector } from './ParentDocumentSelector'; import { DocumentManager } from '../../util/DocumentManager'; import { CollectionViewType } from './CollectionBaseView'; +import { Id } from '../../../new_fields/FieldSymbols'; +import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; @observer export class CollectionDockingView extends React.Component<SubCollectionViewProps> { @@ -75,7 +77,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp @undoBatch @action - public CloseRightSplit(document: Doc): boolean { + 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) => { @@ -118,7 +120,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp // Creates a vertical split on the right side of the docking view, and then adds the Document to that split // @action - public AddRightSplit(document: Doc, minimize: boolean = false) { + public AddRightSplit = (document: Doc, minimize: boolean = false) => { let docs = Cast(this.props.Document.data, listSpec(Doc)); if (docs) { docs.push(document); @@ -155,7 +157,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp return newContentItem; } @action - public AddTab(stack: any, document: Doc) { + public AddTab = (stack: any, document: Doc) => { let docs = Cast(this.props.Document.data, listSpec(Doc)); if (docs) { docs.push(document); @@ -331,7 +333,8 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp let counter: any = this.htmlToElement(`<span class="messageCounter">0</div>`); tab.element.append(counter); let upDiv = document.createElement("span"); - ReactDOM.render(<ParentDocSelector Document={doc} />, upDiv); + const stack = tab.contentItem.parent; + ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, location) => CollectionDockingView.Instance.AddTab(stack, doc)} />, upDiv); tab.reactComponents = [upDiv]; tab.element.append(upDiv); counter.DashDocId = tab.contentItem.config.props.documentId; @@ -412,10 +415,14 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { @observable private _panelWidth = 0; @observable private _panelHeight = 0; @observable private _document: Opt<Doc>; - _stack: any; + 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); - this._stack = (this.props as any).glContainer.parent.parent; DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc)); } @@ -432,7 +439,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { 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 / scale); + return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale); } return Transform.Identity(); } @@ -444,7 +451,6 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { 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); - console.log("Scaling = " + scaling); return scaling; } get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; } @@ -464,7 +470,6 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { <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} - toggleMinimized={emptyFunction} bringToFront={emptyFunction} addDocument={undefined} removeDocument={undefined} diff --git a/src/client/views/collections/CollectionPDFView.scss b/src/client/views/collections/CollectionPDFView.scss index f6fb79582..50201bae8 100644 --- a/src/client/views/collections/CollectionPDFView.scss +++ b/src/client/views/collections/CollectionPDFView.scss @@ -1,49 +1,56 @@ .collectionPdfView-buttonTray { - top : 15px; - left : 20px; - position: relative; + top: 15px; + left: 20px; + position: relative; transform-origin: left top; position: absolute; } + .collectionPdfView-thumb { - width:25px; - height:25px; + width: 25px; + height: 25px; transform-origin: left top; position: absolute; background: darkgray; } + .collectionPdfView-slider { - width:25px; - height:25px; + width: 25px; + height: 25px; transform-origin: left top; position: absolute; background: lightgray; } -.collectionPdfView-cont{ + +.collectionPdfView-cont { width: 100%; height: 100%; - position: absolute; + position: absolute; top: 0; - left:0; + left: 0; + z-index: -1; } + .collectionPdfView-cont-dragging { span { user-select: none; } } + .collectionPdfView-backward { - color : white; + color: white; font-size: 24px; - top :0px; - left : 0px; + top: 0px; + left: 0px; position: absolute; background-color: rgba(50, 50, 50, 0.2); } + .collectionPdfView-forward { - color : white; + color: white; font-size: 24px; - top :0px; - left : 45px; + top: 0px; + left: 45px; position: absolute; background-color: rgba(50, 50, 50, 0.2); }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx index a6614da21..5e51437a4 100644 --- a/src/client/views/collections/CollectionPDFView.tsx +++ b/src/client/views/collections/CollectionPDFView.tsx @@ -8,7 +8,7 @@ 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/RefField"; +import { Id } from "../../../new_fields/FieldSymbols"; @observer diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index df5c52d01..186e006f3 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -11,6 +11,7 @@ position: absolute; width: 100%; height: 100%; + overflow: hidden; .collectionSchemaView-cellContents { height: $MAX_ROW_HEIGHT; diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index f15da41ff..11d71d023 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -2,7 +2,7 @@ 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 } from "mobx"; +import { action, computed, observable, untracked, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table"; import { MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss'; @@ -20,15 +20,22 @@ import { FieldView, FieldViewProps } from "../nodes/FieldView"; import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; import { Opt, Field, Doc, DocListCastAsync, DocListCast } from "../../../new_fields/Doc"; -import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; +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/RefField"; +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 { undoBatch } from "../../util/UndoManager"; +library.add(faCog); +library.add(faPlus); // bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657 @@ -51,7 +58,7 @@ class KeyToggle extends React.Component<{ keyName: string, checked: boolean, tog @observer export class CollectionSchemaView extends CollectionSubView(doc => doc) { private _mainCont?: HTMLDivElement; - private _startSplitPercent = 0; + private _startPreviewWidth = 0; private DIVIDER_WIDTH = 4; @observable _columns: Array<string> = ["title", "data", "author"]; @@ -59,16 +66,18 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @observable _columnsPercentage = 0; @observable _keys: string[] = []; @observable _newKeyName: string = ""; + @observable previewScript: string = ""; - @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage); } + @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); } + @computed get previewHeight() { return () => this.props.PanelHeight() - 2 * this.borderWidth; } + @computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH - this.previewWidth(); } @computed get columns() { return Cast(this.props.Document.schemaColumns, listSpec("string"), []); } @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } - @computed get tableColumns() { return this.columns.map(col => { const ref = React.createRef<HTMLParagraphElement>(); return { - Header: <p ref={ref} onPointerDown={SetupDrag(ref, () => this.onHeaderDrag(col))}>{col}</p>, + Header: <p ref={ref} onPointerDown={SetupDrag(ref, () => this.onHeaderDrag(col), undefined, "copy")}>{col}</p>, accessor: (doc: Doc) => doc ? doc[col] : 0, id: col }; @@ -76,6 +85,15 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } onHeaderDrag = (columnName: string) => { + let schemaDoc = Cast(this.props.Document.schemaDoc, Doc); + if (schemaDoc instanceof Doc) { + let columnDocs = DocListCast(schemaDoc.data); + if (columnDocs) { + let ddoc = columnDocs.find(doc => doc.title === columnName); + if (ddoc) + return ddoc; + } + } return this.props.Document; } @@ -100,7 +118,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { let reference = React.createRef<HTMLDivElement>(); let onItemDown = (e: React.PointerEvent) => (this.props.CollectionView.props.isSelected() ? - SetupDrag(reference, () => props.Document, this.props.moveDocument)(e) : undefined); + SetupDrag(reference, () => props.Document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e) : undefined); let applyToDoc = (doc: Doc, run: (args?: { [name: string]: any }) => any) => { const res = run({ this: doc }); if (!res.success) return false; @@ -115,22 +133,20 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { height={Number(MAX_ROW_HEIGHT)} GetValue={() => { let field = props.Document[props.fieldKey]; - if (field) { - //TODO Types - // return field.ToScriptString(); - return String(field); + if (Field.IsField(field)) { + return Field.toScriptString(field); } return ""; }} SetValue={(value: string) => { - let script = CompileScript(value, { addReturn: true, params: { this: Document.name } }); + let script = CompileScript(value, { addReturn: true, params: { this: Doc.name } }); if (!script.compiled) { return false; } return applyToDoc(props.Document, script.run); }} OnFillDown={async (value: string) => { - let script = CompileScript(value, { addReturn: true, params: { this: Document.name } }); + let script = CompileScript(value, { addReturn: true, params: { this: Doc.name } }); if (!script.compiled) { return; } @@ -187,30 +203,31 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { //toggles preview side-panel of schema @action - toggleExpander = (event: React.ChangeEvent<HTMLInputElement>) => { - this.props.Document.schemaSplitPercentage = this.splitPercentage === 0 ? 33 : 0; + toggleExpander = () => { + this.props.Document.schemaPreviewWidth = this.previewWidth() === 0 ? Math.min(this.tableWidth / 3, 200) : 0; } + onDividerDown = (e: React.PointerEvent) => { + this._startPreviewWidth = this.previewWidth(); + e.stopPropagation(); + e.preventDefault(); + document.addEventListener("pointermove", this.onDividerMove); + document.addEventListener('pointerup', this.onDividerUp); + } @action onDividerMove = (e: PointerEvent): void => { let nativeWidth = this._mainCont!.getBoundingClientRect(); - this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100)); + this.props.Document.schemaPreviewWidth = Math.min(nativeWidth.right - nativeWidth.left - 40, + this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0]); } @action onDividerUp = (e: PointerEvent): void => { document.removeEventListener("pointermove", this.onDividerMove); document.removeEventListener('pointerup', this.onDividerUp); - if (this._startSplitPercent === this.splitPercentage) { - this.props.Document.schemaSplitPercentage = this.splitPercentage === 0 ? 33 : 0; + if (this._startPreviewWidth === this.previewWidth()) { + this.toggleExpander(); } } - onDividerDown = (e: React.PointerEvent) => { - this._startSplitPercent = this.splitPercentage; - e.stopPropagation(); - e.preventDefault(); - document.addEventListener("pointermove", this.onDividerMove); - document.addEventListener('pointerup', this.onDividerUp); - } onPointerDown = (e: React.PointerEvent): void => { if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) { @@ -237,16 +254,17 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { csv = csv.substr(0, csv.length - 1) + "\n"; let self = this; DocListCast(this.props.Document.data).map(doc => { - csv += self.columns.reduce((val, col) => val + (doc[col] ? doc[col]!.toString() : "") + ",", ""); + csv += self.columns.reduce((val, col) => val + (doc[col] ? doc[col]!.toString() : "0") + ",", ""); csv = csv.substr(0, csv.length - 1) + "\n"; }); csv.substring(0, csv.length - 1); let dbName = StrCast(this.props.Document.title); let res = await Gateway.Instance.PostSchema(csv, dbName); if (self.props.CollectionView.props.addDocument) { - let schemaDoc = await Docs.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }); + let schemaDoc = await Docs.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document }); if (schemaDoc) { - self.props.CollectionView.props.addDocument(schemaDoc, false); + //self.props.CollectionView.props.addDocument(schemaDoc, false); + self.props.Document.schemaDoc = schemaDoc; } } } @@ -262,63 +280,16 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { this._newKeyName = e.currentTarget.value; } - @observable previewScript: string = ""; - @action - onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this.previewScript = e.currentTarget.value; - } - @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; } - get tableWidth() { return (this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH) * (1 - this.splitPercentage / 100); } - get previewRegionHeight() { return this.props.PanelHeight() - 2 * this.borderWidth; } - get previewRegionWidth() { return (this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH) * this.splitPercentage / 100; } - - private previewDocNativeWidth = () => Cast(this.previewDocument!.nativeWidth, "number", this.previewRegionWidth); - private previewDocNativeHeight = () => Cast(this.previewDocument!.nativeHeight, "number", this.previewRegionHeight); - private previewContentScaling = () => { - let wscale = this.previewRegionWidth / (this.previewDocNativeWidth() ? this.previewDocNativeWidth() : this.previewRegionWidth); - if (wscale * this.previewDocNativeHeight() > this.previewRegionHeight) { - return this.previewRegionHeight / (this.previewDocNativeHeight() ? this.previewDocNativeHeight() : this.previewRegionHeight); - } - return wscale; - } - private previewPanelWidth = () => this.previewDocNativeWidth() * this.previewContentScaling(); - private previewPanelHeight = () => this.previewDocNativeHeight() * this.previewContentScaling(); - get previewPanelCenteringOffset() { return (this.previewRegionWidth - this.previewDocNativeWidth() * this.previewContentScaling()) / 2; } + getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate( - - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth - this.previewPanelCenteringOffset, - - this.borderWidth).scale(1 / this.previewContentScaling()) + - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth); - @computed - get previewPanel() { - // let doc = CompileScript(this.previewScript, { this: selected }, true)(); - const previewDoc = this.previewDocument; - return (<div className="collectionSchemaView-previewRegion" style={{ width: `${Math.max(0, this.previewRegionWidth - 1)}px` }}> - {!previewDoc || !this.previewRegionWidth ? (null) : ( - <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}> - <DocumentView Document={previewDoc} isTopMost={false} selectOnLoad={false} - toggleMinimized={emptyFunction} - addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} - ScreenToLocalTransform={this.getPreviewTransform} - ContentScaling={this.previewContentScaling} - PanelWidth={this.previewPanelWidth} PanelHeight={this.previewPanelHeight} - ContainingCollectionView={this.props.CollectionView} - focus={emptyFunction} - parentActive={this.props.active} - whenActiveChanged={this.props.whenActiveChanged} - bringToFront={emptyFunction} - addDocTab={this.props.addDocTab} - /> - </div>)} - <input className="collectionSchemaView-input" value={this.previewScript} onChange={this.onPreviewScriptChange} - style={{ left: `calc(50% - ${Math.min(75, (previewDoc ? this.previewPanelWidth() / 2 : 75))}px)` }} /> - </div>); - } get documentKeysCheckList() { const docs = DocListCast(this.props.Document[this.props.fieldKey]); @@ -344,7 +315,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { <div id="schema-options-header"><h5><b>Options</b></h5></div> <div id="options-flyout-div"> <h6 className="schema-options-subHeader">Preview Window</h6> - <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.splitPercentage !== 0} onChange={this.toggleExpander} /> Show Preview </div> + <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} /> Show Preview </div> <h6 className="schema-options-subHeader" >Displayed Columns</h6> <ul id="schema-col-checklist" > {this.documentKeysCheckList} @@ -359,31 +330,130 @@ 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 get dividerDragger() { - return this.splitPercentage === 0 ? (null) : + return this.previewWidth() === 0 ? (null) : <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} + /> + } + @action + setPreviewScript = (script: string) => { + this.previewScript = script; + } render() { - library.add(faCog); - library.add(faPlus); - const children = this.childDocs; + 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}> - <div className="collectionSchemaView-tableContainer" style={{ width: `${this.tableWidth}px` }}> - <ReactTable data={children} page={0} pageSize={children.length} showPagination={false} - columns={this.tableColumns} - column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }} - getTrProps={this.getTrProps} - /> - </div> + {this.reactTable} {this.dividerDragger} - {this.previewPanel} + {!this.previewWidth() ? (null) : this.previewPanel} {this.tableOptionsPanel} </div> ); } +} +interface CollectionSchemaPreviewProps { + Document?: Doc; + width: () => number; + height: () => number; + CollectionView: CollectionView | CollectionPDFView | CollectionVideoView; + getTransform: () => Transform; + addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; + removeDocument: (document: Doc) => boolean; + active: () => boolean; + whenActiveChanged: (isActive: boolean) => void; + addDocTab: (document: Doc, where: string) => void; + setPreviewScript: (script: string) => void; + previewScript?: string; +} + +@observer +export class CollectionSchemaPreview extends React.Component<CollectionSchemaPreviewProps>{ + 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 = () => { + let wscale = this.props.width() / (this.nativeWidth ? this.nativeWidth : this.props.width()); + if (wscale * this.nativeHeight > this.props.height()) { + return this.props.height() / (this.nativeHeight ? this.nativeHeight : this.props.height()); + } + 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); + } + @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; + }); + } + } + 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() }}> + {!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} + ScreenToLocalTransform={this.getTransform} + ContentScaling={this.contentScaling} + 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} + /> + </div>)} + {input} + </div>); + } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss new file mode 100644 index 000000000..4d84aaaa9 --- /dev/null +++ b/src/client/views/collections/CollectionStackingView.scss @@ -0,0 +1,51 @@ +@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%; + margin: 5% 2.5%; + padding-left: 2.5%; + height: auto; + } + + .collectionStackingView-flexCont { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + } + + .collectionStackingView-masonrySingle, .collectionStackingView-masonryGrid{ + width:100%; + height:100%; + position: absolute; + } + .collectionStackingView-masonryGrid { + display:grid; + } + + .collectionStackingView-description { + font-size: 100%; + margin-bottom: 1vw; + padding: 10px; + height: 2vw; + width: 100%; + font-family: $sans-serif; + background: $dark-color; + color: $light-color; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx new file mode 100644 index 000000000..da7ea50c6 --- /dev/null +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -0,0 +1,179 @@ +import React = require("react"); +import { action, computed, IReactionDisposer, reaction } 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 { CollectionSchemaPreview } from "./CollectionSchemaView"; +import "./CollectionStackingView.scss"; +import { CollectionSubView } from "./CollectionSubView"; + +@observer +export class CollectionStackingView extends CollectionSubView(doc => doc) { + _masonryGridRef: HTMLDivElement | null = null; + _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); } + + 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 }); + } + componentWillUnmount() { + if (this._heightDisposer) this._heightDisposer(); + } + + @action + moveDocument = (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean): boolean => { + this.props.removeDocument(doc); + addDocument(doc); + return true; + } + getDocTransform(doc: Doc, dref: HTMLDivElement) { + let { scale, translateX, translateY } = Utils.GetScreenTransform(dref); + let outerXf = Utils.GetScreenTransform(this._masonryGridRef!); + let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); + return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]).scale(NumCast(doc.width, 1) / this.columnWidth); + } + 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" + key={d[Id]} + ref={dref} + style={{ marginTop: `${i ? 2 * this.gridGap : 0}px`, width: colWidth(), height: rowHeight(), marginLeft: margin, marginRight: margin }} > + <CollectionSchemaPreview + Document={d} + width={colWidth} + height={rowHeight} + getTransform={dxf} + 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={emptyFunction} + previewScript={script}> + </CollectionSchemaPreview> + </div>; + }); + } + @computed + get children() { + return this.childDocs.filter(d => !d.isMinimized).map(d => { + 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. + } + 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} + addDocument={this.props.addDocument} + 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} + addDocTab={this.props.addDocTab} + bringToFront={emptyFunction} + whenActiveChanged={this.props.whenActiveChanged} + collapseToPoint={this.collapseToPoint} + /> + </div>); + }) + } + 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))); + return ( + <div className="collectionStackingView" style={{ height: "100%" }} + ref={this.createRef} 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}`, + 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` + }} + > + {this.singleColumn ? this.singleColumnChildren : this.children} + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 864fdfa4b..be37efd3d 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -16,9 +16,8 @@ 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 { ObjectField } from "../../../new_fields/ObjectField"; -import CursorField, { CursorPosition, CursorMetadata } from "../../../new_fields/CursorField"; -import { url } from "inspector"; +import CursorField from "../../../new_fields/CursorField"; +import { DocumentManager } from "../../util/DocumentManager"; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; @@ -72,7 +71,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.data.metadata.id === id)) > -1) { cursors[ind].setPosition(pos); } else { - let entry = new CursorField({ metadata: { id: id, identifier: email }, position: pos }); + let entry = new CursorField({ metadata: { id: id, identifier: email, timestamp: Date.now() }, position: pos }); cursors.push(entry); } } @@ -131,7 +130,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { options.dropAction = "copy"; } if (type.indexOf("html") !== -1) { - if (path.includes('localhost')) { + if (path.includes(window.location.hostname)) { let s = path.split('/'); let id = s[s.length - 1]; DocServer.GetRefField(id).then(field => { @@ -155,6 +154,10 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { @undoBatch @action protected onDrop(e: React.DragEvent, options: DocumentOptions): 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; + } let html = e.dataTransfer.getData("text/html"); let text = e.dataTransfer.getData("text/plain"); @@ -164,6 +167,13 @@ 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.prepend("/doc/"), "").split("?")[0]; + DocServer.GetRefField(docid).then(f => (f instanceof Doc) && this.props.addDocument(f, false)); + return; + } if (html && html.indexOf("<img") !== 0 && !html.startsWith("<a")) { let htmlDoc = Docs.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text }); this.props.addDocument(htmlDoc, false); @@ -210,7 +220,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { }).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: 600, width: 300, title: dropFileName }); + let docPromise = this.getDocumentFromType(type, path, { ...options, nativeWidth: 300, width: 300, title: dropFileName }); docPromise.then(doc => doc && this.props.addDocument(doc)); })); diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 5f82137c6..458030b28 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -46,7 +46,14 @@ .docContainer:hover { .treeViewItem-openRight { - display: inline; + display: inline-block; + height:13px; + // display: inline; + svg { + display:block; + padding:0px; + margin: 0px; + } } } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 72fa69cb1..48da52ffa 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -9,9 +9,9 @@ import { CollectionSubView } from "./CollectionSubView"; import "./CollectionTreeView.scss"; import React = require("react"); import { Document, listSpec } from '../../../new_fields/Schema'; -import { Cast, StrCast, BoolCast, FieldValue } from '../../../new_fields/Types'; +import { Cast, StrCast, BoolCast, FieldValue, NumCast } from '../../../new_fields/Types'; import { Doc, DocListCast } from '../../../new_fields/Doc'; -import { Id } from '../../../new_fields/RefField'; +import { Id } from '../../../new_fields/FieldSymbols'; import { ContextMenu } from '../ContextMenu'; import { undoBatch } from '../../util/UndoManager'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; @@ -19,6 +19,7 @@ import { CollectionDockingView } from './CollectionDockingView'; import { DocumentManager } from '../../util/DocumentManager'; import { Docs } from '../../documents/Documents'; import { MainView } from '../MainView'; +import { CollectionViewType } from './CollectionBaseView'; export interface TreeViewProps { @@ -26,6 +27,7 @@ export interface TreeViewProps { deleteDoc: (doc: Doc) => void; moveDocument: DragManager.MoveFunction; dropAction: "alias" | "copy" | undefined; + addDocTab: (doc: Doc, where: string) => void; } export enum BulletType { @@ -53,7 +55,7 @@ class TreeView extends React.Component<TreeViewProps> { if (this.props.document.dockingConfig) { MainView.Instance.openWorkspace(this.props.document); } else { - CollectionDockingView.Instance.AddRightSplit(this.props.document); + this.props.addDocTab(this.props.document, "openRight"); } } @@ -111,7 +113,7 @@ class TreeView extends React.Component<TreeViewProps> { let editableView = (titleString: string) => (<EditableView oneLine={!this._isOver ? true : false} - display={"inline-block"} + display={"inline"} contents={titleString} height={36} GetValue={() => StrCast(this.props.document.title)} @@ -121,11 +123,11 @@ class TreeView extends React.Component<TreeViewProps> { return true; }} />); - let dataDocs = Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc), []); + let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc), []) : []; let openRight = dataDocs && dataDocs.indexOf(this.props.document) !== -1 ? (null) : ( <div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}> <FontAwesomeIcon icon="angle-right" size="lg" /> - <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} @@ -138,21 +140,19 @@ class TreeView extends React.Component<TreeViewProps> { } onWorkspaceContextMenu = (e: React.MouseEvent): void => { - if (!e.isPropagationStopped() && this.props.document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 + 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 Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.document) }); - ContextMenu.Instance.addItem({ - description: "Open Fields", event: () => CollectionDockingView.Instance.AddRightSplit(Docs.KVPDocument(this.props.document, - { title: this.props.document.title + ".kvp", width: 300, height: 300 })) - }); - 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 Fields", event: () => this.props.addDocTab(Docs.KVPDocument(this.props.document, { width: 300, height: 300 }), "onRight"), icon: "layer-group" }); + 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: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)) }); + } else { + ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.props.document)) }); } - ContextMenu.Instance.addItem({ - description: "Delete", event: undoBatch(() => { - this.props.deleteDoc(this.props.document); - }) - }); ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); e.stopPropagation(); } @@ -180,7 +180,7 @@ class TreeView extends React.Component<TreeViewProps> { {(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)} + {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 { @@ -197,9 +197,9 @@ class TreeView extends React.Component<TreeViewProps> { </li> </div>; } - public static GetChildElements(docs: Doc[], allowMinimized: boolean, remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType) { + 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} />); + <TreeView document={child} key={child[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} addDocTab={addDocTab} />); } } @@ -213,11 +213,10 @@ export class CollectionTreeView extends CollectionSubView(Document) { } } onContextMenu = (e: React.MouseEvent): void => { - if (!e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 + // 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 ContextMenu.Instance.addItem({ description: "Create Workspace", event: undoBatch(() => MainView.Instance.createNewWorkspace()) }); - } - if (!ContextMenu.Instance.getItems().some(item => item.description === "Delete")) { - ContextMenu.Instance.addItem({ description: "Delete", event: undoBatch(() => this.remove(this.props.Document)) }); + ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.remove(this.props.Document)) }); } } render() { @@ -225,7 +224,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { if (!this.childDocs) { return (null); } - let childElements = TreeView.GetChildElements(this.childDocs, false, this.remove, this.props.moveDocument, dropAction); + let childElements = TreeView.GetChildElements(this.childDocs, false, this.remove, this.props.moveDocument, dropAction, this.props.addDocTab); return ( <div id="body" className="collectionTreeView-dropTarget" diff --git a/src/client/views/collections/CollectionVideoView.scss b/src/client/views/collections/CollectionVideoView.scss index db8b84832..9d2c23d3e 100644 --- a/src/client/views/collections/CollectionVideoView.scss +++ b/src/client/views/collections/CollectionVideoView.scss @@ -5,7 +5,7 @@ position: inherit; top: 0; left:0; - + z-index: -1; } .collectionVideoView-time{ color : white; diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx index 9ab959f3c..7853544d5 100644 --- a/src/client/views/collections/CollectionVideoView.tsx +++ b/src/client/views/collections/CollectionVideoView.tsx @@ -1,4 +1,5 @@ import { action, observable, trace } from "mobx"; +import * as htmlToImage from "html-to-image"; import { observer } from "mobx-react"; import { ContextMenu } from "../ContextMenu"; import { CollectionViewType, CollectionBaseView, CollectionRenderProps } from "./CollectionBaseView"; @@ -6,10 +7,14 @@ import React = require("react"); import "./CollectionVideoView.scss"; import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; -import { emptyFunction } from "../../../Utils"; -import { Id } from "../../../new_fields/RefField"; +import { emptyFunction, Utils } from "../../../Utils"; +import { Id } from "../../../new_fields/FieldSymbols"; import { VideoBox } from "../nodes/VideoBox"; -import { NumCast } from "../../../new_fields/Types"; +import { NumCast, Cast, StrCast } from "../../../new_fields/Types"; +import { VideoField } from "../../../new_fields/URLField"; +import { SearchBox } from "../SearchBox"; +import { DocServer } from "../../DocServer"; +import { Docs, DocUtils } from "../../documents/Documents"; @observer @@ -67,6 +72,43 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { 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 } + + let field = Cast(this.props.Document[this.props.fieldKey], VideoField); + if (field) { + let url = field.url.href; + ContextMenu.Instance.addItem({ + description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" + }); + } + let width = NumCast(this.props.Document.width); + let height = NumCast(this.props.Document.height); + ContextMenu.Instance.addItem({ + description: "Take Snapshot", event: async () => { + var canvas = document.createElement('canvas'); + canvas.width = 640; + canvas.height = 640 * NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.nativeWidth); + var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions + ctx && ctx.drawImage(this._videoBox!.player!, 0, 0, canvas.width, canvas.height); + + //convert to desired file format + var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' + // if you want to preview the captured image, + + 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.prepend(returnedFilename); + let imageSummary = Docs.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-" + }); + this.props.addDocument && this.props.addDocument(imageSummary, false); + DocUtils.MakeLink(imageSummary, this.props.Document); + } + }); + }, + icon: "expand-arrows-alt" + }); } setVideoBox = (videoBox: VideoBox) => { this._videoBox = videoBox; }; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index b9ffc11a2..68eefab4c 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,23 +1,27 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faProjectDiagram, faSquare, faTh, faTree } from '@fortawesome/free-solid-svg-icons'; +import { faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons'; import { observer } from "mobx-react"; import * as React from 'react'; -import { Id } from '../../../new_fields/RefField'; +import { Id } from '../../../new_fields/FieldSymbols'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from '../ContextMenuItem'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from './CollectionBaseView'; import { CollectionDockingView } from "./CollectionDockingView"; +import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; import { CollectionSchemaView } from "./CollectionSchemaView"; +import { CollectionStackingView } from './CollectionStackingView'; import { CollectionTreeView } from "./CollectionTreeView"; -import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; export const COLLECTION_BORDER_WIDTH = 2; library.add(faTh); library.add(faTree); library.add(faSquare); library.add(faProjectDiagram); +library.add(faSignature); +library.add(faThList); @observer export class CollectionView extends React.Component<FieldViewProps> { @@ -29,6 +33,7 @@ export class CollectionView extends React.Component<FieldViewProps> { case CollectionViewType.Schema: return (<CollectionSchemaView {...props} CollectionView={this} />); case CollectionViewType.Docking: return (<CollectionDockingView {...props} CollectionView={this} />); case CollectionViewType.Tree: return (<CollectionTreeView {...props} CollectionView={this} />); + case CollectionViewType.Stacking: return (<CollectionStackingView {...props} CollectionView={this} />); case CollectionViewType.Freeform: default: return (<CollectionFreeFormView {...props} CollectionView={this} />); @@ -40,12 +45,15 @@ export class CollectionView extends React.Component<FieldViewProps> { onContextMenu = (e: React.MouseEvent): void => { if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - ContextMenu.Instance.addItem({ description: "Freeform", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Freeform), icon: "project-diagram" }); + let subItems: ContextMenuProps[] = []; + subItems.push({ description: "Freeform", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Freeform), icon: "signature" }); if (CollectionBaseView.InSafeMode()) { ContextMenu.Instance.addItem({ description: "Test Freeform", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Invalid), icon: "project-diagram" }); } - ContextMenu.Instance.addItem({ description: "Schema", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Schema), icon: "project-diagram" }); - ContextMenu.Instance.addItem({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree), icon: "tree" }); + subItems.push({ description: "Schema", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Schema), icon: "th-list" }); + 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 }); } } diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss index f3c605f3e..2dd3e49f2 100644 --- a/src/client/views/collections/ParentDocumentSelector.scss +++ b/src/client/views/collections/ParentDocumentSelector.scss @@ -1,8 +1,22 @@ .PDS-flyout { position: absolute; z-index: 9999; - background-color: #d3d3d3; + background-color: #eeeeee; box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); min-width: 150px; color: black; + top: 12px; + + padding: 10px; + border-radius: 3px; + + hr { + height: 1px; + margin: 0px; + background-color: gray; + border-top: 0px; + border-bottom: 0px; + border-right: 0px; + border-left: 0px; + } }
\ No newline at end of file diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index 52f7914f3..f11af04a3 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -3,36 +3,64 @@ import './ParentDocumentSelector.scss'; import { Doc } from "../../../new_fields/Doc"; import { observer } from "mobx-react"; import { observable, action, runInAction } from "mobx"; -import { Id } from "../../../new_fields/RefField"; +import { Id } from "../../../new_fields/FieldSymbols"; import { SearchUtil } from "../../util/SearchUtil"; import { CollectionDockingView } from "./CollectionDockingView"; +import { NumCast } from "../../../new_fields/Types"; +import { CollectionViewType } from "./CollectionBaseView"; +type SelectorProps = { Document: Doc, addDocTab(doc: Doc, location: string): void }; @observer -export class SelectorContextMenu extends React.Component<{ Document: Doc }> { - @observable private _docs: Doc[] = []; +export class SelectorContextMenu extends React.Component<SelectorProps> { + @observable private _docs: { col: Doc, target: Doc }[] = []; + @observable private _otherDocs: { col: Doc, target: Doc }[] = []; - constructor(props: { Document: Doc }) { + constructor(props: SelectorProps) { super(props); this.fetchDocuments(); } 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); - runInAction(() => this._docs = docs); + const map: Map<Doc, Doc> = new Map; + const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search(`data_l:"${doc[Id]}"`, true))); + 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.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 })); + }); + } + + getOnClick({ col, target }: { col: Doc, target: Doc }) { + return () => { + col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col; + if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) { + const newPanX = NumCast(target.x) + NumCast(target.width) / NumCast(target.zoomBasis, 1) / 2; + const newPanY = NumCast(target.y) + NumCast(target.height) / NumCast(target.zoomBasis, 1) / 2; + col.panX = newPanX; + col.panY = newPanY; + } + this.props.addDocTab(col, "inTab"); + }; } render() { return ( <> - {this._docs.map(doc => <p><a onClick={() => CollectionDockingView.Instance.AddRightSplit(doc)}>{doc.title}</a></p>)} + <p>Contexts:</p> + {this._docs.map(doc => <p><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)} + {this._otherDocs.length ? <hr></hr> : null} + {this._otherDocs.map(doc => <p><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)} </> ); } } @observer -export class ParentDocSelector extends React.Component<{ Document: Doc }> { +export class ParentDocSelector extends React.Component<SelectorProps> { @observable hover = false; @action @@ -49,13 +77,13 @@ export class ParentDocSelector extends React.Component<{ Document: Doc }> { let flyout; if (this.hover) { flyout = ( - <div className="PDS-flyout"> - <SelectorContextMenu Document={this.props.Document} /> + <div className="PDS-flyout" title=" "> + <SelectorContextMenu {...this.props} /> </div> ); } return ( - <span style={{ position: "relative", display: "inline-block" }} + <span style={{ position: "relative", display: "inline-block", paddingLeft: "5px", paddingRight: "5px" }} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}> <p>^</p> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss index 737ffba7d..7a0fd2b31 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss @@ -10,3 +10,9 @@ transform: translate(10000px,10000px); pointer-events: all; } +.collectionfreeformlinkview-linkText { + stroke: rgb(0,0,0); + opacity: 0.5; + transform: translate(10000px,10000px); + pointer-events: all; +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 63d2f7642..301b769af 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -40,18 +40,24 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo let l = this.props.LinkDocs; let a = this.props.A; let b = this.props.B; - let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / 2); - let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / 2); - let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / 2); - let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / 2); + let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / NumCast(a.zoomBasis, 1) / 2); + let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / NumCast(a.zoomBasis, 1) / 2); + 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); return ( <> - <line key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine" - style={{ strokeWidth: `${l.length / 2}` }} + <line key="linkLine" className="collectionfreeformlinkview-linkLine" + style={{ strokeWidth: `${2 * l.length / 2}` }} x1={`${x1}`} y1={`${y1}`} x2={`${x2}`} y2={`${y2}`} /> - <circle key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkCircle" - cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={5} onPointerDown={this.onPointerDown} /> + {/* <circle key="linkCircle" className="collectionfreeformlinkview-linkCircle" + cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={8} onPointerDown={this.onPointerDown} /> */} + <text key="linkText" textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${(x1 + x2) / 2}`} y={`${(y1 + y2) / 2}`}> + {text} + </text> </> ); } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index d5ce4e1e7..a43c5f241 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -11,7 +11,7 @@ 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/RefField"; +import { Id } from "../../../../new_fields/FieldSymbols"; @observer export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> { @@ -32,8 +32,8 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP let srcTarg = srcDoc; let x1 = NumCast(srcDoc.x); let x2 = NumCast(dstDoc.x); - let x1w = NumCast(srcDoc.width, -1); - let x2w = NumCast(dstDoc.width, -1); + 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 => { @@ -60,12 +60,12 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP } }; } + 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), []); - if (dstBrushDocs === undefined) dstTarg.brushingDocs = dstBrushDocs = new List<Doc>(); - else brushAction(dstBrushDocs); - if (srcBrushDocs === undefined) srcTarg.brushingDocs = srcBrushDocs = new List<Doc>(); - else brushAction(srcBrushDocs); + brushAction(dstBrushDocs); + brushAction(srcBrushDocs); } }); }); @@ -100,21 +100,27 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP 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 => - drawnPairs.reduce((found, drawnPair) => { - let match = (possiblePair.a === drawnPair.a && possiblePair.b === drawnPair.b); + possiblePairs.map(possiblePair => { + if (!drawnPairs.reduce((found, drawnPair) => { + let match1 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.a) && Doc.AreProtosEqual(possiblePair.b, drawnPair.b)); + let match2 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.b) && Doc.AreProtosEqual(possiblePair.b, drawnPair.a)); + let match = match1 || match2; if (match && !drawnPair.l.reduce((found, link) => found || link[Id] === connection.l[Id], false)) { drawnPair.l.push(connection.l); } return match || found; - }, false) - || - drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] }) - ); + }, 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] }) + } + }); return drawnPairs; }, [] as { a: Doc, b: Doc, l: Doc[] }[]); - return connections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} - removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />); + 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} />; + }); } render() { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx index 642118d75..2838b7905 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx @@ -9,6 +9,7 @@ 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> { @@ -23,7 +24,9 @@ export class CollectionFreeFormRemoteCursors extends React.Component<CollectionV let cursors = Cast(doc.cursors, listSpec(CursorField)); - return (cursors || []).filter(cursor => cursor.data.metadata.id !== id); + const now = mobxUtils.now(); + // const now = Date.now(); + return (cursors || []).filter(cursor => cursor.data.metadata.id !== id && (now - cursor.data.metadata.timestamp) < 1000); } private crosshairs?: HTMLCanvasElement; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 063c9e2cf..e10ba9d7e 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -25,6 +25,9 @@ height: 100%; width: 100%; } + >.jsx-parser { + z-index:0; + } //nested freeform views // .collectionfreeformview-container { @@ -52,6 +55,10 @@ position: inherit; height: 100%; } + + >.jsx-parser { + z-index:0; + } .formattedTextBox-cont { background: $light-color-secondary; @@ -63,6 +70,7 @@ border-radius: $border-radius; box-sizing: border-box; position:absolute; + z-index: -1; .marqueeView { overflow: hidden; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 9cb8443f4..9d19df540 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -22,9 +22,10 @@ import { createSchema, makeInterface, listSpec } from "../../../../new_fields/Sc import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc"; import { FieldValue, Cast, NumCast, BoolCast } from "../../../../new_fields/Types"; import { pageSchema } from "../../nodes/ImageBox"; -import { Id } from "../../../../new_fields/RefField"; import { InkField, StrokeData } from "../../../../new_fields/InkField"; import { HistoryUtil } from "../../../util/History"; +import { Id } from "../../../../new_fields/FieldSymbols"; +import { DocServer } from "../../../DocServer"; export const panZoomSchema = createSchema({ panX: "number", @@ -135,14 +136,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); if (!this.isAnnotationOverlay) { let minx = docs.length ? NumCast(docs[0].x) : 0; - let maxx = docs.length ? NumCast(docs[0].width) / NumCast(docs[0].zoomBasis) + minx : minx; + let maxx = docs.length ? NumCast(docs[0].width) / NumCast(docs[0].zoomBasis, 1) + minx : minx; let miny = docs.length ? NumCast(docs[0].y) : 0; - let maxy = docs.length ? NumCast(docs[0].height) / NumCast(docs[0].zoomBasis) + miny : miny; + let maxy = docs.length ? NumCast(docs[0].height) / NumCast(docs[0].zoomBasis, 1) + miny : miny; let ranges = docs.filter(doc => doc).reduce((range, doc) => { let x = NumCast(doc.x); - let xe = x + NumCast(doc.width) / NumCast(doc.zoomBasis); + let xe = x + NumCast(doc.width) / NumCast(doc.zoomBasis, 1); let y = NumCast(doc.y); - let ye = y + NumCast(doc.height) / NumCast(doc.zoomBasis); + let ye = y + NumCast(doc.height) / NumCast(doc.zoomBasis, 1); 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]]); @@ -155,12 +156,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { }); } - let panelwidth = this._pwidth / this.zoomScaling() / 2; - let panelheight = this._pheight / this.zoomScaling() / 2; - if (x - dx < ranges[0][0] - panelwidth) x = ranges[0][1] + panelwidth + dx; - if (x - dx > ranges[0][1] + panelwidth) x = ranges[0][0] - panelwidth + dx; - if (y - dy < ranges[1][0] - panelheight) y = ranges[1][1] + panelheight + dy; - if (y - dy > ranges[1][1] + panelheight) y = ranges[1][0] - panelheight + dy; + let panelDim = this.props.ScreenToLocalTransform().transformDirection(this._pwidth / this.zoomScaling(), + this._pheight / this.zoomScaling()); + let panelwidth = panelDim[0]; + let panelheight = panelDim[1]; + if (ranges[0][0] - dx > (this.panX() + panelwidth / 2)) x = ranges[0][1] + panelwidth / 2; + if (ranges[0][1] - dx < (this.panX() - panelwidth / 2)) x = ranges[0][0] - panelwidth / 2; + if (ranges[1][0] - dy > (this.panY() + panelheight / 2)) y = ranges[1][1] + panelheight / 2; + if (ranges[1][1] - dy < (this.panY() - panelheight / 2)) y = ranges[1][0] - panelheight / 2; } this.setPan(x - dx, y - dy); this._lastX = e.pageX; @@ -215,7 +218,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action setPan(panX: number, panY: number) { - this.panDisposer && clearTimeout(this.panDisposer); this.props.Document.panTransformType = "None"; var scale = this.getLocalTransform().inverse().Scale; const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); @@ -227,7 +229,24 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action onDrop = (e: React.DragEvent): void => { var pt = this.getTransform().transformPoint(e.pageX, e.pageY); - super.onDrop(e, { x: pt[0], y: pt[1] }); + 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.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] }); + } } onDragOver = (): void => { @@ -243,7 +262,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { doc.zIndex = docs.length + 1; } - panDisposer?: NodeJS.Timeout; focusDocument = (doc: Doc) => { const panX = this.Document.panX; const panY = this.Document.panY; @@ -265,22 +283,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } } SelectionManager.DeselectAll(); - const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2; - const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2; + const newPanX = NumCast(doc.x) + NumCast(doc.width) / NumCast(doc.zoomBasis, 1) / 2; + const newPanY = NumCast(doc.y) + NumCast(doc.height) / NumCast(doc.zoomBasis, 1) / 2; const newState = HistoryUtil.getState(); 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); - this.panDisposer = setTimeout(() => this.props.Document.panTransformType = "None", 2000); // wait 3 seconds, then reset to false } getDocumentViewProps(document: Doc): DocumentViewProps { return { Document: document, - toggleMinimized: emptyFunction, addDocument: this.props.addDocument, removeDocument: this.props.removeDocument, moveDocument: this.props.moveDocument, @@ -305,7 +321,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let docviews = this.childDocs.reduce((prev, doc) => { if (!(doc instanceof Doc)) return prev; var page = NumCast(doc.page, -1); - if (page === curPage || 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)} />); @@ -324,7 +340,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); } - private childViews = () => [...this.views, <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />]; + 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"; diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 4587c2227..29734fa19 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,5 +1,5 @@ import * as htmlToImage from "html-to-image"; -import { action, computed, observable } from "mobx"; +import { action, computed, observable, trace } from "mobx"; import { observer } from "mobx-react"; import { Docs } from "../../../documents/Documents"; import { SelectionManager } from "../../../util/SelectionManager"; @@ -17,9 +17,11 @@ 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 { Gateway } from "../../../northstar/manager/Gateway"; +import { SearchBox } from "../../SearchBox"; import { DocServer } from "../../../DocServer"; -import { Id } from "../../../../new_fields/RefField"; +import { Id } from "../../../../new_fields/FieldSymbols"; +import { CollectionView } from "../CollectionView"; +import { CollectionViewType } from "../CollectionBaseView"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -83,49 +85,62 @@ export class MarqueeView extends React.Component<MarqueeViewProps> }); })(); } else if (e.key === "b" && e.ctrlKey) { - //heuristically converts pasted text into a table. - // assumes each entry is separated by a tab - // skips all rows until it gets to a row with more than one entry - // assumes that 1st row has header entry for each column - // assumes subsequent rows have entries for each column header OR - // any row that has only one column is a section header-- this header is then added as a column to subsequent rows until the next header - // assumes each cell is a string or a number e.preventDefault(); - (async () => { - let text: string = await navigator.clipboard.readText(); + navigator.clipboard.readText().then(text => { let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); - while (ns.length > 0 && ns[0].split("\t").length < 2) { - ns.splice(0, 1); - } - if (ns.length > 0) { - let columns = ns[0].split("\t"); - let docList: Doc[] = []; - let groupAttr: string | number = ""; - for (let i = 1; i < ns.length - 1; i++) { - let values = ns[i].split("\t"); - if (values.length === 1 && columns.length > 1) { - groupAttr = values[0]; - continue; - } - let doc = new Doc(); - columns.forEach((col, i) => doc[columns[i]] = (values.length > i ? ((values[i].indexOf(Number(values[i]).toString()) !== -1) ? Number(values[i]) : values[i]) : undefined)); - if (groupAttr) { - doc._group = groupAttr; - } - doc.title = i.toString(); - docList.push(doc); - } - let newCol = Docs.SchemaDocument([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 }); - - this.props.addDocument(newCol, false); + if (ns.length === 1 && text.startsWith("http")) { + this.props.addDocument(Docs.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }), false);// paste an image from its URL in the paste buffer + } else { + this.pasteTable(ns, x, y); } - })(); + }); } else { let newBox = Docs.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" }); this.props.addLiveTextDocument(newBox); } e.stopPropagation(); } + //heuristically converts pasted text into a table. + // assumes each entry is separated by a tab + // skips all rows until it gets to a row with more than one entry + // assumes that 1st row has header entry for each column + // assumes subsequent rows have entries for each column header OR + // any row that has only one column is a section header-- this header is then added as a column to subsequent rows until the next header + // assumes each cell is a string or a number + pasteTable(ns: string[], x: number, y: number) { + while (ns.length > 0 && ns[0].split("\t").length < 2) { + ns.splice(0, 1); + } + if (ns.length > 0) { + let columns = ns[0].split("\t"); + let docList: Doc[] = []; + let groupAttr: string | number = ""; + let rowProto = new Doc(); + rowProto.title = rowProto.Id; + rowProto.width = 200; + rowProto.isPrototype = true; + for (let i = 1; i < ns.length - 1; i++) { + let values = ns[i].split("\t"); + if (values.length === 1 && columns.length > 1) { + groupAttr = values[0]; + continue; + } + let docDataProto = Doc.MakeDelegate(rowProto); + docDataProto.isPrototype = true; + columns.forEach((col, i) => docDataProto[columns[i]] = (values.length > i ? ((values[i].indexOf(Number(values[i]).toString()) !== -1) ? Number(values[i]) : values[i]) : undefined)); + if (groupAttr) { + docDataProto._group = groupAttr; + } + docDataProto.title = i.toString(); + let doc = Doc.MakeDelegate(docDataProto); + doc.width = 200; + docList.push(doc); + } + let newCol = Docs.SchemaDocument([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 }); + + this.props.addDocument(newCol, false); + } + } @action onPointerDown = (e: React.PointerEvent): void => { this._downX = this._lastX = e.pageX; @@ -138,7 +153,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> document.addEventListener("pointerup", this.onPointerUp, true); document.addEventListener("keydown", this.marqueeCommand, true); if (e.altKey) { - e.stopPropagation(); + //e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors. e.preventDefault(); } // bcz: do we need this? it kills the context menu on the main collection if !altKey @@ -225,9 +240,10 @@ export class MarqueeView extends React.Component<MarqueeViewProps> this.cleanupInteractions(false); e.stopPropagation(); } - if (e.key === "c" || e.key === "s" || e.key === "e" || e.key === "p") { + if (e.key === "c" || e.key === "s" || e.key === "S" || e.key === "e" || e.key === "p") { this._commandExecuted = true; e.stopPropagation(); + e.preventDefault(); (e as any).propagationIsStopped = true; let bounds = this.Bounds; let selected = this.marqueeSelect(); @@ -254,13 +270,32 @@ export class MarqueeView extends React.Component<MarqueeViewProps> width: bounds.width * zoomBasis, height: bounds.height * zoomBasis, ink: inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined, - title: e.key === "s" ? "-summary-" : e.key === "p" ? "-summary-" : "a nested collection", + title: e.key === "s" || e.key === "S" ? "-summary-" : e.key === "p" ? "-summary-" : "a nested collection", }); + newCollection.zoomBasis = zoomBasis; this.marqueeInkDelete(inkData); - if (e.key === "s" || e.key === "p") { - - htmlToImage.toPng(this._mainCont.current!, { width: bounds.width * zoomBasis, height: bounds.height * zoomBasis, quality: 1 }).then((dataUrl) => { + if (e.key === "s") { + 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.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); + newCollection.proto!.summaryDoc = summary; + selected = [newCollection]; + newCollection.x = bounds.left + bounds.width; + summary.proto!.subBulletDocs = new List<Doc>(selected); + //summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight" + summary.templates = new List<string>([Templates.Bullet.Layout]); + let container = Docs.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, title: "-summary-" }); + container.viewType = CollectionViewType.Stacking; + 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; @@ -268,15 +303,25 @@ export class MarqueeView extends React.Component<MarqueeViewProps> d.page = -1; return d; }); - let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" }); - summary.proto!.thumbnail = new ImageField(new URL(dataUrl)); - summary.proto!.templates = new List<string>([Templates.ImageOverlay(Math.min(50, bounds.width), bounds.height * Math.min(50, bounds.width) / bounds.width, "thumbnail")]); + let summary = Docs.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.prepend(returnedFilename); + let imageSummary = Docs.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); }); } @@ -347,10 +392,10 @@ export class MarqueeView extends React.Component<MarqueeViewProps> } render() { - let p = this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); + let p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0]; return <div className="marqueeView" style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}> <div style={{ position: "relative", transform: `translate(${p[0]}px, ${p[1]}px)` }} > - {!this._visible ? null : this.marqueeDiv} + {this._visible ? this.marqueeDiv : null} <div ref={this._mainCont} style={{ transform: `translate(${-p[0]}px, ${-p[1]}px)` }} > {this.props.children} </div> |
