diff options
Diffstat (limited to 'src/client/views/collections')
28 files changed, 3018 insertions, 1191 deletions
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx new file mode 100644 index 000000000..31bdf213e --- /dev/null +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -0,0 +1,176 @@ +import { action, computed } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { ContextMenu } from '../ContextMenu'; +import { FieldViewProps } from '../nodes/FieldView'; +import { Cast, FieldValue, PromiseValue, NumCast } from '../../../new_fields/Types'; +import { Doc, FieldResult, Opt, DocListCast } from '../../../new_fields/Doc'; +import { listSpec } from '../../../new_fields/Schema'; +import { List } from '../../../new_fields/List'; +import { Id } from '../../../new_fields/RefField'; +import { SelectionManager } from '../../util/SelectionManager'; + +export enum CollectionViewType { + Invalid, + Freeform, + Schema, + Docking, + Tree, +} + +export interface CollectionRenderProps { + addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; + removeDocument: (document: Doc) => boolean; + moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; + active: () => boolean; + whenActiveChanged: (isActive: boolean) => void; +} + +export interface CollectionViewProps extends FieldViewProps { + onContextMenu?: (e: React.MouseEvent) => void; + children: (type: CollectionViewType, props: CollectionRenderProps) => JSX.Element | JSX.Element[] | null; + className?: string; + contentRef?: React.Ref<HTMLDivElement>; +} + + +@observer +export class CollectionBaseView extends React.Component<CollectionViewProps> { + get collectionViewType(): CollectionViewType | undefined { + let Document = this.props.Document; + let viewField = Cast(Document.viewType, "number"); + if (viewField !== undefined) { + return viewField; + } else { + return CollectionViewType.Invalid; + } + } + + active = (): boolean => { + var isSelected = this.props.isSelected(); + var topMost = this.props.isTopMost; + return isSelected || this._isChildActive || topMost; + } + + //TODO should this be observable? + private _isChildActive = false; + whenActiveChanged = (isActive: boolean) => { + this._isChildActive = isActive; + this.props.whenActiveChanged(isActive); + } + + createsCycle(documentToAdd: Doc, containerDocument: Doc): boolean { + if (!(documentToAdd instanceof Doc)) { + return false; + } + let data = DocListCast(documentToAdd.data); + for (const doc of data) { + if (this.createsCycle(doc, containerDocument)) { + return true; + } + } + let annots = DocListCast(documentToAdd.annotations); + for (const annot of annots) { + if (this.createsCycle(annot, containerDocument)) { + return true; + } + } + for (let containerProto: Opt<Doc> = containerDocument; containerProto !== undefined; containerProto = FieldValue(containerProto.proto)) { + if (containerProto[Id] === documentToAdd[Id]) { + return true; + } + } + return false; + } + @computed get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } + + @action.bound + addDocument(doc: Doc, allowDuplicates: boolean = false): boolean { + let props = this.props; + var curPage = NumCast(props.Document.curPage, -1); + Doc.SetOnPrototype(doc, "page", curPage); + if (curPage >= 0) { + Doc.SetOnPrototype(doc, "annotationOn", props.Document); + } + if (!this.createsCycle(doc, props.Document)) { + //TODO This won't create the field if it doesn't already exist + 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])); + } + // 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: Doc): boolean { + const props = this.props; + //TODO This won't create the field if it doesn't already exist + const value = Cast(props.Document[props.fieldKey], listSpec(Doc), []); + let index = -1; + for (let i = 0; i < value.length; i++) { + let v = value[i]; + if (v instanceof Doc && v[Id] === doc[Id]) { + index = i; + break; + } + } + PromiseValue(Cast(doc.annotationOn, Doc)).then((annotationOn) => { + if (annotationOn === props.Document) { + doc.annotationOn = undefined; + } + }); + + if (index !== -1) { + value.splice(index, 1); + + // SelectionManager.DeselectAll() + ContextMenu.Instance.clearItems(); + return true; + } + return false; + } + + @action.bound + 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; + } + + render() { + const props: CollectionRenderProps = { + addDocument: this.addDocument, + removeDocument: this.removeDocument, + moveDocument: this.moveDocument, + active: this.active, + whenActiveChanged: this.whenActiveChanged, + }; + const viewtype = this.collectionViewType; + return ( + <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> + ); + } + +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index 2706c3272..0e7e0afa7 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -1,9 +1,30 @@ +@import "../../views/globalCssVariables.scss"; + .collectiondockingview-content { height: 100%; } +.lm_active .messageCounter{ + color:white; + background: #999999; +} +.messageCounter { + width:18px; + height:20px; + text-align: center; + border-radius: 20px; + margin-left: 5px; + transform: translate(0px, -8px); + display: inline-block; + background: transparent; + border: 1px #999999 solid; +} .collectiondockingview-container { - position: relative; + width: 100%; + height: 100%; + border-style: solid; + border-width: $COLLECTION_BORDER_WIDTH; + position: absolute; top: 0; left: 0; overflow: hidden; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 6a0404663..28624e020 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,41 +1,46 @@ -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 } from "mobx"; +import { action, observable, reaction, Lambda } 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 } from "../../../fields/Field"; -import { Utils } from "../../../Utils"; -import { Server } from "../../Server"; -import { undoBatch } from "../../util/UndoManager"; +import * as GoldenLayout from "../../../client/goldenLayout"; +import { Doc, Field, Opt, DocListCast } from "../../../new_fields/Doc"; +import { FieldId, Id } from "../../../new_fields/RefField"; +import { listSpec } from "../../../new_fields/Schema"; +import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; +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 { COLLECTION_BORDER_WIDTH } from "./CollectionView"; +import { SubCollectionViewProps } from "./CollectionSubView"; import React = require("react"); -import { SubCollectionViewProps } from "./CollectionViewBase"; +import { ParentDocSelector } from './ParentDocumentSelector'; +import { DocumentManager } from '../../util/DocumentManager'; @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 } - } + }; } private _goldenLayout: any = null; private _containerRef = React.createRef<HTMLDivElement>(); - private _fullScreen: any = null; private _flush: boolean = false; + private _ignoreStateChange = ""; constructor(props: SubCollectionViewProps) { super(props); @@ -43,43 +48,84 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp (window as any).React = React; (window as any).ReactDOM = ReactDOM; } - public StartOtherDrag(dragDoc: Document, e: any) { - this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener.onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 }) + 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)] - } + }; var docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout); this._goldenLayout.root.contentItems[0].addChild(docconfig); docconfig.callDownwards('_$init'); this._goldenLayout._$maximiseItem(docconfig); - this._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; + public CloseRightSplit(document: Doc): boolean { + let retVal = false; + if (this._goldenLayout.root.contentItems[0].isRow) { + retVal = Array.from(this._goldenLayout.root.contentItems[0].contentItems).some((child: any) => { + if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" && + Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)) { + child.contentItems[0].remove(); + this.layoutChanged(document); + return true; + } else { + Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => { + if (Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) { + child.contentItems[j].remove(); + child.config.activeItemIndex = Math.max(child.contentItems.length - 1, 0); + let docs = Cast(this.props.Document.data, listSpec(Doc)); + docs && docs.indexOf(document) !== -1 && docs.splice(docs.indexOf(document), 1); + return true; + } + return false; + }); + } + return false; + }); + } + if (retVal) { this.stateChanged(); } + return retVal; + } + + @action + layoutChanged(removed?: Doc) { + this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]); + this._goldenLayout.emit('stateChanged'); + this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); + if (removed) CollectionDockingView.Instance._removedDocs.push(removed); + this.stateChanged(); } // // Creates a vertical split on the right side of the docking view, and then adds the Document to that split // @action - public AddRightSplit(document: Document, minimize: boolean = false) { - this._goldenLayout.emit('stateChanged'); + 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)] - } + }; var newContentItem = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout); @@ -94,32 +140,45 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp newRow.addChild(newContentItem, undefined, true); newRow.addChild(collayout, 0, true); - collayout.config["width"] = 50; - newContentItem.config["width"] = 50; + collayout.config.width = 50; + 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.stateChanged(); + this.layoutChanged(); + return newContentItem; } + @action + public AddTab(stack: any, document: Doc) { + let docs = Cast(this.props.Document.data, listSpec(Doc)); + if (docs) { + docs.push(document); + } + let docContentConfig = CollectionDockingView.makeDocumentConfig(document); + var newContentItem = stack.layoutManager.createContentItem(docContentConfig, this._goldenLayout); + stack.addChild(newContentItem.contentItems[0], undefined); + this.layoutChanged(); + } setupGoldenLayout() { - var config = 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)); } else { - if (config == JSON.stringify(this._goldenLayout.toConfig())) + if (config === JSON.stringify(this._goldenLayout.toConfig())) { return; + } try { this._goldenLayout.unbind('itemDropped', this.itemDropped); this._goldenLayout.unbind('tabCreated', this.tabCreated); + this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed); this._goldenLayout.unbind('stackCreated', this.stackCreated); } catch (e) { } this._goldenLayout.destroy(); @@ -127,6 +186,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } this._goldenLayout.on('itemDropped', this.itemDropped); this._goldenLayout.on('tabCreated', this.tabCreated); + this._goldenLayout.on('tabDestroyed', this.tabDestroyed); this._goldenLayout.on('stackCreated', this.stackCreated); this._goldenLayout.registerComponent('DocumentFrameRenderer', DockedFrameRenderer); this._goldenLayout.container = this._containerRef.current; @@ -140,22 +200,39 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp this._goldenLayout.init(); } } + reactionDisposer?: Lambda; componentDidMount: () => void = () => { if (this._containerRef.current) { - reaction( - () => this.props.Document.GetText(KeyStore.Data, ""), - () => this.setupGoldenLayout(), { fireImmediately: true }); + this.reactionDisposer = reaction( + () => StrCast(this.props.Document.dockingConfig), + () => { + if (!this._goldenLayout || this._ignoreStateChange !== JSON.stringify(this._goldenLayout.toConfig())) { + // Because this is in a set timeout, if this component unmounts right after mounting, + // we will leak a GoldenLayout, because we try to destroy it before we ever create it + setTimeout(() => this.setupGoldenLayout(), 1); + } + this._ignoreStateChange = ""; + }, { fireImmediately: true }); window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window } } componentWillUnmount: () => void = () => { - this._goldenLayout.unbind('itemDropped', this.itemDropped); - this._goldenLayout.unbind('tabCreated', this.tabCreated); - this._goldenLayout.unbind('stackCreated', this.stackCreated); - this._goldenLayout.destroy(); + try { + this._goldenLayout.unbind('itemDropped', this.itemDropped); + this._goldenLayout.unbind('tabCreated', this.tabCreated); + this._goldenLayout.unbind('stackCreated', this.stackCreated); + this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed); + } catch (e) { + + } + if (this._goldenLayout) this._goldenLayout.destroy(); this._goldenLayout = null; window.removeEventListener('resize', this.onResize); + + if (this.reactionDisposer) { + this.reactionDisposer(); + } } @action onResize = (event: any) => { @@ -175,7 +252,36 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp @action onPointerDown = (e: React.PointerEvent): void => { var className = (e.target as any).className; - if (className == "lm_drag_handle" || className == "lm_close" || className == "lm_maximise" || className == "lm_minimise" || className == "lm_close_tab") { + if (className === "messageCounter") { + e.stopPropagation(); + e.preventDefault(); + let x = e.clientX; + let y = e.clientY; + let docid = (e.target as any).DashDocId; + let tab = (e.target as any).parentElement as HTMLElement; + DocServer.GetRefField(docid).then(action(async (sourceDoc: Opt<Field>) => + (sourceDoc instanceof Doc) && DragLinksAsDocuments(tab, x, y, sourceDoc))); + } else + if ((className === "lm_title" || className === "lm_tab lm_active") && !e.shiftKey) { + e.stopPropagation(); + e.preventDefault(); + let x = e.clientX; + let y = e.clientY; + let docid = (e.target as any).DashDocId; + let tab = (e.target as any).parentElement as HTMLElement; + DocServer.GetRefField(docid).then(action((f: Opt<Field>) => { + if (f instanceof Doc) { + DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f]), x, y, + { + handlers: { + dragComplete: emptyFunction, + }, + hideSource: false + }); + } + })); + } + if (className === "lm_drag_handle" || className === "lm_close" || className === "lm_maximise" || className === "lm_minimise" || className === "lm_close_tab") { this._flush = true; } if (this.props.active()) { @@ -185,20 +291,77 @@ 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 = () => { this.stateChanged(); } - tabCreated = (tab: any) => { + + htmlToElement(html: string) { + var template = document.createElement('template'); + html = html.trim(); // Never return a text node of whitespace as the result + template.innerHTML = html; + return template.content.firstChild; + } + + tabCreated = async (tab: any) => { + if (tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") { + if (tab.contentItem.config.fixed) { + tab.contentItem.parent.config.fixed = true; + } + DocServer.GetRefField(tab.contentItem.config.props.documentId).then(async doc => { + if (doc instanceof Doc) { + let counter: any = this.htmlToElement(`<span class="messageCounter">0</div>`); + tab.element.append(counter); + let upDiv = document.createElement("span"); + ReactDOM.render(<ParentDocSelector Document={doc} />, upDiv); + tab.reactComponents = [upDiv]; + tab.element.append(upDiv); + counter.DashDocId = tab.contentItem.config.props.documentId; + tab.reactionDisposer = reaction(() => [doc.linkedFromDocs, doc.LinkedToDocs, doc.title], + () => { + counter.innerHTML = DocListCast(doc.linkedFromDocs).length + DocListCast(doc.linkedToDocs).length; + tab.titleElement[0].textContent = doc.title; + }, { fireImmediately: true }); + tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; + } + }); + } tab.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(); }); } + tabDestroyed = (tab: any) => { + if (tab.reactComponents) { + for (const ele of tab.reactComponents) { + ReactDOM.unmountComponentAtNode(ele); + } + } + } + _removedDocs: Doc[] = []; + stackCreated = (stack: any) => { //stack.header.controlsContainer.find('.lm_popout').hide(); stack.header.controlsContainer.find('.lm_close') //get the close icon @@ -206,70 +369,103 @@ 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 () { + 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"); + })); } render() { return ( <div className="collectiondockingview-container" id="menuContainer" - onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} - style={{ - width: "100%", - height: "100%", - borderStyle: "solid", - borderWidth: `${COLLECTION_BORDER_WIDTH}px`, - }} /> + onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} /> ); } + } interface DockedFrameProps { - documentId: FieldId, + documentId: FieldId; //collectionDockingView: CollectionDockingView } @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>; + _stack: any; constructor(props: any) { super(props); - Server.GetField(this.props.documentId, action((f: Opt<Field>) => this._document = f as Document)); + this._stack = (this.props as any).glContainer.parent.parent; + DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc)); } - private _nativeWidth = () => { return this._document!.GetNumber(KeyStore.NativeWidth, this._panelWidth); } - private _nativeHeight = () => { return this._document!.GetNumber(KeyStore.NativeHeight, this._panelHeight); } - private _contentScaling = () => { return this._panelWidth / (this._nativeWidth() ? this._nativeWidth() : this._panelWidth); } + 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!); - return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(scale / this._contentScaling()) + 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 Transform.Identity(); } + get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; } - render() { - if (!this._document) + addDocTab = (doc: Doc) => { + CollectionDockingView.Instance.AddTab(this._stack, doc); + } + get content() { + if (!this._document) { return (null); - var content = - <div className="collectionDockingView-content" ref={this._mainCont}> - <DocumentView key={this._document.Id} Document={this._document} - AddDocument={undefined} - RemoveDocument={undefined} - ContentScaling={this._contentScaling} - PanelWidth={this._nativeWidth} - PanelHeight={this._nativeHeight} + } + 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} + bringToFront={emptyFunction} + addDocument={undefined} + removeDocument={undefined} + ContentScaling={this.contentScaling} + PanelWidth={this.nativeWidth} + PanelHeight={this.nativeHeight} ScreenToLocalTransform={this.ScreenToLocalTransform} isTopMost={true} - SelectOnLoad={false} - focus={(doc: Document) => { }} + selectOnLoad={false} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + focus={emptyFunction} + addDocTab={this.addDocTab} 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 offset onResize={action((r: any) => { this._panelWidth = r.offset.width; this._panelHeight = r.offset.height; })}> + {({ measureRef }) => <div ref={measureRef}> {theContent} </div>} + </Measure>; } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionFreeFormView.scss b/src/client/views/collections/CollectionFreeFormView.scss deleted file mode 100644 index d487cd7ce..000000000 --- a/src/client/views/collections/CollectionFreeFormView.scss +++ /dev/null @@ -1,75 +0,0 @@ -.collectionfreeformview-container { - - .collectionfreeformview > .jsx-parser{ - position:absolute; - height: 100%; - width: 100%; - } - - border-style: solid; - box-sizing: border-box; - position: relative; - top: 0; - left: 0; - width: 100%; - height: 100%; - overflow: hidden; - .collectionfreeformview { - position: absolute; - top: 0; - left: 0; - width:100%; - height: 100%; - } -} -.collectionfreeformview-marquee{ - border-style: dashed; - box-sizing: border-box; - position: absolute; - border-width: 1px; - border-color: black; -} -.collectionfreeformview-overlay { - - .collectionfreeformview > .jsx-parser{ - position:absolute; - height: 100%; - } - .formattedTextBox-cont { - background:yellow; - } - - border-style: solid; - box-sizing: border-box; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - overflow: hidden; - .collectionfreeformview { - position: absolute; - top: 0; - left: 0; - width:100%; - height: 100%; - } -} - -.border { - border-style: solid; - box-sizing: border-box; - width: 100%; - height: 100%; -} - -//this is an animation for the blinking cursor! -@keyframes blink { - 0% {opacity: 0} - 49%{opacity: 0} - 50% {opacity: 1} -} - -#prevCursor { - animation: blink 1s infinite; -}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx deleted file mode 100644 index b0cd7e017..000000000 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ /dev/null @@ -1,394 +0,0 @@ -import { action, computed, observable } 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 { TextField } from "../../../fields/TextField"; -import { Documents } from "../../documents/Documents"; -import { DragManager } from "../../util/DragManager"; -import { Transform } from "../../util/Transform"; -import { undoBatch } from "../../util/UndoManager"; -import { CollectionDockingView } from "../collections/CollectionDockingView"; -import { CollectionPDFView } from "../collections/CollectionPDFView"; -import { CollectionSchemaView } from "../collections/CollectionSchemaView"; -import { CollectionView } from "../collections/CollectionView"; -import { InkingCanvas } from "../InkingCanvas"; -import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; -import { DocumentView } from "../nodes/DocumentView"; -import { FormattedTextBox } from "../nodes/FormattedTextBox"; -import { ImageBox } from "../nodes/ImageBox"; -import { KeyValueBox } from "../nodes/KeyValueBox"; -import { PDFBox } from "../nodes/PDFBox"; -import { WebBox } from "../nodes/WebBox"; -import "./CollectionFreeFormView.scss"; -import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; -import { CollectionViewBase } from "./CollectionViewBase"; -import React = require("react"); -import { SelectionManager } from "../../util/SelectionManager"; -const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this? - -@observer -export class CollectionFreeFormView extends CollectionViewBase { - private _canvasRef = React.createRef<HTMLDivElement>(); - @observable - private _lastX: number = 0; - @observable - private _lastY: number = 0; - private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type) - - @observable - private _downX: number = 0; - @observable - private _downY: number = 0; - - //determines whether the blinking cursor for indicating whether a text will be made on key down is visible - @observable - private _previewCursorVisible: boolean = false; - - @computed get panX(): number { return this.props.Document.GetNumber(KeyStore.PanX, 0) } - @computed get panY(): number { return this.props.Document.GetNumber(KeyStore.PanY, 0) } - @computed get scale(): number { return this.props.Document.GetNumber(KeyStore.Scale, 1); } - @computed get isAnnotationOverlay() { return this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's? - @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } - @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } - @computed get zoomScaling() { return this.props.Document.GetNumber(KeyStore.Scale, 1); } - @computed get centeringShiftX() { return !this.props.Document.GetNumber(KeyStore.NativeWidth, 0) ? this.props.panelWidth() / 2 : 0; } // shift so pan position is at center of window for non-overlay collections - @computed get centeringShiftY() { return !this.props.Document.GetNumber(KeyStore.NativeHeight, 0) ? this.props.panelHeight() / 2 : 0; }// shift so pan position is at center of window for non-overlay collections - - @undoBatch - @action - drop = (e: Event, de: DragManager.DropEvent) => { - super.drop(e, de); - const docView: DocumentView = de.data["documentView"]; - let doc: Document = docView ? docView.props.Document : de.data["document"]; - if (doc) { - let screenX = de.x - (de.data["xOffset"] as number || 0); - let screenY = de.y - (de.data["yOffset"] as number || 0); - const [x, y] = this.getTransform().transformPoint(screenX, screenY); - doc.SetNumber(KeyStore.X, x); - doc.SetNumber(KeyStore.Y, y); - this.bringToFront(doc); - } - } - - @observable - _marquee = false; - - @action - onPointerDown = (e: React.PointerEvent): void => { - if (((e.button === 2 && this.props.active()) || !e.defaultPrevented) && !e.shiftKey && - (!this.isAnnotationOverlay || this.zoomScaling != 1 || e.button == 0)) { - document.removeEventListener("pointermove", this.onPointerMove); - document.addEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointerup", this.onPointerUp); - this._lastX = e.pageX; - this._lastY = e.pageY; - this._downX = e.pageX; - this._downY = e.pageY; - } - } - - @action - onPointerUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - e.stopPropagation(); - - if (this._marquee) { - if (!e.shiftKey) { - SelectionManager.DeselectAll(); - } - var selectedDocs = this.marqueeSelect(); - selectedDocs.map(s => this.props.CollectionView.SelectedDocs.push(s.Id)); - this._marquee = false; - } - else if (!this._marquee && Math.abs(this._downX - e.clientX) < 3 && Math.abs(this._downY - e.clientY) < 3) { - //show preview text cursor on tap - this._previewCursorVisible = true; - //select is not already selected - if (!this.props.isSelected()) { - this.props.select(false); - } - } - - } - - intersectRect(r1: { left: number, right: number, top: number, bottom: number }, - r2: { left: number, right: number, top: number, bottom: number }) { - return !(r2.left > r1.right || - r2.right < r1.left || - r2.top > r1.bottom || - r2.bottom < r1.top); - } - - marqueeSelect() { - this.props.CollectionView.SelectedDocs.length = 0; - var curPage = this.props.Document.GetNumber(KeyStore.CurPage, 1); - let p = this.getTransform().transformPoint(this._downX, this._downY); - let v = this.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); - let selRect = { left: p[0], top: p[1], right: p[0] + v[0], bottom: p[1] + v[1] } - - var curPage = this.props.Document.GetNumber(KeyStore.CurPage, 1); - const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField); - let selection: Document[] = []; - if (lvalue && lvalue != FieldWaiting) { - lvalue.Data.map(doc => { - var page = doc.GetNumber(KeyStore.Page, 0); - if (page == curPage || page == 0) { - var x = doc.GetNumber(KeyStore.X, 0); - var y = doc.GetNumber(KeyStore.Y, 0); - var w = doc.GetNumber(KeyStore.Width, 0); - var h = doc.GetNumber(KeyStore.Height, 0); - if (this.intersectRect({ left: x, top: y, right: x + w, bottom: y + h }, selRect)) - selection.push(doc) - } - }) - } - return selection; - } - - @action - onPointerMove = (e: PointerEvent): void => { - if (!e.cancelBubble && this.props.active()) { - e.stopPropagation(); - e.preventDefault(); - let wasMarquee = this._marquee; - this._marquee = e.buttons != 2; - if (this._marquee && !wasMarquee) { - document.addEventListener("keydown", this.marqueeCommand); - } - - if (!this._marquee) { - let x = this.props.Document.GetNumber(KeyStore.PanX, 0); - let y = this.props.Document.GetNumber(KeyStore.PanY, 0); - let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); - this._previewCursorVisible = false; - this.SetPan(x - dx, y - dy); - } - } - this._lastX = e.pageX; - this._lastY = e.pageY; - } - - @action - marqueeCommand = (e: KeyboardEvent) => { - if (e.key == "Backspace") { - this.marqueeSelect().map(d => this.props.removeDocument(d)); - } - if (e.key == "c") { - } - } - - @action - onPointerWheel = (e: React.WheelEvent): void => { - e.stopPropagation(); - e.preventDefault(); - let coefficient = 1000; - - if (e.ctrlKey) { - var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); - var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0); - const coefficient = 1000; - let deltaScale = (1 - (e.deltaY / coefficient)); - this.props.Document.SetNumber(KeyStore.NativeWidth, nativeWidth * deltaScale); - this.props.Document.SetNumber(KeyStore.NativeHeight, nativeHeight * deltaScale); - e.stopPropagation(); - e.preventDefault(); - } else { - // if (modes[e.deltaMode] == 'pixels') coefficient = 50; - // else if (modes[e.deltaMode] == 'lines') coefficient = 1000; // This should correspond to line-height?? - let transform = this.getTransform(); - - let deltaScale = (1 - (e.deltaY / coefficient)); - if (deltaScale * this.zoomScaling < 1 && this.isAnnotationOverlay) - deltaScale = 1 / this.zoomScaling; - let [x, y] = transform.transformPoint(e.clientX, e.clientY); - - let localTransform = this.getLocalTransform() - localTransform = localTransform.inverse().scaleAbout(deltaScale, x, y) - // console.log(localTransform) - - this.props.Document.SetNumber(KeyStore.Scale, localTransform.Scale); - this.SetPan(-localTransform.TranslateX / localTransform.Scale, -localTransform.TranslateY / localTransform.Scale); - } - } - - @action - private SetPan(panX: number, panY: number) { - var x1 = this.getLocalTransform().inverse().Scale; - var x2 = this.getTransform().inverse().Scale; - const newPanX = Math.min((1 - 1 / x1) * this.nativeWidth, Math.max(0, panX)); - const newPanY = Math.min((1 - 1 / x1) * 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); - } - - @action - onDrop = (e: React.DragEvent): void => { - var pt = this.getTransform().transformPoint(e.pageX, e.pageY); - super.onDrop(e, { x: pt[0], y: pt[1] }); - } - - onDragOver = (): void => { - } - - @action - onKeyDown = (e: React.KeyboardEvent<Element>) => { - //if not these keys, make a textbox if preview cursor is active! - if (!e.ctrlKey && !e.altKey) { - if (this._previewCursorVisible) { - //make textbox and add it to this collection - let [x, y] = this.getTransform().transformPoint(this._downX, this._downY); (this._downX, this._downY); - let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "new" }); - // mark this collection so that when the text box is created we can send it the SelectOnLoad prop to focus itself - this._selectOnLoaded = newBox.Id; - //set text to be the typed key and get focus on text box - this.props.CollectionView.addDocument(newBox); - //remove cursor from screen - this._previewCursorVisible = false; - } - } - } - - @action - bringToFront(doc: Document) { - const { fieldKey: fieldKey, Document: Document } = this.props; - - const value: Document[] = Document.GetList<Document>(fieldKey, []).slice(); - value.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) - }); - } - - @computed get backgroundLayout(): string | undefined { - let field = this.props.Document.GetT(KeyStore.BackgroundLayout, TextField); - if (field && field !== "<Waiting>") { - return field.Data; - } - } - @computed get overlayLayout(): string | undefined { - let field = this.props.Document.GetT(KeyStore.OverlayLayout, TextField); - if (field && field !== "<Waiting>") { - return field.Data; - } - } - - focusDocument = (doc: Document) => { - let x = doc.GetNumber(KeyStore.X, 0) + doc.GetNumber(KeyStore.Width, 0) / 2; - let y = doc.GetNumber(KeyStore.Y, 0) + doc.GetNumber(KeyStore.Height, 0) / 2; - this.SetPan(x, y); - this.props.focus(this.props.Document); - } - - - @computed - get views() { - var curPage = this.props.Document.GetNumber(KeyStore.CurPage, 1); - const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField); - if (lvalue && lvalue != FieldWaiting) { - return lvalue.Data.map(doc => { - var page = doc.GetNumber(KeyStore.Page, 0); - return (page != curPage && page != 0) ? (null) : - (<CollectionFreeFormDocumentView key={doc.Id} Document={doc} - AddDocument={this.props.addDocument} - RemoveDocument={this.props.removeDocument} - ScreenToLocalTransform={this.getTransform} - isTopMost={false} - SelectOnLoad={doc.Id === this._selectOnLoaded} - ContentScaling={this.noScaling} - PanelWidth={doc.Width} - PanelHeight={doc.Height} - ContainingCollectionView={this.props.CollectionView} - focus={this.focusDocument} - />); - }) - } - return null; - } - - @computed - get backgroundView() { - return !this.backgroundLayout ? (null) : - (<JsxParser - components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, PDFBox }} - bindings={this.props.bindings} - jsx={this.backgroundLayout} - showWarnings={true} - onError={(test: any) => console.log(test)} - />); - } - @computed - get overlayView() { - return !this.overlayLayout ? (null) : - (<JsxParser - components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, PDFBox }} - bindings={this.props.bindings} - jsx={this.overlayLayout} - showWarnings={true} - onError={(test: any) => console.log(test)} - />); - } - - getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH).translate(-this.centeringShiftX, -this.centeringShiftY).transform(this.getLocalTransform()) - getLocalTransform = (): Transform => Transform.Identity.scale(1 / this.scale).translate(this.panX, this.panY); - noScaling = () => 1; - - //when focus is lost, this will remove the preview cursor - @action - onBlur = (e: React.FocusEvent<HTMLDivElement>): void => { - this._previewCursorVisible = false; - } - - render() { - //determines whether preview text cursor should be visible (ie when user taps this collection it should) - let cursor = null; - if (this._previewCursorVisible) { - //get local position and place cursor there! - let [x, y] = this.getTransform().transformPoint(this._downX, this._downY); - cursor = <div id="prevCursor" onKeyPress={this.onKeyDown} style={{ color: "black", position: "absolute", transformOrigin: "left top", transform: `translate(${x}px, ${y}px)` }}>I</div> - } - - let p = this.getTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); - let v = this.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); - var marquee = this._marquee ? <div className="collectionfreeformview-marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}` }}></div> : (null); - - let [dx, dy] = [this.centeringShiftX, this.centeringShiftY]; - - const panx: number = -this.props.Document.GetNumber(KeyStore.PanX, 0); - const pany: number = -this.props.Document.GetNumber(KeyStore.PanY, 0); - - return ( - <div className={`collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`} - onPointerDown={this.onPointerDown} - onKeyPress={this.onKeyDown} - onWheel={this.onPointerWheel} - onDrop={this.onDrop.bind(this)} - onDragOver={this.onDragOver} - onBlur={this.onBlur} - style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px`, }} - tabIndex={0} - ref={this.createDropTarget}> - <div className="collectionfreeformview" - style={{ transformOrigin: "left top", transform: `translate(${dx}px, ${dy}px) scale(${this.zoomScaling}, ${this.zoomScaling}) translate(${panx}px, ${pany}px)` }} - ref={this._canvasRef}> - {this.backgroundView} - <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} /> - {cursor} - {this.views} - {marquee} - </div> - {this.overlayView} - </div> - ); - } -} diff --git a/src/client/views/collections/CollectionPDFView.scss b/src/client/views/collections/CollectionPDFView.scss new file mode 100644 index 000000000..f6fb79582 --- /dev/null +++ b/src/client/views/collections/CollectionPDFView.scss @@ -0,0 +1,49 @@ +.collectionPdfView-buttonTray { + 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; + background-color: rgba(50, 50, 50, 0.2); +} +.collectionPdfView-forward { + color : white; + font-size: 24px; + top :0px; + left : 45px; + position: absolute; + background-color: rgba(50, 50, 50, 0.2); +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx index bcb1cd2f7..a6614da21 100644 --- a/src/client/views/collections/CollectionPDFView.tsx +++ b/src/client/views/collections/CollectionPDFView.tsx @@ -1,57 +1,85 @@ -import { action, computed } from "mobx"; +import { action, observable } from "mobx"; import { observer } from "mobx-react"; -import { Document } from "../../../fields/Document"; -import { KeyStore } from "../../../fields/KeyStore"; import { ContextMenu } from "../ContextMenu"; -import { CollectionView, CollectionViewType } from "./CollectionView"; -import { CollectionViewProps } from "./CollectionViewBase"; +import "./CollectionPDFView.scss"; import React = require("react"); -import { FieldId } from "../../../fields/Field"; +import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; +import { FieldView, FieldViewProps } from "../nodes/FieldView"; +import { CollectionRenderProps, CollectionBaseView, CollectionViewType } from "./CollectionBaseView"; +import { emptyFunction } from "../../../Utils"; +import { NumCast } from "../../../new_fields/Types"; +import { Id } from "../../../new_fields/RefField"; @observer -export class CollectionPDFView extends React.Component<CollectionViewProps> { +export class CollectionPDFView extends React.Component<FieldViewProps> { - public static LayoutString(fieldKey: string = "DataKey") { - return `<${CollectionPDFView.name} Document={Document} - ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} panelWidth={PanelWidth} panelHeight={PanelHeight} isSelected={isSelected} select={select} bindings={bindings} - isTopMost={isTopMost} SelectOnLoad={selectOnLoad} BackgroundView={BackgroundView} focus={focus}/>`; + public static LayoutString(fieldKey: string = "data") { + return FieldView.LayoutString(CollectionPDFView, fieldKey); } + @observable _inThumb = false; - public SelectedDocs: FieldId[] = [] - @action onPageBack = () => this.curPage > 1 ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage - 1) : 0; - @action onPageForward = () => this.curPage < this.numPages ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage + 1) : 0; + 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; - @computed private get curPage() { return this.props.Document.GetNumber(KeyStore.CurPage, 0); } - @computed private get numPages() { return this.props.Document.GetNumber(KeyStore.NumPages, 0); } - @computed private get uIButtons() { + @action + onThumbDown = (e: React.PointerEvent) => { + document.addEventListener("pointermove", this.onThumbMove, false); + document.addEventListener("pointerup", this.onThumbUp, false); + e.stopPropagation(); + this._inThumb = true; + } + @action + onThumbMove = (e: PointerEvent) => { + let pso = (e.clientY - (e as any).target.parentElement.getBoundingClientRect().top) / (e as any).target.parentElement.getBoundingClientRect().height; + this.curPage = Math.trunc(Math.min(this.numPages, pso * this.numPages + 1)); + e.stopPropagation(); + } + @action + onThumbUp = (e: PointerEvent) => { + this._inThumb = false; + document.removeEventListener("pointermove", this.onThumbMove); + document.removeEventListener("pointerup", this.onThumbUp); + } + nativeWidth = () => NumCast(this.props.Document.nativeWidth); + nativeHeight = () => NumCast(this.props.Document.nativeHeight); + private get uIButtons() { + let ratio = (this.curPage - 1) / this.numPages * 100; return ( - <div className="pdfBox-buttonTray" key="tray"> - <button className="pdfButton" onClick={this.onPageBack}>{"<"}</button> - <button className="pdfButton" onClick={this.onPageForward}>{">"}</button> - </div>); + <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> + ); } - // "inherited" CollectionView API starts here... - - public active: () => boolean = () => CollectionView.Active(this); - - addDocument = (doc: Document): void => { CollectionView.AddDocument(this.props, doc); } - removeDocument = (doc: Document): boolean => { return CollectionView.RemoveDocument(this.props, doc); } - - specificContextMenu = (e: React.MouseEvent): void => { - if (!e.isPropagationStopped() && this.props.Document.Id != "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - ContextMenu.Instance.addItem({ description: "PDFOptions", event: () => { }, icon: "file-pdf" }); + onContextMenu = (e: React.MouseEvent): void => { + if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 + ContextMenu.Instance.addItem({ description: "PDFOptions", event: emptyFunction, icon: "file-pdf" }); } } - get collectionViewType(): CollectionViewType { return CollectionViewType.Freeform; } - get subView(): any { return CollectionView.SubView(this); } + private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => { + let props = { ...this.props, ...renderProps }; + return ( + <> + <CollectionFreeFormView {...props} CollectionView={this} /> + {this.props.isSelected() ? this.uIButtons : (null)} + </> + ); + } render() { - return (<div className="collectionView-cont" onContextMenu={this.specificContextMenu}> - {this.subView} - {this.props.isSelected() ? this.uIButtons : (null)} - </div>) + return ( + <CollectionBaseView {...this.props} className={`collectionPdfView-cont${this._inThumb ? "-dragging" : ""}`} onContextMenu={this.onContextMenu}> + {this.subView} + </CollectionBaseView> + ); } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index d40e6d314..5e9d2ac67 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -1,54 +1,110 @@ +@import "../globalCssVariables"; + .collectionSchemaView-container { + border-width: $COLLECTION_BORDER_WIDTH; + border-color : $intermediate-color; border-style: solid; + border-radius: $border-radius; box-sizing: border-box; position: absolute; width: 100%; height: 100%; + + .collectionSchemaView-cellContents { + height: $MAX_ROW_HEIGHT; + img { + width:auto; + max-height: $MAX_ROW_HEIGHT; + } + } + .collectionSchemaView-previewRegion { - position: relative; - background: black; - float: left; + 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; - height: 37px; - width: 20px; + height: 15px; + width: 15px; z-index: 20; right: 0; - top: 0; + top: 20px; background: Black ; } .collectionSchemaView-dividerDragger{ position: relative; background: black; float: left; + height: 37px; + width: 20px; + z-index: 20; + right: 0; + top: 0; + background: $main-accent; + } + .collectionSchemaView-columnsHandle { + position: absolute; + height: 37px; + width: 20px; + z-index: 20; + left: 0; + bottom: 0; + background: $main-accent; + } + .collectionSchemaView-colDividerDragger { + position: relative; + box-sizing: border-box; + border-top: 1px solid $intermediate-color; + border-bottom: 1px solid $intermediate-color; + float: top; + width: 100%; + } + .collectionSchemaView-dividerDragger { + position: relative; + box-sizing: border-box; + border-left: 1px solid $intermediate-color; + border-right: 1px solid $intermediate-color; + float: left; height: 100%; } .collectionSchemaView-tableContainer { position: relative; float: left; - height: 100%; + height: 100%; } - .ReactTable { - position: absolute; - // display: inline-block; - // overflow: auto; + // position: absolute; // display: inline-block; + // overflow: auto; width: 100%; height: 100%; - background: white; + background: $light-color; box-sizing: border-box; + border: none !important; .rt-table { overflow-y: auto; overflow-x: auto; height: 100%; - display: -webkit-inline-box; - direction: ltr; - // direction:rtl; + direction: ltr; // direction:rtl; // display:block; } .rt-tbody { @@ -57,45 +113,113 @@ } .rt-tr-group { direction: ltr; - max-height: 44px; + max-height: $MAX_ROW_HEIGHT; } .rt-td { - border-width: 1; - border-right-color: #aaa; + border-width: 1px; + border-right-color: $intermediate-color; .imageBox-cont { - position:relative; - max-height:100%; + position: relative; + max-height: 100%; } .imageBox-cont img { object-fit: contain; max-width: 100%; - height: 100% + height: 100%; + } + .videoBox-cont { + object-fit: contain; + width: auto; + height: 100%; } - } - .rt-tr-group { - border-width: 1; - border-bottom-color: #aaa } } .ReactTable .rt-thead.-header { - background:grey; - } - .ReactTable .rt-th, .ReactTable .rt-td { - max-height: 44; + background: $intermediate-color; + color: $light-color; + text-transform: uppercase; + letter-spacing: 2px; + font-size: 12px; + height: 30px; + padding-top: 4px; + } + .ReactTable .rt-th, + .ReactTable .rt-td { + max-height: $MAX_ROW_HEIGHT; padding: 3px 7px; + font-size: 13px; + text-align: center; } .ReactTable .rt-tbody .rt-tr-group:last-child { - border-bottom: grey; + border-bottom: $intermediate-color; 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: grey; - .imageBox-cont img { - object-fit: contain; - } + background: $light-color; } } +//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; @@ -204,4 +328,12 @@ -webkit-flex: 1; -ms-flex: 1; flex: 1; +} + +.-even { + background: $light-color !important; +} + +.-odd { + background: $light-color-secondary !important; }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 04f017378..e2b90e26d 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -1,84 +1,131 @@ -import React = require("react") -import { action, observable } from "mobx"; +import React = require("react"); +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faCog, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable, untracked, runInAction } from "mobx"; import { 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 } from "../../../fields/Field"; -import { KeyStore } from "../../../fields/KeyStore"; -import { CompileScript, ToField } from "../../util/Scripting"; +import { emptyFunction, returnFalse, returnZero } from "../../../Utils"; +import { SetupDrag } from "../../util/DragManager"; +import { CompileScript } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; -import { ContextMenu } from "../ContextMenu"; +import { COLLECTION_BORDER_WIDTH } from "../../views/globalCssVariables.scss"; +import { anchorPoints, Flyout } from "../DocumentDecorations"; +import '../DocumentDecorations.scss'; import { EditableView } from "../EditableView"; import { DocumentView } from "../nodes/DocumentView"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; import "./CollectionSchemaView.scss"; -import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; -import { CollectionViewBase } from "./CollectionViewBase"; -import { setupDrag } from "../../util/DragManager"; +import { CollectionSubView } from "./CollectionSubView"; +import { Opt, Field, Doc, DocListCastAsync, DocListCast } from "../../../new_fields/Doc"; +import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; +import { listSpec } from "../../../new_fields/Schema"; +import { List } from "../../../new_fields/List"; +import { Id } from "../../../new_fields/RefField"; +import { Gateway } from "../../northstar/manager/Gateway"; +import { Docs } from "../../documents/Documents"; +import { ContextMenu } from "../ContextMenu"; + // bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657 @observer -export class CollectionSchemaView extends CollectionViewBase { - private _mainCont = React.createRef<HTMLDivElement>(); - private DIVIDER_WIDTH = 5; - - @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; +class KeyToggle extends React.Component<{ keyName: string, checked: boolean, toggle: (key: string) => void }> { + constructor(props: any) { + super(props); + } + + render() { + 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(doc => doc) { + private _mainCont?: HTMLDivElement; + private _startSplitPercent = 0; + private DIVIDER_WIDTH = 4; + + @observable _columns: Array<string> = ["title", "data", "author"]; @observable _selectedIndex = 0; - @observable _splitPercentage: number = 50; + @observable _columnsPercentage = 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 = { - doc: rowProps.value[0], + Document: rowProps.value[0], fieldKey: rowProps.value[1], - isSelected: () => false, - select: () => { }, + ContainingCollectionView: this.props.CollectionView, + isSelected: returnFalse, + select: emptyFunction, isTopMost: false, - bindings: {}, selectOnLoad: false, - } - let contents = ( - <FieldView {...props} /> - ) + ScreenToLocalTransform: Transform.Identity, + focus: emptyFunction, + active: returnFalse, + whenActiveChanged: emptyFunction, + PanelHeight: returnZero, + PanelWidth: returnZero, + }; + let fieldContentView = <FieldView {...props} />; let reference = React.createRef<HTMLDivElement>(); - let onItemDown = setupDrag(reference, () => props.doc); + let onItemDown = (e: React.PointerEvent) => + (this.props.CollectionView.props.isSelected() ? + SetupDrag(reference, () => props.Document, this.props.moveDocument)(e) : undefined); + let applyToDoc = (doc: Doc, run: (args?: { [name: string]: any }) => any) => { + const res = run({ this: doc }); + if (!res.success) return false; + doc[props.fieldKey] = res.result; + return true; + }; return ( - <div onPointerDown={onItemDown} key={props.doc.Id} ref={reference}> - <EditableView contents={contents} - height={36} GetValue={() => { - let field = props.doc.Get(props.fieldKey); - if (field && field instanceof Field) { - return field.ToScriptString(); + <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={props.Document[Id]} ref={reference}> + <EditableView + display={"inline"} + contents={fieldContentView} + height={Number(MAX_ROW_HEIGHT)} + GetValue={() => { + 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, undefined, true); + let script = CompileScript(value, { addReturn: true, params: { this: Document.name } }); if (!script.compiled) { return false; } - let field = script(); - if (field instanceof Field) { - props.doc.Set(props.fieldKey, field); - return true; - } else { - let dataField = ToField(field); - if (dataField) { - props.doc.Set(props.fieldKey, dataField); - return true; - } + return applyToDoc(props.Document, script.run); + }} + OnFillDown={async (value: string) => { + let script = CompileScript(value, { addReturn: true, params: { this: Document.name } }); + if (!script.compiled) { + return; } - return false; + const run = script.run; + //TODO This should be able to be refactored to compile the script once + const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]); + val && val.forEach(doc => applyToDoc(doc, run)); }}> </EditableView> - </div> - ) + </div > + ); } private getTrProps: ComponentPropsGetterR = (state, rowInfo) => { @@ -88,159 +135,240 @@ export class CollectionSchemaView extends CollectionViewBase { } return { onClick: action((e: React.MouseEvent, handleOriginal: Function) => { + that.props.select(e.ctrlKey); that._selectedIndex = rowInfo.index; - this._splitPercentage += 0.05; // bcz - ugh - needed to force Measure to do its thing and call onResize if (handleOriginal) { - handleOriginal() + handleOriginal(); } }), style: { - background: rowInfo.index == this._selectedIndex ? "lightGray" : "white", - //color: rowInfo.index == this._selectedIndex ? "white" : "black" + background: rowInfo.index === this._selectedIndex ? "lightGray" : "white", + //color: rowInfo.index === this._selectedIndex ? "white" : "black" } }; } - _startSplitPercent = 0; + private createTarget = (ele: HTMLDivElement) => { + this._mainCont = ele; + super.CreateDropTarget(ele); + } + + @action + 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.props.Document.schemaSplitPercentage = this.splitPercentage === 0 ? 33 : 0; + } + @action onDividerMove = (e: PointerEvent): void => { - let nativeWidth = this._mainCont.current!.getBoundingClientRect(); - this._splitPercentage = 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._splitPercentage = this._splitPercentage == 1 ? 66 : 100; + if (this._startSplitPercent === this.splitPercentage) { + this.props.Document.schemaSplitPercentage = this.splitPercentage === 0 ? 33 : 0; } } onDividerDown = (e: React.PointerEvent) => { - this._startSplitPercent = this._splitPercentage; + this._startSplitPercent = this.splitPercentage; e.stopPropagation(); e.preventDefault(); document.addEventListener("pointermove", this.onDividerMove); document.addEventListener('pointerup', this.onDividerUp); } - @action - onExpanderMove = (e: PointerEvent): void => { - e.stopPropagation(); - e.preventDefault(); + + onPointerDown = (e: React.PointerEvent): void => { + if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) { + if (this.props.isSelected()) e.stopPropagation(); + else e.preventDefault(); + } } - @action - onExpanderUp = (e: PointerEvent): void => { - e.stopPropagation(); - e.preventDefault(); - document.removeEventListener("pointermove", this.onExpanderMove); - document.removeEventListener('pointerup', this.onExpanderUp); - if (this._startSplitPercent == this._splitPercentage) { - this._splitPercentage = this._splitPercentage == 100 ? 66 : 100; + + onWheel = (e: React.WheelEvent): void => { + if (this.props.active()) { + e.stopPropagation(); } } - onExpanderDown = (e: React.PointerEvent) => { - this._startSplitPercent = this._splitPercentage; - e.stopPropagation(); - e.preventDefault(); - document.addEventListener("pointermove", this.onExpanderMove); - document.addEventListener('pointerup', this.onExpanderUp); - } - - onPointerDown = (e: React.PointerEvent) => { - // if (e.button === 2 && this.active) { - // e.stopPropagation(); - // e.preventDefault(); - // } else - { - if (e.buttons === 1) { - if (this.props.isSelected()) { - e.stopPropagation(); - } + + onContextMenu = (e: React.MouseEvent): void => { + if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 + ContextMenu.Instance.addItem({ description: "Make DB", event: this.makeDB }); + } + } + + @action + makeDB = async () => { + let csv: string = this.columns.reduce((val, col) => val + col + ",", ""); + csv = csv.substr(0, csv.length - 1) + "\n"; + let self = this; + DocListCast(this.props.Document.data).map(doc => { + csv += self.columns.reduce((val, col) => val + (doc[col] ? doc[col]!.toString() : "") + ",", ""); + csv = csv.substr(0, csv.length - 1) + "\n"; + }); + csv.substring(0, csv.length - 1); + let dbName = StrCast(this.props.Document.title); + let res = await Gateway.Instance.PostSchema(csv, dbName); + if (self.props.CollectionView.props.addDocument) { + let schemaDoc = await Docs.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }); + if (schemaDoc) { + self.props.CollectionView.props.addDocument(schemaDoc, false); } } } @action - setScaling = (r: any) => { - const children = this.props.Document.GetList<Document>(this.props.fieldKey, []); - 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); + addColumn = () => { + this.columns.push(this._newKeyName); + this._newKeyName = ""; } - getContentScaling = (): number => this._contentScaling; - getPanelWidth = (): number => this._panelWidth; - getPanelHeight = (): number => this._panelHeight; - getTransform = (): Transform => { - return this.props.ScreenToLocalTransform().translate(- COLLECTION_BORDER_WIDTH - this.DIVIDER_WIDTH - this._dividerX, - COLLECTION_BORDER_WIDTH).scale(1 / this._contentScaling); + @action + newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this._newKeyName = e.currentTarget.value; } - focusDocument = (doc: Document) => { } + @observable previewScript: string = ""; + @action + onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this.previewScript = e.currentTarget.value; + } - render() { - const columns = this.props.Document.GetList(KeyStore.ColumnsKey, [KeyStore.Title, KeyStore.Data, KeyStore.Author]) - const children = this.props.Document.GetList<Document>(this.props.fieldKey, []); - const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined; - let content = this._selectedIndex == -1 || !selected ? (null) : ( - <Measure onResize={this.setScaling}> - {({ measureRef }) => - <div className="collectionSchemaView-content" ref={measureRef}> - <DocumentView Document={selected} - AddDocument={this.props.addDocument} RemoveDocument={this.props.removeDocument} - isTopMost={false} - SelectOnLoad={false} - ScreenToLocalTransform={this.getTransform} - ContentScaling={this.getContentScaling} - PanelWidth={this.getPanelWidth} - PanelHeight={this.getPanelHeight} - ContainingCollectionView={this.props.CollectionView} - focus={this.focusDocument} - /> + @computed + get previewDocument(): Doc | undefined { + const children = DocListCast(this.props.Document[this.props.fieldKey]); + const selected = children.length > this._selectedIndex ? FieldValue(children[this._selectedIndex]) : undefined; + return selected ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined; + } + get tableWidth() { return (this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH) * (1 - this.splitPercentage / 100); } + get previewRegionHeight() { return this.props.PanelHeight() - 2 * this.borderWidth; } + get previewRegionWidth() { return (this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH) * this.splitPercentage / 100; } + + private previewDocNativeWidth = () => Cast(this.previewDocument!.nativeWidth, "number", this.previewRegionWidth); + private previewDocNativeHeight = () => Cast(this.previewDocument!.nativeHeight, "number", this.previewRegionHeight); + private previewContentScaling = () => { + let wscale = this.previewRegionWidth / (this.previewDocNativeWidth() ? this.previewDocNativeWidth() : this.previewRegionWidth); + if (wscale * this.previewDocNativeHeight() > this.previewRegionHeight) { + return this.previewRegionHeight / (this.previewDocNativeHeight() ? this.previewDocNativeHeight() : this.previewRegionHeight); + } + return wscale; + } + private previewPanelWidth = () => this.previewDocNativeWidth() * this.previewContentScaling(); + private previewPanelHeight = () => this.previewDocNativeHeight() * this.previewContentScaling(); + get previewPanelCenteringOffset() { return (this.previewRegionWidth - this.previewDocNativeWidth() * this.previewContentScaling()) / 2; } + getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate( + - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth - this.previewPanelCenteringOffset, + - this.borderWidth).scale(1 / this.previewContentScaling()) + + @computed + get previewPanel() { + // let doc = CompileScript(this.previewScript, { this: selected }, true)(); + const previewDoc = this.previewDocument; + return (<div className="collectionSchemaView-previewRegion" style={{ width: `${Math.max(0, this.previewRegionWidth - 1)}px` }}> + {!previewDoc || !this.previewRegionWidth ? (null) : ( + <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}> + <DocumentView Document={previewDoc} isTopMost={false} selectOnLoad={false} + toggleMinimized={emptyFunction} + addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} + ScreenToLocalTransform={this.getPreviewTransform} + ContentScaling={this.previewContentScaling} + PanelWidth={this.previewPanelWidth} PanelHeight={this.previewPanelHeight} + ContainingCollectionView={this.props.CollectionView} + focus={emptyFunction} + parentActive={this.props.active} + whenActiveChanged={this.props.whenActiveChanged} + bringToFront={emptyFunction} + /> + </div>)} + <input className="collectionSchemaView-input" value={this.previewScript} onChange={this.onPreviewScriptChange} + style={{ left: `calc(50% - ${Math.min(75, (previewDoc ? this.previewPanelWidth() / 2 : 75))}px)` }} /> + </div>); + } + + get documentKeysCheckList() { + const docs = DocListCast(this.props.Document[this.props.fieldKey]); + 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} />)); + } + + 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> - } - </Measure> - ) - let previewHandle = !this.props.active() ? (null) : ( - <div className="collectionSchemaView-previewHandle" onPointerDown={this.onExpanderDown} />); - let dividerDragger = this._splitPercentage == 100 ? (null) : - <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} /> + </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); + const children = this.children; return ( - <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} ref={this._mainCont} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} > - <div className="collectionSchemaView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}> - <Measure onResize={action((r: any) => { - this._dividerX = r.entry.width; - this._panelHeight = r.entry.height; - })}> - {({ measureRef }) => - <div ref={measureRef} className="collectionSchemaView-tableContainer" style={{ width: `${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, - - }} - getTrProps={this.getTrProps} - /> - </div> - } - </Measure> - {dividerDragger} - <div className="collectionSchemaView-previewRegion" style={{ width: `calc(${100 - this._splitPercentage}% - ${this.DIVIDER_WIDTH}px)` }}> - {content} - </div> - {previewHandle} + <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel} + onDrop={(e: React.DragEvent) => this.onDrop(e, {})} onContextMenu={this.onContextMenu} ref={this.createTarget}> + <div className="collectionSchemaView-tableContainer" style={{ width: `${this.tableWidth}px` }}> + <ReactTable data={children} page={0} pageSize={children.length} showPagination={false} + columns={this.columns.map(col => ({ + Header: col, + accessor: (doc: Doc) => [doc, col], + id: col + }))} + column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }} + getTrProps={this.getTrProps} + /> </div> - </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 new file mode 100644 index 000000000..ffd3e0659 --- /dev/null +++ b/src/client/views/collections/CollectionSubView.tsx @@ -0,0 +1,231 @@ +import { action, runInAction } from "mobx"; +import React = require("react"); +import { undoBatch, UndoManager } from "../../util/UndoManager"; +import { DragManager } from "../../util/DragManager"; +import { Docs, DocumentOptions } from "../../documents/Documents"; +import { RouteStore } from "../../../server/RouteStore"; +import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; +import { FieldViewProps } from "../nodes/FieldView"; +import * as rp from 'request-promise'; +import { CollectionView } from "./CollectionView"; +import { CollectionPDFView } from "./CollectionPDFView"; +import { CollectionVideoView } from "./CollectionVideoView"; +import { Doc, Opt, FieldResult, DocListCast } from "../../../new_fields/Doc"; +import { DocComponent } from "../DocComponent"; +import { listSpec } from "../../../new_fields/Schema"; +import { Cast, PromiseValue, FieldValue, ListSpec } from "../../../new_fields/Types"; +import { List } from "../../../new_fields/List"; +import { DocServer } from "../../DocServer"; +import { ObjectField } from "../../../new_fields/ObjectField"; +import CursorField, { CursorPosition, CursorMetadata } from "../../../new_fields/CursorField"; +import { url } from "inspector"; + +export interface CollectionViewProps extends FieldViewProps { + addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; + removeDocument: (document: Doc) => boolean; + moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; + PanelWidth: () => number; + PanelHeight: () => number; +} + +export interface SubCollectionViewProps extends CollectionViewProps { + CollectionView: CollectionView | CollectionPDFView | CollectionVideoView; +} + +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) } }); + } + } + protected CreateDropTarget(ele: HTMLDivElement) { + this.createDropTarget(ele); + } + + 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 DocListCast(this.props.Document[this.props.fieldKey]); + } + + @action + protected async setCursorPosition(position: [number, number]) { + let ind; + let doc = this.props.Document; + let id = CurrentUserUtils.id; + let email = CurrentUserUtils.email; + let pos = { x: position[0], y: position[1] }; + if (id && email) { + const proto = await doc.proto; + if (!proto) { + return; + } + let cursors = Cast(proto.cursors, listSpec(CursorField)); + if (!cursors) { + proto.cursors = cursors = new List<CursorField>(); + } + if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.data.metadata.id === id)) > -1) { + cursors[ind].setPosition(pos); + } else { + let entry = new CursorField({ metadata: { id: id, identifier: email }, position: pos }); + cursors.push(entry); + } + } + } + + @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; + } + return false; + } + + protected async getDocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> { + let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined; + if (type.indexOf("image") !== -1) { + ctor = Docs.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; + } + 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; + } + + @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")) { + let htmlDoc = Docs.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text }); + this.props.addDocument(htmlDoc, false); + return; + } + if (text && text.indexOf("www.youtube.com/watch") !== -1) { + const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/"); + this.props.addDocument(Docs.WebDocument(url, { ...options, width: 300, height: 300 })); + return; + } + + 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); + } + } + + 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 f8d580a7b..411d67ff7 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -1,37 +1,88 @@ -#body { +@import "../globalCssVariables"; + +.collectionTreeView-dropTarget { + border-width: $COLLECTION_BORDER_WIDTH; + border-color: transparent; + border-style: solid; + border-radius: $border-radius; + box-sizing: border-box; + height: 100%; padding: 20px; - background: #bbbbbb; -} + padding-left: 10px; + padding-right: 0px; + background: $light-color-secondary; + font-size: 13px; + overflow: scroll; -ul { - list-style: none; -} + ul { + list-style: none; + padding-left: 20px; + } -li { - margin: 5px 0; -} + li { + margin: 5px 0; + } -.no-indent { - padding-left: 0; -} -.bullet { - width: 1.5em; - display: inline-block; -} + .no-indent { + padding-left: 0; + } -.collectionTreeView-dropTarget { - border-style: solid; - box-sizing: border-box; - height: 100%; -} + .bullet { + float:left; + position: relative; + width: 15px; + display: block; + color: $intermediate-color; + margin-top: 3px; + transform: scale(1.3,1.3); + } + + .docContainer { + margin-left: 10px; + display: block; + // width:100%;//width: max-content; + } + .docContainer:hover { + .treeViewItem-openRight { + display:inline; + } + } + + + .editableView-container { + font-weight: bold; + } -.docContainer { - display: inline-table; -} + .delete-button { + color: $intermediate-color; + // float: right; + margin-left: 15px; + // margin-top: 3px; + display: inline; + } + .treeViewItem-openRight { + margin-left: 5px; + display:none; + } + .docContainer:hover { + .delete-button { + display: inline; + // width: auto; + } + } -.delete-button { - color: #999999; - float: right; - margin-left: 1em; + .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 8b06d9ac4..6acef434e 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,20 +1,32 @@ +import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; +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 { CollectionViewBase } from "./CollectionViewBase"; -import { Document } from "../../../fields/Document"; -import { KeyStore } from "../../../fields/KeyStore"; -import { ListField } from "../../../fields/ListField"; -import React = require("react") -import { TextField } from "../../../fields/TextField"; -import { observable, action } from "mobx"; -import "./CollectionTreeView.scss"; +import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager"; import { EditableView } from "../EditableView"; -import { setupDrag } from "../../util/DragManager"; -import { FieldWaiting } from "../../../fields/Field"; -import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; +import { CollectionSubView } from "./CollectionSubView"; +import "./CollectionTreeView.scss"; +import React = require("react"); +import { Document, listSpec } from '../../../new_fields/Schema'; +import { Cast, StrCast, BoolCast, FieldValue } from '../../../new_fields/Types'; +import { Doc, DocListCast } from '../../../new_fields/Doc'; +import { Id } from '../../../new_fields/RefField'; +import { ContextMenu } from '../ContextMenu'; +import { undoBatch } from '../../util/UndoManager'; +import { Main } from '../Main'; +import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; +import { CollectionDockingView } from './CollectionDockingView'; +import { DocumentManager } from '../../util/DocumentManager'; +import { List } from '../../../new_fields/List'; +import { Docs } from '../../documents/Documents'; + export interface TreeViewProps { - document: Document; - deleteDoc: (doc: Document) => void; + document: Doc; + deleteDoc: (doc: Doc) => void; + moveDocument: DragManager.MoveFunction; + dropAction: "alias" | "copy" | undefined; } export enum BulletType { @@ -23,151 +35,219 @@ export enum BulletType { List } +library.add(faTrashAlt); +library.add(faAngleRight); +library.add(faCaretDown); +library.add(faCaretRight); + @observer /** * Component that takes in a document prop and a boolean whether it's collapsed or not. */ class TreeView extends React.Component<TreeViewProps> { - @observable - collapsed: boolean = false; + @observable _collapsed: boolean = true; + + @undoBatch delete = () => this.props.deleteDoc(this.props.document); - 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); + remove = (document: Document, key: string) => { + let children = Cast(this.props.document[key], listSpec(Doc), []); + if (children) { + children.splice(children.indexOf(document), 1); } } - renderBullet(type: BulletType) { - let onClicked = action(() => this.collapsed = !this.collapsed); + @action + move: DragManager.MoveFunction = (document, target, addDoc) => { + if (this.props.document === target) { + return true; + } + //TODO This should check if it was removed + this.remove(document, "data"); + return addDoc(document); + } + renderBullet(type: BulletType) { + let onClicked = action(() => this._collapsed = !this._collapsed); + let bullet: IconProp | undefined = undefined; switch (type) { - case BulletType.Collapsed: - return <div className="bullet" onClick={onClicked}>▶</div> - case BulletType.Collapsible: - return <div className="bullet" onClick={onClicked}>▼</div> - case BulletType.List: - return <div className="bullet">—</div> + case BulletType.Collapsed: bullet = "caret-right"; break; + case BulletType.Collapsible: bullet = "caret-down"; break; } + return <div className="bullet" onClick={onClicked}>{bullet ? <FontAwesomeIcon icon={bullet} /> : ""} </div>; } + @action + onMouseEnter = () => { + this._isOver = true; + } + @observable _isOver: boolean = false; + @action + onMouseLeave = () => { + this._isOver = false; + } /** * Renders the EditableView title element for placement into the tree. */ renderTitle() { - let title = this.props.document.GetT<TextField>(KeyStore.Title, TextField); - - // if the title hasn't loaded, immediately return the div - if (!title || title === "<Waiting>") { - return <div key={this.props.document.Id}></div>; - } - - return <div className="docContainer"> <EditableView contents={title.Data} - height={36} GetValue={() => { - let title = this.props.document.GetT<TextField>(KeyStore.Title, TextField); - if (title && title !== "<Waiting>") - return title.Data; - return ""; - }} SetValue={(value: string) => { - this.props.document.SetData(KeyStore.Title, value, TextField); - return true; - }} /> - <div className="delete-button" onClick={this.delete}>x</div> - </div > + let reference = React.createRef<HTMLDivElement>(); + let onItemDown = SetupDrag(reference, () => this.props.document, this.props.moveDocument, this.props.dropAction); + let editableView = (titleString: string) => + (<EditableView + oneLine={!this._isOver ? true : false} + display={"block"} + contents={titleString} + height={36} + GetValue={() => StrCast(this.props.document.title)} + SetValue={(value: string) => { + let target = this.props.document.proto ? this.props.document.proto : this.props.document; + target.title = value; + return true; + }} + />); + let dataDocs = 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} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} + style={{ background: BoolCast(this.props.document.protoBrush, false) ? "#06123232" : BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0" }} + onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> + {editableView(StrCast(this.props.document.title))} + {openRight} + {/* {<div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div>} */} + </div >); } - render() { - var children = this.props.document.GetT<ListField<Document>>(KeyStore.Data, ListField); - - let reference = React.createRef<HTMLDivElement>(); - let onItemDown = setupDrag(reference, () => this.props.document); - let titleElement = this.renderTitle(); - - // check if this document is a collection - if (children && children !== FieldWaiting) { - let subView; - - // if uncollapsed, then add the children elements - if (!this.collapsed) { - // render all children elements - let childrenElement = (children.Data.map(value => - <TreeView document={value} deleteDoc={this.remove} />) - ) - subView = - <li key={this.props.document.Id} > - {this.renderBullet(BulletType.Collapsible)} - {titleElement} - <ul key={this.props.document.Id}> - {childrenElement} - </ul> - </li> - } else { - subView = <li key={this.props.document.Id}> - {this.renderBullet(BulletType.Collapsed)} - {titleElement} - </li> + 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) }); + ContextMenu.Instance.addItem({ + description: "Open Fields", event: () => CollectionDockingView.Instance.AddRightSplit(Docs.KVPDocument(this.props.document, + { title: this.props.document.title + ".kvp", width: 300, height: 300 })) + }); + if (DocumentManager.Instance.getDocumentViews(this.props.document).length) { + ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document)) }); } - - return <div className="treeViewItem-container" onPointerDown={onItemDown} ref={reference}> - {subView} - </div> + 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; }; - // otherwise this is a normal leaf node - else { - return <li key={this.props.document.Id}> - {this.renderBullet(BulletType.List)} - {titleElement} - </li>; + render() { + let bulletType = BulletType.List; + let contentElement: (JSX.Element | null)[] = []; + let keys = Array.from(Object.keys(this.props.document)); + if (this.props.document.proto instanceof Doc) { + keys.push(...Array.from(Object.keys(this.props.document.proto))); + while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1); } + keys.map(key => { + let docList = DocListCast(this.props.document[key]); + let doc = Cast(this.props.document[key], Doc); + if (doc instanceof Doc || docList.length) { + if (!this._collapsed) { + bulletType = BulletType.Collapsible; + let spacing = (key === "data") ? 0 : -10; + contentElement.push(<ul key={key + "more"}> + {(key === "data") ? (null) : + <span className="collectionTreeView-keyHeader" style={{ display: "block", marginTop: "7px" }} key={key}>{key}</span>} + <div style={{ display: "block", marginTop: `${spacing}px` }}> + {TreeView.GetChildElements(doc instanceof Doc ? [doc] : docList, key !== "data", (doc: Doc) => this.remove(doc, key), this.move, this.props.dropAction)} + </div> + </ul >); + } else { + bulletType = BulletType.Collapsed; + } + } + }); + return <div className="treeViewItem-container" + onContextMenu={this.onWorkspaceContextMenu}> + <li className="collection-child"> + {this.renderBullet(bulletType)} + {this.renderTitle()} + {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)).map(child => + <TreeView document={child} key={child[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} />); } } - @observer -export class CollectionTreeView extends CollectionViewBase { - +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 titleStr = ""; - let title = this.props.Document.GetT<TextField>(KeyStore.Title, TextField); - if (title && title !== FieldWaiting) { - titleStr = title.Data; + let dropAction = StrCast(this.props.Document.dropAction, "alias") as dropActionType; + if (!this.children) { + return (null); } - - var children = this.props.Document.GetT<ListField<Document>>(KeyStore.Data, ListField); - let childrenElement = !children || children === FieldWaiting ? (null) : - (children.Data.map(value => - <TreeView document={value} key={value.Id} deleteDoc={this.remove} />) - ) + let childElements = TreeView.GetChildElements(this.children, false, this.remove, this.props.moveDocument, dropAction); return ( - <div id="body" className="collectionTreeView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }}> - <h3> - <EditableView contents={titleStr} - height={72} GetValue={() => { - return this.props.Document.Title; - }} SetValue={(value: string) => { - this.props.Document.SetData(KeyStore.Title, value, TextField); + <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} + display={"inline"} + height={72} + GetValue={() => StrCast(this.props.Document.title)} + SetValue={(value: string) => { + let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document; + target.title = value; return true; }} /> - </h3> + </div> <ul className="no-indent"> - {childrenElement} + {childElements} </ul> </div > ); diff --git a/src/client/views/collections/CollectionVideoView.scss b/src/client/views/collections/CollectionVideoView.scss new file mode 100644 index 000000000..db8b84832 --- /dev/null +++ b/src/client/views/collections/CollectionVideoView.scss @@ -0,0 +1,42 @@ + +.collectionVideoView-cont{ + width: 100%; + height: 100%; + position: inherit; + top: 0; + left:0; + +} +.collectionVideoView-time{ + color : white; + top :25px; + left : 25px; + position: absolute; + background-color: rgba(50, 50, 50, 0.2); + transform-origin: left top; +} +.collectionVideoView-play { + width: 25px; + height: 20px; + bottom: 25px; + left : 25px; + position: absolute; + color : white; + background-color: rgba(50, 50, 50, 0.2); + border-radius: 4px; + text-align: center; + transform-origin: left bottom; +} +.collectionVideoView-full { + width: 25px; + height: 20px; + bottom: 25px; + right : 25px; + position: absolute; + color : white; + background-color: rgba(50, 50, 50, 0.2); + border-radius: 4px; + text-align: center; + transform-origin: right bottom; + +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx new file mode 100644 index 000000000..9ab959f3c --- /dev/null +++ b/src/client/views/collections/CollectionVideoView.tsx @@ -0,0 +1,89 @@ +import { action, observable, trace } from "mobx"; +import { observer } from "mobx-react"; +import { ContextMenu } from "../ContextMenu"; +import { CollectionViewType, CollectionBaseView, CollectionRenderProps } from "./CollectionBaseView"; +import React = require("react"); +import "./CollectionVideoView.scss"; +import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; +import { FieldView, FieldViewProps } from "../nodes/FieldView"; +import { emptyFunction } from "../../../Utils"; +import { Id } from "../../../new_fields/RefField"; +import { VideoBox } from "../nodes/VideoBox"; +import { NumCast } from "../../../new_fields/Types"; + + +@observer +export class CollectionVideoView extends React.Component<FieldViewProps> { + private _videoBox?: VideoBox; + + public static LayoutString(fieldKey: string = "data") { + return FieldView.LayoutString(CollectionVideoView, fieldKey); + } + private get uIButtons() { + let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale); + let curTime = NumCast(this.props.Document.curPage); + return ([ + <div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling}, ${scaling})` }}> + <span>{"" + Math.round(curTime)}</span> + <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span> + </div>, + <div className="collectionVideoView-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling}, ${scaling})` }}> + {this._videoBox && this._videoBox.Playing ? "\"" : ">"} + </div>, + <div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling}, ${scaling})` }}> + F + </div> + ]); + } + + @action + onPlayDown = () => { + if (this._videoBox && this._videoBox.player) { + if (this._videoBox.Playing) { + this._videoBox.Pause(); + } else { + this._videoBox.Play(); + } + } + } + + @action + onFullDown = (e: React.PointerEvent) => { + if (this._videoBox) { + this._videoBox.FullScreen(); + e.stopPropagation(); + e.preventDefault(); + } + } + + @action + onResetDown = () => { + if (this._videoBox) { + this._videoBox.Pause(); + this.props.Document.curPage = 0; + } + } + + onContextMenu = (e: React.MouseEvent): void => { + if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 + } + } + + setVideoBox = (videoBox: VideoBox) => { this._videoBox = videoBox; }; + + private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => { + let props = { ...this.props, ...renderProps }; + return (<> + <CollectionFreeFormView {...props} setVideoBox={this.setVideoBox} CollectionView={this} /> + {this.props.isSelected() ? this.uIButtons : (null)} + </>); + } + + render() { + trace(); + return ( + <CollectionBaseView {...this.props} className="collectionVideoView-cont" onContextMenu={this.onContextMenu}> + {this.subView} + </CollectionBaseView>); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index d0e2162b1..3d9b4990a 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,132 +1,56 @@ -import { action, computed, observable } from "mobx"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faProjectDiagram, faSquare, faTh, faTree } from '@fortawesome/free-solid-svg-icons'; import { observer } from "mobx-react"; -import { Document } from "../../../fields/Document"; -import { ListField } from "../../../fields/ListField"; -import { SelectionManager } from "../../util/SelectionManager"; +import * as React from 'react'; +import { Id } from '../../../new_fields/RefField'; +import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; +import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from "../ContextMenu"; -import React = require("react"); -import { KeyStore } from "../../../fields/KeyStore"; -import { NumberField } from "../../../fields/NumberField"; -import { CollectionFreeFormView } from "./CollectionFreeFormView"; +import { FieldView, FieldViewProps } from '../nodes/FieldView'; +import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from './CollectionBaseView'; import { CollectionDockingView } from "./CollectionDockingView"; import { CollectionSchemaView } from "./CollectionSchemaView"; -import { CollectionViewProps } from "./CollectionViewBase"; import { CollectionTreeView } from "./CollectionTreeView"; -import { Field, FieldId } from "../../../fields/Field"; -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faTh } from '@fortawesome/free-solid-svg-icons'; -import { faTree } from '@fortawesome/free-solid-svg-icons'; -import { faSquare } from '@fortawesome/free-solid-svg-icons'; -import { faProjectDiagram } from '@fortawesome/free-solid-svg-icons'; +import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; +export const COLLECTION_BORDER_WIDTH = 2; library.add(faTh); library.add(faTree); library.add(faSquare); library.add(faProjectDiagram); -export enum CollectionViewType { - Invalid, - Freeform, - Schema, - Docking, - Tree -} - -export const COLLECTION_BORDER_WIDTH = 2; - @observer -export class CollectionView extends React.Component<CollectionViewProps> { - - @observable - public SelectedDocs: FieldId[] = []; - - public static LayoutString(fieldKey: string = "DataKey") { - return `<${CollectionView.name} Document={Document} - ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} panelWidth={PanelWidth} panelHeight={PanelHeight} isSelected={isSelected} select={select} bindings={bindings} - isTopMost={isTopMost} SelectOnLoad={selectOnLoad} BackgroundView={BackgroundView} focus={focus}/>`; - } - - public active: () => boolean = () => CollectionView.Active(this); - addDocument = (doc: Document): void => { CollectionView.AddDocument(this.props, doc); } - removeDocument = (doc: Document): boolean => { return CollectionView.RemoveDocument(this.props, doc); } - get subView() { return CollectionView.SubView(this); } - - public static Active(self: CollectionView): boolean { - var isSelected = self.props.isSelected(); - var childSelected = SelectionManager.SelectedDocuments().some(view => view.props.ContainingCollectionView == self); - var topMost = self.props.isTopMost; - return isSelected || childSelected || topMost; - } - - @action - public static AddDocument(props: CollectionViewProps, doc: Document) { - doc.SetNumber(KeyStore.Page, props.Document.GetNumber(KeyStore.CurPage, 0)); - if (props.Document.Get(props.fieldKey) instanceof Field) { - //TODO This won't create the field if it doesn't already exist - const value = props.Document.GetData(props.fieldKey, ListField, new Array<Document>()) - value.push(doc); - } else { - props.Document.SetData(props.fieldKey, [doc], ListField); - } - } - - @action - public static RemoveDocument(props: CollectionViewProps, doc: Document): boolean { - //TODO This won't create the field if it doesn't already exist - const value = props.Document.GetData(props.fieldKey, ListField, new Array<Document>()) - let index = -1; - for (let i = 0; i < value.length; i++) { - if (value[i].Id == doc.Id) { - index = i; - break; - } - } - - if (index !== -1) { - value.splice(index, 1) - - SelectionManager.DeselectAll() - ContextMenu.Instance.clearItems() - return true; - } - return false - } - - get collectionViewType(): CollectionViewType { - let Document = this.props.Document; - let viewField = Document.GetT(KeyStore.ViewType, NumberField); - if (viewField === "<Waiting>") { - return CollectionViewType.Invalid; - } else if (viewField) { - return viewField.Data; - } else { - return CollectionViewType.Freeform; +export class CollectionView extends React.Component<FieldViewProps> { + public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(CollectionView, fieldStr); } + + private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => { + let props = { ...this.props, ...renderProps }; + switch (type) { + case CollectionViewType.Schema: return (<CollectionSchemaView {...props} CollectionView={this} />); + case CollectionViewType.Docking: return (<CollectionDockingView {...props} CollectionView={this} />); + case CollectionViewType.Tree: return (<CollectionTreeView {...props} CollectionView={this} />); + case CollectionViewType.Freeform: + default: + return (<CollectionFreeFormView {...props} CollectionView={this} />); } + return (null); } - specificContextMenu = (e: React.MouseEvent): void => { - if (!e.isPropagationStopped() && this.props.Document.Id != "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - ContextMenu.Instance.addItem({ description: "Freeform", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform), icon: "project-diagram" }) - ContextMenu.Instance.addItem({ description: "Schema", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Schema), icon: "th" }) - ContextMenu.Instance.addItem({ description: "Treeview", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Tree), icon: "tree" }) - ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking), icon: "square" }) - } - } + get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } // bcz: ? Why do we need to compare Id's? - public static SubView(self: CollectionView) { - let subProps = { ...self.props, addDocument: self.addDocument, removeDocument: self.removeDocument, active: self.active, CollectionView: self } - switch (self.collectionViewType) { - case CollectionViewType.Freeform: return (<CollectionFreeFormView {...subProps} />) - case CollectionViewType.Schema: return (<CollectionSchemaView {...subProps} />) - case CollectionViewType.Docking: return (<CollectionDockingView {...subProps} />) - case CollectionViewType.Tree: return (<CollectionTreeView {...subProps} />) + onContextMenu = (e: React.MouseEvent): void => { + if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 + ContextMenu.Instance.addItem({ description: "Freeform", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Freeform), icon: "project-diagram" }); + ContextMenu.Instance.addItem({ description: "Schema", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Schema), icon: "project-diagram" }); + ContextMenu.Instance.addItem({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree), icon: "tree" }); } - return (null); } render() { - return (<div className="collectionView-cont" onContextMenu={this.specificContextMenu}> - {this.subView} - </div>) + return ( + <CollectionBaseView {...this.props} onContextMenu={this.onContextMenu}> + {this.SubView} + </CollectionBaseView> + ); } -} +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx deleted file mode 100644 index b126b40a9..000000000 --- a/src/client/views/collections/CollectionViewBase.tsx +++ /dev/null @@ -1,152 +0,0 @@ -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 } from "../../../fields/Field"; -import { undoBatch } from "../../util/UndoManager"; -import { DragManager } from "../../util/DragManager"; -import { DocumentView } from "../nodes/DocumentView"; -import { Documents, DocumentOptions } from "../../documents/Documents"; -import { Key } from "../../../fields/Key"; -import { Transform } from "../../util/Transform"; -import { CollectionView } from "./CollectionView"; - -export interface CollectionViewProps { - fieldKey: Key; - Document: Document; - ScreenToLocalTransform: () => Transform; - isSelected: () => boolean; - isTopMost: boolean; - select: (ctrlPressed: boolean) => void; - bindings: any; - panelWidth: () => number; - panelHeight: () => number; - focus: (doc: Document) => void; -} -export interface SubCollectionViewProps extends CollectionViewProps { - active: () => boolean; - addDocument: (doc: Document) => void; - removeDocument: (doc: Document) => boolean; - CollectionView: CollectionView; -} - -export class CollectionViewBase extends React.Component<SubCollectionViewProps> { - 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) } }); - } - } - - @undoBatch - @action - protected drop(e: Event, de: DragManager.DropEvent) { - const docView: DocumentView = de.data["documentView"]; - const doc: Document = de.data["document"]; - if (docView && (!docView.props.ContainingCollectionView || docView.props.ContainingCollectionView !== this.props.CollectionView)) { - if (docView.props.RemoveDocument) { - docView.props.RemoveDocument(docView.props.Document); - } - this.props.addDocument(docView.props.Document); - } else if (doc) { - this.props.removeDocument(doc); - this.props.addDocument(doc); - } - e.stopPropagation(); - } - - @action - protected onDrop(e: React.DragEvent, options: DocumentOptions): void { - e.stopPropagation() - e.preventDefault() - let that = this; - - let html = e.dataTransfer.getData("text/html"); - let text = e.dataTransfer.getData("text/plain"); - if (html && html.indexOf("<img") != 0) { - console.log("not good"); - let htmlDoc = Documents.HtmlDocument(html, { ...options, width: 300, height: 300 }); - htmlDoc.SetText(KeyStore.DocumentText, text); - this.props.addDocument(htmlDoc); - return; - } - - console.log(e.dataTransfer.items.length); - - for (let i = 0; i < e.dataTransfer.items.length; i++) { - const upload = window.location.origin + "/upload"; - let item = e.dataTransfer.items[i]; - if (item.kind === "string" && item.type.indexOf("uri") != -1) { - e.dataTransfer.items[i].getAsString(function (s) { - action(() => { - var img = Documents.ImageDocument(s, { ...options, nativeWidth: 300, width: 300, }) - - let docs = that.props.Document.GetT(KeyStore.Data, ListField); - if (docs != FieldWaiting) { - if (!docs) { - docs = new ListField<Document>(); - that.props.Document.Set(KeyStore.Data, docs) - } - docs.Data.push(img); - } - })() - - }) - } - let type = item.type - console.log(type) - if (item.kind == "file") { - let fReader = new FileReader() - let file = item.getAsFile(); - let formData = new FormData() - - if (file) { - formData.append('file', file) - } - - fetch(upload, { - method: 'POST', - body: formData - }) - .then((res: Response) => { - return res.json() - }).then(json => { - - json.map((file: any) => { - let path = window.location.origin + file - runInAction(() => { - var doc: any; - - if (type.indexOf("image") !== -1) { - doc = Documents.ImageDocument(path, { ...options, nativeWidth: 300, width: 300, }) - } - if (type.indexOf("video") !== -1) { - doc = Documents.VideoDocument(path, { ...options, nativeWidth: 300, width: 300, }) - } - if (type.indexOf("audio") !== -1) { - doc = Documents.AudioDocument(path, { ...options, nativeWidth: 300, width: 300, }) - } - let docs = that.props.Document.GetT(KeyStore.Data, ListField); - if (docs != FieldWaiting) { - if (!docs) { - docs = new ListField<Document>(); - that.props.Document.Set(KeyStore.Data, docs) - } - if (doc) { - docs.Data.push(doc); - } - - } - }) - }) - }) - - - } - } - } -} diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss new file mode 100644 index 000000000..f3c605f3e --- /dev/null +++ b/src/client/views/collections/ParentDocumentSelector.scss @@ -0,0 +1,8 @@ +.PDS-flyout { + position: absolute; + z-index: 9999; + background-color: #d3d3d3; + box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); + min-width: 150px; + color: black; +}
\ No newline at end of file diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx new file mode 100644 index 000000000..52f7914f3 --- /dev/null +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -0,0 +1,66 @@ +import * as React from "react"; +import './ParentDocumentSelector.scss'; +import { Doc } from "../../../new_fields/Doc"; +import { observer } from "mobx-react"; +import { observable, action, runInAction } from "mobx"; +import { Id } from "../../../new_fields/RefField"; +import { SearchUtil } from "../../util/SearchUtil"; +import { CollectionDockingView } from "./CollectionDockingView"; + +@observer +export class SelectorContextMenu extends React.Component<{ Document: Doc }> { + @observable private _docs: Doc[] = []; + + constructor(props: { Document: Doc }) { + super(props); + + this.fetchDocuments(); + } + + async fetchDocuments() { + const docs = await SearchUtil.Search(`data_l:"${this.props.Document[Id]}"`, true); + runInAction(() => this._docs = docs); + } + + render() { + return ( + <> + {this._docs.map(doc => <p><a onClick={() => CollectionDockingView.Instance.AddRightSplit(doc)}>{doc.title}</a></p>)} + </> + ); + } +} + +@observer +export class ParentDocSelector extends React.Component<{ Document: Doc }> { + @observable hover = false; + + @action + onMouseLeave = () => { + this.hover = false; + } + + @action + onMouseEnter = () => { + this.hover = true; + } + + render() { + let flyout; + if (this.hover) { + flyout = ( + <div className="PDS-flyout"> + <SelectorContextMenu Document={this.props.Document} /> + </div> + ); + } + return ( + <span style={{ position: "relative", display: "inline-block" }} + onMouseEnter={this.onMouseEnter} + onMouseLeave={this.onMouseLeave}> + <p>^</p> + {flyout} + </span> + ); + } +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss new file mode 100644 index 000000000..737ffba7d --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss @@ -0,0 +1,12 @@ +.collectionfreeformlinkview-linkLine { + stroke: black; + transform: translate(10000px,10000px); + opacity: 0.5; + pointer-events: all; +} +.collectionfreeformlinkview-linkCircle { + stroke: rgb(0,0,0); + opacity: 0.5; + transform: translate(10000px,10000px); + pointer-events: all; +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx new file mode 100644 index 000000000..63d2f7642 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -0,0 +1,58 @@ +import { observer } from "mobx-react"; +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: 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) => { + 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 = 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" + style={{ strokeWidth: `${l.length / 2}` }} + x1={`${x1}`} y1={`${y1}`} + x2={`${x2}`} y2={`${y2}`} /> + <circle key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkCircle" + cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={5} onPointerDown={this.onPointerDown} /> + </> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss new file mode 100644 index 000000000..30e158603 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss @@ -0,0 +1,12 @@ +.collectionfreeformlinksview-svgCanvas{ + transform: translate(-10000px,-10000px); + position: absolute; + top: 0; + left: 0; + width: 20000px; + height: 20000px; + pointer-events: none; + } + .collectionfreeformlinksview-container { + pointer-events: none; + }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx new file mode 100644 index 000000000..d5ce4e1e7 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -0,0 +1,130 @@ +import { computed, IReactionDisposer, reaction, trace } from "mobx"; +import { observer } from "mobx-react"; +import { Utils } from "../../../../Utils"; +import { DocumentManager } from "../../../util/DocumentManager"; +import { DocumentView } from "../../nodes/DocumentView"; +import { CollectionViewProps } from "../CollectionSubView"; +import "./CollectionFreeFormLinksView.scss"; +import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView"; +import React = require("react"); +import { Doc, DocListCastAsync, DocListCast } from "../../../../new_fields/Doc"; +import { Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types"; +import { listSpec } from "../../../../new_fields/Schema"; +import { List } from "../../../../new_fields/List"; +import { Id } from "../../../../new_fields/RefField"; + +@observer +export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> { + + _brushReactionDisposer?: IReactionDisposer; + componentDidMount() { + this._brushReactionDisposer = reaction( + () => { + let doclist = DocListCast(this.props.Document[this.props.fieldKey]); + return { doclist: doclist ? doclist : [], xs: doclist.map(d => d.x) }; + }, + () => { + let doclist = DocListCast(this.props.Document[this.props.fieldKey]); + let views = doclist ? doclist.filter(doc => StrCast(doc.backgroundLayout).indexOf("istogram") !== -1) : []; + views.forEach((dstDoc, i) => { + views.forEach((srcDoc, j) => { + let dstTarg = dstDoc; + let srcTarg = srcDoc; + let x1 = NumCast(srcDoc.x); + let x2 = NumCast(dstDoc.x); + let x1w = NumCast(srcDoc.width, -1); + 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); + } + }; + 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() { + if (this._brushReactionDisposer) { + this._brushReactionDisposer(); + } + } + documentAnchors(view: DocumentView) { + let equalViews = [view]; + 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]; + DocListCast(this.props.Document[this.props.fieldKey]). + 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); + } + + @computed + get uniqueConnections() { + let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => { + let srcViews = this.documentAnchors(connection.a); + let targetViews = this.documentAnchors(connection.b); + let possiblePairs: { a: Doc, b: Doc, }[] = []; + srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document }))); + possiblePairs.map(possiblePair => + 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)) { + drawnPair.l.push(connection.l); + } + return match || found; + }, false) + || + drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] }) + ); + return drawnPairs; + }, [] as { a: Doc, b: Doc, l: Doc[] }[]); + return connections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} + removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />); + } + + render() { + return ( + <div className="collectionfreeformlinksview-container"> + <svg className="collectionfreeformlinksview-svgCanvas"> + {this.uniqueConnections} + </svg> + {this.props.children} + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss new file mode 100644 index 000000000..c5b8fc5e8 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss @@ -0,0 +1,24 @@ +@import "globalCssVariables"; + +.collectionFreeFormRemoteCursors-cont { + + position:absolute; + z-index: $remoteCursors-zindex; + transform-origin: 'center center'; +} +.collectionFreeFormRemoteCursors-canvas { + + position:absolute; + width: 20px; + height: 20px; + opacity: 0.5; + border-radius: 50%; + border: 2px solid black; +} +.collectionFreeFormRemoteCursors-symbol { + font-size: 14; + color: black; + // fontStyle: "italic", + margin-left: -12; + margin-top: 4; +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx new file mode 100644 index 000000000..642118d75 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx @@ -0,0 +1,87 @@ +import { computed } from "mobx"; +import { observer } from "mobx-react"; +import { CollectionViewProps } from "../CollectionSubView"; +import "./CollectionFreeFormView.scss"; +import React = require("react"); +import v5 = require("uuid/v5"); +import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; +import CursorField from "../../../../new_fields/CursorField"; +import { List } from "../../../../new_fields/List"; +import { Cast } from "../../../../new_fields/Types"; +import { listSpec } from "../../../../new_fields/Schema"; + +@observer +export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> { + + protected getCursors(): CursorField[] { + let doc = this.props.Document; + + let id = CurrentUserUtils.id; + if (!id) { + return []; + } + + let cursors = Cast(doc.cursors, listSpec(CursorField)); + + return (cursors || []).filter(cursor => cursor.data.metadata.id !== id); + } + + private crosshairs?: HTMLCanvasElement; + drawCrosshairs = (backgroundColor: string) => { + if (this.crosshairs) { + let ctx = this.crosshairs.getContext('2d'); + if (ctx) { + ctx.fillStyle = backgroundColor; + ctx.fillRect(0, 0, 20, 20); + + ctx.fillStyle = "black"; + ctx.lineWidth = 0.5; + + ctx.beginPath(); + + ctx.moveTo(10, 0); + ctx.lineTo(10, 8); + + ctx.moveTo(10, 20); + ctx.lineTo(10, 12); + + ctx.moveTo(0, 10); + ctx.lineTo(8, 10); + + ctx.moveTo(20, 10); + ctx.lineTo(12, 10); + + ctx.stroke(); + + // ctx.font = "10px Arial"; + // ctx.fillText(CurrentUserUtils.email[0].toUpperCase(), 10, 10); + } + } + } + + get sharedCursors() { + return this.getCursors().map(c => { + let m = c.data.metadata; + let l = c.data.position; + this.drawCrosshairs("#" + v5(m.id, v5.URL).substring(0, 6).toUpperCase() + "22"); + return ( + <div key={m.id} className="collectionFreeFormRemoteCursors-cont" + style={{ transform: `translate(${l.x - 10}px, ${l.y - 10}px)` }} + > + <canvas className="collectionFreeFormRemoteCursors-canvas" + ref={(el) => { if (el) this.crosshairs = el; }} + width={20} + height={20} + /> + <p className="collectionFreeFormRemoteCursors-symbol"> + {m.identifier[0].toUpperCase()} + </p> + </div> + ); + }); + } + + render() { + return this.sharedCursors; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss new file mode 100644 index 000000000..063c9e2cf --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -0,0 +1,107 @@ +@import "../../globalCssVariables"; + +.collectionfreeformview-ease { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + transform-origin: left top; + transition: transform 1s; +} + +.collectionfreeformview-none { + position: inherit; + top: 0; + left: 0; + width: 100%; + height: 100%; + transform-origin: left top; +} + +.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; + // } + 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; + .marqueeView { + 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: 0px solid transparent; + border-radius: $border-radius; + box-sizing: border-box; + position:absolute; + .marqueeView { + 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; +} + +//this is an animation for the blinking cursor! +@keyframes blink { + 0% { + opacity: 0; + } + + 49% { + opacity: 0; + } + + 50% { + opacity: 1; + } +} + +#prevCursor { + 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 new file mode 100644 index 000000000..ad32f70dc --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -0,0 +1,404 @@ +import { action, computed, trace } from "mobx"; +import { observer } from "mobx-react"; +import { emptyFunction, returnFalse, returnOne } from "../../../../Utils"; +import { DocumentManager } from "../../../util/DocumentManager"; +import { DragManager } from "../../../util/DragManager"; +import { SelectionManager } from "../../../util/SelectionManager"; +import { Transform } from "../../../util/Transform"; +import { undoBatch } from "../../../util/UndoManager"; +import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"; +import { InkingCanvas } from "../../InkingCanvas"; +import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; +import { DocumentContentsView } from "../../nodes/DocumentContentsView"; +import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView"; +import { CollectionSubView } from "../CollectionSubView"; +import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView"; +import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; +import "./CollectionFreeFormView.scss"; +import { MarqueeView } from "./MarqueeView"; +import React = require("react"); +import v5 = require("uuid/v5"); +import { createSchema, makeInterface, listSpec } from "../../../../new_fields/Schema"; +import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc"; +import { FieldValue, Cast, NumCast, BoolCast } from "../../../../new_fields/Types"; +import { pageSchema } from "../../nodes/ImageBox"; +import { Id } from "../../../../new_fields/RefField"; +import { InkField, StrokeData } from "../../../../new_fields/InkField"; +import { HistoryUtil } from "../../../util/History"; + +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(PanZoomDocument) { + public static RIGHT_BTN_DRAG = false; + private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type) + private _lastX: number = 0; + private _lastY: number = 0; + private get _pwidth() { return this.props.PanelWidth(); } + private get _pheight() { return this.props.PanelHeight(); } + + @computed get nativeWidth() { return this.Document.nativeWidth || 0; } + @computed get nativeHeight() { return this.Document.nativeHeight || 0; } + private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } + private get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } + private panX = () => this.Document.panX || 0; + private panY = () => this.Document.panY || 0; + private zoomScaling = () => this.Document.scale || 1; + private centeringShiftX = () => !this.nativeWidth ? this._pwidth / 2 : 0; // shift so pan position is at center of window for non-overlay collections + private centeringShiftY = () => !this.nativeHeight ? this._pheight / 2 : 0;// shift so pan position is at center of window for non-overlay collections + private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform()); + private getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth); + private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); + private addLiveTextBox = (newBox: Doc) => { + 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: Doc, allowDuplicates: boolean) => { + this.props.addDocument(newBox, false); + this.bringToFront(newBox); + return true; + } + private selectDocuments = (docs: Doc[]) => { + SelectionManager.DeselectAll; + docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).filter(dv => dv).map(dv => + SelectionManager.SelectDoc(dv!, true)); + } + public getActiveDocuments = () => { + const curPage = FieldValue(this.Document.curPage, -1); + return this.children.filter(doc => { + 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) { + if (de.data.droppedDocuments.length) { + let dragDoc = de.data.droppedDocuments[0]; + let zoom = NumCast(dragDoc.zoomBasis, 1); + let [xp, yp] = this.getTransform().transformPoint(de.x, de.y); + let x = xp - de.data.xOffset / zoom; + let y = yp - de.data.yOffset / zoom; + let dropX = NumCast(de.data.droppedDocuments[0].x); + let dropY = NumCast(de.data.droppedDocuments[0].y); + de.data.droppedDocuments.map(d => { + d.x = x + NumCast(d.x) - dropX; + d.y = y + NumCast(d.y) - dropY; + if (!NumCast(d.width)) { + d.width = 300; + } + if (!NumCast(d.height)) { + let nw = NumCast(d.nativeWidth); + let nh = NumCast(d.nativeHeight); + d.height = nw && nh ? nh / nw * NumCast(d.width) : 300; + } + this.bringToFront(d); + }); + SelectionManager.ReselectAll(); + } + return true; + } + return false; + } + + @action + onPointerDown = (e: React.PointerEvent): void => { + if ((CollectionFreeFormView.RIGHT_BTN_DRAG && + (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) || + (e.button === 0 && e.altKey)) && this.props.active())) || + (!CollectionFreeFormView.RIGHT_BTN_DRAG && + ((e.button === 0 && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) && this.props.active()))) { + document.removeEventListener("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; + } + } + + onPointerUp = (e: PointerEvent): void => { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + + @action + onPointerMove = (e: PointerEvent): void => { + if (!e.cancelBubble) { + let x = this.Document.panX || 0; + let y = this.Document.panY || 0; + let docs = this.children || []; + let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); + if (!this.isAnnotationOverlay) { + let minx = docs.length ? NumCast(docs[0].x) : 0; + let maxx = docs.length ? NumCast(docs[0].width) + minx : minx; + let miny = docs.length ? NumCast(docs[0].y) : 0; + let maxy = docs.length ? NumCast(docs[0].height) + miny : miny; + let ranges = docs.filter(doc => doc).reduce((range, doc) => { + let x = NumCast(doc.x); + let xe = x + NumCast(doc.width); + let y = NumCast(doc.y); + let ye = y + NumCast(doc.height); + return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]], + [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]]; + }, [[minx, maxx], [miny, maxy]]); + let ink = Cast(this.props.Document.ink, InkField); + if (ink && ink.inkData) { + ink.inkData.forEach((value: StrokeData, key: string) => { + let bounds = InkingCanvas.StrokeRect(value); + ranges[0] = [Math.min(ranges[0][0], bounds.left), Math.max(ranges[0][1], bounds.right)]; + ranges[1] = [Math.min(ranges[1][0], bounds.top), Math.max(ranges[1][1], bounds.bottom)]; + }); + } + + let panelwidth = this._pwidth / this.zoomScaling() / 2; + let panelheight = this._pheight / this.zoomScaling() / 2; + if (x - dx < ranges[0][0] - panelwidth) x = ranges[0][1] + panelwidth + dx; + if (x - dx > ranges[0][1] + panelwidth) x = ranges[0][0] - panelwidth + dx; + if (y - dy < ranges[1][0] - panelheight) y = ranges[1][1] + panelheight + dy; + if (y - dy > ranges[1][1] + panelheight) y = ranges[1][0] - panelheight + dy; + } + this.setPan(x - dx, y - dy); + this._lastX = e.pageX; + this._lastY = e.pageY; + 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(); + } + } + + @action + onPointerWheel = (e: React.WheelEvent): void => { + // if (!this.props.active()) { + // return; + // } + let childSelected = this.children.some(doc => { + var dv = DocumentManager.Instance.getDocumentView(doc); + return dv && SelectionManager.IsSelected(dv) ? true : false; + }); + if (!this.props.isSelected() && !childSelected && !this.props.isTopMost) { + return; + } + e.stopPropagation(); + const coefficient = 1000; + + if (e.ctrlKey) { + let deltaScale = (1 - (e.deltaY / coefficient)); + let nw = this.nativeWidth * deltaScale; + let nh = this.nativeHeight * deltaScale; + if (nw && nh) { + this.props.Document.nativeWidth = nw; + this.props.Document.nativeHeight = nh; + } + e.stopPropagation(); + e.preventDefault(); + } else { + // if (modes[e.deltaMode] === 'pixels') coefficient = 50; + // else if (modes[e.deltaMode] === 'lines') coefficient = 1000; // This should correspond to line-height?? + let deltaScale = (1 - (e.deltaY / coefficient)); + 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); + + 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) { + this.panDisposer && clearTimeout(this.panDisposer); + this.props.Document.panTransformType = "None"; + var scale = this.getLocalTransform().inverse().Scale; + const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); + const newPanY = Math.min((1 - 1 / scale) * this.nativeHeight, Math.max(0, panY)); + this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX; + this.props.Document.panY = this.isAnnotationOverlay ? newPanY : panY; + } + + @action + onDrop = (e: React.DragEvent): void => { + var pt = this.getTransform().transformPoint(e.pageX, e.pageY); + super.onDrop(e, { x: pt[0], y: pt[1] }); + } + + onDragOver = (): void => { + } + + bringToFront = (doc: Doc) => { + const docs = this.children; + docs.slice().sort((doc1, doc2) => { + if (doc1 === doc) return 1; + if (doc2 === doc) return -1; + return NumCast(doc1.zIndex) - NumCast(doc2.zIndex); + }).forEach((doc, index) => doc.zIndex = index + 1); + doc.zIndex = docs.length + 1; + } + + panDisposer?: NodeJS.Timeout; + focusDocument = (doc: Doc) => { + const panX = this.Document.panX; + const panY = this.Document.panY; + const id = this.Document[Id]; + const state = HistoryUtil.getState(); + // TODO This technically isn't correct if type !== "doc", as + // currently nothing is done, but we should probably push a new state + if (state.type === "doc" && panX !== undefined && panY !== undefined) { + const init = state.initializers[id]; + if (!init) { + state.initializers[id] = { + panX, panY + }; + HistoryUtil.pushState(state); + } else if (init.panX !== panX || init.panY !== panY) { + init.panX = panX; + init.panY = panY; + HistoryUtil.pushState(state); + } + } + SelectionManager.DeselectAll(); + const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2; + const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2; + const newState = HistoryUtil.getState(); + newState.initializers[id] = { panX: newPanX, panY: newPanY }; + HistoryUtil.pushState(newState); + this.setPan(newPanX, newPanY); + this.props.Document.panTransformType = "Ease"; + this.props.focus(this.props.Document); + this.panDisposer = setTimeout(() => this.props.Document.panTransformType = "None", 2000); // wait 3 seconds, then reset to false + } + + + getDocumentViewProps(document: Doc): DocumentViewProps { + return { + Document: document, + toggleMinimized: emptyFunction, + addDocument: this.props.addDocument, + removeDocument: this.props.removeDocument, + moveDocument: this.props.moveDocument, + ScreenToLocalTransform: this.getTransform, + isTopMost: false, + selectOnLoad: document[Id] === this._selectOnLoaded, + PanelWidth: document[WidthSym], + PanelHeight: document[HeightSym], + ContentScaling: returnOne, + ContainingCollectionView: this.props.CollectionView, + focus: this.focusDocument, + parentActive: this.props.active, + whenActiveChanged: this.props.whenActiveChanged, + bringToFront: this.bringToFront, + addDocTab: this.props.addDocTab, + }; + } + + @computed.struct + get views() { + let curPage = FieldValue(this.Document.curPage, -1); + let docviews = this.children.reduce((prev, doc) => { + if (!(doc instanceof Doc)) return prev; + var page = NumCast(doc.page, -1); + if (page === curPage || page === -1) { + let minim = BoolCast(doc.isMinimized, false); + if (minim === undefined || !minim) { + prev.push(<CollectionFreeFormDocumentView key={doc[Id]} {...this.getDocumentViewProps(doc)} />); + } + } + return prev; + }, [] as JSX.Element[]); + + setTimeout(() => this._selectOnLoaded = "", 600);// bcz: surely there must be a better way .... + + return docviews; + } + + @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 ( + <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> + </MarqueeView> + <CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} {...this.props} /> + </div> + ); + } +} + +@observer +class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> { + @computed get overlayView() { + return (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"} + isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />); + } + render() { + return this.overlayView; + } +} + +@observer +class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> { + @computed get backgroundView() { + return (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"} + isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />); + } + render() { + return this.props.Document.backgroundLayout ? this.backgroundView : (null); + } +} + +interface CollectionFreeFormViewPannableContentsProps { + centeringShiftX: () => number; + centeringShiftY: () => number; + 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={freeformclass} style={{ borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}, ${zoom}) translate(${panx}px, ${pany}px)` }}> + {this.props.children} + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss new file mode 100644 index 000000000..6e8ec8662 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss @@ -0,0 +1,26 @@ + +.marqueeView { + position: inherit; + top:0; + left:0; + width:100%; + height:100%; +} +.marquee { + border-style: dashed; + box-sizing: border-box; + position: absolute; + border-width: 1px; + border-color: black; + pointer-events: none; + .marquee-legend { + bottom:-18px; + left:0; + position: absolute; + font-size: 9; + white-space:nowrap; + } + .marquee-legend::after { + content: "Press: c (collection), s (summary), r (replace) or Delete" + } +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx new file mode 100644 index 000000000..c3c4115b8 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -0,0 +1,367 @@ +import * as htmlToImage from "html-to-image"; +import { action, computed, observable } from "mobx"; +import { observer } from "mobx-react"; +import { Docs } from "../../../documents/Documents"; +import { SelectionManager } from "../../../util/SelectionManager"; +import { Transform } from "../../../util/Transform"; +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 { List } from "../../../../new_fields/List"; +import { ImageField } from "../../../../new_fields/URLField"; +import { Template, Templates } from "../../Templates"; +import { Gateway } from "../../../northstar/manager/Gateway"; +import { DocServer } from "../../../DocServer"; +import { Id } from "../../../../new_fields/RefField"; + +interface MarqueeViewProps { + getContainerTransform: () => Transform; + getTransform: () => Transform; + container: CollectionFreeFormView; + addDocument: (doc: Doc, allowDuplicates: false) => boolean; + activeDocuments: () => Doc[]; + selectDocuments: (docs: Doc[]) => void; + removeDocument: (doc: Doc) => boolean; + addLiveTextDocument: (doc: Doc) => void; + isSelected: () => boolean; +} + +@observer +export class MarqueeView extends React.Component<MarqueeViewProps> +{ + private _mainCont = React.createRef<HTMLDivElement>(); + @observable _lastX: number = 0; + @observable _lastY: number = 0; + @observable _downX: number = 0; + @observable _downY: number = 0; + @observable _visible: boolean = false; + _commandExecuted = false; + + @action + cleanupInteractions = (all: boolean = false) => { + if (all) { + document.removeEventListener("pointerup", this.onPointerUp, true); + document.removeEventListener("pointermove", this.onPointerMove, true); + } + document.removeEventListener("keydown", this.marqueeCommand, true); + this._visible = false; + } + + @undoBatch + @action + onKeyPress = (e: KeyboardEvent) => { + //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: string = await navigator.clipboard.readText(); + let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); + for (let i = 0; i < ns.length - 1; i++) { + while (!(ns[i].trim() === "" || ns[i].endsWith("-\r") || ns[i].endsWith("-") || + ns[i].endsWith(";\r") || ns[i].endsWith(";") || + ns[i].endsWith(".\r") || ns[i].endsWith(".") || + ns[i].endsWith(":\r") || ns[i].endsWith(":")) && i < ns.length - 1) { + let sub = ns[i].endsWith("\r") ? 1 : 0; + let br = ns[i + 1].trim() === ""; + ns.splice(i, 2, ns[i].substr(0, ns[i].length - sub) + ns[i + 1].trimLeft()); + if (br) break; + } + } + 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 if (e.key === "b" && e.ctrlKey) { + //heuristically converts pasted text into a table. + // assumes each entry is separated by a tab + // skips all rows until it gets to a row with more than one entry + // assumes that 1st row has header entry for each column + // assumes subsequent rows have entries for each column header OR + // any row that has only one column is a section header-- this header is then added as a column to subsequent rows until the next header + // assumes each cell is a string or a number + e.preventDefault(); + (async () => { + let text: string = await navigator.clipboard.readText(); + let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); + while (ns.length > 0 && ns[0].split("\t").length < 2) { + ns.splice(0, 1); + } + if (ns.length > 0) { + let columns = ns[0].split("\t"); + let docList: Doc[] = []; + let groupAttr: string | number = ""; + for (let i = 1; i < ns.length - 1; i++) { + let values = ns[i].split("\t"); + if (values.length === 1 && columns.length > 1) { + groupAttr = values[0]; + continue; + } + let doc = new Doc(); + columns.forEach((col, i) => doc[columns[i]] = (values.length > i ? ((values[i].indexOf(Number(values[i]).toString()) !== -1) ? Number(values[i]) : values[i]) : undefined)); + if (groupAttr) { + doc._group = groupAttr; + } + doc.title = i.toString(); + docList.push(doc); + } + let newCol = Docs.SchemaDocument([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 }); + + this.props.addDocument(newCol, false); + } + })(); + } else { + let newBox = Docs.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" }); + this.props.addLiveTextDocument(newBox); + } + e.stopPropagation(); + } + @action + onPointerDown = (e: React.PointerEvent): void => { + 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); + // bcz: do we need this? it kills the context menu on the main collection + // e.stopPropagation(); + } + if (e.altKey) { + e.preventDefault(); + } + } + + @action + onPointerMove = (e: PointerEvent): void => { + this._lastX = e.pageX; + this._lastY = e.pageY; + if (!e.cancelBubble) { + 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(); + } + } + if (e.altKey) { + e.preventDefault(); + } + } + + @action + onPointerUp = (e: PointerEvent): void => { + 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 }, + r2: { left: number, top: number, width: number, height: number }) { + return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top); + } + + @computed + get Bounds() { + let left = this._downX < this._lastX ? this._downX : this._lastX; + let top = this._downY < this._lastY ? this._downY : this._lastY; + let topLeft = this.props.getTransform().transformPoint(left, top); + let size = this.props.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); + return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) }; + } + + @undoBatch + @action + marqueeCommand = async (e: KeyboardEvent) => { + if (this._commandExecuted || (e as any).propagationIsStopped) { + return; + } + if (e.key === "Backspace" || e.key === "Delete" || e.key === "d") { + this._commandExecuted = true; + e.stopPropagation(); + (e as any).propagationIsStopped = true; + this.marqueeSelect().map(d => this.props.removeDocument(d)); + let ink = Cast(this.props.container.props.Document.ink, InkField); + if (ink) { + this.marqueeInkDelete(ink.inkData); + } + SelectionManager.DeselectAll(); + this.cleanupInteractions(false); + e.stopPropagation(); + } + if (e.key === "c" || e.key === "s" || e.key === "e" || e.key === "p") { + this._commandExecuted = true; + e.stopPropagation(); + (e as any).propagationIsStopped = true; + let bounds = this.Bounds; + let selected = this.marqueeSelect(); + if (e.key === "c") { + selected.map(d => { + this.props.removeDocument(d); + d.x = NumCast(d.x) - bounds.left - bounds.width / 2; + d.y = NumCast(d.y) - bounds.top - bounds.height / 2; + d.page = -1; + return d; + }); + } + 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, + 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: e.key === "s" ? "-summary-" : e.key === "p" ? "-summary-" : "a nested collection", + }); + this.marqueeInkDelete(inkData); + + if (e.key === "s" || e.key === "p") { + + htmlToImage.toPng(this._mainCont.current!, { width: bounds.width * zoomBasis, height: bounds.height * zoomBasis, quality: 1 }).then((dataUrl) => { + selected.map(d => { + this.props.removeDocument(d); + d.x = NumCast(d.x) - bounds.left - bounds.width / 2; + d.y = NumCast(d.y) - bounds.top - bounds.height / 2; + d.page = -1; + return d; + }); + let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" }); + summary.proto!.thumbnail = new ImageField(new URL(dataUrl)); + summary.proto!.templates = new List<string>([Templates.ImageOverlay(Math.min(50, bounds.width), bounds.height * Math.min(50, bounds.width) / bounds.width, "thumbnail")]); + newCollection.proto!.summaryDoc = summary; + selected = [newCollection]; + newCollection.x = bounds.left + bounds.width; + this.props.addDocument(newCollection, false); + summary.proto!.summarizedDocs = new List<Doc>(selected.map(s => s.proto!)); + //summary.proto!.maximizeOnRight = true; + //summary.proto!.isButton = true; + //let scrpt = this.props.getTransform().inverse().transformPoint(bounds.left, bounds.top); + // selected.map(summarizedDoc => { + // let maxx = NumCast(summarizedDoc.x, undefined); + // let maxy = NumCast(summarizedDoc.y, undefined); + // let maxw = NumCast(summarizedDoc.width, undefined); + // let maxh = NumCast(summarizedDoc.height, undefined); + // summarizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), 0]); + // }); + this.props.addLiveTextDocument(summary); + }); + } + else { + this.props.addDocument(newCollection, false); + SelectionManager.DeselectAll(); + this.props.selectDocuments([newCollection]); + } + this.cleanupInteractions(false); + } + } + @action + marqueeInkSelect(ink: Map<any, any>) { + let idata = new Map(); + let centerShiftX = 0 - (this.Bounds.left + this.Bounds.width / 2); // moves each point by the offset that shifts the selection's center to the origin. + let centerShiftY = 0 - (this.Bounds.top + this.Bounds.height / 2); + ink.forEach((value: StrokeData, key: string, map: any) => { + if (InkingCanvas.IntersectStrokeRect(value, this.Bounds)) { + idata.set(key, + { + pathData: value.pathData.map(val => ({ x: val.x + centerShiftX, y: val.y + centerShiftY })), + color: value.color, + width: value.width, + tool: value.tool, + page: -1 + }); + } + }); + return idata; + } + + @action + marqueeInkDelete(ink?: Map<any, any>) { + // bcz: this appears to work but when you restart all the deleted strokes come back -- InkField isn't observing its changes so they aren't written to the DB. + // ink.forEach((value: StrokeData, key: string, map: any) => + // InkingCanvas.IntersectStrokeRect(value, this.Bounds) && ink.delete(key)); + + if (ink) { + let idata = new Map(); + ink.forEach((value: StrokeData, key: string, map: any) => + !InkingCanvas.IntersectStrokeRect(value, this.Bounds) && idata.set(key, value)); + Doc.SetOnPrototype(this.props.container.props.Document, "ink", new InkField(idata)); + } + } + + marqueeSelect() { + let selRect = this.Bounds; + let selection: Doc[] = []; + this.props.activeDocuments().map(doc => { + 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); + } + }); + return selection; + } + + @computed + get marqueeDiv() { + let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); + return <div className="marquee" style={{ width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}`, zIndex: 2000 }} > + <span className="marquee-legend" /> + </div>; + } + + render() { + let p = this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); + return <div className="marqueeView" style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}> + <div style={{ position: "relative", transform: `translate(${p[0]}px, ${p[1]}px)` }} > + {!this._visible ? null : this.marqueeDiv} + <div ref={this._mainCont} style={{ transform: `translate(${-p[0]}px, ${-p[1]}px)` }} > + {this.props.children} + </div> + </div> + </div>; + } +}
\ No newline at end of file |
