diff options
| author | Tyler Schicke <tyler_schicke@brown.edu> | 2019-05-07 16:29:02 -0400 |
|---|---|---|
| committer | Tyler Schicke <tyler_schicke@brown.edu> | 2019-05-07 16:29:02 -0400 |
| commit | 14c776b6d30e0bc0d5b3712f28e4b9f1170eae3b (patch) | |
| tree | 5255d8cce8a72a5b09cc1ad58661e2176295467a /src/client/views/collections | |
| parent | e19fdbba4cf672aee5bfb59b91b6162431d146d3 (diff) | |
| parent | 26141a697ae52a7edf3cc6845ce2153111f8860e (diff) | |
Merge branch 'master' of github-tsch-brown:browngraphicslab/Dash-Web into new_search
Diffstat (limited to 'src/client/views/collections')
19 files changed, 1410 insertions, 1138 deletions
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 4fda38a26..14b92af48 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -1,13 +1,14 @@ import { action, computed } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Document } from '../../../fields/Document'; -import { Field, FieldValue, FieldWaiting } from '../../../fields/Field'; -import { KeyStore } from '../../../fields/KeyStore'; -import { ListField } from '../../../fields/ListField'; -import { NumberField } from '../../../fields/NumberField'; 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 { listSpec } from '../../../new_fields/Schema'; +import { List } from '../../../new_fields/List'; +import { Id } from '../../../new_fields/RefField'; +import { SelectionManager } from '../../util/SelectionManager'; export enum CollectionViewType { Invalid, @@ -18,11 +19,11 @@ export enum CollectionViewType { } export interface CollectionRenderProps { - addDocument: (document: Document, allowDuplicates?: boolean) => boolean; - removeDocument: (document: Document) => boolean; - moveDocument: (document: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean; + addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; + removeDocument: (document: Doc) => boolean; + moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; active: () => boolean; - onActiveChanged: (isActive: boolean) => void; + whenActiveChanged: (isActive: boolean) => void; } export interface CollectionViewProps extends FieldViewProps { @@ -37,11 +38,9 @@ export interface CollectionViewProps extends FieldViewProps { export class CollectionBaseView extends React.Component<CollectionViewProps> { get collectionViewType(): CollectionViewType | undefined { let Document = this.props.Document; - let viewField = Document.GetT(KeyStore.ViewType, NumberField); - if (viewField === FieldWaiting) { - return undefined; - } else if (viewField) { - return viewField.Data; + let viewField = Cast(Document.viewType, "number"); + if (viewField !== undefined) { + return viewField; } else { return CollectionViewType.Invalid; } @@ -55,105 +54,82 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { //TODO should this be observable? private _isChildActive = false; - onActiveChanged = (isActive: boolean) => { + whenActiveChanged = (isActive: boolean) => { this._isChildActive = isActive; - this.props.onActiveChanged(isActive); + this.props.whenActiveChanged(isActive); } - createsCycle(documentToAdd: Document, containerDocument: Document): boolean { - if (!(documentToAdd instanceof Document)) { + createsCycle(documentToAdd: Doc, containerDocument: Doc): boolean { + if (!(documentToAdd instanceof Doc)) { return false; } - let data = documentToAdd.GetList(KeyStore.Data, [] as Document[]); + let data = Cast(documentToAdd.data, listSpec(Doc), []); for (const doc of data.filter(d => d instanceof Document)) { if (this.createsCycle(doc, containerDocument)) { return true; } } - let annots = documentToAdd.GetList(KeyStore.Annotations, [] as Document[]); + let annots = Cast(documentToAdd.annotations, listSpec(Doc), []); for (const annot of annots) { if (this.createsCycle(annot, containerDocument)) { return true; } } - for (let containerProto: FieldValue<Document> = containerDocument; containerProto && containerProto !== FieldWaiting; containerProto = containerProto.GetPrototype()) { - if (containerProto.Id === documentToAdd.Id) { + for (let containerProto: Opt<Doc> = containerDocument; containerProto !== undefined; containerProto = FieldValue(containerProto.proto)) { + if (containerProto[Id] === documentToAdd[Id]) { return true; } } return false; } - @computed get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's? + @computed get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } @action.bound - addDocument(doc: Document, allowDuplicates: boolean = false): boolean { + addDocument(doc: Doc, allowDuplicates: boolean = false): boolean { let props = this.props; - var curPage = props.Document.GetNumber(KeyStore.CurPage, -1); - doc.SetOnPrototype(KeyStore.Page, new NumberField(curPage)); - if (this.isAnnotationOverlay) { - doc.SetNumber(KeyStore.Zoom, this.props.Document.GetNumber(KeyStore.Scale, 1)); - } + var curPage = Cast(props.Document.curPage, "number", -1); + Doc.SetOnPrototype(doc, "page", curPage); if (curPage >= 0) { - doc.SetOnPrototype(KeyStore.AnnotationOn, props.Document); + Doc.SetOnPrototype(doc, "annotationOn", props.Document); } - if (props.Document.Get(props.fieldKey) instanceof Field) { + if (!this.createsCycle(doc, props.Document)) { //TODO This won't create the field if it doesn't already exist - const value = props.Document.GetData(props.fieldKey, ListField, new Array<Document>()); - if (!this.createsCycle(doc, props.Document)) { - if (!value.some(v => v.Id === doc.Id) || allowDuplicates) { + const value = Cast(props.Document[props.fieldKey], listSpec(Doc)); + let alreadyAdded = true; + if (value !== undefined) { + if (allowDuplicates || !value.some(v => v instanceof Doc && v[Id] === doc[Id])) { + alreadyAdded = false; value.push(doc); } + } else { + alreadyAdded = false; + Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new List([doc])); } - else { - return false; - } - } else { - let proto = props.Document.GetPrototype(); - if (!proto || proto === FieldWaiting || !this.createsCycle(proto, doc)) { - const field = new ListField([doc]); - // const script = CompileScript(` - // if(added) { - // console.log("added " + field.Title + " " + doc.Title); - // } else { - // console.log("removed " + field.Title + " " + doc.Title); - // } - // `, { - // addReturn: false, - // params: { - // field: Document.name, - // added: "boolean" - // }, - // capturedVariables: { - // doc: this.props.Document - // } - // }); - // if (script.compiled) { - // field.addScript(new ScriptField(script)); - // } - props.Document.SetOnPrototype(props.fieldKey, field); - } - else { - return false; + // set the ZoomBasis only if hasn't already been set -- bcz: maybe set/resetting the ZoomBasis should be a parameter to addDocument? + if (!alreadyAdded && (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid)) { + let zoom = NumCast(this.props.Document.scale, 1); + Doc.SetOnPrototype(doc, "zoomBasis", zoom); } } return true; } @action.bound - removeDocument(doc: Document): boolean { + removeDocument(doc: Doc): boolean { const props = this.props; //TODO This won't create the field if it doesn't already exist - const value = props.Document.GetData(props.fieldKey, ListField, new Array<Document>()); + const value = Cast(props.Document[props.fieldKey], listSpec(Doc), []); let index = -1; for (let i = 0; i < value.length; i++) { - if (value[i].Id === doc.Id) { + let v = value[i]; + if (v instanceof Doc && v[Id] === doc[Id]) { index = i; break; } } - doc.GetTAsync(KeyStore.AnnotationOn, Document).then((annotationOn) => { + PromiseValue(Cast(doc.annotationOn, Doc)).then((annotationOn) => { if (annotationOn === props.Document) { - doc.Set(KeyStore.AnnotationOn, undefined, true); + doc.annotationOn = undefined; } }); @@ -168,11 +144,12 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { } @action.bound - moveDocument(doc: Document, targetCollection: Document, addDocument: (doc: Document) => boolean): boolean { + moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean { if (this.props.Document === targetCollection) { return true; } if (this.removeDocument(doc)) { + SelectionManager.DeselectAll(); return addDocument(doc); } return false; @@ -184,11 +161,13 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { removeDocument: this.removeDocument, moveDocument: this.moveDocument, active: this.active, - onActiveChanged: this.onActiveChanged, + whenActiveChanged: this.whenActiveChanged, }; const viewtype = this.collectionViewType; return ( - <div className={this.props.className || "collectionView-cont"} onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}> + <div className={this.props.className || "collectionView-cont"} + style={{ borderRadius: "inherit", pointerEvents: "all" }} + onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}> {viewtype !== undefined ? this.props.children(viewtype, props) : (null)} </div> ); diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index 50da2b11d..0e7e0afa7 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -2,15 +2,6 @@ .collectiondockingview-content { height: 100%; - text-align:center; - .documentView-node-topmost { - text-align:left; - transform-origin: center top; - display: inline-block; - } -} -.collectiondockingview-content-height { - height: 100%; } .lm_active .messageCounter{ color:white; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 2b886adb6..d894909d0 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,36 +1,35 @@ -import * as GoldenLayout from "golden-layout"; import 'golden-layout/src/css/goldenlayout-base.css'; import 'golden-layout/src/css/goldenlayout-dark-theme.css'; -import { action, observable, reaction, trace } from "mobx"; +import { action, observable, reaction } from "mobx"; import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; -import { Document } from "../../../fields/Document"; -import { KeyStore } from "../../../fields/KeyStore"; import Measure from "react-measure"; -import { FieldId, Opt, Field, FieldWaiting } from "../../../fields/Field"; -import { Utils, returnTrue, emptyFunction, emptyDocFunction, returnOne } from "../../../Utils"; -import { Server } from "../../Server"; -import { undoBatch } from "../../util/UndoManager"; +import * as GoldenLayout from "../../../client/goldenLayout"; +import { Doc, Field, Opt } 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"; +import { emptyFunction, returnTrue, Utils } from "../../../Utils"; +import { DocServer } from "../../DocServer"; +import { DragLinksAsDocuments, DragManager } from "../../util/DragManager"; +import { Transform } from '../../util/Transform'; +import { undoBatch, UndoManager } from "../../util/UndoManager"; import { DocumentView } from "../nodes/DocumentView"; import "./CollectionDockingView.scss"; -import React = require("react"); import { SubCollectionViewProps } from "./CollectionSubView"; -import { ServerUtils } from "../../../server/ServerUtil"; -import { DragManager, DragLinksAsDocuments } from "../../util/DragManager"; -import { TextField } from "../../../fields/TextField"; -import { ListField } from "../../../fields/ListField"; -import { thisExpression } from "babel-types"; +import React = require("react"); @observer export class CollectionDockingView extends React.Component<SubCollectionViewProps> { public static Instance: CollectionDockingView; - public static makeDocumentConfig(document: Document) { + public static makeDocumentConfig(document: Doc, width?: number) { return { type: 'react-component', component: 'DocumentFrameRenderer', - title: document.Title, + title: document.title, + width: width, props: { - documentId: document.Id, + documentId: document[Id], //collectionDockingView: CollectionDockingView.Instance } }; @@ -38,7 +37,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp private _goldenLayout: any = null; private _containerRef = React.createRef<HTMLDivElement>(); - private _fullScreen: any = null; private _flush: boolean = false; private _ignoreStateChange = ""; @@ -48,14 +46,18 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp (window as any).React = React; (window as any).ReactDOM = ReactDOM; } - public StartOtherDrag(dragDocs: Document[], e: any) { + hack: boolean = false; + undohack: any = null; + public StartOtherDrag(dragDocs: Doc[], e: any) { + this.hack = true; + this.undohack = UndoManager.StartBatch("goldenDrag"); dragDocs.map(dragDoc => this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener. onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 })); } @action - public OpenFullScreen(document: Document) { + public OpenFullScreen(document: Doc) { let newItemStackConfig = { type: 'stack', content: [CollectionDockingView.makeDocumentConfig(document)] @@ -64,26 +66,52 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp this._goldenLayout.root.contentItems[0].addChild(docconfig); docconfig.callDownwards('_$init'); this._goldenLayout._$maximiseItem(docconfig); - this._fullScreen = docconfig; this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); this.stateChanged(); } + + @undoBatch @action - public CloseFullScreen() { - if (this._fullScreen) { - this._goldenLayout._$minimiseItem(this._fullScreen); - this._goldenLayout.root.contentItems[0].removeChild(this._fullScreen); - this._fullScreen = null; - this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); - this.stateChanged(); + public CloseRightSplit(document: Doc) { + if (this._goldenLayout.root.contentItems[0].isRow) { + this._goldenLayout.root.contentItems[0].contentItems.map((child: any, i: number) => { + if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" && + child.contentItems[0].config.props.documentId == document[Id]) { + 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]) { + 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(); + } + }); + }) } } + @action + layoutChanged(removed?: Doc) { + this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]); + this._goldenLayout.emit('sbcreteChanged'); + this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); + if (removed) CollectionDockingView.Instance._removedDocs.push(removed); + this.stateChanged(); + } + // // Creates a vertical split on the right side of the docking view, and then adds the Document to that split // @action - public AddRightSplit(document: Document, minimize: boolean = false) { + public AddRightSplit(document: Doc, minimize: boolean = false) { + let docs = Cast(this.props.Document.data, listSpec(Doc)); + if (docs) { + docs.push(document); + } let newItemStackConfig = { type: 'stack', content: [CollectionDockingView.makeDocumentConfig(document)] @@ -106,20 +134,18 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp newContentItem.config.width = 50; } if (minimize) { - newContentItem.config.width = 10; - newContentItem.config.height = 10; + // bcz: this makes the drag image show up better, but it also messes with fixed layout sizes + // newContentItem.config.width = 10; + // newContentItem.config.height = 10; } newContentItem.callDownwards('_$init'); - this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]); - this._goldenLayout.emit('stateChanged'); - this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); - this.stateChanged(); + this.layoutChanged(); return newContentItem; } setupGoldenLayout() { - var config = this.props.Document.GetText(KeyStore.Data, ""); + var config = StrCast(this.props.Document.dockingConfig); if (config) { if (!this._goldenLayout) { this._goldenLayout = new GoldenLayout(JSON.parse(config)); @@ -154,7 +180,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp componentDidMount: () => void = () => { if (this._containerRef.current) { reaction( - () => this.props.Document.GetText(KeyStore.Data, ""), + () => StrCast(this.props.Document.dockingConfig), () => { if (!this._goldenLayout || this._ignoreStateChange !== JSON.stringify(this._goldenLayout.toConfig())) { setTimeout(() => this.setupGoldenLayout(), 1); @@ -202,8 +228,8 @@ 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; - Server.GetField(docid, action(async (sourceDoc: Opt<Field>) => - (sourceDoc instanceof Document) && DragLinksAsDocuments(tab, x, y, sourceDoc))); + DocServer.GetRefField(docid).then(action(async (sourceDoc: Opt<Field>) => + (sourceDoc instanceof Doc) && DragLinksAsDocuments(tab, x, y, sourceDoc))); } else if ((className === "lm_title" || className === "lm_tab lm_active") && !e.shiftKey) { e.stopPropagation(); @@ -212,12 +238,12 @@ 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; - Server.GetField(docid, action((f: Opt<Field>) => { - if (f instanceof Document) { + DocServer.GetRefField(docid).then(action((f: Opt<Field>) => { + if (f instanceof Doc) { DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f]), x, y, { handlers: { - dragComplete: action(emptyFunction), + dragComplete: emptyFunction, }, hideSource: false }); @@ -234,8 +260,18 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp @undoBatch stateChanged = () => { + let docs = Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc)); + CollectionDockingView.Instance._removedDocs.map(theDoc => + docs && docs.indexOf(theDoc) !== -1 && + docs.splice(docs.indexOf(theDoc), 1)); + CollectionDockingView.Instance._removedDocs.length = 0; var json = JSON.stringify(this._goldenLayout.toConfig()); - this.props.Document.SetText(KeyStore.Data, json); + this.props.Document.dockingConfig = json; + if (this.undohack && !this.hack) { + this.undohack.end(); + this.undohack = undefined; + } + this.hack = false; } itemDropped = () => { @@ -249,40 +285,42 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp return template.content.firstChild; } - tabCreated = (tab: any) => { + tabCreated = async (tab: any) => { if (tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") { - Server.GetField(tab.contentItem.config.props.documentId, action((f: Opt<Field>) => { - if (f !== undefined && f instanceof Document) { - f.GetTAsync(KeyStore.Title, TextField, (tfield) => { - if (tfield !== undefined) { - tab.titleElement[0].textContent = f.Title; - } - }); - f.GetTAsync(KeyStore.LinkedFromDocs, ListField).then(lf => - f.GetTAsync(KeyStore.LinkedToDocs, ListField).then(lt => { - let count = (lf ? lf.Data.length : 0) + (lt ? lt.Data.length : 0); - let counter: any = this.htmlToElement(`<div class="messageCounter">${count}</div>`); - tab.element.append(counter); - counter.DashDocId = tab.contentItem.config.props.documentId; - tab.reactionDisposer = reaction(() => [f.GetT(KeyStore.LinkedFromDocs, ListField), f.GetT(KeyStore.LinkedToDocs, ListField)], - (lists) => { - let count = (lists.length > 0 && lists[0] && lists[0]!.Data ? lists[0]!.Data.length : 0) + - (lists.length > 1 && lists[1] && lists[1]!.Data ? lists[1]!.Data.length : 0); - counter.innerHTML = count; - }); - })); + if (tab.contentItem.config.fixed) { + tab.contentItem.parent.config.fixed = true; + } + DocServer.GetRefField(tab.contentItem.config.props.documentId).then(async doc => { + if (doc instanceof Doc) { + let counter: any = this.htmlToElement(`<div class="messageCounter">0</div>`); + tab.element.append(counter); + 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; + tab.titleElement[0].textContent = doc.title; + }, { fireImmediately: true }); tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; } - })); + }); } tab.closeElement.off('click') //unbind the current click handler - .click(function () { + .click(async function () { if (tab.reactionDisposer) { tab.reactionDisposer(); } + let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId); + if (doc instanceof Doc) { + let theDoc = doc; + CollectionDockingView.Instance._removedDocs.push(theDoc); + } tab.contentItem.remove(); }); } + _removedDocs: Doc[] = []; stackCreated = (stack: any) => { //stack.header.controlsContainer.find('.lm_popout').hide(); @@ -291,13 +329,21 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp .click(action(function () { //if (confirm('really close this?')) { stack.remove(); + stack.contentItems.map(async (contentItem: any) => { + let doc = await DocServer.GetRefField(contentItem.config.props.documentId); + if (doc instanceof Doc) { + let theDoc = doc; + CollectionDockingView.Instance._removedDocs.push(theDoc); + } + }); //} })); stack.header.controlsContainer.find('.lm_popout') //get the close icon .off('click') //unbind the current click handler .click(action(function () { - var url = ServerUtils.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId); - let win = window.open(url, stack.contentItems[0].tab.title, "width=300,height=400"); + stack.config.fixed = !stack.config.fixed; + // var url = DocServer.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId); + // let win = window.open(url, stack.contentItems[0].tab.title, "width=300,height=400"); })); } @@ -307,6 +353,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} /> ); } + } interface DockedFrameProps { @@ -315,61 +362,64 @@ interface DockedFrameProps { } @observer export class DockedFrameRenderer extends React.Component<DockedFrameProps> { - - private _mainCont = React.createRef<HTMLDivElement>(); + _mainCont = React.createRef<HTMLDivElement>(); @observable private _panelWidth = 0; @observable private _panelHeight = 0; - @observable private _document: Opt<Document>; + @observable private _document: Opt<Doc>; constructor(props: any) { super(props); - Server.GetField(this.props.documentId, action((f: Opt<Field>) => this._document = f as Document)); + DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc)); } - private _nativeWidth = () => this._document!.GetNumber(KeyStore.NativeWidth, this._panelWidth); - private _nativeHeight = () => this._document!.GetNumber(KeyStore.NativeHeight, this._panelHeight); - private _contentScaling = () => { - let wscale = this._panelWidth / (this._nativeWidth() ? this._nativeWidth() : this._panelWidth); - if (wscale * this._nativeHeight() > this._panelHeight) - return this._panelHeight / (this._nativeHeight() ? this._nativeHeight() : this._panelHeight); - return wscale; + nativeWidth = () => NumCast(this._document!.nativeWidth, this._panelWidth); + nativeHeight = () => NumCast(this._document!.nativeHeight, this._panelHeight); + contentScaling = () => { + const nativeH = this.nativeHeight(); + const nativeW = this.nativeWidth(); + let wscale = this._panelWidth / nativeW; + return wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale; } ScreenToLocalTransform = () => { - let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current!.children[0].firstChild as HTMLElement); - let scaling = scale; - { - let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current!); - scaling = scale; + 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(scaling / this._contentScaling()); + return Transform.Identity(); } + get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; } - render() { - if (!this._document) { + get content() { + if (!this._document) return (null); - } - let wscale = this._panelWidth / (this._nativeWidth() ? this._nativeWidth() : this._panelWidth); - let name = (wscale * this._nativeHeight() > this._panelHeight) ? "" : "-height"; - var content = - <div className={`collectionDockingView-content${name}`} ref={this._mainCont}> - <DocumentView key={this._document.Id} Document={this._document} + return ( + <div className="collectionDockingView-content" ref={this._mainCont} + style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}> + <DocumentView key={this._document![Id]} Document={this._document!} + toggleMinimized={emptyFunction} addDocument={undefined} removeDocument={undefined} - ContentScaling={this._contentScaling} - PanelWidth={this._nativeWidth} - PanelHeight={this._nativeHeight} + ContentScaling={this.contentScaling} + PanelWidth={this.nativeWidth} + PanelHeight={this.nativeHeight} ScreenToLocalTransform={this.ScreenToLocalTransform} isTopMost={true} selectOnLoad={false} parentActive={returnTrue} - onActiveChanged={emptyFunction} - focus={emptyDocFunction} + whenActiveChanged={emptyFunction} + focus={emptyFunction} + bringToFront={emptyFunction} ContainingCollectionView={undefined} /> - </div>; + </div >); + } - return <Measure onResize={action((r: any) => { this._panelWidth = r.entry.width; this._panelHeight = r.entry.height; })}> - {({ measureRef }) => <div ref={measureRef}> {content} </div>} - </Measure>; + render() { + let theContent = this.content; + return !this._document ? (null) : + <Measure onResize={action((r: any) => { this._panelWidth = r.entry.width; this._panelHeight = r.entry.height; })}> + {({ measureRef }) => <div ref={measureRef}> {theContent} </div>} + </Measure>; } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionPDFView.scss b/src/client/views/collections/CollectionPDFView.scss index 0eca3f1cd..f6fb79582 100644 --- a/src/client/views/collections/CollectionPDFView.scss +++ b/src/client/views/collections/CollectionPDFView.scss @@ -1,20 +1,39 @@ .collectionPdfView-buttonTray { - top : 25px; + top : 15px; left : 20px; position: relative; transform-origin: left top; position: absolute; } +.collectionPdfView-thumb { + width:25px; + height:25px; + transform-origin: left top; + position: absolute; + background: darkgray; +} +.collectionPdfView-slider { + width:25px; + height:25px; + transform-origin: left top; + position: absolute; + background: lightgray; +} .collectionPdfView-cont{ width: 100%; height: 100%; position: absolute; top: 0; left:0; - +} +.collectionPdfView-cont-dragging { + span { + user-select: none; + } } .collectionPdfView-backward { color : white; + font-size: 24px; top :0px; left : 0px; position: absolute; @@ -22,8 +41,9 @@ } .collectionPdfView-forward { color : white; + font-size: 24px; top :0px; - left : 35px; + left : 45px; position: absolute; background-color: rgba(50, 50, 50, 0.2); }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx index 229bc4059..b3762206a 100644 --- a/src/client/views/collections/CollectionPDFView.tsx +++ b/src/client/views/collections/CollectionPDFView.tsx @@ -1,6 +1,5 @@ -import { action } from "mobx"; +import { action, observable } from "mobx"; import { observer } from "mobx-react"; -import { KeyStore } from "../../../fields/KeyStore"; import { ContextMenu } from "../ContextMenu"; import "./CollectionPDFView.scss"; import React = require("react"); @@ -8,32 +7,60 @@ import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormV import { FieldView, FieldViewProps } from "../nodes/FieldView"; import { CollectionRenderProps, CollectionBaseView, CollectionViewType } from "./CollectionBaseView"; import { emptyFunction } from "../../../Utils"; +import { NumCast } from "../../../new_fields/Types"; +import { Id } from "../../../new_fields/RefField"; @observer export class CollectionPDFView extends React.Component<FieldViewProps> { - public static LayoutString(fieldKey: string = "DataKey") { + public static LayoutString(fieldKey: string = "data") { return FieldView.LayoutString(CollectionPDFView, fieldKey); } + @observable _inThumb = false; - private get curPage() { return this.props.Document.GetNumber(KeyStore.CurPage, -1); } - private get numPages() { return this.props.Document.GetNumber(KeyStore.NumPages, 0); } - @action onPageBack = () => this.curPage > 1 ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage - 1) : -1; - @action onPageForward = () => this.curPage < this.numPages ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage + 1) : -1; + private set curPage(value: number) { this.props.Document.curPage = value; } + private get curPage() { return NumCast(this.props.Document.curPage, -1); } + private get numPages() { return NumCast(this.props.Document.numPages); } + @action onPageBack = () => this.curPage > 1 ? (this.props.Document.curPage = this.curPage - 1) : -1; + @action onPageForward = () => this.curPage < this.numPages ? (this.props.Document.curPage = this.curPage + 1) : -1; + @action + onThumbDown = (e: React.PointerEvent) => { + document.addEventListener("pointermove", this.onThumbMove, false); + document.addEventListener("pointerup", this.onThumbUp, false); + e.stopPropagation(); + this._inThumb = true; + } + @action + onThumbMove = (e: PointerEvent) => { + let pso = (e.clientY - (e as any).target.parentElement.getBoundingClientRect().top) / (e as any).target.parentElement.getBoundingClientRect().height; + this.curPage = Math.trunc(Math.min(this.numPages, pso * this.numPages + 1)); + e.stopPropagation(); + } + @action + onThumbUp = (e: PointerEvent) => { + this._inThumb = false; + document.removeEventListener("pointermove", this.onThumbMove); + document.removeEventListener("pointerup", this.onThumbUp); + } + nativeWidth = () => NumCast(this.props.Document.nativeWidth); + nativeHeight = () => NumCast(this.props.Document.nativeHeight); private get uIButtons() { - let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale); + let ratio = (this.curPage - 1) / this.numPages * 100; return ( - <div className="collectionPdfView-buttonTray" key="tray" style={{ transform: `scale(${scaling}, ${scaling})` }}> + <div className="collectionPdfView-buttonTray" key="tray" style={{ height: "100%" }}> <button className="collectionPdfView-backward" onClick={this.onPageBack}>{"<"}</button> <button className="collectionPdfView-forward" onClick={this.onPageForward}>{">"}</button> + <div className="collectionPdfView-slider" onPointerDown={this.onThumbDown} style={{ top: 60, left: -20, width: 50, height: `calc(100% - 80px)` }} > + <div className="collectionPdfView-thumb" onPointerDown={this.onThumbDown} style={{ top: `${ratio}%`, width: 50, height: 50 }} /> + </div> </div> ); } onContextMenu = (e: React.MouseEvent): void => { - if (!e.isPropagationStopped() && this.props.Document.Id !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 + 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 }); } } @@ -50,7 +77,7 @@ export class CollectionPDFView extends React.Component<FieldViewProps> { render() { return ( - <CollectionBaseView {...this.props} className="collectionPdfView-cont" onContextMenu={this.onContextMenu}> + <CollectionBaseView {...this.props} className={`collectionPdfView-cont${this._inThumb ? "-dragging" : ""}`} onContextMenu={this.onContextMenu}> {this.subView} </CollectionBaseView> ); diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index c8bfedff4..cfdb3ab22 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -1,61 +1,6 @@ @import "../globalCssVariables"; -//options menu styling -#schemaOptionsMenuBtn { - position: absolute; - height: 20px; - width: 20px; - border-radius: 50%; - z-index: 21; - right: 4px; - top: 4px; - pointer-events: auto; - background-color:black; - display:inline-block; - padding: 0px; - font-size: 100%; -} - -ul { - list-style-type: disc; -} - -#schema-options-header { - text-align: center; - padding: 0px; - margin: 0px; -} -.schema-options-subHeader { - color: $intermediate-color; - margin-bottom: 5px; -} -#schemaOptionsMenuBtn:hover { - transform: scale(1.15); -} - -#preview-schema-checkbox-div { - margin-left: 20px; - font-size: 12px; -} - #options-flyout-div { - text-align: left; - padding:0px; - z-index: 100; - font-family: $sans-serif; - padding-left: 5px; - } - - #schema-col-checklist { - overflow: scroll; - text-align: left; - //background-color: $light-color-secondary; - line-height: 25px; - max-height: 175px; - font-family: $sans-serif; - font-size: 12px; - } - .collectionSchemaView-container { border-width: $COLLECTION_BORDER_WIDTH; @@ -66,18 +11,31 @@ ul { position: absolute; width: 100%; height: 100%; - - .collectionSchemaView-content { - position: absolute; - height: 100%; - width: 100%; - overflow: auto; + + .collectionSchemaView-cellContents { + height: $MAX_ROW_HEIGHT; } + .collectionSchemaView-previewRegion { position: relative; background: $light-color; float: left; height: 100%; + .collectionSchemaView-previewDoc { + height: 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; @@ -151,7 +109,7 @@ ul { } .rt-tr-group { direction: ltr; - max-height: 44px; + max-height: $MAX_ROW_HEIGHT; } .rt-td { border-width: 1px; @@ -183,7 +141,7 @@ ul { } .ReactTable .rt-th, .ReactTable .rt-td { - max-height: 44; + max-height: $MAX_ROW_HEIGHT; padding: 3px 7px; font-size: 13px; text-align: center; @@ -193,13 +151,71 @@ ul { border-bottom-style: solid; border-bottom-width: 1; } + .documentView-node-topmost { + text-align:left; + transform-origin: center top; + display: inline-block; + } .documentView-node:first-child { background: $light-color; - .imageBox-cont img { - object-fit: contain; - } } } +//options menu styling +#schemaOptionsMenuBtn { + position: absolute; + height: 20px; + width: 20px; + border-radius: 50%; + z-index: 21; + right: 4px; + top: 4px; + pointer-events: auto; + background-color:black; + display:inline-block; + padding: 0px; + font-size: 100%; +} + +ul { + list-style-type: disc; +} + +#schema-options-header { + text-align: center; + padding: 0px; + margin: 0px; +} +.schema-options-subHeader { + color: $intermediate-color; + margin-bottom: 5px; +} +#schemaOptionsMenuBtn:hover { + transform: scale(1.15); +} + +#preview-schema-checkbox-div { + margin-left: 20px; + font-size: 12px; +} + + #options-flyout-div { + text-align: left; + padding:0px; + z-index: 100; + font-family: $sans-serif; + padding-left: 5px; + } + + #schema-col-checklist { + overflow: scroll; + text-align: left; + //background-color: $light-color-secondary; + line-height: 25px; + max-height: 175px; + font-family: $sans-serif; + font-size: 12px; + } + .Resizer { box-sizing: border-box; diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 1defdba7e..16818affd 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -2,20 +2,14 @@ import React = require("react"); import { library } from '@fortawesome/fontawesome-svg-core'; import { faCog, faPlus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, untracked } from "mobx"; +import { action, computed, observable, untracked, runInAction } from "mobx"; import { observer } from "mobx-react"; -import Measure from "react-measure"; import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table"; +import { MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss'; import "react-table/react-table.css"; -import { Document } from "../../../fields/Document"; -import { Field, Opt } from "../../../fields/Field"; -import { Key } from "../../../fields/Key"; -import { KeyStore } from "../../../fields/KeyStore"; -import { ListField } from "../../../fields/ListField"; -import { emptyDocFunction, emptyFunction, returnFalse, returnOne } from "../../../Utils"; -import { Server } from "../../Server"; +import { emptyFunction, returnFalse, returnZero } from "../../../Utils"; import { SetupDrag } from "../../util/DragManager"; -import { CompileScript, ToField } from "../../util/Scripting"; +import { CompileScript } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; import { COLLECTION_BORDER_WIDTH } from "../../views/globalCssVariables.scss"; import { anchorPoints, Flyout } from "../DocumentDecorations"; @@ -25,51 +19,47 @@ 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 { listSpec } from "../../../new_fields/Schema"; +import { List } from "../../../new_fields/List"; +import { Id } from "../../../new_fields/RefField"; // bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657 @observer -class KeyToggle extends React.Component<{ keyId: string, checked: boolean, toggle: (key: Key) => void }> { - @observable key: Key | undefined; - - componentWillReceiveProps() { - Server.GetField(this.props.keyId, action((field: Opt<Field>) => { - if (field instanceof Key) { - this.key = field; - } - })); +class KeyToggle extends React.Component<{ keyName: string, checked: boolean, toggle: (key: string) => void }> { + constructor(props: any) { + super(props); } render() { - if (this.key) { - return (<div key={this.key.Id}> - <input type="checkbox" checked={this.props.checked} onChange={() => this.key && this.props.toggle(this.key)} /> - {this.key.Name} - </div>); - } - return (null); + return ( + <div key={this.props.keyName}> + <input type="checkbox" checked={this.props.checked} onChange={() => this.props.toggle(this.props.keyName)} /> + {this.props.keyName} + </div> + ); } } @observer -export class CollectionSchemaView extends CollectionSubView { - private _mainCont = React.createRef<HTMLDivElement>(); +export class CollectionSchemaView extends CollectionSubView(doc => doc) { + private _mainCont?: HTMLDivElement; private _startSplitPercent = 0; private DIVIDER_WIDTH = 4; - @observable _columns: Array<Key> = [KeyStore.Title, KeyStore.Data, KeyStore.Author]; - @observable _contentScaling = 1; // used to transfer the dimensions of the content pane in the DOM to the ContentScaling prop of the DocumentView - @observable _dividerX = 0; - @observable _panelWidth = 0; - @observable _panelHeight = 0; + @observable _columns: Array<string> = ["title", "data", "author"]; @observable _selectedIndex = 0; @observable _columnsPercentage = 0; - @observable _keys: Key[] = []; - - @computed get splitPercentage() { return this.props.Document.GetNumber(KeyStore.SchemaSplitPercentage, 0); } + @observable _keys: string[] = []; + @observable _newKeyName: string = ""; + @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage); } + @computed get columns() { return Cast(this.props.Document.schemaColumns, listSpec("string"), []); } + @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } renderCell = (rowProps: CellInfo) => { let props: FieldViewProps = { @@ -81,43 +71,38 @@ export class CollectionSchemaView extends CollectionSubView { isTopMost: false, selectOnLoad: false, ScreenToLocalTransform: Transform.Identity, - focus: emptyDocFunction, + focus: emptyFunction, active: returnFalse, - onActiveChanged: emptyFunction, + whenActiveChanged: emptyFunction, + PanelHeight: returnZero, + PanelWidth: returnZero, }; let contents = ( <FieldView {...props} /> ); let reference = React.createRef<HTMLDivElement>(); let onItemDown = SetupDrag(reference, () => props.Document, this.props.moveDocument); - let applyToDoc = (doc: Document, run: (args?: { [name: string]: any }) => any) => { + let applyToDoc = (doc: Doc, run: (args?: { [name: string]: any }) => any) => { const res = run({ this: doc }); if (!res.success) return false; const field = res.result; - if (field instanceof Field) { - doc.Set(props.fieldKey, field); - return true; - } else { - let dataField = ToField(field); - if (dataField) { - doc.Set(props.fieldKey, dataField); - return true; - } - } - return false; + doc[props.fieldKey] = field; + return true; }; return ( - <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} style={{ height: "56px" }} key={props.Document.Id} ref={reference}> + <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={props.Document[Id]} ref={reference}> <EditableView display={"inline"} contents={contents} - height={56} + height={Number(MAX_ROW_HEIGHT)} GetValue={() => { - let field = props.Document.Get(props.fieldKey); - if (field && field instanceof Field) { - return field.ToScriptString(); + let field = props.Document[props.fieldKey]; + if (field) { + //TODO Types + // return field.ToScriptString(); + return String(field); } - return field || ""; + return ""; }} SetValue={(value: string) => { let script = CompileScript(value, { addReturn: true, params: { this: Document.name } }); @@ -126,21 +111,18 @@ export class CollectionSchemaView extends CollectionSubView { } return applyToDoc(props.Document, script.run); }} - OnFillDown={(value: string) => { + OnFillDown={async (value: string) => { let script = CompileScript(value, { addReturn: true, params: { this: Document.name } }); if (!script.compiled) { return; } const run = script.run; //TODO This should be able to be refactored to compile the script once - this.props.Document.GetTAsync<ListField<Document>>(this.props.fieldKey, ListField).then((val) => { - if (val) { - val.Data.forEach(doc => applyToDoc(doc, run)); - } - }); + const val = await DocListCast(this.props.Document[this.props.fieldKey]) + val && val.forEach(doc => applyToDoc(doc, run)); }}> </EditableView> - </div> + </div > ); } @@ -165,61 +147,43 @@ export class CollectionSchemaView extends CollectionSubView { }; } - @computed - get columns() { - return this.props.Document.GetList(KeyStore.ColumnsKey, [] as Key[]); + private createTarget = (ele: HTMLDivElement) => { + this._mainCont = ele; + super.CreateDropTarget(ele); } @action - toggleKey = (key: Key) => { - this.props.Document.GetOrCreateAsync<ListField<Key>>(KeyStore.ColumnsKey, ListField, - (field) => { - const index = field.Data.indexOf(key); - if (index === -1) { - this.columns.push(key); - } else { - this.columns.splice(index, 1); - } - - }); + toggleKey = (key: string) => { + let list = Cast(this.props.Document.schemaColumns, listSpec("string")); + if (list === undefined) { + this.props.Document.schemaColumns = list = new List<string>([key]); + } else { + const index = list.indexOf(key); + if (index === -1) { + list.push(key); + } else { + list.splice(index, 1); + } + } } //toggles preview side-panel of schema @action toggleExpander = (event: React.ChangeEvent<HTMLInputElement>) => { - this._startSplitPercent = this.splitPercentage; - if (this._startSplitPercent === this.splitPercentage) { - this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, this.splitPercentage === 0 ? 33 : 0); - } - } - - @computed - get findAllDocumentKeys(): { [id: string]: boolean } { - const docs = this.props.Document.GetList(this.props.fieldKey, [] as Document[]); - let keys: { [id: string]: boolean } = {}; - if (this._optionsActivated > -1) { - // 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 - // invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked. - // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu - // is displayed (unlikely) it won't show up until something else changes. - untracked(() => docs.map(doc => doc.GetAllPrototypes().map(proto => proto._proxies.forEach((val: any, key: string) => keys[key] = false)))); - } - this.columns.forEach(key => keys[key.Id] = true); - return keys; + this.props.Document.schemaSplitPercentage = this.splitPercentage === 0 ? 33 : 0; } @action onDividerMove = (e: PointerEvent): void => { - let nativeWidth = this._mainCont.current!.getBoundingClientRect(); - this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100))); + let nativeWidth = this._mainCont!.getBoundingClientRect(); + this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100)); } @action onDividerUp = (e: PointerEvent): void => { document.removeEventListener("pointermove", this.onDividerMove); document.removeEventListener('pointerup', this.onDividerUp); if (this._startSplitPercent === this.splitPercentage) { - this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, this.splitPercentage === 0 ? 33 : 0); + this.props.Document.schemaSplitPercentage = this.splitPercentage === 0 ? 33 : 0; } } onDividerDown = (e: React.PointerEvent) => { @@ -230,157 +194,154 @@ export class CollectionSchemaView extends CollectionSubView { document.addEventListener('pointerup', this.onDividerUp); } - @observable _tableWidth = 0; - @action - setTableDimensions = (r: any) => { - this._tableWidth = r.entry.width; - } - @action - setScaling = (r: any) => { - const children = this.props.Document.GetList(this.props.fieldKey, [] as Document[]); - const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined; - this._panelWidth = r.entry.width; - this._panelHeight = r.entry.height ? r.entry.height : this._panelHeight; - this._contentScaling = r.entry.width / selected!.GetNumber(KeyStore.NativeWidth, r.entry.width); - } - - @computed - get borderWidth() { return COLLECTION_BORDER_WIDTH; } - getContentScaling = (): number => this._contentScaling; - getPanelWidth = (): number => this._panelWidth; - getPanelHeight = (): number => this._panelHeight; - getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(- this.borderWidth - this.DIVIDER_WIDTH - this._dividerX, - this.borderWidth).scale(1 / this._contentScaling); - getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate(- this.borderWidth - this.DIVIDER_WIDTH - this._dividerX - this._tableWidth, - this.borderWidth).scale(1 / this._contentScaling); - onPointerDown = (e: React.PointerEvent): void => { if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) { - if (this.props.isSelected()) - e.stopPropagation(); + if (this.props.isSelected()) e.stopPropagation(); else e.preventDefault(); } } - @action - addColumn = () => { - this.columns.push(new Key(this.newKeyName)); - this.newKeyName = ""; - } - - @observable - newKeyName: string = ""; - - @action - newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this.newKeyName = e.currentTarget.value; - } onWheel = (e: React.WheelEvent): void => { if (this.props.active()) { e.stopPropagation(); } } - @observable _optionsActivated: number = 0; @action - OptionsMenuDown = (e: React.PointerEvent) => { - this._optionsActivated++; + addColumn = () => { + this.columns.push(this._newKeyName); + this._newKeyName = ""; + } + + @action + newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this._newKeyName = e.currentTarget.value; } - @observable previewScript: string = "this"; + @observable previewScript: string = ""; @action onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.previewScript = e.currentTarget.value; } - render() { - library.add(faCog); - library.add(faPlus); - const columns = this.columns; - const children = this.props.Document.GetList(this.props.fieldKey, [] as Document[]); - const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined; - //all the keys/columns that will be displayed in the schema - const allKeys = this.findAllDocumentKeys; - let doc: any = selected ? selected.Get(new Key(this.previewScript)) : undefined; + get previewDocument(): Doc | undefined { + const children = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []); + const selected = children.length > this._selectedIndex ? FieldValue(children[this._selectedIndex]) : undefined; + return selected ? (this.previewScript ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined; + } + get tableWidth() { return (this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH) * (1 - this.splitPercentage / 100); } + get previewRegionHeight() { return this.props.PanelHeight() - 2 * this.borderWidth; } + get previewRegionWidth() { return (this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH) * this.splitPercentage / 100; } + + private previewDocNativeWidth = () => Cast(this.previewDocument!.nativeWidth, "number", this.previewRegionWidth); + private previewDocNativeHeight = () => Cast(this.previewDocument!.nativeHeight, "number", this.previewRegionHeight); + private previewContentScaling = () => { + let wscale = this.previewRegionWidth / (this.previewDocNativeWidth() ? this.previewDocNativeWidth() : this.previewRegionWidth); + if (wscale * this.previewDocNativeHeight() > this.previewRegionHeight) { + return this.previewRegionHeight / (this.previewDocNativeHeight() ? this.previewDocNativeHeight() : this.previewRegionHeight); + } + return wscale; + } + private previewPanelWidth = () => this.previewDocNativeWidth() * this.previewContentScaling(); + private previewPanelHeight = () => this.previewDocNativeHeight() * this.previewContentScaling(); + get previewPanelCenteringOffset() { return (this.previewRegionWidth - this.previewDocNativeWidth() * this.previewContentScaling()) / 2; } + getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate( + - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth - this.previewPanelCenteringOffset, + - this.borderWidth).scale(1 / this.previewContentScaling()) + @computed + get previewPanel() { // let doc = CompileScript(this.previewScript, { this: selected }, true)(); - let content = this._selectedIndex === -1 || !selected ? (null) : ( - <Measure onResize={this.setScaling}> - {({ measureRef }) => - <div className="collectionSchemaView-content" ref={measureRef}> - {doc instanceof Document ? - <DocumentView Document={doc} - addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} - isTopMost={false} - selectOnLoad={false} - ScreenToLocalTransform={this.getPreviewTransform} - ContentScaling={this.getContentScaling} - PanelWidth={this.getPanelWidth} - PanelHeight={this.getPanelHeight} - ContainingCollectionView={this.props.CollectionView} - focus={emptyDocFunction} - parentActive={this.props.active} - onActiveChanged={this.props.onActiveChanged} /> : null} - <input value={this.previewScript} onChange={this.onPreviewScriptChange} - style={{ position: 'absolute', bottom: '0px' }} /> - </div> - } - </Measure> - ); - let dividerDragger = this.splitPercentage === 0 ? (null) : - <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />; - - //options button and menu - let optionsMenu = !this.props.active() ? (null) : (<Flyout - anchorPoint={anchorPoints.LEFT_TOP} - content={<div> - <div id="schema-options-header"><h5><b>Options</b></h5></div> - <div id="options-flyout-div"> - <h6 className="schema-options-subHeader">Preview Window</h6> - <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.splitPercentage !== 0} onChange={this.toggleExpander} /> Show Preview </div> - <h6 className="schema-options-subHeader" >Displayed Columns</h6> - <ul id="schema-col-checklist" > - {Array.from(Object.keys(allKeys)).map(item => - (<KeyToggle checked={allKeys[item]} key={item} keyId={item} toggle={this.toggleKey} />))} - </ul> - <input value={this.newKeyName} onChange={this.newKeyChange} /> - <button onClick={this.addColumn}><FontAwesomeIcon style={{ color: "white" }} icon="plus" size="lg" /></button> + const previewDoc = this.previewDocument; + return !previewDoc ? (null) : ( + <div className="collectionSchemaView-previewRegion" style={{ width: `${this.previewRegionWidth}px` }}> + <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}> + <DocumentView Document={previewDoc} isTopMost={false} selectOnLoad={false} + toggleMinimized={emptyFunction} + addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} + ScreenToLocalTransform={this.getPreviewTransform} + ContentScaling={this.previewContentScaling} + PanelWidth={this.previewPanelWidth} PanelHeight={this.previewPanelHeight} + ContainingCollectionView={this.props.CollectionView} + focus={emptyFunction} + parentActive={this.props.active} + whenActiveChanged={this.props.whenActiveChanged} + bringToFront={emptyFunction} + /> </div> + <input className="collectionSchemaView-input" value={this.previewScript} onChange={this.onPreviewScriptChange} + style={{ left: `calc(50% - ${Math.min(75, this.previewPanelWidth() / 2)}px)` }} /> </div> - }> - <button id="schemaOptionsMenuBtn" onPointerDown={this.OptionsMenuDown}><FontAwesomeIcon style={{ color: "white" }} icon="cog" size="sm" /></button> - </Flyout>); + ); + } - return ( - <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel} ref={this._mainCont}> - <div className="collectionSchemaView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}> - <Measure onResize={this.setTableDimensions}> - {({ measureRef }) => - <div className="collectionSchemaView-tableContainer" ref={measureRef} style={{ width: `calc(100% - ${this.splitPercentage}%)` }}> - <ReactTable - data={children} - pageSize={children.length} - page={0} - showPagination={false} - columns={columns.map(col => ({ - Header: col.Name, - accessor: (doc: Document) => [doc, col], - id: col.Id - }))} - column={{ - ...ReactTableDefaults.column, - Cell: this.renderCell, + get documentKeysCheckList() { + const docs = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []); + 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 + // invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked. + // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu + // is displayed (unlikely) it won't show up until something else changes. + //TODO Types + untracked(() => docs.map(doc => Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false)))); + + this.columns.forEach(key => keys[key] = true); + return Array.from(Object.keys(keys)).map(item => + (<KeyToggle checked={keys[item]} key={item} keyName={item} toggle={this.toggleKey} />)); + } - }} - getTrProps={this.getTrProps} - /> - </div>} - </Measure> - {dividerDragger} - <div className="collectionSchemaView-previewRegion" style={{ width: `calc(${this.props.Document.GetNumber(KeyStore.SchemaSplitPercentage, 0)}% - ${this.DIVIDER_WIDTH}px)` }}> - {content} + get tableOptionsPanel() { + return !this.props.active() ? (null) : + (<Flyout + anchorPoint={anchorPoints.RIGHT_TOP} + content={<div> + <div id="schema-options-header"><h5><b>Options</b></h5></div> + <div id="options-flyout-div"> + <h6 className="schema-options-subHeader">Preview Window</h6> + <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.splitPercentage !== 0} onChange={this.toggleExpander} /> Show Preview </div> + <h6 className="schema-options-subHeader" >Displayed Columns</h6> + <ul id="schema-col-checklist" > + {this.documentKeysCheckList} + </ul> + <input value={this._newKeyName} onChange={this.newKeyChange} /> + <button onClick={this.addColumn}><FontAwesomeIcon style={{ color: "white" }} icon="plus" size="lg" /></button> </div> - {optionsMenu} </div> - </div > + }> + <button id="schemaOptionsMenuBtn" ><FontAwesomeIcon style={{ color: "white" }} icon="cog" size="sm" /></button> + </Flyout>); + } + + @computed + get dividerDragger() { + return this.splitPercentage === 0 ? (null) : + <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />; + } + + render() { + library.add(faCog); + library.add(faPlus); + //This can't just pass FieldValue to filter because filter passes other arguments to the passed in function, which end up as default values in FieldValue + const children = (this.children || []).filter(doc => FieldValue(doc)); + return ( + <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel} + onDrop={(e: React.DragEvent) => this.onDrop(e, {})} 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 + }))} + column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }} + getTrProps={this.getTrProps} + /> + </div> + {this.dividerDragger} + {this.previewPanel} + {this.tableOptionsPanel} + </div> ); } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index d3d69b1af..828ac880a 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,29 +1,29 @@ import { action, runInAction } from "mobx"; -import { Document } from "../../../fields/Document"; -import { ListField } from "../../../fields/ListField"; import React = require("react"); -import { KeyStore } from "../../../fields/KeyStore"; -import { FieldWaiting, Opt } from "../../../fields/Field"; import { undoBatch, UndoManager } from "../../util/UndoManager"; import { DragManager } from "../../util/DragManager"; -import { Documents, DocumentOptions } from "../../documents/Documents"; +import { Docs, DocumentOptions } from "../../documents/Documents"; import { RouteStore } from "../../../server/RouteStore"; -import { TupleField } from "../../../fields/TupleField"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; -import { NumberField } from "../../../fields/NumberField"; -import { ServerUtils } from "../../../server/ServerUtil"; -import { Server } from "../../Server"; import { FieldViewProps } from "../nodes/FieldView"; import * as rp from 'request-promise'; -import { emptyFunction } from "../../../Utils"; import { CollectionView } from "./CollectionView"; import { CollectionPDFView } from "./CollectionPDFView"; import { CollectionVideoView } from "./CollectionVideoView"; +import { Doc, Opt } from "../../../new_fields/Doc"; +import { DocComponent } from "../DocComponent"; +import { listSpec } from "../../../new_fields/Schema"; +import { Cast, PromiseValue, FieldValue } from "../../../new_fields/Types"; +import { List } from "../../../new_fields/List"; +import { DocServer } from "../../DocServer"; +import { ObjectField } from "../../../new_fields/ObjectField"; export interface CollectionViewProps extends FieldViewProps { - addDocument: (document: Document, allowDuplicates?: boolean) => boolean; - removeDocument: (document: Document) => boolean; - moveDocument: (document: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean; + addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; + removeDocument: (document: Doc) => boolean; + moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; + PanelWidth: () => number; + PanelHeight: () => number; } export interface SubCollectionViewProps extends CollectionViewProps { @@ -32,195 +32,196 @@ export interface SubCollectionViewProps extends CollectionViewProps { export type CursorEntry = TupleField<[string, string], [number, number]>; -export class CollectionSubView extends React.Component<SubCollectionViewProps> { - private dropDisposer?: DragManager.DragDropDisposer; - protected createDropTarget = (ele: HTMLDivElement) => { - if (this.dropDisposer) { - this.dropDisposer(); +export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { + class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) { + private dropDisposer?: DragManager.DragDropDisposer; + protected createDropTarget = (ele: HTMLDivElement) => { + if (this.dropDisposer) { + this.dropDisposer(); + } + if (ele) { + this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + } } - if (ele) { - this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + protected CreateDropTarget(ele: HTMLDivElement) { + this.createDropTarget(ele); } - } - @action - protected setCursorPosition(position: [number, number]) { - let ind; - let doc = this.props.Document; - let id = CurrentUserUtils.id; - let email = CurrentUserUtils.email; - if (id && email) { - let textInfo: [string, string] = [id, email]; - doc.GetTAsync(KeyStore.Prototype, Document).then(proto => { + 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)); + } + + @action + protected async setCursorPosition(position: [number, number]) { + return; + let ind; + let doc = this.props.Document; + let id = CurrentUserUtils.id; + let email = CurrentUserUtils.email; + if (id && email) { + let textInfo: [string, string] = [id, email]; + const proto = await doc.proto; if (!proto) { return; } - proto.GetOrCreateAsync<ListField<CursorEntry>>(KeyStore.Cursors, ListField, action((field: ListField<CursorEntry>) => { - let cursors = field.Data; - if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.Data[0][0] === id)) > -1) { - cursors[ind].Data[1] = position; - } else { - let entry = new TupleField<[string, string], [number, number]>([textInfo, position]); - cursors.push(entry); - } - })); - }); + let cursors = await Cast(proto.cursors, listSpec(ObjectField)); + if (!cursors) { + proto.cursors = cursors = new List<ObjectField>(); + } + if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.Data[0][0] === id)) > -1) { + cursors[ind].Data[1] = position; + } else { + let entry = new TupleField<[string, string], [number, number]>([textInfo, position]); + cursors.push(entry); + } + } } - } - @undoBatch - @action - protected drop(e: Event, de: DragManager.DropEvent): boolean { - if (de.data instanceof DragManager.DocumentDragData) { - if (de.data.aliasOnDrop || de.data.copyOnDrop) { - [KeyStore.Width, KeyStore.Height, KeyStore.CurPage].map(key => - de.data.draggedDocuments.map((draggedDocument: Document, i: number) => - draggedDocument.GetTAsync(key, NumberField, (f: Opt<NumberField>) => f ? de.data.droppedDocuments[i].SetNumber(key, f.Data) : null))); - } - let added = false; - if (de.data.aliasOnDrop || de.data.copyOnDrop) { - added = de.data.droppedDocuments.reduce((added: boolean, d) => { - let moved = this.props.addDocument(d); - return moved || added; - }, false); - } else if (de.data.moveDocument) { - const move = de.data.moveDocument; - added = de.data.droppedDocuments.reduce((added: boolean, d) => { - let moved = move(d, this.props.Document, this.props.addDocument); - return moved || added; - }, false); - } else { - added = de.data.droppedDocuments.reduce((added: boolean, d) => { - let moved = this.props.addDocument(d); - return moved || added; - }, false); + @undoBatch + @action + protected drop(e: Event, de: DragManager.DropEvent): boolean { + if (de.data instanceof DragManager.DocumentDragData) { + if (de.data.dropAction || de.data.userDropAction) { + ["width", "height", "curPage"].map(key => + de.data.draggedDocuments.map((draggedDocument: Doc, i: number) => + PromiseValue(Cast(draggedDocument[key], "number")).then(f => f && (de.data.droppedDocuments[i][key] = f)))); + } + let added = false; + if (de.data.dropAction || de.data.userDropAction) { + added = de.data.droppedDocuments.reduce((added: boolean, d) => { + let moved = this.props.addDocument(d); + return moved || added; + }, false); + } else if (de.data.moveDocument) { + const move = de.data.moveDocument; + added = de.data.droppedDocuments.reduce((added: boolean, d) => { + let moved = move(d, this.props.Document, this.props.addDocument); + return moved || added; + }, false); + } else { + added = de.data.droppedDocuments.reduce((added: boolean, d) => { + let moved = this.props.addDocument(d); + return moved || added; + }, false); + } + e.stopPropagation(); + return added; } - e.stopPropagation(); - return added; + return false; } - return false; - } - protected async getDocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Document>> { - let ctor: ((path: string, options: DocumentOptions) => (Document | Promise<Document | undefined>)) | undefined = undefined; - if (type.indexOf("image") !== -1) { - ctor = Documents.ImageDocument; - } - if (type.indexOf("video") !== -1) { - ctor = Documents.VideoDocument; - } - if (type.indexOf("audio") !== -1) { - ctor = Documents.AudioDocument; - } - if (type.indexOf("pdf") !== -1) { - ctor = Documents.PdfDocument; - options.nativeWidth = 1200; - } - if (type.indexOf("excel") !== -1) { - ctor = Documents.DBDocument; - options.copyDraggedItems = true; - } - if (type.indexOf("html") !== -1) { - if (path.includes('localhost')) { - let s = path.split('/'); - let id = s[s.length - 1]; - Server.GetField(id).then(field => { - if (field instanceof Document) { - let alias = field.CreateAlias(); - alias.SetNumber(KeyStore.X, options.x || 0); - alias.SetNumber(KeyStore.Y, options.y || 0); - alias.SetNumber(KeyStore.Width, options.width || 300); - alias.SetNumber(KeyStore.Height, options.height || options.width || 300); - this.props.addDocument(alias, false); - } - }); - return undefined; + protected async getDocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> { + let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined; + if (type.indexOf("image") !== -1) { + ctor = Docs.ImageDocument; + } + if (type.indexOf("video") !== -1) { + ctor = Docs.VideoDocument; + } + if (type.indexOf("audio") !== -1) { + ctor = Docs.AudioDocument; + } + if (type.indexOf("pdf") !== -1) { + ctor = Docs.PdfDocument; + options.nativeWidth = 1200; } - ctor = Documents.WebDocument; - options = { height: options.width, ...options, title: path }; + if (type.indexOf("excel") !== -1) { + ctor = Docs.DBDocument; + options.dropAction = "copy"; + } + if (type.indexOf("html") !== -1) { + if (path.includes('localhost')) { + let s = path.split('/'); + let id = s[s.length - 1]; + DocServer.GetRefField(id).then(field => { + if (field instanceof Doc) { + let alias = Doc.MakeAlias(field); + alias.x = options.x || 0; + alias.y = options.y || 0; + alias.width = options.width || 300; + alias.height = options.height || options.width || 300; + this.props.addDocument(alias, false); + } + }); + return undefined; + } + ctor = Docs.WebDocument; + options = { height: options.width, ...options, title: path, nativeWidth: undefined }; + } + return ctor ? ctor(path, options) : undefined; } - return ctor ? ctor(path, options) : undefined; - } - @undoBatch - @action - protected onDrop(e: React.DragEvent, options: DocumentOptions): void { - let html = e.dataTransfer.getData("text/html"); - let text = e.dataTransfer.getData("text/plain"); + @undoBatch + @action + protected onDrop(e: React.DragEvent, options: DocumentOptions): void { + let html = e.dataTransfer.getData("text/html"); + let text = e.dataTransfer.getData("text/plain"); - if (text && text.startsWith("<div")) { - return; - } - e.stopPropagation(); - e.preventDefault(); - - if (html && html.indexOf("<img") !== 0 && !html.startsWith("<a")) { - console.log("not good"); - let htmlDoc = Documents.HtmlDocument(html, { ...options, width: 300, height: 300 }); - htmlDoc.SetText(KeyStore.DocumentText, text); - this.props.addDocument(htmlDoc, false); - return; - } + if (text && text.startsWith("<div")) { + return; + } + e.stopPropagation(); + e.preventDefault(); - let batch = UndoManager.StartBatch("collection view drop"); - let promises: Promise<void>[] = []; - // tslint:disable-next-line:prefer-for-of - for (let i = 0; i < e.dataTransfer.items.length; i++) { - const upload = window.location.origin + RouteStore.upload; - let item = e.dataTransfer.items[i]; - if (item.kind === "string" && item.type.indexOf("uri") !== -1) { - let str: string; - let prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve)) - .then(action((s: string) => rp.head(ServerUtils.prepend(RouteStore.corsProxy + "/" + (str = s))))) - .then(result => { - let type = result.headers["content-type"]; - if (type) { - this.getDocumentFromType(type, str, { ...options, width: 300, nativeWidth: 300 }) - .then(doc => doc && this.props.addDocument(doc, false)); - } - }); - promises.push(prom); + if (html && html.indexOf("<img") !== 0 && !html.startsWith("<a")) { + let htmlDoc = Docs.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text }); + this.props.addDocument(htmlDoc, false); + return; } - let type = item.type; - if (item.kind === "file") { - let file = item.getAsFile(); - let formData = new FormData(); - if (file) { - formData.append('file', file); - } - let dropFileName = file ? file.name : "-empty-"; - - let prom = fetch(upload, { - method: 'POST', - body: formData - }).then(async (res: Response) => { - (await res.json()).map(action((file: any) => { - let path = window.location.origin + file; - let docPromise = this.getDocumentFromType(type, path, { ...options, nativeWidth: 600, width: 300, title: dropFileName }); - - docPromise.then(action((doc?: Document) => { - let docs = this.props.Document.GetT(KeyStore.Data, ListField); - if (docs !== FieldWaiting) { - if (!docs) { - docs = new ListField<Document>(); - this.props.Document.Set(KeyStore.Data, docs); - } - if (doc) { - docs.Data.push(doc); - } + let batch = UndoManager.StartBatch("collection view drop"); + let promises: Promise<void>[] = []; + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < e.dataTransfer.items.length; i++) { + const upload = window.location.origin + RouteStore.upload; + let item = e.dataTransfer.items[i]; + if (item.kind === "string" && item.type.indexOf("uri") !== -1) { + let str: string; + let prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve)) + .then(action((s: string) => rp.head(DocServer.prepend(RouteStore.corsProxy + "/" + (str = s))))) + .then(result => { + let type = result["content-type"]; + if (type) { + this.getDocumentFromType(type, str, { ...options, width: 300, nativeWidth: 300 }) + .then(doc => doc && this.props.addDocument(doc, false)); } + }); + promises.push(prom); + } + let type = item.type; + if (item.kind === "file") { + let file = item.getAsFile(); + let formData = new FormData(); + + if (file) { + formData.append('file', file); + } + let dropFileName = file ? file.name : "-empty-"; + + let prom = fetch(upload, { + method: 'POST', + body: formData + }).then(async (res: Response) => { + (await res.json()).map(action((file: any) => { + let path = window.location.origin + file; + let docPromise = this.getDocumentFromType(type, path, { ...options, nativeWidth: 600, width: 300, title: dropFileName }); + + docPromise.then(doc => doc && this.props.addDocument(doc)); })); - })); - }); - promises.push(prom); + }); + promises.push(prom); + } } - } - if (promises.length) { - Promise.all(promises).finally(() => batch.end()); - } else { - batch.end(); + if (promises.length) { + Promise.all(promises).finally(() => batch.end()); + } else { + batch.end(); + } } } + return CollectionSubView; } + diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 8ecc5b67b..411d67ff7 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -23,37 +23,37 @@ margin: 5px 0; } - .collection-child { - margin-top: 10px; - margin-bottom: 10px; - } .no-indent { padding-left: 0; } .bullet { - width: 1.5em; - display: inline-block; + float:left; + position: relative; + width: 15px; + display: block; color: $intermediate-color; - } - - .coll-title { - font-size: 24px; - margin-bottom: 20px; + margin-top: 3px; + transform: scale(1.3,1.3); } .docContainer { - display: inline-table; + margin-left: 10px; + display: block; + // width:100%;//width: max-content; } - .docContainer:hover { - .delete-button { - display: inline; - // width: auto; + .treeViewItem-openRight { + display:inline; } } + + .editableView-container { + font-weight: bold; + } + .delete-button { color: $intermediate-color; // float: right; @@ -61,4 +61,28 @@ // margin-top: 3px; display: inline; } + .treeViewItem-openRight { + margin-left: 5px; + display:none; + } + .docContainer:hover { + .delete-button { + display: inline; + // width: auto; + } + } + + .coll-title { + width:max-content; + display: block; + font-size: 24px; + } + .collection-child { + margin-top: 10px; + margin-bottom: 10px; + } + .collectionTreeView-keyHeader { + font-style: italic; + font-size: 8pt; + } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 51a02fc25..6fa374464 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,27 +1,33 @@ import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faCaretDown, faCaretRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; +import { faCaretDown, faCaretRight, faTrashAlt, faAngleRight } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, observable, trace } from "mobx"; import { observer } from "mobx-react"; -import { Document } from "../../../fields/Document"; -import { FieldWaiting } from "../../../fields/Field"; -import { KeyStore } from "../../../fields/KeyStore"; -import { ListField } from "../../../fields/ListField"; -import { SetupDrag, DragManager } from "../../util/DragManager"; +import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager"; import { EditableView } from "../EditableView"; -import "./CollectionTreeView.scss"; -import { CollectionView } from "./CollectionView"; -import * as globalCssVariables from "../../views/globalCssVariables.scss"; import { CollectionSubView } from "./CollectionSubView"; +import "./CollectionTreeView.scss"; import React = require("react"); -import { props } from 'bluebird'; +import { Document, listSpec } from '../../../new_fields/Schema'; +import { Cast, StrCast, BoolCast, FieldValue } from '../../../new_fields/Types'; +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 { Utils } from '../../../Utils'; +import { List } from '../../../new_fields/List'; +import { indexOf } from 'typescript-collections/dist/lib/arrays'; export interface TreeViewProps { - document: Document; - deleteDoc: (doc: Document) => void; + document: Doc; + deleteDoc: (doc: Doc) => void; moveDocument: DragManager.MoveFunction; - copyOnDrag: boolean; + dropAction: "alias" | "copy" | undefined; } export enum BulletType { @@ -31,6 +37,7 @@ export enum BulletType { } library.add(faTrashAlt); +library.add(faAngleRight); library.add(faCaretDown); library.add(faCaretRight); @@ -42,13 +49,29 @@ class TreeView extends React.Component<TreeViewProps> { @observable _collapsed: boolean = true; - delete = () => this.props.deleteDoc(this.props.document); + @undoBatch delete = () => this.props.deleteDoc(this.props.document); + + @undoBatch openRight = async () => { + if (this.props.document.dockingConfig) { + Main.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)); + } + + onPointerDown = (e: React.PointerEvent) => { + e.stopPropagation(); + } @action remove = (document: Document) => { - var children = this.props.document.GetT<ListField<Document>>(KeyStore.Data, ListField); - if (children && children !== FieldWaiting) { - children.Data.splice(children.Data.indexOf(document), 1); + let children = Cast(this.props.document.data, listSpec(Doc), []); + if (children) { + children.splice(children.indexOf(document), 1); } } @@ -77,83 +100,132 @@ class TreeView extends React.Component<TreeViewProps> { */ renderTitle() { let reference = React.createRef<HTMLDivElement>(); - let onItemDown = SetupDrag(reference, () => this.props.document, this.props.moveDocument, this.props.copyOnDrag); + let onItemDown = SetupDrag(reference, () => this.props.document, this.props.moveDocument, this.props.dropAction); let editableView = (titleString: string) => (<EditableView display={"inline"} contents={titleString} height={36} - GetValue={() => this.props.document.Title} + GetValue={() => StrCast(this.props.document.title)} SetValue={(value: string) => { - this.props.document.SetText(KeyStore.Title, value); + let target = this.props.document.proto ? this.props.document.proto : this.props.document; + target.title = value; return true; }} />); + let dataDocs = Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc), []); + let openRight = dataDocs && dataDocs.indexOf(this.props.document) !== -1 ? (null) : ( + <div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}> + <FontAwesomeIcon icon="angle-right" size="lg" /> + <FontAwesomeIcon icon="angle-right" size="lg" /> + </div>); return ( - <div className="docContainer" ref={reference} onPointerDown={onItemDown}> - {editableView(this.props.document.Title)} - <div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div> + <div className="docContainer" ref={reference} onPointerDown={onItemDown} + style={{ background: BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0" }} + onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> + {editableView(StrCast(this.props.document.title))} + {openRight} + {/* {<div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div>} */} </div >); } + 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 Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.document) }); + if (DocumentManager.Instance.getDocumentViews(this.props.document).length) { + ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document)) }); + } + ContextMenu.Instance.addItem({ description: "Delete", event: undoBatch(() => this.props.deleteDoc(this.props.document)) }); + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); + e.stopPropagation(); + } + } + + onPointerEnter = (e: React.PointerEvent): void => { this.props.document.libraryBrush = true; } + onPointerLeave = (e: React.PointerEvent): void => { this.props.document.libraryBrush = false; } + render() { let bulletType = BulletType.List; - let childElements: JSX.Element | undefined = undefined; - var children = this.props.document.GetT<ListField<Document>>(KeyStore.Data, ListField); - if (children && children !== FieldWaiting) { // add children for a collection - if (!this._collapsed) { - bulletType = BulletType.Collapsible; - childElements = <ul> - {children.Data.map(value => <TreeView key={value.Id} document={value} deleteDoc={this.remove} moveDocument={this.move} copyOnDrag={this.props.copyOnDrag} />)} - </ul >; - } - else bulletType = BulletType.Collapsed; + let contentElement: (JSX.Element | null)[] = []; + let keys = Array.from(Object.keys(this.props.document)); + if (this.props.document.proto instanceof Doc) { + keys.push(...Array.from(Object.keys(this.props.document.proto))); } - return <div className="treeViewItem-container" > + keys.map(key => { + let docList = Cast(this.props.document[key], listSpec(Doc)); + if (docList instanceof List && docList.length && docList[0] instanceof Doc) { + if (!this._collapsed) { + bulletType = BulletType.Collapsible; + contentElement.push(<ul key={key + "more"}> + {(key === "data") ? (null) : + <span className="collectionTreeView-keyHeader" key={key}>{key}</span>} + {TreeView.GetChildElements(docList, key !== "data", this.remove, this.move, this.props.dropAction)} + </ul >); + } else + bulletType = BulletType.Collapsed; + } + }); + return <div className="treeViewItem-container" + onContextMenu={this.onWorkspaceContextMenu}> <li className="collection-child"> {this.renderBullet(bulletType)} {this.renderTitle()} - {childElements ? childElements : (null)} + {contentElement} </li> </div>; } + public static GetChildElements(docs: Doc[], allowMinimized: boolean, remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType) { + return docs.filter(child => !child.excludeFromLibrary && (allowMinimized || !child.isMinimized)).filter(doc => FieldValue(doc)).map(child => + <TreeView document={child} key={child[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} />); + } } @observer -export class CollectionTreeView extends CollectionSubView { - +export class CollectionTreeView extends CollectionSubView(Document) { @action remove = (document: Document) => { - var children = this.props.Document.GetT<ListField<Document>>(KeyStore.Data, ListField); - if (children && children !== FieldWaiting) { - children.Data.splice(children.Data.indexOf(document), 1); + let children = Cast(this.props.Document.data, listSpec(Doc), []); + if (children) { + children.splice(children.indexOf(document), 1); + } + } + 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()) }); + } + if (!ContextMenu.Instance.getItems().some(item => item.description === "Delete")) { + ContextMenu.Instance.addItem({ description: "Delete", event: undoBatch(() => this.remove(this.props.Document)) }); } } - render() { - let children = this.props.Document.GetT<ListField<Document>>(KeyStore.Data, ListField); - let copyOnDrag = this.props.Document.GetBoolean(KeyStore.CopyDraggedItems, false); - let childrenElement = !children || children === FieldWaiting ? (null) : - (children.Data.map(value => - <TreeView document={value} key={value.Id} deleteDoc={this.remove} moveDocument={this.props.moveDocument} copyOnDrag={copyOnDrag} />) - ); + const children = this.children; + let dropAction = StrCast(this.props.Document.dropAction, "alias") as dropActionType; + if (!children) { + return (null); + } + let childElements = TreeView.GetChildElements(children, false, this.remove, this.props.moveDocument, dropAction); return ( - <div id="body" className="collectionTreeView-dropTarget" onWheel={(e: React.WheelEvent) => e.stopPropagation()} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}> + <div id="body" className="collectionTreeView-dropTarget" + style={{ borderRadius: "inherit" }} + onContextMenu={this.onContextMenu} + onWheel={(e: React.WheelEvent) => e.stopPropagation()} + onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}> <div className="coll-title"> <EditableView - contents={this.props.Document.Title} + contents={this.props.Document.title} display={"inline"} height={72} - GetValue={() => this.props.Document.Title} + GetValue={() => StrCast(this.props.Document.title)} SetValue={(value: string) => { - this.props.Document.SetText(KeyStore.Title, value); + let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document; + target.title = value; return true; }} /> </div> - <hr /> <ul className="no-indent"> - {childrenElement} + {childElements} </ul> </div > ); diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx index 29fb342c6..9dee217cb 100644 --- a/src/client/views/collections/CollectionVideoView.tsx +++ b/src/client/views/collections/CollectionVideoView.tsx @@ -1,6 +1,5 @@ import { action, observable, trace } from "mobx"; import { observer } from "mobx-react"; -import { KeyStore } from "../../../fields/KeyStore"; import { ContextMenu } from "../ContextMenu"; import { CollectionViewType, CollectionBaseView, CollectionRenderProps } from "./CollectionBaseView"; import React = require("react"); @@ -8,17 +7,18 @@ import "./CollectionVideoView.scss"; import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; import { emptyFunction } from "../../../Utils"; +import { Id } from "../../../new_fields/RefField"; +import { VideoBox } from "../nodes/VideoBox"; @observer export class CollectionVideoView extends React.Component<FieldViewProps> { - private _intervalTimer: any = undefined; - private _player: HTMLVideoElement | undefined = undefined; + private _videoBox: VideoBox | undefined = undefined; + @observable _playTimer?: NodeJS.Timeout = undefined; @observable _currentTimecode: number = 0; - @observable _isPlaying: boolean = false; - public static LayoutString(fieldKey: string = "DataKey") { + public static LayoutString(fieldKey: string = "data") { return FieldView.LayoutString(CollectionVideoView, fieldKey); } private get uIButtons() { @@ -29,7 +29,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { <span style={{ fontSize: 8 }}>{" " + Math.round((this._currentTimecode - Math.trunc(this._currentTimecode)) * 100)}</span> </div>, <div className="collectionVideoView-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling}, ${scaling})` }}> - {this._isPlaying ? "\"" : ">"} + {this._playTimer ? "\"" : ">"} </div>, <div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling}, ${scaling})` }}> F @@ -38,53 +38,36 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { } @action - mainCont = (ele: HTMLDivElement | null) => { - if (ele) { - this._player = ele.getElementsByTagName("video")[0]; - if (this.props.Document.GetNumber(KeyStore.CurPage, -1) >= 0) { - this._currentTimecode = this.props.Document.GetNumber(KeyStore.CurPage, -1); - } + updateTimecode = () => { + if (this._videoBox && this._videoBox.player) { + this._currentTimecode = this._videoBox.player.currentTime; + this.props.Document.curPage = Math.round(this._currentTimecode); } } - componentDidMount() { - this._intervalTimer = setInterval(this.updateTimecode, 1000); - } + componentDidMount() { this.updateTimecode(); } - componentWillUnmount() { - clearInterval(this._intervalTimer); - } - - @action - updateTimecode = () => { - if (this._player) { - if ((this._player as any).AHackBecauseSomethingResetsTheVideoToZero !== -1) { - this._player.currentTime = (this._player as any).AHackBecauseSomethingResetsTheVideoToZero; - (this._player as any).AHackBecauseSomethingResetsTheVideoToZero = -1; - } else { - this._currentTimecode = this._player.currentTime; - this.props.Document.SetNumber(KeyStore.CurPage, Math.round(this._currentTimecode)); - } - } - } + componentWillUnmount() { if (this._playTimer) clearInterval(this._playTimer); } @action onPlayDown = () => { - if (this._player) { - if (this._player.paused) { - this._player.play(); - this._isPlaying = true; + if (this._videoBox && this._videoBox.player) { + if (this._videoBox.player.paused) { + this._videoBox.player.play(); + if (!this._playTimer) this._playTimer = setInterval(this.updateTimecode, 1000); } else { - this._player.pause(); - this._isPlaying = false; + this._videoBox.player.pause(); + if (this._playTimer) clearInterval(this._playTimer); + this._playTimer = undefined; + } } } @action onFullDown = (e: React.PointerEvent) => { - if (this._player) { - this._player.requestFullscreen(); + if (this._videoBox && this._videoBox.player) { + this._videoBox.player.requestFullscreen(); e.stopPropagation(); e.preventDefault(); } @@ -92,33 +75,34 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { @action onResetDown = () => { - if (this._player) { - this._player.pause(); - this._player.currentTime = 0; + 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(); } - } 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 + 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; } + private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => { let props = { ...this.props, ...renderProps }; - return ( - <> - <CollectionFreeFormView {...props} CollectionView={this} /> - {this.props.isSelected() ? this.uIButtons : (null)} - </> - ); + return (<> + <CollectionFreeFormView {...props} setVideoBox={this.setVideoBox} CollectionView={this} /> + {this.props.isSelected() ? this.uIButtons : (null)} + </>); } render() { - trace(); return ( - <CollectionBaseView {...this.props} className="collectionVideoView-cont" contentRef={this.mainCont} onContextMenu={this.onContextMenu}> + <CollectionBaseView {...this.props} className="collectionVideoView-cont" onContextMenu={this.onContextMenu}> {this.subView} </CollectionBaseView>); } diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 675e720e2..8c1442d38 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -7,14 +7,15 @@ import { CollectionDockingView } from './CollectionDockingView'; import { CollectionTreeView } from './CollectionTreeView'; import { ContextMenu } from '../ContextMenu'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; -import { KeyStore } from '../../../fields/KeyStore'; 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> { - public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(CollectionView, fieldStr); } + public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(CollectionView, fieldStr); } private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => { let props = { ...this.props, ...renderProps }; @@ -29,13 +30,13 @@ export class CollectionView extends React.Component<FieldViewProps> { return (null); } - get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's? + get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } // bcz: ? Why do we need to compare Id's? 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.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform)) }); - ContextMenu.Instance.addItem({ description: "Schema", event: undoBatch(() => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Schema)) }); - ContextMenu.Instance.addItem({ description: "Treeview", event: undoBatch(() => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Tree)) }); + 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) }); } } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss index 3b2f79be1..3e8a8a442 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss @@ -3,4 +3,10 @@ stroke-width: 3; transform: translate(10000px,10000px); pointer-events: all; +} +.collectionfreeformlinkview-linkCircle { + stroke: black; + stroke-width: 3; + transform: translate(10000px,10000px); + pointer-events: all; }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 8868f7df0..3b700b053 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -1,37 +1,58 @@ import { observer } from "mobx-react"; -import { Document } from "../../../../fields/Document"; -import { KeyStore } from "../../../../fields/KeyStore"; import { Utils } from "../../../../Utils"; import "./CollectionFreeFormLinkView.scss"; import React = require("react"); import v5 = require("uuid/v5"); +import { StrCast, NumCast, BoolCast } from "../../../../new_fields/Types"; +import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc"; +import { InkingControl } from "../../InkingControl"; export interface CollectionFreeFormLinkViewProps { - A: Document; - B: Document; - LinkDocs: Document[]; + A: Doc; + B: Doc; + LinkDocs: Doc[]; + addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; + removeDocument: (document: Doc) => boolean; } @observer export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> { onPointerDown = (e: React.PointerEvent) => { - this.props.LinkDocs.map(l => - console.log("Link:" + l.Title)); + if (e.button === 0 && !InkingControl.Instance.selectedTool) { + let a = this.props.A; + let b = this.props.B; + let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : a[WidthSym]() / 2); + let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : a[HeightSym]() / 2); + let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : b[WidthSym]() / 2); + let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : b[HeightSym]() / 2); + this.props.LinkDocs.map(l => { + let width = l[WidthSym](); + l.x = (x1 + x2) / 2 - width / 2; + l.y = (y1 + y2) / 2 + 10; + if (!this.props.removeDocument(l)) this.props.addDocument(l, false); + }); + e.stopPropagation(); + e.preventDefault(); + } } render() { let l = this.props.LinkDocs; let a = this.props.A; let b = this.props.B; - let x1 = a.GetNumber(KeyStore.X, 0) + (a.GetBoolean(KeyStore.Minimized, false) ? 5 : a.Width() / 2); - let y1 = a.GetNumber(KeyStore.Y, 0) + (a.GetBoolean(KeyStore.Minimized, false) ? 5 : a.Height() / 2); - let x2 = b.GetNumber(KeyStore.X, 0) + (b.GetBoolean(KeyStore.Minimized, false) ? 5 : b.Width() / 2); - let y2 = b.GetNumber(KeyStore.Y, 0) + (b.GetBoolean(KeyStore.Minimized, false) ? 5 : b.Height() / 2); + let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / 2); + let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / 2); + let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / 2); + let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / 2); return ( - <line key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine" onPointerDown={this.onPointerDown} - style={{ strokeWidth: `${l.length * 5}` }} - x1={`${x1}`} y1={`${y1}`} - x2={`${x2}`} y2={`${y2}`} /> + <> + <line key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine" + style={{ strokeWidth: `${l.length * 5}` }} + x1={`${x1}`} y1={`${y1}`} + x2={`${x2}`} y2={`${y2}`} /> + <circle key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine" + cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={10} onPointerDown={this.onPointerDown} /> + </> ); } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index cd74d3a84..2d815a302 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -1,8 +1,5 @@ import { computed, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; -import { Document } from "../../../../fields/Document"; -import { KeyStore } from "../../../../fields/KeyStore"; -import { ListField } from "../../../../fields/ListField"; import { Utils } from "../../../../Utils"; import { DocumentManager } from "../../../util/DocumentManager"; import { DocumentView } from "../../nodes/DocumentView"; @@ -10,57 +7,68 @@ import { CollectionViewProps } from "../CollectionSubView"; import "./CollectionFreeFormLinksView.scss"; import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView"; import React = require("react"); +import { Doc, 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"; @observer export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> { _brushReactionDisposer?: IReactionDisposer; componentDidMount() { - this._brushReactionDisposer = reaction(() => this.props.Document.GetList(this.props.fieldKey, [] as Document[]).map(doc => doc.GetNumber(KeyStore.X, 0)), + this._brushReactionDisposer = reaction( () => { - let views = this.props.Document.GetList(this.props.fieldKey, [] as Document[]); - for (let i = 0; i < views.length; i++) { - for (let j = 0; j < views.length; j++) { - let srcDoc = views[j]; - let dstDoc = views[i]; - let x1 = srcDoc.GetNumber(KeyStore.X, 0); - let x1w = srcDoc.GetNumber(KeyStore.Width, -1); - let x2 = dstDoc.GetNumber(KeyStore.X, 0); - let x2w = dstDoc.GetNumber(KeyStore.Width, -1); - if (x1w < 0 || x2w < 0 || i === j) { - continue; - } + 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) : [] }; + }, + async () => { + let doclist = await DocListCast(this.props.Document[this.props.fieldKey]); + let views = doclist ? doclist.filter(doc => StrCast(doc.backgroundLayout).indexOf("istogram") !== -1) : []; + views.forEach((dstDoc, i) => { + views.forEach((srcDoc, j) => { let dstTarg = dstDoc; let srcTarg = srcDoc; - let findBrush = (field: ListField<Document>) => field.Data.findIndex(brush => { - let bdocs = brush ? brush.GetList(KeyStore.BrushingDocs, [] as Document[]) : []; - return (bdocs.length && ((bdocs[0] === dstTarg && bdocs[1] === srcTarg)) ? true : false); - }); - let brushAction = (field: ListField<Document>) => { - let found = findBrush(field); - if (found !== -1) { - console.log("REMOVE BRUSH " + srcTarg.Title + " " + dstTarg.Title); - field.Data.splice(found, 1); - } - }; - if (Math.abs(x1 + x1w - x2) < 20) { - let linkDoc: Document = new Document(); - linkDoc.SetText(KeyStore.Title, "Histogram Brush"); - linkDoc.SetText(KeyStore.LinkDescription, "Brush between " + srcTarg.Title + " and " + dstTarg.Title); - linkDoc.SetData(KeyStore.BrushingDocs, [dstTarg, srcTarg], ListField); - - brushAction = (field: ListField<Document>) => { - if (findBrush(field) === -1) { - console.log("ADD BRUSH " + srcTarg.Title + " " + dstTarg.Title); - (findBrush(field) === -1) && field.Data.push(linkDoc); + let x1 = NumCast(srcDoc.x); + let x2 = NumCast(dstDoc.x); + let x1w = NumCast(srcDoc.width, -1); + let x2w = NumCast(dstDoc.width, -1); + if (x1w < 0 || x2w < 0 || i === j) { } + else { + let findBrush = (field: (Doc | Promise<Doc>)[]) => field.findIndex(brush => { + let bdocs = brush instanceof Doc ? Cast(brush.brushingDocs, listSpec(Doc), []) : undefined; + return bdocs && bdocs.length && ((bdocs[0] === dstTarg && bdocs[1] === srcTarg)) ? true : false; + }); + let brushAction = (field: (Doc | Promise<Doc>)[]) => { + let found = findBrush(field); + if (found !== -1) { + console.log("REMOVE BRUSH " + srcTarg.title + " " + dstTarg.title); + field.splice(found, 1); } }; - } - dstTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, brushAction); - srcTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, brushAction); + if (Math.abs(x1 + x1w - x2) < 20) { + let linkDoc: Doc = new Doc(); + linkDoc.title = "Histogram Brush"; + linkDoc.linkDescription = "Brush between " + StrCast(srcTarg.title) + " and " + StrCast(dstTarg.Title); + linkDoc.brushingDocs = new List([dstTarg, srcTarg]); - } - } + brushAction = (field: (Doc | Promise<Doc>)[]) => { + if (findBrush(field) === -1) { + console.log("ADD BRUSH " + srcTarg.title + " " + dstTarg.title); + field.push(linkDoc); + } + }; + } + let dstBrushDocs = Cast(dstTarg.brushingDocs, listSpec(Doc), []); + let srcBrushDocs = Cast(srcTarg.brushingDocs, listSpec(Doc), []); + if (dstBrushDocs === undefined) dstTarg.brushingDocs = dstBrushDocs = new List<Doc>(); + else brushAction(dstBrushDocs); + if (srcBrushDocs === undefined) srcTarg.brushingDocs = srcBrushDocs = new List<Doc>(); + else brushAction(srcBrushDocs); + } + }) + }) }); } componentWillUnmount() { @@ -70,9 +78,17 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP } documentAnchors(view: DocumentView) { let equalViews = [view]; - let containerDoc = view.props.Document.GetT(KeyStore.AnnotationOn, Document); - if (containerDoc && containerDoc instanceof Document) { - equalViews = DocumentManager.Instance.getDocumentViews(containerDoc.GetPrototype()!); + let containerDoc = FieldValue(Cast(view.props.Document.annotationOn, Doc)); + if (containerDoc) { + equalViews = DocumentManager.Instance.getDocumentViews(containerDoc.proto!); + } + if (view.props.ContainingCollectionView) { + let collid = view.props.ContainingCollectionView.props.Document[Id]; + Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []). + filter(child => + child[Id] === collid).map(view => + DocumentManager.Instance.getDocumentViews(view).map(view => + equalViews.push(view))); } return equalViews.filter(sv => sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === this.props.Document); } @@ -82,12 +98,12 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => { let srcViews = this.documentAnchors(connection.a); let targetViews = this.documentAnchors(connection.b); - let possiblePairs: { a: Document, b: Document, }[] = []; + let possiblePairs: { a: Doc, b: Doc, }[] = []; srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document }))); possiblePairs.map(possiblePair => drawnPairs.reduce((found, drawnPair) => { let match = (possiblePair.a === drawnPair.a && possiblePair.b === drawnPair.b); - if (match && !drawnPair.l.reduce((found, link) => found || link.Id === connection.l.Id, false)) { + if (match && !drawnPair.l.reduce((found, link) => found || link[Id] === connection.l[Id], false)) { drawnPair.l.push(connection.l); } return match || found; @@ -96,8 +112,9 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] }) ); return drawnPairs; - }, [] as { a: Document, b: Document, l: Document[] }[]); - return connections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />); + }, [] as { a: Doc, b: Doc, l: Doc[] }[]); + return connections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} + removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />); } render() { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx index cf0a6de00..036745eca 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx @@ -1,6 +1,5 @@ import { computed } from "mobx"; import { observer } from "mobx-react"; -import { KeyStore } from "../../../../fields/KeyStore"; import { CollectionViewProps, CursorEntry } from "../CollectionSubView"; import "./CollectionFreeFormView.scss"; import React = require("react"); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 392bd514f..cb849b325 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -1,97 +1,103 @@ @import "../../globalCssVariables"; -.collectionfreeformview-measure { - position: inherit; + +.collectionfreeformview-ease { + position: absolute; top: 0; left: 0; width: 100%; height: 100%; - } -.collectionfreeformview { - position: inherit; - top: 0; - left: 0; - width: 100%; - height: 100%; - transform-origin: left top; + transform-origin: left top; + transition: transform 1s; } -.collectionfreeformview-container { - .collectionfreeformview > .jsx-parser { + +.collectionfreeformview-none { position: inherit; - height: 100%; + top: 0; + left: 0; width: 100%; - } + height: 100%; + transform-origin: left top; +} - //nested freeform views - // .collectionfreeformview-container { +.collectionfreeformview-container { + .collectionfreeformview>.jsx-parser { + position: inherit; + height: 100%; + width: 100%; + } + + //nested freeform views + // .collectionfreeformview-container { // background-image: linear-gradient(to right, $light-color-secondary 1px, transparent 1px), // linear-gradient(to bottom, $light-color-secondary 1px, transparent 1px); // background-size: 30px 30px; - // } - - border-width: $COLLECTION_BORDER_WIDTH; - box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw; - border-color: $light-color-secondary; - border-style: solid; - border-radius: $border-radius; - box-sizing: border-box; - position: absolute; - overflow: hidden; - top: 0; - left: 0; - width: 100%; - height: 100%; + // } + box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw; + border: 0px solid $light-color-secondary; + border-radius: $border-radius; + box-sizing: border-box; + position: absolute; + overflow: hidden; + top: 0; + left: 0; + width: 100%; + height: 100%; } + + .collectionfreeformview-overlay { - .collectionfreeformview > .jsx-parser { - position: inherit; - height: 100%; - } - .formattedTextBox-cont { - background: $light-color-secondary; - overflow: visible; - } - - opacity: 0.99; - border-width: 0; - border-color: transparent; - border-style: solid; - border-radius: $border-radius; - box-sizing: border-box; - position: absolute; - overflow: hidden; - top: 0; - left: 0; - width: 100%; - height: 100%; - .collectionfreeformview { + .collectionfreeformview>.jsx-parser { + position: inherit; + height: 100%; + } + .formattedTextBox-cont { - background:yellow; + background: $light-color-secondary; + overflow: visible; + } + + opacity: 0.99; + border: 0px solid transparent; + border-radius: $border-radius; + box-sizing: border-box; + position:absolute; + overflow: hidden; + top: 0; + left: 0; + width: 100%; + height: 100%; + + .collectionfreeformview { + .formattedTextBox-cont { + background: yellow; + } } - } } // selection border...? .border { - border-style: solid; - box-sizing: border-box; - width: 98%; - height: 98%; - border-radius: $border-radius; + border-style: solid; + box-sizing: border-box; + width: 98%; + height: 98%; + border-radius: $border-radius; } //this is an animation for the blinking cursor! @keyframes blink { - 0% { - opacity: 0; - } - 49% { - opacity: 0; - } - 50% { - opacity: 1; - } + 0% { + opacity: 0; + } + + 49% { + opacity: 0; + } + + 50% { + opacity: 1; + } } #prevCursor { - animation: blink 1s infinite; -} + animation: blink 1s infinite; +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 97708ce19..7fa945891 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,8 +1,5 @@ -import { action, computed, observable, trace } from "mobx"; +import { action, computed, trace } from "mobx"; import { observer } from "mobx-react"; -import Measure from "react-measure"; -import { Document } from "../../../../fields/Document"; -import { KeyStore } from "../../../../fields/KeyStore"; import { emptyFunction, returnFalse, returnOne } from "../../../../Utils"; import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager } from "../../../util/DragManager"; @@ -11,10 +8,9 @@ import { Transform } from "../../../util/Transform"; import { undoBatch } from "../../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"; import { InkingCanvas } from "../../InkingCanvas"; -import { MainOverlayTextBox } from "../../MainOverlayTextBox"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; import { DocumentContentsView } from "../../nodes/DocumentContentsView"; -import { DocumentViewProps } from "../../nodes/DocumentView"; +import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView"; import { CollectionSubView } from "../CollectionSubView"; import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView"; import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; @@ -22,70 +18,86 @@ import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); import v5 = require("uuid/v5"); +import { createSchema, makeInterface, listSpec } from "../../../../new_fields/Schema"; +import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc"; +import { FieldValue, Cast, NumCast } from "../../../../new_fields/Types"; +import { pageSchema } from "../../nodes/ImageBox"; +import { Id } from "../../../../new_fields/RefField"; + +export const panZoomSchema = createSchema({ + panX: "number", + panY: "number", + scale: "number" +}); + +type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof positionSchema, typeof pageSchema]>; +const PanZoomDocument = makeInterface(panZoomSchema, positionSchema, pageSchema); @observer -export class CollectionFreeFormView extends CollectionSubView { +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; - @observable private _pwidth: number = 0; - @observable private _pheight: number = 0; + private get _pwidth() { return this.props.PanelWidth(); } + private get _pheight() { return this.props.PanelHeight(); } - @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } - @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } + @computed get nativeWidth() { return FieldValue(this.Document.nativeWidth, 0); } + @computed get nativeHeight() { return FieldValue(this.Document.nativeHeight, 0); } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } - private get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's? - private childViews = () => this.views; - private panX = () => this.props.Document.GetNumber(KeyStore.PanX, 0); - private panY = () => this.props.Document.GetNumber(KeyStore.PanY, 0); - private zoomScaling = () => this.props.Document.GetNumber(KeyStore.Scale, 1); + 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 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 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: Document) => { - this._selectOnLoaded = newBox.Id;// track the new text box so we can give it a prop that tells it to focus itself when it's displayed + private addLiveTextBox = (newBox: Doc) => { + this._selectOnLoaded = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed this.addDocument(newBox, false); } - private addDocument = (newBox: Document, allowDuplicates: boolean) => { - if (this.isAnnotationOverlay) { - newBox.SetNumber(KeyStore.Zoom, this.props.Document.GetNumber(KeyStore.Scale, 1)); - } - return this.props.addDocument(this.bringToFront(newBox), false); + private addDocument = (newBox: Doc, allowDuplicates: boolean) => { + this.props.addDocument(newBox, false); + this.bringToFront(newBox); + return true; } - private selectDocuments = (docs: Document[]) => { + private selectDocuments = (docs: Doc[]) => { SelectionManager.DeselectAll; docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).filter(dv => dv).map(dv => SelectionManager.SelectDoc(dv!, true)); } public getActiveDocuments = () => { - var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1); - return this.props.Document.GetList(this.props.fieldKey, [] as Document[]).reduce((active, doc) => { - var page = doc.GetNumber(KeyStore.Page, -1); - if (page === curPage || page === -1) { - active.push(doc); - } - return active; - }, [] as Document[]); + const curPage = FieldValue(this.Document.curPage, -1); + return FieldValue(this.children, [] as Doc[]).filter(doc => { + var page = NumCast(doc.page, -1); + return page === curPage || page === -1; + }); } @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { if (super.drop(e, de) && de.data instanceof DragManager.DocumentDragData) { - const [x, y] = this.getTransform().transformPoint(de.x - de.data.xOffset, de.y - de.data.yOffset); if (de.data.droppedDocuments.length) { - let dropX = de.data.droppedDocuments[0].GetNumber(KeyStore.X, 0); - let dropY = de.data.droppedDocuments[0].GetNumber(KeyStore.Y, 0); + let dragDoc = de.data.droppedDocuments[0]; + let zoom = NumCast(dragDoc.zoomBasis, 1); + let [xp, yp] = this.getTransform().transformPoint(de.x, de.y); + let x = xp - de.data.xOffset / zoom; + let y = yp - de.data.yOffset / zoom; + let dropX = NumCast(de.data.droppedDocuments[0].x); + let dropY = NumCast(de.data.droppedDocuments[0].y); de.data.droppedDocuments.map(d => { - d.SetNumber(KeyStore.X, x + (d.GetNumber(KeyStore.X, 0) - dropX)); - d.SetNumber(KeyStore.Y, y + (d.GetNumber(KeyStore.Y, 0) - dropY)); - if (!d.GetNumber(KeyStore.Width, 0)) { - d.SetNumber(KeyStore.Width, 300); + d.x = x + NumCast(d.x) - dropX; + d.y = y + NumCast(d.y) - dropY; + if (!NumCast(d.width)) { + d.width = 300; } - if (!d.GetNumber(KeyStore.Height, 0)) { - d.SetNumber(KeyStore.Height, 300); + if (!NumCast(d.height)) { + let nw = NumCast(d.nativeWidth); + let nh = NumCast(d.nativeHeight); + d.height = nw && nh ? nh / nw * NumCast(d.width) : 300; } this.bringToFront(d); }); @@ -97,51 +109,47 @@ export class CollectionFreeFormView extends CollectionSubView { } @action - cleanupInteractions = () => { - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - } - - @action onPointerDown = (e: React.PointerEvent): void => { - let childSelected = this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc).reduce((childSelected, doc) => { + let childSelected = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), [] as Doc[]).filter(doc => doc).reduce((childSelected, doc) => { var dv = DocumentManager.Instance.getDocumentView(doc); return childSelected || (dv && SelectionManager.IsSelected(dv) ? true : false); }, false); - if (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) || (e.button === 0 && e.altKey)) && (childSelected || this.props.active())) { + if ((CollectionFreeFormView.RIGHT_BTN_DRAG && + (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) || + (e.button === 0 && e.altKey)) && (childSelected || this.props.active()))) || + (!CollectionFreeFormView.RIGHT_BTN_DRAG && + ((e.button === 0 && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) && (childSelected || this.props.active())))) { document.removeEventListener("pointermove", this.onPointerMove); - document.addEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointermove", this.onPointerMove); document.addEventListener("pointerup", this.onPointerUp); this._lastX = e.pageX; this._lastY = e.pageY; } } - @action onPointerUp = (e: PointerEvent): void => { - e.stopPropagation(); - - this.cleanupInteractions(); + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); } @action onPointerMove = (e: PointerEvent): void => { if (!e.cancelBubble) { - let x = this.props.Document.GetNumber(KeyStore.PanX, 0); - let y = this.props.Document.GetNumber(KeyStore.PanY, 0); - let docs = this.props.Document.GetList(this.props.fieldKey, [] as Document[]); + let x = Cast(this.props.Document.panX, "number", 0); + let y = Cast(this.props.Document.panY, "number", 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 ? docs[0].GetNumber(KeyStore.X, 0) : 0; - let maxx = docs.length ? docs[0].Width() + minx : minx; - let miny = docs.length ? docs[0].GetNumber(KeyStore.Y, 0) : 0; - let maxy = docs.length ? docs[0].Height() + miny : miny; + 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 ranges = docs.filter(doc => doc).reduce((range, doc) => { - let x = doc.GetNumber(KeyStore.X, 0); - let xe = x + doc.GetNumber(KeyStore.Width, 0); - let y = doc.GetNumber(KeyStore.Y, 0); - let ye = y + doc.GetNumber(KeyStore.Height, 0); + 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); return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]], [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]]; }, [[minx, maxx], [miny, maxy]]); @@ -155,7 +163,7 @@ export class CollectionFreeFormView extends CollectionSubView { this.setPan(x - dx, y - dy); this._lastX = e.pageX; this._lastY = e.pageY; - e.stopPropagation(); + e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); } } @@ -165,10 +173,10 @@ export class CollectionFreeFormView extends CollectionSubView { // if (!this.props.active()) { // return; // } - let childSelected = this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc).reduce((childSelected, doc) => { + let childSelected = (this.children || []).filter(doc => doc).some(doc => { var dv = DocumentManager.Instance.getDocumentView(doc); - return childSelected || (dv && SelectionManager.IsSelected(dv) ? true : false); - }, false); + return dv && SelectionManager.IsSelected(dv) ? true : false; + }); if (!this.props.isSelected() && !childSelected && !this.props.isTopMost) { return; } @@ -177,8 +185,8 @@ export class CollectionFreeFormView extends CollectionSubView { if (e.ctrlKey) { let deltaScale = (1 - (e.deltaY / coefficient)); - this.props.Document.SetNumber(KeyStore.NativeWidth, this.nativeWidth * deltaScale); - this.props.Document.SetNumber(KeyStore.NativeHeight, this.nativeHeight * deltaScale); + this.props.Document.nativeWidth = this.nativeWidth * deltaScale; + this.props.Document.nativeHeight = this.nativeHeight * deltaScale; e.stopPropagation(); e.preventDefault(); } else { @@ -188,23 +196,24 @@ export class CollectionFreeFormView extends CollectionSubView { if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) { deltaScale = 1 / this.zoomScaling(); } + if (deltaScale < 0) deltaScale = -deltaScale; let [x, y] = this.getTransform().transformPoint(e.clientX, e.clientY); let localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); - this.props.Document.SetNumber(KeyStore.Scale, localTransform.Scale); - this.setPan(-localTransform.TranslateX / localTransform.Scale, -localTransform.TranslateY / localTransform.Scale); + let safeScale = Math.abs(localTransform.Scale); + this.props.Document.scale = Math.abs(safeScale); + this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale); e.stopPropagation(); } } @action setPan(panX: number, panY: number) { - MainOverlayTextBox.Instance.SetTextDoc(); var scale = this.getLocalTransform().inverse().Scale; const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); const newPanY = Math.min((1 - 1 / scale) * this.nativeHeight, Math.max(0, panY)); - this.props.Document.SetNumber(KeyStore.PanX, this.isAnnotationOverlay ? newPanX : panX); - this.props.Document.SetNumber(KeyStore.PanY, this.isAnnotationOverlay ? newPanY : panY); + this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX; + this.props.Document.panY = this.isAnnotationOverlay ? newPanY : panY; } @action @@ -216,49 +225,62 @@ export class CollectionFreeFormView extends CollectionSubView { onDragOver = (): void => { } - @action - bringToFront(doc: Document) { - this.props.Document.GetList(this.props.fieldKey, [] as Document[]).slice().sort((doc1, doc2) => { + bringToFront = (doc: Doc) => { + const docs = (this.children || []); + docs.slice().sort((doc1, doc2) => { if (doc1 === doc) return 1; if (doc2 === doc) return -1; - return doc1.GetNumber(KeyStore.ZIndex, 0) - doc2.GetNumber(KeyStore.ZIndex, 0); - }).map((doc, index) => doc.SetNumber(KeyStore.ZIndex, index + 1)); + return NumCast(doc1.zIndex) - NumCast(doc2.zIndex); + }).forEach((doc, index) => doc.zIndex = index + 1); + doc.zIndex = docs.length + 1; return doc; } - focusDocument = (doc: Document) => { + focusDocument = (doc: Doc) => { + SelectionManager.DeselectAll(); + this.props.Document.panTransformType = "Ease"; this.setPan( - doc.GetNumber(KeyStore.X, 0) + doc.Width() / 2, - doc.GetNumber(KeyStore.Y, 0) + doc.Height() / 2); + 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 + } } - getDocumentViewProps(document: Document): DocumentViewProps { + + getDocumentViewProps(document: Doc): DocumentViewProps { return { Document: document, + toggleMinimized: emptyFunction, addDocument: this.props.addDocument, removeDocument: this.props.removeDocument, moveDocument: this.props.moveDocument, ScreenToLocalTransform: this.getTransform, isTopMost: false, - selectOnLoad: document.Id === this._selectOnLoaded, - PanelWidth: document.Width, - PanelHeight: document.Height, + selectOnLoad: document[Id] === this._selectOnLoaded, + PanelWidth: document[WidthSym], + PanelHeight: document[HeightSym], ContentScaling: returnOne, ContainingCollectionView: this.props.CollectionView, focus: this.focusDocument, parentActive: this.props.active, - onActiveChanged: this.props.active, + whenActiveChanged: this.props.whenActiveChanged, + bringToFront: this.bringToFront, }; } - @computed + @computed.struct get views() { - var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1); - let docviews = this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc).reduce((prev, doc) => { - var page = doc.GetNumber(KeyStore.Page, -1); + let curPage = FieldValue(this.Document.curPage, -1); + let docviews = (this.children || []).filter(doc => doc).reduce((prev, doc) => { + if (!(doc instanceof Doc)) return prev; + var page = NumCast(doc.page, -1); if (page === curPage || page === -1) { - prev.push(<CollectionFreeFormDocumentView key={doc.Id} {...this.getDocumentViewProps(doc)} />); + let minim = Cast(doc.isMinimized, "boolean"); + if (minim === undefined || !minim) { + prev.push(<CollectionFreeFormDocumentView key={doc[Id]} {...this.getDocumentViewProps(doc)} />); + } } return prev; }, [] as JSX.Element[]); @@ -269,41 +291,34 @@ export class CollectionFreeFormView extends CollectionSubView { } @action - onResize = (r: any) => { - this._pwidth = r.entry.width; - this._pheight = r.entry.height; - } - @action onCursorMove = (e: React.PointerEvent) => { super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); } + private childViews = () => [...this.views, <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />]; render() { const containerName = `collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`; + const easing = () => this.props.Document.panTransformType === "Ease"; return ( - <Measure onResize={this.onResize}> - {({ measureRef }) => ( - <div className="collectionfreeformview-measure" ref={measureRef}> - <div className={containerName} ref={this.createDropTarget} onWheel={this.onPointerWheel} - onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} > - <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} - addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox} - getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}> - <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} - zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> - <CollectionFreeFormBackgroundView {...this.getDocumentViewProps(this.props.Document)} /> - <CollectionFreeFormLinksView {...this.props} key="freeformLinks"> - <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} > - {this.childViews} - </InkingCanvas> - </CollectionFreeFormLinksView> - <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" /> - </CollectionFreeFormViewPannableContents> - <CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} /> - </MarqueeView> - </div> - </div>)} - </Measure> + <div className={containerName} ref={this.createDropTarget} onWheel={this.onPointerWheel} + style={{ borderRadius: "inherit" }} + onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} > + <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} isSelected={this.props.isSelected} + addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox} + getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}> + <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} + easing={easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> + + <CollectionFreeFormLinksView {...this.props} key="freeformLinks"> + <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} > + {this.childViews} + </InkingCanvas> + </CollectionFreeFormLinksView> + {/* <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" /> */} + </CollectionFreeFormViewPannableContents> + <CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} {...this.props} /> + </MarqueeView> + </div> ); } } @@ -311,9 +326,9 @@ export class CollectionFreeFormView extends CollectionSubView { @observer class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps> { @computed get overlayView() { - let overlayLayout = this.props.Document.GetText(KeyStore.OverlayLayout, ""); + let overlayLayout = Cast(this.props.Document.overlayLayout, "string", ""); return !overlayLayout ? (null) : - (<DocumentContentsView {...this.props} layoutKey={KeyStore.OverlayLayout} + (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"} isTopMost={this.props.isTopMost} isSelected={returnFalse} select={emptyFunction} />); } render() { @@ -322,12 +337,12 @@ class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps> { } @observer -class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps> { +class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> { @computed get backgroundView() { - let backgroundLayout = this.props.Document.GetText(KeyStore.BackgroundLayout, ""); + let backgroundLayout = Cast(this.props.Document.backgroundLayout, "string", ""); return !backgroundLayout ? (null) : - (<DocumentContentsView {...this.props} layoutKey={KeyStore.BackgroundLayout} - isTopMost={this.props.isTopMost} isSelected={returnFalse} select={emptyFunction} />); + (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"} + isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />); } render() { return this.backgroundView; @@ -340,17 +355,19 @@ interface CollectionFreeFormViewPannableContentsProps { panX: () => number; panY: () => number; zoomScaling: () => number; + easing: () => boolean; } @observer class CollectionFreeFormViewPannableContents extends React.Component<CollectionFreeFormViewPannableContentsProps>{ render() { + let freeformclass = "collectionfreeformview" + (this.props.easing() ? "-ease" : "-none"); const cenx = this.props.centeringShiftX(); const ceny = this.props.centeringShiftY(); const panx = -this.props.panX(); const pany = -this.props.panY(); const zoom = this.props.zoomScaling();// needs to be a variable outside of the <Measure> otherwise, reactions won't fire - return <div className="collectionfreeformview" style={{ transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}, ${zoom}) translate(${panx}px, ${pany}px)` }}> + return <div className={freeformclass} style={{ borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}, ${zoom}) translate(${panx}px, ${pany}px)` }}> {this.props.children} </div>; } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 65f461b27..c9b0b28f7 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,28 +1,35 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Document } from "../../../../fields/Document"; -import { FieldWaiting } from "../../../../fields/Field"; -import { InkField, StrokeData } from "../../../../fields/InkField"; -import { KeyStore } from "../../../../fields/KeyStore"; -import { Documents } from "../../../documents/Documents"; +import { Docs } from "../../../documents/Documents"; import { SelectionManager } from "../../../util/SelectionManager"; import { Transform } from "../../../util/Transform"; -import { undoBatch } from "../../../util/UndoManager"; +import { undoBatch, UndoManager } from "../../../util/UndoManager"; import { InkingCanvas } from "../../InkingCanvas"; import { PreviewCursor } from "../../PreviewCursor"; import { CollectionFreeFormView } from "./CollectionFreeFormView"; import "./MarqueeView.scss"; import React = require("react"); +import { Utils } from "../../../../Utils"; +import { Doc } from "../../../../new_fields/Doc"; +import { NumCast, Cast } from "../../../../new_fields/Types"; +import { InkField, StrokeData } from "../../../../new_fields/InkField"; +import { Templates } from "../../Templates"; +import { List } from "../../../../new_fields/List"; +import { emitKeypressEvents } from "readline"; +import { listSpec } from "../../../../new_fields/Schema"; +import { undo } from "prosemirror-history"; +import { FormattedTextBox } from "../../nodes/FormattedTextBox"; interface MarqueeViewProps { getContainerTransform: () => Transform; getTransform: () => Transform; container: CollectionFreeFormView; - addDocument: (doc: Document, allowDuplicates: false) => boolean; - activeDocuments: () => Document[]; - selectDocuments: (docs: Document[]) => void; - removeDocument: (doc: Document) => boolean; - addLiveTextDocument: (doc: Document) => void; + addDocument: (doc: Doc, allowDuplicates: false) => boolean; + activeDocuments: () => Doc[]; + selectDocuments: (docs: Doc[]) => void; + removeDocument: (doc: Doc) => boolean; + addLiveTextDocument: (doc: Doc) => void; + isSelected: () => boolean; } @observer @@ -32,53 +39,62 @@ export class MarqueeView extends React.Component<MarqueeViewProps> @observable _lastY: number = 0; @observable _downX: number = 0; @observable _downY: number = 0; - @observable _used: boolean = false; @observable _visible: boolean = false; - _showOnUp: boolean = false; - static DRAG_THRESHOLD = 4; + _commandExecuted = false; @action cleanupInteractions = (all: boolean = false) => { if (all) { - document.removeEventListener("pointermove", this.onPointerMove, true); document.removeEventListener("pointerup", this.onPointerUp, true); - } else { - this._used = true; + document.removeEventListener("pointermove", this.onPointerMove, true); } document.removeEventListener("keydown", this.marqueeCommand, true); this._visible = false; } + @undoBatch @action onKeyPress = (e: KeyboardEvent) => { - // Mixing events between React and Native is finicky. In FormattedTextBox, we set the - // DASHFormattedTextBoxHandled flag when a text box consumes a key press so that we can ignore - // the keyPress here. - //if not these keys, make a textbox if preview cursor is active! - if (!e.ctrlKey && !e.altKey && !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) { - //make textbox and add it to this collection - let [x, y] = this.props.getTransform().transformPoint(this._downX, this._downY); - let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "typed text" }); + //make textbox and add it to this collection + let [x, y] = this.props.getTransform().transformPoint(this._downX, this._downY); + if (e.key === "q" && e.ctrlKey) { + e.preventDefault(); + (async () => { + let text = await navigator.clipboard.readText(); + let ns = text.split("\n").filter(t => t != "\r"); + for (let i = 0; i < ns.length - 1; i++) { + while (!(ns[i].endsWith("-\r") || ns[i].endsWith(".\r") || ns[i].endsWith(":\r")) && i < ns.length - 1) { + ns.splice(i, 2, ns[i].substr(0, ns[i].length - 1) + ns[i + 1].trimLeft()); + } + } + ns.map(line => { + let indent = line.search(/\S|$/); + let newBox = Docs.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line }); + this.props.addDocument(newBox, false); + y += 40 * this.props.getTransform().Scale; + }) + })(); + } else { + let newBox = Docs.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" }); this.props.addLiveTextDocument(newBox); - PreviewCursor.Visible = false; - e.stopPropagation(); } - } - hideCursor = () => { - document.removeEventListener("keypress", this.onKeyPress, false); + e.stopPropagation(); } @action onPointerDown = (e: React.PointerEvent): void => { - if (e.buttons === 1 && !e.altKey && !e.metaKey && this.props.container.props.active()) { - this._downX = this._lastX = e.pageX; - this._downY = this._lastY = e.pageY; - this._used = false; - this._showOnUp = true; - document.removeEventListener("keypress", this.onKeyPress, false); + this._downX = this._lastX = e.pageX; + 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())) { document.addEventListener("pointermove", this.onPointerMove, true); document.addEventListener("pointerup", this.onPointerUp, true); document.addEventListener("keydown", this.marqueeCommand, true); } + if (e.altKey) { + e.preventDefault(); + } } @action @@ -86,33 +102,45 @@ export class MarqueeView extends React.Component<MarqueeViewProps> this._lastX = e.pageX; this._lastY = e.pageY; if (!e.cancelBubble) { - if (Math.abs(this._downX - e.clientX) > 4 || Math.abs(this._downY - e.clientY) > 4) { - this._showOnUp = false; - PreviewCursor.Visible = false; - } - if (!this._used && e.buttons === 1 && !e.altKey && !e.metaKey && - (Math.abs(this._lastX - this._downX) > MarqueeView.DRAG_THRESHOLD || Math.abs(this._lastY - this._downY) > MarqueeView.DRAG_THRESHOLD)) { - this._visible = true; + if (Math.abs(this._lastX - this._downX) > Utils.DRAG_THRESHOLD || + Math.abs(this._lastY - this._downY) > Utils.DRAG_THRESHOLD) { + if (!this._commandExecuted) { + this._visible = true; + } + e.stopPropagation(); + e.preventDefault(); } - e.stopPropagation(); + } + if (e.altKey) { e.preventDefault(); } } @action onPointerUp = (e: PointerEvent): void => { - this.cleanupInteractions(true); - this._visible = false; - if (this._showOnUp) { - PreviewCursor.Show(this.hideCursor, this._downX, this._downY); - document.addEventListener("keypress", this.onKeyPress, false); - } else { + if (this._visible) { let mselect = this.marqueeSelect(); if (!e.shiftKey) { SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document); } this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]); } + this.cleanupInteractions(true); + if (e.altKey) { + e.preventDefault(); + } + } + + @action + onClick = (e: React.MouseEvent): void => { + if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && + Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { + PreviewCursor.Show(e.clientX, e.clientY, this.onKeyPress); + // let the DocumentView stopPropagation of this event when it selects this document + } else { // why do we get a click event when the cursor have moved a big distance? + // let's cut it off here so no one else has to deal with it. + e.stopPropagation(); + } } intersectRect(r1: { left: number, top: number, width: number, height: number }, @@ -132,41 +160,93 @@ export class MarqueeView extends React.Component<MarqueeViewProps> @undoBatch @action marqueeCommand = (e: KeyboardEvent) => { - if (e.key === "Backspace" || e.key === "Delete") { + if (this._commandExecuted) { + return; + } + if (e.key === "Backspace" || e.key === "Delete" || e.key == "d") { + this._commandExecuted = true; this.marqueeSelect().map(d => this.props.removeDocument(d)); - let ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField); - if (ink && ink !== FieldWaiting) { - this.marqueeInkDelete(ink.Data); + let ink = Cast(this.props.container.props.Document.ink, InkField); + if (ink) { + this.marqueeInkDelete(ink.inkData); } - this.cleanupInteractions(); + this.cleanupInteractions(false); + e.stopPropagation(); } - if (e.key === "c") { + if (e.key === "c" || e.key === "r" || e.key === "R" || e.key === "e") { + this._commandExecuted = true; + e.stopPropagation(); let bounds = this.Bounds; let selected = this.marqueeSelect().map(d => { - this.props.removeDocument(d); - d.SetNumber(KeyStore.X, d.GetNumber(KeyStore.X, 0) - bounds.left - bounds.width / 2); - d.SetNumber(KeyStore.Y, d.GetNumber(KeyStore.Y, 0) - bounds.top - bounds.height / 2); - d.SetNumber(KeyStore.Page, -1); + if (e.key !== "R") { + 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 ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField); - let inkData = ink && ink !== FieldWaiting ? ink.Data : undefined; - //setTimeout(() => { - let newCollection = Documents.FreeformDocument(selected, { + let ink = Cast(this.props.container.props.Document.ink, InkField); + let inkData = ink ? ink.inkData : undefined; + let zoomBasis = NumCast(this.props.container.props.Document.scale, 1); + let newCollection = Docs.FreeformDocument(selected, { x: bounds.left, y: bounds.top, - panx: 0, - pany: 0, - width: bounds.width, - height: bounds.height, - ink: inkData ? this.marqueeInkSelect(inkData) : undefined, + panX: 0, + panY: 0, + borderRounding: e.key === "e" ? -1 : undefined, + scale: zoomBasis, + width: bounds.width * zoomBasis, + height: bounds.height * zoomBasis, + ink: inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined, title: "a nested collection" }); - this.props.addDocument(newCollection, false); + this.marqueeInkDelete(inkData); - // }, 100); - this.cleanupInteractions(); + // SelectionManager.DeselectAll(); + if (e.key === "r" || e.key === "R") { + 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-" }); + + if (e.key === "r") { + summary.proto!.maximizeOnRight = true; + let list = Cast(newCollection.data, listSpec(Doc)); + if (list && list.length === 1) { + selected = list; + } else { + selected = [newCollection]; + this.props.addDocument(newCollection, false); + } + } + summary.proto!.maximizedDocs = new List<Doc>(selected); + summary.proto!.isButton = true; + selected.map(maximizedDoc => { + let maxx = NumCast(maximizedDoc.x, undefined); + let maxy = NumCast(maximizedDoc.y, undefined); + let maxw = NumCast(maximizedDoc.width, undefined); + let maxh = NumCast(maximizedDoc.height, undefined); + maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), 0]) + }); + this.props.addLiveTextDocument(summary); + } + else { + this.props.addDocument(newCollection, false); + } + this.cleanupInteractions(false); + } + 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 @@ -199,19 +279,19 @@ export class MarqueeView extends React.Component<MarqueeViewProps> let idata = new Map(); ink.forEach((value: StrokeData, key: string, map: any) => !InkingCanvas.IntersectStrokeRect(value, this.Bounds) && idata.set(key, value)); - this.props.container.props.Document.SetDataOnPrototype(KeyStore.Ink, idata, InkField); + Doc.SetOnPrototype(this.props.container.props.Document, "ink", new InkField(idata)); } } marqueeSelect() { let selRect = this.Bounds; - let selection: Document[] = []; + let selection: Doc[] = []; this.props.activeDocuments().map(doc => { - var z = doc.GetNumber(KeyStore.Zoom, 1); - var x = doc.GetNumber(KeyStore.X, 0); - var y = doc.GetNumber(KeyStore.Y, 0); - var w = doc.Width() / z; - var h = doc.Height() / z; + var z = NumCast(doc.zoomBasis, 1); + var x = NumCast(doc.x); + var y = NumCast(doc.y); + var w = NumCast(doc.width) / z; + var h = NumCast(doc.height) / z; if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) { selection.push(doc); } @@ -229,7 +309,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> } render() { - return <div className="marqueeView" onPointerDown={this.onPointerDown}> + return <div className="marqueeView" style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}> {this.props.children} {!this._visible ? (null) : this.marqueeDiv} </div>; |
