aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes
diff options
context:
space:
mode:
authorTyler Schicke <tyler_schicke@brown.edu>2019-02-09 19:13:24 -0500
committerTyler Schicke <tyler_schicke@brown.edu>2019-02-09 19:13:24 -0500
commit11134bc5ce01d0a025d311a4f83e67ff6e63ce1c (patch)
treee401d8004481b3d664c751ae2e668c72b6be7aac /src/client/views/nodes
parentc06745a99ed85b215d0ae48bfb2af7c955f0b016 (diff)
Moved client code to client folder
Diffstat (limited to 'src/client/views/nodes')
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx223
-rw-r--r--src/client/views/nodes/DocumentView.tsx153
-rw-r--r--src/client/views/nodes/FieldTextBox.scss14
-rw-r--r--src/client/views/nodes/FieldView.tsx56
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss14
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx127
-rw-r--r--src/client/views/nodes/ImageBox.scss11
-rw-r--r--src/client/views/nodes/ImageBox.tsx92
-rw-r--r--src/client/views/nodes/NodeView.scss23
9 files changed, 713 insertions, 0 deletions
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
new file mode 100644
index 000000000..1d53cedc4
--- /dev/null
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -0,0 +1,223 @@
+import { action, computed } from "mobx";
+import { observer } from "mobx-react";
+import { Key, KeyStore } from "../../../fields/Key";
+import { NumberField } from "../../../fields/NumberField";
+import { DragManager } from "../../util/DragManager";
+import { SelectionManager } from "../../util/SelectionManager";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { CollectionFreeFormView } from "../collections/CollectionFreeFormView";
+import { ContextMenu } from "../ContextMenu";
+import "./NodeView.scss";
+import React = require("react");
+import { DocumentView, DocumentViewProps } from "./DocumentView";
+
+
+@observer
+export class CollectionFreeFormDocumentView extends DocumentView {
+ private _contextMenuCanOpen = false;
+ private _downX: number = 0;
+ private _downY: number = 0;
+
+ constructor(props: DocumentViewProps) {
+ super(props);
+ }
+ get screenRect(): ClientRect | DOMRect {
+ if (this._mainCont.current) {
+ return this._mainCont.current.getBoundingClientRect();
+ }
+ return new DOMRect();
+ }
+
+ @computed
+ get x(): number {
+ return this.props.Document.GetData(KeyStore.X, NumberField, Number(0));
+ }
+
+ @computed
+ get y(): number {
+ return this.props.Document.GetData(KeyStore.Y, NumberField, Number(0));
+ }
+
+ set x(x: number) {
+ this.props.Document.SetData(KeyStore.X, x, NumberField)
+ }
+
+ set y(y: number) {
+ this.props.Document.SetData(KeyStore.Y, y, NumberField)
+ }
+
+ @computed
+ get transform(): string {
+ return `translate(${this.x}px, ${this.y}px)`;
+ }
+
+ @computed
+ get width(): number {
+ return this.props.Document.GetData(KeyStore.Width, NumberField, Number(0));
+ }
+
+ set width(w: number) {
+ this.props.Document.SetData(KeyStore.Width, w, NumberField)
+ }
+
+ @computed
+ get height(): number {
+ return this.props.Document.GetData(KeyStore.Height, NumberField, Number(0));
+ }
+
+ set height(h: number) {
+ this.props.Document.SetData(KeyStore.Height, h, NumberField)
+ }
+
+ @computed
+ get zIndex(): number {
+ return this.props.Document.GetData(KeyStore.ZIndex, NumberField, Number(0));
+ }
+
+ set zIndex(h: number) {
+ this.props.Document.SetData(KeyStore.ZIndex, h, NumberField)
+ }
+
+ @action
+ dragComplete = (e: DragManager.DragCompleteEvent) => {
+ }
+
+ @computed
+ get active(): boolean {
+ return SelectionManager.IsSelected(this) || this.props.ContainingCollectionView === undefined ||
+ this.props.ContainingCollectionView.active;
+ }
+
+ @computed
+ get topMost(): boolean {
+ return this.props.ContainingCollectionView == undefined || this.props.ContainingCollectionView instanceof CollectionDockingView;
+ }
+
+ onPointerDown = (e: React.PointerEvent): void => {
+ this._downX = e.clientX;
+ this._downY = e.clientY;
+ var me = this;
+ if (e.shiftKey && e.buttons === 1) {
+ CollectionDockingView.StartOtherDrag(this._mainCont.current!, this.props.Document);
+ e.stopPropagation();
+ return;
+ }
+ this._contextMenuCanOpen = e.button == 2;
+ if (this.active && !e.isDefaultPrevented()) {
+ e.stopPropagation();
+ if (e.buttons === 2) {
+ e.preventDefault();
+ }
+ document.removeEventListener("pointermove", this.onPointerMove)
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp)
+ document.addEventListener("pointerup", this.onPointerUp);
+ }
+ }
+
+ onPointerMove = (e: PointerEvent): void => {
+ if (e.cancelBubble) {
+ this._contextMenuCanOpen = false;
+ return;
+ }
+ if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
+ this._contextMenuCanOpen = false;
+ if (this._mainCont.current != null && !this.topMost) {
+ this._contextMenuCanOpen = false;
+ const rect = this.screenRect;
+ let dragData: { [id: string]: any } = {};
+ dragData["document"] = this;
+ dragData["xOffset"] = e.x - rect.left;
+ dragData["yOffset"] = e.y - rect.top;
+ DragManager.StartDrag(this._mainCont.current, dragData, {
+ handlers: {
+ dragComplete: this.dragComplete,
+ },
+ hideSource: true
+ })
+ }
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ onPointerUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onPointerMove)
+ document.removeEventListener("pointerup", this.onPointerUp)
+ e.stopPropagation();
+ if (Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientY - this._downY) < 4) {
+ SelectionManager.SelectDoc(this, e.ctrlKey);
+ }
+ }
+
+ openRight = (e: React.MouseEvent): void => {
+ CollectionDockingView.AddRightSplit(this.props.Document);
+ }
+
+ deleteClicked = (e: React.MouseEvent): void => {
+ if (this.props.ContainingCollectionView instanceof CollectionFreeFormView) {
+ this.props.ContainingCollectionView.removeDocument(this.props.Document)
+ }
+ }
+ @action
+ fullScreenClicked = (e: React.MouseEvent): void => {
+ CollectionDockingView.OpenFullScreen(this.props.Document);
+ ContextMenu.Instance.clearItems();
+ ContextMenu.Instance.addItem({ description: "Close Full Screen", event: this.closeFullScreenClicked });
+ ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15)
+ }
+ @action
+ closeFullScreenClicked = (e: React.MouseEvent): void => {
+ CollectionDockingView.CloseFullScreen();
+ ContextMenu.Instance.clearItems();
+ ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked })
+ ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15)
+ }
+
+ @action
+ onContextMenu = (e: React.MouseEvent): void => {
+ if (!SelectionManager.IsSelected(this)) {
+ return;
+ }
+ e.preventDefault()
+
+ if (!this._contextMenuCanOpen) {
+ return;
+ }
+
+ if (this.topMost) {
+ ContextMenu.Instance.clearItems()
+ ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked })
+ ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15)
+ }
+ else {
+ // DocumentViews should stop propogation of this event
+ e.stopPropagation();
+
+ ContextMenu.Instance.clearItems();
+ ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked })
+ ContextMenu.Instance.addItem({ description: "Open Right", event: this.openRight })
+ ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked })
+ ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15)
+ SelectionManager.SelectDoc(this, e.ctrlKey);
+ }
+ }
+
+ render() {
+ var freestyling = this.props.ContainingCollectionView instanceof CollectionFreeFormView;
+ return (
+ <div className="node" ref={this._mainCont} style={{
+ transform: freestyling ? this.transform : "",
+ width: freestyling ? this.width : "100%",
+ height: freestyling ? this.height : "100%",
+ position: freestyling ? "absolute" : "relative",
+ zIndex: freestyling ? this.zIndex : 0,
+ }}
+ onContextMenu={this.onContextMenu}
+ onPointerDown={this.onPointerDown}>
+
+ <DocumentView {...this.props} DocumentView={this} />
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
new file mode 100644
index 000000000..730ce62f2
--- /dev/null
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -0,0 +1,153 @@
+import { action, computed } from "mobx";
+import { observer } from "mobx-react";
+import { Document } from "../../../fields/Document";
+import { Opt, FieldWaiting } from "../../../fields/Field";
+import { Key, KeyStore } from "../../../fields/Key";
+import { ListField } from "../../../fields/ListField";
+import { NumberField } from "../../../fields/NumberField";
+import { TextField } from "../../../fields/TextField";
+import { Utils } from "../../../Utils";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { CollectionFreeFormView } from "../collections/CollectionFreeFormView";
+import { CollectionSchemaView } from "../collections/CollectionSchemaView";
+import { CollectionViewBase, COLLECTION_BORDER_WIDTH } from "../collections/CollectionViewBase";
+import { FormattedTextBox } from "../nodes/FormattedTextBox";
+import { ImageBox } from "../nodes/ImageBox";
+import "./NodeView.scss";
+import React = require("react");
+const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this?
+
+export interface DocumentViewProps {
+ Document: Document;
+ DocumentView: Opt<DocumentView> // needed only to set ContainingDocumentView on CollectionViewProps when invoked from JsxParser -- is there a better way?
+ ContainingCollectionView: Opt<CollectionViewBase>;
+}
+@observer
+export class DocumentView extends React.Component<DocumentViewProps> {
+
+ protected _mainCont = React.createRef<any>();
+ get MainContent() {
+ return this._mainCont;
+ }
+ @computed
+ get layout(): string {
+ return this.props.Document.GetData(KeyStore.Layout, TextField, String("<p>Error loading layout data</p>"));
+ }
+
+ @computed
+ get layoutKeys(): Key[] {
+ return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array<Key>());
+ }
+
+ @computed
+ get layoutFields(): Key[] {
+ return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array<Key>());
+ }
+
+ //
+ // returns the cumulative scaling between the document and the screen
+ //
+ @computed
+ public get ScalingToScreenSpace(): number {
+ if (this.props.ContainingCollectionView != undefined &&
+ this.props.ContainingCollectionView.props.ContainingDocumentView != undefined) {
+ let ss = this.props.ContainingCollectionView.props.DocumentForCollection.GetData(KeyStore.Scale, NumberField, Number(1));
+ return this.props.ContainingCollectionView.props.ContainingDocumentView.ScalingToScreenSpace * ss;
+ }
+ return 1;
+ }
+
+ //
+ // Converts a coordinate in the screen space of the app into a local document coordinate.
+ //
+ public TransformToLocalPoint(screenX: number, screenY: number) {
+ // if this collection view is nested within another collection view, then
+ // first transform the screen point into the parent collection's coordinate space.
+ let { LocalX: parentX, LocalY: parentY } = this.props.ContainingCollectionView != undefined &&
+ this.props.ContainingCollectionView.props.ContainingDocumentView != undefined ?
+ this.props.ContainingCollectionView.props.ContainingDocumentView.TransformToLocalPoint(screenX, screenY) :
+ { LocalX: screenX, LocalY: screenY };
+ let ContainerX: number = parentX - COLLECTION_BORDER_WIDTH;
+ let ContainerY: number = parentY - COLLECTION_BORDER_WIDTH;
+
+ var Xx = this.props.Document.GetData(KeyStore.X, NumberField, Number(0));
+ var Yy = this.props.Document.GetData(KeyStore.Y, NumberField, Number(0));
+ // CollectionDockingViews change the location of their children frames without using a Dash transformation.
+ // They also ignore any transformation that may have been applied to their content document.
+ // NOTE: this currently assumes CollectionDockingViews aren't nested.
+ if (this.props.ContainingCollectionView instanceof CollectionDockingView) {
+ var { translateX: rx, translateY: ry } = Utils.GetScreenTransform(this.MainContent.current!);
+ Xx = rx - COLLECTION_BORDER_WIDTH;
+ Yy = ry - COLLECTION_BORDER_WIDTH;
+ }
+
+ let Ss = this.props.Document.GetData(KeyStore.Scale, NumberField, Number(1));
+ let Panxx = this.props.Document.GetData(KeyStore.PanX, NumberField, Number(0));
+ let Panyy = this.props.Document.GetData(KeyStore.PanY, NumberField, Number(0));
+ let LocalX = (ContainerX - (Xx + Panxx)) / Ss;
+ let LocalY = (ContainerY - (Yy + Panyy)) / Ss;
+
+ return { LocalX, Ss, Panxx, Xx, LocalY, Panyy, Yy, ContainerX, ContainerY };
+ }
+
+ //
+ // Converts a point in the coordinate space of a document to a screen space coordinate.
+ //
+ public TransformToScreenPoint(localX: number, localY: number, Ss: number = 1, Panxx: number = 0, Panyy: number = 0): { ScreenX: number, ScreenY: number } {
+
+ var Xx = this.props.Document.GetData(KeyStore.X, NumberField, Number(0));
+ var Yy = this.props.Document.GetData(KeyStore.Y, NumberField, Number(0));
+ // CollectionDockingViews change the location of their children frames without using a Dash transformation.
+ // They also ignore any transformation that may have been applied to their content document.
+ // NOTE: this currently assumes CollectionDockingViews aren't nested.
+ if (this.props.ContainingCollectionView instanceof CollectionDockingView) {
+ var { translateX: rx, translateY: ry } = Utils.GetScreenTransform(this.MainContent.current!);
+ Xx = rx - COLLECTION_BORDER_WIDTH;
+ Yy = ry - COLLECTION_BORDER_WIDTH;
+ }
+
+ let W = COLLECTION_BORDER_WIDTH;
+ let H = COLLECTION_BORDER_WIDTH;
+ let parentX = (localX - W) * Ss + (Xx + Panxx) + W;
+ let parentY = (localY - H) * Ss + (Yy + Panyy) + H;
+
+ // if this collection view is nested within another collection view, then
+ // first transform the local point into the parent collection's coordinate space.
+ let containingDocView = this.props.ContainingCollectionView != undefined ? this.props.ContainingCollectionView.props.ContainingDocumentView : undefined;
+ if (containingDocView != undefined) {
+ let ss = containingDocView.props.Document.GetData(KeyStore.Scale, NumberField, Number(1));
+ let panxx = containingDocView.props.Document.GetData(KeyStore.PanX, NumberField, Number(0)) + COLLECTION_BORDER_WIDTH * ss;
+ let panyy = containingDocView.props.Document.GetData(KeyStore.PanY, NumberField, Number(0)) + COLLECTION_BORDER_WIDTH * ss;
+ let { ScreenX, ScreenY } = containingDocView.TransformToScreenPoint(parentX, parentY, ss, panxx, panyy);
+ parentX = ScreenX;
+ parentY = ScreenY;
+ }
+ return { ScreenX: parentX, ScreenY: parentY };
+ }
+
+
+ render() {
+ let bindings = { ...this.props } as any;
+ for (const key of this.layoutKeys) {
+ bindings[key.Name + "Key"] = key; // this maps string values of the form <keyname>Key to an actual key Kestore.keyname e.g, "DataKey" => KeyStore.Data
+ }
+ for (const key of this.layoutFields) {
+ let field = this.props.Document.Get(key);
+ bindings[key.Name] = field && field != FieldWaiting ? field.GetValue() : field;
+ }
+ if (bindings.DocumentView === undefined) {
+ bindings.DocumentView = this; // set the DocumentView to this if it hasn't already been set by a sub-class during its render method.
+ }
+ return (
+ <div className="node" ref={this._mainCont} style={{ width: "100%", height: "100%", }}>
+ <JsxParser
+ components={{ FormattedTextBox: FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView }}
+ bindings={bindings}
+ jsx={this.layout}
+ showWarnings={true}
+ onError={(test: any) => { console.log(test) }}
+ />
+ </div>
+ )
+ }
+}
diff --git a/src/client/views/nodes/FieldTextBox.scss b/src/client/views/nodes/FieldTextBox.scss
new file mode 100644
index 000000000..b6ce2fabc
--- /dev/null
+++ b/src/client/views/nodes/FieldTextBox.scss
@@ -0,0 +1,14 @@
+.ProseMirror {
+ margin-top: -1em;
+ width: 100%;
+ height: 100%;
+}
+
+.ProseMirror:focus {
+ outline: none !important
+}
+
+.fieldTextBox-cont {
+ background: white;
+ padding: 1vw;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
new file mode 100644
index 000000000..12371eb2e
--- /dev/null
+++ b/src/client/views/nodes/FieldView.tsx
@@ -0,0 +1,56 @@
+import React = require("react")
+import { observer } from "mobx-react";
+import { computed } from "mobx";
+import { Field, Opt, FieldWaiting, FieldValue } from "../../../fields/Field";
+import { Document } from "../../../fields/Document";
+import { TextField } from "../../../fields/TextField";
+import { NumberField } from "../../../fields/NumberField";
+import { RichTextField } from "../../../fields/RichTextField";
+import { ImageField } from "../../../fields/ImageField";
+import { Key } from "../../../fields/Key";
+import { FormattedTextBox } from "./FormattedTextBox";
+import { ImageBox } from "./ImageBox";
+import { DocumentView } from "./DocumentView";
+
+//
+// these properties get assigned through the render() method of the DocumentView when it creates this node.
+// However, that only happens because the properties are "defined" in the markup for the field view.
+// See the LayoutString method on each field view : ImageBox, FormattedTextBox, etc.
+//
+export interface FieldViewProps {
+ fieldKey: Key;
+ doc: Document;
+ DocumentViewForField: Opt<DocumentView>
+}
+
+@observer
+export class FieldView extends React.Component<FieldViewProps> {
+ public static LayoutString(fieldType: string) { return `<${fieldType} doc={Document} DocumentViewForField={DocumentView} fieldKey={DataKey} />`; }
+ @computed
+ get field(): FieldValue<Field> {
+ const { doc, fieldKey } = this.props;
+ return doc.Get(fieldKey);
+ }
+ render() {
+ const field = this.field;
+ if (!field) {
+ return <p>{'<null>'}</p>
+ }
+ if (field instanceof TextField) {
+ return <p>{field.Data}</p>
+ }
+ else if (field instanceof RichTextField) {
+ return <FormattedTextBox {...this.props} />
+ }
+ else if (field instanceof ImageField) {
+ return <ImageBox {...this.props} />
+ }
+ else if (field instanceof NumberField) {
+ return <p>{field.Data}</p>
+ } else if (field != FieldWaiting) {
+ return <p>{field.GetValue}</p>
+ } else
+ return <p> {"Waiting for server..."} </p>
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
new file mode 100644
index 000000000..492367fce
--- /dev/null
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -0,0 +1,14 @@
+.ProseMirror {
+ margin-top: -1em;
+ width: 100%;
+ height: 100%;
+}
+
+.ProseMirror:focus {
+ outline: none !important
+}
+
+.formattedTextBox-cont {
+ background: white;
+ padding: 1vw;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
new file mode 100644
index 000000000..8bc4c902c
--- /dev/null
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -0,0 +1,127 @@
+import { action, IReactionDisposer, reaction } from "mobx";
+import { observer } from "mobx-react"
+import { baseKeymap } from "prosemirror-commands";
+import { history, redo, undo } from "prosemirror-history";
+import { keymap } from "prosemirror-keymap";
+import { schema } from "prosemirror-schema-basic";
+import { EditorState, Transaction } from "prosemirror-state";
+import { EditorView } from "prosemirror-view";
+import { Opt, FieldWaiting, FieldValue } from "../../../fields/Field";
+import { SelectionManager } from "../../util/SelectionManager";
+import "./FormattedTextBox.scss";
+import React = require("react")
+import { RichTextField } from "../../../fields/RichTextField";
+import { FieldViewProps, FieldView } from "./FieldView";
+import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView";
+
+
+// FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document
+//
+// HTML Markup: <FormattedTextBox Doc={Document's ID} FieldKey={Key's name + "Key"}
+//
+// In Code, the node's HTML is specified in the document's parameterized structure as:
+// document.SetField(KeyStore.Layout, "<FormattedTextBox doc={doc} fieldKey={<KEYNAME>Key} />");
+// and the node's binding to the specified document KEYNAME as:
+// document.SetField(KeyStore.LayoutKeys, new ListField([KeyStore.<KEYNAME>]));
+// The Jsx parser at run time will bind:
+// 'fieldKey' property to the Key stored in LayoutKeys
+// and 'doc' property to the document that is being rendered
+//
+// When rendered() by React, this extracts the TextController from the Document stored at the
+// specified Key and assigns it to an HTML input node. When changes are made tot his node,
+// this will edit the document and assign the new value to that field.
+//]
+@observer
+export class FormattedTextBox extends React.Component<FieldViewProps> {
+
+ public static LayoutString() { return FieldView.LayoutString("FormattedTextBox"); }
+ private _ref: React.RefObject<HTMLDivElement>;
+ private _editorView: Opt<EditorView>;
+ private _reactionDisposer: Opt<IReactionDisposer>;
+
+ constructor(props: FieldViewProps) {
+ super(props);
+
+ this._ref = React.createRef();
+
+ this.onChange = this.onChange.bind(this);
+ }
+
+ dispatchTransaction = (tx: Transaction) => {
+ if (this._editorView) {
+ const state = this._editorView.state.apply(tx);
+ this._editorView.updateState(state);
+ const { doc, fieldKey } = this.props;
+ doc.SetData(fieldKey, JSON.stringify(state.toJSON()), RichTextField);
+ }
+ }
+
+ componentDidMount() {
+ let state: EditorState;
+ const { doc, fieldKey } = this.props;
+ const config = {
+ schema,
+ plugins: [
+ history(),
+ keymap({ "Mod-z": undo, "Mod-y": redo }),
+ keymap(baseKeymap)
+ ]
+ };
+
+ let field = doc.GetT(fieldKey, RichTextField);
+ if (field && field != FieldWaiting) { // bcz: don't think this works
+ state = EditorState.fromJSON(config, JSON.parse(field.Data));
+ } else {
+ state = EditorState.create(config);
+ }
+ if (this._ref.current) {
+ this._editorView = new EditorView(this._ref.current, {
+ state,
+ dispatchTransaction: this.dispatchTransaction
+ });
+ }
+
+ this._reactionDisposer = reaction(() => {
+ const field = this.props.doc.GetT(this.props.fieldKey, RichTextField);
+ return field && field != FieldWaiting ? field.Data : undefined;
+ }, (field) => {
+ if (field && this._editorView) {
+ this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field)));
+ }
+ })
+ }
+
+ componentWillUnmount() {
+ if (this._editorView) {
+ this._editorView.destroy();
+ }
+ if (this._reactionDisposer) {
+ this._reactionDisposer();
+ }
+ }
+
+ shouldComponentUpdate() {
+ return false;
+ }
+
+ @action
+ onChange(e: React.ChangeEvent<HTMLInputElement>) {
+ const { fieldKey, doc } = this.props;
+ doc.SetData(fieldKey, e.target.value, RichTextField);
+ }
+ onPointerDown = (e: React.PointerEvent): void => {
+ let me = this;
+ if (e.buttons === 1 && me.props.DocumentViewForField instanceof CollectionFreeFormDocumentView && SelectionManager.IsSelected(me.props.DocumentViewForField)) {
+ e.stopPropagation();
+ }
+ }
+ render() {
+ return (<div className="formattedTextBox-cont"
+ style={{
+ color: "initial",
+ whiteSpace: "initial"
+ }}
+ onPointerDown={this.onPointerDown}
+ ref={this._ref} />)
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
new file mode 100644
index 000000000..136fda1d0
--- /dev/null
+++ b/src/client/views/nodes/ImageBox.scss
@@ -0,0 +1,11 @@
+
+.imageBox-cont {
+ padding: 0vw;
+}
+
+.imageBox-button {
+ padding : 0vw;
+ border: none;
+ width : 100%;
+ height: 100%;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
new file mode 100644
index 000000000..ab20f140c
--- /dev/null
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -0,0 +1,92 @@
+
+import Lightbox from 'react-image-lightbox';
+import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
+import { SelectionManager } from "../../util/SelectionManager";
+import "./ImageBox.scss";
+import React = require("react")
+import { ImageField } from '../../../fields/ImageField';
+import { FieldViewProps, FieldView } from './FieldView';
+import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView';
+import { FieldWaiting } from '../../../fields/Field';
+import { observer } from "mobx-react"
+import { observable, action } from 'mobx';
+
+@observer
+export class ImageBox extends React.Component<FieldViewProps> {
+
+ public static LayoutString() { return FieldView.LayoutString("ImageBox"); }
+ private _ref: React.RefObject<HTMLDivElement>;
+ private _downX: number = 0;
+ private _downY: number = 0;
+ private _lastTap: number = 0;
+ @observable private _photoIndex: number = 0;
+ @observable private _isOpen: boolean = false;
+
+ constructor(props: FieldViewProps) {
+ super(props);
+
+ this._ref = React.createRef();
+ this.state = {
+ photoIndex: 0,
+ isOpen: false,
+ };
+ }
+
+ componentDidMount() {
+ }
+
+ componentWillUnmount() {
+ }
+
+ onPointerDown = (e: React.PointerEvent): void => {
+ if (Date.now() - this._lastTap < 300) {
+ if (e.buttons === 1 && this.props.DocumentViewForField instanceof CollectionFreeFormDocumentView && SelectionManager.IsSelected(this.props.DocumentViewForField)) {
+ e.stopPropagation();
+ this._downX = e.clientX;
+ this._downY = e.clientY;
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointerup", this.onPointerUp);
+ }
+ } else {
+ this._lastTap = Date.now();
+ }
+ }
+ @action
+ onPointerUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointerup", this.onPointerUp);
+ if (Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2) {
+ this._isOpen = true;
+ }
+ e.stopPropagation();
+ }
+
+ lightbox = (path: string) => {
+ const images = [path, "http://www.cs.brown.edu/~bcz/face.gif"];
+ if (this._isOpen && this.props.DocumentViewForField instanceof CollectionFreeFormDocumentView && SelectionManager.IsSelected(this.props.DocumentViewForField)) {
+ return (<Lightbox
+ mainSrc={images[this._photoIndex]}
+ nextSrc={images[(this._photoIndex + 1) % images.length]}
+ prevSrc={images[(this._photoIndex + images.length - 1) % images.length]}
+ onCloseRequest={() => this.setState({ isOpen: false })}
+ onMovePrevRequest={action(() =>
+ this._photoIndex = (this._photoIndex + images.length - 1) % images.length
+ )}
+ onMoveNextRequest={action(() =>
+ this._photoIndex = (this._photoIndex + 1) % images.length
+ )}
+ />)
+ }
+ }
+
+ render() {
+ let field = this.props.doc.Get(this.props.fieldKey);
+ let path = field == FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" :
+ field instanceof ImageField ? field.Data.href : "http://www.cs.brown.edu/~bcz/face.gif";
+
+ return (
+ <div className="imageBox-cont" onPointerDown={this.onPointerDown} ref={this._ref} >
+ <img src={path} width="100%" alt="Image not found" />
+ {this.lightbox(path)}
+ </div>)
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/NodeView.scss b/src/client/views/nodes/NodeView.scss
new file mode 100644
index 000000000..dac1c0a8e
--- /dev/null
+++ b/src/client/views/nodes/NodeView.scss
@@ -0,0 +1,23 @@
+.node {
+ position: absolute;
+ background: #cdcdcd;
+ overflow: hidden;
+ &.minimized {
+ width: 30px;
+ height: 30px;
+ }
+ .top {
+ background: #232323;
+ height: 20px;
+ cursor: pointer;
+ }
+ .content {
+ padding: 20px 20px;
+ height: auto;
+ box-sizing: border-box;
+ }
+ .scroll-box {
+ overflow-y: scroll;
+ height: calc(100% - 20px);
+ }
+} \ No newline at end of file