diff options
Diffstat (limited to 'src/client')
-rw-r--r-- | src/client/documents/Documents.ts | 6 | ||||
-rw-r--r-- | src/client/views/EditableView.tsx | 5 | ||||
-rw-r--r-- | src/client/views/Main.tsx | 414 | ||||
-rw-r--r-- | src/client/views/collections/CollectionDockingView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/CollectionFreeFormView.scss | 4 | ||||
-rw-r--r-- | src/client/views/collections/CollectionFreeFormView.tsx | 84 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSchemaView.tsx | 6 | ||||
-rw-r--r-- | src/client/views/collections/CollectionTreeView.tsx | 5 | ||||
-rw-r--r-- | src/client/views/collections/CollectionViewBase.tsx | 44 | ||||
-rw-r--r-- | src/client/views/nodes/LinkBox.tsx | 3 | ||||
-rw-r--r-- | src/client/views/nodes/PDFBox.tsx | 3 |
11 files changed, 404 insertions, 172 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 3aa575dbb..fabdaad17 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -60,14 +60,14 @@ export namespace Documents { const videoProtoId = "videoProto" const audioProtoId = "audioProto"; - export function initProtos(mainDocId: string, callback: (mainDoc?: Document) => void) { - Server.GetFields([collProtoId, textProtoId, imageProtoId, mainDocId], (fields) => { + export function initProtos(callback: () => void) { + Server.GetFields([collProtoId, textProtoId, imageProtoId], (fields) => { collProto = fields[collProtoId] as Document; imageProto = fields[imageProtoId] as Document; textProto = fields[textProtoId] as Document; webProto = fields[webProtoId] as Document; kvpProto = fields[kvpProtoId] as Document; - callback(fields[mainDocId] as Document) + callback(); }); } function assignOptions(doc: Document, options: DocumentOptions): Document { diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 3b54c0dbb..98a6ed1ba 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -21,6 +21,7 @@ export interface EditableProps { */ contents: any; height: number + display?: string; } /** @@ -47,10 +48,10 @@ export class EditableView extends React.Component<EditableProps> { render() { if (this.editing) { return <input defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus onBlur={action(() => this.editing = false)} - style={{ display: "inline" }}></input> + style={{ display: this.props.display }}></input> } else { return ( - <div className="editableView-container-editing" style={{ display: "inline", height: "auto", maxHeight: `${this.props.height}` }} + <div className="editableView-container-editing" style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }} onClick={action(() => this.editing = true)}> {this.props.contents} </div> diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 2b5efa07d..26a07fdfe 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -7,17 +7,22 @@ import { KeyStore } from '../../fields/KeyStore'; import "./Main.scss"; import { MessageStore } from '../../server/Message'; import { Utils } from '../../Utils'; +import * as request from 'request' import { Documents } from '../documents/Documents'; import { Server } from '../Server'; import { setupDrag } from '../util/DragManager'; import { Transform } from '../util/Transform'; import { UndoManager } from '../util/UndoManager'; +import { WorkspacesMenu } from '../../server/authentication/controllers/WorkspacesMenu'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { ContextMenu } from './ContextMenu'; import { DocumentDecorations } from './DocumentDecorations'; import { DocumentView } from './nodes/DocumentView'; import "./Main.scss"; +import { observer } from 'mobx-react'; import { InkingControl } from './InkingControl'; +import { RouteStore } from '../../server/RouteStore'; +import { json } from 'body-parser'; import { library } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faFont } from '@fortawesome/free-solid-svg-icons'; @@ -32,170 +37,273 @@ import { faPenNib } from '@fortawesome/free-solid-svg-icons'; import { faFilm } from '@fortawesome/free-solid-svg-icons'; import { faMusic } from '@fortawesome/free-solid-svg-icons'; import Measure from 'react-measure'; +import { DashUserModel } from '../../server/authentication/models/user_model'; +import { ServerUtils } from '../../server/ServerUtil'; +import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; import { Field, Opt } from '../../fields/Field'; import { ListField } from '../../fields/ListField'; +@observer +export class Main extends React.Component { + // dummy initializations keep the compiler happy + @observable private mainContainer?: Document; + @observable private mainfreeform?: Document; + @observable private userWorkspaces: Document[] = []; + @observable public pwidth: number = 0; + @observable public pheight: number = 0; -configure({ enforceActions: "observed" }); // causes errors to be generated when modifying an observable outside of an action -window.addEventListener("drop", (e) => e.preventDefault(), false) -window.addEventListener("dragover", (e) => e.preventDefault(), false) -document.addEventListener("pointerdown", action(function (e: PointerEvent) { - if (!ContextMenu.Instance.intersects(e.pageX, e.pageY)) { - ContextMenu.Instance.clearItems() + private mainDocId: string | undefined; + private currentUser?: DashUserModel; + + constructor(props: Readonly<{}>) { + super(props); + // causes errors to be generated when modifying an observable outside of an action + configure({ enforceActions: "observed" }); + if (window.location.pathname !== RouteStore.home) { + let pathname = window.location.pathname.split("/"); + this.mainDocId = pathname[pathname.length - 1]; + } + + CurrentUserUtils.loadCurrentUser(); + + library.add(faFont); + library.add(faImage); + library.add(faFilePdf); + library.add(faObjectGroup); + library.add(faTable); + library.add(faGlobeAsia); + library.add(faUndoAlt); + library.add(faRedoAlt); + library.add(faPenNib); + library.add(faFilm); + library.add(faMusic); + + this.initEventListeners(); + Documents.initProtos(() => { + this.initAuthenticationRouters(); + }); } -}), true) -const pathname = window.location.pathname.split("/"); -const mainDocId = pathname[pathname.length - 1]; -const pendingDocId = "pending-doc" -var mainContainer: Document; -let mainfreeform: Document; - -class mainDocFrame { - @observable public static pwidth: number = 0; - @observable public static pheight: number = 0; -} -library.add(faFont); -library.add(faImage); -library.add(faFilePdf); -library.add(faObjectGroup); -library.add(faTable); -library.add(faGlobeAsia); -library.add(faUndoAlt); -library.add(faRedoAlt); -library.add(faPenNib); -library.add(faFilm); -library.add(faMusic); - -Documents.initProtos(mainDocId, (res?: Document) => { - if (res instanceof Document) { - mainContainer = res; - mainContainer.GetAsync(KeyStore.ActiveFrame, field => mainfreeform = field as Document); + onHistory = () => { + if (window.location.pathname !== RouteStore.home) { + let pathname = window.location.pathname.split("/"); + this.mainDocId = pathname[pathname.length - 1]; + Server.GetField(this.mainDocId, action((field: Opt<Field>) => { + if (field instanceof Document) { + this.openWorkspace(field, true); + } + })); + } } - else { - mainContainer = Documents.DockDocument(JSON.stringify({ content: [{ type: 'row', content: [] }] }), { title: "main container" }, mainDocId); - // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) - setTimeout(() => { - mainfreeform = Documents.FreeformDocument([], { x: 0, y: 400, title: "mini collection" }, undefined, false); + componentDidMount() { + window.onpopstate = this.onHistory; + } - var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(mainfreeform)] }] }; - mainContainer.SetText(KeyStore.Data, JSON.stringify(dockingLayout)); - mainContainer.Set(KeyStore.ActiveFrame, mainfreeform); - let pendingDocument = Documents.SchemaDocument([], { title: "New Mobile Uploads" }, pendingDocId) - mainContainer.Set(KeyStore.OptionalRightCollection, pendingDocument) - }, 0); + componentWillUnmount() { + window.onpopstate = null; } - // if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized) - setTimeout(() => { - Server.GetField(pendingDocId, (res?: Field) => { - if (res instanceof Document) { - res.GetTAsync<ListField<Document>>(KeyStore.Data, ListField, (f: Opt<ListField<Document>>) => { - if (f && f.Data.length > 0) { - CollectionDockingView.Instance.AddRightSplit(res) + initEventListeners = () => { + // window.addEventListener("pointermove", (e) => this.reportLocation(e)) + window.addEventListener("drop", (e) => e.preventDefault(), false) // drop event handler + window.addEventListener("dragover", (e) => e.preventDefault(), false) // drag event handler + // click interactions for the context menu + document.addEventListener("pointerdown", action(function (e: PointerEvent) { + if (!ContextMenu.Instance.intersects(e.pageX, e.pageY)) { + ContextMenu.Instance.clearItems(); + } + }), true); + } + + initAuthenticationRouters = () => { + // Load the user's active workspace, or create a new one if initial session after signup + request.get(ServerUtils.prepend(RouteStore.getActiveWorkspace), (error, response, body) => { + if (this.mainDocId || body) { + Server.GetField(this.mainDocId || body, field => { + if (field instanceof Document) { + this.openWorkspace(field); + this.populateWorkspaces(); + } else { + this.createNewWorkspace(true, this.mainDocId); } - }) + }); + } else { + this.createNewWorkspace(true, this.mainDocId); } - }) - }, 100) - - 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 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, title: "video node" })); - let addPDFNode = action(() => Documents.PdfDocument(pdfurl, { width: 200, title: "a schema collection" })); - let addImageNode = action(() => Documents.ImageDocument(imgurl, { width: 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(() => - mainfreeform.GetList<Document>(KeyStore.Data, []).push(creator()) - ); - - 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>(); - - ReactDOM.render(( - <div style={{ position: "absolute", width: "100%", height: "100%" }}> - {/* <div id="dash-title">— DASH —</div> */} - <Measure onResize={(r: any) => runInAction(() => { - mainDocFrame.pwidth = r.entry.width; - mainDocFrame.pheight = r.entry.height; - })}> - {({ measureRef }) => - <div ref={measureRef} style={{ position: "absolute", width: "100%", height: "100%" }}> - <DocumentView Document={mainContainer} - AddDocument={undefined} RemoveDocument={undefined} ScreenToLocalTransform={() => Transform.Identity} - ContentScaling={() => 1} - PanelWidth={() => mainDocFrame.pwidth} - PanelHeight={() => mainDocFrame.pheight} - isTopMost={true} - SelectOnLoad={false} - focus={() => { }} - ContainingCollectionView={undefined} /> - </div> + }); + } + + @action + createNewWorkspace = (init: boolean, id?: string): void => { + let mainDoc = Documents.DockDocument(JSON.stringify({ content: [{ type: 'row', content: [] }] }), { title: `Main Container ${this.userWorkspaces.length + 1}` }, id); + let newId = mainDoc.Id; + request.post(ServerUtils.prepend(RouteStore.addWorkspace), { + body: { target: newId }, + json: true + }, () => { if (init) this.populateWorkspaces(); }); + + // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) + setTimeout(() => { + let freeformDoc = Documents.FreeformDocument([], { x: 0, y: 400, title: "mini collection" }); + var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc)] }] }; + mainDoc.SetText(KeyStore.Data, JSON.stringify(dockingLayout)); + mainDoc.Set(KeyStore.ActiveFrame, freeformDoc); + this.openWorkspace(mainDoc); + let pendingDocument = Documents.SchemaDocument([], { title: "New Mobile Uploads" }) + mainDoc.Set(KeyStore.OptionalRightCollection, pendingDocument); + }, 0); + this.userWorkspaces.push(mainDoc); + } + + @action + populateWorkspaces = () => { + // retrieve all workspace documents from the server + request.get(ServerUtils.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 + openWorkspace = (doc: Document, fromHistory = false): void => { + request.post(ServerUtils.prepend(RouteStore.setActiveWorkspace), { + body: { target: doc.Id }, + json: true + }); + this.mainContainer = doc; + fromHistory || window.history.pushState(null, doc.Title, "/doc/" + doc.Id); + this.mainContainer.GetTAsync(KeyStore.ActiveFrame, Document, field => this.mainfreeform = field); + this.mainContainer.GetTAsync(KeyStore.OptionalRightCollection, Document, col => { + // if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized) + setTimeout(() => { + if (col) { + col.GetTAsync<ListField<Document>>(KeyStore.Data, ListField, (f: Opt<ListField<Document>>) => { + if (f && f.Data.length > 0) { + CollectionDockingView.Instance.AddRightSplit(col); + } + }) } - </Measure> - <DocumentDecorations /> - <ContextMenu /> - <button className="clear-db-button" onClick={clearDatabase}>Clear Database</button> - - {/* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */} - < div id="toolbar" > - <button className="toolbar-button round-button" title="Undo" onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button> - <button className="toolbar-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button> - <button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /></button> - </div > - - {/* for the expandable add nodes menu. Not included with the above because once it expands it expands the whole div with it, making canvas interactions limited. */} - < div id="add-nodes-menu" > - <input type="checkbox" id="add-menu-toggle" /> - <label htmlFor="add-menu-toggle" title="Add Node"><p>+</p></label> - - <div id="add-options-content"> - <ul id="add-options-list"> - <li><div ref={textRef}><button className="round-button add-button" title="Add Textbox" onPointerDown={setupDrag(textRef, addTextNode)} onClick={addClick(addTextNode)}> - <FontAwesomeIcon icon="font" size="sm" /> - </button></div></li> - <li><div ref={imgRef}><button className="round-button add-button" title="Add Image" onPointerDown={setupDrag(imgRef, addImageNode)} onClick={addClick(addImageNode)}> - <FontAwesomeIcon icon="image" size="sm" /> - </button></div></li> - <li><div ref={pdfRef}><button className="round-button add-button" title="Add PDF" onPointerDown={setupDrag(pdfRef, addPDFNode)} onClick={addClick(addPDFNode)}> - <FontAwesomeIcon icon="file-pdf" size="sm" /> - </button></div></li> - <li><div ref={videoRef}><button className="round-button add-button" title="Add Video" onPointerDown={setupDrag(videoRef, addVideoNode)} onClick={addClick(addVideoNode)}> - <FontAwesomeIcon icon="film" size="sm" /> - </button></div></li> - <li><div ref={audioRef}><button className="round-button add-button" title="Add Audio" onPointerDown={setupDrag(audioRef, addAudioNode)} onClick={addClick(addAudioNode)}> - <FontAwesomeIcon icon="music" size="sm" /> - </button></div></li> - <li><div ref={webRef}><button className="round-button add-button" title="Add Web Clipping" onPointerDown={setupDrag(webRef, addWebNode)} onClick={addClick(addWebNode)}> - <FontAwesomeIcon icon="globe-asia" size="sm" /> - </button></div></li> - <li><div ref={colRef}><button className="round-button add-button" title="Add Collection" onPointerDown={setupDrag(colRef, addColNode)} onClick={addClick(addColNode)}> - <FontAwesomeIcon icon="object-group" size="sm" /> - </button></div></li> - <li><div ref={schemaRef}><button className="round-button add-button" title="Add Schema" onPointerDown={setupDrag(schemaRef, addSchemaNode)} onClick={addClick(addSchemaNode)}> - <FontAwesomeIcon icon="table" size="sm" /> - </button></div></li> - </ul> - </div> - - </div > - - <InkingControl /> - </div >), - document.getElementById('root')); -}) + }, 100); + }); + } + + toggleWorkspaces = () => { + if (WorkspacesMenu.Instance) { + WorkspacesMenu.Instance.toggle() + } + } + + 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 logoutRef = 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 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())); + + return ( + <div style={{ position: "absolute", width: "100%", height: "100%" }}> + <Measure onResize={(r: any) => runInAction(() => { + this.pwidth = r.entry.width; + this.pheight = r.entry.height; + })}> + {({ measureRef }) => { + if (!this.mainContainer) { + return <div></div> + } + return <div ref={measureRef} style={{ position: "absolute", width: "100%", height: "100%" }}> + <DocumentView Document={this.mainContainer} + AddDocument={undefined} RemoveDocument={undefined} ScreenToLocalTransform={() => Transform.Identity} + ContentScaling={() => 1} + PanelWidth={() => this.pwidth} + PanelHeight={() => this.pheight} + isTopMost={true} + SelectOnLoad={false} + focus={() => { }} + ContainingCollectionView={undefined} /> + </div> + }} + </Measure> + <DocumentDecorations /> + <ContextMenu /> + + <button className="clear-db-button" onClick={clearDatabase}>Clear Database</button> + + {/* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */} + < div id="toolbar" > + <button className="toolbar-button round-button" title="Undo" onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button> + <button className="toolbar-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button> + <button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /></button> + </div > + + <div className="main-buttonDiv" style={{ top: '34px', left: '2px', position: 'absolute' }} ref={workspacesRef}> + <button onClick={this.toggleWorkspaces}>Workspaces</button></div> + <div className="main-buttonDiv" style={{ top: '34px', right: '1px', position: 'absolute' }} ref={logoutRef}> + <button onClick={() => request.get(ServerUtils.prepend(RouteStore.logout), () => { })}>Log Out</button></div> + + <WorkspacesMenu active={this.mainContainer} open={this.openWorkspace} new={this.createNewWorkspace} allWorkspaces={this.userWorkspaces} /> + {/* for the expandable add nodes menu. Not included with the above because once it expands it expands the whole div with it, making canvas interactions limited. */} + < div id="add-nodes-menu" > + <input type="checkbox" id="add-menu-toggle" /> + <label htmlFor="add-menu-toggle" title="Add Node"><p>+</p></label> + + <div id="add-options-content"> + <ul id="add-options-list"> + <li><div ref={textRef}><button className="round-button add-button" title="Add Textbox" onPointerDown={setupDrag(textRef, addTextNode)} onClick={addClick(addTextNode)}> + <FontAwesomeIcon icon="font" size="sm" /> + </button></div></li> + <li><div ref={imgRef}><button className="round-button add-button" title="Add Image" onPointerDown={setupDrag(imgRef, addImageNode)} onClick={addClick(addImageNode)}> + <FontAwesomeIcon icon="image" size="sm" /> + </button></div></li> + <li><div ref={pdfRef}><button className="round-button add-button" title="Add PDF" onPointerDown={setupDrag(pdfRef, addPDFNode)} onClick={addClick(addPDFNode)}> + <FontAwesomeIcon icon="file-pdf" size="sm" /> + </button></div></li> + <li><div ref={videoRef}><button className="round-button add-button" title="Add Video" onPointerDown={setupDrag(videoRef, addVideoNode)} onClick={addClick(addVideoNode)}> + <FontAwesomeIcon icon="film" size="sm" /> + </button></div></li> + <li><div ref={audioRef}><button className="round-button add-button" title="Add Audio" onPointerDown={setupDrag(audioRef, addAudioNode)} onClick={addClick(addAudioNode)}> + <FontAwesomeIcon icon="music" size="sm" /> + </button></div></li> + <li><div ref={webRef}><button className="round-button add-button" title="Add Web Clipping" onPointerDown={setupDrag(webRef, addWebNode)} onClick={addClick(addWebNode)}> + <FontAwesomeIcon icon="globe-asia" size="sm" /> + </button></div></li> + <li><div ref={colRef}><button className="round-button add-button" title="Add Collection" onPointerDown={setupDrag(colRef, addColNode)} onClick={addClick(addColNode)}> + <FontAwesomeIcon icon="object-group" size="sm" /> + </button></div></li> + <li><div ref={schemaRef}><button className="round-button add-button" title="Add Schema" onPointerDown={setupDrag(schemaRef, addSchemaNode)} onClick={addClick(addSchemaNode)}> + <FontAwesomeIcon icon="table" size="sm" /> + </button></div></li> + </ul> + </div> + </div > + + <InkingControl /> + </div> + ); + } +} + +ReactDOM.render(<Main />, document.getElementById('root')); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index ba9e8c29f..19788447e 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -144,7 +144,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp if (this._containerRef.current) { reaction( () => this.props.Document.GetText(KeyStore.Data, ""), - () => this.setupGoldenLayout(), { fireImmediately: true }); + () => setTimeout(() => this.setupGoldenLayout(), 1), { fireImmediately: true }); window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window } diff --git a/src/client/views/collections/CollectionFreeFormView.scss b/src/client/views/collections/CollectionFreeFormView.scss index 79ca5fccd..11addc5a1 100644 --- a/src/client/views/collections/CollectionFreeFormView.scss +++ b/src/client/views/collections/CollectionFreeFormView.scss @@ -8,11 +8,11 @@ } //nested freeform views - //.collectionfreeformview-container { + // .collectionfreeformview-container { // background-image: linear-gradient(to right, $light-color-secondary 1px, transparent 1px), // linear-gradient(to bottom, $light-color-secondary 1px, transparent 1px); // background-size: 30px 30px; - //} + // } border: 0px solid $light-color-secondary; border-radius: $border-radius; diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx index 7ab91ebaa..8bf4a7539 100644 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -11,13 +11,14 @@ import { undoBatch } from "../../util/UndoManager"; import { InkingCanvas } from "../InkingCanvas"; import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; import { DocumentContentsView } from "../nodes/DocumentContentsView"; -import { DocumentView, DocumentViewProps } from "../nodes/DocumentView"; +import { DocumentViewProps } from "../nodes/DocumentView"; import "./CollectionFreeFormView.scss"; import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; import { CollectionViewBase } from "./CollectionViewBase"; import { MarqueeView } from "./MarqueeView"; import { PreviewCursor } from "./PreviewCursor"; import React = require("react"); +import v5 = require("uuid/v5"); @observer export class CollectionFreeFormView extends CollectionViewBase { @@ -290,15 +291,53 @@ export class CollectionFreeFormView extends CollectionViewBase { this.PreviewCursorVisible = false; } + private crosshairs?: HTMLCanvasElement; + drawCrosshairs = (backgroundColor: string) => { + if (this.crosshairs) { + let c = this.crosshairs; + let ctx = c.getContext('2d'); + if (ctx) { + ctx.fillStyle = backgroundColor; + ctx.fillRect(0, 0, 20, 20); + + ctx.fillStyle = "black"; + ctx.lineWidth = 0.5; + + ctx.beginPath(); + + ctx.moveTo(10, 0); + ctx.lineTo(10, 8); + + ctx.moveTo(10, 20); + ctx.lineTo(10, 12); + + ctx.moveTo(0, 10); + ctx.lineTo(8, 10); + + ctx.moveTo(20, 10); + ctx.lineTo(12, 10); + + ctx.stroke(); + + // ctx.font = "10px Arial"; + // ctx.fillText(CurrentUserUtils.email[0].toUpperCase(), 10, 10); + } + } + } + render() { let [dx, dy] = [this.centeringShiftX, this.centeringShiftY]; const panx: number = -this.props.Document.GetNumber(KeyStore.PanX, 0); const pany: number = -this.props.Document.GetNumber(KeyStore.PanY, 0); + // const panx: number = this.props.Document.GetNumber(KeyStore.PanX, 0) + this.centeringShiftX; + // const pany: number = this.props.Document.GetNumber(KeyStore.PanY, 0) + this.centeringShiftY; + // console.log("center:", this.getLocalTransform().transformPoint(this.centeringShiftX, this.centeringShiftY)); return ( <div className={`collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`} onPointerDown={this.onPointerDown} + onPointerMove={(e) => super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY))} onWheel={this.onPointerWheel} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} @@ -313,11 +352,54 @@ export class CollectionFreeFormView extends CollectionViewBase { <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} /> <PreviewCursor container={this} addLiveTextDocument={this.addLiveTextBox} getTransform={this.getTransform} /> {this.views} + {super.getCursors().map(entry => { + if (entry.Data.length > 0) { + let id = entry.Data[0][0]; + let email = entry.Data[0][1]; + let point = entry.Data[1]; + this.drawCrosshairs("#" + v5(id, v5.URL).substring(0, 6).toUpperCase() + "22") + return ( + <div + key={id} + style={{ + position: "absolute", + transform: `translate(${point[0] - 10}px, ${point[1] - 10}px)`, + zIndex: 10000, + transformOrigin: 'center center', + }} + > + <canvas + ref={(el) => { if (el) this.crosshairs = el }} + width={20} + height={20} + style={{ + position: 'absolute', + width: "20px", + height: "20px", + opacity: 0.5, + borderRadius: "50%", + border: "2px solid black" + }} + /> + <p + style={{ + fontSize: 14, + color: "black", + // fontStyle: "italic", + marginLeft: -12, + marginTop: 4 + }} + >{email[0].toUpperCase()}</p> + </div> + ); + } + })} </div> <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} getMarqueeTransform={this.getMarqueeTransform} getTransform={this.getTransform} /> {this.overlayView} + </div> ); } diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index ced08a14e..02e840b39 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -50,7 +50,9 @@ export class CollectionSchemaView extends CollectionViewBase { let onItemDown = setupDrag(reference, () => props.doc, (containingCollection: CollectionView) => this.props.removeDocument(props.doc)); return ( <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} style={{ height: "36px" }} key={props.doc.Id} ref={reference}> - <EditableView contents={contents} + <EditableView + display={"inline"} + contents={contents} height={36} GetValue={() => { let field = props.doc.Get(props.fieldKey); if (field && field instanceof Field) { @@ -59,7 +61,7 @@ export class CollectionSchemaView extends CollectionViewBase { return field || ""; }} SetValue={(value: string) => { - let script = CompileScript(value, undefined, true); + let script = CompileScript(value); if (!script.compiled) { return false; } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 80fc89712..f9da759fd 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -77,7 +77,9 @@ class TreeView extends React.Component<TreeViewProps> { return <div key={this.props.document.Id}></div>; } - return <div className="docContainer"> <EditableView contents={title.Data} + return <div className="docContainer"> <EditableView + display={"inline"} + contents={title.Data} height={36} GetValue={() => { let title = this.props.document.GetT<TextField>(KeyStore.Title, TextField); if (title && title !== "<Waiting>") @@ -167,6 +169,7 @@ export class CollectionTreeView extends CollectionViewBase { <div id="body" className="collectionTreeView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }}> <div className="coll-title"> <EditableView contents={titleStr} + display={"inline"} height={72} GetValue={() => { return this.props.Document.Title; }} SetValue={(value: string) => { diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx index 581853e67..7175e2846 100644 --- a/src/client/views/collections/CollectionViewBase.tsx +++ b/src/client/views/collections/CollectionViewBase.tsx @@ -3,13 +3,16 @@ import { Document } from "../../../fields/Document"; import { ListField } from "../../../fields/ListField"; import React = require("react"); import { KeyStore } from "../../../fields/KeyStore"; -import { FieldWaiting, Field, Opt } from "../../../fields/Field"; +import { FieldWaiting, Opt } from "../../../fields/Field"; import { undoBatch } from "../../util/UndoManager"; import { DragManager } from "../../util/DragManager"; 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"; +import { TupleField } from "../../../fields/TupleField"; +import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { NumberField } from "../../../fields/NumberField"; import { DocumentManager } from "../../util/DocumentManager"; @@ -25,6 +28,7 @@ export interface CollectionViewProps { panelHeight: () => number; focus: (doc: Document) => void; } + export interface SubCollectionViewProps extends CollectionViewProps { active: () => boolean; addDocument: (doc: Document, allowDuplicates: boolean) => void; @@ -32,6 +36,8 @@ export interface SubCollectionViewProps extends CollectionViewProps { CollectionView: CollectionView; } +export type CursorEntry = TupleField<[string, string], [number, number]>; + export class CollectionViewBase extends React.Component<SubCollectionViewProps> { private dropDisposer?: DragManager.DragDropDisposer; protected createDropTarget = (ele: HTMLDivElement) => { @@ -43,14 +49,43 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps> } } + @action + protected setCursorPosition(position: [number, number]) { + let ind; + let doc = this.props.Document; + let id = CurrentUserUtils.id; + let email = CurrentUserUtils.email; + if (id && email) { + let textInfo: [string, string] = [id, email]; + doc.GetOrCreateAsync<ListField<CursorEntry>>(KeyStore.Cursors, ListField, field => { + let cursors = field.Data; + if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.Data[0][0] === id)) > -1) { + cursors[ind].Data[1] = position; + } else { + let entry = new TupleField<[string, string], [number, number]>([textInfo, position]); + cursors.push(entry); + } + }) + + + } + } + + protected getCursors(): CursorEntry[] { + let doc = this.props.Document; + let id = CurrentUserUtils.id; + let cursors = doc.GetList<CursorEntry>(KeyStore.Cursors, []); + let notMe = cursors.filter(entry => entry.Data[0][0] !== id); + return id ? notMe : []; + } + @undoBatch @action protected drop(e: Event, de: DragManager.DropEvent) { if (de.data instanceof DragManager.DocumentDragData) { if (de.data.aliasOnDrop) { - let dragged = de.data.draggedDocument; [KeyStore.Width, KeyStore.Height, KeyStore.CurPage].map(key => - dragged.GetTAsync(key, NumberField, (f: Opt<NumberField>) => f ? de.data.droppedDocument.SetNumber(key, f.Data) : null)); + de.data.draggedDocument.GetTAsync(key, NumberField, (f: Opt<NumberField>) => f ? de.data.droppedDocument.SetNumber(key, f.Data) : null)); } else if (de.data.removeDocument) { de.data.removeDocument(this.props.CollectionView); } @@ -83,7 +118,7 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps> console.log(e.dataTransfer.items.length); for (let i = 0; i < e.dataTransfer.items.length; i++) { - const upload = window.location.origin + "/upload"; + 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(action((s: string) => this.props.addDocument(Documents.WebDocument(s, options), false))) @@ -91,7 +126,6 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps> let type = item.type console.log(type) if (item.kind == "file") { - let fReader = new FileReader() let file = item.getAsFile(); let formData = new FormData() diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 430c1b694..dd2f71b59 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -16,6 +16,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faEye } from '@fortawesome/free-solid-svg-icons'; import { faEdit } from '@fortawesome/free-solid-svg-icons'; import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { undoBatch } from "../../util/UndoManager"; library.add(faEye); @@ -33,8 +34,8 @@ interface Props { @observer export class LinkBox extends React.Component<Props> { + @undoBatch onViewButtonPressed = (e: React.PointerEvent): void => { - console.log("view down"); e.stopPropagation(); let docView = DocumentManager.Instance.getDocumentView(this.props.pairedDoc); if (docView) { diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index b9d260380..3a0ef2d32 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, @@ -441,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}> |