diff options
Diffstat (limited to 'src')
45 files changed, 1167 insertions, 224 deletions
diff --git a/src/.DS_Store b/src/.DS_Store Binary files differindex 620e4ebce..f20f36d63 100644 --- a/src/.DS_Store +++ b/src/.DS_Store diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index bf4acc857..4cab53030 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -14,6 +14,11 @@ import { HtmlField } from "../../fields/HtmlField"; import { Key } from "../../fields/Key" import { Field } from "../../fields/Field"; import { KeyValueBox } from "../views/nodes/KeyValueBox" +import { KVPField } from "../../fields/KVPField"; +import { VideoField } from "../../fields/VideoField" +import { VideoBox } from "../views/nodes/VideoBox"; +import { AudioField } from "../../fields/AudioField"; +import { AudioBox } from "../views/nodes/AudioBox"; import { PDFField } from "../../fields/PDFField"; import { PDFBox } from "../views/nodes/PDFBox"; import { CollectionPDFView } from "../views/collections/CollectionPDFView"; @@ -29,6 +34,7 @@ export interface DocumentOptions { title?: string; panx?: number; pany?: number; + page?: number; scale?: number; layout?: string; layoutKeys?: Key[]; @@ -41,6 +47,8 @@ export namespace Documents { let webProto: Document; let collProto: Document; let kvpProto: Document; + let videoProto: Document; + let audioProto: Document; let pdfProto: Document; const textProtoId = "textProto"; const pdfProtoId = "pdfProto"; @@ -48,6 +56,8 @@ export namespace Documents { const webProtoId = "webProto"; const collProtoId = "collectionProto"; const kvpProtoId = "kvpProto"; + const videoProtoId = "videoProto" + const audioProtoId = "audioProto"; export function initProtos(callback: () => void) { Server.GetFields([collProtoId, textProtoId, imageProtoId], (fields) => { @@ -69,6 +79,7 @@ export namespace Documents { if (options.title !== undefined) { doc.SetText(KeyStore.Title, options.title); } if (options.panx !== undefined) { doc.SetNumber(KeyStore.PanX, options.panx); } if (options.pany !== undefined) { doc.SetNumber(KeyStore.PanY, options.pany); } + if (options.page !== undefined) { doc.SetNumber(KeyStore.Page, options.page); } if (options.scale !== undefined) { doc.SetNumber(KeyStore.Scale, options.scale); } if (options.viewType !== undefined) { doc.SetNumber(KeyStore.ViewType, options.viewType); } if (options.layout !== undefined) { doc.SetText(KeyStore.Layout, options.layout); } @@ -124,15 +135,32 @@ export namespace Documents { kvpProto = setupPrototypeOptions(kvpProtoId, "KVP_PROTO", KeyValueBox.LayoutString(), { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] }) } + function GetVideoPrototype(): Document { + return videoProto ? videoProto : + videoProto = setupPrototypeOptions(videoProtoId, "VIDEO_PROTO", VideoBox.LayoutString(), + { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] }) + } + function GetAudioPrototype(): Document { + return audioProto ? audioProto : + audioProto = setupPrototypeOptions(audioProtoId, "AUDIO_PROTO", AudioBox.LayoutString(), + { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] }) + } + export function ImageDocument(url: string, options: DocumentOptions = {}) { let doc = SetInstanceOptions(GetImagePrototype(), { ...options, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] }, new URL(url), ImageField); - doc.SetText(KeyStore.Caption, "my caption..."); - doc.SetText(KeyStore.BackgroundLayout, EmbeddedCaption()); - doc.SetText(KeyStore.OverlayLayout, FixedCaption()); + // doc.SetText(KeyStore.Caption, "my caption..."); + // doc.SetText(KeyStore.BackgroundLayout, EmbeddedCaption()); + // doc.SetText(KeyStore.OverlayLayout, FixedCaption()); return doc; } + export function VideoDocument(url: string, options: DocumentOptions = {}) { + return SetInstanceOptions(GetVideoPrototype(), options, new URL(url), VideoField); + } + export function AudioDocument(url: string, options: DocumentOptions = {}) { + return SetInstanceOptions(GetAudioPrototype(), options, new URL(url), AudioField); + } export function TextDocument(options: DocumentOptions = {}) { return SetInstanceOptions(GetTextPrototype(), options, undefined, undefined); } @@ -170,10 +198,10 @@ export namespace Documents { + FormattedTextBox.LayoutString("CaptionKey") + `</div> </div>` }; - function FixedCaption() { + function FixedCaption(fieldName: string = "Caption") { return `<div style="position:absolute; height:30px; bottom:0; width:100%"> <div style="position:absolute; width:100%; height:100%; text-align:center;bottom:0;">` - + FormattedTextBox.LayoutString("CaptionKey") + + + FormattedTextBox.LayoutString(fieldName + "Key") + `</div> </div>` }; }
\ No newline at end of file diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts new file mode 100644 index 000000000..5b99b4ef8 --- /dev/null +++ b/src/client/util/DocumentManager.ts @@ -0,0 +1,51 @@ +import React = require('react') +import { observer } from 'mobx-react'; +import { observable, action } from 'mobx'; +import { Document } from "../../fields/Document" +import { DocumentView } from '../views/nodes/DocumentView'; + + +export class DocumentManager { + + //global holds all of the nodes (regardless of which collection they're in) + @observable + public DocumentViews: DocumentView[] = []; + + // singleton instance + private static _instance: DocumentManager; + + // create one and only one instance of NodeManager + public static get Instance(): DocumentManager { + return this._instance || (this._instance = new this()); + } + + //private constructor so no other class can create a nodemanager + private constructor() { + // this.DocumentViews = new Array<DocumentView>(); + } + + public getDocumentView(toFind: Document): DocumentView | null { + + let toReturn: DocumentView | null; + toReturn = null; + + //gets document view that is in a freeform canvas collection + DocumentManager.Instance.DocumentViews.map(view => { + let doc = view.props.Document; + // if (view.props.ContainingCollectionView instanceof CollectionFreeFormView) { + // if (Object.is(doc, toFind)) { + // toReturn = view; + // return; + // } + // } + + if (Object.is(doc, toFind)) { + toReturn = view; + return; + } + + }) + + return (toReturn); + } +}
\ No newline at end of file diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 513a6ac9e..9bd7d5c24 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -97,9 +97,9 @@ export namespace DragManager { } export function StartDrag(ele: HTMLElement, dragData: { [id: string]: any }, options?: DragOptions) { - DocumentDecorations.Instance.Hidden = true; if (!dragDiv) { dragDiv = document.createElement("div"); + dragDiv.className = "dragManager-dragDiv" DragManager.Root().appendChild(dragDiv); } const w = ele.offsetWidth, h = ele.offsetHeight; @@ -124,17 +124,21 @@ export namespace DragManager { // So we replace the pdf's canvas with the image thumbnail const docView: DocumentView = dragData["documentView"]; const doc: Document = docView ? docView.props.Document : dragData["document"]; - var pdfBox = dragElement.getElementsByClassName("pdfBox-cont")[0] as HTMLElement; - let thumbnail = doc.GetT(KeyStore.Thumbnail, ImageField); - if (pdfBox && pdfBox.childElementCount && thumbnail) { - let img = new Image(); - img!.src = thumbnail.toString(); - img!.style.position = "absolute"; - img!.style.width = `${rect.width / scaleX}px`; - img!.style.height = `${rect.height / scaleY}px`; - pdfBox.replaceChild(img!, pdfBox.children[0]) + + if (doc) { + var pdfBox = dragElement.getElementsByClassName("pdfBox-cont")[0] as HTMLElement; + let thumbnail = doc.GetT(KeyStore.Thumbnail, ImageField); + if (pdfBox && pdfBox.childElementCount && thumbnail) { + let img = new Image(); + img!.src = thumbnail.toString(); + img!.style.position = "absolute"; + img!.style.width = `${rect.width / scaleX}px`; + img!.style.height = `${rect.height / scaleY}px`; + pdfBox.replaceChild(img!, pdfBox.children[0]) + } } + dragDiv.appendChild(dragElement); let hideSource = false; @@ -154,7 +158,7 @@ export namespace DragManager { e.preventDefault(); x += e.movementX; y += e.movementY; - if (e.shiftKey) { + if (e.shiftKey && (e.button == 2 || e.altKey)) { abortDrag(); CollectionDockingView.Instance.StartOtherDrag(doc, { pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 }); } diff --git a/src/client/views/.DS_Store b/src/client/views/.DS_Store Binary files differnew file mode 100644 index 000000000..6bd614c8b --- /dev/null +++ b/src/client/views/.DS_Store diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss index ea40c8e99..41994ef79 100644 --- a/src/client/views/ContextMenu.scss +++ b/src/client/views/ContextMenu.scss @@ -26,7 +26,8 @@ border-bottom-style: solid; padding: 10px; white-space: nowrap; - font-size: 1.5vw; + // font-size: 1.5vw; + font-size: 12px; } .contextMenu-item:hover { @@ -35,7 +36,7 @@ } .contextMenu-description { - font-size: 1.5vw; + // font-size: 1.5vw; text-align: left; - width: 8vw; + // width: 8vw; }
\ No newline at end of file diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index fcb934860..12352c667 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -44,8 +44,13 @@ export class ContextMenu extends React.Component { @action displayMenu(x: number, y: number) { - this._pageX = x - this._pageY = y + //maxX and maxY will change if the UI/font size changes, but will work for any amount + //of items added to the menu + let maxX = window.innerWidth - 150; + let maxY = window.innerHeight - (this._items.length * 34 + 30); + + this._pageX = x > maxX ? maxX : x; + this._pageY = y > maxY ? maxY : y; this._searchString = ""; @@ -64,6 +69,7 @@ export class ContextMenu extends React.Component { } render() { + return ( <div className="contextMenu-cont" style={{ left: this._pageX, top: this._pageY, display: this._display }} ref={this.ref}> <input className="contextMenu-item" type="text" placeholder="Search . . ." value={this._searchString} onChange={this.onChange}></input> diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index e8b93a18b..fb9091dfc 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -2,7 +2,7 @@ position: absolute; display: grid; z-index: 1000; - grid-template-rows: 20px 1fr 20px; + grid-template-rows: 20px 1fr 20px 0px; grid-template-columns: 20px 1fr 20px; pointer-events: none; #documentDecorations-centerCont { @@ -29,4 +29,23 @@ #documentDecorations-rightResizer { cursor: ew-resize; } + +} +.linkButton-empty { + height: 20px; + width: 20px; + margin-top: 10px; + border-radius: 50%; + opacity: 0.6; + pointer-events: auto; + background-color: #2B6091; +} +.linkButton-nonempty { + height: 20px; + width: 20px; + margin-top: 10px; + border-radius: 50%; + opacity: 0.6; + pointer-events: auto; + background-color: rgb(35, 165, 42); }
\ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 9fd73a33b..dc62f97cf 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,16 +1,24 @@ -import { observable, computed } from "mobx"; +import { observable, computed, action } from "mobx"; import React = require("react"); import { SelectionManager } from "../util/SelectionManager"; import { observer } from "mobx-react"; import './DocumentDecorations.scss' import { KeyStore } from '../../fields/KeyStore' import { NumberField } from "../../fields/NumberField"; +import { props } from "bluebird"; +import { DragManager } from "../util/DragManager"; +import { LinkMenu } from "./nodes/LinkMenu"; +import { ListField } from "../../fields/ListField"; +const higflyout = require("@hig/flyout"); +const { anchorPoints } = higflyout; +const Flyout = higflyout.default; @observer export class DocumentDecorations extends React.Component { static Instance: DocumentDecorations private _resizer = "" private _isPointerDown = false; + private _linkButton = React.createRef<HTMLDivElement>(); @observable private _hidden = false; constructor(props: Readonly<{}>) { @@ -52,6 +60,46 @@ export class DocumentDecorations extends React.Component { } } + onLinkButtonDown = (e: React.PointerEvent): void => { + // if () + // let linkMenu = new LinkMenu(SelectionManager.SelectedDocuments()[0]); + // linkMenu.Hidden = false; + console.log("down"); + + e.stopPropagation(); + document.removeEventListener("pointermove", this.onLinkButtonMoved) + document.addEventListener("pointermove", this.onLinkButtonMoved); + document.removeEventListener("pointerup", this.onLinkButtonUp) + document.addEventListener("pointerup", this.onLinkButtonUp); + + } + + onLinkButtonUp = (e: PointerEvent): void => { + console.log("up"); + document.removeEventListener("pointermove", this.onLinkButtonMoved) + document.removeEventListener("pointerup", this.onLinkButtonUp) + e.stopPropagation(); + } + + + onLinkButtonMoved = (e: PointerEvent): void => { + console.log("moved"); + let dragData: { [id: string]: any } = {}; + dragData["linkSourceDoc"] = SelectionManager.SelectedDocuments()[0]; + if (this._linkButton.current != null) { + DragManager.StartDrag(this._linkButton.current, dragData, { + handlers: { + dragComplete: action(() => { }), + }, + hideSource: false + }) + } + document.removeEventListener("pointermove", this.onLinkButtonMoved) + document.removeEventListener("pointerup", this.onLinkButtonUp) + e.stopPropagation(); + } + + onPointerMove = (e: PointerEvent): void => { e.stopPropagation(); e.preventDefault(); @@ -138,6 +186,13 @@ export class DocumentDecorations extends React.Component { } } + changeFlyoutContent = (): void => { + + } + // buttonOnPointerUp = (e: React.PointerEvent): void => { + // e.stopPropagation(); + // } + render() { var bounds = this.Bounds; if (this.Hidden) { @@ -147,6 +202,19 @@ export class DocumentDecorations extends React.Component { console.log("DocumentDecorations: Bounds Error") return (null); } + + let linkButton = null; + if (SelectionManager.SelectedDocuments().length > 0) { + let selFirst = SelectionManager.SelectedDocuments()[0]; + linkButton = (<Flyout + anchorPoint={anchorPoints.RIGHT_TOP} + content={ + <LinkMenu docView={selFirst} changeFlyout={this.changeFlyoutContent}> + </LinkMenu> + }> + <div className={"linkButton-" + (selFirst.props.Document.GetData(KeyStore.LinkedToDocs, ListField, []).length ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} ref={this._linkButton} /> + </Flyout>); + } return ( <div id="documentDecorations-container" style={{ width: (bounds.r - bounds.x + 40) + "px", @@ -163,7 +231,10 @@ export class DocumentDecorations extends React.Component { <div id="documentDecorations-bottomLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> <div id="documentDecorations-bottomResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> <div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> - </div> + + {linkButton} + + </div > ) } }
\ No newline at end of file diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index 929fb42a1..955831fb6 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -1,5 +1,5 @@ import { observable, action, computed } from "mobx"; -import { CirclePicker, ColorResult } from 'react-color' +import { CirclePicker, ColorResult } from 'react-color'; import React = require("react"); import "./InkingCanvas.scss" import { InkTool } from "../../fields/InkField"; diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 44fdf60ce..4a0f2b021 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -22,6 +22,8 @@ import "./Main.scss"; import { observer } from 'mobx-react'; import { Field, Opt } from '../../fields/Field'; import { InkingControl } from './InkingControl'; +import { RouteStore } from '../../server/RouteStore'; +import { Database } from '../../server/database'; @observer export class Main extends React.Component { @@ -29,6 +31,7 @@ export class Main extends React.Component { @observable private mainContainer?: Document; @observable private mainfreeform?: Document; @observable private userWorkspaces: Document[] = []; + @observable private activeUsers: Document[] = []; constructor(props: Readonly<{}>) { super(props); @@ -54,11 +57,11 @@ export class Main extends React.Component { initAuthenticationRouters = () => { // Load the user's active workspace, or create a new one if initial session after signup - request.get(this.contextualize("getActiveWorkspaceId"), (error, response, body) => { + request.get(this.prepend(RouteStore.getActiveWorkspace), (error, response, body) => { if (body) { Server.GetField(body, field => { if (field instanceof Document) { - this.openDocument(field); + this.openWorkspace(field); this.populateWorkspaces(); } else { this.createNewWorkspace(true); @@ -74,7 +77,7 @@ export class Main extends React.Component { createNewWorkspace = (init: boolean): void => { let mainDoc = Documents.DockDocument(JSON.stringify({ content: [{ type: 'row', content: [] }] }), { title: `Main Container ${this.userWorkspaces.length + 1}` }); let newId = mainDoc.Id; - request.post(this.contextualize("addWorkspaceId"), { + request.post(this.prepend(RouteStore.addWorkspace), { body: { target: newId }, json: true }, () => { if (init) this.populateWorkspaces(); }); @@ -85,23 +88,24 @@ export class Main extends React.Component { var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc)] }] }; mainDoc.SetText(KeyStore.Data, JSON.stringify(dockingLayout)); mainDoc.Set(KeyStore.ActiveFrame, freeformDoc); - this.openDocument(mainDoc); + this.openWorkspace(mainDoc); }, 0); this.userWorkspaces.push(mainDoc); + mainDoc.GetList<Document>(KeyStore.ActiveUsers, []); } @action populateWorkspaces = () => { // retrieve all workspace documents from the server - request.get(this.contextualize("getAllWorkspaceIds"), (error, res, body) => { + request.get(this.prepend(RouteStore.getAllWorkspaces), (error, res, body) => { let ids = JSON.parse(body) as string[]; Server.GetFields(ids, action((fields: { [id: string]: Field }) => this.userWorkspaces = ids.map(id => fields[id] as Document))); }); } @action - openDocument = (doc: Document): void => { - request.post(this.contextualize("setActiveWorkspaceId"), { + openWorkspace = (doc: Document): void => { + request.post(this.prepend(RouteStore.setActiveWorkspace), { body: { target: doc.Id }, json: true }); @@ -115,27 +119,34 @@ export class Main extends React.Component { } } - contextualize = (extension: string) => window.location.origin + "/" + extension; + prepend = (extension: string) => window.location.origin + extension; render() { let imgRef = React.createRef<HTMLDivElement>(); + let pdfRef = React.createRef<HTMLDivElement>(); let webRef = React.createRef<HTMLDivElement>(); let textRef = React.createRef<HTMLDivElement>(); let schemaRef = React.createRef<HTMLDivElement>(); + let videoRef = React.createRef<HTMLDivElement>(); + let audioRef = React.createRef<HTMLDivElement>(); let colRef = React.createRef<HTMLDivElement>(); let workspacesRef = React.createRef<HTMLDivElement>(); - let pdfRef = React.createRef<HTMLDivElement>(); let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; let pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf" let weburl = "https://cs.brown.edu/courses/cs166/"; + let audiourl = "http://techslides.com/demos/samples/sample.mp3"; + let videourl = "http://techslides.com/demos/sample-videos/small.mp4"; + let clearDatabase = action(() => Utils.Emit(Server.Socket, MessageStore.DeleteAll, {})) let addTextNode = action(() => Documents.TextDocument({ width: 200, height: 200, title: "a text note" })) - let addColNode = action(() => Documents.FreeformDocument([], { width: 200, height: 200, title: "a feeform collection" })); - let addPDFNode = action(() => Documents.PdfDocument(pdfurl, { width: 200, height: 200, title: "a schema collection" })); + let addColNode = action(() => Documents.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" })); let addSchemaNode = action(() => Documents.SchemaDocument([Documents.TextDocument()], { width: 200, height: 200, title: "a schema collection" })); + let addVideoNode = action(() => Documents.VideoDocument(videourl, { width: 200, height: 200, title: "video node" })); + let addPDFNode = action(() => Documents.PdfDocument(pdfurl, { width: 200, height: 200, title: "a schema collection" })); let addImageNode = action(() => Documents.ImageDocument(imgurl, { width: 200, height: 200, title: "an image of a cat" })); let addWebNode = action(() => Documents.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" })); + let addAudioNode = action(() => Documents.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" })) let addClick = (creator: () => Document) => action(() => this.mainfreeform!.GetList<Document>(KeyStore.Data, []).push(creator())); @@ -155,7 +166,7 @@ export class Main extends React.Component { ContainingCollectionView={undefined} /> <DocumentDecorations /> <ContextMenu /> - <WorkspacesMenu active={this.mainContainer} open={this.openDocument} new={this.createNewWorkspace} allWorkspaces={this.userWorkspaces} /> + <WorkspacesMenu active={this.mainContainer} open={this.openWorkspace} new={this.createNewWorkspace} allWorkspaces={this.userWorkspaces} /> <div className="main-buttonDiv" style={{ bottom: '0px' }} ref={imgRef} > <button onPointerDown={setupDrag(imgRef, addImageNode)} onClick={addClick(addImageNode)}>Add Image</button></div> <div className="main-buttonDiv" style={{ bottom: '25px' }} ref={webRef} > @@ -170,6 +181,10 @@ export class Main extends React.Component { <button onClick={clearDatabase}>Clear Database</button></div> <div className="main-buttonDiv" style={{ top: '25px' }} ref={workspacesRef}> <button onClick={this.toggleWorkspaces}>View Workspaces</button></div> + <div className="main-buttonDiv" style={{ bottom: '175px' }} ref={videoRef}> + <button onPointerDown={setupDrag(videoRef, addVideoNode)} onClick={addClick(addVideoNode)}>Add Video</button></div> + <div className="main-buttonDiv" style={{ bottom: '200px' }} ref={audioRef}> + <button onPointerDown={setupDrag(audioRef, addAudioNode)} onClick={addClick(addAudioNode)}>Add Audio</button></div> <div className="main-buttonDiv" style={{ bottom: '150px' }} ref={pdfRef}> <button onPointerDown={setupDrag(pdfRef, addPDFNode)} onClick={addClick(addPDFNode)}>Add PDF</button></div> <button className="main-undoButtons" style={{ bottom: '25px' }} onClick={() => UndoManager.Undo()}>Undo</button> diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index ceb9d0a55..b5cafe681 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -44,7 +44,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp (window as any).ReactDOM = ReactDOM; } public StartOtherDrag(dragDoc: Document, e: any) { - this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener.onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: e.button }) + this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener.onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 }) } @action @@ -150,9 +150,13 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } } componentWillUnmount: () => void = () => { - this._goldenLayout.unbind('itemDropped', this.itemDropped); - this._goldenLayout.unbind('tabCreated', this.tabCreated); - this._goldenLayout.unbind('stackCreated', this.stackCreated); + try { + this._goldenLayout.unbind('itemDropped', this.itemDropped); + this._goldenLayout.unbind('tabCreated', this.tabCreated); + this._goldenLayout.unbind('stackCreated', this.stackCreated); + } catch (e) { + + } this._goldenLayout.destroy(); this._goldenLayout = null; window.removeEventListener('resize', this.onResize); @@ -174,17 +178,12 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } @action onPointerDown = (e: React.PointerEvent): void => { - if (e.button === 2 && this.props.active()) { + var className = (e.target as any).className; + if (className == "lm_drag_handle" || className == "lm_close" || className == "lm_maximise" || className == "lm_minimise" || className == "lm_close_tab") { + this._flush = true; + } + if (this.props.active()) { e.stopPropagation(); - e.preventDefault(); - } else { - var className = (e.target as any).className; - if (className == "lm_drag_handle" || className == "lm_close" || className == "lm_maximise" || className == "lm_minimise" || className == "lm_close_tab") { - this._flush = true; - } - if (e.buttons === 1 && this.props.active()) { - e.stopPropagation(); - } } } diff --git a/src/client/views/collections/CollectionFreeFormView.scss b/src/client/views/collections/CollectionFreeFormView.scss index b7b5faf6d..28242c939 100644 --- a/src/client/views/collections/CollectionFreeFormView.scss +++ b/src/client/views/collections/CollectionFreeFormView.scss @@ -21,6 +21,13 @@ height: 100%; } } +.collectionfreeformview-marquee{ + border-style: dashed; + box-sizing: border-box; + position: absolute; + border-width: 1px; + border-color: black; +} .collectionfreeformview-overlay { .collectionfreeformview > .jsx-parser{ diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx index 0d5e15767..0c74d1852 100644 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -1,4 +1,4 @@ -import { action, computed, observable, reaction, trace } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import { Document } from "../../../fields/Document"; import { FieldWaiting } from "../../../fields/Field"; @@ -10,9 +10,9 @@ import { DragManager } from "../../util/DragManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { CollectionPDFView } from "../collections/CollectionPDFView"; import { CollectionSchemaView } from "../collections/CollectionSchemaView"; import { CollectionView } from "../collections/CollectionView"; -import { CollectionPDFView } from "../collections/CollectionPDFView"; import { InkingCanvas } from "../InkingCanvas"; import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; import { DocumentView } from "../nodes/DocumentView"; @@ -25,13 +25,15 @@ import "./CollectionFreeFormView.scss"; import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; import { CollectionViewBase } from "./CollectionViewBase"; import React = require("react"); -import { render } from "pug"; +import { SelectionManager } from "../../util/SelectionManager"; const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this? @observer export class CollectionFreeFormView extends CollectionViewBase { private _canvasRef = React.createRef<HTMLDivElement>(); + @observable private _lastX: number = 0; + @observable private _lastY: number = 0; private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type) @@ -60,18 +62,23 @@ export class CollectionFreeFormView extends CollectionViewBase { super.drop(e, de); const docView: DocumentView = de.data["documentView"]; let doc: Document = docView ? docView.props.Document : de.data["document"]; - let screenX = de.x - (de.data["xOffset"] as number || 0); - let screenY = de.y - (de.data["yOffset"] as number || 0); - const [x, y] = this.getTransform().transformPoint(screenX, screenY); - doc.SetNumber(KeyStore.X, x); - doc.SetNumber(KeyStore.Y, y); - this.bringToFront(doc); + if (doc) { + let screenX = de.x - (de.data["xOffset"] as number || 0); + let screenY = de.y - (de.data["yOffset"] as number || 0); + const [x, y] = this.getTransform().transformPoint(screenX, screenY); + doc.SetNumber(KeyStore.X, x); + doc.SetNumber(KeyStore.Y, y); + this.bringToFront(doc); + } } + @observable + _marquee = false; + @action onPointerDown = (e: React.PointerEvent): void => { - if (((e.button === 2 && this.props.active()) || - !e.defaultPrevented) && (!this.isAnnotationOverlay || this.zoomScaling != 1 || e.button == 0)) { + if (((e.button === 2 && this.props.active()) || !e.defaultPrevented) && + (!this.isAnnotationOverlay || this.zoomScaling != 1 || e.button == 0)) { document.removeEventListener("pointermove", this.onPointerMove); document.addEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); @@ -85,10 +92,19 @@ export class CollectionFreeFormView extends CollectionViewBase { @action onPointerUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); + if (this._marquee) { + document.removeEventListener("keydown", this.marqueeCommand); + } e.stopPropagation(); - if (Math.abs(this._downX - e.clientX) < 3 && Math.abs(this._downY - e.clientY) < 3) { + + if (this._marquee) { + if (!e.shiftKey) { + SelectionManager.DeselectAll(); + } + var selectedDocs = this.marqueeSelect(); + selectedDocs.map(s => this.props.CollectionView.SelectedDocs.push(s.Id)); + } + else if (!this._marquee && Math.abs(this._downX - e.clientX) < 3 && Math.abs(this._downY - e.clientY) < 3) { //show preview text cursor on tap this._previewCursorVisible = true; //select is not already selected @@ -96,7 +112,48 @@ export class CollectionFreeFormView extends CollectionViewBase { this.props.select(false); } } + this.cleanupInteractions(); + } + + @action + cleanupInteractions = () => { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + this._marquee = false; + } + intersectRect(r1: { left: number, right: number, top: number, bottom: number }, + r2: { left: number, right: number, top: number, bottom: number }) { + return !(r2.left > r1.right || + r2.right < r1.left || + r2.top > r1.bottom || + r2.bottom < r1.top); + } + + marqueeSelect() { + this.props.CollectionView.SelectedDocs.length = 0; + var curPage = this.props.Document.GetNumber(KeyStore.CurPage, 1); + let p = this.getTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); + let v = this.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); + let selRect = { left: p[0], top: p[1], right: p[0] + v[0], bottom: p[1] + v[1] } + + var curPage = this.props.Document.GetNumber(KeyStore.CurPage, 1); + const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField); + let selection: Document[] = []; + if (lvalue && lvalue != FieldWaiting) { + lvalue.Data.map(doc => { + var page = doc.GetNumber(KeyStore.Page, 0); + if (page == curPage || page == 0) { + var x = doc.GetNumber(KeyStore.X, 0); + var y = doc.GetNumber(KeyStore.Y, 0); + var w = doc.GetNumber(KeyStore.Width, 0); + var h = doc.GetNumber(KeyStore.Height, 0); + if (this.intersectRect({ left: x, top: y, right: x + w, bottom: y + h }, selRect)) + selection.push(doc) + } + }) + } + return selection; } @action @@ -104,17 +161,51 @@ export class CollectionFreeFormView extends CollectionViewBase { if (!e.cancelBubble && this.props.active()) { e.stopPropagation(); e.preventDefault(); - let x = this.props.Document.GetNumber(KeyStore.PanX, 0); - let y = this.props.Document.GetNumber(KeyStore.PanY, 0); - let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); - this._previewCursorVisible = false; - this.SetPan(x - dx, y - dy); + let wasMarquee = this._marquee; + this._marquee = e.buttons != 2 && !e.altKey && !e.metaKey; + if (this._marquee && !wasMarquee) { + this._previewCursorVisible = false; + document.addEventListener("keydown", this.marqueeCommand); + } + + if (!this._marquee) { + let x = this.props.Document.GetNumber(KeyStore.PanX, 0); + let y = this.props.Document.GetNumber(KeyStore.PanY, 0); + let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); + this._previewCursorVisible = false; + this.SetPan(x - dx, y - dy); + } } this._lastX = e.pageX; this._lastY = e.pageY; } @action + marqueeCommand = (e: KeyboardEvent) => { + if (e.key == "Backspace") { + this.marqueeSelect().map(d => this.props.removeDocument(d)); + this.cleanupInteractions(); + } + if (e.key == "c") { + let p = this.getTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); + let v = this.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); + + let selected = this.marqueeSelect().map(m => m); + this.marqueeSelect().map(d => this.props.removeDocument(d)); + //setTimeout(() => { + this.props.CollectionView.addDocument(Documents.FreeformDocument(selected.map(d => { + d.SetNumber(KeyStore.X, d.GetNumber(KeyStore.X, 0) - p[0] - v[0] / 2); + d.SetNumber(KeyStore.Y, d.GetNumber(KeyStore.Y, 0) - p[1] - v[1] / 2); + d.SetNumber(KeyStore.Page, this.props.Document.GetNumber(KeyStore.Page, 0)); + d.SetText(KeyStore.Title, "" + d.GetNumber(KeyStore.Width, 0) + " " + d.GetNumber(KeyStore.Height, 0)); + return d; + }), { x: p[0], y: p[1], panx: 0, pany: 0, width: v[0], height: v[1], title: "a nested collection" })); + // }, 100); + this.cleanupInteractions(); + } + } + + @action onPointerWheel = (e: React.WheelEvent): void => { e.stopPropagation(); e.preventDefault(); @@ -291,6 +382,10 @@ export class CollectionFreeFormView extends CollectionViewBase { cursor = <div id="prevCursor" onKeyPress={this.onKeyDown} style={{ color: "black", position: "absolute", transformOrigin: "left top", transform: `translate(${x}px, ${y}px)` }}>I</div> } + let p = this.getTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); + let v = this.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); + var marquee = this._marquee ? <div className="collectionfreeformview-marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}` }}></div> : (null); + let [dx, dy] = [this.centeringShiftX, this.centeringShiftY]; const panx: number = -this.props.Document.GetNumber(KeyStore.PanX, 0); @@ -317,6 +412,7 @@ export class CollectionFreeFormView extends CollectionViewBase { <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} /> {cursor} {this.views} + {marquee} </div> {this.overlayView} </div> diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx index 7fd9f0f11..f22c07060 100644 --- a/src/client/views/collections/CollectionPDFView.tsx +++ b/src/client/views/collections/CollectionPDFView.tsx @@ -6,6 +6,7 @@ import { ContextMenu } from "../ContextMenu"; import { CollectionView, CollectionViewType } from "./CollectionView"; import { CollectionViewProps } from "./CollectionViewBase"; import React = require("react"); +import { FieldId } from "../../../fields/Field"; @observer @@ -17,6 +18,7 @@ export class CollectionPDFView extends React.Component<CollectionViewProps> { isTopMost={isTopMost} SelectOnLoad={selectOnLoad} BackgroundView={BackgroundView} focus={focus}/>`; } + public SelectedDocs: FieldId[] = [] @action onPageBack = () => this.curPage > 1 ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage - 1) : 0; @action onPageForward = () => this.curPage < this.numPages ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage + 1) : 0; diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 2868e1322..17eeaa5a2 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -205,6 +205,8 @@ export class CollectionSchemaView extends CollectionViewBase { ) let previewHandle = !this.props.active() ? (null) : ( <div className="collectionSchemaView-previewHandle" onPointerDown={this.onExpanderDown} />); + let dividerDragger = this._splitPercentage == 100 ? (null) : + <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} /> return ( <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} ref={this._mainCont} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} > <div className="collectionSchemaView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}> @@ -234,7 +236,7 @@ export class CollectionSchemaView extends CollectionViewBase { </div> } </Measure> - <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} /> + {dividerDragger} <div className="collectionSchemaView-previewRegion" style={{ width: `calc(${100 - this._splitPercentage}% - ${this.DIVIDER_WIDTH}px)` }}> {content} </div> diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 49df04163..17a0fbd23 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,4 +1,4 @@ -import { action, computed } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import { Document } from "../../../fields/Document"; import { ListField } from "../../../fields/ListField"; @@ -12,7 +12,7 @@ import { CollectionDockingView } from "./CollectionDockingView"; import { CollectionSchemaView } from "./CollectionSchemaView"; import { CollectionViewProps } from "./CollectionViewBase"; import { CollectionTreeView } from "./CollectionTreeView"; -import { Field } from "../../../fields/Field"; +import { Field, FieldId } from "../../../fields/Field"; export enum CollectionViewType { Invalid, @@ -27,6 +27,9 @@ export const COLLECTION_BORDER_WIDTH = 2; @observer export class CollectionView extends React.Component<CollectionViewProps> { + @observable + public SelectedDocs: FieldId[] = []; + public static LayoutString(fieldKey: string = "DataKey") { return `<${CollectionView.name} Document={Document} ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} panelWidth={PanelWidth} panelHeight={PanelHeight} isSelected={isSelected} select={select} bindings={bindings} @@ -96,7 +99,6 @@ export class CollectionView extends React.Component<CollectionViewProps> { ContextMenu.Instance.addItem({ description: "Freeform", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform) }) ContextMenu.Instance.addItem({ description: "Schema", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Schema) }) ContextMenu.Instance.addItem({ description: "Treeview", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Tree) }) - ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) }) } } @@ -116,4 +118,4 @@ export class CollectionView extends React.Component<CollectionViewProps> { {this.subView} </div>) } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx index 0a3b965f2..ddde05011 100644 --- a/src/client/views/collections/CollectionViewBase.tsx +++ b/src/client/views/collections/CollectionViewBase.tsx @@ -1,4 +1,4 @@ -import { action } from "mobx"; +import { action, runInAction } from "mobx"; import { Document } from "../../../fields/Document"; import { ListField } from "../../../fields/ListField"; import React = require("react"); @@ -11,6 +11,7 @@ import { Documents, DocumentOptions } from "../../documents/Documents"; import { Key } from "../../../fields/Key"; import { Transform } from "../../util/Transform"; import { CollectionView } from "./CollectionView"; +import { RouteStore } from "../../../server/RouteStore"; export interface CollectionViewProps { fieldKey: Key; @@ -47,7 +48,7 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps> protected drop(e: Event, de: DragManager.DropEvent) { const docView: DocumentView = de.data["documentView"]; const doc: Document = de.data["document"]; - if (docView && docView.props.ContainingCollectionView && docView.props.ContainingCollectionView !== this.props.CollectionView) { + if (docView && (!docView.props.ContainingCollectionView || docView.props.ContainingCollectionView !== this.props.CollectionView)) { if (docView.props.RemoveDocument) { docView.props.RemoveDocument(docView.props.Document); } @@ -68,13 +69,17 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps> let html = e.dataTransfer.getData("text/html"); let text = e.dataTransfer.getData("text/plain"); if (html && html.indexOf("<img") != 0) { + console.log("not good"); let htmlDoc = Documents.HtmlDocument(html, { ...options, width: 300, height: 300 }); htmlDoc.SetText(KeyStore.DocumentText, text); this.props.addDocument(htmlDoc); return; } + console.log(e.dataTransfer.items.length); + for (let i = 0; i < e.dataTransfer.items.length; i++) { + const upload = window.location.origin + RouteStore.upload; let item = e.dataTransfer.items[i]; if (item.kind === "string" && item.type.indexOf("uri") != -1) { e.dataTransfer.items[i].getAsString(function (s) { @@ -93,28 +98,58 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps> }) } - if (item.kind == "file" && item.type.indexOf("image")) { + let type = item.type + console.log(type) + if (item.kind == "file") { let fReader = new FileReader() let file = item.getAsFile(); - - fReader.addEventListener("load", action("drop", () => { - if (fReader.result) { - let url = "" + fReader.result; - let doc = Documents.ImageDocument(url, options) - let docs = that.props.Document.GetT(KeyStore.Data, ListField); - if (docs != FieldWaiting) { - if (!docs) { - docs = new ListField<Document>(); - that.props.Document.Set(KeyStore.Data, docs) - } - docs.Data.push(doc); - } - } - }), false) + let formData = new FormData() if (file) { - fReader.readAsDataURL(file) + formData.append('file', file) } + + fetch(upload, { + method: 'POST', + body: formData + }) + .then((res: Response) => { + return res.json() + }).then(json => { + + json.map((file: any) => { + let path = window.location.origin + file + runInAction(() => { + var doc: any; + + if (type.indexOf("image") !== -1) { + doc = Documents.ImageDocument(path, { ...options, nativeWidth: 300, width: 300, }) + } + if (type.indexOf("video") !== -1) { + doc = Documents.VideoDocument(path, { ...options, nativeWidth: 300, width: 300, }) + } + if (type.indexOf("audio") !== -1) { + doc = Documents.AudioDocument(path, { ...options, nativeWidth: 300, width: 300, }) + } + if (type.indexOf("pdf") !== -1) { + doc = Documents.PdfDocument(path, { ...options, nativeWidth: 300, width: 300, }) + } + let docs = that.props.Document.GetT(KeyStore.Data, ListField); + if (docs != FieldWaiting) { + if (!docs) { + docs = new ListField<Document>(); + that.props.Document.Set(KeyStore.Data, docs) + } + if (doc) { + docs.Data.push(doc); + } + + } + }) + }) + }) + + } } } 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.tsx b/src/client/views/nodes/DocumentView.tsx index bfc45cf3a..da40f0dc1 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"; @@ -16,12 +16,16 @@ 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? @@ -86,6 +90,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { private _documentBindings: any = null; private _downX: number = 0; private _downY: number = 0; + 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>"); } @@ -95,15 +100,15 @@ export class DocumentView extends React.Component<DocumentViewProps> { 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); + 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 { 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) @@ -111,25 +116,74 @@ export class DocumentView extends React.Component<DocumentViewProps> { } } } + + 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) { 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 - }) + document.removeEventListener("pointerup", this.onPointerUp); + if (!this.topMost || e.buttons == 2 || e.altKey) { + this.startDragging(e.x, e.y); } } e.stopPropagation(); @@ -170,6 +224,34 @@ export class DocumentView extends React.Component<DocumentViewProps> { } @action + drop = (e: Event, de: DragManager.DropEvent) => { + console.log("drop"); + const sourceDocView: DocumentView = de.data["linkSourceDoc"]; + if (!sourceDocView) { + return; + } + let sourceDoc: Document = sourceDocView.props.Document; + let destDoc: Document = this.props.Document; + if (this.props.isTopMost) { + return; + } + 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; @@ -197,7 +279,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { get mainContent() { return <JsxParser - components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, PDFBox }} + components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, VideoBox, AudioBox, PDFBox }} bindings={this._documentBindings} jsx={this.layout} showWarnings={true} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 9e63006d1..49f4cefce 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -8,10 +8,15 @@ 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 { 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. @@ -55,6 +60,12 @@ export class FieldView extends React.Component<FieldViewProps> { } 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) { diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index a6cee9957..5c7aaf9fe 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -69,7 +69,7 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { }; let field = this.props.doc.GetT(this.props.fieldKey, RichTextField); - if (field && field != FieldWaiting) { + if (field && field != FieldWaiting && field.Data) { state = EditorState.fromJSON(config, JSON.parse(field.Data)); } else { state = EditorState.create(config); @@ -113,7 +113,7 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { this.props.doc.SetData(this.props.fieldKey, e.target.value, RichTextField); } onPointerDown = (e: React.PointerEvent): void => { - if (e.buttons === 1 && this.props.isSelected()) { + if (e.buttons === 1 && this.props.isSelected() && !e.altKey) { e.stopPropagation(); } } @@ -151,8 +151,12 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { }) } + 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/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.tsx b/src/client/views/nodes/PDFBox.tsx index 70a70c7c8..1f873f8c5 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -17,6 +17,7 @@ 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") +import { RouteStore } from "../../../server/RouteStore"; /** ALSO LOOK AT: Annotation.tsx, Sticky.tsx * This method renders PDF and puts all kinds of functionalities such as annotation, highlighting, @@ -423,7 +424,9 @@ export class PDFBox extends React.Component<FieldViewProps> { // 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); + var nativeHeight = nativeWidth * r.entry.height / r.entry.width; + this.props.doc.SetNumber(KeyStore.Height, nativeHeight / nativeWidth * this.props.doc.GetNumber(KeyStore.Width, 0)); + this.props.doc.SetNumber(KeyStore.NativeHeight, nativeHeight); } if (!this.props.doc.GetT(KeyStore.Thumbnail, ImageField)) { this.saveThumbnail(); @@ -439,7 +442,7 @@ export class PDFBox extends React.Component<FieldViewProps> { 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}`}> + <Document file={window.origin + RouteStore.corsProxy + `${pdfUrl}`}> <Measure onResize={this.setScaling}> {({ measureRef }) => <div className="pdfBox-page" ref={measureRef}> 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 index e72b3c4da..a535b2638 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -4,6 +4,7 @@ position: absolute; width: 100%; height: 100%; + overflow: scroll; } .webBox-button { diff --git a/src/fields/AudioField.ts b/src/fields/AudioField.ts new file mode 100644 index 000000000..aefcc15c1 --- /dev/null +++ b/src/fields/AudioField.ts @@ -0,0 +1,31 @@ +import { BasicField } from "./BasicField"; +import { Field, FieldId } from "./Field"; +import { Types } from "../server/Message"; + +export class AudioField extends BasicField<URL> { + constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) { + super(data == undefined ? new URL("http://techslides.com/demos/samples/sample.mp3") : data, save, id); + } + + toString(): string { + return this.Data.href; + } + + + ToScriptString(): string { + return `new AudioField("${this.Data}")`; + } + + Copy(): Field { + return new AudioField(this.Data); + } + + ToJson(): { type: Types, data: URL, _id: string } { + return { + type: Types.Audio, + data: this.Data, + _id: this.Id + } + } + +}
\ No newline at end of file diff --git a/src/fields/Document.ts b/src/fields/Document.ts index 2e873439c..25e239417 100644 --- a/src/fields/Document.ts +++ b/src/fields/Document.ts @@ -1,6 +1,6 @@ import { Key } from "./Key" import { KeyStore } from "./KeyStore"; -import { Field, Cast, FieldWaiting, FieldValue, FieldId } from "./Field" +import { Field, Cast, FieldWaiting, FieldValue, FieldId, Opt } from "./Field" import { NumberField } from "./NumberField"; import { ObservableMap, computed, action } from "mobx"; import { TextField } from "./TextField"; @@ -128,6 +128,12 @@ export class Document extends Field { return false; } + GetTAsync<T extends Field>(key: Key, ctor: { new(): T }, callback: (field: Opt<T>) => void): boolean { + return this.GetAsync(key, (field) => { + callback(Cast(field, ctor)); + }) + } + /** * Same as {@link Document#GetAsync}, except a field of the given type * will be created if there is no field associated with the given key, diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts index f67257093..71b189e19 100644 --- a/src/fields/KeyStore.ts +++ b/src/fields/KeyStore.ts @@ -27,8 +27,13 @@ export namespace KeyStore { export const Caption = new Key("Caption"); export const ActiveFrame = new Key("ActiveFrame"); export const DocumentText = new Key("DocumentText"); + export const LinkedToDocs = new Key("LinkedToDocs"); + export const LinkedFromDocs = new Key("LinkedFromDocs"); + export const LinkDescription = new Key("LinkDescription"); + export const LinkTags = new Key("LinkTag"); export const Thumbnail = new Key("Thumbnail"); export const CurPage = new Key("CurPage"); export const NumPages = new Key("NumPages"); export const Ink = new Key("Ink"); + export const ActiveUsers = new Key("Active Users"); } diff --git a/src/fields/VideoField.ts b/src/fields/VideoField.ts new file mode 100644 index 000000000..5f4ae19bf --- /dev/null +++ b/src/fields/VideoField.ts @@ -0,0 +1,30 @@ +import { BasicField } from "./BasicField"; +import { Field, FieldId } from "./Field"; +import { Types } from "../server/Message"; + +export class VideoField extends BasicField<URL> { + constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) { + super(data == undefined ? new URL("http://techslides.com/demos/sample-videos/small.mp4") : data, save, id); + } + + toString(): string { + return this.Data.href; + } + + ToScriptString(): string { + return `new VideoField("${this.Data}")`; + } + + Copy(): Field { + return new VideoField(this.Data); + } + + ToJson(): { type: Types, data: URL, _id: string } { + return { + type: Types.Video, + data: this.Data, + _id: this.Id + } + } + +}
\ No newline at end of file diff --git a/src/server/Message.ts b/src/server/Message.ts index 5e97a5edf..8a00f6b59 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -45,7 +45,7 @@ export class GetFieldArgs { } export enum Types { - Number, List, Key, Image, Web, Document, Text, RichText, DocumentReference, Html, Ink, PDF + Number, List, Key, Image, Web, Document, Text, RichText, DocumentReference, Html, Video, Audio, Ink, PDF } export class DocumentTransfer implements Transferable { diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts new file mode 100644 index 000000000..ace2152d7 --- /dev/null +++ b/src/server/RouteStore.ts @@ -0,0 +1,28 @@ +// PREPEND ALL ROUTES WITH FORWARD SLASHES! + +export enum RouteStore { + // GENERAL + root = "/root", + home = "/home", + corsProxy = "/corsProxy", + delete = "/delete", + + // UPLOAD AND STATIC FILE SERVING + public = "/public", + upload = "/upload", + images = "/images", + + // USER AND WORKSPACES + addWorkspace = "/addWorkspaceId", + getAllWorkspaces = "/getAllWorkspaceIds", + getActiveWorkspace = "/getActiveWorkspaceId", + setActiveWorkspace = "/setActiveWorkspaceId", + + // AUTHENTICATION + signup = "/signup", + login = "/login", + logout = "/logout", + forgot = "/forgotpassword", + reset = "/reset/:token", + +}
\ No newline at end of file diff --git a/src/server/ServerUtil.ts b/src/server/ServerUtil.ts index 3b9d14891..5331c9e30 100644 --- a/src/server/ServerUtil.ts +++ b/src/server/ServerUtil.ts @@ -1,17 +1,24 @@ -import {HtmlField} from '../fields/HtmlField'; + +import { Field } from './../fields/Field'; +import { TextField } from './../fields/TextField'; +import { NumberField } from './../fields/NumberField'; +import { RichTextField } from './../fields/RichTextField'; +import { Key } from './../fields/Key'; +import { ImageField } from './../fields/ImageField'; +import { ListField } from './../fields/ListField'; +import { Document } from './../fields/Document'; +import { Server } from './../client/Server'; +import { Types } from './Message'; +import { Utils } from '../Utils'; +import { HtmlField } from '../fields/HtmlField'; +import { WebField } from '../fields/WebField'; +import { AudioField } from '../fields/AudioField'; +import { VideoField } from '../fields/VideoField'; import {InkField} from '../fields/InkField'; import {PDFField} from '../fields/PDFField'; -import {WebField} from '../fields/WebField'; -import {Utils} from '../Utils'; -import {Document} from './../fields/Document'; -import {Field} from './../fields/Field'; -import {ImageField} from './../fields/ImageField'; -import {Key} from './../fields/Key'; -import {ListField} from './../fields/ListField'; -import {NumberField} from './../fields/NumberField'; -import {RichTextField} from './../fields/RichTextField'; -import {TextField} from './../fields/TextField'; -import {Types} from './Message'; + + + export class ServerUtils { public static FromJson(json: any): Field { @@ -44,6 +51,10 @@ export class ServerUtils { return new PDFField(new URL(data), id, false) case Types.List: return ListField.FromJson(id, data) + case Types.Audio: + return new AudioField(new URL(data), id, false) + case Types.Video: + return new VideoField(new URL(data), id, false) case Types.Ink: return InkField.FromJson(id, data); case Types.Document: diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts index d90bedb18..b6fe15655 100644 --- a/src/server/authentication/config/passport.ts +++ b/src/server/authentication/config/passport.ts @@ -4,6 +4,7 @@ import * as mongodb from 'mongodb'; import * as _ from "lodash"; import { default as User } from '../models/user_model'; import { Request, Response, NextFunction } from "express"; +import { RouteStore } from '../../RouteStore'; const LocalStrategy = passportLocal.Strategy; @@ -35,7 +36,7 @@ export let isAuthenticated = (req: Request, res: Response, next: NextFunction) = if (req.isAuthenticated()) { return next(); } - return res.redirect("/login"); + return res.redirect(RouteStore.login); } export let isAuthorized = (req: Request, res: Response, next: NextFunction) => { diff --git a/src/server/authentication/controllers/user_controller.ts b/src/server/authentication/controllers/user_controller.ts index 7b89b5152..2cef958e8 100644 --- a/src/server/authentication/controllers/user_controller.ts +++ b/src/server/authentication/controllers/user_controller.ts @@ -10,18 +10,7 @@ import * as pug from 'pug'; import * as async from 'async'; import * as nodemailer from 'nodemailer'; import c = require("crypto"); - - -/** - * GET / - * Whenever a user navigates to the root of Dash - * (doesn't specify a sub-route), redirect to login. - * If the user is already signed in, it will effectively - * automatically redirect them to /home instead - */ -export let getEntry = (req: Request, res: Response) => { - res.redirect("/login"); -} +import { RouteStore } from "../../RouteStore"; /** * GET /signup @@ -31,7 +20,7 @@ export let getEntry = (req: Request, res: Response) => { export let getSignup = (req: Request, res: Response) => { if (req.user) { let user = req.user; - return res.redirect("/home"); + return res.redirect(RouteStore.home); } res.render("signup.pug", { title: "Sign Up", @@ -56,7 +45,7 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => { title: "Sign Up", user: req.user, }); - return res.redirect("/signup"); + return res.redirect(RouteStore.signup); } const email = req.body.email; @@ -71,7 +60,7 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => { User.findOne({ email }, (err, existingUser) => { if (err) { return next(err); } if (existingUser) { - return res.redirect("/login"); + return res.redirect(RouteStore.login); } user.save((err) => { if (err) { return next(err); } @@ -79,7 +68,7 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => { if (err) { return next(err); } - res.redirect("/home"); + res.redirect(RouteStore.home); }); }); }); @@ -93,7 +82,7 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => { */ export let getLogin = (req: Request, res: Response) => { if (req.user) { - return res.redirect("/home"); + return res.redirect(RouteStore.home); } res.render("login.pug", { title: "Log In", @@ -104,7 +93,7 @@ export let getLogin = (req: Request, res: Response) => { /** * POST /login * Sign in using email and password. - * On failure, redirect to login page + * On failure, redirect to signup page */ export let postLogin = (req: Request, res: Response, next: NextFunction) => { req.assert("email", "Email is not valid").isEmail(); @@ -115,17 +104,17 @@ export let postLogin = (req: Request, res: Response, next: NextFunction) => { if (errors) { req.flash("errors", "Unable to login at this time. Please try again."); - return res.redirect("/signup"); + return res.redirect(RouteStore.signup); } passport.authenticate("local", (err: Error, user: DashUserModel, info: IVerifyOptions) => { if (err) { return next(err); } if (!user) { - return res.redirect("/signup"); + return res.redirect(RouteStore.signup); } req.logIn(user, (err) => { if (err) { return next(err); } - res.redirect("/home"); + res.redirect(RouteStore.home); }); })(req, res, next); }; @@ -136,16 +125,12 @@ export let postLogin = (req: Request, res: Response, next: NextFunction) => { * and destroys the user's current session. */ export let getLogout = (req: Request, res: Response) => { - const dashUser: DashUserModel | undefined = req.user; - if (dashUser) { - dashUser.update({ $set: { didSelectSessionWorkspace: false } }, () => { }) - } req.logout(); const sess = req.session; if (sess) { sess.destroy((err) => { if (err) { console.log(err); } }); } - res.redirect('/login'); + res.redirect(RouteStore.login); } export let getForgot = function (req: Request, res: Response) { @@ -172,7 +157,7 @@ export let postForgot = function (req: Request, res: Response, next: NextFunctio User.findOne({ email }, function (err, user: DashUserModel) { if (!user) { // NO ACCOUNT WITH SUBMITTED EMAIL - return res.redirect('/forgot'); + return res.redirect(RouteStore.forgot); } user.passwordResetToken = token; user.passwordResetExpires = new Date(Date.now() + 3600000); // 1 HOUR @@ -205,14 +190,14 @@ export let postForgot = function (req: Request, res: Response, next: NextFunctio } ], function (err) { if (err) return next(err); - res.redirect('/forgot'); + res.redirect(RouteStore.forgot); }) } export let getReset = function (req: Request, res: Response) { User.findOne({ passwordResetToken: req.params.token, passwordResetExpires: { $gt: Date.now() } }, function (err, user: DashUserModel) { if (!user || err) { - return res.redirect('/forgot'); + return res.redirect(RouteStore.forgot); } res.render("reset.pug", { title: "Reset Password", @@ -242,7 +227,7 @@ export let postReset = function (req: Request, res: Response) { user.save(function (err) { if (err) { - return res.redirect("/login"); + return res.redirect(RouteStore.login); } req.logIn(user, function (err) { if (err) { @@ -273,6 +258,6 @@ export let postReset = function (req: Request, res: Response) { }); } ], function (err) { - res.redirect('/login'); + res.redirect(RouteStore.login); }); }
\ No newline at end of file diff --git a/src/server/authentication/models/user_model.ts b/src/server/authentication/models/user_model.ts index 29076ba19..3d4ed6896 100644 --- a/src/server/authentication/models/user_model.ts +++ b/src/server/authentication/models/user_model.ts @@ -23,6 +23,7 @@ export type DashUserModel = mongoose.Document & { allWorkspaceIds: Array<String>, activeWorkspaceId: String, + activeUsersId: String, profile: { name: string, @@ -53,6 +54,7 @@ const userSchema = new mongoose.Schema({ default: [] }, activeWorkspaceId: String, + activeUsersId: String, facebook: String, twitter: String, diff --git a/src/server/database.ts b/src/server/database.ts index 1553dd94e..f414266e2 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -16,12 +16,12 @@ export class Database { }) } - public update(id: string, value: any) { + public update(id: string, value: any, callback: () => void) { if (this.db) { let collection = this.db.collection('documents'); collection.update({ _id: id }, { $set: value }, { upsert: true - }); + }, callback); } } diff --git a/src/server/index.ts b/src/server/index.ts index fad30f3ad..6f6f620d8 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -3,6 +3,8 @@ const app = express() import * as webpack from 'webpack' import * as wdm from 'webpack-dev-middleware'; import * as whm from 'webpack-hot-middleware'; +import * as path from 'path' +import * as formidable from 'formidable' import * as passport from 'passport'; import { MessageStore, Message, SetFieldArgs, GetFieldArgs, Transferable } from "./Message"; import { Client } from './Client'; @@ -17,7 +19,7 @@ import * as bcrypt from "bcrypt-nodejs"; import { Document } from '../fields/Document'; import * as io from 'socket.io' import * as passportConfig from './authentication/config/passport'; -import { getLogin, postLogin, getSignup, postSignup, getLogout, getEntry, postReset, getForgot, postForgot, getReset } from './authentication/controllers/user_controller'; +import { getLogin, postLogin, getSignup, postSignup, getLogout, postReset, getForgot, postForgot, getReset } from './authentication/controllers/user_controller'; const config = require('../../webpack.config'); const compiler = webpack(config); const port = 1050; // default port to listen @@ -33,10 +35,10 @@ import c = require("crypto"); const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); import { performance } from 'perf_hooks' -import * as path from 'path' import User, { DashUserModel } from './authentication/models/user_model'; import * as fs from 'fs'; import * as request from 'request' +import { RouteStore } from './RouteStore'; const download = (url: string, dest: fs.PathLike) => { request.get(url).pipe(fs.createWriteStream(dest)); @@ -61,11 +63,7 @@ app.use(session({ url: 'mongodb://localhost:27017/Dash' }) })); -// app.use(cookieSession({ -// name: 'authentication', -// keys: [`${c.randomBytes(8)}`, `${c.randomBytes(8)}`, `${c.randomBytes(8)}`], -// maxAge: 7 * 24 * 60 * 60 * 1000 -// })); + app.use(flash()); app.use(expressFlash()); app.use(bodyParser.json()); @@ -78,97 +76,123 @@ app.use((req, res, next) => { next(); }); -// AUTHENTICATION ROUTING - enum Method { - Get, - Post + GET, + POST } +/** + * Please invoke this function when adding a new route to Dash's server. + * It ensures that any requests leading to or containing user-sensitive information + * does not execute unless Passport authentication detects a user logged in. + * @param method whether or not the request is a GET or a POST + * @param route the forward slash prepended path name (reference and add to RouteStore.ts) + * @param handler the action to invoke, recieving a DashUserModel and the expected request and response + * @param onRejection an optional callback invoked on return if no user is found to be logged in + */ function addSecureRoute(method: Method, route: string, handler: (user: DashUserModel, req: express.Request, res: express.Response) => void, - nope: (res: express.Response) => any) { - route = "/" + route; + onRejection: (res: express.Response) => any = (res) => res.redirect(RouteStore.logout)) { switch (method) { - case Method.Get: + case Method.GET: app.get(route, (req, res) => { const dashUser: DashUserModel = req.user; - if (!dashUser) return nope(res); + if (!dashUser) return onRejection(res); handler(dashUser, req, res); }); break; - case Method.Post: + case Method.POST: app.post(route, (req, res) => { const dashUser: DashUserModel = req.user; - if (!dashUser) return nope(res); + if (!dashUser) return onRejection(res); handler(dashUser, req, res); }); break; } } -// *** -// Look for the definitions of these get and post -// functions in the exports of user.ts +// STATIC FILE SERVING -addSecureRoute(Method.Get, "home", (user, req, res) => { +let FieldStore: ObservableMap<FieldId, Field> = new ObservableMap(); + +app.use(express.static(__dirname + RouteStore.public)); +app.use(RouteStore.images, express.static(__dirname + RouteStore.public)) + +addSecureRoute(Method.POST, RouteStore.upload, (user, req, res) => { + let form = new formidable.IncomingForm() + form.uploadDir = __dirname + "/public/files/" + form.keepExtensions = true + // let path = req.body.path; + console.log("upload") + form.parse(req, (err, fields, files) => { + console.log("parsing") + let names: any[] = []; + for (const name in files) { + let file = files[name]; + names.push(`/files/` + path.basename(file.path)); + } + res.send(names); + }); +}); + +// anyone attempting to navigate to localhost at this port will +// first have to login +addSecureRoute(Method.GET, RouteStore.root, (user, req, res) => { + +}, res => { + res.send() +}); + +// YAY! SHOW THEM THEIR WORKSPACES NOW +addSecureRoute(Method.GET, RouteStore.home, (user, req, res) => { res.sendFile(path.join(__dirname, '../../deploy/index.html')); -}, res => res.redirect("/login")) +}); -addSecureRoute(Method.Get, "getActiveWorkspaceId", (user, req, res) => { +addSecureRoute(Method.GET, RouteStore.getActiveWorkspace, (user, req, res) => { res.send(user.activeWorkspaceId || ""); -}, () => { }); +}); -addSecureRoute(Method.Get, "getAllWorkspaceIds", (user, req, res) => { +addSecureRoute(Method.GET, RouteStore.getAllWorkspaces, (user, req, res) => { res.send(JSON.stringify(user.allWorkspaceIds as Array<String>)); -}, () => { }); +}); -addSecureRoute(Method.Post, "setActiveWorkspaceId", (user, req) => { +addSecureRoute(Method.POST, RouteStore.setActiveWorkspace, (user, req) => { user.update({ $set: { activeWorkspaceId: req.body.target } }, () => { }); -}, () => { }); +}); -addSecureRoute(Method.Post, "addWorkspaceId", (user, req) => { +addSecureRoute(Method.POST, RouteStore.addWorkspace, (user, req) => { user.update({ $push: { allWorkspaceIds: req.body.target } }, () => { }); -}, () => { }); +}); -// anyone attempting to navigate to localhost at this port will -// first have to login -app.get("/", getEntry); +// AUTHENTICATION // Sign Up -app.get("/signup", getSignup); -app.post("/signup", postSignup); +app.get(RouteStore.signup, getSignup); +app.post(RouteStore.signup, postSignup); // Log In -app.get("/login", getLogin); -app.post("/login", postLogin); +app.get(RouteStore.login, getLogin); +app.post(RouteStore.login, postLogin); // Log Out -app.get('/logout', getLogout); - -// *** +app.get(RouteStore.logout, getLogout); // FORGOT PASSWORD EMAIL HANDLING -app.get('/forgot', getForgot) -app.post('/forgot', postForgot) +app.get(RouteStore.forgot, getForgot) +app.post(RouteStore.forgot, postForgot) // RESET PASSWORD EMAIL HANDLING -app.get('/reset/:token', getReset); -app.post('/reset/:token', postReset); +app.get(RouteStore.reset, getReset); +app.post(RouteStore.reset, postReset); -let FieldStore: ObservableMap<FieldId, Field> = new ObservableMap(); -app.get("/hello", (req, res) => { - res.send("<p>Hello</p>"); -}) - -app.use("/corsProxy", (req, res) => { +app.use(RouteStore.corsProxy, (req, res) => { req.pipe(request(req.url.substring(1))).pipe(res); }); -app.get("/delete", (req, res) => { +app.get(RouteStore.delete, (req, res) => { deleteAll(); - res.redirect("/"); + res.redirect(RouteStore.home); }); app.use(wdm(compiler, { @@ -208,10 +232,6 @@ function barReceived(guid: String) { clients[guid.toString()] = new Client(guid.toString()); } -function addDocument(document: Document) { - -} - function getField([id, callback]: [string, (result: any) => void]) { Database.Instance.getDocument(id, (result: any) => { if (result) { @@ -228,8 +248,9 @@ function getFields([ids, callback]: [string[], (result: any) => void]) { } function setField(socket: Socket, newValue: Transferable) { - Database.Instance.update(newValue._id, newValue) - socket.broadcast.emit(MessageStore.SetField.Message, newValue) + Database.Instance.update(newValue._id, newValue, () => { + socket.broadcast.emit(MessageStore.SetField.Message, newValue); + }) } server.listen(serverPort); |