diff options
Diffstat (limited to 'src/client/views/collections')
15 files changed, 489 insertions, 253 deletions
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 645296d27..84ffbac36 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -1,4 +1,4 @@ -import { action, computed } from 'mobx'; +import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ContextMenu } from '../ContextMenu'; @@ -36,9 +36,20 @@ export interface CollectionViewProps extends FieldViewProps { @observer export class CollectionBaseView extends React.Component<CollectionViewProps> { + @observable private static _safeMode = false; + static InSafeMode() { return this._safeMode; } + static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; } get collectionViewType(): CollectionViewType | undefined { let Document = this.props.Document; let viewField = Cast(Document.viewType, "number"); + if (CollectionBaseView._safeMode) { + if (viewField === CollectionViewType.Freeform) { + return CollectionViewType.Tree; + } + if (viewField === CollectionViewType.Invalid) { + return CollectionViewType.Freeform; + } + } if (viewField !== undefined) { return viewField; } else { @@ -82,12 +93,12 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { } return false; } - @computed get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } + @computed get isAnnotationOverlay() { return this.props.fieldKey === "annotations"; } @action.bound addDocument(doc: Doc, allowDuplicates: boolean = false): boolean { let props = this.props; - var curPage = Cast(props.Document.curPage, "number", -1); + var curPage = NumCast(props.Document.curPage, -1); Doc.SetOnPrototype(doc, "page", curPage); if (curPage >= 0) { Doc.SetOnPrototype(doc, "annotationOn", props.Document); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index efcee9c02..06b262d79 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,11 +1,11 @@ import 'golden-layout/src/css/goldenlayout-base.css'; import 'golden-layout/src/css/goldenlayout-dark-theme.css'; -import { action, observable, reaction } from "mobx"; +import { action, observable, reaction, Lambda } from "mobx"; import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; import Measure from "react-measure"; import * as GoldenLayout from "../../../client/goldenLayout"; -import { Doc, Field, Opt } from "../../../new_fields/Doc"; +import { Doc, Field, Opt, DocListCast } from "../../../new_fields/Doc"; import { FieldId, Id } from "../../../new_fields/RefField"; import { listSpec } from "../../../new_fields/Schema"; import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; @@ -18,6 +18,9 @@ import { DocumentView } from "../nodes/DocumentView"; import "./CollectionDockingView.scss"; import { SubCollectionViewProps } from "./CollectionSubView"; import React = require("react"); +import { ParentDocSelector } from './ParentDocumentSelector'; +import { DocumentManager } from '../../util/DocumentManager'; +import { CollectionViewType } from './CollectionBaseView'; @observer export class CollectionDockingView extends React.Component<SubCollectionViewProps> { @@ -72,32 +75,40 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp @undoBatch @action - public CloseRightSplit(document: Doc) { + public CloseRightSplit(document: Doc): boolean { + let retVal = false; if (this._goldenLayout.root.contentItems[0].isRow) { - this._goldenLayout.root.contentItems[0].contentItems.map((child: any, i: number) => { + retVal = Array.from(this._goldenLayout.root.contentItems[0].contentItems).some((child: any) => { if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" && - child.contentItems[0].config.props.documentId == document[Id]) { + Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)) { child.contentItems[0].remove(); this.layoutChanged(document); - this.stateChanged(); - } else - child.contentItems.map((tab: any, j: number) => { - if (tab.config.component === "DocumentFrameRenderer" && tab.config.props.documentId === document[Id]) { + 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); - this.stateChanged(); + 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('sbcreteChanged'); + this._goldenLayout.emit('stateChanged'); this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); if (removed) CollectionDockingView.Instance._removedDocs.push(removed); this.stateChanged(); @@ -143,6 +154,17 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp return newContentItem; } + @action + public AddTab(stack: any, document: Doc) { + let docs = Cast(this.props.Document.data, listSpec(Doc)); + if (docs) { + docs.push(document); + } + let docContentConfig = CollectionDockingView.makeDocumentConfig(document); + var newContentItem = stack.layoutManager.createContentItem(docContentConfig, this._goldenLayout); + stack.addChild(newContentItem.contentItems[0], undefined); + this.layoutChanged(); + } setupGoldenLayout() { var config = StrCast(this.props.Document.dockingConfig); @@ -157,6 +179,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp 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(); @@ -164,6 +187,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } 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; @@ -177,12 +201,15 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp this._goldenLayout.init(); } } + reactionDisposer?: Lambda; componentDidMount: () => void = () => { if (this._containerRef.current) { - reaction( + 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._ignoreStateChange = ""; @@ -196,12 +223,17 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp 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 = (event: any) => { @@ -238,6 +270,10 @@ 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; + 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, @@ -292,21 +328,23 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } DocServer.GetRefField(tab.contentItem.config.props.documentId).then(async doc => { if (doc instanceof Doc) { - let counter: any = this.htmlToElement(`<div class="messageCounter">0</div>`); + 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); + tab.reactComponents = [upDiv]; + tab.element.append(upDiv); counter.DashDocId = tab.contentItem.config.props.documentId; tab.reactionDisposer = reaction(() => [doc.linkedFromDocs, doc.LinkedToDocs, doc.title], () => { - const lf = Cast(doc.linkedFromDocs, listSpec(Doc), []); - const lt = Cast(doc.linkedToDocs, listSpec(Doc), []); - let count = (lf ? lf.length : 0) + (lt ? lt.length : 0); - counter.innerHTML = count; + 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; } }); } + tab.titleElement[0].Tab = tab; tab.closeElement.off('click') //unbind the current click handler .click(async function () { if (tab.reactionDisposer) { @@ -320,6 +358,14 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp tab.contentItem.remove(); }); } + + tabDestroyed = (tab: any) => { + if (tab.reactComponents) { + for (const ele of tab.reactComponents) { + ReactDOM.unmountComponentAtNode(ele); + } + } + } _removedDocs: Doc[] = []; stackCreated = (stack: any) => { @@ -366,9 +412,10 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { @observable private _panelWidth = 0; @observable private _panelHeight = 0; @observable private _document: Opt<Doc>; - + _stack: any; 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)); } @@ -385,19 +432,37 @@ 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(scale / this.contentScaling()); + return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / 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); + console.log("Scaling = " + scaling); + return scaling; + } get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; } + addDocTab = (doc: Doc, location: string) => { + if (location === "onRight") { + CollectionDockingView.Instance.AddRightSplit(doc); + } else { + CollectionDockingView.Instance.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)` }}> + style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px) scale(${this.scaleToFitMultiplier}, ${this.scaleToFitMultiplier})` }}> <DocumentView key={this._document[Id]} Document={this._document} toggleMinimized={emptyFunction} bringToFront={emptyFunction} @@ -412,6 +477,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { parentActive={returnTrue} whenActiveChanged={emptyFunction} focus={emptyFunction} + addDocTab={this.addDocTab} ContainingCollectionView={undefined} /> </div >); } diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx index b3762206a..a6614da21 100644 --- a/src/client/views/collections/CollectionPDFView.tsx +++ b/src/client/views/collections/CollectionPDFView.tsx @@ -61,7 +61,7 @@ export class CollectionPDFView 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 - ContextMenu.Instance.addItem({ description: "PDFOptions", event: emptyFunction }); + ContextMenu.Instance.addItem({ description: "PDFOptions", event: emptyFunction, icon: "file-pdf" }); } } diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index cfdb3ab22..df5c52d01 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -4,7 +4,7 @@ .collectionSchemaView-container { border-width: $COLLECTION_BORDER_WIDTH; - border-color : $intermediate-color; + border-color: $intermediate-color; border-style: solid; border-radius: $border-radius; box-sizing: border-box; @@ -14,42 +14,52 @@ .collectionSchemaView-cellContents { height: $MAX_ROW_HEIGHT; + + img { + width: auto; + max-height: $MAX_ROW_HEIGHT; + } } - + .collectionSchemaView-previewRegion { position: relative; background: $light-color; float: left; height: 100%; + .collectionSchemaView-previewDoc { height: 100%; - width: 100%; + width: 100%; position: absolute; } + .collectionSchemaView-input { position: absolute; max-width: 150px; width: 100%; bottom: 0px; } + .documentView-node:first-child { position: relative; background: $light-color; } } + .collectionSchemaView-previewHandle { position: absolute; height: 15px; - width: 15px; - z-index: 20; - right: 0; - top: 20px; - background: Black ; + width: 15px; + z-index: 20; + right: 0; + top: 20px; + background: Black; } - .collectionSchemaView-dividerDragger{ - position: relative; - background: black; - float: left; + + .collectionSchemaView-dividerDragger { + position: relative; + background: black; + float: left; height: 37px; width: 20px; z-index: 20; @@ -57,6 +67,7 @@ top: 0; background: $main-accent; } + .collectionSchemaView-columnsHandle { position: absolute; height: 37px; @@ -66,6 +77,7 @@ bottom: 0; background: $main-accent; } + .collectionSchemaView-colDividerDragger { position: relative; box-sizing: border-box; @@ -74,6 +86,7 @@ float: top; width: 100%; } + .collectionSchemaView-dividerDragger { position: relative; box-sizing: border-box; @@ -82,11 +95,13 @@ float: left; height: 100%; } + .collectionSchemaView-tableContainer { position: relative; float: left; height: 100%; } + .ReactTable { // position: absolute; // display: inline-block; // overflow: auto; @@ -95,6 +110,7 @@ background: $light-color; box-sizing: border-box; border: none !important; + .rt-table { overflow-y: auto; overflow-x: auto; @@ -103,42 +119,50 @@ direction: ltr; // direction:rtl; // display:block; } + .rt-tbody { //direction: ltr; direction: rtl; } + .rt-tr-group { direction: ltr; max-height: $MAX_ROW_HEIGHT; } + .rt-td { border-width: 1px; border-right-color: $intermediate-color; + .imageBox-cont { position: relative; max-height: 100%; } + .imageBox-cont img { object-fit: contain; max-width: 100%; height: 100%; } - .videobox-cont { + + .videoBox-cont { object-fit: contain; width: auto; height: 100%; } } } + .ReactTable .rt-thead.-header { background: $intermediate-color; color: $light-color; - text-transform: uppercase; + // text-transform: uppercase; letter-spacing: 2px; font-size: 12px; height: 30px; padding-top: 4px; } + .ReactTable .rt-th, .ReactTable .rt-td { max-height: $MAX_ROW_HEIGHT; @@ -146,32 +170,36 @@ font-size: 13px; text-align: center; } + .ReactTable .rt-tbody .rt-tr-group:last-child { border-bottom: $intermediate-color; border-bottom-style: solid; border-bottom-width: 1; } + .documentView-node-topmost { - text-align:left; + text-align: left; transform-origin: center top; display: inline-block; } + .documentView-node:first-child { background: $light-color; } } + //options menu styling #schemaOptionsMenuBtn { position: absolute; height: 20px; width: 20px; border-radius: 50%; - z-index: 21; + z-index: 21; right: 4px; - top: 4px; + top: 4px; pointer-events: auto; - background-color:black; - display:inline-block; + background-color: black; + display: inline-block; padding: 0px; font-size: 100%; } @@ -185,10 +213,12 @@ ul { padding: 0px; margin: 0px; } + .schema-options-subHeader { color: $intermediate-color; margin-bottom: 5px; } + #schemaOptionsMenuBtn:hover { transform: scale(1.15); } @@ -198,15 +228,15 @@ ul { font-size: 12px; } - #options-flyout-div { +#options-flyout-div { text-align: left; - padding:0px; + padding: 0px; z-index: 100; font-family: $sans-serif; padding-left: 5px; - } +} - #schema-col-checklist { +#schema-col-checklist { overflow: scroll; text-align: left; //background-color: $light-color-secondary; @@ -214,8 +244,8 @@ ul { max-height: 175px; font-family: $sans-serif; font-size: 12px; - } - +} + .Resizer { box-sizing: border-box; @@ -223,6 +253,7 @@ ul { opacity: 0.5; z-index: 1; background-clip: padding-box; + &.horizontal { height: 11px; margin: -5px 0; @@ -230,22 +261,26 @@ ul { border-bottom: 5px solid rgba(255, 255, 255, 0); cursor: row-resize; width: 100%; + &:hover { border-top: 5px solid rgba(0, 0, 0, 0.5); border-bottom: 5px solid rgba(0, 0, 0, 0.5); } } + &.vertical { width: 11px; margin: 0 -5px; border-left: 5px solid rgba(255, 255, 255, 0); border-right: 5px solid rgba(255, 255, 255, 0); cursor: col-resize; + &:hover { border-left: 5px solid rgba(0, 0, 0, 0.5); border-right: 5px solid rgba(0, 0, 0, 0.5); } } + &:hover { -webkit-transition: all 2s ease; transition: all 2s ease; @@ -266,10 +301,12 @@ ul { -ms-flex-direction: column; flex-direction: column; } + header { padding: 1rem; background: #eee; } + footer { padding: 1rem; background: #eee; @@ -283,10 +320,12 @@ ul { display: flex; flex-direction: column; } + header { padding: 1rem; background: #eee; } + footer { padding: 1rem; background: #eee; diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 18319dc77..f15da41ff 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -20,10 +20,13 @@ 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 } from "../../../new_fields/Types"; +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 { Gateway } from "../../northstar/manager/Gateway"; +import { Docs } from "../../documents/Documents"; +import { ContextMenu } from "../ContextMenu"; // bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657 @@ -61,10 +64,25 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @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>, + accessor: (doc: Doc) => doc ? doc[col] : 0, + id: col + }; + }); + } + + onHeaderDrag = (columnName: string) => { + return this.props.Document; + } + renderCell = (rowProps: CellInfo) => { let props: FieldViewProps = { - Document: rowProps.value[0], - fieldKey: rowProps.value[1], + Document: rowProps.original, + fieldKey: rowProps.column.id as string, ContainingCollectionView: this.props.CollectionView, isSelected: returnFalse, select: emptyFunction, @@ -76,24 +94,24 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { whenActiveChanged: emptyFunction, PanelHeight: returnZero, PanelWidth: returnZero, + addDocTab: this.props.addDocTab, }; - let contents = ( - <FieldView {...props} /> - ); + let fieldContentView = <FieldView {...props} />; let reference = React.createRef<HTMLDivElement>(); - let onItemDown = SetupDrag(reference, () => props.Document, this.props.moveDocument); + let onItemDown = (e: React.PointerEvent) => + (this.props.CollectionView.props.isSelected() ? + SetupDrag(reference, () => props.Document, this.props.moveDocument)(e) : undefined); let applyToDoc = (doc: Doc, run: (args?: { [name: string]: any }) => any) => { const res = run({ this: doc }); if (!res.success) return false; - const field = res.result; - doc[props.fieldKey] = field; + doc[props.fieldKey] = res.result; return true; }; return ( <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={props.Document[Id]} ref={reference}> <EditableView display={"inline"} - contents={contents} + contents={fieldContentView} height={Number(MAX_ROW_HEIGHT)} GetValue={() => { let field = props.Document[props.fieldKey]; @@ -118,7 +136,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } const run = script.run; //TODO This should be able to be refactored to compile the script once - const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]) + const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]); val && val.forEach(doc => applyToDoc(doc, run)); }}> </EditableView> @@ -207,6 +225,32 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } } + 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: "Make DB", event: this.makeDB }); + } + } + + @action + makeDB = async () => { + let csv: string = this.columns.reduce((val, col) => val + col + ",", ""); + 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 = 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 }); + if (schemaDoc) { + self.props.CollectionView.props.addDocument(schemaDoc, false); + } + } + } + @action addColumn = () => { this.columns.push(this._newKeyName); @@ -224,10 +268,11 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { this.previewScript = e.currentTarget.value; } + @computed get previewDocument(): Doc | undefined { - const children = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []); + const children = DocListCast(this.props.Document[this.props.fieldKey]); const selected = children.length > this._selectedIndex ? FieldValue(children[this._selectedIndex]) : undefined; - return selected ? (this.previewScript ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : 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; } @@ -253,8 +298,8 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { get previewPanel() { // let doc = CompileScript(this.previewScript, { this: selected }, true)(); const previewDoc = this.previewDocument; - return !previewDoc || !this.previewRegionWidth ? (null) : ( - <div className="collectionSchemaView-previewRegion" style={{ width: `${this.previewRegionWidth}px` }}> + 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} @@ -267,12 +312,12 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { 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, this.previewPanelWidth() / 2)}px)` }} /> - </div> - ); + </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() { @@ -319,20 +364,18 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />; } + + render() { library.add(faCog); library.add(faPlus); - const children = this.children; + const children = this.childDocs; return ( <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel} - onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createTarget}> + 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.columns.map(col => ({ - Header: col, - accessor: (doc: Doc) => [doc, col], - id: col - }))} + columns={this.tableColumns} column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }} getTrProps={this.getTrProps} /> diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index a86d250bd..864fdfa4b 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -18,6 +18,7 @@ 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"; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; @@ -46,7 +47,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { this.createDropTarget(ele); } - get children() { + get childDocs() { //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]); @@ -168,6 +169,11 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { this.props.addDocument(htmlDoc, false); return; } + if (text && text.indexOf("www.youtube.com/watch") !== -1) { + const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/"); + this.props.addDocument(Docs.WebDocument(url, { ...options, width: 300, height: 300 })); + return; + } let batch = UndoManager.StartBatch("collection view drop"); let promises: Promise<void>[] = []; diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 411d67ff7..5f82137c6 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -29,23 +29,24 @@ } .bullet { - float:left; + float: left; position: relative; width: 15px; display: block; color: $intermediate-color; margin-top: 3px; - transform: scale(1.3,1.3); + transform: scale(1.3, 1.3); } .docContainer { margin-left: 10px; display: block; - // width:100%;//width: max-content; + // width:100%;//width: max-content; } + .docContainer:hover { .treeViewItem-openRight { - display:inline; + display: inline; } } @@ -61,10 +62,12 @@ // margin-top: 3px; display: inline; } + .treeViewItem-openRight { margin-left: 5px; - display:none; + display: none; } + .docContainer:hover { .delete-button { display: inline; @@ -73,14 +76,16 @@ } .coll-title { - width:max-content; + width: max-content; display: block; font-size: 24px; } + .collection-child { margin-top: 10px; margin-bottom: 10px; } + .collectionTreeView-keyHeader { font-style: italic; font-size: 8pt; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index d22418b2c..72fa69cb1 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -14,12 +14,11 @@ import { Doc, DocListCast } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/RefField'; import { ContextMenu } from '../ContextMenu'; import { undoBatch } from '../../util/UndoManager'; -import { Main } from '../Main'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; import { CollectionDockingView } from './CollectionDockingView'; import { DocumentManager } from '../../util/DocumentManager'; -import { List } from '../../../new_fields/List'; import { Docs } from '../../documents/Documents'; +import { MainView } from '../MainView'; export interface TreeViewProps { @@ -52,11 +51,11 @@ class TreeView extends React.Component<TreeViewProps> { @undoBatch openRight = async () => { if (this.props.document.dockingConfig) { - Main.Instance.openWorkspace(this.props.document); + MainView.Instance.openWorkspace(this.props.document); } else { CollectionDockingView.Instance.AddRightSplit(this.props.document); } - }; + } get children() { return Cast(this.props.document.data, listSpec(Doc), []); // bcz: needed? .filter(doc => FieldValue(doc)); @@ -112,7 +111,7 @@ class TreeView extends React.Component<TreeViewProps> { let editableView = (titleString: string) => (<EditableView oneLine={!this._isOver ? true : false} - display={"block"} + display={"inline-block"} contents={titleString} height={36} GetValue={() => StrCast(this.props.document.title)} @@ -130,7 +129,7 @@ class TreeView extends React.Component<TreeViewProps> { </div>); return ( <div className="docContainer" ref={reference} onPointerDown={onItemDown} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} - style={{ background: BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0" }} + 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} @@ -140,7 +139,7 @@ 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 - ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => Main.Instance.openWorkspace(this.props.document)) }); + 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, @@ -184,8 +183,9 @@ class TreeView extends React.Component<TreeViewProps> { {TreeView.GetChildElements(doc instanceof Doc ? [doc] : docList, key !== "data", (doc: Doc) => this.remove(doc, key), this.move, this.props.dropAction)} </div> </ul >); - } else + } else { bulletType = BulletType.Collapsed; + } } }); return <div className="treeViewItem-container" @@ -198,8 +198,8 @@ class TreeView extends React.Component<TreeViewProps> { </div>; } public static GetChildElements(docs: Doc[], allowMinimized: boolean, remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType) { - return docs.filter(child => child instanceof Doc && !child.excludeFromLibrary && (allowMinimized || !child.isMinimized)).filter(doc => FieldValue(doc)).map(child => - <TreeView document={child as Doc} key={(child as Doc)[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} />); + return docs.filter(child => !child.excludeFromLibrary && (allowMinimized || !child.isMinimized)).map(child => + <TreeView document={child} key={child[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} />); } } @@ -214,7 +214,7 @@ 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 - ContextMenu.Instance.addItem({ description: "Create Workspace", event: undoBatch(() => Main.Instance.createNewWorkspace()) }); + 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)) }); @@ -222,16 +222,16 @@ export class CollectionTreeView extends CollectionSubView(Document) { } render() { let dropAction = StrCast(this.props.Document.dropAction, "alias") as dropActionType; - if (!this.children) { + if (!this.childDocs) { return (null); } - let childElements = TreeView.GetChildElements(this.children, false, this.remove, this.props.moveDocument, dropAction); + let childElements = TreeView.GetChildElements(this.childDocs, false, this.remove, this.props.moveDocument, dropAction); return ( <div id="body" className="collectionTreeView-dropTarget" style={{ borderRadius: "inherit" }} onContextMenu={this.onContextMenu} - onWheel={(e: React.WheelEvent) => e.stopPropagation()} + onWheel={(e: React.WheelEvent) => this.props.isSelected() && e.stopPropagation()} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}> <div className="coll-title"> <EditableView diff --git a/src/client/views/collections/CollectionVideoView.scss b/src/client/views/collections/CollectionVideoView.scss index ed56ad268..db8b84832 100644 --- a/src/client/views/collections/CollectionVideoView.scss +++ b/src/client/views/collections/CollectionVideoView.scss @@ -2,7 +2,7 @@ .collectionVideoView-cont{ width: 100%; height: 100%; - position: absolute; + position: inherit; top: 0; left:0; diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx index cb3fd1ba4..9ab959f3c 100644 --- a/src/client/views/collections/CollectionVideoView.tsx +++ b/src/client/views/collections/CollectionVideoView.tsx @@ -9,27 +9,26 @@ import { FieldView, FieldViewProps } from "../nodes/FieldView"; import { emptyFunction } from "../../../Utils"; import { Id } from "../../../new_fields/RefField"; import { VideoBox } from "../nodes/VideoBox"; +import { NumCast } from "../../../new_fields/Types"; @observer export class CollectionVideoView extends React.Component<FieldViewProps> { - private _videoBox: VideoBox | undefined = undefined; - @observable _playTimer?: NodeJS.Timeout = undefined; - - @observable _currentTimecode: number = 0; + private _videoBox?: VideoBox; public static LayoutString(fieldKey: string = "data") { return FieldView.LayoutString(CollectionVideoView, fieldKey); } private get uIButtons() { let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale); + let curTime = NumCast(this.props.Document.curPage); return ([ <div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling}, ${scaling})` }}> - <span>{"" + Math.round(this._currentTimecode)}</span> - <span style={{ fontSize: 8 }}>{" " + Math.round((this._currentTimecode - Math.trunc(this._currentTimecode)) * 100)}</span> + <span>{"" + Math.round(curTime)}</span> + <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span> </div>, <div className="collectionVideoView-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling}, ${scaling})` }}> - {this._playTimer ? "\"" : ">"} + {this._videoBox && this._videoBox.Playing ? "\"" : ">"} </div>, <div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling}, ${scaling})` }}> F @@ -38,36 +37,20 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { } @action - updateTimecode = () => { - if (this._videoBox && this._videoBox.player) { - this._currentTimecode = this._videoBox.player.currentTime; - this.props.Document.curPage = Math.round(this._currentTimecode); - } - } - - componentDidMount() { this.updateTimecode(); } - - componentWillUnmount() { if (this._playTimer) clearInterval(this._playTimer); } - - @action onPlayDown = () => { if (this._videoBox && this._videoBox.player) { - if (this._videoBox.player.paused) { - this._videoBox.player.play(); - if (!this._playTimer) this._playTimer = setInterval(this.updateTimecode, 1000); + if (this._videoBox.Playing) { + this._videoBox.Pause(); } else { - this._videoBox.player.pause(); - if (this._playTimer) clearInterval(this._playTimer); - this._playTimer = undefined; - + this._videoBox.Play(); } } } @action onFullDown = (e: React.PointerEvent) => { - if (this._videoBox && this._videoBox.player) { - this._videoBox.player.requestFullscreen(); + if (this._videoBox) { + this._videoBox.FullScreen(); e.stopPropagation(); e.preventDefault(); } @@ -75,22 +58,18 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { @action onResetDown = () => { - if (this._videoBox && this._videoBox.player) { - this._videoBox.player.pause(); - this._videoBox.player.currentTime = 0; - if (this._playTimer) clearInterval(this._playTimer); - this._playTimer = undefined; - this.updateTimecode(); + if (this._videoBox) { + this._videoBox.Pause(); + this.props.Document.curPage = 0; } } 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: "VideoOptions", event: emptyFunction }); } } - setVideoBox = (player: VideoBox) => { this._videoBox = player; }; + setVideoBox = (videoBox: VideoBox) => { this._videoBox = videoBox; }; private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => { let props = { ...this.props, ...renderProps }; @@ -101,6 +80,7 @@ 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 8c1442d38..b9ffc11a2 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,17 +1,23 @@ +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faProjectDiagram, faSquare, faTh, faTree } from '@fortawesome/free-solid-svg-icons'; +import { observer } from "mobx-react"; import * as React from 'react'; -import { FieldViewProps, FieldView } from '../nodes/FieldView'; -import { CollectionBaseView, CollectionViewType, CollectionRenderProps } from './CollectionBaseView'; -import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; -import { CollectionSchemaView } from './CollectionSchemaView'; -import { CollectionDockingView } from './CollectionDockingView'; -import { CollectionTreeView } from './CollectionTreeView'; -import { ContextMenu } from '../ContextMenu'; +import { Id } from '../../../new_fields/RefField'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; -import { observer } from 'mobx-react'; import { undoBatch } from '../../util/UndoManager'; -import { trace } from 'mobx'; -import { Id } from '../../../new_fields/RefField'; -import { Main } from '../Main'; +import { ContextMenu } from "../ContextMenu"; +import { FieldView, FieldViewProps } from '../nodes/FieldView'; +import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from './CollectionBaseView'; +import { CollectionDockingView } from "./CollectionDockingView"; +import { CollectionSchemaView } from "./CollectionSchemaView"; +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); @observer export class CollectionView extends React.Component<FieldViewProps> { @@ -34,9 +40,12 @@ 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) }); - ContextMenu.Instance.addItem({ description: "Schema", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Schema) }); - ContextMenu.Instance.addItem({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree) }); + ContextMenu.Instance.addItem({ description: "Freeform", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Freeform), icon: "project-diagram" }); + 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" }); } } diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss new file mode 100644 index 000000000..f3c605f3e --- /dev/null +++ b/src/client/views/collections/ParentDocumentSelector.scss @@ -0,0 +1,8 @@ +.PDS-flyout { + position: absolute; + z-index: 9999; + background-color: #d3d3d3; + box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); + min-width: 150px; + color: black; +}
\ No newline at end of file diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx new file mode 100644 index 000000000..52f7914f3 --- /dev/null +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -0,0 +1,66 @@ +import * as React from "react"; +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 { SearchUtil } from "../../util/SearchUtil"; +import { CollectionDockingView } from "./CollectionDockingView"; + +@observer +export class SelectorContextMenu extends React.Component<{ Document: Doc }> { + @observable private _docs: Doc[] = []; + + constructor(props: { Document: Doc }) { + super(props); + + this.fetchDocuments(); + } + + async fetchDocuments() { + const docs = await SearchUtil.Search(`data_l:"${this.props.Document[Id]}"`, true); + runInAction(() => this._docs = docs); + } + + render() { + return ( + <> + {this._docs.map(doc => <p><a onClick={() => CollectionDockingView.Instance.AddRightSplit(doc)}>{doc.title}</a></p>)} + </> + ); + } +} + +@observer +export class ParentDocSelector extends React.Component<{ Document: Doc }> { + @observable hover = false; + + @action + onMouseLeave = () => { + this.hover = false; + } + + @action + onMouseEnter = () => { + this.hover = true; + } + + render() { + let flyout; + if (this.hover) { + flyout = ( + <div className="PDS-flyout"> + <SelectorContextMenu Document={this.props.Document} /> + </div> + ); + } + return ( + <span style={{ position: "relative", display: "inline-block" }} + onMouseEnter={this.onMouseEnter} + onMouseLeave={this.onMouseLeave}> + <p>^</p> + {flyout} + </span> + ); + } +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index dc86f38b6..9cb8443f4 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -20,10 +20,11 @@ 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 } from "../../../../new_fields/Types"; +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"; export const panZoomSchema = createSchema({ panX: "number", @@ -36,23 +37,22 @@ const PanZoomDocument = makeInterface(panZoomSchema, positionSchema, pageSchema) @observer export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { - public static RIGHT_BTN_DRAG = false; private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type) private _lastX: number = 0; private _lastY: number = 0; private get _pwidth() { return this.props.PanelWidth(); } private get _pheight() { return this.props.PanelHeight(); } - @computed get nativeWidth() { return NumCast(this.Document.nativeWidth, 0); } - @computed get nativeHeight() { return NumCast(this.Document.nativeHeight, 0); } + @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"; } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } - private get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } - private panX = () => FieldValue(this.Document.panX, 0); - private panY = () => FieldValue(this.Document.panY, 0); - private zoomScaling = () => FieldValue(this.Document.scale, 1); + private panX = () => this.Document.panX || 0; + private panY = () => this.Document.panY || 0; + private zoomScaling = () => 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, -this.borderWidth).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform()); + private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform()); private getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth); private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); private addLiveTextBox = (newBox: Doc) => { @@ -65,13 +65,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return true; } private selectDocuments = (docs: Doc[]) => { - SelectionManager.DeselectAll; + SelectionManager.DeselectAll(); docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).filter(dv => dv).map(dv => SelectionManager.SelectDoc(dv!, true)); } public getActiveDocuments = () => { const curPage = FieldValue(this.Document.curPage, -1); - return this.children.filter(doc => { + return this.childDocs.filter(doc => { var page = NumCast(doc.page, -1); return page === curPage || page === -1; }); @@ -89,7 +89,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { 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.map(d => { + de.data.droppedDocuments.forEach(d => { d.x = x + NumCast(d.x) - dropX; d.y = y + NumCast(d.y) - dropY; if (!NumCast(d.width)) { @@ -111,11 +111,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action onPointerDown = (e: React.PointerEvent): void => { - if ((CollectionFreeFormView.RIGHT_BTN_DRAG && - (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) || - (e.button === 0 && e.altKey)) && this.props.active())) || - (!CollectionFreeFormView.RIGHT_BTN_DRAG && - ((e.button === 0 && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) && this.props.active()))) { + if (e.button === 0 && !e.shiftKey && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); @@ -133,20 +129,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action onPointerMove = (e: PointerEvent): void => { if (!e.cancelBubble) { - let x = NumCast(this.props.Document.panX); - let y = NumCast(this.props.Document.panY); - let docs = this.children || []; + let x = this.Document.panX || 0; + let y = this.Document.panY || 0; + let docs = this.childDocs || []; 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) + minx : minx; + let maxx = docs.length ? NumCast(docs[0].width) / NumCast(docs[0].zoomBasis) + minx : minx; let miny = docs.length ? NumCast(docs[0].y) : 0; - let maxy = docs.length ? NumCast(docs[0].height) + miny : miny; + let maxy = docs.length ? NumCast(docs[0].height) / NumCast(docs[0].zoomBasis) + miny : miny; let ranges = docs.filter(doc => doc).reduce((range, doc) => { let x = NumCast(doc.x); - let xe = x + NumCast(doc.width); + let xe = x + NumCast(doc.width) / NumCast(doc.zoomBasis); let y = NumCast(doc.y); - let ye = y + NumCast(doc.height); + let ye = y + NumCast(doc.height) / NumCast(doc.zoomBasis); 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]]); @@ -179,7 +175,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { // if (!this.props.active()) { // return; // } - let childSelected = this.children.some(doc => { + let childSelected = this.childDocs.some(doc => { var dv = DocumentManager.Instance.getDocumentView(doc); return dv && SelectionManager.IsSelected(dv) ? true : false; }); @@ -210,7 +206,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let [x, y] = this.getTransform().transformPoint(e.clientX, e.clientY); let localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); - let safeScale = Math.abs(localTransform.Scale); + let safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40); this.props.Document.scale = Math.abs(safeScale); this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale); e.stopPropagation(); @@ -219,6 +215,8 @@ 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)); const newPanY = Math.min((1 - 1 / scale) * this.nativeHeight, Math.max(0, panY)); @@ -236,7 +234,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } bringToFront = (doc: Doc) => { - const docs = this.children; + const docs = this.childDocs; docs.slice().sort((doc1, doc2) => { if (doc1 === doc) return 1; if (doc2 === doc) return -1; @@ -245,16 +243,37 @@ 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; + const id = this.Document[Id]; + const state = HistoryUtil.getState(); + // TODO This technically isn't correct if type !== "doc", as + // currently nothing is done, but we should probably push a new state + if (state.type === "doc" && panX !== undefined && panY !== undefined) { + const init = state.initializers[id]; + if (!init) { + state.initializers[id] = { + panX, panY + }; + HistoryUtil.pushState(state); + } else if (init.panX !== panX || init.panY !== panY) { + init.panX = panX; + init.panY = panY; + HistoryUtil.pushState(state); + } + } SelectionManager.DeselectAll(); + const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2; + const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2; + const newState = HistoryUtil.getState(); + newState.initializers[id] = { panX: newPanX, panY: newPanY }; + HistoryUtil.pushState(newState); + this.setPan(newPanX, newPanY); this.props.Document.panTransformType = "Ease"; - this.setPan( - NumCast(doc.x) + NumCast(doc.width) / 2, - NumCast(doc.y) + NumCast(doc.height) / 2); this.props.focus(this.props.Document); - if (this.props.Document.panTransformType === "Ease") { - setTimeout(() => this.props.Document.panTransformType = "None", 2000); // wait 3 seconds, then reset to false - } + this.panDisposer = setTimeout(() => this.props.Document.panTransformType = "None", 2000); // wait 3 seconds, then reset to false } @@ -276,17 +295,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { parentActive: this.props.active, whenActiveChanged: this.props.whenActiveChanged, bringToFront: this.bringToFront, + addDocTab: this.props.addDocTab, }; } @computed.struct get views() { let curPage = FieldValue(this.Document.curPage, -1); - let docviews = this.children.reduce((prev, doc) => { + let docviews = this.childDocs.reduce((prev, doc) => { if (!(doc instanceof Doc)) return prev; var page = NumCast(doc.page, -1); if (page === curPage || page === -1) { - let minim = Cast(doc.isMinimized, "boolean"); + let minim = BoolCast(doc.isMinimized, false); if (minim === undefined || !minim) { prev.push(<CollectionFreeFormDocumentView key={doc[Id]} {...this.getDocumentViewProps(doc)} />); } @@ -350,7 +370,7 @@ class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />); } render() { - return this.backgroundView; + return this.props.Document.backgroundLayout ? this.backgroundView : (null); } } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 080c484f4..4587c2227 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -17,6 +17,9 @@ 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 { DocServer } from "../../../DocServer"; +import { Id } from "../../../../new_fields/RefField"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -60,7 +63,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> e.preventDefault(); (async () => { let text: string = await navigator.clipboard.readText(); - let ns = text.split("\n").filter(t => t.trim() != "\r" && t.trim() != ""); + let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); for (let i = 0; i < ns.length - 1; i++) { while (!(ns[i].trim() === "" || ns[i].endsWith("-\r") || ns[i].endsWith("-") || ns[i].endsWith(";\r") || ns[i].endsWith(";") || @@ -77,9 +80,9 @@ export class MarqueeView extends React.Component<MarqueeViewProps> let newBox = Docs.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line }); this.props.addDocument(newBox, false); y += 40 * this.props.getTransform().Scale; - }) + }); })(); - } else if (e.key === "t" && e.ctrlKey) { + } 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 @@ -90,9 +93,10 @@ export class MarqueeView extends React.Component<MarqueeViewProps> e.preventDefault(); (async () => { let text: string = await navigator.clipboard.readText(); - let ns = text.split("\n").filter(t => t.trim() != "\r" && t.trim() != ""); - while (ns.length > 0 && ns[0].split("\t").length < 2) + 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[] = []; @@ -104,18 +108,15 @@ export class MarqueeView extends React.Component<MarqueeViewProps> continue; } let doc = new Doc(); - columns.forEach((col, i) => { - console.log(values[i] + " " + Number(values[i]).toString()); - doc[columns[i]] = (values.length > i ? ((values[i].indexOf(Number(values[i]).toString()) !== -1) ? Number(values[i]) : values[i]) : undefined); - }); + 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._group = groupAttr; } doc.title = i.toString(); docList.push(doc); } - let newCol = Docs.SchemaDocument(docList, { x: x, y: y, title: "-dropped table-", width: 300, height: 100 }); - newCol.proto!.schemaColumns = new List<string>([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)]); + 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); } })(); @@ -131,17 +132,18 @@ export class MarqueeView extends React.Component<MarqueeViewProps> this._downY = this._lastY = e.pageY; this._commandExecuted = false; PreviewCursor.Visible = false; - if ((CollectionFreeFormView.RIGHT_BTN_DRAG && e.button === 0 && !e.altKey && !e.metaKey && this.props.container.props.active()) || - (!CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && this.props.container.props.active())) { + 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); document.addEventListener("pointerup", this.onPointerUp, true); document.addEventListener("keydown", this.marqueeCommand, true); - // bcz: do we need this? it kills the context menu on the main collection + if (e.altKey) { + e.stopPropagation(); + e.preventDefault(); + } + // bcz: do we need this? it kills the context menu on the main collection if !altKey // e.stopPropagation(); } - if (e.altKey) { - e.preventDefault(); - } } @action @@ -207,11 +209,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps> @undoBatch @action marqueeCommand = async (e: KeyboardEvent) => { - if (this._commandExecuted) { + if (this._commandExecuted || (e as any).propagationIsStopped) { return; } if (e.key === "Backspace" || e.key === "Delete" || e.key === "d") { this._commandExecuted = true; + 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) { @@ -221,26 +225,21 @@ export class MarqueeView extends React.Component<MarqueeViewProps> this.cleanupInteractions(false); e.stopPropagation(); } - if (e.key === "c" || e.key === "r" || e.key === "s" || e.key === "e" || e.key === "p") { + if (e.key === "c" || e.key === "s" || e.key === "e" || e.key === "p") { this._commandExecuted = true; e.stopPropagation(); + (e as any).propagationIsStopped = true; let bounds = this.Bounds; - let selected = this.marqueeSelect().map(d => { - if (e.key === "s") { - let dCopy = Doc.MakeCopy(d); - dCopy.x = NumCast(d.x) - bounds.left - bounds.width / 2; - dCopy.y = NumCast(d.y) - bounds.top - bounds.height / 2; - dCopy.page = -1; - return dCopy; - } - else if (e.key !== "r") { + let selected = this.marqueeSelect(); + if (e.key === "c") { + 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; - }); + 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); @@ -250,39 +249,36 @@ export class MarqueeView extends React.Component<MarqueeViewProps> 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: "a nested collection", + title: e.key === "s" ? "-summary-" : e.key === "p" ? "-summary-" : "a nested collection", }); - this.marqueeInkDelete(inkData); - // SelectionManager.DeselectAll(); - if (e.key === "s" || e.key === "r" || e.key === "p") { - e.preventDefault(); - let scrpt = this.props.getTransform().inverse().transformPoint(bounds.left, bounds.top); - let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" }); - let dataUrl = await htmlToImage.toPng(this._mainCont.current!, { width: bounds.width, height: bounds.height, quality: 1 }); - summary.proto!.thumbnail = new ImageField(new URL(dataUrl)); + if (e.key === "s" || e.key === "p") { - summary.proto!.templates = new List<string>([Templates.ImageOverlay(Math.min(50, bounds.width), bounds.height * Math.min(50, bounds.width) / bounds.width, "thumbnail")]); - if (e.key === "s" || e.key === "p") { - summary.proto!.maximizeOnRight = true; + htmlToImage.toPng(this._mainCont.current!, { width: bounds.width * zoomBasis, height: bounds.height * zoomBasis, quality: 1 }).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.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")]); newCollection.proto!.summaryDoc = summary; selected = [newCollection]; - } - summary.proto!.summarizedDocs = new List<Doc>(selected); - //summary.proto!.isButton = true; - selected.map(summarizedDoc => { - let maxx = NumCast(summarizedDoc.x, undefined); - let maxy = NumCast(summarizedDoc.y, undefined); - let maxw = NumCast(summarizedDoc.width, undefined); - let maxh = NumCast(summarizedDoc.height, undefined); - summarizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), 0]) + 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); }); - this.props.addLiveTextDocument(summary); } else { this.props.addDocument(newCollection, false); @@ -290,20 +286,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> this.props.selectDocuments([newCollection]); } this.cleanupInteractions(false); - } else - if (e.key === "s") { - // this._commandExecuted = true; - // e.stopPropagation(); - // e.preventDefault(); - // let bounds = this.Bounds; - // let selected = this.marqueeSelect(); - // SelectionManager.DeselectAll(); - // let summary = Docs.TextDocument({ x: bounds.left + bounds.width + 25, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" }); - // this.props.addLiveTextDocument(summary); - // selected.forEach(select => Doc.MakeLink(summary.proto!, select.proto!)); - - // this.cleanupInteractions(false); - } + } } @action marqueeInkSelect(ink: Map<any, any>) { |
