diff options
| author | Andrew Kim <andrewdkim@users.noreply.github.com> | 2019-03-05 18:51:20 -0500 |
|---|---|---|
| committer | Andrew Kim <andrewdkim@users.noreply.github.com> | 2019-03-05 18:51:20 -0500 |
| commit | 7f93e6639e8fee3e3760d13c69d65b343875091a (patch) | |
| tree | d29b45310f92a53935177d969ce3c1bee9920c32 /src/client/views/nodes | |
| parent | 9b839a93b98b850aa77087218d4862b97fb24d15 (diff) | |
| parent | 2cc5eb6ff512dc6128d25903bcb852f25bcadcca (diff) | |
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web into PDFNode
Diffstat (limited to 'src/client/views/nodes')
| -rw-r--r-- | src/client/views/nodes/Annotation.tsx | 117 | ||||
| -rw-r--r-- | src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 86 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentView.scss | 23 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 250 | ||||
| -rw-r--r-- | src/client/views/nodes/FieldTextBox.scss | 14 | ||||
| -rw-r--r-- | src/client/views/nodes/FieldView.tsx | 73 | ||||
| -rw-r--r-- | src/client/views/nodes/FormattedTextBox.scss | 20 | ||||
| -rw-r--r-- | src/client/views/nodes/FormattedTextBox.tsx | 149 | ||||
| -rw-r--r-- | src/client/views/nodes/ImageBox.scss | 22 | ||||
| -rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 111 | ||||
| -rw-r--r-- | src/client/views/nodes/KeyValueBox.scss | 31 | ||||
| -rw-r--r-- | src/client/views/nodes/KeyValueBox.tsx | 85 | ||||
| -rw-r--r-- | src/client/views/nodes/KeyValuePair.tsx | 58 | ||||
| -rw-r--r-- | src/client/views/nodes/PDFNode.tsx | 453 | ||||
| -rw-r--r-- | src/client/views/nodes/Sticky.tsx | 83 | ||||
| -rw-r--r-- | src/client/views/nodes/WebBox.scss | 14 | ||||
| -rw-r--r-- | src/client/views/nodes/WebBox.tsx | 38 |
17 files changed, 1627 insertions, 0 deletions
diff --git a/src/client/views/nodes/Annotation.tsx b/src/client/views/nodes/Annotation.tsx new file mode 100644 index 000000000..a2c7be1a8 --- /dev/null +++ b/src/client/views/nodes/Annotation.tsx @@ -0,0 +1,117 @@ +import "./ImageBox.scss"; +import React = require("react") +import { observer } from "mobx-react" +import { observable, action } from 'mobx'; +import 'react-pdf/dist/Page/AnnotationLayer.css' + +interface IProps{ + Span: HTMLSpanElement; + X: number; + Y: number; + Highlights: any[]; + Annotations: any[]; + CurrAnno: any[]; + +} + +/** + * Annotation class is used to take notes on a particular highlight. You can also change highlighted span's color + * Improvements to be made: Removing the annotation when onRemove is called. (Removing this, not just the highlighted span). + * Also need to support multiline highlighting + * + * Written by: Andrew Kim + */ +@observer +export class Annotation extends React.Component<IProps> { + + /** + * changes color of the span (highlighted section) + */ + onColorChange = (e:React.PointerEvent) => { + if (e.currentTarget.innerHTML == "r"){ + this.props.Span.style.backgroundColor = "rgba(255,0,0, 0.3)" + } else if (e.currentTarget.innerHTML == "b"){ + this.props.Span.style.backgroundColor = "rgba(0,255, 255, 0.3)" + } else if (e.currentTarget.innerHTML == "y"){ + this.props.Span.style.backgroundColor = "rgba(255,255,0, 0.3)" + } else if (e.currentTarget.innerHTML == "g"){ + this.props.Span.style.backgroundColor = "rgba(76, 175, 80, 0.3)" + } + + } + + /** + * removes the highlighted span. Supposed to remove Annotation too, but I don't know how to unmount this + */ + @action + onRemove = (e:any) => { + let index:number = -1; + //finding the highlight in the highlight array + this.props.Highlights.forEach((e) => { + for (let i = 0; i < e.spans.length; i++){ + if (e.spans[i] == this.props.Span){ + index = this.props.Highlights.indexOf(e); + this.props.Highlights.splice(index, 1); + } + } + }) + + //removing from CurrAnno and Annotation array + this.props.Annotations.splice(index, 1); + this.props.CurrAnno.pop() + + //removing span from div + if(this.props.Span.parentElement){ + let nodesArray = this.props.Span.parentElement.childNodes; + nodesArray.forEach((e) => { + if (e == this.props.Span){ + if (this.props.Span.parentElement){ + this.props.Highlights.forEach((item) => { + if (item == e){ + item.remove(); + } + }) + e.remove(); + } + } + }) + } + + + } + + render() { + return ( + <div + style = {{ + position: "absolute", + top: "20px", + left: "0px", + zIndex: 1, + transform: `translate(${this.props.X}px, ${this.props.Y}px)`, + + }}> + <div style = {{width:"200px", height:"50px", backgroundColor: "orange"}}> + <button + style = {{borderRadius: "25px", width:"25%", height:"100%"}} + onClick = {this.onRemove} + >x</button> + <div style = {{width:"75%", height: "100%" , display:"inline-block"}}> + <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"red", borderRadius:"50%", color: "transparent"}}>r</button> + <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"blue", borderRadius:"50%", color: "transparent"}}>b</button> + <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"yellow", borderRadius:"50%", color:"transparent"}}>y</button> + <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"green", borderRadius:"50%", color:"transparent"}}>g</button> + </div> + + </div> + <div style = {{width:"200px", height:"200"}}> + <textarea style = {{width: "100%", height: "100%"}} + defaultValue = "Enter Text Here..." + + ></textarea> + </div> + </div> + + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx new file mode 100644 index 000000000..50dc5a619 --- /dev/null +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -0,0 +1,86 @@ +import { computed, trace } from "mobx"; +import { observer } from "mobx-react"; +import { KeyStore } from "../../../fields/KeyStore"; +import { NumberField } from "../../../fields/NumberField"; +import { Transform } from "../../util/Transform"; +import { DocumentView, DocumentViewProps } from "./DocumentView"; +import "./DocumentView.scss"; +import React = require("react"); + + +@observer +export class CollectionFreeFormDocumentView extends React.Component<DocumentViewProps> { + private _mainCont = React.createRef<HTMLDivElement>(); + + constructor(props: DocumentViewProps) { + super(props); + } + get screenRect(): ClientRect | DOMRect { + if (this._mainCont.current) { + return this._mainCont.current.getBoundingClientRect(); + } + return new DOMRect(); + } + + @computed + get transform(): string { + return `scale(${this.props.ContentScaling()}, ${this.props.ContentScaling()}) translate(${this.props.Document.GetNumber(KeyStore.X, 0)}px, ${this.props.Document.GetNumber(KeyStore.Y, 0)}px)`; + } + + @computed get zIndex(): number { return this.props.Document.GetNumber(KeyStore.ZIndex, 0); } + @computed get width(): number { return this.props.Document.Width(); } + @computed get height(): number { return this.props.Document.Height(); } + @computed get nativeWidth(): number { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } + @computed get nativeHeight(): number { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } + + set width(w: number) { + this.props.Document.SetData(KeyStore.Width, w, NumberField) + if (this.nativeWidth && this.nativeHeight) { + this.props.Document.SetNumber(KeyStore.Height, this.nativeHeight / this.nativeWidth * w) + } + } + + set height(h: number) { + this.props.Document.SetData(KeyStore.Height, h, NumberField); + if (this.nativeWidth && this.nativeHeight) { + this.props.Document.SetNumber(KeyStore.Width, this.nativeWidth / this.nativeHeight * h) + } + } + + set zIndex(h: number) { + this.props.Document.SetData(KeyStore.ZIndex, h, NumberField) + } + + contentScaling = () => { + return this.nativeWidth > 0 ? this.width / this.nativeWidth : 1; + } + + getTransform = (): Transform => { + return this.props.ScreenToLocalTransform(). + translate(-this.props.Document.GetNumber(KeyStore.X, 0), -this.props.Document.GetNumber(KeyStore.Y, 0)).scale(1 / this.contentScaling()); + } + + @computed + get docView() { + return <DocumentView {...this.props} + ContentScaling={this.contentScaling} + ScreenToLocalTransform={this.getTransform} + /> + } + + render() { + return ( + <div className="collectionFreeFormDocumentView-container" ref={this._mainCont} style={{ + transformOrigin: "left top", + transform: this.transform, + width: this.width, + height: this.height, + position: "absolute", + zIndex: this.zIndex, + backgroundColor: "transparent" + }} > + {this.docView} + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss new file mode 100644 index 000000000..8e2ebd690 --- /dev/null +++ b/src/client/views/nodes/DocumentView.scss @@ -0,0 +1,23 @@ +.documentView-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 diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx new file mode 100644 index 000000000..e01e1d4cd --- /dev/null +++ b/src/client/views/nodes/DocumentView.tsx @@ -0,0 +1,250 @@ +import { action, computed } from "mobx"; +import { observer } from "mobx-react"; +import { Document } from "../../../fields/Document"; +import { Field, FieldWaiting, Opt } from "../../../fields/Field"; +import { Key } from "../../../fields/Key"; +import { KeyStore } from "../../../fields/KeyStore"; +import { ListField } from "../../../fields/ListField"; +import { DragManager } from "../../util/DragManager"; +import { SelectionManager } from "../../util/SelectionManager"; +import { Transform } from "../../util/Transform"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { CollectionFreeFormView } from "../collections/CollectionFreeFormView"; +import { CollectionSchemaView } from "../collections/CollectionSchemaView"; +import { CollectionView, CollectionViewType } from "../collections/CollectionView"; +import { ContextMenu } from "../ContextMenu"; +import { FormattedTextBox } from "../nodes/FormattedTextBox"; +import { ImageBox } from "../nodes/ImageBox"; +import { Documents } from "../../documents/Documents" +import { KeyValueBox } from "./KeyValueBox" +import { WebBox } from "../nodes/WebBox"; +import "./DocumentView.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 { + ContainingCollectionView: Opt<CollectionView>; + Document: Document; + AddDocument?: (doc: Document) => void; + RemoveDocument?: (doc: Document) => boolean; + ScreenToLocalTransform: () => Transform; + isTopMost: boolean; + ContentScaling: () => number; + PanelWidth: () => number; + PanelHeight: () => number; + focus: (doc: Document) => void; + SelectOnLoad: boolean; +} +export interface JsxArgs extends DocumentViewProps { + Keys: { [name: string]: Key } + Fields: { [name: string]: Field } +} + +/* +This function is pretty much a hack that lets us fill out the fields in JsxArgs with something that +jsx-to-string can recover the jsx from +Example usage of this function: + public static LayoutString() { + let args = FakeJsxArgs(["Data"]); + return jsxToString( + <CollectionFreeFormView + doc={args.Document} + fieldKey={args.Keys.Data} + DocumentViewForField={args.DocumentView} />, + { useFunctionCode: true, functionNameOnly: true } + ) + } +*/ +export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs { + let Keys: { [name: string]: any } = {} + let Fields: { [name: string]: any } = {} + for (const key of keys) { + let fn = () => { } + Object.defineProperty(fn, "name", { value: key + "Key" }) + Keys[key] = fn; + } + for (const field of fields) { + let fn = () => { } + Object.defineProperty(fn, "name", { value: field }) + Fields[field] = fn; + } + let args: JsxArgs = { + Document: function Document() { }, + DocumentView: function DocumentView() { }, + Keys, + Fields + } as any; + return args; +} + +@observer +export class DocumentView extends React.Component<DocumentViewProps> { + private _mainCont = React.createRef<HTMLDivElement>(); + private _documentBindings: any = null; + private _downX: number = 0; + private _downY: number = 0; + @computed get active(): boolean { return SelectionManager.IsSelected(this) || !this.props.ContainingCollectionView || this.props.ContainingCollectionView.active(); } + @computed get topMost(): boolean { return !this.props.ContainingCollectionView || this.props.ContainingCollectionView.collectionViewType == CollectionViewType.Docking; } + @computed get layout(): string { return this.props.Document.GetText(KeyStore.Layout, "<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>()); } + screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect(); + onPointerDown = (e: React.PointerEvent): void => { + this._downX = e.clientX; + this._downY = e.clientY; + if (e.shiftKey && e.buttons === 1) { + CollectionDockingView.Instance.StartOtherDrag(this.props.Document, e); + e.stopPropagation(); + } else { + 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) { + return; + } + if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { + document.removeEventListener("pointermove", this.onPointerMove) + document.removeEventListener("pointerup", this.onPointerUp) + if (this._mainCont.current != null && !this.topMost) { + const [left, top] = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + let dragData: { [id: string]: any } = {}; + dragData["documentView"] = this; + dragData["xOffset"] = e.x - left; + dragData["yOffset"] = e.y - top; + DragManager.StartDrag(this._mainCont.current, dragData, { + handlers: { + dragComplete: action(() => { }), + }, + 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); + } + } + + deleteClicked = (): void => { + if (this.props.RemoveDocument) { + this.props.RemoveDocument(this.props.Document); + } + } + + fieldsClicked = (e: React.MouseEvent): void => { + if (this.props.AddDocument) { + this.props.AddDocument(Documents.KVPDocument(this.props.Document)); + } + } + fullScreenClicked = (e: React.MouseEvent): void => { + CollectionDockingView.Instance.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) + } + + closeFullScreenClicked = (e: React.MouseEvent): void => { + CollectionDockingView.Instance.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 => { + e.stopPropagation(); + let moved = Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3; + if (moved || e.isDefaultPrevented()) { + e.preventDefault() + return; + } + e.preventDefault() + + ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }) + ContextMenu.Instance.addItem({ description: "Fields", event: this.fieldsClicked }) + ContextMenu.Instance.addItem({ description: "Center", event: () => this.props.focus(this.props.Document) }) + ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document) }) + //ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) }) + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) + if (!this.topMost) { + // DocumentViews should stop propagation of this event + e.stopPropagation(); + } + + ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked }) + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) + SelectionManager.SelectDoc(this, e.ctrlKey); + } + @computed get mainContent() { + return <JsxParser + components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox }} + bindings={this._documentBindings} + jsx={this.layout} + showWarnings={true} + onError={(test: any) => { console.log(test) }} + /> + } + + isSelected = () => { + return SelectionManager.IsSelected(this); + } + + select = (ctrlPressed: boolean) => { + SelectionManager.SelectDoc(this, ctrlPressed) + } + + render() { + if (!this.props.Document) return <div></div> + let lkeys = this.props.Document.GetT(KeyStore.LayoutKeys, ListField); + if (!lkeys || lkeys === "<Waiting>") { + return <p>Error loading layout keys</p>; + } + this._documentBindings = { + ...this.props, + isSelected: this.isSelected, + select: this.select, + focus: this.props.focus + }; + for (const key of this.layoutKeys) { + this._documentBindings[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); + this._documentBindings[key.Name] = field && field != FieldWaiting ? field.GetValue() : field; + } + this._documentBindings.bindings = this._documentBindings; + var scaling = this.props.ContentScaling(); + var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); + var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0); + return ( + <div className="documentView-node" ref={this._mainCont} + style={{ + width: nativeWidth > 0 ? nativeWidth.toString() + "px" : "100%", + height: nativeHeight > 0 ? nativeHeight.toString() + "px" : "100%", + transformOrigin: "left top", + transform: `scale(${scaling} , ${scaling})` + }} + onContextMenu={this.onContextMenu} + onPointerDown={this.onPointerDown} > + {this.mainContent} + </div> + ) + } +}
\ No newline at end of file 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..9e63006d1 --- /dev/null +++ b/src/client/views/nodes/FieldView.tsx @@ -0,0 +1,73 @@ +import React = require("react") +import { observer } from "mobx-react"; +import { computed } from "mobx"; +import { Field, 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 { WebField } from "../../../fields/WebField"; +import { Key } from "../../../fields/Key"; +import { FormattedTextBox } from "./FormattedTextBox"; +import { ImageBox } from "./ImageBox"; +import { WebBox } from "./WebBox"; + +// +// 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; + isSelected: () => boolean; + select: () => void; + isTopMost: boolean; + selectOnLoad: boolean; + bindings: any; +} + +@observer +export class FieldView extends React.Component<FieldViewProps> { + public static LayoutString(fieldType: { name: string }, fieldStr: string = "DataKey") { + return `<${fieldType.name} doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={${fieldStr}} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost} />`; + } + + @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 WebField) { + return <WebBox {...this.props} /> + } + // bcz: this belongs here, but it doesn't render well so taking it out for now + // else if (field instanceof HtmlField) { + // return <WebBox {...this.props} /> + // } + else if (field instanceof NumberField) { + return <p>{field.Data}</p> + } + else if (field != FieldWaiting) { + return <p>{JSON.stringify(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..21bd43b6e --- /dev/null +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -0,0 +1,20 @@ +.ProseMirror { + width: 100%; + height: auto; + min-height: 100% +} + +.ProseMirror:focus { + outline: none !important +} + +.formattedTextBox-cont { + background: white; + padding: 1; + border: black; + border-width: 10; + overflow-y: scroll; + overflow-x: hidden; + color: initial; + height: 100%; +}
\ 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..04eb2052d --- /dev/null +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -0,0 +1,149 @@ +import { action, IReactionDisposer, reaction } from "mobx"; +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 } from "../../../fields/Field"; +import "./FormattedTextBox.scss"; +import React = require("react") +import { RichTextField } from "../../../fields/RichTextField"; +import { FieldViewProps, FieldView } from "./FieldView"; +import { ContextMenu } from "../../views/ContextMenu"; + + + + +// 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. +//] +export class FormattedTextBox extends React.Component<FieldViewProps> { + + public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(FormattedTextBox, fieldStr) } + 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); + this.props.doc.SetData(this.props.fieldKey, JSON.stringify(state.toJSON()), RichTextField); + } + } + + componentDidMount() { + let state: EditorState; + const config = { + schema, + plugins: [ + history(), + keymap({ "Mod-z": undo, "Mod-y": redo }), + keymap(baseKeymap), + ] + }; + + let field = this.props.doc.GetT(this.props.fieldKey, RichTextField); + if (field && field != FieldWaiting) { + 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))); + } + }) + if (this.props.selectOnLoad) { + this.props.select(); + this._editorView!.focus(); + } + } + + componentWillUnmount() { + if (this._editorView) { + this._editorView.destroy(); + } + if (this._reactionDisposer) { + this._reactionDisposer(); + } + } + + shouldComponentUpdate() { + return false; + } + + @action + onChange(e: React.ChangeEvent<HTMLInputElement>) { + this.props.doc.SetData(this.props.fieldKey, e.target.value, RichTextField); + } + onPointerDown = (e: React.PointerEvent): void => { + if (e.buttons === 1 && this.props.isSelected()) { + e.stopPropagation(); + } + } + + //REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE + textCapability = (e: React.MouseEvent): void => { + } + + specificContextMenu = (e: React.MouseEvent): void => { + ContextMenu.Instance.addItem({ description: "Text Capability", event: this.textCapability }); + // ContextMenu.Instance.addItem({ + // description: "Submenu", + // items: [ + // { + // description: "item 1", event: + // }, + // { + // description: "item 2", event: + // } + // ] + // }) + // e.stopPropagation() + + } + + onPointerWheel = (e: React.WheelEvent): void => { + e.stopPropagation(); + } + + render() { + return (<div className="formattedTextBox-cont" + onPointerDown={this.onPointerDown} + onContextMenu={this.specificContextMenu} + onWheel={this.onPointerWheel} + 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..ea459b911 --- /dev/null +++ b/src/client/views/nodes/ImageBox.scss @@ -0,0 +1,22 @@ + +.imageBox-cont { + padding: 0vw; + position: relative; + text-align: center; + width: 100%; + height: auto; + max-width: 100%; + max-height: 100% +} + +.imageBox-cont img { + object-fit: contain; + height: 100%; +} + +.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..8c44395f4 --- /dev/null +++ b/src/client/views/nodes/ImageBox.tsx @@ -0,0 +1,111 @@ + +import Lightbox from 'react-image-lightbox'; +import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app +import "./ImageBox.scss"; +import React = require("react") +import { ImageField } from '../../../fields/ImageField'; +import { FieldViewProps, FieldView } from './FieldView'; +import { FieldWaiting } from '../../../fields/Field'; +import { observer } from "mobx-react" +import { ContextMenu } from "../../views/ContextMenu"; +import { observable, action } from 'mobx'; +import { KeyStore } from '../../../fields/KeyStore'; + +@observer +export class ImageBox extends React.Component<FieldViewProps> { + + public static LayoutString() { return FieldView.LayoutString(ImageBox) } + private _ref: React.RefObject<HTMLDivElement>; + private _imgRef: React.RefObject<HTMLImageElement>; + 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._imgRef = React.createRef(); + this.state = { + photoIndex: 0, + isOpen: false, + }; + } + + @action + onLoad = (target: any) => { + var h = this._imgRef.current!.naturalHeight; + var w = this._imgRef.current!.naturalWidth; + this.props.doc.SetNumber(KeyStore.NativeHeight, this.props.doc.GetNumber(KeyStore.NativeWidth, 0) * h / w) + } + + componentDidMount() { + } + + componentWillUnmount() { + } + + onPointerDown = (e: React.PointerEvent): void => { + if (Date.now() - this._lastTap < 300) { + if (e.buttons === 1 && this.props.isSelected()) { + 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.isSelected()) { + return (<Lightbox + mainSrc={images[this._photoIndex]} + nextSrc={images[(this._photoIndex + 1) % images.length]} + prevSrc={images[(this._photoIndex + images.length - 1) % images.length]} + onCloseRequest={action(() => + this._isOpen = false + )} + onMovePrevRequest={action(() => + this._photoIndex = (this._photoIndex + images.length - 1) % images.length + )} + onMoveNextRequest={action(() => + this._photoIndex = (this._photoIndex + 1) % images.length + )} + />) + } + } + + //REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE + imageCapability = (e: React.MouseEvent): void => { + } + + specificContextMenu = (e: React.MouseEvent): void => { + ContextMenu.Instance.addItem({ description: "Image Capability", event: this.imageCapability }); + } + + 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"; + let nativeWidth = this.props.doc.GetNumber(KeyStore.NativeWidth, 1); + return ( + <div className="imageBox-cont" onPointerDown={this.onPointerDown} ref={this._ref} onContextMenu={this.specificContextMenu}> + <img src={path} width={nativeWidth} alt="Image not found" ref={this._imgRef} onLoad={this.onLoad} /> + {this.lightbox(path)} + </div>) + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss new file mode 100644 index 000000000..1295266e5 --- /dev/null +++ b/src/client/views/nodes/KeyValueBox.scss @@ -0,0 +1,31 @@ +.keyValueBox-cont { + overflow-y:scroll; + height: 100%; + border: black; + border-width: 1px; + border-style: solid; + box-sizing: border-box; + display: inline-block; + .imageBox-cont img { + max-height:45px; + height: auto; + } +} +.keyValueBox-table { + position: relative; +} +.keyValueBox-header { + background:gray; +} +.keyValueBox-evenRow { + background: white; + .formattedTextBox-cont { + background: white; + } +} +.keyValueBox-oddRow { + background: lightGray; + .formattedTextBox-cont { + background: lightgray; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx new file mode 100644 index 000000000..e8ebd50be --- /dev/null +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -0,0 +1,85 @@ + +import { IReactionDisposer } from 'mobx'; +import { observer } from "mobx-react"; +import { EditorView } from 'prosemirror-view'; +import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app +import { Document } from '../../../fields/Document'; +import { Opt, FieldWaiting } from '../../../fields/Field'; +import { KeyStore } from '../../../fields/KeyStore'; +import { FieldView, FieldViewProps } from './FieldView'; +import { KeyValuePair } from "./KeyValuePair"; +import "./KeyValueBox.scss"; +import React = require("react") + +@observer +export class KeyValueBox extends React.Component<FieldViewProps> { + + public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(KeyValueBox, fieldStr) } + private _ref: React.RefObject<HTMLDivElement>; + private _editorView: Opt<EditorView>; + private _reactionDisposer: Opt<IReactionDisposer>; + + + constructor(props: FieldViewProps) { + super(props); + + this._ref = React.createRef(); + } + + + + shouldComponentUpdate() { + return false; + } + + + onPointerDown = (e: React.PointerEvent): void => { + if (e.buttons === 1 && this.props.isSelected()) { + e.stopPropagation(); + } + } + onPointerWheel = (e: React.WheelEvent): void => { + e.stopPropagation(); + } + + createTable = () => { + let doc = this.props.doc.GetT(KeyStore.Data, Document); + if (!doc || doc == FieldWaiting) { + return <tr><td>Loading...</td></tr> + } + let realDoc = doc; + + let ids: { [key: string]: string } = {}; + let protos = doc.GetAllPrototypes(); + for (const proto of protos) { + proto._proxies.forEach((val, key) => { + if (!(key in ids)) { + ids[key] = key; + } + }) + } + + let rows: JSX.Element[] = []; + let i = 0; + for (let key in ids) { + rows.push(<KeyValuePair doc={realDoc} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} fieldId={key} key={key} />) + } + return rows; + } + + + render() { + + return (<div className="keyValueBox-cont" onWheel={this.onPointerWheel}> + <table className="keyValueBox-table"> + <tbody> + <tr className="keyValueBox-header"> + <th>Key</th> + <th>Fields</th> + </tr> + {this.createTable()} + </tbody> + </table> + </div>) + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx new file mode 100644 index 000000000..a97e98313 --- /dev/null +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -0,0 +1,58 @@ +import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app +import "./KeyValueBox.scss"; +import React = require("react") +import { FieldViewProps, FieldView } from './FieldView'; +import { Opt, Field } from '../../../fields/Field'; +import { observer } from "mobx-react" +import { observable, action } from 'mobx'; +import { Document } from '../../../fields/Document'; +import { Key } from '../../../fields/Key'; +import { Server } from "../../Server" + +// Represents one row in a key value plane + +export interface KeyValuePairProps { + rowStyle: string; + fieldId: string; + doc: Document; +} +@observer +export class KeyValuePair extends React.Component<KeyValuePairProps> { + + @observable + private key: Opt<Key> + + constructor(props: KeyValuePairProps) { + super(props); + Server.GetField(this.props.fieldId, + action((field: Opt<Field>) => { + if (field) { + this.key = field as Key; + } + })); + + } + + + render() { + if (!this.key) { + return <tr><td>error</td><td></td></tr> + + } + let props: FieldViewProps = { + doc: this.props.doc, + fieldKey: this.key, + isSelected: () => false, + select: () => { }, + isTopMost: false, + bindings: {}, + selectOnLoad: false, + } + return ( + <tr className={this.props.rowStyle}> + <td>{this.key.Name}</td> + <td><FieldView {...props} /></td> + </tr> + ) + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/PDFNode.tsx b/src/client/views/nodes/PDFNode.tsx new file mode 100644 index 000000000..755994d6d --- /dev/null +++ b/src/client/views/nodes/PDFNode.tsx @@ -0,0 +1,453 @@ +import 'react-image-lightbox/style.css'; +import "./ImageBox.scss"; +import React = require("react") +import { observer } from "mobx-react" +import { observable, action } from 'mobx'; +import 'react-pdf/dist/Page/AnnotationLayer.css' +//@ts-ignore +import { Document, Page, PDFPageProxy, PageAnnotation } from "react-pdf"; +import { Utils } from '../../../Utils'; +import { Sticky } from './Sticky'; //you should look at sticky and annotation, because they are used here +import { Annotation } from './Annotation'; +import { ObjectPositionProperty } from 'csstype'; +import { keydownHandler } from 'prosemirror-keymap'; +import { FieldViewProps, FieldView } from './FieldView'; + +/** ALSO LOOK AT: Annotation.tsx, Sticky.tsx + * This method renders PDF and puts all kinds of functionalities such as annotation, highlighting, + * area selection (I call it stickies), embedded ink node for directly annotating using a pen or + * mouse, and pagination. + * + * + * HOW TO USE: + * AREA selection: + * 1) Click on Area button. + * 2) click on any part of the PDF, and drag to get desired sized area shape + * 3) You can write on the area (hence the reason why it's called sticky) + * 4) to make another area, you need to click on area button AGAIN. + * + * HIGHLIGHT: (Buggy. No multiline/multidiv text highlighting for now...) + * 1) just click and drag on a text + * 2) click highlight + * 3) for annotation, just pull your cursor over to that text + * 4) another method: click on highlight first and then drag on your desired text + * 5) To make another highlight, you need to reclick on the button + * + * Draw: + * 1) click draw and select color. then just draw like there's no tomorrow. + * 2) once you finish drawing your masterpiece, just reclick on the draw button to end your drawing session. + * + * Pagination: + * 1) click on arrows. You'll notice that stickies will stay in those page. But... highlights won't. + * 2) to test this out, make few area/stickies and then click on next page then come back. You'll see that they are all saved. + * + * + * written by: Andrew Kim + */ +@observer +export class PDFNode extends React.Component<FieldViewProps> { + public static LayoutString() { return FieldView.LayoutString(PDFNode); } + + private _mainDiv = React.createRef<HTMLDivElement>() + private _pdf = React.createRef<HTMLCanvasElement>(); + + //very useful for keeping track of X and y position throughout the PDF Canvas + private initX: number = 0; + private initY: number = 0; + + //checks if tool is on + private _toolOn: boolean = false; //checks if tool is on + private _pdfContext: any = null; //gets pdf context + private bool: Boolean = false; //general boolean debounce + private currSpan: any;//keeps track of current span (for highlighting) + + private _currTool: any; //keeps track of current tool button reference + private _drawToolOn: boolean = false; //boolean that keeps track of the drawing tool + private _drawTool = React.createRef<HTMLButtonElement>()//drawing tool button reference + + private _colorTool = React.createRef<HTMLButtonElement>(); //color button reference + private _currColor: string = "black"; //current color that user selected (for ink/pen) + + private _highlightTool = React.createRef<HTMLButtonElement>(); //highlighter button reference + private _highlightToolOn: boolean = false; + + @observable perPage: Object[] = []; //stores pageInfo + @observable pageInfo: any = { area: [], divs: [], anno: [] }; //divs is array of objects linked to anno + + @observable private page: number = 1; //default is the first page. + @observable private numPage: number = 1; //default number of pages + private _pdfCanvas: any; + + /** + * for pagination backwards + */ + @action + onPageBack = () => { + if (this.page > 1) { + this.page -= 1; + this.currAnno = []; + this.perPage[this.page] = this.pageInfo + this.pageInfo = { area: [], divs: [], anno: [] }; //resets the object to default + if (this.perPage[this.page - 1]) { + this.pageInfo = this.perPage[this.page - 1]; + } + } + } + + /** + * for pagination forwards + */ + @action + onPageForward = () => { + if (this.page < this.numPage) { + this.page += 1; + this.currAnno = []; + this.perPage[this.page - 2] = this.pageInfo; + this.pageInfo = { area: [], divs: [], anno: [] }; //resets the object to default + if (this.perPage[this.page - 1]) { + this.pageInfo = this.perPage[this.page - 1]; + } + } + } + + /** + * selection tool used for area highlighting (stickies). Kinda temporary + */ + selectionTool = () => { + this._toolOn = true; + } + /** + * when user draws on the canvas. When mouse pointer is down + */ + drawDown = (e: PointerEvent) => { + this.initX = e.offsetX; + this.initY = e.offsetY; + this._pdfContext.beginPath(); + this._pdfContext.lineTo(this.initX, this.initY); + this._pdfContext.strokeStyle = this._currColor; + this._pdfCanvas.addEventListener("pointermove", this.drawMove); + this._pdfCanvas.addEventListener("pointerup", this.drawUp); + + } + //when user drags + drawMove = (e: PointerEvent): void => { + //x and y mouse movement + let x = this.initX += e.movementX, + y = this.initY += e.movementY; + //connects the point + this._pdfContext.lineTo(x, y); + this._pdfContext.stroke(); + } + + drawUp = (e: PointerEvent) => { + this._pdfContext.closePath(); + this._pdfCanvas.removeEventListener("pointermove", this.drawMove); + this._pdfCanvas.removeEventListener("pointerdown", this.drawDown); + this._pdfCanvas.addEventListener("pointerdown", this.drawDown); + } + + + /** + * highlighting helper function + */ + makeEditableAndHighlight = (colour: string) => { + var range, sel = window.getSelection(); + if (sel.rangeCount && sel.getRangeAt) { + range = sel.getRangeAt(0); + } + document.designMode = "on"; + if (!document.execCommand("HiliteColor", false, colour)) { + document.execCommand("HiliteColor", false, colour); + } + + if (range) { + sel.removeAllRanges(); + sel.addRange(range); + + let obj: Object = { parentDivs: [], spans: [] }; + //@ts-ignore + if (range.commonAncestorContainer.className == 'react-pdf__Page__textContent') { //multiline highlighting case + obj = this.highlightNodes(range.commonAncestorContainer.childNodes) + } else { //single line highlighting case + let parentDiv = range.commonAncestorContainer.parentElement + if (parentDiv) { + if (parentDiv.className == 'react-pdf__Page__textContent') { //when highlight is overwritten + obj = this.highlightNodes(parentDiv.childNodes) + } else { + parentDiv.childNodes.forEach((child) => { + if (child.nodeName == 'SPAN') { + //@ts-ignore + obj.parentDivs.push(parentDiv) + //@ts-ignore + child.id = "highlighted" + //@ts-ignore + obj.spans.push(child) + child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler + } + }) + } + } + } + this.pageInfo.divs.push(obj); + + } + document.designMode = "off"; + } + + highlightNodes = (nodes: NodeListOf<ChildNode>) => { + let temp = { parentDivs: [], spans: [] } + nodes.forEach((div) => { + div.childNodes.forEach((child) => { + if (child.nodeName == 'SPAN') { + //@ts-ignore + temp.parentDivs.push(div) + //@ts-ignore + child.id = "highlighted" + //@ts-ignore + temp.spans.push(child) + child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler + } + }) + + }) + return temp; + } + + /** + * when the cursor enters the highlight, it pops out annotation. ONLY WORKS FOR SINGLE DIV LINES + */ + @observable private currAnno: any = [] + @action + onEnter = (e: any) => { + let span: HTMLSpanElement = e.toElement; + let index: any; + this.pageInfo.divs.forEach((obj: any) => { + obj.spans.forEach((element: any) => { + if (element == span) { + if (!index) { + index = this.pageInfo.divs.indexOf(obj); + } + } + }) + }) + + if (this.pageInfo.anno.length >= index + 1) { + if (this.currAnno.length == 0) { + this.currAnno.push(this.pageInfo.anno[index]); + } + } else { + if (this.currAnno.length == 0) { //if there are no current annotation + let div = span.offsetParent; + //@ts-ignore + let divX = div.style.left + //@ts-ignore + let divY = div.style.top + //slicing "px" from the end + divX = divX.slice(0, divX.length - 2); //gets X of the DIV element (parent of Span) + divY = divY.slice(0, divY.length - 2); //gets Y of the DIV element (parent of Span) + let annotation = <Annotation key={Utils.GenerateGuid()} Span={span} X={divX} Y={divY - 300} Highlights={this.pageInfo.divs} Annotations={this.pageInfo.anno} CurrAnno={this.currAnno} /> + this.pageInfo.anno.push(annotation); + this.currAnno.push(annotation); + } + } + + } + + /** + * highlight function for highlighting actual text. This works fine. + */ + highlight = (color: string) => { + if (window.getSelection()) { + try { + if (!document.execCommand("hiliteColor", false, color)) { + this.makeEditableAndHighlight(color); + } + } catch (ex) { + this.makeEditableAndHighlight(color) + } + } + } + + /** + * controls the area highlighting (stickies) Kinda temporary + */ + onPointerDown = (e: React.PointerEvent) => { + if (this._toolOn) { + let mouse = e.nativeEvent; + this.initX = mouse.offsetX; + this.initY = mouse.offsetY; + + } + } + + /** + * controls area highlighting and partially highlighting. Kinda temporary + */ + @action + onPointerUp = (e: React.PointerEvent) => { + + if (this._highlightToolOn) { + this.highlight("rgba(76, 175, 80, 0.3)"); //highlights to this default color. + this._highlightToolOn = false; + } + if (this._toolOn) { + let mouse = e.nativeEvent; + let finalX = mouse.offsetX; + let finalY = mouse.offsetY; + let width = Math.abs(finalX - this.initX); //width + let height = Math.abs(finalY - this.initY); //height + + //these two if statements are bidirectional dragging. You can drag from any point to another point and generate sticky + if (finalX < this.initX) { + this.initX = finalX; + } + if (finalY < this.initY) { + this.initY = finalY; + } + + if (this._mainDiv.current) { + let sticky = <Sticky key={Utils.GenerateGuid()} Height={height} Width={width} X={this.initX} Y={this.initY} /> + this.pageInfo.area.push(sticky); + } + this._toolOn = false; + } + + } + + /** + * starts drawing the line when user presses down. + */ + onDraw = () => { + if (this._currTool != null) { + this._currTool.style.backgroundColor = "grey"; + } + + if (this._drawTool.current) { + this._currTool = this._drawTool.current; + if (this._drawToolOn) { + this._drawToolOn = false; + this._pdfCanvas.removeEventListener("pointerdown", this.drawDown); + this._pdfCanvas.removeEventListener("pointerup", this.drawUp); + this._pdfCanvas.removeEventListener("pointermove", this.drawMove); + this._drawTool.current.style.backgroundColor = "grey"; + } else { + this._drawToolOn = true; + this._pdfCanvas.addEventListener("pointerdown", this.drawDown); + this._drawTool.current.style.backgroundColor = "cyan"; + } + } + } + + + /** + * for changing color (for ink/pen) + */ + onColorChange = (e: React.PointerEvent) => { + if (e.currentTarget.innerHTML == "Red") { + this._currColor = "red"; + } else if (e.currentTarget.innerHTML == "Blue") { + this._currColor = "blue"; + } else if (e.currentTarget.innerHTML == "Green") { + this._currColor = "green"; + } else if (e.currentTarget.innerHTML == "Black") { + this._currColor = "black"; + } + + } + + + /** + * For highlighting (text drag highlighting) + */ + onHighlight = () => { + this._drawToolOn = false; + if (this._currTool != null) { + this._currTool.style.backgroundColor = "grey"; + } + if (this._highlightTool.current) { + this._currTool = this._drawTool.current; + if (this._highlightToolOn) { + this._highlightToolOn = false; + this._highlightTool.current.style.backgroundColor = "grey"; + } else { + this._highlightToolOn = true; + this._highlightTool.current.style.backgroundColor = "orange"; + } + } + } + + + /** + * renders whole lot of shets, including pdf, stickies, and annotations. + */ + + reHighlight = () => { + let div = document.getElementsByClassName("react-pdf__Page__textContent"); + if (div) { + + } + + } + + + render() { + return ( + <div ref={this._mainDiv} + onPointerDown={this.onPointerDown} + onPointerUp={this.onPointerUp} + > + + {this.pageInfo.area.filter(() => { + return this.pageInfo.area + }).map((element: any) => { + return element + }) + } + {this.currAnno.map((element: any) => { + return element + })} + + <button onClick={this.onPageBack}>{"<"}</button> + <button onClick={this.onPageForward}>{">"}</button> + <button onClick={this.selectionTool}>{"Area"}</button> + <button style={{ color: "white", backgroundColor: "grey" }} onClick={this.onHighlight} ref={this._highlightTool}>Highlight</button> + <button style={{ color: "white", backgroundColor: "grey" }} ref={this._drawTool} onClick={this.onDraw}>{"Draw"}</button> + <button ref={this._colorTool} onPointerDown={this.onColorChange}>{"Red"}</button> + <button ref={this._colorTool} onPointerDown={this.onColorChange}>{"Blue"}</button> + <button ref={this._colorTool} onPointerDown={this.onColorChange}>{"Green"}</button> + <button ref={this._colorTool} onPointerDown={this.onColorChange}>{"Black"}</button> + + <Document file={"https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf"}> + <Page + pageNumber={this.page} + onLoadSuccess={ + (page: any) => { + if (this._mainDiv.current) { + this._mainDiv.current.childNodes.forEach((element) => { + if (element.nodeName == "DIV") { + element.childNodes[0].childNodes.forEach((e) => { + + if (e.nodeName == "CANVAS") { + this._pdfCanvas = e; + //@ts-ignore + this._pdfContext = e.getContext("2d") + + } + + }) + } + }) + } + this.numPage = page.transport.numPages + if (this.perPage.length == 0) { //Makes sure it only runs once + this.perPage = [...Array(this.numPage)] + } + } + } + /> + </Document> + </div> + ); + } + +}
\ No newline at end of file diff --git a/src/client/views/nodes/Sticky.tsx b/src/client/views/nodes/Sticky.tsx new file mode 100644 index 000000000..d57dd5c0b --- /dev/null +++ b/src/client/views/nodes/Sticky.tsx @@ -0,0 +1,83 @@ +import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app +import React = require("react") +import { observer } from "mobx-react" +import 'react-pdf/dist/Page/AnnotationLayer.css' + +interface IProps { + Height: number; + Width: number; + X: number; + Y: number; +} + +/** + * Sticky, also known as area highlighting, is used to highlight large selection of the PDF file. + * Improvements that could be made: maybe store line array and store that somewhere for future rerendering. + * + * Written By: Andrew Kim + */ +@observer +export class Sticky extends React.Component<IProps> { + + private initX: number = 0; + private initY: number = 0; + + private _ref = React.createRef<HTMLCanvasElement>(); + private ctx: any; //context that keeps track of sticky canvas + + /** + * drawing. Registers the first point that user clicks when mouse button is pressed down on canvas + */ + drawDown = (e: React.PointerEvent) => { + if (this._ref.current) { + this.ctx = this._ref.current.getContext("2d"); + let mouse = e.nativeEvent; + this.initX = mouse.offsetX; + this.initY = mouse.offsetY; + this.ctx.beginPath(); + this.ctx.lineTo(this.initX, this.initY); + this.ctx.strokeStyle = "black"; + document.addEventListener("pointermove", this.drawMove); + document.addEventListener("pointerup", this.drawUp); + } + } + + //when user drags + drawMove = (e: PointerEvent): void => { + //x and y mouse movement + let x = this.initX += e.movementX, + y = this.initY += e.movementY; + //connects the point + this.ctx.lineTo(x, y); + this.ctx.stroke(); + + } + + /** + * when user lifts the mouse, the drawing ends + */ + drawUp = (e: PointerEvent) => { + this.ctx.closePath(); + console.log(this.ctx); + document.removeEventListener("pointermove", this.drawMove); + } + + render() { + return ( + <div onPointerDown={this.drawDown}> + <canvas ref={this._ref} height={this.props.Height} width={this.props.Width} + style={{ + position: "absolute", + top: "20px", + left: "0px", + zIndex: 1, + background: "yellow", + transform: `translate(${this.props.X}px, ${this.props.Y}px)`, + opacity: 0.4 + }} + /> + + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss new file mode 100644 index 000000000..e72b3c4da --- /dev/null +++ b/src/client/views/nodes/WebBox.scss @@ -0,0 +1,14 @@ + +.webBox-cont { + padding: 0vw; + position: absolute; + width: 100%; + height: 100%; +} + +.webBox-button { + padding : 0vw; + border: none; + width : 100%; + height: 100%; +}
\ No newline at end of file diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx new file mode 100644 index 000000000..2ca8d49ce --- /dev/null +++ b/src/client/views/nodes/WebBox.tsx @@ -0,0 +1,38 @@ +import "./WebBox.scss"; +import React = require("react") +import { WebField } from '../../../fields/WebField'; +import { FieldViewProps, FieldView } from './FieldView'; +import { FieldWaiting } from '../../../fields/Field'; +import { observer } from "mobx-react" +import { computed } from 'mobx'; +import { KeyStore } from '../../../fields/KeyStore'; + +@observer +export class WebBox extends React.Component<FieldViewProps> { + + public static LayoutString() { return FieldView.LayoutString(WebBox); } + + constructor(props: FieldViewProps) { + super(props); + } + + @computed get html(): string { return this.props.doc.GetHtml(KeyStore.Data, ""); } + + 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 WebField ? field.Data.href : "https://crossorigin.me/" + "https://cs.brown.edu"; + + let content = this.html ? + <span dangerouslySetInnerHTML={{ __html: this.html }}></span> : + <div style={{ width: "100%", height: "100%", position: "absolute" }}> + <iframe src={path} style={{ position: "absolute", width: "100%", height: "100%" }}></iframe> + {this.props.isSelected() ? (null) : <div style={{ width: "100%", height: "100%", position: "absolute" }} />} + </div>; + + return ( + <div className="webBox-cont" > + {content} + </div>) + } +}
\ No newline at end of file |
