diff options
| author | Tyler Schicke <tyler_schicke@brown.edu> | 2019-02-09 19:13:24 -0500 |
|---|---|---|
| committer | Tyler Schicke <tyler_schicke@brown.edu> | 2019-02-09 19:13:24 -0500 |
| commit | 11134bc5ce01d0a025d311a4f83e67ff6e63ce1c (patch) | |
| tree | e401d8004481b3d664c751ae2e668c72b6be7aac /src/client/views/collections | |
| parent | c06745a99ed85b215d0ae48bfb2af7c955f0b016 (diff) | |
Moved client code to client folder
Diffstat (limited to 'src/client/views/collections')
| -rw-r--r-- | src/client/views/collections/CollectionDockingView.scss | 336 | ||||
| -rw-r--r-- | src/client/views/collections/CollectionDockingView.tsx | 281 | ||||
| -rw-r--r-- | src/client/views/collections/CollectionFreeFormView.scss | 20 | ||||
| -rw-r--r-- | src/client/views/collections/CollectionFreeFormView.tsx | 205 | ||||
| -rw-r--r-- | src/client/views/collections/CollectionSchemaView.scss | 108 | ||||
| -rw-r--r-- | src/client/views/collections/CollectionSchemaView.tsx | 144 | ||||
| -rw-r--r-- | src/client/views/collections/CollectionViewBase.tsx | 57 |
7 files changed, 1151 insertions, 0 deletions
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss new file mode 100644 index 000000000..7c0b512a7 --- /dev/null +++ b/src/client/views/collections/CollectionDockingView.scss @@ -0,0 +1,336 @@ +.collectiondockingview-container { + position: relative; + top: 0; + left: 0; + overflow: hidden; + .lm_controls>li { + opacity: 0.6; + transform: scale(1.2); + } + .lm_maximised .lm_controls .lm_maximise { + opacity: 1; + transform: scale(0.8); + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAKElEQVR4nGP8////fwYCgImQAgYGBgYWKM2IR81/okwajIpgvsMbVgAwgQYRVakEKQAAAABJRU5ErkJggg==) !important; + } + .flexlayout__layout { + left: 0; + top: 0; + right: 0; + bottom: 0; + position: absolute; + overflow: hidden; + } + .flexlayout__splitter { + background-color: black; + } + .flexlayout__splitter:hover { + background-color: #333; + } + .flexlayout__splitter_drag { + border-radius: 5px; + background-color: #444; + z-index: 1000; + } + .flexlayout__outline_rect { + position: absolute; + cursor: move; + border: 2px solid #cfe8ff; + box-shadow: inset 0 0 60px rgba(0, 0, 0, .2); + border-radius: 5px; + z-index: 1000; + box-sizing: border-box; + } + .flexlayout__outline_rect_edge { + cursor: move; + border: 2px solid #b7d1b5; + box-shadow: inset 0 0 60px rgba(0, 0, 0, .2); + border-radius: 5px; + z-index: 1000; + box-sizing: border-box; + } + .flexlayout__edge_rect { + position: absolute; + z-index: 1000; + box-shadow: inset 0 0 5px rgba(0, 0, 0, .2); + background-color: lightgray; + } + .flexlayout__drag_rect { + position: absolute; + cursor: move; + border: 2px solid #aaaaaa; + box-shadow: inset 0 0 60px rgba(0, 0, 0, .3); + border-radius: 5px; + z-index: 1000; + box-sizing: border-box; + background-color: #eeeeee; + opacity: 0.9; + text-align: center; + display: flex; + justify-content: center; + flex-direction: column; + overflow: hidden; + padding: 10px; + word-wrap: break-word; + } + .flexlayout__tabset { + overflow: hidden; + background-color: #222; + box-sizing: border-box; + } + .flexlayout__tab { + overflow: auto; + position: absolute; + box-sizing: border-box; + background-color: #222; + color: black; + } + .flexlayout__tab_button { + cursor: pointer; + padding: 2px 8px 3px 8px; + margin: 2px; + /*box-shadow: inset 0px 0px 5px rgba(0, 0, 0, .15);*/ + /*border-top-left-radius: 3px;*/ + /*border-top-right-radius: 3px;*/ + float: left; + vertical-align: top; + box-sizing: border-box; + } + .flexlayout__tab_button--selected { + color: #ddd; + background-color: #222; + } + .flexlayout__tab_button--unselected { + color: gray; + } + .flexlayout__tab_button_leading { + display: inline-block; + } + .flexlayout__tab_button_content { + display: inline-block; + } + .flexlayout__tab_button_textbox { + float: left; + border: none; + color: lightgreen; + background-color: #222; + } + .flexlayout__tab_button_textbox:focus { + outline: none; + } + .flexlayout__tab_button_trailing { + display: inline-block; + margin-left: 5px; + margin-top: 3px; + width: 8px; + height: 8px; + } + .flexlayout__tab_button:hover .flexlayout__tab_button_trailing, + .flexlayout__tab_button--selected .flexlayout__tab_button_trailing { + background: transparent url("../../../../node_modules/flexlayout-react/images/close_white.png") no-repeat center; + } + .flexlayout__tab_button_overflow { + float: left; + width: 20px; + height: 15px; + margin-top: 2px; + padding-left: 12px; + border: none; + font-size: 10px; + color: lightgray; + font-family: Arial, sans-serif; + background: transparent url("../../../../node_modules/flexlayout-react/images/more.png") no-repeat left; + } + .flexlayout__tabset_header { + position: absolute; + left: 0; + right: 0; + color: #eee; + background-color: #212121; + padding: 3px 3px 3px 5px; + /*box-shadow: inset 0px 0px 3px 0px rgba(136, 136, 136, 0.54);*/ + box-sizing: border-box; + } + .flexlayout__tab_header_inner { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 10000px; + } + .flexlayout__tab_header_outer { + background-color: black; + position: absolute; + left: 0; + right: 0; + /*top: 0px;*/ + /*height: 100px;*/ + overflow: hidden; + } + .flexlayout__tabset-selected { + background-image: linear-gradient(#111, #444); + } + .flexlayout__tabset-maximized { + background-image: linear-gradient(#666, #333); + } + .flexlayout__tab_toolbar { + position: absolute; + display: flex; + flex-direction: row-reverse; + align-items: center; + top: 0; + bottom: 0; + right: 0; + } + .flexlayout__tab_toolbar_button-min { + width: 20px; + height: 20px; + border: none; + outline-width: 0; + background: transparent url("../../../../node_modules/flexlayout-react/images/maximize.png") no-repeat center; + } + .flexlayout__tab_toolbar_button-max { + width: 20px; + height: 20px; + border: none; + outline-width: 0; + background: transparent url("../../../../node_modules/flexlayout-react/images/restore.png") no-repeat center; + } + .flexlayout__popup_menu {} + .flexlayout__popup_menu_item { + padding: 2px 10px 2px 10px; + color: #ddd; + } + .flexlayout__popup_menu_item:hover { + background-color: #444444; + } + .flexlayout__popup_menu_container { + box-shadow: inset 0 0 5px rgba(0, 0, 0, .15); + border: 1px solid #555; + background: #222; + border-radius: 3px; + position: absolute; + z-index: 1000; + } + .flexlayout__border_top { + background-color: black; + border-bottom: 1px solid #ddd; + box-sizing: border-box; + overflow: hidden; + } + .flexlayout__border_bottom { + background-color: black; + border-top: 1px solid #333; + box-sizing: border-box; + overflow: hidden; + } + .flexlayout__border_left { + background-color: black; + border-right: 1px solid #333; + box-sizing: border-box; + overflow: hidden; + } + .flexlayout__border_right { + background-color: black; + border-left: 1px solid #333; + box-sizing: border-box; + overflow: hidden; + } + .flexlayout__border_inner_bottom { + display: flex; + } + .flexlayout__border_inner_left { + position: absolute; + white-space: nowrap; + right: 23px; + transform-origin: top right; + transform: rotate(-90deg); + } + .flexlayout__border_inner_right { + position: absolute; + white-space: nowrap; + left: 23px; + transform-origin: top left; + transform: rotate(90deg); + } + .flexlayout__border_button { + background-color: #222; + color: white; + display: inline-block; + white-space: nowrap; + cursor: pointer; + padding: 2px 8px 3px 8px; + margin: 2px; + vertical-align: top; + box-sizing: border-box; + } + .flexlayout__border_button--selected { + color: #ddd; + background-color: #222; + } + .flexlayout__border_button--unselected { + color: gray; + } + .flexlayout__border_button_leading { + float: left; + display: inline; + } + .flexlayout__border_button_content { + display: inline-block; + } + .flexlayout__border_button_textbox { + float: left; + border: none; + color: green; + background-color: #ddd; + } + .flexlayout__border_button_textbox:focus { + outline: none; + } + .flexlayout__border_button_trailing { + display: inline-block; + margin-left: 5px; + margin-top: 3px; + width: 8px; + height: 8px; + } + .flexlayout__border_button:hover .flexlayout__border_button_trailing, + .flexlayout__border_button--selected .flexlayout__border_button_trailing { + background: transparent url("../../../../node_modules/flexlayout-react/images/close_white.png") no-repeat center; + } + .flexlayout__border_toolbar_left { + position: absolute; + display: flex; + flex-direction: column-reverse; + align-items: center; + bottom: 0; + left: 0; + right: 0; + } + .flexlayout__border_toolbar_right { + position: absolute; + display: flex; + flex-direction: column-reverse; + align-items: center; + bottom: 0; + left: 0; + right: 0; + } + .flexlayout__border_toolbar_top { + position: absolute; + display: flex; + flex-direction: row-reverse; + align-items: center; + top: 0; + bottom: 0; + right: 0; + } + .flexlayout__border_toolbar_bottom { + position: absolute; + display: flex; + flex-direction: row-reverse; + align-items: center; + top: 0; + bottom: 0; + right: 0; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx new file mode 100644 index 000000000..9aee9c10f --- /dev/null +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -0,0 +1,281 @@ +import { observer } from "mobx-react"; +import { KeyStore } from "../../../fields/Key"; +import React = require("react"); +import FlexLayout from "flexlayout-react"; +import { action, observable, computed } from "mobx"; +import { Document } from "../../../fields/Document"; +import { DocumentView } from "../nodes/DocumentView"; +import { ListField } from "../../../fields/ListField"; +import { NumberField } from "../../../fields/NumberField"; +import "./CollectionDockingView.scss" +import 'golden-layout/src/css/goldenlayout-base.css'; +import 'golden-layout/src/css/goldenlayout-dark-theme.css'; +import * as GoldenLayout from "golden-layout"; +import * as ReactDOM from 'react-dom'; +import { DragManager } from "../../util/DragManager"; +import { CollectionViewBase, CollectionViewProps, COLLECTION_BORDER_WIDTH } from "./CollectionViewBase"; + +@observer +export class CollectionDockingView extends CollectionViewBase { + + private static UseGoldenLayout = true; + public static LayoutString() { return CollectionViewBase.LayoutString("CollectionDockingView"); } + private _containerRef = React.createRef<HTMLDivElement>(); + @computed + private get modelForFlexLayout() { + const { CollectionFieldKey: fieldKey, DocumentForCollection: Document } = this.props; + const value: Document[] = Document.GetData(fieldKey, ListField, []); + var docs = value.map(doc => { + return { type: 'tabset', weight: 50, selected: 0, children: [{ type: "tab", name: doc.Title, component: doc.Id }] }; + }); + return FlexLayout.Model.fromJson({ + global: {}, borders: [], + layout: { + "type": "row", + "weight": 100, + "children": docs + } + }); + } + @computed + private get modelForGoldenLayout(): any { + const { CollectionFieldKey: fieldKey, DocumentForCollection: Document } = this.props; + const value: Document[] = Document.GetData(fieldKey, ListField, []); + var docs = value.map(doc => { + return { type: 'component', componentName: 'documentViewComponent', componentState: { doc: doc } }; + }); + return new GoldenLayout({ + settings: { + selectionEnabled: true + }, content: [{ type: 'row', content: docs }] + }); + } + constructor(props: CollectionViewProps) { + super(props); + } + + componentDidMount: () => void = () => { + if (this._containerRef.current && CollectionDockingView.UseGoldenLayout) { + this.goldenLayoutFactory(); + window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window + } + } + componentWillUnmount: () => void = () => { + window.removeEventListener('resize', this.onResize); + } + private nextId = (function () { var _next_id = 0; return function () { return _next_id++; } })(); + + + @action + onResize = (event: any) => { + var cur = this.props.ContainingDocumentView!.MainContent.current; + + // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed + CollectionDockingView.myLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height); + } + + @action + onPointerDown = (e: React.PointerEvent): void => { + if (e.button === 2 && this.active) { + e.stopPropagation(); + e.preventDefault(); + } else { + if (e.buttons === 1 && this.active) { + e.stopPropagation(); + } + } + } + + flexLayoutFactory = (node: any): any => { + var component = node.getComponent(); + if (component === "button") { + return <button>{node.getName()}</button>; + } + const { CollectionFieldKey: fieldKey, DocumentForCollection: Document } = this.props; + const value: Document[] = Document.GetData(fieldKey, ListField, []); + for (var i: number = 0; i < value.length; i++) { + if (value[i].Id === component) { + return (<DocumentView key={value[i].Id} ContainingCollectionView={this} Document={value[i]} DocumentView={undefined} />); + } + } + if (component === "text") { + return (<div className="panel">Panel {node.getName()}</div>); + } + } + + public static myLayout: any = null; + + private static _dragDiv: any = null; + private static _dragParent: HTMLElement | null = null; + private static _dragElement: HTMLDivElement; + private static _dragFakeElement: HTMLDivElement; + public static StartOtherDrag(dragElement: HTMLDivElement, dragDoc: Document) { + var newItemConfig = { + type: 'component', + componentName: 'documentViewComponent', + componentState: { doc: dragDoc } + }; + this._dragElement = dragElement; + this._dragParent = dragElement.parentElement; + // bcz: we want to copy this document into the header, not move it there. + // However, GoldenLayout is setup to move things, so we have to do some kludgy stuff: + + // - create a temporary invisible div and register that as a DragSource with GoldenLayout + this._dragDiv = document.createElement("div"); + this._dragDiv.style.opacity = 0; + DragManager.Root().appendChild(this._dragDiv); + CollectionDockingView.myLayout.createDragSource(this._dragDiv, newItemConfig); + + // - add our document to that div so that GoldenLayout will get the move events its listening for + this._dragDiv.appendChild(this._dragElement); + + // - add a duplicate of our document to the original document's container + // (GoldenLayout will be removing our original one) + this._dragFakeElement = dragElement.cloneNode(true) as HTMLDivElement; + this._dragParent!.appendChild(this._dragFakeElement); + + // all of this must be undone when the document has been dropped (see tabCreated) + } + + _makeFullScreen: boolean = false; + _maximizedStack: any = null; + public static OpenFullScreen(document: Document) { + var newItemConfig = { + type: 'component', + componentName: 'documentViewComponent', + componentState: { doc: document } + }; + CollectionDockingView.myLayout._makeFullScreen = true; + CollectionDockingView.myLayout.root.contentItems[0].addChild(newItemConfig); + } + public static CloseFullScreen() { + if (CollectionDockingView.myLayout._maximizedStack != null) { + CollectionDockingView.myLayout._maximizedStack.header.controlsContainer.find('.lm_close').click(); + CollectionDockingView.myLayout._maximizedStack = null; + } + } + + // + // Creates a vertical split on the right side of the docking view, and then adds the Document to that split + // + public static AddRightSplit(document: Document) { + var newItemConfig = { + type: 'component', + componentName: 'documentViewComponent', + componentState: { doc: document } + } + let newItemStackConfig = { + type: 'stack', + content: [newItemConfig] + }; + var newContentItem = new CollectionDockingView.myLayout._typeToItem[newItemStackConfig.type](CollectionDockingView.myLayout, newItemStackConfig, parent); + + if (CollectionDockingView.myLayout.root.contentItems[0].isRow) { + var rowlayout = CollectionDockingView.myLayout.root.contentItems[0]; + var lastRowItem = rowlayout.contentItems[rowlayout.contentItems.length - 1]; + + lastRowItem.config["width"] *= 0.5; + newContentItem.config["width"] = lastRowItem.config["width"]; + rowlayout.addChild(newContentItem, rowlayout.contentItems.length, true); + rowlayout.callDownwards('setSize'); + } + else { + var collayout = CollectionDockingView.myLayout.root.contentItems[0]; + var newRow = collayout.layoutManager.createContentItem({ type: "row" }, CollectionDockingView.myLayout); + collayout.parent.replaceChild(collayout, newRow); + + newRow.addChild(newContentItem, undefined, true); + newRow.addChild(collayout, 0, true); + + collayout.config["width"] = 50; + newContentItem.config["width"] = 50; + collayout.parent.callDownwards('setSize'); + } + } + goldenLayoutFactory() { + CollectionDockingView.myLayout = this.modelForGoldenLayout; + + var layout = CollectionDockingView.myLayout; + CollectionDockingView.myLayout.on('tabCreated', function (tab: any) { + if (CollectionDockingView._dragDiv) { + CollectionDockingView._dragDiv.removeChild(CollectionDockingView._dragElement); + CollectionDockingView._dragParent!.removeChild(CollectionDockingView._dragFakeElement); + CollectionDockingView._dragParent!.appendChild(CollectionDockingView._dragElement); + DragManager.Root().removeChild(CollectionDockingView._dragDiv); + CollectionDockingView._dragDiv = null; + } + tab.setTitle(tab.contentItem.config.componentState.doc.Title); + tab.closeElement.off('click') //unbind the current click handler + .click(function () { + tab.contentItem.remove(); + }); + }); + + CollectionDockingView.myLayout.on('stackCreated', function (stack: any) { + if (CollectionDockingView.myLayout._makeFullScreen) { + CollectionDockingView.myLayout._maximizedStack = stack; + CollectionDockingView.myLayout._maxstack = stack.header.controlsContainer.find('.lm_maximise'); + } + //stack.header.controlsContainer.find('.lm_popout').hide(); + stack.header.controlsContainer.find('.lm_close') //get the close icon + .off('click') //unbind the current click handler + .click(function () { + //if (confirm('really close this?')) { + stack.remove(); + //} + }); + }); + + var me = this; + CollectionDockingView.myLayout.registerComponent('documentViewComponent', function (container: any, state: any) { + // bcz: this is crufty + // calling html() causes a div tag to be added in the DOM with id 'containingDiv'. + // Apparently, we need to wait to allow a live html div element to actually be instantiated. + // After a timeout, we lookup the live html div element and add our React DocumentView to it. + var containingDiv = "component_" + me.nextId(); + container.getElement().html("<div id='" + containingDiv + "'></div>"); + setTimeout(function () { + ReactDOM.render(( + <DocumentView key={state.doc.Id} Document={state.doc} ContainingCollectionView={me} DocumentView={undefined} /> + ), + document.getElementById(containingDiv) + ); + if (CollectionDockingView.myLayout._maxstack != null) { + CollectionDockingView.myLayout._maxstack.click(); + } + }, 0); + }); + CollectionDockingView.myLayout.container = this._containerRef.current; + CollectionDockingView.myLayout.init(); + } + + + render() { + const { CollectionFieldKey: fieldKey, DocumentForCollection: Document } = this.props; + const value: Document[] = Document.GetData(fieldKey, ListField, []); + // bcz: not sure why, but I need these to force the flexlayout to update when the collection size changes. + var s = this.props.ContainingDocumentView != undefined ? this.props.ContainingDocumentView!.ScalingToScreenSpace : 1; + var w = Document.GetData(KeyStore.Width, NumberField, Number(0)) / s; + var h = Document.GetData(KeyStore.Height, NumberField, Number(0)) / s; + + var chooseLayout = () => { + if (!CollectionDockingView.UseGoldenLayout) + return <FlexLayout.Layout model={this.modelForFlexLayout} factory={this.flexLayoutFactory} />; + } + + return ( + <div className="border" style={{ + borderStyle: "solid", + borderWidth: `${COLLECTION_BORDER_WIDTH}px`, + }}> + <div className="collectiondockingview-container" id="menuContainer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} ref={this._containerRef} + style={{ + width: CollectionDockingView.UseGoldenLayout || s > 1 ? "100%" : w - 2 * COLLECTION_BORDER_WIDTH, + height: CollectionDockingView.UseGoldenLayout || s > 1 ? "100%" : h - 2 * COLLECTION_BORDER_WIDTH + }} > + {chooseLayout()} + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionFreeFormView.scss b/src/client/views/collections/CollectionFreeFormView.scss new file mode 100644 index 000000000..e9d134e7b --- /dev/null +++ b/src/client/views/collections/CollectionFreeFormView.scss @@ -0,0 +1,20 @@ +.collectionfreeformview-container { + position: relative; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + .collectionfreeformview { + position: absolute; + top: 0; + left: 0; + } +} + +.border { + border-style: solid; + box-sizing: border-box; + width: 100%; + height: 100%; +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx new file mode 100644 index 000000000..9cf29d000 --- /dev/null +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -0,0 +1,205 @@ +import { observer } from "mobx-react"; +import React = require("react"); +import { action, observable, computed } from "mobx"; +import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; +import { DragManager } from "../../util/DragManager"; +import "./CollectionFreeFormView.scss"; +import { Utils } from "../../../Utils"; +import { CollectionViewBase, CollectionViewProps, COLLECTION_BORDER_WIDTH } from "./CollectionViewBase"; +import { SelectionManager } from "../../util/SelectionManager"; +import { Key, KeyStore } from "../../../fields/Key"; +import { Document } from "../../../fields/Document"; +import { ListField } from "../../../fields/ListField"; +import { NumberField } from "../../../fields/NumberField"; +import { Documents } from "../../documents/Documents"; +import { FieldWaiting } from "../../../fields/Field"; + +@observer +export class CollectionFreeFormView extends CollectionViewBase { + public static LayoutString() { return CollectionViewBase.LayoutString("CollectionFreeFormView"); } + private _containerRef = React.createRef<HTMLDivElement>(); + private _canvasRef = React.createRef<HTMLDivElement>(); + private _nodeContainerRef = React.createRef<HTMLDivElement>(); + private _lastX: number = 0; + private _lastY: number = 0; + + constructor(props: CollectionViewProps) { + super(props); + } + + @action + drop = (e: Event, de: DragManager.DropEvent) => { + const doc = de.data["document"]; + var me = this; + if (doc instanceof CollectionFreeFormDocumentView) { + if (doc.props.ContainingCollectionView && doc.props.ContainingCollectionView !== this) { + doc.props.ContainingCollectionView.removeDocument(doc.props.Document); + this.addDocument(doc.props.Document); + } + const xOffset = de.data["xOffset"] as number || 0; + const yOffset = de.data["yOffset"] as number || 0; + const { scale, translateX, translateY } = Utils.GetScreenTransform(this._canvasRef.current!); + let sscale = this.props.ContainingDocumentView!.props.Document.GetData(KeyStore.Scale, NumberField, Number(1)) + const screenX = de.x - xOffset; + const screenY = de.y - yOffset; + const docX = (screenX - translateX) / sscale / scale; + const docY = (screenY - translateY) / sscale / scale; + doc.x = docX; + doc.y = docY; + this.bringToFront(doc); + } + e.stopPropagation(); + } + + componentDidMount() { + if (this._containerRef.current) { + DragManager.MakeDropTarget(this._containerRef.current, { + handlers: { + drop: this.drop + } + }); + } + } + + @action + onPointerDown = (e: React.PointerEvent): void => { + if ((e.button === 2 && this.active) || + !e.defaultPrevented) { + 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; + } + } + + @action + onPointerUp = (e: PointerEvent): void => { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + e.stopPropagation(); + SelectionManager.DeselectAll(); + } + + @action + onPointerMove = (e: PointerEvent): void => { + var me = this; + if (!e.cancelBubble && this.active) { + e.preventDefault(); + e.stopPropagation(); + let currScale: number = this.props.ContainingDocumentView!.ScalingToScreenSpace; + let x = this.props.DocumentForCollection.GetData(KeyStore.PanX, NumberField, Number(0)); + let y = this.props.DocumentForCollection.GetData(KeyStore.PanY, NumberField, Number(0)); + this.props.DocumentForCollection.SetData(KeyStore.PanX, x + (e.pageX - this._lastX) / currScale, NumberField); + this.props.DocumentForCollection.SetData(KeyStore.PanY, y + (e.pageY - this._lastY) / currScale, NumberField); + } + this._lastX = e.pageX; + this._lastY = e.pageY; + } + + @action + onPointerWheel = (e: React.WheelEvent): void => { + e.stopPropagation(); + + let { LocalX, Ss, Panxx, Xx, LocalY, Panyy, Yy, ContainerX, ContainerY } = this.props.ContainingDocumentView!.TransformToLocalPoint(e.pageX, e.pageY); + + var deltaScale = (1 - (e.deltaY / 1000)) * Ss; + + var newContainerX = LocalX * deltaScale + Panxx + Xx; + var newContainerY = LocalY * deltaScale + Panyy + Yy; + + let dx = ContainerX - newContainerX; + let dy = ContainerY - newContainerY; + + this.props.DocumentForCollection.Set(KeyStore.Scale, new NumberField(deltaScale)); + this.props.DocumentForCollection.SetData(KeyStore.PanX, Panxx + dx, NumberField); + this.props.DocumentForCollection.SetData(KeyStore.PanY, Panyy + dy, NumberField); + } + + @action + onDrop = (e: React.DragEvent): void => { + e.stopPropagation() + e.preventDefault() + let fReader = new FileReader() + let file = e.dataTransfer.items[0].getAsFile(); + let that = this; + const panx: number = this.props.DocumentForCollection.GetData(KeyStore.PanX, NumberField, Number(0)); + const pany: number = this.props.DocumentForCollection.GetData(KeyStore.PanY, NumberField, Number(0)); + let x = e.pageX - panx + let y = e.pageY - pany + + fReader.addEventListener("load", action("drop", (event) => { + if (fReader.result) { + let url = "" + fReader.result; + let doc = Documents.ImageDocument(url, { + x: x, y: y + }) + let docs = that.props.DocumentForCollection.GetT(KeyStore.Data, ListField); + if (docs != FieldWaiting) { + if (!docs) { + docs = new ListField<Document>(); + that.props.DocumentForCollection.Set(KeyStore.Data, docs) + } + docs.Data.push(doc); + } + } + }), false) + + if (file) { + fReader.readAsDataURL(file) + } + } + + onDragOver = (e: React.DragEvent): void => { + } + + @action + bringToFront(doc: CollectionFreeFormDocumentView) { + const { CollectionFieldKey: fieldKey, DocumentForCollection: Document } = this.props; + + const value: Document[] = Document.GetList<Document>(fieldKey, []); + var topmost = value.reduce((topmost, d) => Math.max(d.GetNumber(KeyStore.ZIndex, 0), topmost), -1000); + value.map(d => { + var zind = d.GetNumber(KeyStore.ZIndex, 0); + if (zind != topmost - 1 - (topmost - zind) && d != doc.props.Document) { + d.SetData(KeyStore.ZIndex, topmost - 1 - (topmost - zind), NumberField); + } + }) + + if (doc.props.Document.GetNumber(KeyStore.ZIndex, 0) != 0) { + doc.props.Document.SetData(KeyStore.ZIndex, 0, NumberField); + } + } + + render() { + const { CollectionFieldKey: fieldKey, DocumentForCollection: Document } = this.props; + const value: Document[] = Document.GetList<Document>(fieldKey, []); + const panx: number = Document.GetNumber(KeyStore.PanX, 0); + const pany: number = Document.GetNumber(KeyStore.PanY, 0); + const currScale: number = Document.GetNumber(KeyStore.Scale, 1); + + return ( + <div className="border" style={{ + borderWidth: `${COLLECTION_BORDER_WIDTH}px`, + }}> + <div className="collectionfreeformview-container" + onPointerDown={this.onPointerDown} + onWheel={this.onPointerWheel} + onContextMenu={(e) => e.preventDefault()} + onDrop={this.onDrop} + onDragOver={this.onDragOver} + ref={this._containerRef}> + <div className="collectionfreeformview" style={{ transform: `translate(${panx}px, ${pany}px) scale(${currScale}, ${currScale})`, transformOrigin: `left, top` }} ref={this._canvasRef}> + + <div className="node-container" ref={this._nodeContainerRef}> + {value.map(doc => { + return (<CollectionFreeFormDocumentView key={doc.Id} ContainingCollectionView={this} Document={doc} DocumentView={undefined} />); + })} + </div> + </div> + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss new file mode 100644 index 000000000..707b44db6 --- /dev/null +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -0,0 +1,108 @@ +.Resizer { + box-sizing: border-box; + background: #000; + opacity: 0.5; + z-index: 1; + background-clip: padding-box; + &.horizontal { + height: 11px; + margin: -5px 0; + border-top: 5px solid rgba(255, 255, 255, 0); + border-bottom: 5px solid rgba(255, 255, 255, 0); + cursor: row-resize; + width: 100%; + &:hover { + border-top: 5px solid rgba(0, 0, 0, 0.5); + border-bottom: 5px solid rgba(0, 0, 0, 0.5); + } + } + &.vertical { + width: 11px; + margin: 0 -5px; + border-left: 5px solid rgba(255, 255, 255, 0); + border-right: 5px solid rgba(255, 255, 255, 0); + cursor: col-resize; + &:hover { + border-left: 5px solid rgba(0, 0, 0, 0.5); + border-right: 5px solid rgba(0, 0, 0, 0.5); + } + } + &:hover { + -webkit-transition: all 2s ease; + transition: all 2s ease; + } +} + +.vertical { + section { + width: 100vh; + height: 100vh; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + } + header { + padding: 1rem; + background: #eee; + } + footer { + padding: 1rem; + background: #eee; + } +} + +.horizontal { + section { + width: 100vh; + height: 100vh; + display: flex; + flex-direction: column; + } + header { + padding: 1rem; + background: #eee; + } + footer { + padding: 1rem; + background: #eee; + } +} + +.parent { + width: 100%; + height: 100%; + -webkit-box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; +} + +.header { + background: #aaa; + height: 3rem; + line-height: 3rem; +} + +.wrapper { + background: #ffa; + margin: 5rem; + -webkit-box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx new file mode 100644 index 000000000..2d5bd6c99 --- /dev/null +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -0,0 +1,144 @@ +import React = require("react") +import ReactTable, { ReactTableDefaults, CellInfo, ComponentPropsGetterRC, ComponentPropsGetterR } from "react-table"; +import { observer } from "mobx-react"; +import { FieldView, FieldViewProps } from "../nodes/FieldView"; +import "react-table/react-table.css" +import { observable, action, computed } from "mobx"; +import SplitPane from "react-split-pane" +import "./CollectionSchemaView.scss" +import { ScrollBox } from "../../util/ScrollBox"; +import { CollectionViewBase } from "./CollectionViewBase"; +import { DocumentView } from "../nodes/DocumentView"; +import { EditableView } from "../EditableView"; +import { CompileScript, ToField } from "../../util/Scripting"; +import { KeyStore as KS, Key } from "../../../fields/Key"; +import { Document } from "../../../fields/Document"; +import { Field } from "../../../fields/Field"; + +@observer +export class CollectionSchemaView extends CollectionViewBase { + public static LayoutString() { return CollectionViewBase.LayoutString("CollectionSchemaView"); } + + @observable + selectedIndex = 0; + + renderCell = (rowProps: CellInfo) => { + let props: FieldViewProps = { + doc: rowProps.value[0], + fieldKey: rowProps.value[1], + DocumentViewForField: undefined + } + let contents = ( + <FieldView {...props} /> + ) + return ( + <EditableView contents={contents} GetValue={() => { + let field = props.doc.Get(props.fieldKey); + if (field && field instanceof Field) { + return field.ToScriptString(); + } + return field || ""; + }} SetValue={(value: string) => { + let script = CompileScript(value); + 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 false; + }}></EditableView> + ) + } + + private getTrProps: ComponentPropsGetterR = (state, rowInfo) => { + const that = this; + if (!rowInfo) { + return {}; + } + return { + onClick: action((e: React.MouseEvent, handleOriginal: Function) => { + that.selectedIndex = rowInfo.index; + const doc: Document = rowInfo.original; + console.log("Row clicked: ", doc.Title) + + if (handleOriginal) { + handleOriginal() + } + }), + style: { + background: rowInfo.index == this.selectedIndex ? "#00afec" : "white", + color: rowInfo.index == this.selectedIndex ? "white" : "black" + } + }; + } + + onPointerDown = (e: React.PointerEvent) => { + let target = e.target as HTMLElement; + if (target.tagName == "SPAN" && target.className.includes("Resizer")) { + e.stopPropagation(); + } + if (e.button === 2 && this.active) { + e.stopPropagation(); + e.preventDefault(); + } else { + if (e.buttons === 1 && this.active) { + e.stopPropagation(); + } + } + } + + render() { + const { DocumentForCollection: Document, CollectionFieldKey: fieldKey } = this.props; + const children = Document.GetList<Document>(fieldKey, []); + const columns = Document.GetList(KS.ColumnsKey, + [KS.Title, KS.Data, KS.Author]) + let content; + if (this.selectedIndex != -1) { + content = ( + <DocumentView Document={children[this.selectedIndex]} DocumentView={undefined} ContainingCollectionView={this} /> + ) + } else { + content = <div /> + } + return ( + <div onPointerDown={this.onPointerDown} > + <SplitPane split={"vertical"} defaultSize="60%"> + <ScrollBox> + <ReactTable + data={children} + pageSize={children.length} + page={0} + showPagination={false} + style={{ + display: "inline-block" + }} + columns={columns.map(col => { + return ( + { + Header: col.Name, + accessor: (doc: Document) => [doc, col], + id: col.Id + }) + })} + column={{ + ...ReactTableDefaults.column, + Cell: this.renderCell + }} + getTrProps={this.getTrProps} + /> + </ScrollBox> + {content} + </SplitPane> + </div> + ) + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx new file mode 100644 index 000000000..09e8ec729 --- /dev/null +++ b/src/client/views/collections/CollectionViewBase.tsx @@ -0,0 +1,57 @@ +import { action, computed } from "mobx"; +import { observer } from "mobx-react"; +import { Document } from "../../../fields/Document"; +import { Opt } from "../../../fields/Field"; +import { Key, KeyStore } from "../../../fields/Key"; +import { ListField } from "../../../fields/ListField"; +import { SelectionManager } from "../../util/SelectionManager"; +import { ContextMenu } from "../ContextMenu"; +import React = require("react"); +import { DocumentView } from "../nodes/DocumentView"; +import { CollectionDockingView } from "./CollectionDockingView"; +import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; + + +export interface CollectionViewProps { + CollectionFieldKey: Key; + DocumentForCollection: Document; + ContainingDocumentView: Opt<DocumentView>; +} + +export const COLLECTION_BORDER_WIDTH = 2; + +@observer +export class CollectionViewBase extends React.Component<CollectionViewProps> { + + public static LayoutString(collectionType: string) { + return `<${collectionType} DocumentForCollection={Document} CollectionFieldKey={DataKey} ContainingDocumentView={DocumentView}/>`; + } + @computed + public get active(): boolean { + var isSelected = (this.props.ContainingDocumentView instanceof CollectionFreeFormDocumentView && SelectionManager.IsSelected(this.props.ContainingDocumentView)); + var childSelected = SelectionManager.SelectedDocuments().some(view => view.props.ContainingCollectionView == this); + var topMost = this.props.ContainingDocumentView != undefined && ( + this.props.ContainingDocumentView.props.ContainingCollectionView == undefined || + this.props.ContainingDocumentView.props.ContainingCollectionView instanceof CollectionDockingView); + return isSelected || childSelected || topMost; + } + @action + addDocument = (doc: Document): void => { + //TODO This won't create the field if it doesn't already exist + const value = this.props.DocumentForCollection.GetData(this.props.CollectionFieldKey, ListField, new Array<Document>()) + value.push(doc); + } + + @action + removeDocument = (doc: Document): void => { + //TODO This won't create the field if it doesn't already exist + const value = this.props.DocumentForCollection.GetData(this.props.CollectionFieldKey, ListField, new Array<Document>()) + if (value.indexOf(doc) !== -1) { + value.splice(value.indexOf(doc), 1) + + SelectionManager.DeselectAll() + ContextMenu.Instance.clearItems() + } + } + +}
\ No newline at end of file |
