diff options
17 files changed, 1412 insertions, 185 deletions
diff --git a/package.json b/package.json index 713c5d585..804cdd262 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,8 @@ "dependencies": { "@fortawesome/fontawesome-free-solid": "^5.0.13", "@fortawesome/fontawesome-svg-core": "^1.2.15", + "@fortawesome/free-brands-svg-icons": "^5.9.0", + "@fortawesome/free-regular-svg-icons": "^5.9.0", "@fortawesome/free-solid-svg-icons": "^5.7.2", "@fortawesome/react-fontawesome": "^0.1.4", "@hig/flyout": "^1.0.3", diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 862395d74..16b88b027 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,4 +1,4 @@ -import { computed, observable } from 'mobx'; +import { computed, observable, action } from 'mobx'; import { DocumentView } from '../views/nodes/DocumentView'; import { Doc, DocListCast, Opt } from '../../new_fields/Doc'; import { FieldValue, Cast, NumCast, BoolCast } from '../../new_fields/Types'; @@ -115,7 +115,7 @@ export class DocumentManager { } @undoBatch - public jumpToDocument = async (docDelegate: Doc, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number, docContext?: Doc): Promise<void> => { + public jumpToDocument = async (docDelegate: Doc, willZoom: boolean, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number, docContext?: Doc): Promise<void> => { let doc = Doc.GetProto(docDelegate); const contextDoc = await Cast(doc.annotationOn, Doc); if (contextDoc) { @@ -129,18 +129,18 @@ export class DocumentManager { if (!forceDockFunc && (docView = DocumentManager.Instance.getDocumentView(doc))) { docView.props.Document.libraryBrush = true; if (linkPage !== undefined) docView.props.Document.curPage = linkPage; - docView.props.focus(docView.props.Document); + docView.props.focus(docView.props.Document, willZoom); } else { if (!contextDoc) { if (docContext) { let targetContextView: DocumentView | null; if (!forceDockFunc && docContext && (targetContextView = DocumentManager.Instance.getDocumentView(docContext))) { docContext.panTransformType = "Ease"; - targetContextView.props.focus(docDelegate); + targetContextView.props.focus(docDelegate, willZoom); } else { (dockFunc || CollectionDockingView.Instance.AddRightSplit)(docContext); setTimeout(() => { - this.jumpToDocument(docDelegate, forceDockFunc, dockFunc, linkPage); + this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage); }, 10); } } else { @@ -154,14 +154,37 @@ export class DocumentManager { docDelegate.libraryBrush = true; if (!forceDockFunc && (contextView = DocumentManager.Instance.getDocumentView(contextDoc))) { contextDoc.panTransformType = "Ease"; - contextView.props.focus(docDelegate); + contextView.props.focus(docDelegate, willZoom); } else { (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc); setTimeout(() => { - this.jumpToDocument(docDelegate, forceDockFunc, dockFunc, linkPage); + this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage); }, 10); } } } } + + @action + zoomIntoScale = (docDelegate: Doc, scale: number) => { + let doc = Doc.GetProto(docDelegate); + + let docView: DocumentView | null; + docView = DocumentManager.Instance.getDocumentView(doc); + if (docView) { + docView.props.zoomToScale(scale); + } + } + + getScaleOfDocView = (docDelegate: Doc) => { + let doc = Doc.GetProto(docDelegate); + + let docView: DocumentView | null; + docView = DocumentManager.Instance.getDocumentView(doc); + if (docView) { + return docView.props.getScale(); + } else { + return 1; + } + } }
\ No newline at end of file diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index f2559db74..cbabadd64 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -207,7 +207,7 @@ export class TooltipTextMenu { DocServer.GetRefField(docid).then(action((f: Opt<Field>) => { if (f instanceof Doc) { if (DocumentManager.Instance.getDocumentView(f)) { - DocumentManager.Instance.getDocumentView(f)!.props.focus(f); + DocumentManager.Instance.getDocumentView(f)!.props.focus(f, false); } else if (CollectionDockingView.Instance) CollectionDockingView.Instance.AddRightSplit(f); } diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 51630c29b..10feaa79c 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -15,7 +15,7 @@ import { Docs } from '../documents/Documents'; import { SetupDrag, DragManager } from '../util/DragManager'; import { Transform } from '../util/Transform'; import { UndoManager } from '../util/UndoManager'; -import { PresentationView } from './PresentationView'; +import { PresentationView } from './presentationview/PresentationView'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { ContextMenu } from './ContextMenu'; import { DocumentDecorations } from './DocumentDecorations'; @@ -33,10 +33,10 @@ import { listSpec } from '../../new_fields/Schema'; import { Id } from '../../new_fields/FieldSymbols'; import { HistoryUtil } from '../util/History'; import { CollectionBaseView } from './collections/CollectionBaseView'; +import { List } from '../../new_fields/List'; import PDFMenu from './pdf/PDFMenu'; import { InkTool } from '../../new_fields/InkField'; - @observer export class MainView extends React.Component { public static Instance: MainView; @@ -49,7 +49,7 @@ export class MainView extends React.Component { private set mainContainer(doc: Opt<Doc>) { if (doc) { if (!("presentationView" in doc)) { - doc.presentationView = Docs.TreeDocument([], { title: "Presentation" }); + doc.presentationView = new List<Doc>([Docs.TreeDocument([], { title: "Presentation" })]); } CurrentUserUtils.UserDocument.activeWorkspace = doc; } @@ -216,13 +216,16 @@ export class MainView extends React.Component { parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} - ContainingCollectionView={undefined} />; - const pres = mainCont ? FieldValue(Cast(mainCont.presentationView, Doc)) : undefined; + ContainingCollectionView={undefined} + zoomToScale={emptyFunction} + getScale={returnOne} + />; + let castRes = mainCont ? FieldValue(Cast(mainCont.presentationView, listSpec(Doc))) : undefined; return <Measure offset onResize={this.onResize}> {({ measureRef }) => <div ref={measureRef} id="mainContent-div" onDrop={this.onDrop}> {content} - {pres ? <PresentationView Document={pres} key="presentation" /> : null} + {castRes ? <PresentationView Documents={castRes} key="presentation" /> : null} </div> } </Measure>; diff --git a/src/client/views/PresentationView.tsx b/src/client/views/PresentationView.tsx deleted file mode 100644 index 1dacbb663..000000000 --- a/src/client/views/PresentationView.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import { action, observable } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, DocListCast } from "../../new_fields/Doc"; -import { Id } from "../../new_fields/FieldSymbols"; -import { List } from "../../new_fields/List"; -import { listSpec } from "../../new_fields/Schema"; -import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../new_fields/Types"; -import { DocumentManager } from "../util/DocumentManager"; -import "./PresentationView.scss"; -import React = require("react"); - -export interface PresViewProps { - Document: Doc; -} - -interface PresListProps extends PresViewProps { - deleteDocument(index: number): void; - gotoDocument(index: number): void; -} - -@observer -/** - * Component that takes in a document prop and a boolean whether it's collapsed or not. - */ -class PresentationViewList extends React.Component<PresListProps> { - - - /** - * Renders a single child document. It will just append a list element. - * @param document The document to render. - */ - renderChild = (document: Doc, index: number) => { - let title = document.title; - - //to get currently selected presentation doc - let selected = NumCast(this.props.Document.selectedDoc, 0); - - let className = "presentationView-item"; - if (selected === index) { - //this doc is selected - className += " presentationView-selected"; - } - let onEnter = (e: React.PointerEvent) => { document.libraryBrush = true; }; - let onLeave = (e: React.PointerEvent) => { document.libraryBrush = false; }; - return ( - <div className={className} key={document[Id] + index} - onPointerEnter={onEnter} onPointerLeave={onLeave} - style={{ - outlineColor: "maroon", - outlineStyle: "dashed", - outlineWidth: BoolCast(document.libraryBrush, false) || BoolCast(document.protoBrush, false) ? `1px` : "0px", - }} - onClick={e => { this.props.gotoDocument(index); e.stopPropagation(); }}> - <strong className="presentationView-name"> - {`${index + 1}. ${title}`} - </strong> - <button className="presentation-icon" onClick={e => { this.props.deleteDocument(index); e.stopPropagation(); }}>X</button> - </div> - ); - - } - - render() { - const children = DocListCast(this.props.Document.data); - - return ( - <div className="presentationView-listCont"> - {children.map(this.renderChild)} - </div> - ); - } -} - - -@observer -export class PresentationView extends React.Component<PresViewProps> { - public static Instance: PresentationView; - - //observable means render is re-called every time variable is changed - @observable - collapsed: boolean = false; - closePresentation = action(() => this.props.Document.width = 0); - next = () => { - const current = NumCast(this.props.Document.selectedDoc); - this.gotoDocument(current + 1); - - } - back = () => { - const current = NumCast(this.props.Document.selectedDoc); - this.gotoDocument(current - 1); - } - - @action - public RemoveDoc = (index: number) => { - const value = FieldValue(Cast(this.props.Document.data, listSpec(Doc))); - if (value) { - value.splice(index, 1); - } - } - - public gotoDocument = async (index: number) => { - const list = FieldValue(Cast(this.props.Document.data, listSpec(Doc))); - if (!list) { - return; - } - if (index < 0 || index >= list.length) { - return; - } - - this.props.Document.selectedDoc = index; - const doc = await list[index]; - DocumentManager.Instance.jumpToDocument(doc); - } - - //initilize class variables - constructor(props: PresViewProps) { - super(props); - PresentationView.Instance = this; - } - - /** - * Adds a document to the presentation view - **/ - @action - public PinDoc(doc: Doc) { - //add this new doc to props.Document - const data = Cast(this.props.Document.data, listSpec(Doc)); - if (data) { - data.push(doc); - } else { - this.props.Document.data = new List([doc]); - } - - this.props.Document.width = 300; - } - - render() { - let titleStr = StrCast(this.props.Document.title); - let width = NumCast(this.props.Document.width); - - //TODO: next and back should be icons - return ( - <div className="presentationView-cont" style={{ width: width, overflow: "hidden" }}> - <div className="presentationView-heading"> - <div className="presentationView-title">{titleStr}</div> - <button className='presentation-icon' onClick={this.closePresentation}>X</button> - </div> - <div className="presentation-buttons"> - <button className="presentation-button" onClick={this.back}>back</button> - <button className="presentation-button" onClick={this.next}>next</button> - </div> - <PresentationViewList Document={this.props.Document} deleteDocument={this.RemoveDoc} gotoDocument={this.gotoDocument} /> - </div> - ); - } -}
\ No newline at end of file diff --git a/src/client/views/SearchItem.tsx b/src/client/views/SearchItem.tsx index 6901f60da..13e4b88f7 100644 --- a/src/client/views/SearchItem.tsx +++ b/src/client/views/SearchItem.tsx @@ -20,7 +20,7 @@ library.add(faFilm); export class SearchItem extends React.Component<SearchProps> { onClick = () => { - DocumentManager.Instance.jumpToDocument(this.props.doc); + DocumentManager.Instance.jumpToDocument(this.props.doc, false); } //needs help diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 1e42593d1..ae77ef42d 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -187,4 +187,4 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { ); } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index f1473139c..dd99649b8 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -10,7 +10,7 @@ import { Id } from '../../../new_fields/FieldSymbols'; import { FieldId } from "../../../new_fields/RefField"; import { listSpec } from "../../../new_fields/Schema"; import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; -import { emptyFunction, returnTrue, Utils } from "../../../Utils"; +import { emptyFunction, returnTrue, Utils, returnOne } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { DocumentManager } from '../../util/DocumentManager'; import { DragLinksAsDocuments, DragManager } from "../../util/DragManager"; @@ -529,7 +529,9 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { whenActiveChanged={emptyFunction} focus={emptyFunction} addDocTab={this.addDocTab} - ContainingCollectionView={undefined} /> + ContainingCollectionView={undefined} + zoomToScale={emptyFunction} + getScale={returnOne} /> </div >); } diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 9cc8961e3..97781f722 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -6,12 +6,12 @@ import { action, computed, observable, trace, untracked } from "mobx"; import { observer } from "mobx-react"; import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table"; import "react-table/react-table.css"; +import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils"; import { Doc, DocListCast, DocListCastAsync, Field } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; -import { emptyFunction, returnFalse, returnZero } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { Gateway } from "../../northstar/manager/Gateway"; import { SetupDrag } from "../../util/DragManager"; @@ -434,6 +434,8 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre whenActiveChanged={this.props.whenActiveChanged} bringToFront={emptyFunction} addDocTab={this.props.addDocTab} + zoomToScale={emptyFunction} + getScale={returnOne} /> </div>)} {input} diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index c855cb43a..597e4795c 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -99,7 +99,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { let dref = React.createRef<HTMLDivElement>(); let dxf = () => this.getDocTransform(d, dref.current!).scale(this.columnWidth / d[WidthSym]()); let width = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth; - let height = () => aspect ? width() / aspect : d[HeightSym]() + let height = () => aspect ? width() / aspect : d[HeightSym](); let rowSpan = Math.ceil((height() + this.gridGap) / (this._gridSize + this.gridGap)); return (<div className="collectionStackingView-masonryDoc" key={d[Id]} diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index eaa3add40..18654905f 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -216,7 +216,7 @@ class TreeView extends React.Component<TreeViewProps> { ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, "inTab"), icon: "folder" }); ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, "onRight"), icon: "caret-square-right" }); if (DocumentManager.Instance.getDocumentViews(this.props.document).length) { - ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document)) }); + ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document, false)) }); } ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)) }); } else { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 4b4e7465a..663fa5f9d 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -262,7 +262,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { doc.zIndex = docs.length + 1; } - focusDocument = (doc: Doc) => { + focusDocument = (doc: Doc, willZoom: boolean) => { const panX = this.Document.panX; const panY = this.Document.panY; const id = this.Document[Id]; @@ -289,8 +289,40 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { newState.initializers[id] = { panX: newPanX, panY: newPanY }; HistoryUtil.pushState(newState); this.setPan(newPanX, newPanY); + this.props.Document.panTransformType = "Ease"; this.props.focus(this.props.Document); + if (willZoom) { + this.setScaleToZoom(doc); + } + + } + + setScaleToZoom = (doc: Doc) => { + let p = this.props; + let PanelHeight = p.PanelHeight(); + let panelWidth = p.PanelWidth(); + + let docHeight = NumCast(doc.height); + let docWidth = NumCast(doc.width); + let targetHeight = 0.5 * PanelHeight; + let targetWidth = 0.5 * panelWidth; + + let maxScaleX: number = targetWidth / docWidth; + let maxScaleY: number = targetHeight / docHeight; + let maxApplicableScale = Math.min(maxScaleX, maxScaleY); + this.Document.scale = maxApplicableScale; + } + + zoomToScale = (scale: number) => { + this.Document.scale = scale; + } + + getScale = () => { + if (this.Document.scale) { + return this.Document.scale; + } + return 1; } @@ -312,6 +344,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { whenActiveChanged: this.props.whenActiveChanged, bringToFront: this.bringToFront, addDocTab: this.props.addDocTab, + zoomToScale: this.zoomToScale, + getScale: this.getScale }; } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 522c37989..10d777865 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -23,7 +23,7 @@ import { CollectionVideoView } from "../collections/CollectionVideoView"; import { CollectionView } from "../collections/CollectionView"; import { ContextMenu } from "../ContextMenu"; import { DocComponent } from "../DocComponent"; -import { PresentationView } from "../PresentationView"; +import { PresentationView } from "../presentationview/PresentationView"; import { Template } from "./../Templates"; import { DocumentContentsView } from "./DocumentContentsView"; import * as rp from "request-promise"; @@ -75,12 +75,15 @@ export interface DocumentViewProps { ContentScaling: () => number; PanelWidth: () => number; PanelHeight: () => number; - focus: (doc: Doc) => void; + focus: (doc: Doc, willZoom: boolean) => void; selectOnLoad: boolean; parentActive: () => boolean; whenActiveChanged: (isActive: boolean) => void; bringToFront: (doc: Doc) => void; addDocTab: (doc: Doc, where: string) => void; + collapseToPoint?: (scrpt: number[], expandedDocs: Doc[] | undefined) => void; + zoomToScale: (scale: number) => void; + getScale: () => number; animateBetweenIcon?: (iconPos: number[], startTime: number, maximizing: boolean) => void; } @@ -89,6 +92,7 @@ const schema = createSchema({ nativeWidth: "number", nativeHeight: "number", backgroundColor: "string", + opacity: "number", hidden: "boolean" }); @@ -333,7 +337,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (!linkedFwdDocs.some(l => l instanceof Promise)) { let maxLocation = StrCast(linkedFwdDocs[altKey ? 1 : 0].maximizeLocation, "inTab"); let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined; - DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, document => this.props.addDocTab(document, maxLocation), linkedFwdPage[altKey ? 1 : 0], targetContext); + DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, false, document => this.props.addDocTab(document, maxLocation), linkedFwdPage[altKey ? 1 : 0], targetContext); } } } @@ -488,7 +492,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.props.addDocTab && this.props.addDocTab(Docs.SchemaDocument(["title"], aliases, {}), "onRight"); }, icon: "search" }); - cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document), icon: "crosshairs" }); + cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" }); cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])), icon: "link" }); cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" }); cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" }); @@ -559,7 +563,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu background: this.Document.backgroundColor || "", width: nativeWidth, height: nativeHeight, - transform: `scale(${scaling}, ${scaling})` + transform: `scale(${scaling}, ${scaling})`, + opacity: NumCast(this.props.Document.opacity, 1) }} onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx new file mode 100644 index 000000000..07cdcd43a --- /dev/null +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -0,0 +1,405 @@ +import { observer } from "mobx-react"; +import React = require("react"); +import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; +import { NumCast, BoolCast, StrCast, Cast } from "../../../new_fields/Types"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { observable, action, computed, runInAction } from "mobx"; +import "./PresentationView.scss"; +import { Utils } from "../../../Utils"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faFile as fileSolid, faLocationArrow, faArrowUp, faSearch } from '@fortawesome/free-solid-svg-icons'; +import { faFile as fileRegular } from '@fortawesome/free-regular-svg-icons'; +import { List } from "../../../new_fields/List"; +import { listSpec } from "../../../new_fields/Schema"; + + + + +library.add(faArrowUp); +library.add(fileSolid); +library.add(fileRegular); +library.add(faLocationArrow); +library.add(faSearch); + +interface PresentationElementProps { + mainDocument: Doc; + document: Doc; + index: number; + deleteDocument(index: number): void; + gotoDocument(index: number, fromDoc: number): Promise<void>; + allListElements: Doc[]; + groupMappings: Map<String, Doc[]>; + presStatus: boolean; + presButtonBackUp: Doc; + presGroupBackUp: Doc; + + +} + +//enum for the all kinds of buttons a doc in presentation can have +export enum buttonIndex { + Show = 0, + Navigate = 1, + HideTillPressed = 2, + FadeAfter = 3, + HideAfter = 4, + Group = 5, + +} + +/** + * This class models the view a document added to presentation will have in the presentation. + * It involves some functionality for its buttons and options. + */ +@observer +export default class PresentationElement extends React.Component<PresentationElementProps> { + + @observable private selectedButtons: boolean[]; + + + constructor(props: PresentationElementProps) { + super(props); + this.selectedButtons = new Array(6); + } + + /** + * Getter to get the status of the buttons. + */ + @computed + get selected() { + return this.selectedButtons; + } + + //Lifecycle function that makes sure that button BackUp is received when mounted. + async componentDidMount() { + this.receiveButtonBackUp(); + + } + + //Lifecycle function that makes sure button BackUp is received when not re-mounted bu re-rendered. + async componentDidUpdate() { + this.receiveButtonBackUp(); + } + + receiveButtonBackUp = async () => { + + //get the list that stores docs that keep track of buttons + let castedList = Cast(this.props.presButtonBackUp.selectedButtonDocs, listSpec(Doc)); + if (!castedList) { + this.props.presButtonBackUp.selectedButtonDocs = castedList = new List<Doc>(); + } + //if this is the first time this doc mounts, push a doc for it to store + if (castedList.length <= this.props.index) { + let newDoc = new Doc(); + let defaultBooleanArray: boolean[] = new Array(6); + newDoc.selectedButtons = new List(defaultBooleanArray); + castedList.push(newDoc); + //otherwise update the selected buttons depending on storage. + } else { + let curDoc: Doc = await castedList[this.props.index]; + let selectedButtonOfDoc = Cast(curDoc.selectedButtons, listSpec("boolean"), null); + if (selectedButtonOfDoc !== undefined) { + runInAction(() => this.selectedButtons = selectedButtonOfDoc); + } + } + + } + + /** + * The function that is called to group docs together. It tries to group a doc + * that turned grouping option with the above document. If that doc is grouped with + * other documents. Those other documents will be grouped with doc's above document as well. + */ + @action + onGroupClick = (document: Doc, index: number, buttonStatus: boolean) => { + let p = this.props; + if (index >= 1) { + //checking if options was turned true + if (buttonStatus) { + //getting the id of the above-doc and the doc + let aboveGuid = StrCast(p.allListElements[index - 1].presentId, null); + let docGuid = StrCast(document.presentId, null); + //the case where above-doc is already in group + if (p.groupMappings.has(aboveGuid)) { + let aboveArray = p.groupMappings.get(aboveGuid)!; + //case where doc is already in group + if (p.groupMappings.has(docGuid)) { + let docsArray = p.groupMappings.get(docGuid)!; + docsArray.forEach((doc: Doc) => { + if (!aboveArray.includes(doc)) { + aboveArray.push(doc); + } + doc.presentId = aboveGuid; + }); + p.groupMappings.delete(docGuid); + //the case where doc was not in group + } else { + if (!aboveArray.includes(document)) { + aboveArray.push(document); + + } + + } + //the case where above-doc was not in group + } else { + let newAboveArray: Doc[] = []; + newAboveArray.push(p.allListElements[index - 1]); + + //the case where doc is in group + if (p.groupMappings.has(docGuid)) { + let docsArray = p.groupMappings.get(docGuid)!; + docsArray.forEach((doc: Doc) => { + newAboveArray.push(doc); + doc.presentId = aboveGuid; + }); + p.groupMappings.delete(docGuid); + + //the case where doc is not in a group + } else { + newAboveArray.push(document); + + } + p.groupMappings.set(aboveGuid, newAboveArray); + + } + document.presentId = aboveGuid; + + //when grouping is turned off + } else { + let curArray = p.groupMappings.get(StrCast(document.presentId, Utils.GenerateGuid()))!; + let targetIndex = curArray.indexOf(document); + let firstPart = curArray.slice(0, targetIndex); + let firstPartNewGuid = Utils.GenerateGuid(); + firstPart.forEach((doc: Doc) => doc.presentId = firstPartNewGuid); + let secondPart = curArray.slice(targetIndex); + p.groupMappings.set(StrCast(p.allListElements[index - 1].presentId, Utils.GenerateGuid()), firstPart); + p.groupMappings.set(StrCast(document.presentId, Utils.GenerateGuid()), secondPart); + + + } + + } + this.autoSaveGroupChanges(); + + } + + + /** + * This function is called at the end of each group update to update the group updates. + */ + @action + autoSaveGroupChanges = () => { + let castedList: List<Doc> = new List<Doc>(); + this.props.presGroupBackUp.groupDocs = castedList; + this.props.groupMappings.forEach((docArray: Doc[], id: String) => { + //create a new doc for each group + let newGroupDoc = new Doc(); + castedList.push(newGroupDoc); + //store the id of the group in the doc + newGroupDoc.presentIdStore = id.toString(); + //store the doc array which represents the group in the doc + newGroupDoc.grouping = new List(docArray); + }); + + } + + /** + * Function that is called on click to change the group status of a docus, by turning the option on/off. + */ + @action + changeGroupStatus = () => { + if (this.selectedButtons[buttonIndex.Group]) { + this.selectedButtons[buttonIndex.Group] = false; + } else { + this.selectedButtons[buttonIndex.Group] = true; + } + this.autoSaveButtonChange(buttonIndex.Group); + + } + + /** + * The function that is called on click to turn Hiding document till press option on/off. + * It also sets the beginning and end opacitys. + */ + @action + onHideDocumentUntilPressClick = (e: React.MouseEvent) => { + e.stopPropagation(); + const current = NumCast(this.props.mainDocument.selectedDoc); + if (this.selectedButtons[buttonIndex.HideTillPressed]) { + this.selectedButtons[buttonIndex.HideTillPressed] = false; + if (this.props.index >= current) { + this.props.document.opacity = 1; + } + } else { + this.selectedButtons[buttonIndex.HideTillPressed] = true; + if (this.props.presStatus) { + if (this.props.index > current) { + this.props.document.opacity = 0; + } + } + } + this.autoSaveButtonChange(buttonIndex.HideTillPressed); + } + + /** + * This function is called to get the updates for the changed buttons. + */ + @action + autoSaveButtonChange = async (index: buttonIndex) => { + let castedList = (await DocListCastAsync(this.props.presButtonBackUp.selectedButtonDocs))!; + castedList[this.props.index].selectedButtons = new List(this.selectedButtons); + + } + + /** + * The function that is called on click to turn Hiding document after presented option on/off. + * It also makes sure that the option swithches from fade-after to this one, since both + * can't coexist. + */ + @action + onHideDocumentAfterPresentedClick = (e: React.MouseEvent) => { + e.stopPropagation(); + const current = NumCast(this.props.mainDocument.selectedDoc); + if (this.selectedButtons[buttonIndex.HideAfter]) { + this.selectedButtons[buttonIndex.HideAfter] = false; + if (this.props.index <= current) { + this.props.document.opacity = 1; + } + } else { + if (this.selectedButtons[buttonIndex.FadeAfter]) { + this.selectedButtons[buttonIndex.FadeAfter] = false; + } + this.selectedButtons[buttonIndex.HideAfter] = true; + if (this.props.presStatus) { + if (this.props.index < current) { + this.props.document.opacity = 0; + } + } + } + this.autoSaveButtonChange(buttonIndex.HideAfter); + + } + + /** + * The function that is called on click to turn fading document after presented option on/off. + * It also makes sure that the option swithches from hide-after to this one, since both + * can't coexist. + */ + @action + onFadeDocumentAfterPresentedClick = (e: React.MouseEvent) => { + e.stopPropagation(); + const current = NumCast(this.props.mainDocument.selectedDoc); + if (this.selectedButtons[buttonIndex.FadeAfter]) { + this.selectedButtons[buttonIndex.FadeAfter] = false; + if (this.props.index <= current) { + this.props.document.opacity = 1; + } + } else { + if (this.selectedButtons[buttonIndex.HideAfter]) { + this.selectedButtons[buttonIndex.HideAfter] = false; + } + this.selectedButtons[buttonIndex.FadeAfter] = true; + if (this.props.presStatus) { + if (this.props.index < current) { + this.props.document.opacity = 0.5; + } + } + } + this.autoSaveButtonChange(buttonIndex.FadeAfter); + + } + + /** + * The function that is called on click to turn navigation option of docs on/off. + */ + @action + onNavigateDocumentClick = (e: React.MouseEvent) => { + e.stopPropagation(); + if (this.selectedButtons[buttonIndex.Navigate]) { + this.selectedButtons[buttonIndex.Navigate] = false; + + } else { + if (this.selectedButtons[buttonIndex.Show]) { + this.selectedButtons[buttonIndex.Show] = false; + } + this.selectedButtons[buttonIndex.Navigate] = true; + const current = NumCast(this.props.mainDocument.selectedDoc); + if (current === this.props.index) { + this.props.gotoDocument(this.props.index, this.props.index); + } + } + + this.autoSaveButtonChange(buttonIndex.Navigate); + + } + + /** + * The function that is called on click to turn zoom option of docs on/off. + */ + @action + onZoomDocumentClick = (e: React.MouseEvent) => { + e.stopPropagation(); + if (this.selectedButtons[buttonIndex.Show]) { + this.selectedButtons[buttonIndex.Show] = false; + this.props.document.viewScale = 1; + + } else { + if (this.selectedButtons[buttonIndex.Navigate]) { + this.selectedButtons[buttonIndex.Navigate] = false; + } + this.selectedButtons[buttonIndex.Show] = true; + const current = NumCast(this.props.mainDocument.selectedDoc); + if (current === this.props.index) { + this.props.gotoDocument(this.props.index, this.props.index); + } + } + + this.autoSaveButtonChange(buttonIndex.Show); + + } + + + render() { + let p = this.props; + let title = p.document.title; + + //to get currently selected presentation doc + let selected = NumCast(p.mainDocument.selectedDoc, 0); + + let className = "presentationView-item"; + if (selected === p.index) { + //this doc is selected + className += " presentationView-selected"; + } + let onEnter = (e: React.PointerEvent) => { p.document.libraryBrush = true; }; + let onLeave = (e: React.PointerEvent) => { p.document.libraryBrush = false; }; + return ( + <div className={className} key={p.document[Id] + p.index} + onPointerEnter={onEnter} onPointerLeave={onLeave} + style={{ + outlineColor: "maroon", + outlineStyle: "dashed", + outlineWidth: BoolCast(p.document.libraryBrush, false) || BoolCast(p.document.protoBrush, false) ? `1px` : "0px", + }} + onClick={e => { p.gotoDocument(p.index, NumCast(this.props.mainDocument.selectedDoc)); e.stopPropagation(); }}> + <strong className="presentationView-name"> + {`${p.index + 1}. ${title}`} + </strong> + <button className="presentation-icon" onClick={e => { this.props.deleteDocument(p.index); e.stopPropagation(); }}>X</button> + <br></br> + <button title="Zoom" className={this.selectedButtons[buttonIndex.Show] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button> + <button title="Navigate" className={this.selectedButtons[buttonIndex.Navigate] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button> + <button title="Hide Document Till Presented" className={this.selectedButtons[buttonIndex.HideTillPressed] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button> + <button title="Fade Document After Presented" className={this.selectedButtons[buttonIndex.FadeAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={fileRegular} color={"gray"} /></button> + <button title="Hide Document After Presented" className={this.selectedButtons[buttonIndex.HideAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={fileRegular} /></button> + <button title="Group With Up" className={this.selectedButtons[buttonIndex.Group] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={(e) => { + e.stopPropagation(); + this.changeGroupStatus(); + this.onGroupClick(p.document, p.index, this.selectedButtons[buttonIndex.Group]); + }}> <FontAwesomeIcon icon={"arrow-up"} /> </button> + + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/presentationview/PresentationList.tsx b/src/client/views/presentationview/PresentationList.tsx new file mode 100644 index 000000000..7abd3e366 --- /dev/null +++ b/src/client/views/presentationview/PresentationList.tsx @@ -0,0 +1,104 @@ +import { observer } from "mobx-react"; +import React = require("react"); +import { action } from "mobx"; +import "./PresentationView.scss"; +import { Utils } from "../../../Utils"; +import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; +import { NumCast, StrCast } from "../../../new_fields/Types"; +import { Id } from "../../../new_fields/FieldSymbols"; +import PresentationElement, { buttonIndex } from "./PresentationElement"; + + + + +interface PresListProps { + mainDocument: Doc; + deleteDocument(index: number): void; + gotoDocument(index: number, fromDoc: number): Promise<void>; + groupMappings: Map<String, Doc[]>; + presElementsMappings: Map<Doc, PresentationElement>; + setChildrenDocs: (docList: Doc[]) => void; + presStatus: boolean; + presButtonBackUp: Doc; + presGroupBackUp: Doc; +} + + +@observer +/** + * Component that takes in a document prop and a boolean whether it's collapsed or not. + */ +export default class PresentationViewList extends React.Component<PresListProps> { + + /** + * Method that initializes presentation ids for the + * docs that is in the presentation, when presentation list + * gets re-rendered. It makes sure to not assign ids to the + * docs that are in the group, so that mapping won't be disrupted. + */ + + @action + initializeGroupIds = async (docList: Doc[]) => { + docList.forEach(async (doc: Doc, index: number) => { + let docGuid = StrCast(doc.presentId, null); + //checking if part of group + let storedGuids: string[] = []; + let castedGroupDocs = await DocListCastAsync(this.props.presGroupBackUp.groupDocs); + //making sure the docs that were in groups, which were stored, to not get new guids. + if (castedGroupDocs !== undefined) { + castedGroupDocs.forEach((doc: Doc) => { + let storedGuid = StrCast(doc.presentIdStore, null); + if (storedGuid) { + storedGuids.push(storedGuid); + } + + }); + } + if (!this.props.groupMappings.has(docGuid) && !storedGuids.includes(docGuid)) { + doc.presentId = Utils.GenerateGuid(); + } + }); + } + + /** + * Initially every document starts with a viewScale 1, which means + * that they will be displayed in a canvas with scale 1. + */ + @action + initializeScaleViews = (docList: Doc[]) => { + docList.forEach((doc: Doc) => { + let curScale = NumCast(doc.viewScale, null); + if (curScale === undefined) { + doc.viewScale = 1; + } + }); + } + + render() { + const children = DocListCast(this.props.mainDocument.data); + this.initializeGroupIds(children); + this.initializeScaleViews(children); + this.props.setChildrenDocs(children); + return ( + + <div className="presentationView-listCont"> + {children.map((doc: Doc, index: number) => + <PresentationElement + ref={(e) => { if (e) { this.props.presElementsMappings.set(doc, e); } }} + key={doc[Id]} + mainDocument={this.props.mainDocument} + document={doc} + index={index} + deleteDocument={this.props.deleteDocument} + gotoDocument={this.props.gotoDocument} + groupMappings={this.props.groupMappings} + allListElements={children} + presStatus={this.props.presStatus} + presButtonBackUp={this.props.presButtonBackUp} + presGroupBackUp={this.props.presGroupBackUp} + /> + )} + </div> + ); + } +} diff --git a/src/client/views/PresentationView.scss b/src/client/views/presentationview/PresentationView.scss index fb4a851c4..a35a5849b 100644 --- a/src/client/views/PresentationView.scss +++ b/src/client/views/presentationview/PresentationView.scss @@ -47,20 +47,30 @@ padding-bottom: 3px; font-size: 25px; display: inline-block; + width: calc(100% - 160px); } .presentation-icon { float: right; } +.presentation-interaction { + float: left; +} + +.presentation-interaction-selected { + background: #505050; + float: left; +} + .presentationView-name { font-size: 15px; display: inline-block; } .presentation-button { - margin-right: 12.5%; - margin-left: 12.5%; + margin-right: 2.5%; + margin-left: 2.5%; width: 25%; } diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx new file mode 100644 index 000000000..20d0e113a --- /dev/null +++ b/src/client/views/presentationview/PresentationView.tsx @@ -0,0 +1,793 @@ +import { observer } from "mobx-react"; +import React = require("react"); +import { observable, action, runInAction, reaction } from "mobx"; +import "./PresentationView.scss"; +import { DocumentManager } from "../../util/DocumentManager"; +import { Utils } from "../../../Utils"; +import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; +import { listSpec } from "../../../new_fields/Schema"; +import { Cast, NumCast, FieldValue, PromiseValue, StrCast, BoolCast } from "../../../new_fields/Types"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { List } from "../../../new_fields/List"; +import PresentationElement, { buttonIndex } from "./PresentationElement"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faArrowRight, faArrowLeft, faPlay, faStop, faPlus, faTimes, faMinus, faEdit } from '@fortawesome/free-solid-svg-icons'; +import { Docs } from "../../documents/Documents"; +import { undoBatch, UndoManager } from "../../util/UndoManager"; +import PresentationViewList from "./PresentationList"; + +library.add(faArrowLeft); +library.add(faArrowRight); +library.add(faPlay); +library.add(faStop); +library.add(faPlus); +library.add(faTimes); +library.add(faMinus); +library.add(faEdit); + + +export interface PresViewProps { + Documents: List<Doc>; +} + +@observer +export class PresentationView extends React.Component<PresViewProps> { + public static Instance: PresentationView; + + //Mapping from presentation ids to a list of doc that represent a group + @observable groupMappings: Map<String, Doc[]> = new Map(); + //mapping from docs to their rendered component + @observable presElementsMappings: Map<Doc, PresentationElement> = new Map(); + //variable that holds all the docs in the presentation + @observable childrenDocs: Doc[] = []; + //variable to hold if presentation is started + @observable presStatus: boolean = false; + //back-up so that presentation stays the way it's when refreshed + @observable presGroupBackUp: Doc = new Doc(); + @observable presButtonBackUp: Doc = new Doc(); + + //Keeping track of the doc for the current presentation + @observable curPresentation: Doc = new Doc(); + //Mapping of guids to presentations. + @observable presentationsMapping: Map<String, Doc> = new Map(); + //Mapping of presentations to guid, so that select option values can be given. + @observable presentationsKeyMapping: Map<Doc, String> = new Map(); + //Variable to keep track of guid of the current presentation + @observable currentSelectedPresValue: string | undefined; + //A flag to keep track if title input is open, which is used in rendering. + @observable PresTitleInputOpen: boolean = false; + //Variable that holds reference to title input, so that new presentations get titles assigned. + @observable titleInputElement: HTMLInputElement | undefined; + @observable PresTitleChangeOpen: boolean = false; + + //initilize class variables + constructor(props: PresViewProps) { + super(props); + PresentationView.Instance = this; + } + + //The first lifecycle function that gets called to set up the current presentation. + async componentWillMount() { + this.props.Documents.forEach(async (doc, index: number) => { + + //For each presentation received from mainContainer, a mapping is created. + let curDoc: Doc = await doc; + let newGuid = Utils.GenerateGuid(); + this.presentationsKeyMapping.set(curDoc, newGuid); + this.presentationsMapping.set(newGuid, curDoc); + + //The Presentation at first index gets set as default start presentation + if (index === 0) { + runInAction(() => this.currentSelectedPresValue = newGuid); + runInAction(() => this.curPresentation = curDoc); + } + }); + } + + //Second lifecycle function that gets called when component mounts. It makes sure to + //get the back-up information from previous session for the current presentation. + async componentDidMount() { + let docAtZero = await this.props.Documents[0]; + runInAction(() => this.curPresentation = docAtZero); + + this.setPresentationBackUps(); + + } + + + /** + * The function that retrieves the backUps for the current Presentation if present, + * otherwise initializes. + */ + setPresentationBackUps = async () => { + //getting both backUp documents + + let castedGroupBackUp = Cast(this.curPresentation.presGroupBackUp, Doc); + let castedButtonBackUp = Cast(this.curPresentation.presButtonBackUp, Doc); + //if instantiated before + if (castedGroupBackUp instanceof Promise) { + castedGroupBackUp.then(doc => { + let toAssign = doc ? doc : new Doc(); + this.curPresentation.presGroupBackUp = toAssign; + runInAction(() => this.presGroupBackUp = toAssign); + if (doc) { + if (toAssign[Id] === doc[Id]) { + this.retrieveGroupMappings(); + } + } + }); + + //if never instantiated a store doc yet + } else if (castedGroupBackUp instanceof Doc) { + let castedDoc: Doc = await castedGroupBackUp; + runInAction(() => this.presGroupBackUp = castedDoc); + this.retrieveGroupMappings(); + } else { + runInAction(() => { + let toAssign = new Doc(); + this.presGroupBackUp = toAssign; + this.curPresentation.presGroupBackUp = toAssign; + + }); + + } + //if instantiated before + if (castedButtonBackUp instanceof Promise) { + castedButtonBackUp.then(doc => { + let toAssign = doc ? doc : new Doc(); + this.curPresentation.presButtonBackUp = toAssign; + runInAction(() => this.presButtonBackUp = toAssign); + }); + + //if never instantiated a store doc yet + } else if (castedButtonBackUp instanceof Doc) { + let castedDoc: Doc = await castedButtonBackUp; + runInAction(() => this.presButtonBackUp = castedDoc); + + } else { + runInAction(() => { + let toAssign = new Doc(); + this.presButtonBackUp = toAssign; + this.curPresentation.presButtonBackUp = toAssign; + }); + + } + + + //storing the presentation status,ie. whether it was stopped or playing + let presStatusBackUp = BoolCast(this.curPresentation.presStatus, null); + runInAction(() => this.presStatus = presStatusBackUp); + } + + /** + * This is the function that is called to retrieve the groups that have been stored and + * push them to the groupMappings. + */ + retrieveGroupMappings = async () => { + let castedGroupDocs = await DocListCastAsync(this.presGroupBackUp.groupDocs); + if (castedGroupDocs !== undefined) { + castedGroupDocs.forEach(async (groupDoc: Doc, index: number) => { + let castedGrouping = await DocListCastAsync(groupDoc.grouping); + let castedKey = StrCast(groupDoc.presentIdStore, null); + if (castedGrouping) { + castedGrouping.forEach((doc: Doc) => { + doc.presentId = castedKey; + }); + } + if (castedGrouping !== undefined && castedKey !== undefined) { + this.groupMappings.set(castedKey, castedGrouping); + } + }); + } + } + + //observable means render is re-called every time variable is changed + @observable + collapsed: boolean = false; + closePresentation = action(() => this.curPresentation.width = 0); + next = async () => { + const current = NumCast(this.curPresentation.selectedDoc); + //asking to get document at current index + let docAtCurrentNext = await this.getDocAtIndex(current + 1); + if (docAtCurrentNext === undefined) { + return; + } + //asking for it's presentation id + let curNextPresId = StrCast(docAtCurrentNext.presentId); + let nextSelected = current + 1; + + //if curDoc is in a group, selection slides until last one, if not it's next one + if (this.groupMappings.has(curNextPresId)) { + let currentsArray = this.groupMappings.get(StrCast(docAtCurrentNext.presentId))!; + nextSelected = current + currentsArray.length - currentsArray.indexOf(docAtCurrentNext); + + //end of grup so go beyond + if (nextSelected === current) nextSelected = current + 1; + } + + this.gotoDocument(nextSelected, current); + + } + back = async () => { + const current = NumCast(this.curPresentation.selectedDoc); + //requesting for the doc at current index + let docAtCurrent = await this.getDocAtIndex(current); + if (docAtCurrent === undefined) { + return; + } + + //asking for its presentation id. + let curPresId = StrCast(docAtCurrent.presentId); + let prevSelected = current - 1; + let zoomOut: boolean = false; + + //checking if this presentation id is mapped to a group, if so chosing the first element in group + if (this.groupMappings.has(curPresId)) { + let currentsArray = this.groupMappings.get(StrCast(docAtCurrent.presentId))!; + prevSelected = current - currentsArray.length + (currentsArray.length - currentsArray.indexOf(docAtCurrent)) - 1; + //end of grup so go beyond + if (prevSelected === current) prevSelected = current - 1; + + //checking if any of the group members had used zooming in + currentsArray.forEach((doc: Doc) => { + if (this.presElementsMappings.get(doc)!.selected[buttonIndex.Show]) { + zoomOut = true; + return; + } + }); + + } + + // if a group set that flag to zero or a single element + //If so making sure to zoom out, which goes back to state before zooming action + if (current > 0) { + if (zoomOut || this.presElementsMappings.get(docAtCurrent)!.selected[buttonIndex.Show]) { + let prevScale = NumCast(this.childrenDocs[prevSelected].viewScale, null); + let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[current]); + if (prevScale !== undefined) { + if (prevScale !== curScale) { + DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale); + } + } + } + } + this.gotoDocument(prevSelected, current); + + } + + /** + * This is the method that checks for the actions that need to be performed + * after the document has been presented, which involves 3 button options: + * Hide Until Presented, Hide After Presented, Fade After Presented + */ + showAfterPresented = (index: number) => { + this.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => { + let selectedButtons: boolean[] = presElem.selected; + //the order of cases is aligned based on priority + if (selectedButtons[buttonIndex.HideTillPressed]) { + if (this.childrenDocs.indexOf(key) <= index) { + key.opacity = 1; + } + } + if (selectedButtons[buttonIndex.HideAfter]) { + if (this.childrenDocs.indexOf(key) < index) { + key.opacity = 0; + } + } + if (selectedButtons[buttonIndex.FadeAfter]) { + if (this.childrenDocs.indexOf(key) < index) { + key.opacity = 0.5; + } + } + }); + } + + /** + * This is the method that checks for the actions that need to be performed + * before the document has been presented, which involves 3 button options: + * Hide Until Presented, Hide After Presented, Fade After Presented + */ + hideIfNotPresented = (index: number) => { + this.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => { + let selectedButtons: boolean[] = presElem.selected; + + //the order of cases is aligned based on priority + + if (selectedButtons[buttonIndex.HideAfter]) { + if (this.childrenDocs.indexOf(key) >= index) { + key.opacity = 1; + } + } + if (selectedButtons[buttonIndex.FadeAfter]) { + if (this.childrenDocs.indexOf(key) >= index) { + key.opacity = 1; + } + } + if (selectedButtons[buttonIndex.HideTillPressed]) { + if (this.childrenDocs.indexOf(key) > index) { + key.opacity = 0; + } + } + }); + } + + /** + * This method makes sure that cursor navigates to the element that + * has the option open and last in the group. If not in the group, and it has + * te option open, navigates to that element. + */ + navigateToElement = async (curDoc: Doc, fromDoc: number) => { + let docToJump: Doc = curDoc; + let curDocPresId = StrCast(curDoc.presentId, null); + let willZoom: boolean = false; + + //checking if in group + if (curDocPresId !== undefined) { + if (this.groupMappings.has(curDocPresId)) { + let currentDocGroup = this.groupMappings.get(curDocPresId)!; + currentDocGroup.forEach((doc: Doc, index: number) => { + let selectedButtons: boolean[] = this.presElementsMappings.get(doc)!.selected; + if (selectedButtons[buttonIndex.Navigate]) { + docToJump = doc; + willZoom = false; + } + if (selectedButtons[buttonIndex.Show]) { + docToJump = doc; + willZoom = true; + } + }); + } + + } + //docToJump stayed same meaning, it was not in the group or was the last element in the group + if (docToJump === curDoc) { + //checking if curDoc has navigation open + let curDocButtons = this.presElementsMappings.get(curDoc)!.selected; + if (curDocButtons[buttonIndex.Navigate]) { + DocumentManager.Instance.jumpToDocument(curDoc, false); + } else if (curDocButtons[buttonIndex.Show]) { + let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]); + //awaiting jump so that new scale can be found, since jumping is async + await DocumentManager.Instance.jumpToDocument(curDoc, true); + let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc); + curDoc.viewScale = newScale; + + //saving the scale user was on before zooming in + if (curScale !== 1) { + this.childrenDocs[fromDoc].viewScale = curScale; + } + + } + return; + } + let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]); + + //awaiting jump so that new scale can be found, since jumping is async + await DocumentManager.Instance.jumpToDocument(docToJump, willZoom); + let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc); + curDoc.viewScale = newScale; + //saving the scale that user was on + if (curScale !== 1) { + this.childrenDocs[fromDoc].viewScale = curScale; + } + + } + + /** + * Async function that supposedly return the doc that is located at given index. + */ + getDocAtIndex = async (index: number) => { + const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc))); + if (!list) { + return undefined; + } + if (index < 0 || index >= list.length) { + return undefined; + } + + this.curPresentation.selectedDoc = index; + //awaiting async call to finish to get Doc instance + const doc = await list[index]; + return doc; + } + + /** + * The function that removes a doc from a presentation. It also makes sure to + * do necessary updates to backUps and mappings stored locally. + */ + @action + public RemoveDoc = async (index: number) => { + const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc))); + if (value) { + let removedDoc = await value.splice(index, 1)[0]; + + //removing the Presentation Element stored for it + this.presElementsMappings.delete(removedDoc); + + let removedDocPresentId = StrCast(removedDoc.presentId); + + //Removing it from local mapping of the groups + if (this.groupMappings.has(removedDocPresentId)) { + let removedDocsGroup = this.groupMappings.get(removedDocPresentId); + if (removedDocsGroup) { + removedDocsGroup.splice(removedDocsGroup.indexOf(removedDoc), 1); + if (removedDocsGroup.length === 0) { + this.groupMappings.delete(removedDocPresentId); + } + } + } + + //removing it from the backUp of selected Buttons + let castedList = Cast(this.presButtonBackUp.selectedButtonDocs, listSpec(Doc)); + if (castedList) { + castedList.splice(index, 1); + } + + //removing it from the backup of groups + let castedGroupDocs = await DocListCastAsync(this.presGroupBackUp.groupDocs); + if (castedGroupDocs) { + castedGroupDocs.forEach(async (groupDoc: Doc, index: number) => { + let castedKey = StrCast(groupDoc.presentIdStore, null); + if (castedKey === removedDocPresentId) { + let castedGrouping = await DocListCastAsync(groupDoc.grouping); + if (castedGrouping) { + castedGrouping.splice(castedGrouping.indexOf(removedDoc), 1); + if (castedGrouping.length === 0) { + castedGroupDocs!.splice(castedGroupDocs!.indexOf(groupDoc), 1); + } + } + } + + }); + + } + + + } + } + + //The function that is called when a document is clicked or reached through next or back. + //it'll also execute the necessary actions if presentation is playing. + @action + public gotoDocument = async (index: number, fromDoc: number) => { + const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc))); + if (!list) { + return; + } + if (index < 0 || index >= list.length) { + return; + } + this.curPresentation.selectedDoc = index; + + if (!this.presStatus) { + this.presStatus = true; + this.startPresentation(index); + } + + const doc = await list[index]; + if (this.presStatus) { + this.navigateToElement(doc, fromDoc); + this.hideIfNotPresented(index); + this.showAfterPresented(index); + } + + } + + //Function that is called to resetGroupIds, so that documents get new groupIds at + //first load, when presentation is changed. + resetGroupIds = async () => { + let castedGroupDocs = await DocListCastAsync(this.presGroupBackUp.groupDocs); + if (castedGroupDocs !== undefined) { + castedGroupDocs.forEach(async (groupDoc: Doc, index: number) => { + let castedGrouping = await DocListCastAsync(groupDoc.grouping); + if (castedGrouping) { + castedGrouping.forEach((doc: Doc) => { + doc.presentId = Utils.GenerateGuid(); + }); + } + }); + } + runInAction(() => this.groupMappings = new Map()); + } + + /** + * Adds a document to the presentation view + **/ + @action + public PinDoc(doc: Doc) { + //add this new doc to props.Document + const data = Cast(this.curPresentation.data, listSpec(Doc)); + if (data) { + data.push(doc); + } else { + this.curPresentation.data = new List([doc]); + } + + this.curPresentation.width = 400; + } + + //Function that sets the store of the children docs. + @action + setChildrenDocs = (docList: Doc[]) => { + this.childrenDocs = docList; + } + + //The function that is called to render the play or pause button depending on + //if presentation is running or not. + renderPlayPauseButton = () => { + if (this.presStatus) { + return <button title="Reset Presentation" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="stop" /></button>; + } else { + return <button title="Start Presentation From Start" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="play" /></button>; + } + } + + //The function that starts or resets presentaton functionally, depending on status flag. + @action + startOrResetPres = () => { + if (this.presStatus) { + this.resetPresentation(); + } else { + this.presStatus = true; + this.startPresentation(0); + const current = NumCast(this.curPresentation.selectedDoc); + this.gotoDocument(0, current); + } + this.curPresentation.presStatus = this.presStatus; + } + + //The function that resets the presentation by removing every action done by it. It also + //stops the presentaton. + @action + resetPresentation = () => { + this.childrenDocs.forEach((doc: Doc) => { + doc.opacity = 1; + doc.viewScale = 1; + }); + this.curPresentation.selectedDoc = 0; + this.presStatus = false; + this.curPresentation.presStatus = this.presStatus; + if (this.childrenDocs.length === 0) { + return; + } + DocumentManager.Instance.zoomIntoScale(this.childrenDocs[0], 1); + } + + + //The function that starts the presentation, also checking if actions should be applied + //directly at start. + startPresentation = (startIndex: number) => { + let selectedButtons: boolean[]; + this.presElementsMappings.forEach((component: PresentationElement, doc: Doc) => { + selectedButtons = component.selected; + if (selectedButtons[buttonIndex.HideTillPressed]) { + if (this.childrenDocs.indexOf(doc) > startIndex) { + doc.opacity = 0; + } + + } + if (selectedButtons[buttonIndex.HideAfter]) { + if (this.childrenDocs.indexOf(doc) < startIndex) { + doc.opacity = 0; + } + } + if (selectedButtons[buttonIndex.FadeAfter]) { + if (this.childrenDocs.indexOf(doc) < startIndex) { + doc.opacity = 0.5; + } + } + + }); + + } + + /** + * The function that is called to add a new presentation to the presentationView. + * It sets up te mappings and local copies of it. Resets the groupings and presentation. + * Makes the new presentation current selected, and retrieve the back-Ups if present. + */ + @action + addNewPresentation = (presTitle: string) => { + //creating a new presentation doc + let newPresentationDoc = Docs.TreeDocument([], { title: presTitle }); + this.props.Documents.push(newPresentationDoc); + + //setting that new doc as current + this.curPresentation = newPresentationDoc; + + //storing the doc in local copies for easier access + let newGuid = Utils.GenerateGuid(); + this.presentationsMapping.set(newGuid, newPresentationDoc); + this.presentationsKeyMapping.set(newPresentationDoc, newGuid); + + //resetting the previous presentation's actions so that new presentation can be loaded. + this.resetGroupIds(); + this.resetPresentation(); + this.presElementsMappings = new Map(); + this.currentSelectedPresValue = newGuid; + this.setPresentationBackUps(); + + } + + /** + * The function that is called to change the current selected presentation. + * Changes the presentation, also resetting groupings and presentation in process. + * Plus retrieving the backUps for the newly selected presentation. + */ + @action + getSelectedPresentation = (e: React.ChangeEvent<HTMLSelectElement>) => { + //get the guid of the selected presentation + let selectedGuid = e.target.value; + //set that as current presentation + this.curPresentation = this.presentationsMapping.get(selectedGuid)!; + + //reset current Presentations local things so that new one can be loaded + this.resetGroupIds(); + this.resetPresentation(); + this.presElementsMappings = new Map(); + this.currentSelectedPresValue = selectedGuid; + this.setPresentationBackUps(); + + + } + + /** + * The function that is called to render either select for presentations, or title inputting. + */ + renderSelectOrPresSelection = () => { + let presentationList = DocListCast(this.props.Documents); + if (this.PresTitleInputOpen || this.PresTitleChangeOpen) { + return <input ref={(e) => this.titleInputElement = e!} type="text" className="presentationView-title" placeholder="Enter Name!" onKeyDown={this.submitPresentationTitle} />; + } else { + return <select value={this.currentSelectedPresValue} id="pres_selector" className="presentationView-title" onChange={this.getSelectedPresentation}> + {presentationList.map((doc: Doc, index: number) => { + let mappedGuid = this.presentationsKeyMapping.get(doc); + let docGuid: string = mappedGuid ? mappedGuid.toString() : ""; + return <option key={docGuid} value={docGuid}>{StrCast(doc.title)}</option>; + })} + </select>; + } + } + + /** + * The function that is called on enter press of title input. It gives the + * new presentation the title user entered. If nothing is entered, gives a default title. + */ + @action + submitPresentationTitle = (e: React.KeyboardEvent) => { + if (e.keyCode === 13) { + let presTitle = this.titleInputElement!.value; + this.titleInputElement!.value = ""; + if (this.PresTitleInputOpen) { + if (presTitle === "") { + presTitle = "Presentation"; + } + this.PresTitleInputOpen = false; + this.addNewPresentation(presTitle); + } else if (this.PresTitleChangeOpen) { + this.PresTitleChangeOpen = false; + this.changePresentationTitle(presTitle); + } + } + } + + /** + * The function that is called to remove a presentation from all its copies, and the main Container's + * list. Sets up the next presentation as current. + */ + @action + removePresentation = async () => { + if (this.presentationsMapping.size !== 1) { + let presentationList = Cast(this.props.Documents, listSpec(Doc)); + let batch = UndoManager.StartBatch("presRemoval"); + + //getting the presentation that will be removed + let removedDoc = this.presentationsMapping.get(this.currentSelectedPresValue!); + //that presentation is removed + presentationList!.splice(presentationList!.indexOf(removedDoc!), 1); + + //its mappings are removed from local copies + this.presentationsKeyMapping.delete(removedDoc!); + this.presentationsMapping.delete(this.currentSelectedPresValue!); + + //the next presentation is set as current + let remainingPresentations = this.presentationsMapping.values(); + let nextDoc = remainingPresentations.next().value; + this.curPresentation = nextDoc; + + + //Storing these for being able to undo changes + let curGuid = this.currentSelectedPresValue!; + let curPresStatus = this.presStatus; + + //resetting the groups and presentation actions so that next presentation gets loaded + this.resetGroupIds(); + this.resetPresentation(); + this.currentSelectedPresValue = this.presentationsKeyMapping.get(nextDoc)!.toString(); + this.setPresentationBackUps(); + + //Storing for undo + let currentGroups = this.groupMappings; + let curPresElemMapping = this.presElementsMappings; + + //Event to undo actions that are not related to doc directly, aka. local things + UndoManager.AddEvent({ + undo: action(() => { + this.curPresentation = removedDoc!; + this.presentationsMapping.set(curGuid, removedDoc!); + this.presentationsKeyMapping.set(removedDoc!, curGuid); + this.currentSelectedPresValue = curGuid; + + this.presStatus = curPresStatus; + this.groupMappings = currentGroups; + this.presElementsMappings = curPresElemMapping; + this.setPresentationBackUps(); + + }), + redo: action(() => { + this.curPresentation = nextDoc; + this.presStatus = false; + this.presentationsKeyMapping.delete(removedDoc!); + this.presentationsMapping.delete(curGuid); + this.currentSelectedPresValue = this.presentationsKeyMapping.get(nextDoc)!.toString(); + this.setPresentationBackUps(); + + }), + }); + + batch.end(); + } + } + + /** + * The function that is called to change title of presentation to what user entered. + */ + @undoBatch + changePresentationTitle = (newTitle: string) => { + if (newTitle === "") { + return; + } + this.curPresentation.title = newTitle; + } + + + render() { + + let width = NumCast(this.curPresentation.width); + + return ( + <div className="presentationView-cont" style={{ width: width, overflow: "hidden" }}> + <div className="presentationView-heading"> + {this.renderSelectOrPresSelection()} + <button title="Close Presentation" className='presentation-icon' onClick={this.closePresentation}><FontAwesomeIcon icon={"times"} /></button> + <button title="Add Presentation" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => { + runInAction(() => { if (this.PresTitleChangeOpen) { this.PresTitleChangeOpen = false; } }); + runInAction(() => this.PresTitleInputOpen ? this.PresTitleInputOpen = false : this.PresTitleInputOpen = true); + }}><FontAwesomeIcon icon={"plus"} /></button> + <button title="Remove Presentation" className='presentation-icon' style={{ marginRight: 10 }} onClick={this.removePresentation}><FontAwesomeIcon icon={"minus"} /></button> + <button title="Change Presentation Title" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => { + runInAction(() => { if (this.PresTitleInputOpen) { this.PresTitleInputOpen = false; } }); + runInAction(() => this.PresTitleChangeOpen ? this.PresTitleChangeOpen = false : this.PresTitleChangeOpen = true); + }}><FontAwesomeIcon icon={"edit"} /></button> + </div> + <div className="presentation-buttons"> + <button title="Back" className="presentation-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button> + {this.renderPlayPauseButton()} + <button title="Next" className="presentation-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button> + </div> + <PresentationViewList + mainDocument={this.curPresentation} + deleteDocument={this.RemoveDoc} + gotoDocument={this.gotoDocument} + groupMappings={this.groupMappings} + presElementsMappings={this.presElementsMappings} + setChildrenDocs={this.setChildrenDocs} + presStatus={this.presStatus} + presButtonBackUp={this.presButtonBackUp} + presGroupBackUp={this.presGroupBackUp} + /> + </div> + ); + } +} |