diff options
| author | Andrew Kim <andrewdkim@users.noreply.github.com> | 2019-03-05 18:51:20 -0500 |
|---|---|---|
| committer | Andrew Kim <andrewdkim@users.noreply.github.com> | 2019-03-05 18:51:20 -0500 |
| commit | 7f93e6639e8fee3e3760d13c69d65b343875091a (patch) | |
| tree | d29b45310f92a53935177d969ce3c1bee9920c32 /src/client/views/collections | |
| parent | 9b839a93b98b850aa77087218d4862b97fb24d15 (diff) | |
| parent | 2cc5eb6ff512dc6128d25903bcb852f25bcadcca (diff) | |
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web into PDFNode
Diffstat (limited to 'src/client/views/collections')
| -rw-r--r-- | src/client/views/collections/CollectionDockingView.scss | 340 | ||||
| -rw-r--r-- | src/client/views/collections/CollectionDockingView.tsx | 280 | ||||
| -rw-r--r-- | src/client/views/collections/CollectionFreeFormView.scss | 41 | ||||
| -rw-r--r-- | src/client/views/collections/CollectionFreeFormView.tsx | 312 | ||||
| -rw-r--r-- | src/client/views/collections/CollectionSchemaView.scss | 207 | ||||
| -rw-r--r-- | src/client/views/collections/CollectionSchemaView.tsx | 244 | ||||
| -rw-r--r-- | src/client/views/collections/CollectionTreeView.scss | 37 | ||||
| -rw-r--r-- | src/client/views/collections/CollectionTreeView.tsx | 175 | ||||
| -rw-r--r-- | src/client/views/collections/CollectionView.tsx | 133 | ||||
| -rw-r--r-- | src/client/views/collections/CollectionViewBase.tsx | 121 |
10 files changed, 1890 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..2706c3272 --- /dev/null +++ b/src/client/views/collections/CollectionDockingView.scss @@ -0,0 +1,340 @@ +.collectiondockingview-content { + height: 100%; +} + +.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..c51fba908 --- /dev/null +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -0,0 +1,280 @@ +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 { 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 { DocumentView } from "../nodes/DocumentView"; +import "./CollectionDockingView.scss"; +import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; +import React = require("react"); +import { SubCollectionViewProps } from "./CollectionViewBase"; + +@observer +export class CollectionDockingView extends React.Component<SubCollectionViewProps> { + public static Instance: CollectionDockingView; + public static makeDocumentConfig(document: Document) { + return { + type: 'react-component', + component: 'DocumentFrameRenderer', + title: document.Title, + props: { + documentId: document.Id, + //collectionDockingView: CollectionDockingView.Instance + } + } + } + + private _goldenLayout: any = null; + private _containerRef = React.createRef<HTMLDivElement>(); + private _fullScreen: any = null; + + constructor(props: SubCollectionViewProps) { + super(props); + CollectionDockingView.Instance = this; + (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: e.button }) + } + + @action + public OpenFullScreen(document: Document) { + 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.stateChanged(); + } + @action + public CloseFullScreen() { + if (this._fullScreen) { + this._goldenLayout._$minimiseItem(this._fullScreen); + this._goldenLayout.root.contentItems[0].removeChild(this._fullScreen); + this._fullScreen = null; + 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'); + let newItemStackConfig = { + type: 'stack', + content: [CollectionDockingView.makeDocumentConfig(document)] + } + + var newContentItem = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout); + + if (this._goldenLayout.root.contentItems[0].isRow) { + this._goldenLayout.root.contentItems[0].addChild(newContentItem); + } + else { + var collayout = this._goldenLayout.root.contentItems[0]; + var newRow = collayout.layoutManager.createContentItem({ type: "row" }, this._goldenLayout); + collayout.parent.replaceChild(collayout, newRow); + + newRow.addChild(newContentItem, undefined, true); + newRow.addChild(collayout, 0, true); + + collayout.config["width"] = 50; + newContentItem.config["width"] = 50; + } + if (minimize) { + 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(); + return newContentItem; + } + + setupGoldenLayout() { + var config = this.props.Document.GetText(KeyStore.Data, ""); + if (config) { + if (!this._goldenLayout) { + this._goldenLayout = new GoldenLayout(JSON.parse(config)); + } + else { + if (config == JSON.stringify(this._goldenLayout.toConfig())) + return; + try { + this._goldenLayout.unbind('itemDropped', this.itemDropped); + this._goldenLayout.unbind('tabCreated', this.tabCreated); + this._goldenLayout.unbind('stackCreated', this.stackCreated); + } catch (e) { } + this._goldenLayout.destroy(); + this._goldenLayout = new GoldenLayout(JSON.parse(config)); + } + this._goldenLayout.on('itemDropped', this.itemDropped); + this._goldenLayout.on('tabCreated', this.tabCreated); + this._goldenLayout.on('stackCreated', this.stackCreated); + this._goldenLayout.registerComponent('DocumentFrameRenderer', DockedFrameRenderer); + this._goldenLayout.container = this._containerRef.current; + if (this._goldenLayout.config.maximisedItemId === '__glMaximised') { + try { + this._goldenLayout.config.root.getItemsById(this._goldenLayout.config.maximisedItemId)[0].toggleMaximise(); + } catch (e) { + this._goldenLayout.config.maximisedItemId = null; + } + } + this._goldenLayout.init(); + } + } + componentDidMount: () => void = () => { + if (this._containerRef.current) { + reaction( + () => this.props.Document.GetText(KeyStore.Data, ""), + () => this.setupGoldenLayout(), { 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(); + this._goldenLayout = null; + window.removeEventListener('resize', this.onResize); + } + @action + onResize = (event: any) => { + var cur = this._containerRef.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 + this._goldenLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height); + } + + _flush: boolean = false; + @action + onPointerUp = (e: React.PointerEvent): void => { + if (this._flush) { + this._flush = false; + setTimeout(() => this.stateChanged(), 10); + } + } + @action + onPointerDown = (e: React.PointerEvent): void => { + if (e.button === 2 && this.props.active()) { + e.stopPropagation(); + e.preventDefault(); + } else { + 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") { + this._flush = true; + } + if (e.buttons === 1 && this.props.active()) { + e.stopPropagation(); + } + } + } + + @undoBatch + stateChanged = () => { + var json = JSON.stringify(this._goldenLayout.toConfig()); + this.props.Document.SetText(KeyStore.Data, json) + } + + itemDropped = () => { + this.stateChanged(); + } + tabCreated = (tab: any) => { + tab.closeElement.off('click') //unbind the current click handler + .click(function () { + tab.contentItem.remove(); + }); + } + + stackCreated = (stack: any) => { + //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(action(function () { + //if (confirm('really close this?')) { + stack.remove(); + //} + })); + } + + 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`, + }} /> + ); + } +} + +interface DockedFrameProps { + documentId: FieldId, + //collectionDockingView: CollectionDockingView +} +@observer +export class DockedFrameRenderer extends React.Component<DockedFrameProps> { + + private _mainCont = React.createRef<HTMLDivElement>(); + @observable private _panelWidth = 0; + @observable private _panelHeight = 0; + @observable private _document: Opt<Document>; + + constructor(props: any) { + super(props); + Server.GetField(this.props.documentId, action((f: Opt<Field>) => this._document = f as Document)); + } + + 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); } + + ScreenToLocalTransform = () => { + let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current!); + return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(scale / this._contentScaling()) + } + + render() { + 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} + ScreenToLocalTransform={this.ScreenToLocalTransform} + isTopMost={true} + SelectOnLoad={false} + focus={(doc: Document) => { }} + ContainingCollectionView={undefined} /> + </div> + + return <Measure onResize={action((r: any) => { this._panelWidth = r.entry.width; this._panelHeight = r.entry.height; })}> + {({ measureRef }) => <div ref={measureRef}> {content} </div>} + </Measure> + } +}
\ 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..f432e8cc3 --- /dev/null +++ b/src/client/views/collections/CollectionFreeFormView.scss @@ -0,0 +1,41 @@ +.collectionfreeformview-container { + + .collectionfreeformview > .jsx-parser{ + position:absolute; + height: 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% + } +} + +.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 new file mode 100644 index 000000000..f71f2791c --- /dev/null +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -0,0 +1,312 @@ +import { observable, action, computed } 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 { DragManager } from "../../util/DragManager"; +import { Transform } from "../../util/Transform"; +import { undoBatch } from "../../util/UndoManager"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { CollectionSchemaView } from "../collections/CollectionSchemaView"; +import { CollectionView } from "../collections/CollectionView"; +import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; +import { DocumentView } from "../nodes/DocumentView"; +import { FormattedTextBox } from "../nodes/FormattedTextBox"; +import { ImageBox } from "../nodes/ImageBox"; +import { WebBox } from "../nodes/WebBox"; +import { KeyValueBox } from "../nodes/KeyValueBox" +import "./CollectionFreeFormView.scss"; +import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; +import { CollectionViewBase } from "./CollectionViewBase"; +import { Documents } from "../../documents/Documents"; +import React = require("react"); +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>(); + private _lastX: number = 0; + 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"]; + 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); + } + + @action + onPointerDown = (e: React.PointerEvent): void => { + if ((e.button === 2 && this.props.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; + 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 (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); + } + } + + } + + @action + onPointerMove = (e: PointerEvent): void => { + if (!e.cancelBubble && this.props.active()) { + e.stopPropagation(); + 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 + 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) { + const newPanX = Math.max((1 - this.zoomScaling) * this.nativeWidth, Math.min(0, panX)); + const newPanY = Math.max((1 - this.zoomScaling) * this.nativeHeight, Math.min(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() { + const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField); + if (lvalue && lvalue != FieldWaiting) { + return lvalue.Data.map(doc => { + return (<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, WebBox, KeyValueBox }} + 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, WebBox, KeyValueBox }} + 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 [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-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} + {cursor} + {this.views} + </div> + {this.overlayView} + </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..d40e6d314 --- /dev/null +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -0,0 +1,207 @@ + + +.collectionSchemaView-container { + border-style: solid; + box-sizing: border-box; + position: absolute; + width: 100%; + height: 100%; + .collectionSchemaView-previewRegion { + position: relative; + background: black; + float: left; + height: 100%; + } + .collectionSchemaView-previewHandle { + position: absolute; + height: 37px; + width: 20px; + z-index: 20; + right: 0; + top: 0; + background: Black ; + } + .collectionSchemaView-dividerDragger{ + position: relative; + background: black; + float: left; + height: 100%; + } + .collectionSchemaView-tableContainer { + position: relative; + float: left; + height: 100%; + } + + .ReactTable { + position: absolute; + // display: inline-block; + // overflow: auto; + width: 100%; + height: 100%; + background: white; + box-sizing: border-box; + .rt-table { + overflow-y: auto; + overflow-x: auto; + height: 100%; + + display: -webkit-inline-box; + direction: ltr; + // direction:rtl; + // display:block; + } + .rt-tbody { + //direction: ltr; + direction: rtl; + } + .rt-tr-group { + direction: ltr; + max-height: 44px; + } + .rt-td { + border-width: 1; + border-right-color: #aaa; + .imageBox-cont { + position:relative; + max-height:100%; + } + .imageBox-cont img { + object-fit: contain; + max-width: 100%; + 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; + padding: 3px 7px; + } + .ReactTable .rt-tbody .rt-tr-group:last-child { + border-bottom: grey; + border-bottom-style: solid; + border-bottom-width: 1; + } + .documentView-node:first-child { + background: grey; + .imageBox-cont img { + object-fit: contain; + } + } +} + +.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..49f95c014 --- /dev/null +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -0,0 +1,244 @@ +import React = require("react") +import { action, observable } from "mobx"; +import { observer } from "mobx-react"; +import Measure from "react-measure"; +import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table"; +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 { Transform } from "../../util/Transform"; +import { ContextMenu } from "../ContextMenu"; +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"; + +// 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; + @observable _selectedIndex = 0; + @observable _splitPercentage: number = 50; + + renderCell = (rowProps: CellInfo) => { + let props: FieldViewProps = { + doc: rowProps.value[0], + fieldKey: rowProps.value[1], + isSelected: () => false, + select: () => { }, + isTopMost: false, + bindings: {}, + selectOnLoad: false, + } + let contents = ( + <FieldView {...props} /> + ) + let reference = React.createRef<HTMLDivElement>(); + let onItemDown = setupDrag(reference, () => props.doc); + 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(); + } + return field || ""; + }} + SetValue={(value: string) => { + let script = CompileScript(value, undefined, true); + 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> + </div> + ) + } + + private getTrProps: ComponentPropsGetterR = (state, rowInfo) => { + const that = this; + if (!rowInfo) { + return {}; + } + return { + onClick: action((e: React.MouseEvent, handleOriginal: Function) => { + that._selectedIndex = rowInfo.index; + this._splitPercentage += 0.05; // bcz - ugh - needed to force Measure to do its thing and call onResize + + if (handleOriginal) { + handleOriginal() + } + }), + style: { + background: rowInfo.index == this._selectedIndex ? "lightGray" : "white", + //color: rowInfo.index == this._selectedIndex ? "white" : "black" + } + }; + } + + _startSplitPercent = 0; + @action + onDividerMove = (e: PointerEvent): void => { + let nativeWidth = this._mainCont.current!.getBoundingClientRect(); + this._splitPercentage = 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; + } + } + onDividerDown = (e: React.PointerEvent) => { + 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(); + } + @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; + } + } + 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(); + } + } + } + } + + @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); + } + + 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); + } + + focusDocument = (doc: Document) => { } + + 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} + /> + </div> + } + </Measure> + ) + let previewHandle = !this.props.active() ? (null) : ( + <div className="collectionSchemaView-previewHandle" onPointerDown={this.onExpanderDown} />); + 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> + <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} /> + <div className="collectionSchemaView-previewRegion" style={{ width: `calc(${100 - this._splitPercentage}% - ${this.DIVIDER_WIDTH}px)` }}> + {content} + </div> + {previewHandle} + </div> + </div > + ) + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss new file mode 100644 index 000000000..f8d580a7b --- /dev/null +++ b/src/client/views/collections/CollectionTreeView.scss @@ -0,0 +1,37 @@ +#body { + padding: 20px; + background: #bbbbbb; +} + +ul { + list-style: none; +} + +li { + margin: 5px 0; +} + +.no-indent { + padding-left: 0; +} + +.bullet { + width: 1.5em; + display: inline-block; +} + +.collectionTreeView-dropTarget { + border-style: solid; + box-sizing: border-box; + height: 100%; +} + +.docContainer { + display: inline-table; +} + +.delete-button { + color: #999999; + float: right; + margin-left: 1em; +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx new file mode 100644 index 000000000..8b06d9ac4 --- /dev/null +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -0,0 +1,175 @@ +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 { EditableView } from "../EditableView"; +import { setupDrag } from "../../util/DragManager"; +import { FieldWaiting } from "../../../fields/Field"; +import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; + +export interface TreeViewProps { + document: Document; + deleteDoc: (doc: Document) => void; +} + +export enum BulletType { + Collapsed, + Collapsible, + List +} + +@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; + + delete = () => { + this.props.deleteDoc(this.props.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); + } + } + + renderBullet(type: BulletType) { + let onClicked = action(() => this.collapsed = !this.collapsed); + + 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> + } + } + + /** + * 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 > + } + + 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> + } + + return <div className="treeViewItem-container" onPointerDown={onItemDown} ref={reference}> + {subView} + </div> + } + + // otherwise this is a normal leaf node + else { + return <li key={this.props.document.Id}> + {this.renderBullet(BulletType.List)} + {titleElement} + </li>; + } + } +} + + +@observer +export class CollectionTreeView extends CollectionViewBase { + + @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); + } + } + + render() { + let titleStr = ""; + let title = this.props.Document.GetT<TextField>(KeyStore.Title, TextField); + if (title && title !== FieldWaiting) { + titleStr = title.Data; + } + + 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} />) + ) + + 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); + return true; + }} /> + </h3> + <ul className="no-indent"> + {childrenElement} + </ul> + </div > + ); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx new file mode 100644 index 000000000..31824763d --- /dev/null +++ b/src/client/views/collections/CollectionView.tsx @@ -0,0 +1,133 @@ +import { action } from "mobx"; +import { observer } from "mobx-react"; +import { Document } from "../../../fields/Document"; +import { ListField } from "../../../fields/ListField"; +import { SelectionManager } from "../../util/SelectionManager"; +import { ContextMenu } from "../ContextMenu"; +import React = require("react"); +import { KeyStore } from "../../../fields/KeyStore"; +import { NumberField } from "../../../fields/NumberField"; +import { CollectionFreeFormView } from "./CollectionFreeFormView"; +import { CollectionDockingView } from "./CollectionDockingView"; +import { CollectionSchemaView } from "./CollectionSchemaView"; +import { CollectionViewProps } from "./CollectionViewBase"; +import { CollectionTreeView } from "./CollectionTreeView"; +import { Field } from "../../../fields/Field"; + +export enum CollectionViewType { + Invalid, + Freeform, + Schema, + Docking, + Tree +} + +export const COLLECTION_BORDER_WIDTH = 2; + +@observer +export class CollectionView extends React.Component<CollectionViewProps> { + + public static LayoutString(fieldKey: string = "DataKey") { + return `<CollectionView 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 = () => { + var isSelected = this.props.isSelected(); + var childSelected = SelectionManager.SelectedDocuments().some(view => view.props.ContainingCollectionView == this); + var topMost = this.props.isTopMost; + return isSelected || childSelected || topMost; + } + @action + addDocument = (doc: Document): void => { + if (this.props.Document.Get(this.props.fieldKey) instanceof Field) { + //TODO This won't create the field if it doesn't already exist + const value = this.props.Document.GetData(this.props.fieldKey, ListField, new Array<Document>()) + value.push(doc); + } else { + this.props.Document.SetData(this.props.fieldKey, [doc], ListField); + } + } + + + @action + removeDocument = (doc: Document): boolean => { + //TODO This won't create the field if it doesn't already exist + const value = this.props.Document.GetData(this.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; + } + } + + set collectionViewType(type: CollectionViewType) { + let Document = this.props.Document; + Document.SetData(KeyStore.ViewType, type, NumberField); + } + + specificContextMenu = (e: React.MouseEvent): void => { + if (!e.isPropagationStopped) { // 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) }) + ContextMenu.Instance.addItem({ description: "Schema", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Schema) }) + ContextMenu.Instance.addItem({ description: "Treeview", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Tree) }) + ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) }) + } + } + + render() { + let viewType = this.collectionViewType; + let subView: JSX.Element; + switch (viewType) { + case CollectionViewType.Freeform: + subView = (<CollectionFreeFormView {...this.props} + addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active} + CollectionView={this} />) + break; + case CollectionViewType.Schema: + subView = (<CollectionSchemaView {...this.props} + addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active} + CollectionView={this} />) + break; + case CollectionViewType.Docking: + subView = (<CollectionDockingView {...this.props} + addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active} + CollectionView={this} />) + break; + case CollectionViewType.Tree: + subView = (<CollectionTreeView {...this.props} + addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active} + CollectionView={this} />) + break; + default: + subView = <div></div> + break; + } + return (<div onContextMenu={this.specificContextMenu}> + {subView} + </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..0a3b965f2 --- /dev/null +++ b/src/client/views/collections/CollectionViewBase.tsx @@ -0,0 +1,121 @@ +import { action } 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) { + let htmlDoc = Documents.HtmlDocument(html, { ...options, width: 300, height: 300 }); + htmlDoc.SetText(KeyStore.DocumentText, text); + this.props.addDocument(htmlDoc); + return; + } + + for (let i = 0; i < e.dataTransfer.items.length; i++) { + 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); + } + })() + + }) + } + if (item.kind == "file" && item.type.indexOf("image")) { + let fReader = new FileReader() + let file = item.getAsFile(); + + fReader.addEventListener("load", action("drop", () => { + if (fReader.result) { + let url = "" + fReader.result; + let doc = Documents.ImageDocument(url, options) + 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(doc); + } + } + }), false) + + if (file) { + fReader.readAsDataURL(file) + } + } + } + } +} |
