aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections
diff options
context:
space:
mode:
authorab <abdullah_ahmed@brown.edu>2019-03-09 15:59:36 -0500
committerab <abdullah_ahmed@brown.edu>2019-03-09 15:59:36 -0500
commit30b082a9d94465ae24d9c392843518e8df2c9d66 (patch)
tree01c464b96defe06b2c99e8e298b9287901d4d9b0 /src/client/views/collections
parent12d20ab1b9b843d816bde445d9b67b3aa6abc6ae (diff)
parent96eede5f7d1706a3f7ac6ee02a85bb3da217f467 (diff)
merging
Diffstat (limited to 'src/client/views/collections')
-rw-r--r--src/client/views/collections/CollectionDockingView.scss4
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx466
-rw-r--r--src/client/views/collections/CollectionFreeFormView.scss53
-rw-r--r--src/client/views/collections/CollectionFreeFormView.tsx388
-rw-r--r--src/client/views/collections/CollectionPDFView.tsx55
-rw-r--r--src/client/views/collections/CollectionSchemaView.scss90
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx297
-rw-r--r--src/client/views/collections/CollectionTreeView.scss37
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx175
-rw-r--r--src/client/views/collections/CollectionView.tsx119
-rw-r--r--src/client/views/collections/CollectionViewBase.tsx151
11 files changed, 1242 insertions, 593 deletions
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index 7c0b512a7..2706c3272 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -1,3 +1,7 @@
+.collectiondockingview-content {
+ height: 100%;
+}
+
.collectiondockingview-container {
position: relative;
top: 0;
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 1653994cf..f01c538e6 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -1,190 +1,94 @@
-import FlexLayout from "flexlayout-react";
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, computed, reaction, observable } from "mobx";
+import { action, observable, reaction } from "mobx";
import { observer } from "mobx-react";
+import * as ReactDOM from 'react-dom';
import { Document } from "../../../fields/Document";
-import { KeyStore } from "../../../fields/Key";
-import { ListField } from "../../../fields/ListField";
-import { DragManager } from "../../util/DragManager";
-import { Transform } from "../../util/Transform";
+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 { CollectionViewBase, CollectionViewProps, COLLECTION_BORDER_WIDTH } from "./CollectionViewBase";
+import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
import React = require("react");
-import * as ReactDOM from 'react-dom';
-import Measure from "react-measure";
+import { SubCollectionViewProps } 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
+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
}
- });
- }
- @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, scaling: 1 } };
- });
- 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;
+ private _goldenLayout: any = null;
+ private _containerRef = React.createRef<HTMLDivElement>();
+ private _fullScreen: any = null;
+ private _flush: boolean = false;
- // 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);
+ 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
- onPointerDown = (e: React.PointerEvent): void => {
- if (e.button === 2 && this.active) {
- e.stopPropagation();
- e.preventDefault();
- } else {
- if (e.buttons === 1 && this.active) {
- e.stopPropagation();
- }
+ 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();
}
-
- 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} Document={value[i]}
- AddDocument={this.addDocument} RemoveDocument={this.removeDocument}
- GetTransform={() => Transform.Identity}
- ParentScaling={1}
- ContainingCollectionView={this} DocumentView={undefined} />);
- }
- }
- if (component === "text") {
- return (<div className="panel">Panel {node.getName()}</div>);
+ @action
+ public CloseFullScreen() {
+ if (this._fullScreen) {
+ this._goldenLayout._$minimiseItem(this._fullScreen);
+ this._goldenLayout.root.contentItems[0].removeChild(this._fullScreen);
+ this._fullScreen = null;
+ this.stateChanged();
}
}
- 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, scaling: 1 }
- };
- 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 }
- }
+ @action
+ public AddRightSplit(document: Document, minimize: boolean = false) {
+ this._goldenLayout.emit('stateChanged');
let newItemStackConfig = {
type: 'stack',
- content: [newItemConfig]
- };
- var newContentItem = new CollectionDockingView.myLayout._typeToItem[newItemStackConfig.type](CollectionDockingView.myLayout, newItemStackConfig, parent);
+ content: [CollectionDockingView.makeDocumentConfig(document)]
+ }
- if (CollectionDockingView.myLayout.root.contentItems[0].isRow) {
- var rowlayout = CollectionDockingView.myLayout.root.contentItems[0];
- var lastRowItem = rowlayout.contentItems[rowlayout.contentItems.length - 1];
+ var newContentItem = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout);
- lastRowItem.config["width"] *= 0.5;
- newContentItem.config["width"] = lastRowItem.config["width"];
- rowlayout.addChild(newContentItem, rowlayout.contentItems.length, true);
- rowlayout.callDownwards('setSize');
+ if (this._goldenLayout.root.contentItems[0].isRow) {
+ this._goldenLayout.root.contentItems[0].addChild(newContentItem);
}
else {
- var collayout = CollectionDockingView.myLayout.root.contentItems[0];
- var newRow = collayout.layoutManager.createContentItem({ type: "row" }, CollectionDockingView.myLayout);
+ 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);
@@ -192,131 +96,185 @@ export class CollectionDockingView extends CollectionViewBase {
collayout.config["width"] = 50;
newContentItem.config["width"] = 50;
- collayout.parent.callDownwards('setSize');
}
+ 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;
}
- 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;
+ setupGoldenLayout() {
+ var config = this.props.Document.GetText(KeyStore.Data, "");
+ if (config) {
+ if (!this._goldenLayout) {
+ this._goldenLayout = new GoldenLayout(JSON.parse(config));
}
- 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');
+ 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));
}
- //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 () {
- let divContainer = document.getElementById(containingDiv);
- if (divContainer) {
- let props: DockingProps = {
- ContainingDiv: containingDiv,
- Document: state.doc,
- Container: container,
- CollectionDockingView: me,
- HtmlElement: divContainer
- }
- ReactDOM.render((<RenderClass {...props} />), divContainer);
- if (CollectionDockingView.myLayout._maxstack) {
- CollectionDockingView.myLayout._maxstack.click();
- }
+ 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;
}
- }, 0);
- });
- CollectionDockingView.myLayout.container = this._containerRef.current;
- CollectionDockingView.myLayout.init();
+ }
+ 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;
- 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.GetNumber(KeyStore.Width, 0) / s;
- var h = Document.GetNumber(KeyStore.Height, 0) / s;
+ // 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);
+ }
- var chooseLayout = () => {
- if (!CollectionDockingView.UseGoldenLayout)
- return <FlexLayout.Layout model={this.modelForFlexLayout} factory={this.flexLayoutFactory} />;
+ @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} onContextMenu={(e) => e.preventDefault()} ref={this._containerRef}
+ onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} 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,
+ width: "100%",
+ height: "100%",
borderStyle: "solid",
borderWidth: `${COLLECTION_BORDER_WIDTH}px`,
- }} >
- {chooseLayout()}
- </div>
+ }} />
);
}
}
-interface DockingProps {
- ContainingDiv: string,
- Document: Document,
- Container: any,
- HtmlElement: HTMLElement,
- CollectionDockingView: CollectionDockingView
+interface DockedFrameProps {
+ documentId: FieldId,
+ //collectionDockingView: CollectionDockingView
}
@observer
-export class RenderClass extends React.Component<DockingProps> {
- @observable
- private _parentScaling = 1; // used to transfer the dimensions of the content pane in the DOM to the ParentScaling prop of the DocumentView
+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() {
- let nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0);
- var layout = this.props.Document.GetText(KeyStore.Layout, "");
+ if (!this._document)
+ return (null);
var content =
- <DocumentView key={this.props.Document.Id} Document={this.props.Document}
- AddDocument={this.props.CollectionDockingView.addDocument}
- RemoveDocument={this.props.CollectionDockingView.removeDocument}
- GetTransform={() => Transform.Identity}
- ParentScaling={this._parentScaling}
- ContainingCollectionView={this.props.CollectionDockingView} DocumentView={undefined} />
+ <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>
- if (nativeWidth > 0 && (layout.indexOf("CollectionFreeForm") == -1 || layout.indexOf("AnnotationsKey") != -1)) {
- return <Measure onResize={
- action((r: any) => this._parentScaling = nativeWidth > 0 ? r.entry.width / nativeWidth : 1)}
- >
- {({ measureRef }) => <div ref={measureRef}> {content} </div>}
- </Measure>
- }
- return <div> {content} </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
index 4cf474f77..b059163ed 100644
--- a/src/client/views/collections/CollectionFreeFormView.scss
+++ b/src/client/views/collections/CollectionFreeFormView.scss
@@ -1,4 +1,11 @@
.collectionfreeformview-container {
+
+ .collectionfreeformview > .jsx-parser{
+ position:absolute;
+ height: 100%;
+ width: 100%;
+ }
+
border-style: solid;
box-sizing: border-box;
position: relative;
@@ -11,5 +18,51 @@
position: absolute;
top: 0;
left: 0;
+ width:100%;
+ height: 100%;
+ }
+}
+.collectionfreeformview-overlay {
+
+ .collectionfreeformview > .jsx-parser{
+ position:absolute;
+ height: 100%;
+ }
+ .formattedTextBox-cont {
+ background:yellow;
}
+
+ border-style: solid;
+ box-sizing: border-box;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ .collectionfreeformview {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width:100%;
+ height: 100%;
+ }
+}
+
+.border {
+ border-style: solid;
+ box-sizing: border-box;
+ width: 100%;
+ height: 100%;
+}
+
+//this is an animation for the blinking cursor!
+@keyframes blink {
+ 0% {opacity: 0}
+ 49%{opacity: 0}
+ 50% {opacity: 1}
+}
+
+#prevCursor {
+ animation: blink 1s infinite;
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx
index 04373df12..16002ad9f 100644
--- a/src/client/views/collections/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/CollectionFreeFormView.tsx
@@ -1,90 +1,85 @@
+import { action, computed, observable, reaction, trace } from "mobx";
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 { FieldWaiting } from "../../../fields/Field";
+import { KeyStore } from "../../../fields/KeyStore";
import { ListField } from "../../../fields/ListField";
-import { NumberField } from "../../../fields/NumberField";
+import { TextField } from "../../../fields/TextField";
import { Documents } from "../../documents/Documents";
-import { FieldWaiting } from "../../../fields/Field";
+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 { CollectionPDFView } from "../collections/CollectionPDFView";
+import { InkingCanvas } from "../InkingCanvas";
+import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
+import { DocumentView } from "../nodes/DocumentView";
+import { FormattedTextBox } from "../nodes/FormattedTextBox";
+import { ImageBox } from "../nodes/ImageBox";
+import { KeyValueBox } from "../nodes/KeyValueBox";
+import { PDFBox } from "../nodes/PDFBox";
+import { WebBox } from "../nodes/WebBox";
+import "./CollectionFreeFormView.scss";
+import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
+import { CollectionViewBase } from "./CollectionViewBase";
+import React = require("react");
+import { render } from "pug";
+const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this?
@observer
export class CollectionFreeFormView extends CollectionViewBase {
- public static LayoutString(fieldKey: string = "DataKey") { return CollectionViewBase.LayoutString("CollectionFreeFormView", fieldKey); }
- private _containerRef = React.createRef<HTMLDivElement>();
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;
- constructor(props: CollectionViewProps) {
- super(props);
- }
+ //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 isAnnotationOverlay() { return this.props.CollectionFieldKey == KeyStore.Annotations; }
-
- @computed
- get nativeWidth() { return this.props.DocumentForCollection.GetNumber(KeyStore.NativeWidth, 0); }
- @computed
- get nativeHeight() { return this.props.DocumentForCollection.GetNumber(KeyStore.NativeHeight, 0); }
-
- @computed
- get zoomScaling() { return this.props.DocumentForCollection.GetNumber(KeyStore.Scale, 1); }
-
- @computed
- get resizeScaling() { return this.isAnnotationOverlay ? this.props.DocumentForCollection.GetNumber(KeyStore.Width, 0) / this.nativeWidth : 1; }
+ @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) => {
- 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 { translateX, translateY } = Utils.GetScreenTransform(this._canvasRef.current!);
- const currScale = this.resizeScaling * this.zoomScaling * this.props.ContainingDocumentView!.ScalingToScreenSpace;
- const screenX = de.x - xOffset;
- const screenY = de.y - yOffset;
- doc.props.Document.SetNumber(KeyStore.X, (screenX - translateX) / currScale);
- doc.props.Document.SetNumber(KeyStore.Y, (screenY - translateY) / currScale);
- this.bringToFront(doc);
- }
- e.stopPropagation();
- }
-
- componentDidMount() {
- if (this._containerRef.current) {
- DragManager.MakeDropTarget(this._containerRef.current, {
- handlers: {
- drop: this.drop
- }
- });
- }
+ 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.active) ||
- !e.defaultPrevented) {
+ if (((e.button === 2 && this.props.active()) ||
+ !e.defaultPrevented) && (!this.isAnnotationOverlay || this.zoomScaling != 1 || e.button == 0)) {
document.removeEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointerup", this.onPointerUp);
- this._downX = this._lastX = e.pageX;
- this._downY = this._lastY = e.pageY;
+ this._lastX = e.pageX;
+ this._lastY = e.pageY;
+ this._downX = e.pageX;
+ this._downY = e.pageY;
}
}
@@ -94,22 +89,26 @@ export class CollectionFreeFormView extends CollectionViewBase {
document.removeEventListener("pointerup", this.onPointerUp);
e.stopPropagation();
if (Math.abs(this._downX - e.clientX) < 3 && Math.abs(this._downY - e.clientY) < 3) {
- if (!SelectionManager.IsSelected(this.props.ContainingDocumentView as CollectionFreeFormDocumentView)) {
- SelectionManager.SelectDoc(this.props.ContainingDocumentView as CollectionFreeFormDocumentView, false);
+ //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.active) {
- e.preventDefault();
+ if (!e.cancelBubble && this.props.active()) {
e.stopPropagation();
- let currScale: number = this.props.ContainingDocumentView!.ScalingToScreenSpace;
- let x = this.props.DocumentForCollection.GetNumber(KeyStore.PanX, 0);
- let y = this.props.DocumentForCollection.GetNumber(KeyStore.PanY, 0);
-
- this.SetPan(x + (e.pageX - this._lastX) / currScale, y + (e.pageY - this._lastY) / currScale);
+ e.preventDefault();
+ 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;
@@ -119,134 +118,205 @@ export class CollectionFreeFormView extends CollectionViewBase {
onPointerWheel = (e: React.WheelEvent): void => {
e.stopPropagation();
e.preventDefault();
- let modes = ['pixels', 'lines', 'page'];
let coefficient = 1000;
- // if (modes[e.deltaMode] == 'pixels') coefficient = 50;
- // else if (modes[e.deltaMode] == 'lines') coefficient = 1000; // This should correspond to line-height??
- let { LocalX, LocalY, ContainerX, ContainerY } = this.props.ContainingDocumentView!.TransformToLocalPoint(e.pageX, e.pageY);
- var Xx = this.props.ContainingDocumentView!.LeftCorner();
- var Yy = this.props.ContainingDocumentView!.TopCorner();
- var deltaScale = (1 - (e.deltaY / coefficient)) * this.props.ContainingDocumentView!.props.Document.GetNumber(KeyStore.Scale, 1);
- var newDeltaScale = this.isAnnotationOverlay ? Math.max(1, deltaScale) : deltaScale;
+ 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);
- this.props.DocumentForCollection.SetNumber(KeyStore.Scale, newDeltaScale);
- this.SetPan(ContainerX - (LocalX * newDeltaScale + Xx), ContainerY - (LocalY * newDeltaScale + Yy));
+ 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.DocumentForCollection.SetNumber(KeyStore.PanX, this.isAnnotationOverlay ? newPanX : panX);
- this.props.DocumentForCollection.SetNumber(KeyStore.PanY, this.isAnnotationOverlay ? newPanY : panY);
+ var x1 = this.getLocalTransform().inverse().Scale;
+ var x2 = this.getTransform().inverse().Scale;
+ const newPanX = Math.min((1 - 1 / x1) * this.nativeWidth, Math.max(0, panX));
+ const newPanY = Math.min((1 - 1 / x1) * this.nativeHeight, Math.max(0, panY));
+ this.props.Document.SetNumber(KeyStore.PanX, this.isAnnotationOverlay ? newPanX : panX);
+ this.props.Document.SetNumber(KeyStore.PanY, this.isAnnotationOverlay ? newPanY : panY);
}
@action
onDrop = (e: React.DragEvent): void => {
- e.stopPropagation()
- e.preventDefault()
- let fReader = new FileReader()
- let file = e.dataTransfer.items[0].getAsFile();
- let that = this;
- const panx: number = this.props.DocumentForCollection.GetNumber(KeyStore.PanX, 0);
- const pany: number = this.props.DocumentForCollection.GetNumber(KeyStore.PanY, 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)
+ var pt = this.getTransform().transformPoint(e.pageX, e.pageY);
+ super.onDrop(e, { x: pt[0], y: pt[1] });
+ }
- if (file) {
- fReader.readAsDataURL(file)
- }
+ onDragOver = (): void => {
}
- onDragOver = (e: React.DragEvent): 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: 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);
+ 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)
+ });
+ }
- if (doc.props.Document.GetNumber(KeyStore.ZIndex, 0) != 0) {
- doc.props.Document.SetData(KeyStore.ZIndex, 0, NumberField);
+ @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 translate(): [number, number] {
- const x = this.props.DocumentForCollection.GetNumber(KeyStore.PanX, 0);
- const y = this.props.DocumentForCollection.GetNumber(KeyStore.PanY, 0);
- return [x, y];
+ get views() {
+ var curPage = this.props.Document.GetNumber(KeyStore.CurPage, 1);
+ const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField);
+ if (lvalue && lvalue != FieldWaiting) {
+ return lvalue.Data.map(doc => {
+ var page = doc.GetNumber(KeyStore.Page, 0);
+ return (page != curPage && page != 0) ? (null) :
+ (<CollectionFreeFormDocumentView key={doc.Id} Document={doc}
+ AddDocument={this.props.addDocument}
+ RemoveDocument={this.props.removeDocument}
+ ScreenToLocalTransform={this.getTransform}
+ isTopMost={false}
+ SelectOnLoad={doc.Id === this._selectOnLoaded}
+ ContentScaling={this.noScaling}
+ PanelWidth={doc.Width}
+ PanelHeight={doc.Height}
+ ContainingCollectionView={this.props.CollectionView}
+ focus={this.focusDocument}
+ />);
+ })
+ }
+ return null;
}
@computed
- get scale(): number {
- return this.props.DocumentForCollection.GetNumber(KeyStore.Scale, 1);
+ get backgroundView() {
+ return !this.backgroundLayout ? (null) :
+ (<JsxParser
+ components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, PDFBox }}
+ bindings={this.props.bindings}
+ jsx={this.backgroundLayout}
+ showWarnings={true}
+ onError={(test: any) => console.log(test)}
+ />);
+ }
+ @computed
+ get overlayView() {
+ return !this.overlayLayout ? (null) :
+ (<JsxParser
+ components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, PDFBox }}
+ bindings={this.props.bindings}
+ jsx={this.overlayLayout}
+ showWarnings={true}
+ onError={(test: any) => console.log(test)}
+ />);
}
- getTransform = (): Transform => {
- const [x, y] = this.translate;
- return this.props.GetTransform().scaled(this.scale).translate(x, y);
+ 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() {
- const Document: Document = this.props.DocumentForCollection;
- const value: Document[] = Document.GetList<Document>(this.props.CollectionFieldKey, []);
- const panx: number = Document.GetNumber(KeyStore.PanX, 0);
- const pany: number = Document.GetNumber(KeyStore.PanY, 0);
- var me = this;
+ //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"
+ <div className={`collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`}
onPointerDown={this.onPointerDown}
+ onKeyPress={this.onKeyDown}
onWheel={this.onPointerWheel}
- onContextMenu={(e) => e.preventDefault()}
- onDrop={this.onDrop}
+ onDrop={this.onDrop.bind(this)}
onDragOver={this.onDragOver}
- style={{
- borderWidth: `${COLLECTION_BORDER_WIDTH}px`,
- }}
- ref={this._containerRef}>
+ onBlur={this.onBlur}
+ style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px`, }}
+ tabIndex={0}
+ ref={this.createDropTarget}>
<div className="collectionfreeformview"
- style={{ width: "100%", transformOrigin: "left top", transform: ` translate(${panx}px, ${pany}px) scale(${this.zoomScaling}, ${this.zoomScaling})` }}
+ style={{ transformOrigin: "left top", transform: `translate(${dx}px, ${dy}px) scale(${this.zoomScaling}, ${this.zoomScaling}) translate(${panx}px, ${pany}px)` }}
ref={this._canvasRef}>
-
- {this.props.BackgroundView}
- {value.map(doc => {
- return (<CollectionFreeFormDocumentView key={doc.Id} Document={doc}
- AddDocument={this.addDocument}
- RemoveDocument={this.removeDocument}
- GetTransform={this.getTransform}
- ParentScaling={1}
- ContainingCollectionView={this} DocumentView={undefined} />);
- })}
+ {this.backgroundView}
+ <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} />
+ {cursor}
+ {this.views}
</div>
+ {this.overlayView}
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx
new file mode 100644
index 000000000..7fd9f0f11
--- /dev/null
+++ b/src/client/views/collections/CollectionPDFView.tsx
@@ -0,0 +1,55 @@
+import { action, computed } from "mobx";
+import { observer } from "mobx-react";
+import { Document } from "../../../fields/Document";
+import { KeyStore } from "../../../fields/KeyStore";
+import { ContextMenu } from "../ContextMenu";
+import { CollectionView, CollectionViewType } from "./CollectionView";
+import { CollectionViewProps } from "./CollectionViewBase";
+import React = require("react");
+
+
+@observer
+export class CollectionPDFView extends React.Component<CollectionViewProps> {
+
+ public static LayoutString(fieldKey: string = "DataKey") {
+ return `<${CollectionPDFView.name} Document={Document}
+ ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} panelWidth={PanelWidth} panelHeight={PanelHeight} isSelected={isSelected} select={select} bindings={bindings}
+ isTopMost={isTopMost} SelectOnLoad={selectOnLoad} BackgroundView={BackgroundView} focus={focus}/>`;
+ }
+
+ @action onPageBack = () => this.curPage > 1 ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage - 1) : 0;
+ @action onPageForward = () => this.curPage < this.numPages ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage + 1) : 0;
+
+ @computed private get curPage() { return this.props.Document.GetNumber(KeyStore.CurPage, 0); }
+ @computed private get numPages() { return this.props.Document.GetNumber(KeyStore.NumPages, 0); }
+ @computed private get uIButtons() {
+ return (
+ <div className="pdfBox-buttonTray" key="tray">
+ <button className="pdfButton" onClick={this.onPageBack}>{"<"}</button>
+ <button className="pdfButton" onClick={this.onPageForward}>{">"}</button>
+ </div>);
+ }
+
+ // "inherited" CollectionView API starts here...
+
+ public active: () => boolean = () => CollectionView.Active(this);
+
+ addDocument = (doc: Document): void => { CollectionView.AddDocument(this.props, doc); }
+ removeDocument = (doc: Document): boolean => { return CollectionView.RemoveDocument(this.props, doc); }
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ if (!e.isPropagationStopped() && this.props.Document.Id != "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
+ ContextMenu.Instance.addItem({ description: "PDFOptions", event: () => { } });
+ }
+ }
+
+ get collectionViewType(): CollectionViewType { return CollectionViewType.Freeform; }
+ get subView(): any { return CollectionView.SubView(this); }
+
+ render() {
+ return (<div className="collectionView-cont" onContextMenu={this.specificContextMenu}>
+ {this.subView}
+ {this.props.isSelected() ? this.uIButtons : (null)}
+ </div>)
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss
index 633e3ca1b..d40e6d314 100644
--- a/src/client/views/collections/CollectionSchemaView.scss
+++ b/src/client/views/collections/CollectionSchemaView.scss
@@ -1,58 +1,98 @@
+
.collectionSchemaView-container {
border-style: solid;
box-sizing: border-box;
position: absolute;
width: 100%;
height: 100%;
- .collectionfreeformview-container {
- border-width: 0px;
- .collectionfreeformview > .jsx-parser{
- position:absolute
- }
+ .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%;
}
- .imageBox-cont {
- position:relative;
- max-height:100%;
+ .collectionSchemaView-tableContainer {
+ position: relative;
+ float: left;
+ height: 100%;
}
+
.ReactTable {
position: absolute;
- display: inline-block;
+ // display: inline-block;
+ // overflow: auto;
width: 100%;
- overflow: auto;
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: 75px;
+ 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;
}
- .ReactTable .rt-td {
- border-width: 1;
- border-right-color: #aaa
- }
- .ReactTable .rt-tr-group {
- border-width: 1;
- border-bottom-color: #aaa
- }
- .imageBox-cont img {
- object-fit: contain;
- height: 100%
- }
.documentView-node:first-child {
background: grey;
.imageBox-cont img {
object-fit: contain;
- max-width: 100%;
- height: 100%
}
}
}
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 76706f520..49f95c014 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -1,63 +1,83 @@
import React = require("react")
-import ReactTable, { ReactTableDefaults, CellInfo, ComponentPropsGetterRC, ComponentPropsGetterR } from "react-table";
+import { action, observable } from "mobx";
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, COLLECTION_BORDER_WIDTH } from "./CollectionViewBase";
-import { DocumentView } from "../nodes/DocumentView";
-import { EditableView } from "../EditableView";
-import { CompileScript, ToField } from "../../util/Scripting";
-import { KeyStore as KS, Key, KeyStore } from "../../../fields/Key";
+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 Measure from "react-measure";
+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 {
- public static LayoutString() { return CollectionViewBase.LayoutString("CollectionSchemaView"); }
+ private _mainCont = React.createRef<HTMLDivElement>();
+ private DIVIDER_WIDTH = 5;
- @observable
- selectedIndex = 0;
+ @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],
- DocumentViewForField: undefined,
+ isSelected: () => false,
+ select: () => { },
+ isTopMost: false,
+ bindings: {},
+ selectOnLoad: false,
}
let contents = (
<FieldView {...props} />
)
+ let reference = React.createRef<HTMLDivElement>();
+ let onItemDown = setupDrag(reference, () => props.doc);
return (
- <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);
- 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 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>
)
}
@@ -68,96 +88,157 @@ export class CollectionSchemaView extends CollectionViewBase {
}
return {
onClick: action((e: React.MouseEvent, handleOriginal: Function) => {
- that.selectedIndex = rowInfo.index;
- const doc: Document = rowInfo.original;
- console.log("Row clicked: ", doc.Title)
+ 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 ? "#00afec" : "white",
- color: rowInfo.index == this.selectedIndex ? "white" : "black"
+ background: rowInfo.index == this._selectedIndex ? "lightGray" : "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();
+ _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 && this.active) {
- e.stopPropagation();
+ 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) => { }
- @observable
- private _parentScaling = 1; // used to transfer the dimensions of the content pane in the DOM to the ParentScaling prop of the DocumentView
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;
- var me = this;
- if (this.selectedIndex != -1) {
- content = (
- <Measure onResize={action((r: any) => {
- var doc = children[this.selectedIndex];
- var n = doc.GetNumber(KeyStore.NativeWidth, 0);
- if (n > 0 && r.entry.width > 0) {
- this._parentScaling = r.entry.width / n;
- }
- })}>
- {({ measureRef }) =>
- <div ref={measureRef}>
- <DocumentView Document={children[this.selectedIndex]}
- AddDocument={this.addDocument} RemoveDocument={this.removeDocument}
- GetTransform={() => Transform.Identity}//TODO This should probably be an actual transform
- ParentScaling={this._parentScaling}
- DocumentView={undefined} ContainingCollectionView={me} />
- </div>
- }
- </Measure>
- )
- } else {
- content = <div />
- }
+ 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 onPointerDown={this.onPointerDown} className="collectionSchemaView-container"
- style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px`, }} >
- <SplitPane split={"vertical"} defaultSize="60%" style={{ height: "100%", position: "relative", overflow: "none" }}>
- <ReactTable
- data={children}
- pageSize={children.length}
- page={0}
- showPagination={false}
- 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}
- />
- {content}
- </SplitPane>
- </div>
+ <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}>&#9654;</div>
+ case BulletType.Collapsible:
+ return <div className="bullet" onClick={onClicked}>&#9660;</div>
+ case BulletType.List:
+ return <div className="bullet">&mdash;</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..49df04163
--- /dev/null
+++ b/src/client/views/collections/CollectionView.tsx
@@ -0,0 +1,119 @@
+import { action, computed } 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.name} Document={Document}
+ ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} panelWidth={PanelWidth} panelHeight={PanelHeight} isSelected={isSelected} select={select} bindings={bindings}
+ isTopMost={isTopMost} SelectOnLoad={selectOnLoad} BackgroundView={BackgroundView} focus={focus}/>`;
+ }
+
+ public active: () => boolean = () => CollectionView.Active(this);
+ addDocument = (doc: Document): void => { CollectionView.AddDocument(this.props, doc); }
+ removeDocument = (doc: Document): boolean => { return CollectionView.RemoveDocument(this.props, doc); }
+ get subView() { return CollectionView.SubView(this); }
+
+ public static Active(self: CollectionView): boolean {
+ var isSelected = self.props.isSelected();
+ var childSelected = SelectionManager.SelectedDocuments().some(view => view.props.ContainingCollectionView == self);
+ var topMost = self.props.isTopMost;
+ return isSelected || childSelected || topMost;
+ }
+
+ @action
+ public static AddDocument(props: CollectionViewProps, doc: Document) {
+ doc.SetNumber(KeyStore.Page, props.Document.GetNumber(KeyStore.CurPage, 0));
+ if (props.Document.Get(props.fieldKey) instanceof Field) {
+ //TODO This won't create the field if it doesn't already exist
+ const value = props.Document.GetData(props.fieldKey, ListField, new Array<Document>())
+ value.push(doc);
+ } else {
+ props.Document.SetData(props.fieldKey, [doc], ListField);
+ }
+ }
+
+ @action
+ public static RemoveDocument(props: CollectionViewProps, doc: Document): boolean {
+ //TODO This won't create the field if it doesn't already exist
+ const value = props.Document.GetData(props.fieldKey, ListField, new Array<Document>())
+ let index = -1;
+ for (let i = 0; i < value.length; i++) {
+ if (value[i].Id == doc.Id) {
+ index = i;
+ break;
+ }
+ }
+
+ if (index !== -1) {
+ value.splice(index, 1)
+
+ SelectionManager.DeselectAll()
+ ContextMenu.Instance.clearItems()
+ return true;
+ }
+ return false
+ }
+
+ get collectionViewType(): CollectionViewType {
+ let Document = this.props.Document;
+ let viewField = Document.GetT(KeyStore.ViewType, NumberField);
+ if (viewField === "<Waiting>") {
+ return CollectionViewType.Invalid;
+ } else if (viewField) {
+ return viewField.Data;
+ } else {
+ return CollectionViewType.Freeform;
+ }
+ }
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ if (!e.isPropagationStopped() && this.props.Document.Id != "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
+ ContextMenu.Instance.addItem({ description: "Freeform", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform) })
+ 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) })
+ }
+ }
+
+ public static SubView(self: CollectionView) {
+ let subProps = { ...self.props, addDocument: self.addDocument, removeDocument: self.removeDocument, active: self.active, CollectionView: self }
+ switch (self.collectionViewType) {
+ case CollectionViewType.Freeform: return (<CollectionFreeFormView {...subProps} />)
+ case CollectionViewType.Schema: return (<CollectionSchemaView {...subProps} />)
+ case CollectionViewType.Docking: return (<CollectionDockingView {...subProps} />)
+ case CollectionViewType.Tree: return (<CollectionTreeView {...subProps} />)
+ }
+ return (null);
+ }
+
+ render() {
+ return (<div className="collectionView-cont" onContextMenu={this.specificContextMenu}>
+ {this.subView}
+ </div>)
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
index 1cf07ce05..0a3b965f2 100644
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -1,64 +1,121 @@
-import { action, computed } from "mobx";
-import { observer } from "mobx-react";
+import { action } from "mobx";
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 { 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 { CollectionDockingView } from "./CollectionDockingView";
-import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
+import { Documents, DocumentOptions } from "../../documents/Documents";
+import { Key } from "../../../fields/Key";
import { Transform } from "../../util/Transform";
-
+import { CollectionView } from "./CollectionView";
export interface CollectionViewProps {
- CollectionFieldKey: Key;
- DocumentForCollection: Document;
- ContainingDocumentView: Opt<DocumentView>;
- GetTransform: () => Transform;
- BackgroundView: Opt<DocumentView>;
- ParentScaling: number;
+ 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 const COLLECTION_BORDER_WIDTH = 2;
-
-@observer
-export class CollectionViewBase extends React.Component<CollectionViewProps> {
-
- public static LayoutString(collectionType: string, fieldKey: string = "DataKey") {
- return `<${collectionType} ParentScaling={ParentScaling} DocumentForCollection={Document} CollectionFieldKey={${fieldKey}} ContainingDocumentView={DocumentView} BackgroundView={BackgroundView} />`;
- }
- @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;
+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
- 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);
+ 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
- removeDocument = (doc: Document): boolean => {
- //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>())
- let index = value.indexOf(doc);
- if (index !== -1) {
- value.splice(index, 1)
+ protected onDrop(e: React.DragEvent, options: DocumentOptions): void {
+ e.stopPropagation()
+ e.preventDefault()
+ let that = this;
- SelectionManager.DeselectAll()
- ContextMenu.Instance.clearItems()
- return true;
+ 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;
}
- return false
- }
-} \ No newline at end of file
+ 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)
+ }
+ }
+ }
+ }
+}