diff options
| author | Sam Wilkins <abdullah_ahmed@brown.edu> | 2019-05-16 13:13:50 -0400 |
|---|---|---|
| committer | Sam Wilkins <abdullah_ahmed@brown.edu> | 2019-05-16 13:13:50 -0400 |
| commit | 4dacd1220e6a0ef73167f187f52f3b4c222c2586 (patch) | |
| tree | 12d481d4d421fda6bd4490af4d0b5d77c6c1131c /src/client/views/collections | |
| parent | 3451ce40cbd488cede7d29b6e39594f740e366b5 (diff) | |
| parent | 53351f6c5b448b93f2865eb38868bddb95ec4c1d (diff) | |
pulled from master and resolved
Diffstat (limited to 'src/client/views/collections')
14 files changed, 376 insertions, 159 deletions
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 2b1f7bb37..31bdf213e 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { ContextMenu } from '../ContextMenu'; import { FieldViewProps } from '../nodes/FieldView'; import { Cast, FieldValue, PromiseValue, NumCast } from '../../../new_fields/Types'; -import { Doc, FieldResult, Opt } from '../../../new_fields/Doc'; +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'; @@ -63,13 +63,13 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { if (!(documentToAdd instanceof Doc)) { return false; } - let data = Cast(documentToAdd.data, listSpec(Doc), []).filter(d => d).map(d => d as Doc); - for (const doc of data.filter(d => d instanceof Document)) { + let data = DocListCast(documentToAdd.data); + for (const doc of data) { if (this.createsCycle(doc, containerDocument)) { return true; } } - let annots = Cast(documentToAdd.annotations, listSpec(Doc), []).filter(d => d).map(d => d as Doc); + let annots = DocListCast(documentToAdd.annotations); for (const annot of annots) { if (this.createsCycle(annot, containerDocument)) { return true; @@ -87,7 +87,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { @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 159815ea5..58f1e33a1 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,8 @@ 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'; @observer export class CollectionDockingView extends React.Component<SubCollectionViewProps> { @@ -72,26 +74,33 @@ 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(); + return true; } else - child.contentItems.map((tab: any, j: number) => { - if (tab.config.component === "DocumentFrameRenderer" && tab.config.props.documentId === document[Id]) { + 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 @@ -157,6 +166,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 +174,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 +188,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 +210,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) => { @@ -292,15 +311,16 @@ 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; @@ -320,6 +340,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) => { @@ -412,7 +440,6 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { parentActive={returnTrue} whenActiveChanged={emptyFunction} focus={emptyFunction} - bringToFront={emptyFunction} ContainingCollectionView={undefined} /> </div >); } @@ -420,7 +447,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { render() { let theContent = this.content; return !this._document ? (null) : - <Measure onResize={action((r: any) => { this._panelWidth = r.entry.width; this._panelHeight = r.entry.height; })}> + <Measure offset onResize={action((r: any) => { this._panelWidth = r.offset.width; this._panelHeight = r.offset.height; })}> {({ measureRef }) => <div ref={measureRef}> {theContent} </div>} </Measure>; } diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index cfdb3ab22..5e9d2ac67 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -14,6 +14,10 @@ .collectionSchemaView-cellContents { height: $MAX_ROW_HEIGHT; + img { + width:auto; + max-height: $MAX_ROW_HEIGHT; + } } .collectionSchemaView-previewRegion { @@ -123,7 +127,7 @@ max-width: 100%; height: 100%; } - .videobox-cont { + .videoBox-cont { object-fit: contain; width: auto; height: 100%; diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index ae949b2ed..f4ad5b357 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -19,11 +19,21 @@ import { DocumentView } from "../nodes/DocumentView"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; -import { Opt, Field, Doc, DocListCast } from "../../../new_fields/Doc"; -import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; +import { Opt, Field, 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 { isUndefined } from "typescript-collections/dist/lib/util"; +import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; +import { Gateway } from "../../northstar/manager/Gateway"; +import { DocServer } from "../../DocServer"; +import { ColumnAttributeModel } from "../../northstar/core/attribute/AttributeModel"; +import { HistogramOperation } from "../../northstar/operations/HistogramOperation"; +import { AggregateFunction } from "../../northstar/model/idea/idea"; +import { AttributeTransformationModel } from "../../northstar/core/attribute/AttributeTransformationModel"; +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 @@ -77,23 +87,22 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { PanelHeight: returnZero, PanelWidth: returnZero, }; - 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 +127,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 DocListCast(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 +216,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 +259,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 +289,8 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { get previewPanel() { // let doc = CompileScript(this.previewScript, { this: selected }, true)(); const previewDoc = this.previewDocument; - return !previewDoc ? (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} @@ -268,15 +304,14 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { whenActiveChanged={this.props.whenActiveChanged} bringToFront={emptyFunction} /> - </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() { - const docs = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).filter(d => d).map(d => d as Doc); + const docs = DocListCast(this.props.Document[this.props.fieldKey]); let keys: { [key: string]: boolean } = {}; // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields. // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be @@ -325,7 +360,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { const children = this.children; 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 => ({ diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 0b08e150a..ffd3e0659 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -10,7 +10,7 @@ import * as rp from 'request-promise'; import { CollectionView } from "./CollectionView"; import { CollectionPDFView } from "./CollectionPDFView"; import { CollectionVideoView } from "./CollectionVideoView"; -import { Doc, Opt, FieldResult } from "../../../new_fields/Doc"; +import { Doc, Opt, FieldResult, DocListCast } from "../../../new_fields/Doc"; import { DocComponent } from "../DocComponent"; import { listSpec } from "../../../new_fields/Schema"; import { Cast, PromiseValue, FieldValue, ListSpec } from "../../../new_fields/Types"; @@ -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; @@ -49,7 +50,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { get children() { //TODO tfs: This might not be what we want? //This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue) - return Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).filter(doc => FieldValue(doc)).map(doc => doc as Doc); + return DocListCast(this.props.Document[this.props.fieldKey]); } @action @@ -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.tsx b/src/client/views/collections/CollectionTreeView.tsx index 33787f06b..70c09d97c 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -18,9 +18,8 @@ import { Main } from '../Main'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; import { CollectionDockingView } from './CollectionDockingView'; import { DocumentManager } from '../../util/DocumentManager'; -import { Utils } from '../../../Utils'; import { List } from '../../../new_fields/List'; -import { indexOf } from 'typescript-collections/dist/lib/arrays'; +import { Docs } from '../../documents/Documents'; export interface TreeViewProps { @@ -95,6 +94,15 @@ class TreeView extends React.Component<TreeViewProps> { return <div className="bullet" onClick={onClicked}>{bullet ? <FontAwesomeIcon icon={bullet} /> : ""} </div>; } + @action + onMouseEnter = () => { + this._isOver = true; + } + @observable _isOver: boolean = false; + @action + onMouseLeave = () => { + this._isOver = false; + } /** * Renders the EditableView title element for placement into the tree. */ @@ -103,7 +111,8 @@ class TreeView extends React.Component<TreeViewProps> { let onItemDown = SetupDrag(reference, () => this.props.document, this.props.moveDocument, this.props.dropAction); let editableView = (titleString: string) => (<EditableView - display={"inline"} + oneLine={!this._isOver ? true : false} + display={"block"} contents={titleString} height={36} GetValue={() => StrCast(this.props.document.title)} @@ -120,8 +129,8 @@ class TreeView extends React.Component<TreeViewProps> { <FontAwesomeIcon icon="angle-right" size="lg" /> </div>); return ( - <div className="docContainer" ref={reference} onPointerDown={onItemDown} - style={{ background: BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0" }} + <div className="docContainer" ref={reference} onPointerDown={onItemDown} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} + style={{ background: BoolCast(this.props.document.protoBrush, false) ? "#06123232" : BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0" }} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> {editableView(StrCast(this.props.document.title))} {openRight} @@ -133,6 +142,10 @@ class TreeView extends React.Component<TreeViewProps> { 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 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)) }); } @@ -155,16 +168,21 @@ class TreeView extends React.Component<TreeViewProps> { let keys = Array.from(Object.keys(this.props.document)); if (this.props.document.proto instanceof Doc) { keys.push(...Array.from(Object.keys(this.props.document.proto))); + while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1); } keys.map(key => { - let docList = Cast(this.props.document[key], listSpec(Doc)); - if (docList instanceof List && docList.length && docList[0] instanceof Doc) { + let docList = DocListCast(this.props.document[key]); + let doc = Cast(this.props.document[key], Doc); + if (doc instanceof Doc || docList.length) { if (!this._collapsed) { bulletType = BulletType.Collapsible; + let spacing = (key === "data") ? 0 : -10; contentElement.push(<ul key={key + "more"}> {(key === "data") ? (null) : - <span className="collectionTreeView-keyHeader" key={key}>{key}</span>} - {TreeView.GetChildElements(docList, key !== "data", (doc: Doc) => this.remove(doc, key), this.move, this.props.dropAction)} + <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)} + </div> </ul >); } else bulletType = BulletType.Collapsed; @@ -179,7 +197,7 @@ class TreeView extends React.Component<TreeViewProps> { </li> </div>; } - public static GetChildElements(docs: (Doc | Promise<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) { 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} />); } @@ -203,12 +221,11 @@ export class CollectionTreeView extends CollectionSubView(Document) { } } render() { - const children = this.children; let dropAction = StrCast(this.props.Document.dropAction, "alias") as dropActionType; - if (!children) { + if (!this.children) { return (null); } - let childElements = TreeView.GetChildElements(children, false, this.remove, this.props.moveDocument, dropAction); + let childElements = TreeView.GetChildElements(this.children, false, this.remove, this.props.moveDocument, dropAction); return ( <div id="body" className="collectionTreeView-dropTarget" 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..16121bb1b 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,12 +58,9 @@ 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; } } @@ -90,7 +70,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { } } - 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 +81,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..55fd2a284 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -9,9 +9,7 @@ import { ContextMenu } from '../ContextMenu'; 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'; @observer export class CollectionView extends React.Component<FieldViewProps> { 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/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index 62b1456cf..d5ce4e1e7 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -7,7 +7,7 @@ import { CollectionViewProps } from "../CollectionSubView"; import "./CollectionFreeFormLinksView.scss"; import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView"; import React = require("react"); -import { Doc, DocListCast } from "../../../../new_fields/Doc"; +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"; @@ -20,11 +20,11 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP componentDidMount() { this._brushReactionDisposer = reaction( () => { - let doclist = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []); - return { doclist: doclist ? doclist : [], xs: doclist instanceof List ? doclist.map(d => d instanceof Doc && d.x) : [] }; + let doclist = DocListCast(this.props.Document[this.props.fieldKey]); + return { doclist: doclist ? doclist : [], xs: doclist.map(d => d.x) }; }, - async () => { - let doclist = await DocListCast(this.props.Document[this.props.fieldKey]); + () => { + let doclist = DocListCast(this.props.Document[this.props.fieldKey]); let views = doclist ? doclist.filter(doc => StrCast(doc.backgroundLayout).indexOf("istogram") !== -1) : []; views.forEach((dstDoc, i) => { views.forEach((srcDoc, j) => { @@ -84,7 +84,7 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP } if (view.props.ContainingCollectionView) { let collid = view.props.ContainingCollectionView.props.Document[Id]; - Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).filter(d => d).map(d => d as Doc). + DocListCast(this.props.Document[this.props.fieldKey]). filter(child => child[Id] === collid).map(view => DocumentManager.Instance.getDocumentViews(view).map(view => diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 6861ce58b..b8bed795d 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -20,9 +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", @@ -42,16 +44,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private get _pwidth() { return this.props.PanelWidth(); } private get _pheight() { return this.props.PanelHeight(); } - @computed get nativeWidth() { return FieldValue(this.Document.nativeWidth, 0); } - @computed get nativeHeight() { return FieldValue(this.Document.nativeHeight, 0); } + @computed get nativeWidth() { return this.Document.nativeWidth || 0; } + @computed get nativeHeight() { return this.Document.nativeHeight || 0; } 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) => { @@ -132,23 +134,32 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action onPointerMove = (e: PointerEvent): void => { if (!e.cancelBubble) { - let x = Cast(this.props.Document.panX, "number", 0); - let y = Cast(this.props.Document.panY, "number", 0); + let x = this.props.Document.panX || 0; + let y = this.props.Document.panY || 0; let docs = this.children || []; let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); if (!this.isAnnotationOverlay) { - let minx = docs.length ? Cast(docs[0].x, "number", 0) : 0; - let maxx = docs.length ? Cast(docs[0].width, "number", 0) + minx : minx; - let miny = docs.length ? Cast(docs[0].y, "number", 0) : 0; - let maxy = docs.length ? Cast(docs[0].height, "number", 0) + miny : miny; + let minx = docs.length ? NumCast(docs[0].x) : 0; + let maxx = docs.length ? NumCast(docs[0].width) + minx : minx; + let miny = docs.length ? NumCast(docs[0].y) : 0; + let maxy = docs.length ? NumCast(docs[0].height) + miny : miny; let ranges = docs.filter(doc => doc).reduce((range, doc) => { - let x = Cast(doc.x, "number", 0); - let xe = x + Cast(doc.width, "number", 0); - let y = Cast(doc.y, "number", 0); - let ye = y + Cast(doc.height, "number", 0); + let x = NumCast(doc.x); + let xe = x + NumCast(doc.width); + let y = NumCast(doc.y); + let ye = y + NumCast(doc.height); return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]], [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]]; }, [[minx, maxx], [miny, maxy]]); + let ink = Cast(this.props.Document.ink, InkField); + if (ink && ink.inkData) { + ink.inkData.forEach((value: StrokeData, key: string) => { + let bounds = InkingCanvas.StrokeRect(value); + ranges[0] = [Math.min(ranges[0][0], bounds.left), Math.max(ranges[0][1], bounds.right)]; + ranges[1] = [Math.min(ranges[1][0], bounds.top), Math.max(ranges[1][1], bounds.bottom)]; + }); + } + 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; @@ -181,8 +192,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { if (e.ctrlKey) { let deltaScale = (1 - (e.deltaY / coefficient)); - this.props.Document.nativeWidth = this.nativeWidth * deltaScale; - this.props.Document.nativeHeight = this.nativeHeight * deltaScale; + let nw = this.nativeWidth * deltaScale; + let nh = this.nativeHeight * deltaScale; + if (nw && nh) { + this.props.Document.nativeWidth = nw; + this.props.Document.nativeHeight = nh; + } e.stopPropagation(); e.preventDefault(); } else { @@ -205,6 +220,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)); @@ -231,16 +248,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 } @@ -272,7 +310,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { 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)} />); } @@ -336,7 +374,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 1bf39e335..865bae729 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,3 +1,4 @@ +import * as htmlToImage from "html-to-image"; import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import { Docs } from "../../../documents/Documents"; @@ -14,6 +15,11 @@ import { Doc } from "../../../../new_fields/Doc"; import { NumCast, Cast } from "../../../../new_fields/Types"; import { InkField, StrokeData } from "../../../../new_fields/InkField"; import { List } from "../../../../new_fields/List"; +import { ImageField } from "../../../../new_fields/URLField"; +import { Template, Templates } from "../../Templates"; +import { Gateway } from "../../../northstar/manager/Gateway"; +import { DocServer } from "../../../DocServer"; +import { Id } from "../../../../new_fields/RefField"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -30,6 +36,7 @@ interface MarqueeViewProps { @observer export class MarqueeView extends React.Component<MarqueeViewProps> { + private _mainCont = React.createRef<HTMLDivElement>(); @observable _lastX: number = 0; @observable _lastY: number = 0; @observable _downX: number = 0; @@ -55,13 +62,9 @@ export class MarqueeView extends React.Component<MarqueeViewProps> if (e.key === "q" && e.ctrlKey) { e.preventDefault(); (async () => { - let text = await navigator.clipboard.readText(); - let ns = text.split("\n").filter(t => t != "\r"); + let text: string = await navigator.clipboard.readText(); + let ns = text.split("\n").filter(t => t.trim() != "\r" && t.trim() != ""); for (let i = 0; i < ns.length - 1; i++) { - if (ns[i].trim() === "") { - ns.splice(i, 1); - continue; - } while (!(ns[i].trim() === "" || ns[i].endsWith("-\r") || ns[i].endsWith("-") || ns[i].endsWith(";\r") || ns[i].endsWith(";") || ns[i].endsWith(".\r") || ns[i].endsWith(".") || @@ -79,6 +82,43 @@ export class MarqueeView extends React.Component<MarqueeViewProps> y += 40 * this.props.getTransform().Scale; }) })(); + } 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(); + 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); + } + })(); } else { let newBox = Docs.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" }); this.props.addLiveTextDocument(newBox); @@ -166,12 +206,14 @@ export class MarqueeView extends React.Component<MarqueeViewProps> @undoBatch @action - marqueeCommand = (e: KeyboardEvent) => { - if (this._commandExecuted) { + marqueeCommand = async (e: KeyboardEvent) => { + 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) { @@ -184,6 +226,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> if (e.key === "c" || e.key === "r" || 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") { @@ -214,7 +257,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> 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 === "r" ? "-replacement-" : e.key === "p" ? "-summary-" : "a nested collection", }); this.marqueeInkDelete(inkData); @@ -224,19 +267,22 @@ export class MarqueeView extends React.Component<MarqueeViewProps> 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)); + + 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; newCollection.proto!.summaryDoc = summary; selected = [newCollection]; } summary.proto!.summarizedDocs = new List<Doc>(selected); - summary.proto!.isButton = true; + //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]) }); this.props.addLiveTextDocument(summary); } @@ -246,20 +292,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>) { @@ -313,17 +346,21 @@ export class MarqueeView extends React.Component<MarqueeViewProps> @computed get marqueeDiv() { - let p = this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); - return <div className="marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}` }} > + return <div className="marquee" style={{ width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}`, zIndex: 2000 }} > <span className="marquee-legend" /> </div>; } render() { + let p = this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); return <div className="marqueeView" style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}> - {this.props.children} - {!this._visible ? (null) : this.marqueeDiv} + <div style={{ position: "relative", transform: `translate(${p[0]}px, ${p[1]}px)` }} > + {!this._visible ? null : this.marqueeDiv} + <div ref={this._mainCont} style={{ transform: `translate(${-p[0]}px, ${-p[1]}px)` }} > + {this.props.children} + </div> + </div> </div>; } }
\ No newline at end of file |
