diff options
25 files changed, 781 insertions, 553 deletions
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 432e53825..03178bbdb 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -17,7 +17,7 @@ export enum DocumentType { TEMPLATE = "template", EXTENSION = "extension", YOUTUBE = "youtube", - FONTICONBOX = "fonticonbox", + FONTICON = "fonticonbox", PRES = "presentation", LINKFOLLOW = "linkfollow", PRESELEMENT = "preselement", diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 1f89d2993..f4fce34ac 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -40,7 +40,7 @@ import { ButtonBox } from "../views/nodes/ButtonBox"; import { FontIconBox } from "../views/nodes/FontIconBox"; import { SchemaHeaderField, RandomPastel } from "../../new_fields/SchemaHeaderField"; import { PresBox } from "../views/nodes/PresBox"; -import { ComputedField } from "../../new_fields/ScriptField"; +import { ComputedField, ScriptField } from "../../new_fields/ScriptField"; import { ProxyField } from "../../new_fields/Proxy"; import { DocumentType } from "./DocumentTypes"; import { LinkFollowBox } from "../views/linking/LinkFollowBox"; @@ -65,7 +65,10 @@ export interface DocumentOptions { page?: number; scale?: number; fitWidth?: boolean; + forceActive?: boolean; + preventTreeViewOpen?: boolean; // ignores the treeViewOpen Doc flag which allows a treeViewItem's expande/collapse state to be independent of other views of the same document in the tree view layout?: string | Doc; + hideHeadings?: boolean; // whether stacking view column headings should be hidden isTemplate?: boolean; templates?: List<string>; viewType?: number; @@ -84,11 +87,23 @@ export interface DocumentOptions { documentText?: string; borderRounding?: string; boxShadow?: string; + sectionFilter?: string; // field key used to determine headings for sections in stacking and masonry views schemaColumns?: List<SchemaHeaderField>; dockingConfig?: string; autoHeight?: boolean; + removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document dbDoc?: Doc; + unchecked?: ScriptField; // returns whether a check box is unchecked + activePen?: Doc; // which pen document is currently active (used as the radio button state for the 'unhecked' pen tool scripts) + onClick?: ScriptField; + onDragStart?: ScriptField; //script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop icon?: string; + gridGap?: number; // gap between items in masonry view + xMargin?: number; // gap between left edge of document and start of masonry/stacking layouts + yMargin?: number; // gap between top edge of dcoument and start of masonry/stacking layouts + panel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script + targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script + convertToButtons?: boolean; // whether documents dropped onto a collection should be converted to buttons that will construct the dropped document // [key: string]: Opt<Field>; } @@ -185,7 +200,7 @@ export namespace Docs { layout: { view: PresBox }, options: {} }], - [DocumentType.FONTICONBOX, { + [DocumentType.FONTICON, { layout: { view: FontIconBox }, options: { width: 40, height: 40 }, }], @@ -457,6 +472,10 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Freeform }, id); } + export function LinearDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", backgroundColor: "black", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Linear }, id); + } + export function SchemaDocument(schemaColumns: SchemaHeaderField[], documents: Array<Doc>, options: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List(schemaColumns), ...options, viewType: CollectionViewType.Schema }); } @@ -479,7 +498,7 @@ export namespace Docs { export function FontIconDocument(options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.FONTICONBOX), undefined, { ...(options || {}) }); + return InstanceFromProto(Prototypes.get(DocumentType.FONTICON), undefined, { ...(options || {}) }); } export function LinkFollowBoxDocument(options?: DocumentOptions) { diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 2c316ccdf..92666c03c 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -234,9 +234,9 @@ export namespace DragManager { export let StartDragFunctions: (() => void)[] = []; - export async function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) { + export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) { runInAction(() => StartDragFunctions.map(func => func())); - await dragData.draggedDocuments.map(d => d.dragFactory); + dragData.draggedDocuments.map(d => d.dragFactory); // does this help? trying to make sure the dragFactory Doc is loaded StartDrag(eles, dragData, downX, downY, options, options && options.finishDrag ? options.finishDrag : (dropData: { [id: string]: any }) => { (dropData.droppedDocuments = @@ -430,12 +430,13 @@ export namespace DragManager { } if (((options && !options.withoutShiftDrag) || !options) && e.shiftKey && CollectionDockingView.Instance) { AbortDrag(); + finishDrag && finishDrag(dragData); CollectionDockingView.Instance.StartOtherDrag({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 - }, docs); + }, dragData.droppedDocuments); } //TODO: Why can't we use e.movementX and e.movementY? let moveX = e.pageX - lastX; diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index df1b46b33..2d717ca57 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -45,17 +45,6 @@ export namespace SelectionManager { } const manager = new Manager(); - reaction(() => manager.SelectedDocuments, sel => { - let targetColor = "#FFFFFF"; - if (sel.length > 0) { - let firstView = sel[0]; - let doc = firstView.props.Document; - let targetDoc = doc.isTemplate ? doc : Doc.GetProto(doc); - let stored = StrCast(targetDoc.backgroundColor); - stored.length > 0 && (targetColor = stored); - } - InkingControl.Instance.updateSelectedColor(targetColor); - }, { fireImmediately: true }); export function DeselectDoc(docView: DocumentView): void { manager.DeselectDoc(docView); diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index c82d3bc63..31d98887f 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -850,7 +850,7 @@ export class TooltipTextMenu { } this.view = view; let state = view.state; - DocumentDecorations.Instance.TextBar && DocumentDecorations.Instance.setTextBar(DocumentDecorations.Instance.TextBar); + DocumentDecorations.Instance.showTextBar(); props && (this.editorProps = props); // Don't do anything if the document/selection didn't change if (lastState && lastState.doc.eq(state.doc) && diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index 7abb9d1ee..472afac1d 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -73,8 +73,8 @@ export namespace UndoManager { } type UndoBatch = UndoEvent[]; - let undoStack: UndoBatch[] = observable([]); - let redoStack: UndoBatch[] = observable([]); + export let undoStack: UndoBatch[] = observable([]); + export let redoStack: UndoBatch[] = observable([]); let currentBatch: UndoBatch | undefined; let batchCounter = 0; let undoing = false; diff --git a/src/client/views/CollectionLinearView.scss b/src/client/views/CollectionLinearView.scss index 46a226eef..4423a7020 100644 --- a/src/client/views/CollectionLinearView.scss +++ b/src/client/views/CollectionLinearView.scss @@ -4,74 +4,70 @@ .collectionLinearView-outer{ overflow: hidden; height:100%; - padding:5px; -} -.collectionLinearView { - display:flex; - >label { - background: $dark-color; - color: $light-color; - display: inline-block; - border-radius: 18px; - font-size: 25px; - width: 36px; - height: 36px; - margin-right: 10px; - cursor: pointer; - transition: transform 0.2s; - } + .collectionLinearView { + display:flex; + height: 100%; + >label { + background: $dark-color; + color: $light-color; + display: inline-block; + border-radius: 18px; + font-size: 12.5px; + width: 18px; + height: 18px; + margin-top:auto; + margin-bottom:auto; + cursor: pointer; + transition: transform 0.2s; + } - label p { - padding-left: 10.5px; - width: 500px; - height: 500px; - } + label p { + padding-left:5px; + } - label:hover { - background: $main-accent; - transform: scale(1.15); - } + label:hover { + background: $main-accent; + transform: scale(1.15); + } - >input { - display: none; - } - >input:not(:checked)~.collectionLinearView-content { - display: none; - } + >input { + display: none; + } + >input:not(:checked)~.collectionLinearView-content { + display: none; + } - >input:checked~label { - transform: rotate(45deg); - transition: transform 0.5s; - cursor: pointer; - } + >input:checked~label { + transform: rotate(45deg); + transition: transform 0.5s; + cursor: pointer; + } - .collectionLinearView-content { - display: flex; - opacity: 1; - padding: 0; - position: relative; - .collectionFreeFormDocumentView-container { + .collectionLinearView-content { + display: flex; + opacity: 1; position: relative; - } - .collectionLinearView-docBtn { - position:relative; - margin-right: 10px; - } - .collectionLinearView-round-button { - width: 36px; - height: 36px; - border-radius: 18px; - font-size: 15px; - } - - .collectionLinearView-round-button:hover { - transform: scale(1.15); + margin-top: auto; + + .collectionLinearView-docBtn, .collectionLinearView-docBtn-scalable { + position:relative; + margin-top: auto; + margin-bottom: auto; + } + .collectionLinearView-docBtn-scalable:hover { + transform: scale(1.15); + } + + .collectionLinearView-round-button { + width: 18px; + height: 18px; + border-radius: 18px; + font-size: 15px; + } + + .collectionLinearView-round-button:hover { + transform: scale(1.15); + } } - - } - - .collectionLinearView-add-button { - position: relative; - margin-right: 10px; } } diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx index 692f940f8..eb3c768d0 100644 --- a/src/client/views/CollectionLinearView.tsx +++ b/src/client/views/CollectionLinearView.tsx @@ -1,22 +1,20 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, observable, computed } from 'mobx'; +import { action, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast, Opt } from '../../new_fields/Doc'; -import { InkTool } from '../../new_fields/InkField'; +import { Doc, HeightSym, WidthSym } from '../../new_fields/Doc'; import { ObjectField } from '../../new_fields/ObjectField'; +import { makeInterface } from '../../new_fields/Schema'; import { ScriptField } from '../../new_fields/ScriptField'; -import { NumCast, StrCast } from '../../new_fields/Types'; -import { emptyFunction, returnEmptyString, returnOne, returnTrue, returnFalse, Utils } from '../../Utils'; +import { BoolCast, NumCast, StrCast } from '../../new_fields/Types'; +import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../Utils'; import { Docs } from '../documents/Documents'; import { DragManager } from '../util/DragManager'; import { Transform } from '../util/Transform'; -import { UndoManager } from '../util/UndoManager'; -import { InkingControl } from './InkingControl'; -import { DocumentView, documentSchema } from './nodes/DocumentView'; import "./CollectionLinearView.scss"; -import { makeInterface } from '../../new_fields/Schema'; +import { CollectionViewType } from './collections/CollectionBaseView'; import { CollectionSubView } from './collections/CollectionSubView'; +import { documentSchema, DocumentView } from './nodes/DocumentView'; +import { translate } from 'googleapis/build/src/apis/translate'; type LinearDocument = makeInterface<[typeof documentSchema,]>; @@ -26,11 +24,20 @@ const LinearDocument = makeInterface(documentSchema); export class CollectionLinearView extends CollectionSubView(LinearDocument) { @observable public addMenuToggle = React.createRef<HTMLInputElement>(); private _dropDisposer?: DragManager.DragDropDisposer; + private _heightDisposer?: IReactionDisposer; componentWillUnmount() { this._dropDisposer && this._dropDisposer(); + this._heightDisposer && this._heightDisposer(); } + componentDidMount() { + // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported). + this._heightDisposer = reaction(() => NumCast(this.props.Document.height, 0) + this.childDocs.length + (this.props.Document.isExpanded ? 1 : 0), + () => this.props.Document.width = 18 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10), + { fireImmediately: true } + ); + } protected createDropTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view this._dropDisposer && this._dropDisposer(); if (ele) { @@ -41,11 +48,13 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { drop = action((e: Event, de: DragManager.DropEvent) => { (de.data as DragManager.DocumentDragData).draggedDocuments.map((doc, i) => { let dbox = doc; - if (!doc.onDragStart && this.props.Document.convertToButtons) { + if (!doc.onDragStart && !doc.onClick && this.props.Document.convertToButtons && doc.viewType !== CollectionViewType.Linear) { dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: "bolt" }); dbox.dragFactory = doc; dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined; dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); + } else if (doc.viewType === CollectionViewType.Linear) { + dbox.ignoreClick = true; } (de.data as DragManager.DocumentDragData).droppedDocuments[i] = dbox; }); @@ -53,63 +62,68 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { return super.drop(e, de); }); - selected = (tool: InkTool) => { - if (!InkingControl.Instance || InkingControl.Instance.selectedTool === InkTool.None) return { display: "none" }; - if (InkingControl.Instance.selectedTool === tool) { - return { color: "#61aaa3", fontSize: "50%" }; - } - return { fontSize: "50%" }; - } - public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); } - dimension = () => NumCast(this.props.Document.height) - 5; + dimension = () => NumCast(this.props.Document.height); // 2 * the padding + getTransform = (ele: React.RefObject<HTMLDivElement>) => () => { + if (!ele.current) return Transform.Identity(); + let { scale, translateX, translateY } = Utils.GetScreenTransform(ele.current); + return new Transform(-translateX, -translateY, 1 / scale); + }; + _spacing = 20; render() { let guid = Utils.GenerateGuid(); - return <div className="collectionLinearView-outer"><div className="collectionLinearView" ref={this.createDropTarget} > - <input id={`${guid}`} type="checkbox" ref={this.addMenuToggle} /> - <label htmlFor={`${guid}`} style={{ marginTop: (this.dimension() - 36) / 2, marginBottom: "auto" }} title="Close Menu"><p>+</p></label> + return <div className="collectionLinearView-outer"> + <div className="collectionLinearView" ref={this.createDropTarget} > + <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.isExpanded)} ref={this.addMenuToggle} + onChange={action((e: any) => this.props.Document.isExpanded = this.addMenuToggle.current!.checked)} /> + <label htmlFor={`${guid}`} style={{ marginTop: "auto", marginBottom: "auto", background: StrCast(this.props.Document.backgroundColor, "black") === StrCast(this.props.Document.color, "white") ? "black" : StrCast(this.props.Document.backgroundColor, "black") }} title="Close Menu"><p>+</p></label> - <div className="collectionLinearView-content"> - {this.props.showHiddenControls ? <button key="undo" className="collectionLinearView-add-button collectionLinearView-round-button" title="Undo" style={{ opacity: UndoManager.CanUndo() ? 1 : 0.5, transition: "0.4s ease all" }} onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button> : (null)} - {this.props.showHiddenControls ? <button key="redo" className="collectionLinearView-add-button collectionLinearView-round-button" title="Redo" style={{ opacity: UndoManager.CanRedo() ? 1 : 0.5, transition: "0.4s ease all" }} onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button> : (null)} + <div className="collectionLinearView-content"> + {this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => { + let nested = pair.layout.viewType === CollectionViewType.Linear; + let dref = React.createRef<HTMLDivElement>(); + let nativeWidth = NumCast(pair.layout.nativeWidth, this.dimension()); + let scalingContent = nested ? 1 : this.dimension() / (this._spacing + nativeWidth); + let scalingBox = nested ? 1 : this.dimension() / nativeWidth; + let deltaSize = nativeWidth * scalingBox - nativeWidth * scalingContent; + return <div className={`collectionLinearView-docBtn` + (pair.layout.onClick || pair.layout.onDragStart ? "-scalable" : "")} key={StrCast(pair.layout.title)} ref={dref} + style={{ + width: nested ? pair.layout[WidthSym]() : this.dimension(), + height: nested && pair.layout.isExpanded ? pair.layout[HeightSym]() : this.dimension(), + transform: nested ? undefined : `translate(${deltaSize / 2}px, ${deltaSize / 2}px)` + }} > + <DocumentView + Document={pair.layout} + DataDoc={pair.data} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + addDocTab={this.props.addDocTab} + pinToPres={emptyFunction} + removeDocument={this.props.removeDocument} + ruleProvider={undefined} + onClick={undefined} + ScreenToLocalTransform={this.getTransform(dref)} + ContentScaling={() => scalingContent} // ugh - need to get rid of this inline function to avoid recomputing + PanelWidth={() => nested ? pair.layout[WidthSym]() : this.dimension()} + PanelHeight={() => nested ? pair.layout[HeightSym]() : this.dimension()} + renderDepth={this.props.renderDepth + 1} + focus={emptyFunction} + backgroundColor={returnEmptyString} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + bringToFront={emptyFunction} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + zoomToScale={emptyFunction} + getScale={returnOne}> + </DocumentView> + </div> + })} + {/* <li key="undoTest"><button className="add-button round-button" title="Click if undo isn't working" onClick={() => UndoManager.TraceOpenBatches()}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li> */} - {this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => - <div className="collectionLinearView-docBtn" style={{ width: this.dimension(), height: this.dimension() }} key={StrCast(pair.layout.title)} > - <DocumentView - Document={pair.layout} - DataDoc={pair.data} - addDocument={this.props.addDocument} - addDocTab={returnFalse} - pinToPres={emptyFunction} - removeDocument={this.props.removeDocument} - ruleProvider={undefined} - onClick={undefined} - ScreenToLocalTransform={Transform.Identity} - ContentScaling={() => this.dimension() / (10 + NumCast(pair.layout.nativeWidth, this.dimension()))} // ugh - need to get rid of this inline function to avoid recomputing - PanelWidth={this.dimension} - PanelHeight={this.dimension} - renderDepth={this.props.renderDepth + 1} - focus={emptyFunction} - backgroundColor={returnEmptyString} - parentActive={returnTrue} - whenActiveChanged={emptyFunction} - bringToFront={emptyFunction} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - zoomToScale={emptyFunction} - getScale={returnOne}> - </DocumentView> - </div>)} - {/* <li key="undoTest"><button className="add-button round-button" title="Click if undo isn't working" onClick={() => UndoManager.TraceOpenBatches()}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li> */} - {this.props.showHiddenControls ? <> - <button className="collectionLinearView-toolbar-button collectionLinearView-round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /> </button> - <button key="pen" onClick={() => InkingControl.Instance.switchTool(InkTool.Pen)} title="Pen" style={this.selected(InkTool.Pen)}><FontAwesomeIcon icon="pen" size="lg" /></button> - <button key="marker" onClick={() => InkingControl.Instance.switchTool(InkTool.Highlighter)} title="Highlighter" style={this.selected(InkTool.Highlighter)}><FontAwesomeIcon icon="highlighter" size="lg" /></button> - <button key="eraser" onClick={() => InkingControl.Instance.switchTool(InkTool.Eraser)} title="Eraser" style={this.selected(InkTool.Eraser)}><FontAwesomeIcon icon="eraser" size="lg" /></button> - <InkingControl /> - </> : (null)} + </div> </div> - </div></div>; + </div>; } }
\ No newline at end of file diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 0ed443a99..9d7c06750 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -2,20 +2,53 @@ import * as React from 'react'; import { Doc } from '../../new_fields/Doc'; import { Touchable } from './Touchable'; import { computed, action } from 'mobx'; -import { Cast } from '../../new_fields/Types'; +import { Cast, BoolCast } from '../../new_fields/Types'; import { listSpec } from '../../new_fields/Schema'; +import { InkingControl } from './InkingControl'; +import { InkTool } from '../../new_fields/InkField'; +<<<<<<< HEAD export function DocComponent<P extends { Document: Doc }, T>(schemaCtor: (doc: Doc) => T) { class Component extends Touchable<P> { +======= + +/// DocComponents returns a generic base class for React views of document fields that are not interactive +interface DocComponentProps { + Document: Doc; +} +export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: Doc) => T) { + class Component extends React.Component<P> { +>>>>>>> 33811c112c7e479813908ba10f72813954a3e289 + //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then + @computed + get Document(): T { + return schemaCtor(this.props.Document); + } + } + return Component; +} + + +/// DocStaticProps return a base class for React views of document fields that are interactive only when selected (e.g. ColorBox) +interface DocStaticProps { + Document: Doc; + isSelected: () => boolean; + renderDepth: number; +} +export function DocStaticComponent<P extends DocStaticProps, T>(schemaCtor: (doc: Doc) => T) { + class Component extends React.Component<P> { //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then @computed get Document(): T { return schemaCtor(this.props.Document); } + active = () => (this.props.Document.forceActive || this.props.isSelected() || this.props.renderDepth === 0);// && !InkingControl.Instance.selectedTool; // bcz: inking state shouldn't affect static tools } return Component; } + +/// DocAnnotatbleComponent return a base class for React views of document fields that are annotatable *and* interactive when selected (e.g., pdf, image) interface DocAnnotatableProps { Document: Doc; DataDoc?: Doc; @@ -58,7 +91,7 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema return Doc.AddDocToList(this.extensionDoc, this.props.fieldExt, doc); } whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive); - active = () => this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0; + active = () => (InkingControl.Instance.selectedTool === InkTool.None) && (BoolCast(this.props.Document.forceActive) || this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0); } return Component; }
\ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 1d9f0c74b..3d73f048d 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -547,10 +547,14 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } TextBar: HTMLDivElement | undefined; - setTextBar = (ele: HTMLDivElement) => { + private setTextBar = (ele: HTMLDivElement) => { if (ele) { this.TextBar = ele; - TooltipTextMenu.Toolbar && Array.from(ele.childNodes).indexOf(TooltipTextMenu.Toolbar) === -1 && ele.appendChild(TooltipTextMenu.Toolbar); + } + } + public showTextBar = () => { + if (this.TextBar) { + TooltipTextMenu.Toolbar && Array.from(this.TextBar.childNodes).indexOf(TooltipTextMenu.Toolbar) === -1 && this.TextBar.appendChild(TooltipTextMenu.Toolbar); } } render() { diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index 38734a03d..149a4fc8a 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -1,38 +1,30 @@ -import { observable, action, computed, runInAction } from "mobx"; +import { action, computed, observable } from "mobx"; import { ColorResult } from 'react-color'; -import React = require("react"); -import { observer } from "mobx-react"; -import "./InkingControl.scss"; -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faPen, faHighlighter, faEraser, faBan } from '@fortawesome/free-solid-svg-icons'; -import { SelectionManager } from "../util/SelectionManager"; -import { InkTool } from "../../new_fields/InkField"; import { Doc } from "../../new_fields/Doc"; -import { undoBatch, UndoManager } from "../util/UndoManager"; -import { StrCast, NumCast, Cast } from "../../new_fields/Types"; -import { listSpec } from "../../new_fields/Schema"; +import { InkTool } from "../../new_fields/InkField"; import { List } from "../../new_fields/List"; +import { listSpec } from "../../new_fields/Schema"; +import { Cast, NumCast, StrCast } from "../../new_fields/Types"; import { Utils } from "../../Utils"; +import { Scripting } from "../util/Scripting"; +import { SelectionManager } from "../util/SelectionManager"; +import { undoBatch, UndoManager } from "../util/UndoManager"; -library.add(faPen, faHighlighter, faEraser, faBan); -@observer -export class InkingControl extends React.Component { +export class InkingControl { @observable static Instance: InkingControl; @observable private _selectedTool: InkTool = InkTool.None; @observable private _selectedColor: string = "rgb(244, 67, 54)"; @observable private _selectedWidth: string = "5"; @observable public _open: boolean = false; - constructor(props: Readonly<{}>) { - super(props); - runInAction(() => InkingControl.Instance = this); + constructor() { + InkingControl.Instance = this; } - @action - switchTool = (tool: InkTool): void => { + switchTool = action((tool: InkTool): void => { this._selectedTool = tool; - } + }) decimalToHexString(number: number) { if (number < 0) { number = 0xFFFFFFFF + number + 1; @@ -48,6 +40,14 @@ export class InkingControl extends React.Component { let selected = SelectionManager.SelectedDocuments(); let oldColors = selected.map(view => { let targetDoc = view.props.Document.layout instanceof Doc ? view.props.Document.layout : view.props.Document.isTemplate ? view.props.Document : Doc.GetProto(view.props.Document); + let sel = window.getSelection(); + if (StrCast(targetDoc.layout).indexOf("FormattedTextBox") !== -1 && (!sel || sel.toString() !== "")) { + targetDoc.color = this._selectedColor; + return { + target: targetDoc, + previous: StrCast(targetDoc.color) + }; + } let oldColor = StrCast(targetDoc.backgroundColor); let matchedColor = this._selectedColor; const cvd = view.props.ContainingCollectionDoc; @@ -116,22 +116,10 @@ export class InkingControl extends React.Component { return this._selectedWidth; } - @action - toggleDisplay = () => { - this._open = !this._open; - this.switchTool(this._open ? InkTool.Pen : InkTool.None); - } - render() { - return ( - <ul className="inking-control" style={this._open ? { display: "flex" } : { display: "none" }}> - <li className="ink-size ink-panel"> - <label htmlFor="stroke-width">SIZE: </label> - <input type="text" min="1" max="100" value={this._selectedWidth} name="stroke-width" - onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.switchWidth(e.target.value)} /> - <input type="range" min="1" max="100" value={this._selectedWidth} name="stroke-width" - onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.switchWidth(e.target.value)} /> - </li> - </ul > - ); - } -}
\ No newline at end of file +} +Scripting.addGlobal(function activatePen(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Pen : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.updateSelectedColor(color); }); +Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Highlighter : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.updateSelectedColor(color); }); +Scripting.addGlobal(function activateEraser(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Eraser : InkTool.None); }); +Scripting.addGlobal(function deactivateInk() { return InkingControl.Instance.switchTool(InkTool.None); }); +Scripting.addGlobal(function setInkWidth(width: any) { return InkingControl.Instance.switchWidth(width); }); +Scripting.addGlobal(function setInkColor(color: any) { return InkingControl.Instance.updateSelectedColor(color); });
\ No newline at end of file diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index 851818099..a6b28f488 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -34,10 +34,15 @@ display: flex; flex-direction: column; position: absolute; +<<<<<<< HEAD width: 100%; height: 100%; border: black 1px solid; +======= + width:100%; + height:100%; +>>>>>>> 33811c112c7e479813908ba10f72813954a3e289 .documentView-node-topmost { background: lightgrey; } @@ -68,8 +73,13 @@ .mainView-expandFlyoutButton { position: absolute; +<<<<<<< HEAD top: 100px; right: 30px; +======= + top: 5px; + right: 5px; +>>>>>>> 33811c112c7e479813908ba10f72813954a3e289 cursor: pointer; } diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index dd4e07165..2f48b66c0 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,5 +1,8 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv, faChevronRight, faEllipsisV, faCompressArrowsAlt } from '@fortawesome/free-solid-svg-icons'; +import { + faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, + faMusic, faObjectGroup, faPause, faPenNib, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faCompressArrowsAlt +} from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; @@ -100,6 +103,8 @@ export class MainView extends React.Component { library.add(faGlobeAsia); library.add(faUndoAlt); library.add(faRedoAlt); + library.add(faPen); + library.add(faEraser); library.add(faPenNib); library.add(faFilm); library.add(faMusic); @@ -254,9 +259,11 @@ export class MainView extends React.Component { @computed get dockingContent() { const mainContainer = this.mainContainer; + let flyoutWidth = this.flyoutWidth; // bcz: need to be here because Measure messes with observables. + let flyoutTranslate = this._flyoutTranslate; return <Measure offset onResize={this.onResize}> {({ measureRef }) => - <div ref={measureRef} className="mainView-mainDiv" onDrop={this.onDrop}> + <div ref={measureRef} id="mainContent-div" style={{ width: `calc(100% - ${flyoutTranslate ? flyoutWidth : 0}px`, transform: `translate(${flyoutTranslate ? flyoutWidth : 0}px, 0px)` }} onDrop={this.onDrop}> {!mainContainer ? (null) : <DocumentView Document={mainContainer} DataDoc={undefined} @@ -338,18 +345,19 @@ export class MainView extends React.Component { return CollectionDockingView.AddRightSplit(doc, undefined); } } + mainContainerXf = () => new Transform(0, -this._buttonBarHeight, 1); @computed get flyout() { let sidebarContent = this.userDoc && this.userDoc.sidebarContainer; if (!(sidebarContent instanceof Doc)) { return (null); } - let libraryButtonDoc = Cast(CurrentUserUtils.UserDocument.libraryButtons, Doc) as Doc; - libraryButtonDoc.columnWidth = this.flyoutWidth / 3 - 30; + let sidebarButtonsDoc = Cast(CurrentUserUtils.UserDocument.sidebarButtons, Doc) as Doc; + sidebarButtonsDoc.columnWidth = this.flyoutWidth / 3 - 30; return <div className="mainView-flyoutContainer"> <div className="mainView-tabButtons" style={{ height: `${this._buttonBarHeight}px` }}> <DocumentView - Document={libraryButtonDoc} + Document={sidebarButtonsDoc} DataDoc={undefined} addDocument={undefined} addDocTab={this.addDocTabFunc} @@ -383,7 +391,7 @@ export class MainView extends React.Component { removeDocument={returnFalse} ruleProvider={undefined} onClick={undefined} - ScreenToLocalTransform={Transform.Identity} + ScreenToLocalTransform={this.mainContainerXf} ContentScaling={returnOne} PanelWidth={this.flyoutWidthFunc} PanelHeight={this.getContentsHeight} @@ -437,51 +445,64 @@ export class MainView extends React.Component { } @computed get expandButton() { - return !this._flyoutTranslate ? (<div className="mainView-expandFlyoutButton" title="Re-attach sidebar" onPointerDown={() => { - runInAction(() => { - this.flyoutWidth = 250; - this._flyoutTranslate = true; - }); - }}><FontAwesomeIcon icon="chevron-right" color="grey" size="lg" /></div>) : (null); + return !this._flyoutTranslate ? (<div className="mainView-expandFlyoutButton" title="Re-attach sidebar" onPointerDown={action(() => { + this.flyoutWidth = 250; + this._flyoutTranslate = true; + })}><FontAwesomeIcon icon="chevron-right" color="grey" size="lg" /></div>) : (null); } addButtonDoc = (doc: Doc) => { - Doc.AddDocToList(CurrentUserUtils.UserDocument, "docButtons", doc); + Doc.AddDocToList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", doc); return true; } remButtonDoc = (doc: Doc) => { - Doc.RemoveDocFromList(CurrentUserUtils.UserDocument, "docButtons", doc); + Doc.RemoveDocFromList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", doc); return true; } + @action + moveButtonDoc = (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean): boolean => { + return this.remButtonDoc(doc) && addDocument(doc); + } + buttonBarXf = () => { + if (!this._docBtnRef.current) return Transform.Identity(); + let { scale, translateX, translateY } = Utils.GetScreenTransform(this._docBtnRef.current); + return new Transform(-translateX, -translateY, 1 / scale); + } + _docBtnRef = React.createRef<HTMLDivElement>(); @computed get docButtons() { - return <div className="mainView-docButtons" style={{ left: (this._flyoutTranslate ? this.flyoutWidth : 0) + 20 }} > - <MainViewNotifs /> - <CollectionLinearView Document={CurrentUserUtils.UserDocument} DataDoc={undefined} - fieldKey={"docButtons"} - fieldExt={""} - showHiddenControls={true} - select={emptyFunction} - chromeCollapsed={true} - active={returnFalse} - isSelected={returnFalse} - moveDocument={returnFalse} - CollectionView={undefined} - addDocument={this.addButtonDoc} - addDocTab={this.addDocTabFunc} - pinToPres={emptyFunction} - removeDocument={this.remButtonDoc} - ruleProvider={undefined} - onClick={undefined} - ScreenToLocalTransform={Transform.Identity} - ContentScaling={returnOne} - PanelWidth={this.flyoutWidthFunc} - PanelHeight={this.getContentsHeight} - renderDepth={0} - focus={emptyFunction} - whenActiveChanged={emptyFunction} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} /> - </div>; + if (CurrentUserUtils.UserDocument.expandingButtons instanceof Doc) { + return <div className="mainView-docButtons" ref={this._docBtnRef} + style={{ left: (this._flyoutTranslate ? this.flyoutWidth : 0) + 20, height: !CurrentUserUtils.UserDocument.expandingButtons.isExpanded ? "42px" : undefined }} > + <MainViewNotifs /> + <CollectionLinearView + Document={CurrentUserUtils.UserDocument.expandingButtons} + DataDoc={undefined} + fieldKey={"data"} + fieldExt={""} + select={emptyFunction} + chromeCollapsed={true} + active={returnFalse} + isSelected={returnFalse} + moveDocument={this.moveButtonDoc} + CollectionView={undefined} + addDocument={this.addButtonDoc} + addDocTab={this.addDocTabFunc} + pinToPres={emptyFunction} + removeDocument={this.remButtonDoc} + ruleProvider={undefined} + onClick={undefined} + ScreenToLocalTransform={this.buttonBarXf} + ContentScaling={returnOne} + PanelWidth={this.flyoutWidthFunc} + PanelHeight={this.getContentsHeight} + renderDepth={0} + focus={emptyFunction} + whenActiveChanged={emptyFunction} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} /> + </div>; + } + return (null); } render() { diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 57722b9b7..90fc9c3e0 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -49,7 +49,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { return Math.min(this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin, this.isStackingView ? Number.MAX_VALUE : NumCast(this.props.Document.columnWidth, 250)); } - @computed get NodeWidth() { return this.props.PanelWidth(); } + @computed get NodeWidth() { return this.props.PanelWidth() - this.gridGap; } childDocHeight(child: Doc) { return this.getDocHeight(Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, child).layout); } @@ -119,8 +119,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { (this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap), this.yMargin)), 0); } else { let sum = Array.from(this._heightMap.values()).reduce((acc: number, curr: number) => acc += curr, 0); - sum += 30; - return this.props.ContentScaling() * (sum + (this.Sections.size ? 50 : 0)); + return this.props.ContentScaling() * (sum + (this.Sections.size ? 85 : -15)); } } return -1; @@ -399,13 +398,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { {sections.map(section => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1]))} {!this.showAddAGroup ? (null) : <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" - style={{ width: this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}> + style={{ width: !this.isStackingView ? "100%" : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}> <EditableView {...editableViewProps} /> </div>} - {this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.chromeStatus !== 'disabled' ? <Switch + {this.props.Document.chromeStatus !== 'disabled' ? <Switch onChange={this.onToggle} onClick={this.onToggle} - defaultChecked={this.props.ContainingCollectionDoc.chromeStatus !== 'view-mode'} + defaultChecked={this.props.Document.chromeStatus !== 'view-mode'} checkedChildren="edit" unCheckedChildren="view" /> : null} diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 6e8e4fa12..fdbe5339d 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -33,7 +33,6 @@ export interface CollectionViewProps extends FieldViewProps { VisibleHeight?: () => number; chromeCollapsed: boolean; setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; - showHiddenControls?: boolean; // hack for showing the undo/redo/ink controls in a linear view -- needs to be redone } export interface SubCollectionViewProps extends CollectionViewProps { diff --git a/src/client/views/nodes/ColorBox.scss b/src/client/views/nodes/ColorBox.scss index 8df617fca..32a7891c6 100644 --- a/src/client/views/nodes/ColorBox.scss +++ b/src/client/views/nodes/ColorBox.scss @@ -1,10 +1,13 @@ -.colorBox-container { +.colorBox-container, .colorBox-container-interactive { width:100%; height:100%; position: relative; - pointer-events:all; + pointer-events: none; .sketch-picker { margin:auto; } +} +.colorBox-container-interactive { + pointer-events:all; }
\ No newline at end of file diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx index 4aff770f9..87c91c121 100644 --- a/src/client/views/nodes/ColorBox.tsx +++ b/src/client/views/nodes/ColorBox.tsx @@ -4,13 +4,48 @@ import { SketchPicker } from 'react-color'; import { FieldView, FieldViewProps } from './FieldView'; import "./ColorBox.scss"; import { InkingControl } from "../InkingControl"; +import { DocStaticComponent } from "../DocComponent"; +import { documentSchema } from "./DocumentView"; +import { makeInterface } from "../../../new_fields/Schema"; +import { trace, reaction, observable, action, IReactionDisposer } from "mobx"; +import { SelectionManager } from "../../util/SelectionManager"; +import { StrCast } from "../../../new_fields/Types"; + +type ColorDocument = makeInterface<[typeof documentSchema]>; +const ColorDocument = makeInterface(documentSchema); @observer -export class ColorBox extends React.Component<FieldViewProps> { +export class ColorBox extends DocStaticComponent<FieldViewProps, ColorDocument>(ColorDocument) { public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ColorBox, fieldKey); } + _selectedDisposer: IReactionDisposer | undefined; + componentDidMount() { + this._selectedDisposer = reaction(() => SelectionManager.SelectedDocuments(), + action(() => this._startupColor = SelectionManager.SelectedDocuments().length ? StrCast(SelectionManager.SelectedDocuments()[0].Document.backgroundColor, "black") : "black"), + { fireImmediately: true }); + + // compare to this reaction that used to be in Selection Manager + // reaction(() => manager.SelectedDocuments, sel => { + // let targetColor = "#FFFFFF"; + // if (sel.length > 0) { + // let firstView = sel[0]; + // let doc = firstView.props.Document; + // let targetDoc = doc.isTemplate ? doc : Doc.GetProto(doc); + // let stored = StrCast(targetDoc.backgroundColor); + // stored.length > 0 && (targetColor = stored); + // } + // InkingControl.Instance.updateSelectedColor(targetColor); + // }, { fireImmediately: true }); + } + componentWillUnmount() { + this._selectedDisposer && this._selectedDisposer(); + } + + @observable _startupColor = "black"; + render() { - return <div className="colorBox-container" > - <SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} /> + return <div className={`colorBox-container${this.active() ? "-interactive" : ""}`} + onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}> + <SketchPicker color={this._startupColor} onChange={InkingControl.Instance.switchColor} /> </div>; } -}
\ No newline at end of file +}
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index ca1cdbd9d..32acad9d9 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -40,6 +40,7 @@ import SharingManager from '../../util/SharingManager'; import { Scripting } from '../../util/Scripting'; import { InteractionUtils } from '../../util/InteractionUtils'; import { DictationOverlay } from '../DictationOverlay'; +import { CollectionViewType } from '../collections/CollectionBaseView'; library.add(fa.faEdit); library.add(fa.faTrash); @@ -106,8 +107,8 @@ export const documentSchema = createSchema({ dropAction: "string", // override specifying what should happen when this document is dropped (can be "alias" or "copy") removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped onClick: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop) - onDragStart: ScriptField, // script to run when document is dragged (without being selected). the script should return an Doc to drag. - dragFactory: Doc, // the document that serves as the "template" for the onDragStart script + onDragStart: ScriptField, // script to run when document is dragged (without being selected). the script should return the Doc to be dropped. + dragFactory: Doc, // the document that serves as the "template" for the onDragStart script. ie, to drag out copies of the dragFactory document. ignoreAspect: "boolean", // whether aspect ratio should be ignored when laying out or manipulating the document autoHeight: "boolean", // whether the height of the document should be computed automatically based on its contents isTemplate: "boolean", // whether this document acts as a template layout for describing how other documents should be displayed @@ -168,7 +169,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (this._mainCont.current) { let dragData = new DragManager.DocumentDragData([this.props.Document]); const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0); - dragData.offset = this.Document.onDragStart ? [0, 0] : this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); + dragData.offset = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); dragData.dropAction = dropAction; dragData.moveDocument = this.Document.onDragStart ? undefined : this.props.moveDocument; dragData.applyAsTemplate = applyAsTemplate; @@ -176,7 +177,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu handlers: { dragComplete: action((emptyFunction)) }, - hideSource: !dropAction && !this.Document.onDragStart + hideSource: !dropAction && !this.Document.onDragStart && !this.Document.onClick }); } } @@ -186,7 +187,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { e.stopPropagation(); let preventDefault = true; - if (this._doubleTap && this.props.renderDepth) { + if (this._doubleTap && this.props.renderDepth && (!this.onClickHandler || !this.onClickHandler.script)) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click let fullScreenAlias = Doc.MakeAlias(this.props.Document); let layoutNative = await PromiseValue(Cast(this.props.Document.layoutNative, Doc)); if (layoutNative && fullScreenAlias.layout === layoutNative.layout) { @@ -197,6 +198,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu Doc.UnBrushDoc(this.props.Document); } else if (this.onClickHandler && this.onClickHandler.script) { this.onClickHandler.script.run({ this: this.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log); + } else if (this.props.Document.type === DocumentType.BUTTON) { + ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY); } else if (this.Document.isButton) { SelectionManager.SelectDoc(this, e.ctrlKey); // don't think this should happen if a button action is actually triggered. this.buttonClick(e.altKey, e.ctrlKey); @@ -248,7 +251,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this._hitTemplateDrag = true; } } - if ((this.active || this.Document.onDragStart) && e.button === 0 && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag); + if ((this.active || this.Document.onDragStart || this.Document.onClick) && e.button === 0 && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag); document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); @@ -261,9 +264,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (e.cancelBubble && this.active) { document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView) } - else if (!e.cancelBubble && (SelectionManager.IsSelected(this) || this.props.parentActive() || this.Document.onDragStart) && !this.Document.lockedPosition && !this.Document.inOverlay) { + else if (!e.cancelBubble && (SelectionManager.IsSelected(this) || this.props.parentActive() || this.Document.onDragStart || this.Document.onClick) && !this.Document.lockedPosition && !this.Document.inOverlay) { if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { - if (!e.altKey && (!this.topMost || this.Document.onDragStart) && e.buttons === 1) { + if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick) && e.buttons === 1) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag); @@ -677,6 +680,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"]; const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid", "solid"]; + let highlighting = fullDegree && this.props.Document.type !== DocumentType.FONTICON && this.props.Document.viewType !== CollectionViewType.Linear return ( <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} @@ -684,9 +688,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu transition: this.props.Document.isAnimating !== undefined ? ".5s linear" : StrCast(this.Document.transition), pointerEvents: this.Document.isBackground && !this.isSelected() ? "none" : "all", color: StrCast(this.Document.color), - outline: fullDegree && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", - border: fullDegree && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, - background: backgroundColor, + outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", + border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, + background: this.props.Document.type === DocumentType.FONTICON || this.props.Document.viewType === CollectionViewType.Linear ? undefined : backgroundColor, width: animwidth, height: animheight, transform: `scale(${this.props.Document.fitWidth ? 1 : this.props.ContentScaling()})`, diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index 3b580d851..efe47d8a8 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -5,6 +5,7 @@ import { createSchema, makeInterface } from '../../../new_fields/Schema'; import { DocComponent } from '../DocComponent'; import './FontIconBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; +import { StrCast } from '../../../new_fields/Types'; const FontIconSchema = createSchema({ icon: "string" }); @@ -16,6 +17,8 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>( public static LayoutString() { return FieldView.LayoutString(FontIconBox); } render() { - return <div className="fontIconBox-outerDiv" > <FontAwesomeIcon className="fontIconBox-icon" icon={this.Document.icon as any} size="lg" color="white" /> </div>; + return <button className="fontIconBox-outerDiv" style={{ background: StrCast(this.props.Document.backgroundColor) }}> + <FontAwesomeIcon className="fontIconBox-icon" icon={this.Document.icon as any} size="sm" opacity={this.props.Document.unchecked ? "0.5" : "1"} /> + </button>; } }
\ No newline at end of file diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index b05d0046c..1bdff3ec7 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -43,7 +43,6 @@ import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './Format import React = require("react"); import { ContextMenuProps } from '../ContextMenuItem'; import { ContextMenu } from '../ContextMenu'; -import { TextShadowProperty } from 'csstype'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -490,7 +489,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe ); this._heightReactionDisposer = reaction( - () => this.props.Document[WidthSym](), + () => [this.props.Document[WidthSym](), this.props.Document.autoHeight], () => this.tryUpdateHeight() ); @@ -928,16 +927,18 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe onMouseUp = (e: React.MouseEvent): void => { e.stopPropagation(); - // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there are nested prosemirrors. We only want the lowest level prosemirror to be invoked. - if ((this._editorView as any).mouseDown) { - let originalUpHandler = (this._editorView as any).mouseDown.up; - (this._editorView as any).root.removeEventListener("mouseup", originalUpHandler); - (this._editorView as any).mouseDown.up = (e: MouseEvent) => { + let view = this._editorView as any; + // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there + // are nested prosemirrors. We only want the lowest level prosemirror to be invoked. + if (view.mouseDown) { + let originalUpHandler = view.mouseDown.up; + view.root.removeEventListener("mouseup", originalUpHandler); + view.mouseDown.up = (e: MouseEvent) => { !(e as any).formattedHandled && originalUpHandler(e); - e.stopPropagation(); + // e.stopPropagation(); (e as any).formattedHandled = true; }; - (this._editorView as any).root.addEventListener("mouseup", (this._editorView as any).mouseDown.up); + view.root.addEventListener("mouseup", view.mouseDown.up); } } @@ -984,13 +985,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe @action tryUpdateHeight() { - const ChromeHeight = this.props.ChromeHeight; - let sh = this._ref.current ? this._ref.current.scrollHeight : 0; - if (!this.props.Document.isAnimating && this.props.Document.autoHeight && sh !== 0 && getComputedStyle(this._ref.current!.parentElement!).top === "0px") { + let scrollHeight = this._ref.current ? this._ref.current.scrollHeight : 0; + if (!this.props.Document.isAnimating && this.props.Document.autoHeight && scrollHeight !== 0 && + getComputedStyle(this._ref.current!.parentElement!).top === "0px") { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation let nh = this.props.Document.isTemplate ? 0 : NumCast(this.dataDoc.nativeHeight, 0); let dh = NumCast(this.props.Document.height, 0); - this.props.Document.height = Math.max(10, (nh ? dh / nh * sh : sh) + (ChromeHeight ? ChromeHeight() : 0)); - this.dataDoc.nativeHeight = nh ? sh : undefined; + this.props.Document.height = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0)); + this.dataDoc.nativeHeight = nh ? scrollHeight : undefined; } } diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 63b412a23..19797400f 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -115,7 +115,6 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument> private newScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => this._scriptValue = e.currentTarget.value; whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive); - active = () => this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0; setPdfViewer = (pdfViewer: PDFViewer) => { this._pdfViewer = pdfViewer; }; searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value; @@ -205,7 +204,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument> _initialScale: number | undefined; // the initial scale of the PDF when first rendered which determines whether the document will be live on startup or not. Getting bigger after startup won't make it automatically be live.... render() { const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); - let classname = "pdfBox-cont" + (InkingControl.Instance.selectedTool || !this.active ? "" : "-interactive"); + let classname = "pdfBox-cont" + (this.active() ? "-interactive" : ""); let noPdf = !(pdfUrl instanceof PdfField) || !this._pdf; if (this._initialScale === undefined) this._initialScale = this.props.ScreenToLocalTransform().Scale; if (this.props.isSelected() || this.props.Document.scrollY !== undefined) this._everActive = true; diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 66036f673..276596fb8 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -14,6 +14,7 @@ import { ComputedField } from "./ScriptField"; import { BoolCast, Cast, FieldValue, NumCast, PromiseValue, StrCast, ToConstructor } from "./Types"; import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util"; import { intersectRect } from "../Utils"; +import { UndoManager } from "../client/util/UndoManager"; export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { @@ -524,6 +525,7 @@ export namespace Doc { export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string): Doc { const copy = new Doc(copyProtoId, true); Object.keys(doc).forEach(key => { + let cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); const field = ProxyField.WithoutProxy(() => doc[key]); if (key === "proto" && copyProto) { if (doc[key] instanceof Doc) { @@ -532,6 +534,8 @@ export namespace Doc { } else { if (field instanceof RefField) { copy[key] = field; + } else if (cfield instanceof ComputedField) { + copy[key] = ComputedField.MakeFunction(cfield.script.originalScript); } else if (field instanceof ObjectField) { copy[key] = ObjectField.MakeCopy(field); } else if (field instanceof Promise) { @@ -733,4 +737,7 @@ Scripting.addGlobal(function getAlias(doc: any) { return Doc.MakeAlias(doc); }); Scripting.addGlobal(function getCopy(doc: any, copyProto: any) { return Doc.MakeCopy(doc, copyProto); }); Scripting.addGlobal(function copyField(field: any) { return ObjectField.MakeCopy(field); }); Scripting.addGlobal(function aliasDocs(field: any) { return new List<Doc>(field.map((d: any) => Doc.MakeAlias(d))); }); -Scripting.addGlobal(function docList(field: any) { return DocListCast(field); });
\ No newline at end of file +Scripting.addGlobal(function docList(field: any) { return DocListCast(field); }); +Scripting.addGlobal(function sameDocs(doc1: any, doc2: any) { return Doc.AreProtosEqual(doc1, doc2) }); +Scripting.addGlobal(function undo() { return UndoManager.Undo(); }); +Scripting.addGlobal(function redo() { return UndoManager.Redo(); });
\ No newline at end of file diff --git a/src/server/RouteSubscriber.ts b/src/server/RouteSubscriber.ts new file mode 100644 index 000000000..e49be8af5 --- /dev/null +++ b/src/server/RouteSubscriber.ts @@ -0,0 +1,26 @@ +export default class RouteSubscriber { + private _root: string; + private requestParameters: string[] = []; + + constructor(root: string) { + this._root = root; + } + + add(...parameters: string[]) { + this.requestParameters.push(...parameters); + return this; + } + + public get root() { + return this._root; + } + + public get build() { + let output = this._root; + if (this.requestParameters.length) { + output = `${output}/:${this.requestParameters.join("/:")}`; + } + return output; + } + +}
\ No newline at end of file diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 3858907ba..3c4a46ed8 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -1,18 +1,18 @@ -import { action, computed, observable, runInAction } from "mobx"; +import { action, computed, observable, reaction, runInAction } from "mobx"; import * as rp from 'request-promise'; import { DocServer } from "../../../client/DocServer"; import { Docs } from "../../../client/documents/Documents"; import { Attribute, AttributeGroup, Catalog, Schema } from "../../../client/northstar/model/idea/idea"; import { ArrayUtil } from "../../../client/northstar/utils/ArrayUtil"; -import { CollectionViewType } from "../../../client/views/collections/CollectionBaseView"; -import { CollectionView } from "../../../client/views/collections/CollectionView"; +import { UndoManager } from "../../../client/util/UndoManager"; import { Doc, DocListCast } from "../../../new_fields/Doc"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; -import { Cast, StrCast, PromiseValue } from "../../../new_fields/Types"; +import { ScriptField, ComputedField } from "../../../new_fields/ScriptField"; +import { Cast, PromiseValue } from "../../../new_fields/Types"; import { Utils } from "../../../Utils"; import { RouteStore } from "../../RouteStore"; -import { ScriptField } from "../../../new_fields/ScriptField"; +import { InkingControl } from "../../../client/views/InkingControl"; export class CurrentUserUtils { private static curr_id: string; @@ -20,167 +20,184 @@ export class CurrentUserUtils { private static mainDocId: string | undefined; public static get id() { return this.curr_id; } - @computed public static get UserDocument() { return Doc.UserDoc(); } public static get MainDocId() { return this.mainDocId; } public static set MainDocId(id: string | undefined) { this.mainDocId = id; } + @computed public static get UserDocument() { return Doc.UserDoc(); } @observable public static GuestTarget: Doc | undefined; @observable public static GuestWorkspace: Doc | undefined; private static createUserDocument(id: string): Doc { let doc = new Doc(id, true); - doc.viewType = CollectionViewType.Tree; - doc.layout = CollectionView.LayoutString(); doc.title = Doc.CurrentUserEmail; - this.updateUserDocument(doc); - doc.data = new List<Doc>(); - doc.gridGap = 5; - doc.xMargin = 5; - doc.yMargin = 5; - doc.height = 42; - doc.boxShadow = "0 0"; - doc.convertToButtons = true; // for CollectionLinearView used as the docButton layout - doc.optionalRightCollection = Docs.Create.StackingDocument([], { title: "New mobile uploads" }); - return doc; + return this.updateUserDocument(doc);// this should be the last } - static updateUserDocument(doc: Doc) { + // a default set of note types .. not being used yet... + static setupNoteTypes(doc: Doc) { + let notes = [ + Docs.Create.TextDocument({ title: "Note", backgroundColor: "yellow", isTemplate: true }), + Docs.Create.TextDocument({ title: "Idea", backgroundColor: "pink", isTemplate: true }), + Docs.Create.TextDocument({ title: "Topic", backgroundColor: "lightBlue", isTemplate: true }), + Docs.Create.TextDocument({ title: "Person", backgroundColor: "lightGreen", isTemplate: true }) + ]; + doc.noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", height: 75 }); + } + + // setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools + static setupCreatorButtons(doc: Doc) { + doc.activePen = doc; + let docProtoData: { title: string, icon: string, drag?: string, click?: string, unchecked?: string, activePen?: Doc, backgroundColor?: string }[] = [ + { title: "collection", icon: "folder", drag: 'Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })' }, + { title: "web page", icon: "globe-asia", drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })' }, + { title: "image", icon: "cat", drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })' }, + { title: "button", icon: "bolt", drag: 'Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })' }, + { title: "presentation", icon: "tv", drag: 'Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" })' }, + { title: "import folder", icon: "cloud-upload-alt", drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })' }, + { title: "pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc }, + { title: "highlighter", icon: "pen", click: 'activateBrush(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc }, + { title: "eraser", icon: "eraser", click: 'activateEraser(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc }, + { title: "none", icon: "pause", click: 'deactivateInk();this.activePen.pen = this;', unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc }, + ]; + return docProtoData.map(data => Docs.Create.FontIconDocument({ + nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, + onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined, + unchecked: data.unchecked ? ComputedField.MakeFunction(data.unchecked) : undefined, activePen: data.activePen, + backgroundColor: data.backgroundColor + })); + } + + // setup the Creator button which will display the creator panel. This panel will include the drag creators and the color picker. when clicked, this panel will be displayed in the target container (ie, sidebarContainer) + static setupCreatePanel(sidebarContainer: Doc, doc: Doc) { + // setup a masonry view of all he creators + const dragCreators = Docs.Create.MasonryDocument(CurrentUserUtils.setupCreatorButtons(doc), { + width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons" + }); + // setup a color picker + const color = Docs.Create.ColorDocument({ + title: "color picker", width: 400, removeDropProperties: new List<string>(["dropAction", "forceActive"]) + }); + color.dropAction = "alias"; // these must be set on the view document so they can't be part of the creator above. + color.forceActive = true; + return Docs.Create.ButtonDocument({ + width: 35, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Create", targetContainer: sidebarContainer, + panel: Docs.Create.StackingDocument([dragCreators, color], { + width: 500, height: 800, chromeStatus: "disabled", title: "creator stack" + }), + onClick: ScriptField.MakeScript("this.targetContainer.proto = this.panel") + }); + } + + // setup the Library button which will display the library panel. This panel includes a collection of workspaces, documents, and recently closed views + static setupLibraryPanel(sidebarContainer: Doc, doc: Doc) { // setup workspaces library item - if (doc.workspaces === undefined) { - const workspaces = Docs.Create.TreeDocument([], { title: "WORKSPACES", height: 100 }); - workspaces.boxShadow = "0 0"; - doc.workspaces = workspaces; - } - PromiseValue(Cast(doc.workspaces, Doc)).then(workspaces => { - if (workspaces) { - workspaces.backgroundColor = "#eeeeee"; - workspaces.preventTreeViewOpen = true; - workspaces.forceActive = true; - workspaces.lockedPosition = true; - if (StrCast(workspaces.title) === "Workspaces") { - workspaces.title = "WORKSPACES"; - } - } + doc.workspaces = Docs.Create.TreeDocument([], { + title: "WORKSPACES", height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true, backgroundColor: "#eeeeee" }); - // setup notes list - if (doc.noteTypes === undefined) { - let notes = [Docs.Create.TextDocument({ title: "Note", backgroundColor: "yellow", isTemplate: true }), - Docs.Create.TextDocument({ title: "Idea", backgroundColor: "pink", isTemplate: true }), - Docs.Create.TextDocument({ title: "Topic", backgroundColor: "lightBlue", isTemplate: true }), - Docs.Create.TextDocument({ title: "Person", backgroundColor: "lightGreen", isTemplate: true })]; - const noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", height: 75 }); - doc.noteTypes = noteTypes; - } - PromiseValue(Cast(doc.noteTypes, Doc)).then(noteTypes => noteTypes && PromiseValue(noteTypes.data).then(DocListCast)); + doc.documents = Docs.Create.TreeDocument([], { + title: "DOCUMENTS", gridGap: 5, xMargin: 5, yMargin: 5, height: 42, width: 100, boxShadow: "0 0", backgroundColor: "#eeeeee", preventTreeViewOpen: true, forceActive: true, lockedPosition: true + }); // setup Recently Closed library item - if (doc.recentlyClosed === undefined) { - const recentlyClosed = Docs.Create.TreeDocument([], { title: "Recently Closed".toUpperCase(), height: 75 }); - recentlyClosed.boxShadow = "0 0"; - doc.recentlyClosed = recentlyClosed; - } - PromiseValue(Cast(doc.recentlyClosed, Doc)).then(recent => { - if (recent) { - recent.backgroundColor = "#eeeeee"; - recent.preventTreeViewOpen = true; - recent.forceActive = true; - recent.lockedPosition = true; - if (StrCast(recent.title) === "Recently Closed") { - recent.title = "RECENTLY CLOSED"; - } - } + doc.recentlyClosed = Docs.Create.TreeDocument([], { + title: "Recently Closed".toUpperCase(), height: 75, boxShadow: "0 0", preventTreeViewOpen: true, forceActive: true, lockedPosition: true, backgroundColor: "#eeeeee" }); + return Docs.Create.ButtonDocument({ + width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Library", + panel: Docs.Create.TreeDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { + title: "Library", xMargin: 5, yMargin: 5, gridGap: 5, forceActive: true, dropAction: "alias", lockedPosition: true + }), + targetContainer: sidebarContainer, + onClick: ScriptField.MakeScript("this.targetContainer.proto = this.panel") + }); + } - if (doc.curPresentation === undefined) { - const curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation" }); - curPresentation.boxShadow = "0 0"; - doc.curPresentation = curPresentation; - } + // setup the Search button which will display the search panel. + static setupSearchPanel(sidebarContainer: Doc) { + return Docs.Create.ButtonDocument({ + width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Search", + panel: Docs.Create.QueryDocument({ + title: "search stack", ignoreClick: true + }), + targetContainer: sidebarContainer, + onClick: ScriptField.MakeScript("this.targetContainer.proto = this.panel") + }); + } - if (doc.Library === undefined) { - let Search = Docs.Create.ButtonDocument({ width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Search" }); - let Library = Docs.Create.ButtonDocument({ width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Library" }); - let Create = Docs.Create.ButtonDocument({ width: 35, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Create" }); - if (doc.sidebarContainer === undefined) { - doc.sidebarContainer = new Doc(); - (doc.sidebarContainer as Doc).chromeStatus = "disabled"; - } + // setup the list of sidebar mode buttons which determine what is displayed in the sidebar + static setupSidebarButtons(doc: Doc) { + doc.sidebarContainer = new Doc(); + (doc.sidebarContainer as Doc).chromeStatus = "disabled"; - const library = Docs.Create.TreeDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { title: "Library" }); - library.forceActive = true; - library.lockedPosition = true; - library.gridGap = 5; - library.xMargin = 5; - library.yMargin = 5; - library.dropAction = "alias"; - Library.targetContainer = doc.sidebarContainer; - Library.library = library; - Library.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.library"); + doc.CreateBtn = this.setupCreatePanel(doc.sidebarContainer as Doc, doc); + doc.LibraryBtn = this.setupLibraryPanel(doc.sidebarContainer as Doc, doc); + doc.SearchBtn = this.setupSearchPanel(doc.sidebarContainer as Doc); - const searchBox = Docs.Create.QueryDocument({ title: "search stack" }); - searchBox.ignoreClick = true; - Search.searchBox = searchBox; - Search.targetContainer = doc.sidebarContainer; - Search.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.searchBox"); + // Finally, setup the list of buttons to display in the sidebar + doc.sidebarButtons = Docs.Create.StackingDocument([doc.SearchBtn as Doc, doc.LibraryBtn as Doc, doc.CreateBtn as Doc], { + width: 500, height: 80, boxShadow: "0 0", sectionFilter: "title", hideHeadings: true, ignoreClick: true, + backgroundColor: "lightgrey", chromeStatus: "disabled", title: "library stack" + }); + } - let createCollection = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Collection", icon: "folder" }); - createCollection.onDragStart = ScriptField.MakeFunction('Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })'); - let createWebPage = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Web Page", icon: "globe-asia" }); - createWebPage.onDragStart = ScriptField.MakeFunction('Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })'); - let createCatImage = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Image", icon: "cat" }); - createCatImage.onDragStart = ScriptField.MakeFunction('Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })'); - let createButton = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Button", icon: "bolt" }); - createButton.onDragStart = ScriptField.MakeFunction('Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })'); - let createPresentation = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Presentation", icon: "tv" }); - createPresentation.onDragStart = ScriptField.MakeFunction('Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" })'); - let createFolderImport = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Import Folder", icon: "cloud-upload-alt" }); - createFolderImport.onDragStart = ScriptField.MakeFunction('Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })'); - const dragCreators = Docs.Create.MasonryDocument([createCollection, createWebPage, createCatImage, createButton, createPresentation, createFolderImport], { width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons" }); - const color = Docs.Create.ColorDocument({ title: "color picker", width: 400 }); - color.dropAction = "alias"; - color.ignoreClick = true; - color.removeDropProperties = new List<string>(["dropAction", "ignoreClick"]); - const creators = Docs.Create.StackingDocument([dragCreators, color], { width: 500, height: 800, chromeStatus: "disabled", title: "creator stack" }); - Create.targetContainer = doc.sidebarContainer; - Create.creators = creators; - Create.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.creators"); + /// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window + static setupExpandingButtons(doc: Doc) { + doc.undoBtn = Docs.Create.FontIconDocument( + { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: "alias", onClick: ScriptField.MakeScript("undo()"), title: "undo button", icon: "undo-alt" }); + doc.redoBtn = Docs.Create.FontIconDocument( + { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: "alias", onClick: ScriptField.MakeScript("redo()"), title: "redo button", icon: "redo-alt" }); - const libraryButtons = Docs.Create.StackingDocument([Search, Library, Create], { width: 500, height: 80, chromeStatus: "disabled", title: "library stack" }); - libraryButtons.sectionFilter = "title"; - libraryButtons.boxShadow = "0 0"; - libraryButtons.ignoreClick = true; - libraryButtons.hideHeadings = true; - libraryButtons.backgroundColor = "lightgrey"; + doc.expandingButtons = Docs.Create.LinearDocument([doc.undoBtn as Doc, doc.redoBtn as Doc], { + title: "expanding buttons", gridGap: 5, xMargin: 5, yMargin: 5, height: 42, width: 100, boxShadow: "0 0", + backgroundColor: "black", preventTreeViewOpen: true, forceActive: true, lockedPosition: true, convertToButtons: true + }); + } - doc.libraryButtons = libraryButtons; - doc.Library = Library; - doc.Create = Create; - doc.Search = Search; - } - PromiseValue(Cast(doc.libraryButtons, Doc)).then(libraryButtons => { }); - PromiseValue(Cast(doc.Library, Doc)).then(library => library && library.library && library.targetContainer && (library.onClick as ScriptField).script.run({ this: library })); - PromiseValue(Cast(doc.Create, Doc)).then(async create => create && create.creators && create.targetContainer); - PromiseValue(Cast(doc.Search, Doc)).then(async search => search && search.searchBox && search.targetContainer); + // sets up the default set of documents to be shown in the Overlay layer + static setupOverlays(doc: Doc) { + doc.overlays = Docs.Create.FreeformDocument([], { title: "Overlays", backgroundColor: "#aca3a6" }); + doc.linkFollowBox = Docs.Create.LinkFollowBoxDocument({ x: 250, y: 20, width: 500, height: 370, title: "Link Follower" }); + Doc.AddDocToList(doc.overlays as Doc, "data", doc.linkFollowBox as Doc); + } - if (doc.overlays === undefined) { - const overlays = Docs.Create.FreeformDocument([], { title: "Overlays" }); - Doc.GetProto(overlays).backgroundColor = "#aca3a6"; - doc.overlays = overlays; - } + // the initial presentation Doc to use + static setupDefaultPresentation(doc: Doc) { + doc.curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", boxShadow: "0 0" }); + } - if (doc.linkFollowBox === undefined) { - PromiseValue(Cast(doc.overlays, Doc)).then(overlays => overlays && Doc.AddDocToList(overlays, "data", doc.linkFollowBox = Docs.Create.LinkFollowBoxDocument({ x: 250, y: 20, width: 500, height: 370, title: "Link Follower" }))); - } + static setupMobileUploads(doc: Doc) { + doc.optionalRightCollection = Docs.Create.StackingDocument([], { title: "New mobile uploads" }); + } + + static updateUserDocument(doc: Doc) { + new InkingControl(); + (doc.optionalRightCollection === undefined) && CurrentUserUtils.setupMobileUploads(doc); + (doc.noteTypes === undefined) && CurrentUserUtils.setupNoteTypes(doc); + (doc.overlays === undefined) && CurrentUserUtils.setupOverlays(doc); + (doc.expandingButtons === undefined) && CurrentUserUtils.setupExpandingButtons(doc); + (doc.curPresentation === undefined) && CurrentUserUtils.setupDefaultPresentation(doc); + (doc.sidebarButtons === undefined) && CurrentUserUtils.setupSidebarButtons(doc); - doc.title = "DOCUMENTS"; - doc.backgroundColor = "#eeeeee"; - doc.width = 100; - doc.preventTreeViewOpen = true; - doc.forceActive = true; - doc.lockedPosition = true; + // this is equivalent to using PrefetchProxies to make sure all the sidebarButtons and noteType internal Doc's have been retrieved. + PromiseValue(Cast(doc.noteTypes, Doc)).then(noteTypes => noteTypes && PromiseValue(noteTypes.data).then(DocListCast)); + PromiseValue(Cast(doc.sidebarButtons, Doc)).then(stackingDoc => { + stackingDoc && PromiseValue(Cast(stackingDoc.data, listSpec(Doc))).then(sidebarButtons => { + sidebarButtons && sidebarButtons.map((sidebarBtn, i) => { + sidebarBtn && PromiseValue(Cast(sidebarBtn, Doc)).then(async btn => { + btn && btn.panel && btn.targetContainer && i === 1 && (btn.onClick as ScriptField).script.run({ this: btn }); + }); + }); + }); + }); + + // setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet + doc.undoBtn && reaction(() => UndoManager.undoStack.slice(), () => (doc.undoBtn as Doc).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true }); + doc.redoBtn && reaction(() => UndoManager.redoStack.slice(), () => (doc.redoBtn as Doc).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true }); + + return doc; } public static loadCurrentUser() { @@ -200,12 +217,8 @@ export class CurrentUserUtils { await rp.get(Utils.prepend(RouteStore.getUserDocumentId)).then(id => { if (id && id !== "guest") { return DocServer.GetRefField(id).then(async field => { - if (field instanceof Doc) { - await this.updateUserDocument(field); - runInAction(() => Doc.SetUserDoc(field)); - } else { - runInAction(() => Doc.SetUserDoc(this.createUserDocument(id))); - } + let userDoc = field instanceof Doc ? await this.updateUserDocument(field) : this.createUserDocument(id); + runInAction(() => Doc.SetUserDoc(userDoc)); }); } else { throw new Error("There should be a user id! Why does Dash think there isn't one?"); diff --git a/src/server/index.ts b/src/server/index.ts index 010a851bc..c1dba2976 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -55,6 +55,7 @@ import { ParsedPDF } from "./PdfTypes"; import { reject } from 'bluebird'; import { ExifData } from 'exif'; import { Result } from '../client/northstar/model/idea/idea'; +import RouteSubscriber from './RouteSubscriber'; const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest)); let youtubeApiKey: string; @@ -106,6 +107,26 @@ enum Method { POST } +export type ValidationHandler = (user: DashUserModel, req: express.Request, res: express.Response) => any | Promise<any>; +export type RejectionHandler = (req: express.Request, res: express.Response) => any | Promise<any>; +export type ErrorHandler = (req: express.Request, res: express.Response, error: any) => any | Promise<any>; + +const LoginRedirect: RejectionHandler = (_req, res) => res.redirect(RouteStore.login); + +export interface RouteInitializer { + method: Method; + subscribers: string | RouteSubscriber | (string | RouteSubscriber)[]; + onValidation: ValidationHandler; + onRejection?: RejectionHandler; + onError?: ErrorHandler; +} + +const isSharedDocAccess = (target: string) => { + const shared = qs.parse(qs.extract(target), { sort: false }).sharing === "true"; + const docAccess = target.startsWith("/doc/"); + return shared && docAccess; +}; + /** * 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 @@ -115,22 +136,40 @@ enum Method { * @param onRejection an optional callback invoked on return if no user is found to be logged in * @param subscribers the forward slash prepended path names (reference and add to RouteStore.ts) that will all invoke the given @param handler */ -function addSecureRoute(method: Method, - handler: (user: DashUserModel, res: express.Response, req: express.Request) => void, - onRejection: (res: express.Response, req: express.Request) => any = res => res.redirect(RouteStore.login), - ...subscribers: string[] -) { - let abstracted = (req: express.Request, res: express.Response) => { - let sharing = qs.parse(qs.extract(req.originalUrl), { sort: false }).sharing === "true"; - sharing = sharing && req.originalUrl.startsWith("/doc/"); - if (req.user || sharing) { - handler(req.user as any, res, req); +function addSecureRoute(initializer: RouteInitializer) { + const { method, subscribers, onValidation, onRejection, onError } = initializer; + let abstracted = async (req: express.Request, res: express.Response) => { + const { user, originalUrl: target } = req; + if (user || isSharedDocAccess(target)) { + try { + await onValidation(user as any, req, res); + } catch (e) { + if (onError) { + onError(req, res, e); + } else { + _error(res, `The server encountered an internal error handling ${target}.`, e); + } + } } else { - req.session!.target = req.originalUrl; - onRejection(res, req); + req.session!.target = target; + try { + await (onRejection || LoginRedirect)(req, res); + } catch (e) { + if (onError) { + onError(req, res, e); + } else { + _error(res, `The server encountered an internal error when rejecting ${target}.`, e); + } + } } }; - subscribers.forEach(route => { + const subscribe = (subscriber: RouteSubscriber | string) => { + let route: string; + if (typeof subscriber === "string") { + route = subscriber; + } else { + route = subscriber.build; + } switch (method) { case Method.GET: app.get(route, abstracted); @@ -139,7 +178,12 @@ function addSecureRoute(method: Method, app.post(route, abstracted); break; } - }); + }; + if (Array.isArray(subscribers)) { + subscribers.forEach(subscribe); + } else { + subscribe(subscribers); + } } // STATIC FILE SERVING @@ -323,13 +367,17 @@ app.get("/serializeDoc/:docId", async (req, res) => { export type Hierarchy = { [id: string]: string | Hierarchy }; export type ZipMutator = (file: Archiver.Archiver) => void | Promise<void>; -app.get(`${RouteStore.imageHierarchyExport}/:docId`, async (req, res) => { - const id = req.params.docId; - const hierarchy: Hierarchy = {}; - await targetedVisitorRecursive(id, hierarchy); - BuildAndDispatchZip(res, async zip => { - await hierarchyTraverserRecursive(zip, hierarchy); - }); +addSecureRoute({ + method: Method.GET, + subscribers: new RouteSubscriber(RouteStore.imageHierarchyExport).add('docId'), + onValidation: async (_user, req, res) => { + const id = req.params.docId; + const hierarchy: Hierarchy = {}; + await targetedVisitorRecursive(id, hierarchy); + BuildAndDispatchZip(res, async zip => { + await hierarchyTraverserRecursive(zip, hierarchy); + }); + } }); const BuildAndDispatchZip = async (res: Response, mutator: ZipMutator): Promise<void> => { @@ -576,50 +624,49 @@ function LoadPage(file: string, pageNumber: number, res: Response) { }); } -// anyone attempting to navigate to localhost at this port will -// first have to login -addSecureRoute( - Method.GET, - (user, res) => res.redirect(RouteStore.home), - undefined, - RouteStore.root -); - -addSecureRoute( - Method.GET, - async (_, res) => { +/** + * Anyone attempting to navigate to localhost at this port will + * first have to log in. + */ +addSecureRoute({ + method: Method.GET, + subscribers: RouteStore.root, + onValidation: (_user, _req, res) => res.redirect(RouteStore.home) +}); + +addSecureRoute({ + method: Method.GET, + subscribers: RouteStore.getUsers, + onValidation: async (_user, _req, res) => { const cursor = await Database.Instance.query({}, { email: 1, userDocumentId: 1 }, "users"); const results = await cursor.toArray(); res.send(results.map(user => ({ email: user.email, userDocumentId: user.userDocumentId }))); }, - undefined, - RouteStore.getUsers -); +}); -addSecureRoute( - Method.GET, - (user, res, req) => { +addSecureRoute({ + method: Method.GET, + subscribers: [RouteStore.home, RouteStore.openDocumentWithId], + onValidation: (_user, req, res) => { let detector = new mobileDetect(req.headers['user-agent'] || ""); let filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html'; res.sendFile(path.join(__dirname, '../../deploy/' + filename)); }, - undefined, - RouteStore.home, RouteStore.openDocumentWithId -); - -addSecureRoute( - Method.GET, - (user, res) => res.send(user.userDocumentId), - (res) => res.send(undefined), - RouteStore.getUserDocumentId, -); - -addSecureRoute( - Method.GET, - (user, res) => { res.send(JSON.stringify({ id: user.id, email: user.email })); }, - (res) => res.send(JSON.stringify({ id: "__guest__", email: "" })), - RouteStore.getCurrUser -); +}); + +addSecureRoute({ + method: Method.GET, + subscribers: RouteStore.getUserDocumentId, + onValidation: (user, _req, res) => res.send(user.userDocumentId), + onRejection: (_req, res) => res.send(undefined) +}); + +addSecureRoute({ + method: Method.GET, + subscribers: RouteStore.getCurrUser, + onValidation: (user, _req, res) => { res.send(JSON.stringify(user)); }, + onRejection: (_req, res) => res.send(JSON.stringify({ id: "__guest__", email: "" })) +}); const ServicesApiKeyMap = new Map<string, string | undefined>([ ["face", process.env.FACE], @@ -627,10 +674,14 @@ const ServicesApiKeyMap = new Map<string, string | undefined>([ ["handwriting", process.env.HANDWRITING] ]); -addSecureRoute(Method.GET, (user, res, req) => { - let service = req.params.requestedservice; - res.send(ServicesApiKeyMap.get(service)); -}, undefined, `${RouteStore.cognitiveServices}/:requestedservice`); +addSecureRoute({ + method: Method.GET, + subscribers: new RouteSubscriber(RouteStore.cognitiveServices).add('requestedservice'), + onValidation: (_user, req, res) => { + let service = req.params.requestedservice; + res.send(ServicesApiKeyMap.get(service)); + } +}); class NodeCanvasFactory { create = (width: number, height: number) => { @@ -668,10 +719,10 @@ interface ImageFileResponse { exif: Opt<DashUploadUtils.EnrichedExifData>; } -// SETTERS -app.post( - RouteStore.upload, - (req, res) => { +addSecureRoute({ + method: Method.POST, + subscribers: RouteStore.upload, + onValidation: (_user, req, res) => { let form = new formidable.IncomingForm(); form.uploadDir = uploadDirectory; form.keepExtensions = true; @@ -704,20 +755,25 @@ app.post( _success(res, results); }); } -); +}); -app.post(RouteStore.inspectImage, async (req, res) => { - const { source } = req.body; - if (typeof source === "string") { - const uploadInformation = await DashUploadUtils.UploadImage(source); - return res.send(await DashUploadUtils.InspectImage(uploadInformation.mediaPaths[0])); +addSecureRoute({ + method: Method.POST, + subscribers: RouteStore.inspectImage, + onValidation: async (_user, req, res) => { + const { source } = req.body; + if (typeof source === "string") { + const uploadInformation = await DashUploadUtils.UploadImage(source); + return res.send(await DashUploadUtils.InspectImage(uploadInformation.mediaPaths[0])); + } + res.send({}); } - res.send({}); }); -addSecureRoute( - Method.POST, - (user, res, req) => { +addSecureRoute({ + method: Method.POST, + subscribers: RouteStore.dataUriToImage, + onValidation: (_user, req, res) => { const uri = req.body.uri; const filename = req.body.name; if (!uri || !filename) { @@ -750,10 +806,9 @@ addSecureRoute( } res.send("/files/" + filename + ext); }); - }, - undefined, - RouteStore.dataUriToImage -); + } +}); + // AUTHENTICATION // Sign Up @@ -792,29 +847,27 @@ app.use(RouteStore.corsProxy, (req, res) => { }).pipe(res); }); -addSecureRoute( - Method.GET, - (user, res, req) => { +addSecureRoute({ + method: Method.GET, + subscribers: RouteStore.delete, + onValidation: (_user, _req, res) => { if (release) { return _permission_denied(res, deletionPermissionError); } deleteFields().then(() => res.redirect(RouteStore.home)); - }, - undefined, - RouteStore.delete -); + } +}); -addSecureRoute( - Method.GET, - (_user, res, _req) => { +addSecureRoute({ + method: Method.GET, + subscribers: RouteStore.deleteAll, + onValidation: (_user, _req, res) => { if (release) { return _permission_denied(res, deletionPermissionError); } deleteAll().then(() => res.redirect(RouteStore.home)); - }, - undefined, - RouteStore.deleteAll -); + } +}); app.use(wdm(compiler, { publicPath: config.output.publicPath })); @@ -945,20 +998,28 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => { }); }); -app.get(RouteStore.readGoogleAccessToken, async (req, res) => { - const userId = req.header("userId")!; - const token = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId); - const information = { credentialsPath, userId }; - if (!token) { - return res.send(await GoogleApiServerUtils.GenerateAuthenticationUrl(information)); +addSecureRoute({ + method: Method.GET, + subscribers: RouteStore.readGoogleAccessToken, + onValidation: async (user, _req, res) => { + const userId = user.id; + const token = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId); + const information = { credentialsPath, userId }; + if (!token) { + return res.send(await GoogleApiServerUtils.GenerateAuthenticationUrl(information)); + } + GoogleApiServerUtils.RetrieveAccessToken(information).then(token => res.send(token)); } - GoogleApiServerUtils.RetrieveAccessToken(information).then(token => res.send(token)); }); -app.post(RouteStore.writeGoogleAccessToken, async (req, res) => { - const userId = req.header("userId")!; - const information = { credentialsPath, userId }; - res.send(await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode)); +addSecureRoute({ + method: Method.POST, + subscribers: RouteStore.writeGoogleAccessToken, + onValidation: async (user, req, res) => { + const userId = user.id; + const information = { credentialsPath, userId }; + res.send(await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode)); + } }); const tokenError = "Unable to successfully upload bytes for all images!"; @@ -972,47 +1033,50 @@ export interface NewMediaItem { }; } -app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { - const { media } = req.body; - const userId = req.header("userId"); - - if (!userId) { - return _error(res, userIdError); - } - - await GooglePhotosUploadUtils.initialize({ credentialsPath, userId }); - - let failed: number[] = []; +addSecureRoute({ + method: Method.POST, + subscribers: RouteStore.googlePhotosMediaUpload, + onValidation: async (user, req, res) => { + const { media } = req.body; + const userId = user.id; + if (!userId) { + return _error(res, userIdError); + } - const newMediaItems = await BatchedArray.from<GooglePhotosUploadUtils.MediaInput>(media, { batchSize: 25 }).batchedMapPatientInterval( - { magnitude: 100, unit: TimeUnit.Milliseconds }, - async (batch: GooglePhotosUploadUtils.MediaInput[]) => { - const newMediaItems: NewMediaItem[] = []; - for (let index = 0; index < batch.length; index++) { - const element = batch[index]; - const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url); - if (!uploadToken) { - failed.push(index); - } else { - newMediaItems.push({ - description: element.description, - simpleMediaItem: { uploadToken } - }); + await GooglePhotosUploadUtils.initialize({ credentialsPath, userId }); + + let failed: number[] = []; + + const newMediaItems = await BatchedArray.from<GooglePhotosUploadUtils.MediaInput>(media, { batchSize: 25 }).batchedMapPatientInterval( + { magnitude: 100, unit: TimeUnit.Milliseconds }, + async (batch: GooglePhotosUploadUtils.MediaInput[]) => { + const newMediaItems: NewMediaItem[] = []; + for (let index = 0; index < batch.length; index++) { + const element = batch[index]; + const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url); + if (!uploadToken) { + failed.push(index); + } else { + newMediaItems.push({ + description: element.description, + simpleMediaItem: { uploadToken } + }); + } } + return newMediaItems; } - return newMediaItems; + ); + + const failedCount = failed.length; + if (failedCount) { + console.error(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`); } - ); - const failedCount = failed.length; - if (failedCount) { - console.log(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`); + GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then( + result => _success(res, { results: result.newMediaItemResults, failed }), + error => _error(res, mediaError, error) + ); } - - GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then( - result => _success(res, { results: result.newMediaItemResults, failed }), - error => _error(res, mediaError, error) - ); }); interface MediaItem { |