diff options
Diffstat (limited to 'src/client/views/nodes')
26 files changed, 1571 insertions, 190 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/AudioBox.scss b/src/client/views/nodes/AudioBox.scss new file mode 100644 index 000000000..704cdc31c --- /dev/null +++ b/src/client/views/nodes/AudioBox.scss @@ -0,0 +1,4 @@ +.audiobox-cont{ + height: 100%; + width: 100%; +}
\ No newline at end of file diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx new file mode 100644 index 000000000..f7d89843d --- /dev/null +++ b/src/client/views/nodes/AudioBox.tsx @@ -0,0 +1,44 @@ +import React = require("react") +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'; +import { AudioField } from "../../../fields/AudioField"; +import "./AudioBox.scss" +import { NumberField } from "../../../fields/NumberField"; + +@observer +export class AudioBox extends React.Component<FieldViewProps> { + + public static LayoutString() { return FieldView.LayoutString(AudioBox) } + + constructor(props: FieldViewProps) { + super(props); + } + + + + componentDidMount() { + } + + componentWillUnmount() { + } + + + render() { + let field = this.props.doc.Get(this.props.fieldKey) + let path = field == FieldWaiting ? "http://techslides.com/demos/samples/sample.mp3": + field instanceof AudioField ? field.Data.href : "http://techslides.com/demos/samples/sample.mp3"; + + return ( + <div> + <audio controls className = "audiobox-cont"> + <source src = {path} type="audio/mpeg"/> + Not supported. + </audio> + </div> + ) + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 8e2ebd690..ab913897b 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -1,7 +1,7 @@ .documentView-node { position: absolute; background: #cdcdcd; - overflow: hidden; + //overflow: hidden; &.minimized { width: 30px; height: 30px; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index a14239e94..dc793c16d 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,4 +1,4 @@ -import { action, computed } from "mobx"; +import { action, computed, IReactionDisposer, runInAction, reaction } from "mobx"; import { observer } from "mobx-react"; import { Document } from "../../../fields/Document"; import { Field, FieldWaiting, Opt } from "../../../fields/Field"; @@ -12,24 +12,35 @@ import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionFreeFormView } from "../collections/CollectionFreeFormView"; import { CollectionSchemaView } from "../collections/CollectionSchemaView"; import { CollectionView, CollectionViewType } from "../collections/CollectionView"; -import { WebView } from "./WebView"; +import { CollectionPDFView } from "../collections/CollectionPDFView"; import { ContextMenu } from "../ContextMenu"; import { FormattedTextBox } from "../nodes/FormattedTextBox"; import { ImageBox } from "../nodes/ImageBox"; +import { VideoBox } from "../nodes/VideoBox"; +import { AudioBox } from "../nodes/AudioBox"; +import { Documents } from "../../documents/Documents" +import { KeyValueBox } from "./KeyValueBox" +import { WebBox } from "../nodes/WebBox"; +import { PDFBox } from "../nodes/PDFBox"; import "./DocumentView.scss"; import React = require("react"); +import { TextField } from "../../../fields/TextField"; +import { DocumentManager } from "../../util/DocumentManager"; 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; //tfs: This shouldn't be necessary I don't think + isTopMost: boolean; ContentScaling: () => number; PanelWidth: () => number; PanelHeight: () => number; + focus: (doc: Document) => void; + SelectOnLoad: boolean; } export interface JsxArgs extends DocumentViewProps { Keys: { [name: string]: Key } @@ -51,7 +62,6 @@ Example usage of this function: ) } */ - export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs { let Keys: { [name: string]: any } = {} let Fields: { [name: string]: any } = {} @@ -74,41 +84,31 @@ export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs { return args; } -@observer export class DocumentView extends React.Component<DocumentViewProps> { +@observer +export class DocumentView extends React.Component<DocumentViewProps> { private _mainCont = React.createRef<HTMLDivElement>(); private _documentBindings: any = null; - private _contextMenuCanOpen = false; 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>()); - } + private _reactionDisposer: Opt<IReactionDisposer>; + @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._mainCont.current!, this.props.Document); + if (e.shiftKey && e.buttons === 2) { + if (this.props.isTopMost) { + this.startDragging(e.pageX, e.pageY); + } + else CollectionDockingView.Instance.StartOtherDrag(this.props.Document, e); e.stopPropagation(); } else { - this._contextMenuCanOpen = true; 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) @@ -116,33 +116,74 @@ export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs { } } } + + private dropDisposer?: DragManager.DragDropDisposer; + protected createDropTarget = (ele: HTMLDivElement) => { + + } + + componentDidMount() { + if (this._mainCont.current) { + this.dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } }); + } + runInAction(() => { + DocumentManager.Instance.DocumentViews.push(this); + }) + this._reactionDisposer = reaction( + () => this.props.ContainingCollectionView && this.props.ContainingCollectionView.SelectedDocs.slice(), + () => { + if (this.props.ContainingCollectionView && this.props.ContainingCollectionView.SelectedDocs.indexOf(this.props.Document.Id) != -1) + SelectionManager.SelectDoc(this, true); + }); + } + + componentDidUpdate() { + if (this.dropDisposer) { + this.dropDisposer(); + } + if (this._mainCont.current) { + this.dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } }); + } + } + + componentWillUnmount() { + if (this.dropDisposer) { + this.dropDisposer(); + } + runInAction(() => { + DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1); + + }) + if (this._reactionDisposer) { + this._reactionDisposer(); + } + } + + startDragging(x: number, y: number) { + if (this._mainCont.current) { + const [left, top] = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + let dragData: { [id: string]: any } = {}; + dragData["documentView"] = this; + dragData["xOffset"] = x - left; + dragData["yOffset"] = y - top; + DragManager.StartDrag(this._mainCont.current, dragData, { + handlers: { + dragComplete: action(() => { }), + }, + hideSource: true + }) + } + } + 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 [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((e: DragManager.DragCompleteEvent) => { } - ), - } - , hideSource: true - } - ) + document.removeEventListener("pointermove", this.onPointerMove) + document.removeEventListener("pointerup", this.onPointerUp); + if (!this.topMost || e.buttons == 2) { + this.startDragging(e.x, e.y); } } e.stopPropagation(); @@ -156,87 +197,104 @@ export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs { SelectionManager.SelectDoc(this, e.ctrlKey); } } - deleteClicked = (e: React.MouseEvent): void => { + + 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.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.addItem({ description: "Full Screen", event: this.fullScreenClicked }) ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) } - @action onContextMenu = (e: React.MouseEvent): void => { - e.preventDefault() - e.stopPropagation(); - if (!SelectionManager.IsSelected(this) || !this._contextMenuCanOpen) { + + @action + drop = (e: Event, de: DragManager.DropEvent) => { + console.log("drop"); + const sourceDocView: DocumentView = de.data["linkSourceDoc"]; + if (!sourceDocView) { return; } - if (this.topMost) { - ContextMenu.Instance.addItem({ - description: "Full Screen", event: this.fullScreenClicked - }) - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) + let sourceDoc: Document = sourceDocView.props.Document; + let destDoc: Document = this.props.Document; + if (this.props.isTopMost) { + return; } - else { + let linkDoc: Document = new Document(); + + linkDoc.Set(KeyStore.Title, new TextField("New Link")); + linkDoc.Set(KeyStore.LinkDescription, new TextField("")); + linkDoc.Set(KeyStore.LinkTags, new TextField("Default")); + + sourceDoc.GetOrCreateAsync(KeyStore.LinkedToDocs, ListField, field => { (field as ListField<Document>).Data.push(linkDoc) }); + linkDoc.Set(KeyStore.LinkedToDocs, destDoc); + destDoc.GetOrCreateAsync(KeyStore.LinkedFromDocs, ListField, field => { (field as ListField<Document>).Data.push(linkDoc) }); + linkDoc.Set(KeyStore.LinkedFromDocs, sourceDoc); + + + + e.stopPropagation(); + } + + @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 - ContextMenu.Instance.addItem({ - description: "Full Screen", event: this.fullScreenClicked - } - ) - ContextMenu.Instance.addItem({ - description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document) - } - ) - ContextMenu.Instance.addItem({ - description: "Delete", event: this.deleteClicked - } - ) - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) - SelectionManager.SelectDoc(this, e.ctrlKey); + 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() { - var val = this.props.Document.Id; - return <JsxParser components={ - { - FormattedTextBox, - ImageBox, - CollectionFreeFormView, - CollectionDockingView, - CollectionSchemaView, - CollectionView, - WebView - } - } - bindings={ - this._documentBindings - } - jsx={ - this.layout - } - showWarnings={ - true - } - onError={ - (test: any) => { - console.log(test) - } - } + + get mainContent() { + return <JsxParser + components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, VideoBox, AudioBox, PDFBox }} + 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); @@ -245,9 +303,10 @@ export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs { } this._documentBindings = { ...this.props, - isSelected: () => SelectionManager.IsSelected(this), select: (ctrlPressed: boolean) => SelectionManager.SelectDoc(this, ctrlPressed) - } - ; + 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 } @@ -259,29 +318,18 @@ export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs { 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>) + 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/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 1a9d325db..49f4cefce 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -1,17 +1,22 @@ import React = require("react") import { observer } from "mobx-react"; import { computed } from "mobx"; -import { Field, Opt, FieldWaiting, FieldValue } from "../../../fields/Field"; +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 { VideoField } from "../../../fields/VideoField" import { Key } from "../../../fields/Key"; import { FormattedTextBox } from "./FormattedTextBox"; import { ImageBox } from "./ImageBox"; -import { HtmlField } from "../../../fields/HtmlField"; -import { WebView } from "./WebView"; +import { WebBox } from "./WebBox"; +import { VideoBox } from "./VideoBox"; +import { AudioBox } from "./AudioBox"; +import { AudioField } from "../../../fields/AudioField"; + // // these properties get assigned through the render() method of the DocumentView when it creates this node. @@ -24,12 +29,15 @@ export interface FieldViewProps { 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} isTopMost={isTopMost} />`; } + 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> { @@ -50,13 +58,26 @@ export class FieldView extends React.Component<FieldViewProps> { else if (field instanceof ImageField) { return <ImageBox {...this.props} /> } + else if (field instanceof WebField) { + return <WebBox {...this.props} /> + } + else if (field instanceof VideoField){ + return <VideoBox {...this.props}/> + } + else if (field instanceof AudioField){ + return <AudioBox {...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 instanceof HtmlField) { - return <WebView {...this.props} /> - } else if (field != FieldWaiting) { - return <p>{field.GetValue}</p> - } else + } + else if (field != FieldWaiting) { + return <p>{JSON.stringify(field.GetValue())}</p> + } + else return <p> {"Waiting for server..."} </p> } diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 0389a3f85..ab5849f09 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -9,10 +9,30 @@ } .formattedTextBox-cont { - background: beige; - padding: 0; + background: white; + padding: 1; + border-width: 1px; + border-radius: 2px; + border-color:black; + box-sizing: border-box; + background: white; + border-style:solid; overflow-y: scroll; overflow-x: hidden; color: initial; height: 100%; -}
\ No newline at end of file +} + +.menuicon { + display: inline-block; + border-right: 1px solid rgba(0, 0, 0, 0.2); + color: #888; + line-height: 1; + padding: 0 7px; + margin: 1px; + cursor: pointer; + text-align: center; + min-width: 1.4em; + } + .strong, .heading { font-weight: bold; } + .em { font-style: italic; }
\ No newline at end of file diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index baedf0852..411b43cd5 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -2,17 +2,22 @@ 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 { schema } from "../../util/RichTextSchema"; +import { EditorState, Transaction, } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; -import { Opt, FieldWaiting, FieldValue } from "../../../fields/Field"; +import { Opt, FieldWaiting } from "../../../fields/Field"; import "./FormattedTextBox.scss"; import React = require("react") import { RichTextField } from "../../../fields/RichTextField"; import { FieldViewProps, FieldView } from "./FieldView"; +import { Plugin } from 'prosemirror-state' +import { Decoration, DecorationSet } from 'prosemirror-view' +import { TooltipTextMenu } from "../../util/TooltipTextMenu" 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"} @@ -40,7 +45,6 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { super(props); this._ref = React.createRef(); - this.onChange = this.onChange.bind(this); } @@ -48,25 +52,24 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { 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); + this.props.doc.SetData(this.props.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) + keymap(baseKeymap), + this.tooltipMenuPlugin() ] }; - let field = doc.GetT(fieldKey, RichTextField); - if (field && field != FieldWaiting) { // bcz: don't think this works + let field = this.props.doc.GetT(this.props.fieldKey, RichTextField); + if (field && field != FieldWaiting && field.Data) { state = EditorState.fromJSON(config, JSON.parse(field.Data)); } else { state = EditorState.create(config); @@ -86,6 +89,10 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field))); } }) + if (this.props.selectOnLoad) { + this.props.select(); + this._editorView!.focus(); + } } componentWillUnmount() { @@ -103,11 +110,9 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { @action onChange(e: React.ChangeEvent<HTMLInputElement>) { - const { fieldKey, doc } = this.props; - doc.SetData(fieldKey, e.target.value, RichTextField); + this.props.doc.SetData(this.props.fieldKey, e.target.value, RichTextField); } onPointerDown = (e: React.PointerEvent): void => { - let me = this; if (e.buttons === 1 && this.props.isSelected()) { e.stopPropagation(); } @@ -139,8 +144,20 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { e.stopPropagation(); } + tooltipMenuPlugin() { + return new Plugin({ + view(_editorView) { + return new TooltipTextMenu(_editorView) + } + }) + } + + onKeyPress(e: React.KeyboardEvent) { + e.stopPropagation(); + } render() { return (<div className="formattedTextBox-cont" + onKeyPress={this.onKeyPress} onPointerDown={this.onPointerDown} onContextMenu={this.specificContextMenu} onWheel={this.onPointerWheel} diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 8c44395f4..30910fb1f 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,15 +1,15 @@ +import { action, observable } from 'mobx'; +import { observer } from "mobx-react"; 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 { ImageField } from '../../../fields/ImageField'; import { KeyStore } from '../../../fields/KeyStore'; +import { ContextMenu } from "../../views/ContextMenu"; +import { FieldView, FieldViewProps } from './FieldView'; +import "./ImageBox.scss"; +import React = require("react") @observer export class ImageBox extends React.Component<FieldViewProps> { 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..ac8c949a9 --- /dev/null +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -0,0 +1,66 @@ + +import { observer } from "mobx-react"; +import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app +import { Document } from '../../../fields/Document'; +import { FieldWaiting } from '../../../fields/Field'; +import { KeyStore } from '../../../fields/KeyStore'; +import { FieldView, FieldViewProps } from './FieldView'; +import "./KeyValueBox.scss"; +import { KeyValuePair } from "./KeyValuePair"; +import React = require("react") + +@observer +export class KeyValueBox extends React.Component<FieldViewProps> { + + public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(KeyValueBox, fieldStr) } + + 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/LinkBox.scss b/src/client/views/nodes/LinkBox.scss new file mode 100644 index 000000000..00e5ebb3d --- /dev/null +++ b/src/client/views/nodes/LinkBox.scss @@ -0,0 +1,39 @@ +.link-container { + width: 100%; + height: 30px; + display: flex; + flex-direction: row; + border-top: 0.5px solid #bababa; +} + +.info-container { + width: 60%; + padding-top: 5px; + padding-left: 5px; + display: flex; + flex-direction: column +} + +.link-name { + font-size: 11px; +} + +.doc-name { + font-size: 8px; +} + +.button-container { + width: 40%; + display: flex; + flex-direction: row; +} + +.button { + height: 15px; + width: 15px; + margin: 8px 5px; + border-radius: 50%; + opacity: 0.6; + pointer-events: auto; + background-color: #2B6091; +}
\ No newline at end of file diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx new file mode 100644 index 000000000..69df676ff --- /dev/null +++ b/src/client/views/nodes/LinkBox.tsx @@ -0,0 +1,89 @@ +import { observable, computed, action } from "mobx"; +import React = require("react"); +import { SelectionManager } from "../../util/SelectionManager"; +import { observer } from "mobx-react"; +import './LinkBox.scss' +import { KeyStore } from '../../../fields/KeyStore' +import { props } from "bluebird"; +import { DocumentView } from "./DocumentView"; +import { Document } from "../../../fields/Document"; +import { ListField } from "../../../fields/ListField"; +import { DocumentManager } from "../../util/DocumentManager"; +import { LinkEditor } from "./LinkEditor"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; + +interface Props { + linkDoc: Document; + linkName: String; + pairedDoc: Document; + type: String; + showEditor: () => void +} + +@observer +export class LinkBox extends React.Component<Props> { + + onViewButtonPressed = (e: React.PointerEvent): void => { + console.log("view down"); + e.stopPropagation(); + let docView = DocumentManager.Instance.getDocumentView(this.props.pairedDoc); + if (docView) { + docView.props.focus(this.props.pairedDoc); + } else { + CollectionDockingView.Instance.AddRightSplit(this.props.pairedDoc) + } + } + + onEditButtonPressed = (e: React.PointerEvent): void => { + console.log("edit down"); + e.stopPropagation(); + + this.props.showEditor(); + } + + onDeleteButtonPressed = (e: React.PointerEvent): void => { + console.log("delete down"); + e.stopPropagation(); + this.props.linkDoc.GetTAsync(KeyStore.LinkedFromDocs, Document, field => { + if (field) { + field.GetTAsync<ListField<Document>>(KeyStore.LinkedToDocs, ListField, field => { + if (field) { + field.Data.splice(field.Data.indexOf(this.props.linkDoc)); + } + }) + } + }); + this.props.linkDoc.GetTAsync(KeyStore.LinkedToDocs, Document, field => { + if (field) { + field.GetTAsync<ListField<Document>>(KeyStore.LinkedFromDocs, ListField, field => { + if (field) { + field.Data.splice(field.Data.indexOf(this.props.linkDoc)); + } + }) + } + }); + } + + render() { + + return ( + //<LinkEditor linkBox={this} linkDoc={this.props.linkDoc} /> + <div className="link-container"> + <div className="info-container" onPointerDown={this.onViewButtonPressed}> + <div className="link-name"> + <p>{this.props.linkName}</p> + </div> + <div className="doc-name"> + <p>{this.props.type}{this.props.pairedDoc.Title}</p> + </div> + </div> + + <div className="button-container"> + <div className="button" onPointerDown={this.onViewButtonPressed}></div> + <div className="button" onPointerDown={this.onEditButtonPressed}></div> + <div className="button" onPointerDown={this.onDeleteButtonPressed}></div> + </div> + </div> + ) + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss new file mode 100644 index 000000000..cb191dc8c --- /dev/null +++ b/src/client/views/nodes/LinkEditor.scss @@ -0,0 +1,29 @@ +.edit-container { + width: 100%; + height: auto; + display: flex; + flex-direction: column; +} + +.name-input { + margin-bottom: 10px; + padding: 5px; + font-size: 12px; +} + +.description-input { + font-size: 12px; + padding: 5px; + margin-bottom: 10px; +} + +.save-button { + width: 50px; + height: 20px; + background-color: #2B6091; + margin: 0 auto; + color: white; + text-align: center; + line-height: 20px; + font-size: 12px; +}
\ No newline at end of file diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx new file mode 100644 index 000000000..3f7b4bf2d --- /dev/null +++ b/src/client/views/nodes/LinkEditor.tsx @@ -0,0 +1,58 @@ +import { observable, computed, action } from "mobx"; +import React = require("react"); +import { SelectionManager } from "../../util/SelectionManager"; +import { observer } from "mobx-react"; +import './LinkEditor.scss' +import { KeyStore } from '../../../fields/KeyStore' +import { props } from "bluebird"; +import { DocumentView } from "./DocumentView"; +import { Document } from "../../../fields/Document"; +import { TextField } from "../../../fields/TextField"; +import { link } from "fs"; + +interface Props { + linkDoc: Document; + showLinks: () => void; +} + +@observer +export class LinkEditor extends React.Component<Props> { + + @observable private _nameInput: string = this.props.linkDoc.GetText(KeyStore.Title, ""); + @observable private _descriptionInput: string = this.props.linkDoc.GetText(KeyStore.LinkDescription, ""); + + + onSaveButtonPressed = (e: React.PointerEvent): void => { + console.log("view down"); + e.stopPropagation(); + + this.props.linkDoc.SetData(KeyStore.Title, this._nameInput, TextField); + this.props.linkDoc.SetData(KeyStore.LinkDescription, this._descriptionInput, TextField); + + this.props.showLinks(); + } + + + + render() { + + return ( + <div className="edit-container"> + <input onChange={this.onNameChanged} className="name-input" type="text" value={this._nameInput} placeholder="Name . . ."></input> + <textarea onChange={this.onDescriptionChanged} className="description-input" value={this._descriptionInput} placeholder="Description . . ."></textarea> + <div className="save-button" onPointerDown={this.onSaveButtonPressed}>SAVE</div> + </div> + + ) + } + + @action + onNameChanged = (e: React.ChangeEvent<HTMLInputElement>) => { + this._nameInput = e.target.value; + } + + @action + onDescriptionChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) => { + this._descriptionInput = e.target.value; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/nodes/LinkMenu.scss new file mode 100644 index 000000000..a120ab2a7 --- /dev/null +++ b/src/client/views/nodes/LinkMenu.scss @@ -0,0 +1,20 @@ +#linkMenu-container { + width: 100%; + height: auto; + display: flex; + flex-direction: column; +} + +#linkMenu-searchBar { + width: 100%; + padding: 5px; + margin-bottom: 10px; + font-size: 12px; +} + +#linkMenu-list { + margin-top: 5px; + width: 100%; + height: 100px; + overflow-y: scroll; +}
\ No newline at end of file diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx new file mode 100644 index 000000000..5c6b06d00 --- /dev/null +++ b/src/client/views/nodes/LinkMenu.tsx @@ -0,0 +1,54 @@ +import { action, observable } from "mobx"; +import { observer } from "mobx-react"; +import { Document } from "../../../fields/Document"; +import { FieldWaiting } from "../../../fields/Field"; +import { Key } from "../../../fields/Key"; +import { KeyStore } from '../../../fields/KeyStore'; +import { ListField } from "../../../fields/ListField"; +import { DocumentView } from "./DocumentView"; +import { LinkBox } from "./LinkBox"; +import { LinkEditor } from "./LinkEditor"; +import './LinkMenu.scss'; +import React = require("react"); + +interface Props { + docView: DocumentView; + changeFlyout: () => void +} + +@observer +export class LinkMenu extends React.Component<Props> { + + @observable private _editingLink?: Document; + + renderLinkItems(links: Document[], key: Key, type: string) { + return links.map(link => { + let doc = link.GetT(key, Document); + if (doc && doc != FieldWaiting) { + return <LinkBox key={doc.Id} linkDoc={link} linkName={link.Title} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} /> + } + }) + } + + render() { + //get list of links from document + let linkFrom: Document[] = this.props.docView.props.Document.GetData(KeyStore.LinkedFromDocs, ListField, []); + let linkTo: Document[] = this.props.docView.props.Document.GetData(KeyStore.LinkedToDocs, ListField, []); + if (this._editingLink === undefined) { + return ( + <div id="linkMenu-container"> + <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input> + <div id="linkMenu-list"> + {this.renderLinkItems(linkTo, KeyStore.LinkedToDocs, "Source: ")} + {this.renderLinkItems(linkFrom, KeyStore.LinkedFromDocs, "Destination: ")} + </div> + </div> + ) + } else { + return ( + <LinkEditor linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor> + ) + } + + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss new file mode 100644 index 000000000..9f92410d4 --- /dev/null +++ b/src/client/views/nodes/PDFBox.scss @@ -0,0 +1,15 @@ +.react-pdf__Page { + transform-origin: left top; + position: absolute; +} +.react-pdf__Document { + position: absolute; +} +.pdfBox-buttonTray { + position:absolute; + z-index: 25; +} +.pdfBox-contentContainer { + position: absolute; + transform-origin: "left top"; +}
\ No newline at end of file diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx new file mode 100644 index 000000000..70a70c7c8 --- /dev/null +++ b/src/client/views/nodes/PDFBox.tsx @@ -0,0 +1,490 @@ +import * as htmlToImage from "html-to-image"; +import { action, computed, observable, reaction, IReactionDisposer } from 'mobx'; +import { observer } from "mobx-react"; +import 'react-image-lightbox/style.css'; +import Measure from "react-measure"; +//@ts-ignore +import { Document, Page } from "react-pdf"; +import 'react-pdf/dist/Page/AnnotationLayer.css'; +import { FieldWaiting, Opt } from '../../../fields/Field'; +import { ImageField } from '../../../fields/ImageField'; +import { KeyStore } from '../../../fields/KeyStore'; +import { PDFField } from '../../../fields/PDFField'; +import { Utils } from '../../../Utils'; +import { Annotation } from './Annotation'; +import { FieldView, FieldViewProps } from './FieldView'; +import "./ImageBox.scss"; +import "./PDFBox.scss"; +import { Sticky } from './Sticky'; //you should look at sticky and annotation, because they are used here +import React = require("react") + +/** 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 PDFBox extends React.Component<FieldViewProps> { + public static LayoutString() { return FieldView.LayoutString(PDFBox); } + + 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; + private initPage: boolean = false; + + //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; + private _pdfCanvas: any; + private _reactionDisposer: Opt<IReactionDisposer>; + + @observable private _perPageInfo: Object[] = []; //stores pageInfo + @observable private _pageInfo: any = { area: [], divs: [], anno: [] }; //divs is array of objects linked to anno + + @observable private _currAnno: any = [] + @observable private _interactive: boolean = false; + @observable private _loaded: boolean = false; + + @computed private get curPage() { return this.props.doc.GetNumber(KeyStore.CurPage, 0); } + + componentDidMount() { + this._reactionDisposer = reaction( + () => this.curPage, + () => { + if (this.curPage && this.initPage) { + this.saveThumbnail(); + this._interactive = true; + } else { + if (this.curPage) + this.initPage = true; + } + }, + { fireImmediately: true }); + + } + + componentWillUnmount() { + if (this._reactionDisposer) { + this._reactionDisposer(); + } + } + + /** + * 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 + */ + @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; + } + this._interactive = true; + } + + /** + * 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"; + } + } + } + + + @action + saveThumbnail = () => { + setTimeout(() => { + var me = this; + htmlToImage.toPng(this._mainDiv.current!, + { width: me.props.doc.GetNumber(KeyStore.NativeWidth, 0), height: me.props.doc.GetNumber(KeyStore.NativeHeight, 0), quality: 0.5 }) + .then(function (dataUrl: string) { + me.props.doc.SetData(KeyStore.Thumbnail, new URL(dataUrl), ImageField); + }) + .catch(function (error: any) { + console.error('oops, something went wrong!', error); + }); + }, 1000); + } + + @action + onLoaded = (page: any) => { + if (this._mainDiv.current) { + this._mainDiv.current.childNodes.forEach((element) => { + if (element.nodeName == "DIV") { + element.childNodes[0].childNodes.forEach((e) => { + + if (e instanceof HTMLCanvasElement) { + this._pdfCanvas = e; + this._pdfContext = e.getContext("2d") + + } + + }) + } + }) + } + + // bcz: the number of pages should really be set when the document is imported. + this.props.doc.SetNumber(KeyStore.NumPages, page._transport.numPages); + if (this._perPageInfo.length == 0) { //Makes sure it only runs once + this._perPageInfo = [...Array(page._transport.numPages)] + } + this._loaded = true; + } + + @action + setScaling = (r: any) => { + // bcz: the nativeHeight should really be set when the document is imported. + // also, the native dimensions could be different for different pages of the PDF + // so this design is flawed. + var nativeWidth = this.props.doc.GetNumber(KeyStore.NativeWidth, 0); + if (!this.props.doc.GetNumber(KeyStore.NativeHeight, 0)) { + this.props.doc.SetNumber(KeyStore.NativeHeight, nativeWidth * r.entry.height / r.entry.width); + } + if (!this.props.doc.GetT(KeyStore.Thumbnail, ImageField)) { + this.saveThumbnail(); + } + } + + @computed + get pdfContent() { + let page = this.curPage; + if (page == 0) + page = 1; + const renderHeight = 2400; + let pdfUrl = this.props.doc.GetT(this.props.fieldKey, PDFField); + let xf = this.props.doc.GetNumber(KeyStore.NativeHeight, 0) / renderHeight; + return <div className="pdfBox-contentContainer" key="container" style={{ transform: `scale(${xf}, ${xf})` }}> + <Document file={window.origin + "/corsProxy/" + `${pdfUrl}`}> + <Measure onResize={this.setScaling}> + {({ measureRef }) => + <div className="pdfBox-page" ref={measureRef}> + <Page height={renderHeight} pageNumber={page} onLoadSuccess={this.onLoaded} /> + </div> + } + </Measure> + </Document> + </div >; + } + + @computed + get pdfRenderer() { + let proxy = this._loaded ? (null) : this.imageProxyRenderer; + let pdfUrl = this.props.doc.GetT(this.props.fieldKey, PDFField); + if ((!this._interactive && proxy) || !pdfUrl || pdfUrl == FieldWaiting) { + return proxy; + } + return [ + this._pageInfo.area.filter(() => this._pageInfo.area).map((element: any) => element), + this._currAnno.map((element: any) => element), + <div key="pdfBox-contentShell"> + {this.pdfContent} + {proxy} + </div> + ]; + } + + @computed + get imageProxyRenderer() { + let field = this.props.doc.Get(KeyStore.Thumbnail); + if (field) { + let path = field == FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" : + field instanceof ImageField ? field.Data.href : "http://cs.brown.edu/people/bcz/prairie.jpg"; + return <img src={path} width="100%" />; + } + return (null); + } + + render() { + return ( + <div className="pdfBox-cont" ref={this._mainDiv} onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} > + {this.pdfRenderer} + </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/VideoBox.scss b/src/client/views/nodes/VideoBox.scss new file mode 100644 index 000000000..7306450d9 --- /dev/null +++ b/src/client/views/nodes/VideoBox.scss @@ -0,0 +1,4 @@ +.videobox-cont{ + width: 100%; + height: 100%; +}
\ No newline at end of file diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx new file mode 100644 index 000000000..22ff5c5ad --- /dev/null +++ b/src/client/views/nodes/VideoBox.tsx @@ -0,0 +1,43 @@ +import React = require("react") +import { FieldViewProps, FieldView } from './FieldView'; +import { FieldWaiting } from '../../../fields/Field'; +import { observer } from "mobx-react" +import { VideoField } from '../../../fields/VideoField'; +import "./VideoBox.scss" +import { ContextMenu } from "../../views/ContextMenu"; +import { observable, action } from 'mobx'; +import { KeyStore } from '../../../fields/KeyStore'; + +@observer +export class VideoBox extends React.Component<FieldViewProps> { + + public static LayoutString() { return FieldView.LayoutString(VideoBox) } + + constructor(props: FieldViewProps) { + super(props); + } + + + + componentDidMount() { + } + + componentWillUnmount() { + } + + + render() { + let field = this.props.doc.Get(this.props.fieldKey) + let path = field == FieldWaiting ? "http://techslides.com/demos/sample-videos/small.mp4": + field instanceof VideoField ? field.Data.href : "http://techslides.com/demos/sample-videos/small.mp4"; + + return ( + <div> + <video width = {200} height = {200} controls className = "videobox-cont"> + <source src = {path} type = "video/mp4"/> + Not supported. + </video> + </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..a535b2638 --- /dev/null +++ b/src/client/views/nodes/WebBox.scss @@ -0,0 +1,15 @@ + +.webBox-cont { + padding: 0vw; + position: absolute; + width: 100%; + height: 100%; + overflow: scroll; +} + +.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 diff --git a/src/client/views/nodes/WebView.tsx b/src/client/views/nodes/WebView.tsx deleted file mode 100644 index 717aa8bf5..000000000 --- a/src/client/views/nodes/WebView.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { FieldViewProps, FieldView } from "./FieldView"; -import { computed } from "mobx"; -import { observer } from "mobx-react"; -import { KeyStore } from "../../../fields/KeyStore"; -import React = require('react') -import { TextField } from "../../../fields/TextField"; -import { HtmlField } from "../../../fields/HtmlField"; -import { RichTextField } from "../../../fields/RichTextField"; - -@observer -export class WebView extends React.Component<FieldViewProps> { - public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(WebView, fieldStr) } - - @computed - get html(): string { - return this.props.doc.GetData(KeyStore.Data, HtmlField, "" as string); - } - - render() { - return <span dangerouslySetInnerHTML={{ __html: this.html }}></span> - } -}
\ No newline at end of file |
