aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/presentationview
diff options
context:
space:
mode:
authorandrewdkim <adkim414@gmail.com>2019-07-19 11:21:47 -0400
committerandrewdkim <adkim414@gmail.com>2019-07-19 11:21:47 -0400
commite453e7010def252b6cc10ad49d64708767c5589b (patch)
tree4353a4be5495acd61d813066d001e2b12da7d207 /src/client/views/presentationview
parent9cad9abcf164c7d81b8debf4aa2639d83edd227b (diff)
parent8854d3277541a67aef4187b5d3592bea5a7fcfa2 (diff)
merge from master
Diffstat (limited to 'src/client/views/presentationview')
-rw-r--r--src/client/views/presentationview/PresentationElement.tsx818
-rw-r--r--src/client/views/presentationview/PresentationList.tsx116
-rw-r--r--src/client/views/presentationview/PresentationView.scss97
-rw-r--r--src/client/views/presentationview/PresentationView.tsx838
4 files changed, 1869 insertions, 0 deletions
diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx
new file mode 100644
index 000000000..329630875
--- /dev/null
+++ b/src/client/views/presentationview/PresentationElement.tsx
@@ -0,0 +1,818 @@
+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, faFileDownload, 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";
+import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager";
+import { SelectionManager } from "../../util/SelectionManager";
+import { indexOf } from "typescript-collections/dist/lib/arrays";
+import { map } from "bluebird";
+
+library.add(faArrowUp);
+library.add(fileSolid);
+library.add(faLocationArrow);
+library.add(fileRegular as any);
+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;
+ removeDocByRef(doc: Doc): boolean;
+ PresElementsMappings: Map<Doc, PresentationElement>;
+
+
+}
+
+//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[];
+ private header?: HTMLDivElement | undefined;
+ private listdropDisposer?: DragManager.DragDropDisposer;
+ private presElRef: React.RefObject<HTMLDivElement>;
+ private backUpDoc: Doc | undefined;
+
+
+
+
+
+ constructor(props: PresentationElementProps) {
+ super(props);
+ this.selectedButtons = new Array(6);
+
+ this.presElRef = React.createRef();
+ }
+
+
+ componentWillUnmount() {
+ this.listdropDisposer && this.listdropDisposer();
+ }
+
+
+ /**
+ * 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();
+ if (this.presElRef.current) {
+ this.header = this.presElRef.current;
+ this.createListDropTarget(this.presElRef.current);
+ }
+ }
+
+ //Lifecycle function that makes sure button BackUp is received when not re-mounted bu re-rendered.
+ async componentDidUpdate() {
+ if (this.presElRef.current) {
+ this.header = this.presElRef.current;
+ this.createListDropTarget(this.presElRef.current);
+ }
+ }
+
+ 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>();
+ }
+
+ let foundDoc: boolean = false;
+
+ //if this is the first time this doc mounts, push a doc for it to store
+
+ for (let doc of castedList) {
+ let curDoc = await doc;
+ let curDocId = StrCast(curDoc.docId);
+ if (curDocId === this.props.document[Id]) {
+ let selectedButtonOfDoc = Cast(curDoc.selectedButtons, listSpec("boolean"), null);
+ if (selectedButtonOfDoc !== undefined) {
+ runInAction(() => this.selectedButtons = selectedButtonOfDoc);
+ foundDoc = true;
+ this.backUpDoc = curDoc;
+ break;
+ }
+ }
+ }
+
+ if (!foundDoc) {
+ let newDoc = new Doc();
+ let defaultBooleanArray: boolean[] = new Array(6);
+ newDoc.selectedButtons = new List(defaultBooleanArray);
+ newDoc.docId = this.props.document[Id];
+ castedList.push(newDoc);
+ this.backUpDoc = newDoc;
+ }
+
+ }
+
+ /**
+ * 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) => {
+ if (this.backUpDoc) {
+ this.backUpDoc.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);
+
+ }
+
+ /**
+ * Creating a drop target for drag and drop when called.
+ */
+ protected createListDropTarget = (ele: HTMLDivElement) => {
+ this.listdropDisposer && this.listdropDisposer();
+ if (ele) {
+ this.listdropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.listDrop.bind(this) } });
+ }
+ }
+
+ /**
+ * Returns a local transformed coordinate array for given coordinates.
+ */
+ ScreenToLocalListTransform = (xCord: number, yCord: number) => {
+ return [xCord, yCord];
+ }
+
+ /**
+ * This method is called when a element is dropped on a already esstablished target.
+ * It makes sure to do appropirate action depending on if the item is dropped before
+ * or after the target.
+ */
+ listDrop = async (e: Event, de: DragManager.DropEvent) => {
+ let x = this.ScreenToLocalListTransform(de.x, de.y);
+ let rect = this.header!.getBoundingClientRect();
+ let bounds = this.ScreenToLocalListTransform(rect.left, rect.top + rect.height / 2);
+ let before = x[1] < bounds[1];
+ if (de.data instanceof DragManager.DocumentDragData) {
+ let addDoc = (doc: Doc) => Doc.AddDocToList(this.props.mainDocument, "data", doc, this.props.document, before);
+ e.stopPropagation();
+ //where does treeViewId come from
+ let movedDocs = (de.data.options === this.props.mainDocument[Id] ? de.data.draggedDocuments : de.data.droppedDocuments);
+ //console.log("How is this causing an issue");
+ let droppedDoc: Doc = de.data.droppedDocuments[0];
+ await this.updateGroupsOnDrop(droppedDoc, de);
+ document.removeEventListener("pointermove", this.onDragMove, true);
+ return (de.data.dropAction || de.data.userDropAction) ?
+ de.data.droppedDocuments.reduce((added: boolean, d: Doc) => Doc.AddDocToList(this.props.mainDocument, "data", d, this.props.document, before) || added, false)
+ : (de.data.moveDocument) ?
+ movedDocs.reduce((added: boolean, d: Doc) => de.data.moveDocument(d, this.props.document, addDoc) || added, false)
+ : de.data.droppedDocuments.reduce((added: boolean, d: Doc) => Doc.AddDocToList(this.props.mainDocument, "data", d, this.props.document, before), false);
+ }
+ document.removeEventListener("pointermove", this.onDragMove, true);
+
+ return false;
+ }
+
+ /**
+ * This method is called to update groups when the user drags and drops an
+ * element to a different place. It follows the default behaviour and reconstructs
+ * the groups in the way they would appear if clicked by user.
+ */
+ updateGroupsOnDrop = async (droppedDoc: Doc, de: DragManager.DropEvent) => {
+
+ let x = this.ScreenToLocalListTransform(de.x, de.y);
+ let rect = this.header!.getBoundingClientRect();
+ let bounds = this.ScreenToLocalListTransform(rect.left, rect.top + rect.height / 2);
+ let before = x[1] < bounds[1];
+
+ let droppedDocIndex = this.props.allListElements.indexOf(droppedDoc);
+
+ let dropIndexDiff = droppedDocIndex - this.props.index;
+
+ //checking if the position it's dropped corresponds to current location with 3 cases.
+ if (droppedDocIndex === this.props.index) {
+ return;
+ }
+
+ if (dropIndexDiff === 1 && !before) {
+ return;
+ }
+ if (dropIndexDiff === -1 && before) {
+ return;
+ }
+
+ let p = this.props;
+ let droppedDocSelectedButtons: boolean[] = await this.getSelectedButtonsOfDoc(droppedDoc);
+ let curDocGuid = StrCast(droppedDoc.presentId, null);
+
+ //Splicing the doc from its current group, since it's moved
+ if (p.groupMappings.has(curDocGuid)) {
+ let groupArray = this.props.groupMappings.get(curDocGuid)!;
+
+ if (droppedDocSelectedButtons[buttonIndex.Group]) {
+ let groupIndexOfDrop = groupArray.indexOf(droppedDoc);
+ let firstPart = groupArray.splice(0, groupIndexOfDrop);
+
+ if (firstPart.length > 1) {
+ let newGroupGuid = Utils.GenerateGuid();
+ firstPart.forEach((doc: Doc) => doc.presentId = newGroupGuid);
+ this.props.groupMappings.set(newGroupGuid, firstPart);
+ }
+ }
+
+ groupArray.splice(groupArray.indexOf(droppedDoc), 1);
+ if (groupArray.length === 0) {
+ this.props.groupMappings.delete(curDocGuid);
+ }
+ droppedDoc.presentId = Utils.GenerateGuid();
+
+ //making sure to correct to groups after splicing, in case the dragged element
+ //had the grouping on.
+ let indexOfBelow = droppedDocIndex + 1;
+ if (indexOfBelow < this.props.allListElements.length && indexOfBelow > 1) {
+ let selectedButtonsOrigBelow: boolean[] = await this.getSelectedButtonsOfDoc(this.props.allListElements[indexOfBelow]);
+ let aboveBelowDoc: Doc = this.props.allListElements[droppedDocIndex - 1];
+ let aboveBelowDocSelectedButtons: boolean[] = await this.getSelectedButtonsOfDoc(aboveBelowDoc);
+ let belowDoc: Doc = this.props.allListElements[indexOfBelow];
+ let belowDocPresId = StrCast(belowDoc.presentId);
+
+ if (selectedButtonsOrigBelow[buttonIndex.Group]) {
+ let belowDocGroup: Doc[] = this.props.groupMappings.get(belowDocPresId)!;
+ if (aboveBelowDocSelectedButtons[buttonIndex.Group]) {
+ let aboveBelowDocPresId = StrCast(aboveBelowDoc.presentId);
+ if (this.props.groupMappings.has(aboveBelowDocPresId)) {
+ let aboveBelowDocGroup: Doc[] = this.props.groupMappings.get(aboveBelowDocPresId)!;
+ aboveBelowDocGroup.push(...belowDocGroup);
+ this.props.groupMappings.delete(belowDocPresId);
+ belowDocGroup.forEach((doc: Doc) => doc.presentId = aboveBelowDocPresId);
+
+ }
+ } else {
+ belowDocGroup.unshift(aboveBelowDoc);
+ aboveBelowDoc.presentId = belowDocPresId;
+ }
+
+
+ }
+ }
+
+ }
+
+ //Case, when the dropped doc had the group button clicked.
+ if (droppedDocSelectedButtons[buttonIndex.Group]) {
+ if (before) {
+ if (this.props.index > 0) {
+ let aboveDoc = this.props.allListElements[this.props.index - 1];
+ let aboveDocGuid = StrCast(aboveDoc.presentId);
+ if (this.props.groupMappings.has(aboveDocGuid)) {
+ this.protectOrderAndPush(aboveDocGuid, aboveDoc, droppedDoc);
+ } else {
+ this.createNewGroup(aboveDoc, droppedDoc, aboveDocGuid);
+ }
+ } else {
+ let propsPresId = StrCast(this.props.document.presentId);
+ if (this.selectedButtons[buttonIndex.Group]) {
+ let propsArray = this.props.groupMappings.get(propsPresId)!;
+ propsArray.unshift(droppedDoc);
+ droppedDoc.presentId = propsPresId;
+ }
+ }
+ } else {
+ let propsDocGuid = StrCast(this.props.document.presentId);
+ if (this.props.groupMappings.has(propsDocGuid)) {
+ this.protectOrderAndPush(propsDocGuid, this.props.document, droppedDoc);
+
+ } else {
+ this.createNewGroup(this.props.document, droppedDoc, propsDocGuid);
+ }
+ }
+
+
+ //if the group button of the element was not clicked.
+ } else {
+ if (before) {
+ if (this.props.index > 0) {
+
+ let aboveDoc = this.props.allListElements[this.props.index - 1];
+ let aboveDocGuid = StrCast(aboveDoc.presentId);
+ let aboveDocSelectedButtons: boolean[] = await this.getSelectedButtonsOfDoc(aboveDoc);
+
+
+ if (this.selectedButtons[buttonIndex.Group]) {
+ if (aboveDocSelectedButtons[buttonIndex.Group]) {
+ let aboveGroupArray = this.props.groupMappings.get(aboveDocGuid)!;
+ let propsDocPresId = StrCast(this.props.document.presentId);
+
+ this.halveGroupArray(aboveDoc, aboveGroupArray, droppedDoc, propsDocPresId);
+
+ } else {
+ let belowPresentId = StrCast(this.props.document.presentId);
+ let belowGroup = this.props.groupMappings.get(belowPresentId)!;
+ belowGroup.splice(belowGroup.indexOf(aboveDoc), 1);
+ belowGroup.unshift(droppedDoc);
+ droppedDoc.presentId = belowPresentId;
+ aboveDoc.presentId = Utils.GenerateGuid();
+ }
+
+
+ }
+ } else {
+ let propsPresId = StrCast(this.props.document.presentId);
+ if (this.selectedButtons[buttonIndex.Group]) {
+ let propsArray = this.props.groupMappings.get(propsPresId)!;
+ propsArray.unshift(droppedDoc);
+ droppedDoc.presentId = propsPresId;
+ }
+ }
+ } else {
+ if (this.props.index < this.props.allListElements.length - 1) {
+ let belowDoc = this.props.allListElements[this.props.index + 1];
+ let belowDocGuid = StrCast(belowDoc.presentId);
+ let belowDocSelectedButtons: boolean[] = await this.getSelectedButtonsOfDoc(belowDoc);
+
+ let propsDocGuid = StrCast(this.props.document.presentId);
+
+ if (belowDocSelectedButtons[buttonIndex.Group]) {
+ let belowGroupArray = this.props.groupMappings.get(belowDocGuid)!;
+ if (this.selectedButtons[buttonIndex.Group]) {
+
+ let propsGroupArray = this.props.groupMappings.get(propsDocGuid)!;
+
+ this.halveGroupArray(this.props.document, propsGroupArray, droppedDoc, belowDocGuid);
+
+ } else {
+ belowGroupArray.splice(belowGroupArray.indexOf(this.props.document), 1);
+ this.props.document.presentId = Utils.GenerateGuid();
+ belowGroupArray.unshift(droppedDoc);
+ droppedDoc.presentId = belowDocGuid;
+ }
+ }
+
+ }
+ }
+ }
+ this.autoSaveGroupChanges();
+
+ }
+
+ /**
+ * This method returns the selectedButtons boolean array of the passed in doc,
+ * retrieving it from the back-up.
+ */
+ getSelectedButtonsOfDoc = async (paramDoc: Doc) => {
+ let castedList = Cast(this.props.presButtonBackUp.selectedButtonDocs, listSpec(Doc));
+ let foundSelectedButtons: boolean[] = new Array(6);
+
+ //if this is the first time this doc mounts, push a doc for it to store
+ for (let doc of castedList!) {
+ let curDoc = await doc;
+ let curDocId = StrCast(curDoc.docId);
+ if (curDocId === paramDoc[Id]) {
+ let selectedButtonOfDoc = Cast(curDoc.selectedButtons, listSpec("boolean"), null);
+ if (selectedButtonOfDoc !== undefined) {
+ return selectedButtonOfDoc;
+ }
+ }
+ }
+
+ return foundSelectedButtons;
+
+ }
+
+ //This is used to add dragging as an event.
+ onPointerEnter = (e: React.PointerEvent): void => {
+ this.props.document.libraryBrush = true;
+ if (e.buttons === 1 && SelectionManager.GetIsDragging()) {
+ let selected = NumCast(this.props.mainDocument.selectedDoc, 0);
+
+ this.header!.className = "presentationView-item";
+
+
+ if (selected === this.props.index) {
+ //this doc is selected
+ this.header!.className = "presentationView-item presentationView-selected";
+ }
+ document.addEventListener("pointermove", this.onDragMove, true);
+ }
+ }
+
+ //This is used to remove the dragging when dropped.
+ onPointerLeave = (e: React.PointerEvent): void => {
+ this.props.document.libraryBrush = false;
+ //to get currently selected presentation doc
+ let selected = NumCast(this.props.mainDocument.selectedDoc, 0);
+
+ this.header!.className = "presentationView-item";
+
+
+ if (selected === this.props.index) {
+ //this doc is selected
+ this.header!.className = "presentationView-item presentationView-selected";
+
+ }
+ document.removeEventListener("pointermove", this.onDragMove, true);
+ }
+
+ /**
+ * This method is passed in to be used when dragging a document.
+ * It makes it possible to show dropping lines on drop targets.
+ */
+ onDragMove = (e: PointerEvent): void => {
+ this.props.document.libraryBrush = false;
+ let x = this.ScreenToLocalListTransform(e.clientX, e.clientY);
+ let rect = this.header!.getBoundingClientRect();
+ let bounds = this.ScreenToLocalListTransform(rect.left, rect.top + rect.height / 2);
+ let before = x[1] < bounds[1];
+ this.header!.className = "presentationView-item";
+ if (before) {
+ this.header!.className += " presentationView-item-above";
+ }
+ else if (!before) {
+ this.header!.className += " presentationView-item-below";
+ }
+ e.stopPropagation();
+ }
+
+ /**
+ * This method is passed in to on down event of presElement, so that drag and
+ * drop can be completed with DragManager functionality.
+ */
+ @action
+ move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => {
+ return this.props.document !== target && this.props.removeDocByRef(doc) && addDoc(doc);
+ }
+
+ /**
+ * Helper method that gets called to divide a group array into two different groups
+ * including the targetDoc in first part.
+ * @param targetDoc document that is targeted as slicing point
+ * @param propsGroupArray the array that gets divided into 2
+ * @param droppedDoc the dropped document
+ * @param belowDocGuid presentId of the belowGroup
+ */
+ private halveGroupArray(targetDoc: Doc, propsGroupArray: Doc[], droppedDoc: Doc, belowDocGuid: string) {
+ let targetIndex = propsGroupArray.indexOf(targetDoc);
+ let firstPart = propsGroupArray.slice(0, targetIndex + 1);
+ let firstPartNewGuid = Utils.GenerateGuid();
+ firstPart.forEach((doc: Doc) => doc.presentId = firstPartNewGuid);
+ let secondPart = propsGroupArray.slice(targetIndex + 1);
+ secondPart.unshift(droppedDoc);
+ droppedDoc.presentId = belowDocGuid;
+ this.props.groupMappings.set(firstPartNewGuid, firstPart);
+ this.props.groupMappings.set(belowDocGuid, secondPart);
+ }
+
+ /**
+ * Helper method that creates a new group, pushing above document first,
+ * and dropped document second.
+ * @param aboveDoc the document above dropped document
+ * @param droppedDoc the dropped document itself
+ * @param aboveDocGuid above document's presentId
+ */
+ private createNewGroup(aboveDoc: Doc, droppedDoc: Doc, aboveDocGuid: string) {
+ let newGroup: Doc[] = [];
+ newGroup.push(aboveDoc);
+ newGroup.push(droppedDoc);
+ droppedDoc.presentId = aboveDocGuid;
+ this.props.groupMappings.set(aboveDocGuid, newGroup);
+ }
+
+ /**
+ * Helper method that finds the above document's group, and pushes the
+ * dropped document into that group, protecting the visual order of the
+ * presentation elements.
+ * @param aboveDoc the document above dropped document
+ * @param droppedDoc the dropped document itself
+ * @param aboveDocGuid above document's presentId
+ */
+ private protectOrderAndPush(aboveDocGuid: string, aboveDoc: Doc, droppedDoc: Doc) {
+ let groupArray = this.props.groupMappings.get(aboveDocGuid)!;
+ let tempStack: Doc[] = [];
+ while (groupArray[groupArray.length - 1] !== aboveDoc) {
+ tempStack.push(groupArray.pop()!);
+ }
+ groupArray.push(droppedDoc);
+ droppedDoc.presentId = aboveDocGuid;
+ while (tempStack.length !== 0) {
+ groupArray.push(tempStack.pop()!);
+ }
+ }
+
+
+
+
+ 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 dropAction = StrCast(this.props.document.dropAction) as dropActionType;
+ let onItemDown = SetupDrag(this.presElRef, () => p.document, this.move, dropAction, this.props.mainDocument[Id], true);
+ return (
+ <div className={className} key={p.document[Id] + p.index}
+ ref={this.presElRef}
+ onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
+ onPointerDown={onItemDown}
+ style={{
+ outlineColor: "maroon",
+ outlineStyle: "dashed",
+ outlineWidth: BoolCast(p.document.libraryBrush) ? `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" onPointerDown={(e) => e.stopPropagation()} 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"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
+ <button title="Navigate" className={this.selectedButtons[buttonIndex.Navigate] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
+ <button title="Hide Document Till Presented" className={this.selectedButtons[buttonIndex.HideTillPressed] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
+ <button title="Fade Document After Presented" className={this.selectedButtons[buttonIndex.FadeAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} color={"gray"} /></button>
+ <button title="Hide Document After Presented" className={this.selectedButtons[buttonIndex.HideAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
+ <button title="Group With Up" className={this.selectedButtons[buttonIndex.Group] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} 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..2d63d41b5
--- /dev/null
+++ b/src/client/views/presentationview/PresentationList.tsx
@@ -0,0 +1,116 @@
+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";
+import { DragManager } from "../../util/DragManager";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import "../../../new_fields/Doc";
+
+
+
+
+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;
+ removeDocByRef(doc: Doc): boolean;
+ clearElemMap(): void;
+
+}
+
+
+@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);
+ this.props.clearElemMap();
+ return (
+ <div className="presentationView-listCont" >
+ {children.map((doc: Doc, index: number) =>
+ <PresentationElement
+ ref={(e) => {
+ if (e && e !== null) {
+ 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}
+ removeDocByRef={this.props.removeDocByRef}
+ PresElementsMappings={this.props.PresElementsMappings}
+ />
+ )}
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/presentationview/PresentationView.scss b/src/client/views/presentationview/PresentationView.scss
new file mode 100644
index 000000000..2bb0ec8c8
--- /dev/null
+++ b/src/client/views/presentationview/PresentationView.scss
@@ -0,0 +1,97 @@
+.presentationView-cont {
+ position: absolute;
+ background: white;
+ z-index: 1;
+ box-shadow: #AAAAAA .2vw .2vw .4vw;
+ right: 0;
+ top: 0;
+ bottom: 0;
+}
+
+.presentationView-item {
+ padding: 10px;
+ display: inline-block;
+ width: 100%;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ transition: all .1s;
+}
+
+.presentationView-item-above {
+ border-top: black 2px solid;
+}
+
+.presentationView-item-below {
+ border-bottom: black 2px solid;
+}
+
+.presentationView-listCont {
+ padding-left: 10px;
+ padding-right: 10px;
+}
+
+.presentationView-item:hover {
+ transition: all .1s;
+ background: #AAAAAA
+}
+
+.presentationView-selected {
+ background: gray;
+}
+
+.presentationView-heading {
+ background: lightseagreen;
+ padding: 10px;
+ display: inline-block;
+ width: 100%;
+}
+
+.presentationView-title {
+ padding-top: 3px;
+ 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: 2.5%;
+ margin-left: 2.5%;
+ width: 25%;
+}
+
+.presentation-buttons {
+ padding: 10px;
+}
+
+.presentation-next:hover {
+ transition: all .1s;
+ background: #AAAAAA
+}
+
+.presentation-back:hover {
+ transition: all .1s;
+ background: #AAAAAA
+} \ No newline at end of file
diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx
new file mode 100644
index 000000000..f80840f96
--- /dev/null
+++ b/src/client/views/presentationview/PresentationView.tsx
@@ -0,0 +1,838 @@
+import { observer } from "mobx-react";
+import React = require("react");
+import { observable, action, runInAction, reaction, autorun } 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) => {
+ //let presElem: PresentationElement | undefined = this.presElementsMappings.get(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.forEach(async (doc, indexOfDoc) => {
+ // let curDoc = await doc;
+ // let curDocId = StrCast(curDoc.docId);
+ // if (curDocId === removedDoc[Id]) {
+ // if (castedList) {
+ // castedList.splice(indexOfDoc, 1);
+ // return;
+ // }
+ // }
+ // });
+
+ // }
+ //removing it from the backUp of selected Buttons
+
+ let castedList = Cast(this.presButtonBackUp.selectedButtonDocs, listSpec(Doc));
+ if (castedList) {
+ for (let doc of castedList) {
+ let curDoc = await doc;
+ let curDocId = StrCast(curDoc.docId);
+ if (curDocId === removedDoc[Id]) {
+ castedList.splice(castedList.indexOf(curDoc), 1);
+ break;
+
+ }
+ }
+ }
+
+ //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);
+ }
+ }
+ }
+
+ });
+
+ }
+
+
+ }
+ }
+
+ public removeDocByRef = (doc: Doc) => {
+ let indexOfDoc = this.childrenDocs.indexOf(doc);
+ const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+ if (value) {
+ value.splice(indexOfDoc, 1)[0];
+ }
+ //this.RemoveDoc(indexOfDoc, true);
+ if (indexOfDoc !== - 1) {
+ return true;
+ }
+ return false;
+ }
+
+ //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
+ **/
+ @undoBatch
+ @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.Create.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;
+ }
+
+ addPressElem = (keyDoc: Doc, elem: PresentationElement) => {
+ this.presElementsMappings.set(keyDoc, elem);
+ }
+
+
+ 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}
+ removeDocByRef={this.removeDocByRef}
+ clearElemMap={() => this.presElementsMappings.clear()}
+ />
+ </div>
+ );
+ }
+}