diff options
Diffstat (limited to 'src/client/views/nodes/trails/PresBox.tsx')
-rw-r--r-- | src/client/views/nodes/trails/PresBox.tsx | 2440 |
1 files changed, 2440 insertions, 0 deletions
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx new file mode 100644 index 000000000..cfc3e75cc --- /dev/null +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -0,0 +1,2440 @@ +import React = require("react"); +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tooltip } from "@material-ui/core"; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import { ColorState, SketchPicker } from "react-color"; +import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal'; +import { Doc, DocListCast, DocListCastAsync, FieldResult } from "../../../../fields/Doc"; +import { documentSchema } from "../../../../fields/documentSchemas"; +import { InkTool } from "../../../../fields/InkField"; +import { List } from "../../../../fields/List"; +import { PrefetchProxy } from "../../../../fields/Proxy"; +import { listSpec, makeInterface } from "../../../../fields/Schema"; +import { ScriptField } from "../../../../fields/ScriptField"; +import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types"; +import { emptyFunction, returnFalse, returnOne, returnTrue } from '../../../../Utils'; +import { Docs } from "../../../documents/Documents"; +import { DocumentType } from "../../../documents/DocumentTypes"; +import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; +import { DocumentManager } from "../../../util/DocumentManager"; +import { Scripting } from "../../../util/Scripting"; +import { SelectionManager } from "../../../util/SelectionManager"; +import { undoBatch, UndoManager } from "../../../util/UndoManager"; +import { CollectionDockingView } from "../../collections/CollectionDockingView"; +import { CollectionView, CollectionViewType } from "../../collections/CollectionView"; +import { TabDocView } from "../../collections/TabDocView"; +import { ViewBoxBaseComponent } from "../../DocComponent"; +import { Colors } from "../../global/globalEnums"; +import { LightboxView } from "../../LightboxView"; +import { CollectionFreeFormDocumentView } from "../CollectionFreeFormDocumentView"; +import { FieldView, FieldViewProps } from '../FieldView'; +import "./PresBox.scss"; +import Color = require("color"); +import { PresEffect, PresStatus, PresMovement } from "./PresEnums"; + +export class PinProps { + audioRange?: boolean; + unpin?: boolean; + setPosition?: boolean; + hidePresBox?: boolean; +} + +type PresBoxSchema = makeInterface<[typeof documentSchema]>; +const PresBoxDocument = makeInterface(documentSchema); + +@observer +export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>(PresBoxDocument) { + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresBox, fieldKey); } + + static renderEffectsDoc(renderDoc: any, layoutDoc: Doc) { + const effectProps = { + left: layoutDoc.presEffectDirection === PresEffect.Left, + right: layoutDoc.presEffectDirection === PresEffect.Right, + top: layoutDoc.presEffectDirection === PresEffect.Top, + bottom: layoutDoc.presEffectDirection === PresEffect.Bottom, + opposite: true, + delay: layoutDoc.presTransition, + // when: this.layoutDoc === PresBox.Instance.childDocs[PresBox.Instance.itemIndex]?.presentationTargetDoc, + }; + switch (layoutDoc.presEffect) { + case PresEffect.Zoom: return (<Zoom {...effectProps}>{renderDoc}</Zoom>); + case PresEffect.Fade: return (<Fade {...effectProps}>{renderDoc}</Fade>); + case PresEffect.Flip: return (<Flip {...effectProps}>{renderDoc}</Flip>); + case PresEffect.Rotate: return (<Rotate {...effectProps}>{renderDoc}</Rotate>); + case PresEffect.Bounce: return (<Bounce {...effectProps}>{renderDoc}</Bounce>); + case PresEffect.Roll: return (<Roll {...effectProps}>{renderDoc}</Roll>); + case PresEffect.Lightspeed: return (<LightSpeed {...effectProps}>{renderDoc}</LightSpeed>); + case PresEffect.None: + default: return renderDoc; + } + } + public static EffectsProvider(layoutDoc: Doc, renderDoc: any) { + return PresBox.Instance && layoutDoc === PresBox.Instance.childDocs[PresBox.Instance.itemIndex]?.presentationTargetDoc ? + PresBox.renderEffectsDoc(renderDoc, layoutDoc) + : + renderDoc; + } + + @observable public static Instance: PresBox; + + @observable _isChildActive = false; + @observable _moveOnFromAudio: boolean = true; + @observable _presTimer!: NodeJS.Timeout; + @observable _presKeyEventsActive: boolean = false; + + @observable _selectedArray: ObservableMap = new ObservableMap<Doc, any>(); + @observable _eleArray: HTMLElement[] = []; + @observable _dragArray: HTMLElement[] = []; + @observable _pathBoolean: boolean = false; + @observable _expandBoolean: boolean = false; + + private _disposers: { [name: string]: IReactionDisposer } = {}; + @observable private transitionTools: boolean = false; + @observable private newDocumentTools: boolean = false; + @observable private progressivizeTools: boolean = false; + @observable private openMovementDropdown: boolean = false; + @observable private openEffectDropdown: boolean = false; + @observable private presentTools: boolean = false; + @computed get childDocs() { return DocListCast(this.dataDoc[this.fieldKey]); } + @computed get tagDocs() { + const tagDocs: Doc[] = []; + for (const doc of this.childDocs) { + const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); + tagDocs.push(tagDoc); + } + return tagDocs; + } + @computed get itemIndex() { return NumCast(this.rootDoc._itemIndex); } + @computed get activeItem() { return Cast(this.childDocs[NumCast(this.rootDoc._itemIndex)], Doc, null); } + @computed get targetDoc() { return Cast(this.activeItem?.presentationTargetDoc, Doc, null); } + @computed get scrollable(): boolean { + if (this.targetDoc.type === DocumentType.PDF || this.targetDoc.type === DocumentType.WEB || this.targetDoc.type === DocumentType.RTF || this.targetDoc._viewType === CollectionViewType.Stacking) return true; + else return false; + } + @computed get panable(): boolean { + if ((this.targetDoc.type === DocumentType.COL && this.targetDoc._viewType === CollectionViewType.Freeform) || this.targetDoc.type === DocumentType.IMG) return true; + else return false; + } + @computed get presElement() { return Cast(Doc.UserDoc().presElement, Doc, null); } + constructor(props: any) { + super(props); + if (Doc.UserDoc().activePresentation = this.rootDoc) runInAction(() => PresBox.Instance = this); + if (!this.presElement) { // create exactly one presElmentBox template to use by any and all presentations. + Doc.UserDoc().presElement = new PrefetchProxy(Docs.Create.PresElementBoxDocument({ + title: "pres element template", type: DocumentType.PRESELEMENT, _xMargin: 0, isTemplateDoc: true, isTemplateForField: "data" + })); + // this script will be called by each presElement to get rendering-specific info that the PresBox knows about but which isn't written to the PresElement + // this is a design choice -- we could write this data to the presElements which would require a reaction to keep it up to date, and it would prevent + // the preselement docs from being part of multiple presentations since they would all have the same field, or we'd have to keep per-presentation data + // stored on each pres element. + (this.presElement as Doc).lookupField = ScriptField.MakeFunction("lookupPresBoxField(container, field, data)", + { field: "string", data: Doc.name, container: Doc.name }); + } + this.props.Document.presentationFieldKey = this.fieldKey; // provide info to the presElement script so that it can look up rendering information about the presBox + } + @computed get selectedDocumentView() { + if (SelectionManager.Views().length) return SelectionManager.Views()[0]; + if (this._selectedArray.size) return DocumentManager.Instance.getDocumentView(this.rootDoc); + } + @computed get isPres(): boolean { + document.removeEventListener("keydown", PresBox.keyEventsWrapper, true); + if (this.selectedDoc?.type === DocumentType.PRES) { + document.removeEventListener("keydown", PresBox.keyEventsWrapper, true); + document.addEventListener("keydown", PresBox.keyEventsWrapper, true); + return true; + } + return false; + } + @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } + + @action + componentWillUnmount() { + document.removeEventListener("keydown", PresBox.keyEventsWrapper, true); + this._presKeyEventsActive = false; + this.resetPresentation(); + // Turn of progressivize editors + this.turnOffEdit(true); + Object.values(this._disposers).forEach(disposer => disposer?.()); + } + + @action + componentDidMount() { + this.rootDoc.presBox = this.rootDoc; + this.rootDoc._forceRenderEngine = "timeline"; + this.layoutDoc.presStatus = PresStatus.Edit; + this.layoutDoc._gridGap = 0; + this.layoutDoc._yMargin = 0; + this.turnOffEdit(true); + DocListCastAsync((Doc.UserDoc().myPresentations as Doc).data).then(pres => + !pres?.includes(this.rootDoc) && Doc.AddDocToList(Doc.UserDoc().myPresentations as Doc, "data", this.rootDoc)); + this._disposers.selection = reaction(() => SelectionManager.Views(), + views => views.some(view => view.props.Document === this.rootDoc) && this.updateCurrentPresentation()); + } + + @action + updateCurrentPresentation = (pres?: Doc) => { + if (pres) Doc.UserDoc().activePresentation = pres; + else Doc.UserDoc().activePresentation = this.rootDoc; + document.removeEventListener("keydown", PresBox.keyEventsWrapper, true); + document.addEventListener("keydown", PresBox.keyEventsWrapper, true); + this._presKeyEventsActive = true; + PresBox.Instance = this; + } + + // There are still other internal frames and should go through all frames before going to next slide + nextInternalFrame = (targetDoc: Doc, activeItem: Doc) => { + const currentFrame = Cast(targetDoc?._currentFrame, "number", null); + const childDocs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]); + targetDoc._viewTransition = "all 1s"; + setTimeout(() => targetDoc._viewTransition = undefined, 1010); + this.nextKeyframe(targetDoc, activeItem); + if (activeItem.presProgressivize) CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, targetDoc); + else targetDoc.keyFrameEditing = true; + } + + _mediaTimer!: [NodeJS.Timeout, Doc]; + // 'Play on next' for audio or video therefore first navigate to the audio/video before it should be played + startTempMedia = (targetDoc: Doc, activeItem: Doc) => { + const duration: number = NumCast(activeItem.presEndTime) - NumCast(activeItem.presStartTime); + if ([DocumentType.VID, DocumentType.AUDIO].includes(targetDoc.type as any)) { + const targMedia = DocumentManager.Instance.getDocumentView(targetDoc); + targMedia?.ComponentView?.playFrom?.(NumCast(activeItem.presStartTime), NumCast(activeItem.presStartTime) + duration); + } + // if (targetDoc.type === DocumentType.AUDIO) { + // if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]); + // targetDoc._triggerAudio = NumCast(activeItem.presStartTime); + // this._mediaTimer = [setTimeout(() => targetDoc._audioStop = true, duration * 1000), targetDoc]; + // } else if (targetDoc.type === DocumentType.VID) { + // targetDoc._triggerVideoStop = true; + // setTimeout(() => targetDoc._currentTimecode = NumCast(activeItem.presStartTime), 10); + // setTimeout(() => targetDoc._triggerVideo = true, 20); + // this._mediaTimer = [setTimeout(() => targetDoc._triggerVideoStop = true, (duration * 1000) + 20), targetDoc]; + // } + } + + stopTempMedia = (targetDocField: FieldResult) => { + const targetDoc = Cast(targetDocField, Doc, null); + if (targetDoc?.type === DocumentType.AUDIO) { + if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]); + targetDoc._audioStop = true; + } else if (targetDoc?.type === DocumentType.VID) { + if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]); + targetDoc._triggerVideoStop = true; + } + } + + // No more frames in current doc and next slide is defined, therefore move to next slide + nextSlide = (activeNext: Doc) => { + const targetNext = Cast(activeNext.presentationTargetDoc, Doc, null); + let nextSelected = this.itemIndex + 1; + this.gotoDocument(nextSelected, this.activeItem); + for (nextSelected = nextSelected + 1; nextSelected < this.childDocs.length; nextSelected++) { + if (!this.childDocs[nextSelected].groupWithUp) { + break; + } else { + console.log("Title: " + this.childDocs[nextSelected].title); + this.gotoDocument(nextSelected, this.activeItem, true); + } + } + } + + // Called when the user activates 'next' - to move to the next part of the pres. trail + @action + next = () => { + const activeNext = Cast(this.childDocs[this.itemIndex + 1], Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + const lastFrame = Cast(targetDoc?.lastFrame, "number", null); + const curFrame = NumCast(targetDoc?._currentFrame); + let internalFrames: boolean = false; + if (activeItem.presProgressivize || activeItem.zoomProgressivize || targetDoc.scrollProgressivize) internalFrames = true; + if (internalFrames && lastFrame !== undefined && curFrame < lastFrame) { + // Case 1: There are still other frames and should go through all frames before going to next slide + this.nextInternalFrame(targetDoc, activeItem); + } else if (this.childDocs[this.itemIndex + 1] !== undefined) { + // Case 2: No more frames in current doc and next slide is defined, therefore move to next slide + this.nextSlide(activeNext); + } else if (this.childDocs[this.itemIndex + 1] === undefined && (this.layoutDoc.presLoop || this.layoutDoc.presStatus === PresStatus.Edit)) { + // Case 3: Last slide and presLoop is toggled ON or it is in Edit mode + this.gotoDocument(0, this.activeItem); + } + } + + // Called when the user activates 'back' - to move to the previous part of the pres. trail + @action + back = () => { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + const prevItem = Cast(this.childDocs[Math.max(0, this.itemIndex - 1)], Doc, null); + const prevTargetDoc = Cast(prevItem.presentationTargetDoc, Doc, null); + const lastFrame = Cast(targetDoc.lastFrame, "number", null); + const curFrame = NumCast(targetDoc._currentFrame); + let prevSelected = this.itemIndex; + // Functionality for group with up + let didZoom = activeItem.presMovement; + for (; !didZoom && prevSelected > 0 && this.childDocs[prevSelected].groupButton; prevSelected--) { + didZoom = this.childDocs[prevSelected].presMovement; + } + if (lastFrame !== undefined && curFrame >= 1) { + // Case 1: There are still other frames and should go through all frames before going to previous slide + this.prevKeyframe(targetDoc, activeItem); + } else if (activeItem && this.childDocs[this.itemIndex - 1] !== undefined) { + // Case 2: There are no other frames so it should go to the previous slide + prevSelected = Math.max(0, prevSelected - 1); + this.gotoDocument(prevSelected, activeItem); + if (NumCast(prevTargetDoc.lastFrame) > 0) prevTargetDoc._currentFrame = NumCast(prevTargetDoc.lastFrame); + } else if (this.childDocs[this.itemIndex - 1] === undefined && this.layoutDoc.presLoop) { + // Case 3: Pres loop is on so it should go to the last slide + this.gotoDocument(this.childDocs.length - 1, activeItem); + } + } + + //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. + public gotoDocument = action((index: number, from?: Doc, group?: boolean) => { + Doc.UnBrushAllDocs(); + if (index >= 0 && index < this.childDocs.length) { + this.rootDoc._itemIndex = index; + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + if (from?.mediaStopTriggerList && this.layoutDoc.presStatus !== PresStatus.Edit) { + DocListCast(from.mediaStopTriggerList).forEach(this.stopTempMedia); + } + if (from?.mediaStop === "auto" && this.layoutDoc.presStatus !== PresStatus.Edit) { + this.stopTempMedia(from.presentationTargetDoc); + } + // If next slide is audio / video 'Play automatically' then the next slide should be played + if (this.layoutDoc.presStatus !== PresStatus.Edit && (targetDoc.type === DocumentType.AUDIO || targetDoc.type === DocumentType.VID) && (activeItem.mediaStart === "auto")) { + this.startTempMedia(targetDoc, activeItem); + } + if (targetDoc) { + targetDoc && runInAction(() => { + if (activeItem.presMovement === PresMovement.Jump) targetDoc.focusSpeed = 0; + else targetDoc.focusSpeed = activeItem.presTransition ? activeItem.presTransition : 500; + }); + setTimeout(() => targetDoc.focusSpeed = 500, this.activeItem.presTransition ? NumCast(this.activeItem.presTransition) + 10 : 510); + } + if (targetDoc?.lastFrame !== undefined) { + targetDoc._currentFrame = 0; + } + if (!group) this._selectedArray.clear(); + this.childDocs[index] && this._selectedArray.set(this.childDocs[index], undefined); //Update selected array + if (this.layoutDoc._viewType === "stacking" && !group) this.navigateToElement(this.childDocs[index]); //Handles movement to element only when presTrail is list + this.onHideDocument(); //Handles hide after/before + } + }); + + _navTimer!: NodeJS.Timeout; + navigateToView = (targetDoc: Doc, activeItem: Doc) => { + clearTimeout(this._navTimer); + const bestTarget = DocumentManager.Instance.getFirstDocumentView(targetDoc)?.props.Document; + bestTarget && runInAction(() => { + if (bestTarget.type === DocumentType.PDF || bestTarget.type === DocumentType.WEB || bestTarget.type === DocumentType.RTF || bestTarget._viewType === CollectionViewType.Stacking) { + bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s'; + bestTarget._scrollTop = activeItem.presPinViewScroll; + } else if (bestTarget.type === DocumentType.COMPARISON) { + bestTarget._clipWidth = activeItem.presPinClipWidth; + } else if (bestTarget.type === DocumentType.VID) { + bestTarget._currentTimecode = activeItem.presPinTimecode; + } else { + bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s'; + bestTarget._panX = activeItem.presPinViewX; + bestTarget._panY = activeItem.presPinViewY; + bestTarget._viewScale = activeItem.presPinViewScale; + } + this._navTimer = setTimeout(() => bestTarget._viewTransition = undefined, activeItem.presTransition ? NumCast(activeItem.presTransition) + 10 : 510); + }); + } + + /** + * This method makes sure that cursor navigates to the element that + * has the option open and last in the group. + * Design choice: If the next document is not in presCollection or + * presCollection itself then if there is a presCollection it will add + * a new tab. If presCollection is undefined it will open the document + * on the right. + */ + navigateToElement = async (curDoc: Doc) => { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + const srcContext = Cast(targetDoc.context, Doc, null); + const presCollection = Cast(this.layoutDoc.presCollection, Doc, null); + const collectionDocView = presCollection ? DocumentManager.Instance.getDocumentView(presCollection) : undefined; + const includesDoc: boolean = DocListCast(presCollection?.data).includes(targetDoc); + const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === srcContext); + this.turnOffEdit(); + // Handles the setting of presCollection + if (includesDoc) { + //Case 1: Pres collection should not change as it is already the same + } else if (tab !== undefined) { + // Case 2: Pres collection should update + this.layoutDoc.presCollection = srcContext; + } + const presStatus = this.rootDoc.presStatus; + const selViewCache = Array.from(this._selectedArray.keys()); + const dragViewCache = Array.from(this._dragArray); + const eleViewCache = Array.from(this._eleArray); + const self = this; + const resetSelection = action(() => { + const presDocView = DocumentManager.Instance.getDocumentView(self.rootDoc); + if (presDocView) SelectionManager.SelectView(presDocView, false); + self.rootDoc.presStatus = presStatus; + self._selectedArray.clear(); + selViewCache.forEach(doc => self._selectedArray.set(doc, undefined)); + self._dragArray.splice(0, self._dragArray.length, ...dragViewCache); + self._eleArray.splice(0, self._eleArray.length, ...eleViewCache); + }); + const openInTab = (doc: Doc, finished?: () => void) => { + collectionDocView ? collectionDocView.props.addDocTab(doc, "") : this.props.addDocTab(doc, ":left"); + this.layoutDoc.presCollection = targetDoc; + // this still needs some fixing + setTimeout(resetSelection, 500); + if (doc !== targetDoc) { + setTimeout(finished ?? emptyFunction, 100); /// give it some time to create the targetDoc if we're opening up its context + } else { + finished?.(); + } + }; + // If openDocument is selected then it should open the document for the user + if (activeItem.openDocument) { + LightboxView.SetLightboxDoc(targetDoc); + } else if (curDoc.presMovement === PresMovement.Pan && targetDoc) { + LightboxView.SetLightboxDoc(undefined); + await DocumentManager.Instance.jumpToDocument(targetDoc, false, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection); // documents open in new tab instead of on right + } else if ((curDoc.presMovement === PresMovement.Zoom || curDoc.presMovement === PresMovement.Jump) && targetDoc) { + LightboxView.SetLightboxDoc(undefined); + //awaiting jump so that new scale can be found, since jumping is async + await DocumentManager.Instance.jumpToDocument(targetDoc, true, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection); // documents open in new tab instead of on right + } + // After navigating to the document, if it is added as a presPinView then it will + // adjust the pan and scale to that of the pinView when it was added. + if (activeItem.presPinView) { + // if targetDoc is not displayed but one of its aliases is, then we need to modify that alias, not the original target + this.navigateToView(targetDoc, activeItem); + } + // TODO: Add progressivize for navigating web (storing websites for given frames) + + } + + /** + * Uses the viewfinder to progressivize through the different views of a single collection. + * @param presTargetDoc: document for which internal zoom is used + */ + zoomProgressivizeNext = (activeItem: Doc) => { + const targetDoc: Doc = this.targetDoc; + const srcContext = Cast(targetDoc?.context, Doc, null); + const docView = DocumentManager.Instance.getDocumentView(targetDoc); + const vfLeft = this.checkList(targetDoc, activeItem["viewfinder-left-indexed"]); + const vfWidth = this.checkList(targetDoc, activeItem["viewfinder-width-indexed"]); + const vfTop = this.checkList(targetDoc, activeItem["viewfinder-top-indexed"]); + const vfHeight = this.checkList(targetDoc, activeItem["viewfinder-height-indexed"]); + // Case 1: document that is not a Golden Layout tab + if (srcContext) { + const srcDocView = DocumentManager.Instance.getDocumentView(srcContext); + if (srcDocView) { + const layoutdoc = Doc.Layout(targetDoc); + const panelWidth: number = srcDocView.props.PanelWidth(); + const panelHeight: number = srcDocView.props.PanelHeight(); + const newPanX = NumCast(targetDoc.x) + NumCast(layoutdoc._width) / 2; + const newPanY = NumCast(targetDoc.y) + NumCast(layoutdoc._height) / 2; + const newScale = 0.9 * Math.min(Number(panelWidth) / vfWidth, Number(panelHeight) / vfHeight); + srcContext._panX = newPanX + (vfLeft + (vfWidth / 2)); + srcContext._panY = newPanY + (vfTop + (vfHeight / 2)); + srcContext._viewScale = newScale; + } + } + // Case 2: document is the containing collection + if (docView && !srcContext) { + const panelWidth: number = docView.props.PanelWidth(); + const panelHeight: number = docView.props.PanelHeight(); + const newScale = 0.9 * Math.min(Number(panelWidth) / vfWidth, Number(panelHeight) / vfHeight); + targetDoc._panX = vfLeft + (vfWidth / 2); + targetDoc._panY = vfTop + (vfWidth / 2); + targetDoc._viewScale = newScale; + } + const resize = document.getElementById('resizable'); + if (resize) { + resize.style.width = vfWidth + 'px'; + resize.style.height = vfHeight + 'px'; + resize.style.top = vfTop + 'px'; + resize.style.left = vfLeft + 'px'; + } + } + + /** + * For 'Hide Before' and 'Hide After' buttons making sure that + * they are hidden each time the presentation is updated. + */ + @action + onHideDocument = () => { + this.childDocs.forEach((doc, index) => { + const curDoc = Cast(doc, Doc, null); + const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null); + if (tagDoc) tagDoc.opacity = 1; + const itemIndexes: number[] = this.getAllIndexes(this.tagDocs, tagDoc); + const curInd: number = itemIndexes.indexOf(index); + if (tagDoc === this.layoutDoc.presCollection) { tagDoc.opacity = 1; } + else { + if (itemIndexes.length > 1 && curDoc.presHideBefore && curInd !== 0) { } + else if (curDoc.presHideBefore) { + if (index > this.itemIndex) { + tagDoc.opacity = 0; + } else if (!curDoc.presHideAfter) { + tagDoc.opacity = 1; + } + } + if (itemIndexes.length > 1 && curDoc.presHideAfter && curInd !== (itemIndexes.length - 1)) { } + else if (curDoc.presHideAfter) { + if (index < this.itemIndex) { + tagDoc.opacity = 0; + } else if (!curDoc.presHideBefore) { + tagDoc.opacity = 1; + } + } + } + }); + } + + + + //The function that starts or resets presentaton functionally, depending on presStatus of the layoutDoc + @action + startAutoPres = (startSlide: number) => { + this.updateCurrentPresentation(); + let activeItem: Doc = this.activeItem; + let targetDoc: Doc = this.targetDoc; + let duration = NumCast(activeItem.presDuration) + NumCast(activeItem.presTransition); + const timer = (ms: number) => new Promise(res => this._presTimer = setTimeout(res, ms)); + const load = async () => { // Wrap the loop into an async function for this to work + for (var i = startSlide; i < this.childDocs.length; i++) { + activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + duration = NumCast(activeItem.presDuration) + NumCast(activeItem.presTransition); + if (duration < 100) { duration = 2500; } + if (NumCast(targetDoc.lastFrame) > 0) { + for (var f = 0; f < NumCast(targetDoc.lastFrame); f++) { + await timer(duration / NumCast(targetDoc.lastFrame)); + this.next(); + } + } + + await timer(duration); this.next(); // then the created Promise can be awaited + if (i === this.childDocs.length - 1) { + setTimeout(() => { + clearTimeout(this._presTimer); + if (this.layoutDoc.presStatus === 'auto' && !this.layoutDoc.presLoop) this.layoutDoc.presStatus = PresStatus.Manual; + else if (this.layoutDoc.presLoop) this.startAutoPres(0); + }, duration); + } + } + }; + this.layoutDoc.presStatus = PresStatus.Autoplay; + this.startPresentation(startSlide); + this.gotoDocument(startSlide, activeItem); + load(); + } + + @action + pauseAutoPres = () => { + if (this.layoutDoc.presStatus === PresStatus.Autoplay) { + if (this._presTimer) clearTimeout(this._presTimer); + this.layoutDoc.presStatus = PresStatus.Manual; + this.layoutDoc.presLoop = false; + this.childDocs.forEach(this.stopTempMedia); + } + } + + //The function that resets the presentation by removing every action done by it. It also + //stops the presentaton. + resetPresentation = () => { + this.rootDoc._itemIndex = 0; + this.childDocs.map(doc => Cast(doc.presentationTargetDoc, Doc, null)).filter(doc => doc instanceof Doc).forEach(doc => { + try { + doc.opacity = 1; + } catch (e) { + console.log("Reset presentation error: ", e); + } + }); + } + + @action togglePath = (srcContext: Doc, off?: boolean) => { + if (off) { + this._pathBoolean = false; + srcContext.presPathView = false; + } else { + runInAction(() => this._pathBoolean = !this._pathBoolean); + srcContext.presPathView = this._pathBoolean; + } + } + + @action toggleExpandMode = () => { + runInAction(() => this._expandBoolean = !this._expandBoolean); + this.rootDoc.expandBoolean = this._expandBoolean; + this.childDocs.forEach((doc) => { + doc.presExpandInlineButton = this._expandBoolean; + }); + } + + /** + * The function that starts the presentation at the given index, also checking if actions should be applied + * directly at start. + * @param startIndex: index that the presentation will start at + */ + startPresentation = (startIndex: number) => { + this.updateCurrentPresentation(); + this.childDocs.map(doc => { + const tagDoc = doc.presentationTargetDoc as Doc; + if (doc.presHideBefore && this.childDocs.indexOf(doc) > startIndex) { + tagDoc.opacity = 0; + } + if (doc.presHideAfter && this.childDocs.indexOf(doc) < startIndex) { + tagDoc.opacity = 0; + } + }); + } + + /** + * The method called to open the presentation as a minimized view + */ + @action + updateMinimize = () => { + const docView = DocumentManager.Instance.getDocumentView(this.layoutDoc); + if (CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { + console.log("case 1"); + this.layoutDoc.presStatus = PresStatus.Edit; + Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc); + CollectionDockingView.AddSplit(this.rootDoc, "right"); + } else if ((true || this.layoutDoc.context) && docView) { + console.log("case 2"); + this.layoutDoc.presStatus = PresStatus.Edit; + clearTimeout(this._presTimer); + const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + this.rootDoc.x = pt[0] + (this.props.PanelWidth() - 250); + this.rootDoc.y = pt[1] + 10; + this.rootDoc._height = 35; + this.rootDoc._width = 250; + docView.props.removeDocument?.(this.layoutDoc); + Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc); + } else { + console.log("case 3"); + // TODO glr: fix this case + } + } + + /** + * Called when the user changes the view type + * Either 'List' (stacking) or 'Slides' (carousel) + */ + // @undoBatch + viewChanged = action((e: React.ChangeEvent) => { + //@ts-ignore + const viewType = e.target.selectedOptions[0].value as CollectionViewType; + // pivot field may be set by the user in timeline view (or some other way) -- need to reset it here + viewType === CollectionViewType.Stacking && (this.rootDoc._pivotField = undefined); + this.rootDoc._viewType = viewType; + if (viewType === CollectionViewType.Stacking) this.layoutDoc._gridGap = 0; + }); + + /** + * Called when the user changes the view type + * Either 'List' (stacking) or 'Slides' (carousel) + */ + // @undoBatch + mediaStopChanged = action((e: React.ChangeEvent) => { + const activeItem: Doc = this.activeItem; + //@ts-ignore + const stopDoc = e.target.selectedOptions[0].value as string; + const stopDocIndex: number = Number(stopDoc[0]); + activeItem.mediaStopDoc = stopDocIndex; + if (this.childDocs[stopDocIndex - 1].mediaStopTriggerList) { + const list = DocListCast(this.childDocs[stopDocIndex - 1].mediaStopTriggerList); + list.push(activeItem); + // this.childDocs[stopDocIndex - 1].mediaStopTriggerList = list; + console.log(list); + } else { + this.childDocs[stopDocIndex - 1].mediaStopTriggerList = new List<Doc>(); + const list = DocListCast(this.childDocs[stopDocIndex - 1].mediaStopTriggerList); + list.push(activeItem); + // this.childDocs[stopDocIndex - 1].mediaStopTriggerList = list; + console.log(list); + } + }); + + + setMovementName = action((movement: any, activeItem: Doc): string => { + let output: string = 'none'; + switch (movement) { + case PresMovement.Zoom: output = 'Pan & Zoom'; break; //Pan and zoom + case PresMovement.Pan: output = 'Pan'; break; //Pan + case PresMovement.Jump: output = 'Jump cut'; break; //Jump Cut + case PresMovement.None: output = 'None'; break; //None + default: output = 'Zoom'; activeItem.presMovement = 'zoom'; break; //default set as zoom + } + return output; + }); + + whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isChildActive = isActive)); + // For dragging documents into the presentation trail + addDocumentFilter = (docs: Doc[]) => { + docs.forEach((doc, i) => { + if (doc.presentationTargetDoc) return true; + if (doc.type === DocumentType.LABEL) { + const audio = Cast(doc.annotationOn, Doc, null); + if (audio) { + audio.mediaStart = "manual"; + audio.mediaStop = "manual"; + audio.presStartTime = NumCast(doc._timecodeToShow /* audioStart */, NumCast(doc._timecodeToShow /* videoStart */)); + audio.presEndTime = NumCast(doc._timecodeToHide /* audioEnd */, NumCast(doc._timecodeToHide /* videoEnd */)); + audio.presDuration = audio.presStartTime - audio.presEndTime; + TabDocView.PinDoc(audio, { audioRange: true }); + setTimeout(() => this.removeDocument(doc), 0); + return false; + } + } else { + if (!doc.aliasOf) { + const original = Doc.MakeAlias(doc); + TabDocView.PinDoc(original); + setTimeout(() => this.removeDocument(doc), 0); + return false; + } else { + if (!doc.presentationTargetDoc) doc.title = doc.title + " - Slide"; + doc.aliasOf instanceof Doc && (doc.presentationTargetDoc = doc.aliasOf); + doc.presMovement = PresMovement.Zoom; + if (this._expandBoolean) doc.presExpandInlineButton = true; + } + } + }); + return true; + } + childLayoutTemplate = () => this.rootDoc._viewType !== CollectionViewType.Stacking ? undefined : this.presElement; + removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.dataDoc, this.fieldKey, doc); + getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight + panelHeight = () => this.props.PanelHeight() - 40; + isContentActive = (outsideReaction?: boolean) => ((CurrentUserUtils.SelectedTool === InkTool.None && this.props.layerProvider?.(this.layoutDoc) !== false) && + (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false) + + /** + * For sorting the array so that the order is maintained when it is dropped. + */ + @action + sortArray = (): Doc[] => { + return this.childDocs.filter(doc => this._selectedArray.has(doc)); + } + + /** + * Method to get the list of selected items in the order in which they have been selected + */ + @computed get listOfSelected() { + const list = Array.from(this._selectedArray.keys()).map((doc: Doc, index: any) => { + const curDoc = Cast(doc, Doc, null); + const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null); + if (curDoc && curDoc === this.activeItem) return <div key={index} className="selectedList-items"><b>{index + 1}. {curDoc.title}</b></div>; + else if (tagDoc) return <div key={index} className="selectedList-items">{index + 1}. {curDoc.title}</div>; + else if (curDoc) return <div key={index} className="selectedList-items">{index + 1}. {curDoc.title}</div>; + }); + return list; + } + + @action + selectPres = () => { + const presDocView = DocumentManager.Instance.getDocumentView(this.rootDoc); + presDocView && SelectionManager.SelectView(presDocView, false); + } + + //Regular click + @action + selectElement = async (doc: Doc) => { + const context = Cast(doc.context, Doc, null); + this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem); + if (doc.presPinView || doc.presentationTargetDoc === this.layoutDoc.presCollection) setTimeout(() => this.updateCurrentPresentation(context), 0); + else this.updateCurrentPresentation(context); + + if (this.activeItem.setPosition && + this.activeItem.y !== undefined && + this.activeItem.x !== undefined && + this.targetDoc.x !== undefined && + this.targetDoc.y !== undefined) { + const timer = (ms: number) => new Promise(res => this._presTimer = setTimeout(res, ms)); + const time = 10; + const ydiff = NumCast(this.activeItem.y) - NumCast(this.targetDoc.y); + const xdiff = NumCast(this.activeItem.x) - NumCast(this.targetDoc.x); + + for (let i = 0; i < time; i++) { + this.targetDoc.x = NumCast(this.targetDoc.x) + xdiff / time; + this.targetDoc.y = NumCast(this.targetDoc.y) + ydiff / time; + await timer(0.1); + } + } + } + + //Command click + @action + multiSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement) => { + if (!this._selectedArray.has(doc)) { + this._selectedArray.set(doc, undefined); + this._eleArray.push(ref); + this._dragArray.push(drag); + } else { + this._selectedArray.delete(doc); + this.removeFromArray(this._eleArray, doc); + this.removeFromArray(this._dragArray, doc); + } + this.selectPres(); + } + + removeFromArray = (arr: any[], val: any) => { + const index: number = arr.indexOf(val); + const ret: any[] = arr.splice(index, 1); + arr = ret; + } + + //Shift click + @action + shiftSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement) => { + this._selectedArray.clear(); + // const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + if (this.activeItem) { + for (let i = Math.min(this.itemIndex, this.childDocs.indexOf(doc)); i <= Math.max(this.itemIndex, this.childDocs.indexOf(doc)); i++) { + this._selectedArray.set(this.childDocs[i], undefined); + this._eleArray.push(ref); + this._dragArray.push(drag); + } + } + this.selectPres(); + } + + //regular click + @action + regularSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, focus: boolean) => { + this._selectedArray.clear(); + this._selectedArray.set(doc, undefined); + this._eleArray.splice(0, this._eleArray.length, ref); + this._dragArray.splice(0, this._dragArray.length, drag); + focus && this.selectElement(doc); + this.selectPres(); + } + + modifierSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, focus: boolean, cmdClick: boolean, shiftClick: boolean) => { + if (cmdClick) this.multiSelect(doc, ref, drag); + else if (shiftClick) this.shiftSelect(doc, ref, drag); + else this.regularSelect(doc, ref, drag, focus); + } + + static keyEventsWrapper = (e: KeyboardEvent) => { + PresBox.Instance.keyEvents(e); + } + + // Key for when the presentaiton is active + @action + keyEvents = (e: KeyboardEvent) => { + if (e.target instanceof HTMLInputElement) return; + let handled = false; + const anchorNode = document.activeElement as HTMLDivElement; + if (anchorNode && anchorNode.className?.includes("lm_title")) return; + switch (e.key) { + case "Backspace": + if (this.layoutDoc.presStatus === "edit") { + undoBatch(action(() => { + for (const doc of Array.from(this._selectedArray.keys())) { + this.removeDocument(doc); + } + this._selectedArray.clear(); + this._eleArray.length = 0; + this._dragArray.length = 0; + }))(); + handled = true; + } + break; + case "Escape": + if (CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { this.updateMinimize(); } + else if (this.layoutDoc.presStatus === "edit") { this._selectedArray.clear(); this._eleArray.length = this._dragArray.length = 0; } + else this.layoutDoc.presStatus = "edit"; + if (this._presTimer) clearTimeout(this._presTimer); + handled = true; + break; + case "Down": case "ArrowDown": + case "Right": case "ArrowRight": + if (e.shiftKey && this.itemIndex < this.childDocs.length - 1) { // TODO: update to work properly + this.rootDoc._itemIndex = NumCast(this.rootDoc._itemIndex) + 1; + this._selectedArray.set(this.childDocs[this.rootDoc._itemIndex], undefined); + } else { + this.next(); + if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; } + } + handled = true; + break; + case "Up": case "ArrowUp": + case "Left": case "ArrowLeft": + if (e.shiftKey && this.itemIndex !== 0) { // TODO: update to work properly + this.rootDoc._itemIndex = NumCast(this.rootDoc._itemIndex) - 1; + this._selectedArray.set(this.childDocs[this.rootDoc._itemIndex], undefined); + } else { + this.back(); + if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; } + } + handled = true; + break; + case "Spacebar": case " ": + if (this.layoutDoc.presStatus === PresStatus.Manual) this.startAutoPres(this.itemIndex); + else if (this.layoutDoc.presStatus === PresStatus.Autoplay) if (this._presTimer) clearTimeout(this._presTimer); + handled = true; + break; + case "a": + if ((e.metaKey || e.altKey) && this.layoutDoc.presStatus === "edit") { + this._selectedArray.clear(); + this.childDocs.forEach(doc => this._selectedArray.set(doc, undefined)); + handled = true; + } + default: + break; + } + if (handled) { + e.stopPropagation(); + e.preventDefault(); + } + } + + /** + * + */ + @action + viewPaths = () => { + const srcContext = Cast(this.rootDoc.presCollection, Doc, null); + if (srcContext) { + this.togglePath(srcContext); + } + } + + getAllIndexes = (arr: Doc[], val: Doc): number[] => { + const indexes = []; + for (let i = 0; i < arr.length; i++) { + arr[i] === val && indexes.push(i); + } + return indexes; + } + + // Adds the index in the pres path graphically + @computed get order() { + const order: JSX.Element[] = []; + const docs: Doc[] = []; + const presCollection = Cast(this.rootDoc.presCollection, Doc, null); + const dv = DocumentManager.Instance.getDocumentView(presCollection); + this.childDocs.filter(doc => Cast(doc.presentationTargetDoc, Doc, null)).forEach((doc, index) => { + const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); + const srcContext = Cast(tagDoc.context, Doc, null); + const width = NumCast(tagDoc._width) / 10; + const height = Math.max(NumCast(tagDoc._height) / 10, 15); + const edge = Math.max(width, height); + const fontSize = edge * 0.8; + const gap = 2; + if (presCollection === srcContext) { + // Case A: Document is contained within the collection + if (docs.includes(tagDoc)) { + const prevOccurances: number = this.getAllIndexes(docs, tagDoc).length; + docs.push(tagDoc); + order.push( + <div className="pathOrder" + key={tagDoc.id + 'pres' + index} + style={{ top: NumCast(tagDoc.y) + (prevOccurances * (edge + gap) - (edge / 2)), left: NumCast(tagDoc.x) - (edge / 2), width: edge, height: edge, fontSize: fontSize }} + onClick={() => this.selectElement(doc)}> + <div className="pathOrder-frame">{index + 1}</div> + </div>); + } else { + docs.push(tagDoc); + order.push( + <div className="pathOrder" + key={tagDoc.id + 'pres' + index} + style={{ top: NumCast(tagDoc.y) - (edge / 2), left: NumCast(tagDoc.x) - (edge / 2), width: edge, height: edge, fontSize: fontSize }} + onClick={() => this.selectElement(doc)}> + <div className="pathOrder-frame">{index + 1}</div> + </div>); + } + } else if (doc.presPinView && presCollection === tagDoc && dv) { + // Case B: Document is presPinView and is presCollection + const scale: number = 1 / NumCast(doc.presPinViewScale); + const height: number = dv.props.PanelHeight() * scale; + const width: number = dv.props.PanelWidth() * scale; + const indWidth = width / 10; + const indHeight = Math.max(height / 10, 15); + const indEdge = Math.max(indWidth, indHeight); + const indFontSize = indEdge * 0.8; + const xLoc: number = NumCast(doc.presPinViewX) - (width / 2); + const yLoc: number = NumCast(doc.presPinViewY) - (height / 2); + docs.push(tagDoc); + order.push( + <> + <div className="pathOrder" + key={tagDoc.id + 'pres' + index} + style={{ top: yLoc - (indEdge / 2), left: xLoc - (indEdge / 2), width: indEdge, height: indEdge, fontSize: indFontSize }} + onClick={() => this.selectElement(doc)} + > + <div className="pathOrder-frame">{index + 1}</div> + </div> + <div className="pathOrder-presPinView" style={{ top: yLoc, left: xLoc, width: width, height: height, borderWidth: indEdge / 10 }}></div> + </>); + } + }); + return order; + } + + /** + * Method called for viewing paths which adds a single line with + * points at the center of each document added. + * Design choice: When this is called it sets _fitToBox as true so the + * user can have an overview of all of the documents in the collection. + * (Design needed for when documents in presentation trail are in another + * collection) + */ + @computed get paths() { + let pathPoints = ""; + const presCollection = Cast(this.rootDoc.presCollection, Doc, null); + this.childDocs.forEach((doc, index) => { + const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); + const srcContext = Cast(tagDoc?.context, Doc, null); + if (tagDoc && presCollection === srcContext) { + const n1x = NumCast(tagDoc.x) + (NumCast(tagDoc._width) / 2); + const n1y = NumCast(tagDoc.y) + (NumCast(tagDoc._height) / 2); + if (index = 0) pathPoints = n1x + "," + n1y; + else pathPoints = pathPoints + " " + n1x + "," + n1y; + } else if (doc.presPinView && presCollection === tagDoc) { + const n1x = NumCast(doc.presPinViewX); + const n1y = NumCast(doc.presPinViewY); + if (index = 0) pathPoints = n1x + "," + n1y; + else pathPoints = pathPoints + " " + n1x + "," + n1y; + } + }); + return (<polyline + points={pathPoints} + style={{ + opacity: 1, + stroke: "#69a6db", + strokeWidth: 5, + strokeDasharray: '10 5', + boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.25)', + }} + fill="none" + markerStart="url(#markerArrow)" + markerMid="url(#markerSquare)" + markerEnd="url(#markerSquareFilled)" + />); + } + + // Converts seconds to ms and updates presTransition + setTransitionTime = (number: String, change?: number) => { + let timeInMS = Number(number) * 1000; + if (change) timeInMS += change; + if (timeInMS < 100) timeInMS = 100; + if (timeInMS > 10000) timeInMS = 10000; + Array.from(this._selectedArray.keys()).forEach((doc) => doc.presTransition = timeInMS); + } + + // Converts seconds to ms and updates presDuration + setDurationTime = (number: String, change?: number) => { + let timeInMS = Number(number) * 1000; + if (change) timeInMS += change; + if (timeInMS < 100) timeInMS = 100; + if (timeInMS > 20000) timeInMS = 20000; + Array.from(this._selectedArray.keys()).forEach((doc) => doc.presDuration = timeInMS); + } + + /** + * When the movement dropdown is changes + */ + @undoBatch + updateMovement = action((movement: any, all?: boolean) => { + const array: any[] = all ? this.childDocs : Array.from(this._selectedArray.keys()); + array.forEach((doc) => { + switch (movement) { + case PresMovement.Zoom: //Pan and zoom + doc.presMovement = PresMovement.Zoom; + break; + case PresMovement.Pan: //Pan + doc.presMovement = PresMovement.Pan; + break; + case PresMovement.Jump: //Jump Cut + doc.presJump = true; + doc.presMovement = PresMovement.Jump; + break; + case PresMovement.None: default: + doc.presMovement = PresMovement.None; + break; + } + }); + }); + + @undoBatch + @action + updateHideBefore = (activeItem: Doc) => { + activeItem.presHideBefore = !activeItem.presHideBefore; + Array.from(this._selectedArray.keys()).forEach((doc) => doc.presHideBefore = activeItem.presHideBefore); + } + + @undoBatch + @action + updateHideAfter = (activeItem: Doc) => { + activeItem.presHideAfter = !activeItem.presHideAfter; + Array.from(this._selectedArray.keys()).forEach((doc) => doc.presHideAfter = activeItem.presHideAfter); + } + + @undoBatch + @action + updateOpenDoc = (activeItem: Doc) => { + activeItem.openDocument = !activeItem.openDocument; + Array.from(this._selectedArray.keys()).forEach((doc) => { + doc.openDocument = activeItem.openDocument; + }); + } + + @undoBatch + @action + updateEffectDirection = (effect: any, all?: boolean) => { + const array: any[] = all ? this.childDocs : Array.from(this._selectedArray.keys()); + array.forEach((doc) => { + const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); + switch (effect) { + case PresEffect.Left: + tagDoc.presEffectDirection = PresEffect.Left; + break; + case PresEffect.Right: + tagDoc.presEffectDirection = PresEffect.Right; + break; + case PresEffect.Top: + tagDoc.presEffectDirection = PresEffect.Top; + break; + case PresEffect.Bottom: + tagDoc.presEffectDirection = PresEffect.Bottom; + break; + case PresEffect.Center: default: + tagDoc.presEffectDirection = PresEffect.Center; + break; + } + }); + } + + @undoBatch + @action + updateEffect = (effect: any, all?: boolean) => { + const array: any[] = all ? this.childDocs : Array.from(this._selectedArray.keys()); + array.forEach((doc) => { + const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); + switch (effect) { + case PresEffect.Bounce: + tagDoc.presEffect = PresEffect.Bounce; + break; + case PresEffect.Fade: + tagDoc.presEffect = PresEffect.Fade; + break; + case PresEffect.Flip: + tagDoc.presEffect = PresEffect.Flip; + break; + case PresEffect.Roll: + tagDoc.presEffect = PresEffect.Roll; + break; + case PresEffect.Rotate: + tagDoc.presEffect = PresEffect.Rotate; + break; + case PresEffect.None: default: + tagDoc.presEffect = PresEffect.None; + break; + } + }); + } + + _batch: UndoManager.Batch | undefined = undefined; + + @computed get transitionDropdown() { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + const type = targetDoc.type; + const isPresCollection: boolean = (targetDoc === this.layoutDoc.presCollection); + const isPinWithView: boolean = BoolCast(activeItem.presPinView); + if (activeItem && targetDoc) { + const transitionSpeed = activeItem.presTransition ? NumCast(activeItem.presTransition) / 1000 : 0.5; + let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 2; + if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration); + const effect = targetDoc.presEffect ? targetDoc.presEffect : 'None'; + activeItem.presMovement = activeItem.presMovement ? activeItem.presMovement : 'Zoom'; + return ( + <div className={`presBox-ribbon ${this.transitionTools && this.layoutDoc.presStatus === PresStatus.Edit ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onClick={action(e => { e.stopPropagation(); this.openMovementDropdown = false; this.openEffectDropdown = false; })}> + <div className="ribbon-box"> + Movement + {isPresCollection || (isPresCollection && isPinWithView) ? + <div className="ribbon-property" style={{ marginLeft: 0, height: 25, textAlign: 'left', paddingLeft: 5, paddingRight: 5, fontSize: 10 }}> + {this.scrollable ? "Scroll to pinned view" : !isPinWithView ? "No movement" : "Pan & Zoom to pinned view"} + </div> + : + <div className="presBox-dropdown" onClick={action(e => { e.stopPropagation(); this.openMovementDropdown = !this.openMovementDropdown; })} style={{ borderBottomLeftRadius: this.openMovementDropdown ? 0 : 5, border: this.openMovementDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}> + {this.setMovementName(activeItem.presMovement, activeItem)} + <FontAwesomeIcon className='presBox-dropdownIcon' style={{ gridColumn: 2, color: this.openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={"angle-down"} /> + <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} onPointerDown={e => e.stopPropagation()} style={{ display: this.openMovementDropdown ? "grid" : "none" }}> + <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.None ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.None)}>None</div> + <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Zoom ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Zoom)}>Pan {"&"} Zoom</div> + <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Pan ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Pan)}>Pan</div> + <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Jump ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Jump)}>Jump cut</div> + </div> + </div> + } + <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "inline-flex" : "none" }}> + <div className="presBox-subheading">Movement Speed</div> + <div className="ribbon-property"> + <input className="presBox-input" + type="number" value={transitionSpeed} + onChange={action((e) => this.setTransitionTime(e.target.value))} /> s + </div> + <div className="ribbon-propertyUpDown"> + <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setTransitionTime(String(transitionSpeed), 1000))}> + <FontAwesomeIcon icon={"caret-up"} /> + </div> + <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setTransitionTime(String(transitionSpeed), -1000))}> + <FontAwesomeIcon icon={"caret-down"} /> + </div> + </div> + </div> + <input type="range" step="0.1" min="0.1" max="10" value={transitionSpeed} + className={`toolbar-slider ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "" : "none"}`} + id="toolbar-slider" + onPointerDown={() => this._batch = UndoManager.StartBatch("presTransition")} + onPointerUp={() => this._batch?.end()} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => { + e.stopPropagation(); + this.setTransitionTime(e.target.value); + }} /> + <div className={`slider-headers ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "" : "none"}`}> + <div className="slider-text">Fast</div> + <div className="slider-text">Medium</div> + <div className="slider-text">Slow</div> + </div> + </div> + <div className="ribbon-box"> + Visibility {"&"} Duration + <div className="ribbon-doubleButton"> + {isPresCollection ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Hide before presented"}</div></>}><div className={`ribbon-toggle ${activeItem.presHideBefore ? "active" : ""}`} onClick={() => this.updateHideBefore(activeItem)}>Hide before</div></Tooltip>} + {isPresCollection ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Hide after presented"}</div></>}><div className={`ribbon-toggle ${activeItem.presHideAfter ? "active" : ""}`} onClick={() => this.updateHideAfter(activeItem)}>Hide after</div></Tooltip>} + <Tooltip title={<><div className="dash-tooltip">{"Open in lightbox view"}</div></>}><div className="ribbon-toggle" style={{ backgroundColor: activeItem.openDocument ? Colors.LIGHT_BLUE : "" }} onClick={() => this.updateOpenDoc(activeItem)}>Lightbox</div></Tooltip> + </div> + {(type === DocumentType.AUDIO || type === DocumentType.VID) ? (null) : <> + <div className="ribbon-doubleButton" > + <div className="presBox-subheading">Slide Duration</div> + <div className="ribbon-property"> + <input className="presBox-input" + type="number" value={duration} + onChange={action((e) => this.setDurationTime(e.target.value))} /> s + </div> + <div className="ribbon-propertyUpDown"> + <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setDurationTime(String(duration), 1000))}> + <FontAwesomeIcon icon={"caret-up"} /> + </div> + <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setDurationTime(String(duration), -1000))}> + <FontAwesomeIcon icon={"caret-down"} /> + </div> + </div> + </div> + <input type="range" step="0.1" min="0.1" max="20" value={duration} + style={{ display: targetDoc.type === DocumentType.AUDIO ? "none" : "block" }} + className={"toolbar-slider"} id="duration-slider" + onPointerDown={() => { this._batch = UndoManager.StartBatch("presDuration"); }} + onPointerUp={() => { if (this._batch) this._batch.end(); }} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => { e.stopPropagation(); this.setDurationTime(e.target.value); }} + /> + <div className={"slider-headers"} style={{ display: targetDoc.type === DocumentType.AUDIO ? "none" : "grid" }}> + <div className="slider-text">Short</div> + <div className="slider-text">Medium</div> + <div className="slider-text">Long</div> + </div> + </>} + </div> + {isPresCollection ? (null) : <div className="ribbon-box"> + Effects + <div className="presBox-dropdown" onClick={action(e => { e.stopPropagation(); this.openEffectDropdown = !this.openEffectDropdown; })} style={{ borderBottomLeftRadius: this.openEffectDropdown ? 0 : 5, border: this.openEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}> + {effect} + <FontAwesomeIcon className='presBox-dropdownIcon' style={{ gridColumn: 2, color: this.openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={"angle-down"} /> + <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} style={{ display: this.openEffectDropdown ? "grid" : "none" }} onPointerDown={e => e.stopPropagation()}> + <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.None || !targetDoc.presEffect ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.None)}>None</div> + <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.Fade ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Fade)}>Fade In</div> + <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.Flip ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Flip)}>Flip</div> + <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.Rotate ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Rotate)}>Rotate</div> + <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.Bounce ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Bounce)}>Bounce</div> + <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.Roll ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Roll)}>Roll</div> + </div> + </div> + <div className="ribbon-doubleButton" style={{ display: effect === 'None' ? "none" : "inline-flex" }}> + <div className="presBox-subheading" >Effect direction</div> + <div className="ribbon-property"> + {this.effectDirection} + </div> + </div> + <div className="effectDirection" style={{ display: effect === 'None' ? "none" : "grid", width: 40 }}> + <Tooltip title={<><div className="dash-tooltip">{"Enter from left"}</div></>}><div style={{ gridColumn: 1, gridRow: 2, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Left ? Colors.LIGHT_BLUE : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Left)}><FontAwesomeIcon icon={"angle-right"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from right"}</div></>}><div style={{ gridColumn: 3, gridRow: 2, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Right ? Colors.LIGHT_BLUE : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Right)}><FontAwesomeIcon icon={"angle-left"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from top"}</div></>}><div style={{ gridColumn: 2, gridRow: 1, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Top ? Colors.LIGHT_BLUE : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Top)}><FontAwesomeIcon icon={"angle-down"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from bottom"}</div></>}><div style={{ gridColumn: 2, gridRow: 3, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Bottom ? Colors.LIGHT_BLUE : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Bottom)}><FontAwesomeIcon icon={"angle-up"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from center"}</div></>}><div style={{ gridColumn: 2, gridRow: 2, width: 10, height: 10, alignSelf: 'center', justifySelf: 'center', border: targetDoc.presEffectDirection === PresEffect.Center || !targetDoc.presEffectDirection ? `solid 2px ${Colors.LIGHT_BLUE}` : "solid 2px black", borderRadius: "100%", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Center)}></div></Tooltip> + </div> + </div>} + <div className="ribbon-final-box"> + <div className="ribbon-final-button-hidden" onClick={() => this.applyTo(this.childDocs)}> + Apply to all + </div> + </div> + </div > + ); + } + } + + @computed get effectDirection(): string { + let effect = ''; + switch (this.targetDoc.presEffectDirection) { + case 'left': effect = "Enter from left"; break; + case 'right': effect = "Enter from right"; break; + case 'top': effect = "Enter from top"; break; + case 'bottom': effect = "Enter from bottom"; break; + default: effect = "Enter from center"; break; + } + return effect; + } + + @undoBatch + @action + applyTo = (array: Doc[]) => { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + this.updateMovement(activeItem.presMovement, true); + this.updateEffect(targetDoc.presEffect, true); + this.updateEffectDirection(targetDoc.presEffectDirection, true); + array.forEach((doc) => { + const curDoc = Cast(doc, Doc, null); + const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null); + if (tagDoc && targetDoc) { + curDoc.presTransition = activeItem.presTransition; + curDoc.presDuration = activeItem.presDuration; + curDoc.presHideBefore = activeItem.presHideBefore; + curDoc.presHideAfter = activeItem.presHideAfter; + } + }); + } + + @computed get presPinViewOptionsDropdown() { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ margin: "auto", width: 16, filter: 'invert(1)' }} />; + return ( + <> + {this.panable || this.scrollable || this.targetDoc.type === DocumentType.COMPARISON ? 'Pinned view' : (null)} + <div className="ribbon-doubleButton"> + <Tooltip title={<><div className="dash-tooltip">{activeItem.presPinView ? "Turn off pin with view" : "Turn on pin with view"}</div></>}><div className="ribbon-toggle" style={{ width: 20, padding: 0, backgroundColor: activeItem.presPinView ? Colors.LIGHT_BLUE : "" }} + onClick={() => { + activeItem.presPinView = !activeItem.presPinView; + targetDoc.presPinView = activeItem.presPinView; + if (activeItem.presPinView) { + if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.RTF || targetDoc.type === DocumentType.WEB || targetDoc._viewType === CollectionViewType.Stacking) { + const scroll = targetDoc._scrollTop; + activeItem.presPinView = true; + activeItem.presPinViewScroll = scroll; + } else if (targetDoc.type === DocumentType.VID) { + activeItem.presPinTimecode = targetDoc._currentTimecode; + } else if ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG) { + const x = targetDoc._panX; + const y = targetDoc._panY; + const scale = targetDoc._viewScale; + activeItem.presPinView = true; + activeItem.presPinViewX = x; + activeItem.presPinViewY = y; + activeItem.presPinViewScale = scale; + } else if (targetDoc.type === DocumentType.COMPARISON) { + const width = targetDoc._clipWidth; + activeItem.presPinClipWidth = width; + activeItem.presPinView = true; + } + } + }}>{presPinWithViewIcon}</div></Tooltip> + {activeItem.presPinView ? <Tooltip title={<><div className="dash-tooltip">{"Update the pinned view with the view of the selected document"}</div></>}><div className="ribbon-button" + onClick={() => { + if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF) { + const scroll = targetDoc._scrollTop; + activeItem.presPinViewScroll = scroll; + } else if (targetDoc.type === DocumentType.VID) { + activeItem.presPinTimecode = targetDoc._currentTimecode; + } else if (targetDoc.type === DocumentType.COMPARISON) { + const clipWidth = targetDoc._clipWidth; + activeItem.presPinClipWidth = clipWidth; + } else { + const x = targetDoc._panX; + const y = targetDoc._panY; + const scale = targetDoc._viewScale; + activeItem.presPinViewX = x; + activeItem.presPinViewY = y; + activeItem.presPinViewScale = scale; + } + }}>Update</div></Tooltip> : (null)} + </div> + </> + ); + } + + @computed get panOptionsDropdown() { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + return ( + <> + {this.panable ? <div style={{ display: activeItem.presPinView ? "block" : "none" }}> + <div className="ribbon-doubleButton" style={{ marginRight: 10 }}> + <div className="presBox-subheading">Pan X</div> + <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}> + <input className="presBox-input" + style={{ textAlign: 'left', width: 50 }} + type="number" value={NumCast(activeItem.presPinViewX)} + onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presPinViewX = Number(val); })} /> + </div> + </div> + <div className="ribbon-doubleButton" style={{ marginRight: 10 }}> + <div className="presBox-subheading">Pan Y</div> + <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}> + <input className="presBox-input" + style={{ textAlign: 'left', width: 50 }} + type="number" value={NumCast(activeItem.presPinViewY)} + onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presPinViewY = Number(val); })} /> + </div> + </div> + <div className="ribbon-doubleButton" style={{ marginRight: 10 }}> + <div className="presBox-subheading">Scale</div> + <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}> + <input className="presBox-input" + style={{ textAlign: 'left', width: 50 }} + type="number" value={NumCast(activeItem.presPinViewScale)} + onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presPinViewScale = Number(val); })} /> + </div> + </div> + </div> : (null)} + </> + ); + } + + @computed get scrollOptionsDropdown() { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + return ( + <> + {this.scrollable ? <div style={{ display: activeItem.presPinView ? "block" : "none" }}> + <div className="ribbon-doubleButton" style={{ marginRight: 10 }}> + <div className="presBox-subheading">Scroll</div> + <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}> + <input className="presBox-input" + style={{ textAlign: 'left', width: 50 }} + type="number" value={NumCast(activeItem.presPinViewScroll)} + onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presPinViewScroll = Number(val); })} /> + </div> + </div> + </div> : (null)} + </> + ); + } + + @computed get mediaStopSlides() { + const activeItem: Doc = this.activeItem; + const list = this.childDocs.map((doc, i) => { + if (i > this.itemIndex) { + return ( + <option>{i + 1}. {doc.title}</option> + ); + } + }); + return ( + list + ); + } + + @computed get mediaOptionsDropdown() { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + const duration = Math.round(NumCast(activeItem[`${Doc.LayoutFieldKey(activeItem)}-duration`]) * 10); + const mediaStopDocInd: number = NumCast(activeItem.mediaStopDoc); + const mediaStopDocStr: string = mediaStopDocInd ? mediaStopDocInd + ". " + this.childDocs[mediaStopDocInd - 1].title : ""; + if (activeItem && targetDoc) { + return ( + <div> + <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> + <div> + <div className="ribbon-box"> + Start {"&"} End Time + <div className={"slider-headers"}> + <div className="slider-block" > + <div className="slider-text" style={{ fontWeight: 500 }}> + Start time (s) + </div> + <div id={"startTime"} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}> + <input className="presBox-input" + style={{ textAlign: 'center', width: 30, height: 15, fontSize: 10 }} + type="number" value={NumCast(activeItem.presStartTime)} + onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { activeItem.presStartTime = Number(e.target.value); })} + /> + </div> + </div> + <div className="slider-block"> + <div className="slider-text" style={{ fontWeight: 500 }}> + Duration (s) + </div> + <div className="slider-number" style={{ backgroundColor: Colors.LIGHT_BLUE }}> + {Math.round((NumCast(activeItem.presEndTime) - NumCast(activeItem.presStartTime)) * 10) / 10} + </div> + </div> + <div className="slider-block"> + <div className="slider-text" style={{ fontWeight: 500 }}> + End time (s) + </div> + <div id={"endTime"} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}> + <input className="presBox-input" + style={{ textAlign: 'center', width: 30, height: 15, fontSize: 10 }} + type="number" value={NumCast(activeItem.presEndTime)} + onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { activeItem.presEndTime = Number(e.target.value); })} + /> + </div> + </div> + </div> + <div className="multiThumb-slider"> + <input type="range" step="0.1" min="0" max={duration / 10} value={NumCast(activeItem.presEndTime)} + style={{ gridColumn: 1, gridRow: 1 }} + className={`toolbar-slider ${"end"}`} + id="toolbar-slider" + onPointerDown={() => { + this._batch = UndoManager.StartBatch("presEndTime"); + const endBlock = document.getElementById("endTime"); + if (endBlock) { + endBlock.style.color = Colors.LIGHT_GRAY; + endBlock.style.backgroundColor = Colors.MEDIUM_BLUE; + } + }} + onPointerUp={() => { + this._batch?.end(); + const endBlock = document.getElementById("endTime"); + if (endBlock) { + endBlock.style.color = Colors.BLACK; + endBlock.style.backgroundColor = Colors.LIGHT_GRAY; + } + }} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => { + e.stopPropagation(); + activeItem.presEndTime = Number(e.target.value); + }} /> + <input type="range" step="0.1" min="0" max={duration / 10} value={NumCast(activeItem.presStartTime)} + style={{ gridColumn: 1, gridRow: 1 }} + className={`toolbar-slider ${"start"}`} + id="toolbar-slider" + onPointerDown={() => { + this._batch = UndoManager.StartBatch("presStartTime"); + const startBlock = document.getElementById("startTime"); + if (startBlock) { + startBlock.style.color = Colors.LIGHT_GRAY; + startBlock.style.backgroundColor = Colors.MEDIUM_BLUE; + } + }} + onPointerUp={() => { + this._batch?.end(); + const startBlock = document.getElementById("startTime"); + if (startBlock) { + startBlock.style.color = Colors.BLACK; + startBlock.style.backgroundColor = Colors.LIGHT_GRAY; + } + }} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => { + e.stopPropagation(); + activeItem.presStartTime = Number(e.target.value); + }} /> + </div> + <div className={`slider-headers ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "" : "none"}`}> + <div className="slider-text">0 s</div> + <div className="slider-text"></div> + <div className="slider-text">{duration / 10} s</div> + </div> + </div> + <div className="ribbon-final-box"> + Playback + <div className="presBox-subheading">Start playing:</div> + <div className="presBox-radioButtons"> + <div className="checkbox-container"> + <input className="presBox-checkbox" + type="checkbox" + onChange={() => activeItem.mediaStart = "manual"} + checked={activeItem.mediaStart === "manual"} + /> + <div>On click</div> + </div> + <div className="checkbox-container"> + <input className="presBox-checkbox" + type="checkbox" + onChange={() => activeItem.mediaStart = "auto"} + checked={activeItem.mediaStart === "auto"} + /> + <div>Automatically</div> + </div> + </div> + <div className="presBox-subheading">Stop playing:</div> + <div className="presBox-radioButtons"> + <div className="checkbox-container"> + <input className="presBox-checkbox" + type="checkbox" + onChange={() => activeItem.mediaStop = "manual"} + checked={activeItem.mediaStop === "manual"} + /> + <div>At audio end time</div> + </div> + <div className="checkbox-container"> + <input className="presBox-checkbox" + type="checkbox" + onChange={() => activeItem.mediaStop = "auto"} + checked={activeItem.mediaStop === "auto"} + /> + <div>On slide change</div> + </div> + {/* <div className="checkbox-container"> + <input className="presBox-checkbox" + type="checkbox" + onChange={() => activeItem.mediaStop = "afterSlide"} + checked={activeItem.mediaStop === "afterSlide"} + /> + <div className="checkbox-dropdown"> + After chosen slide + <select className="presBox-viewPicker" + style={{ opacity: activeItem.mediaStop === "afterSlide" && this.itemIndex !== this.childDocs.length - 1 ? 1 : 0.3 }} + onPointerDown={e => e.stopPropagation()} + onChange={this.mediaStopChanged} + value={mediaStopDocStr}> + {this.mediaStopSlides} + </select> + </div> + </div> */} + </div> + </div> + </div> + </div> + </div > + ); + } + } + + @computed get newDocumentToolbarDropdown() { + return ( + <div> + <div className={'presBox-toolbar-dropdown'} style={{ display: this.newDocumentTools && this.layoutDoc.presStatus === "edit" ? "inline-flex" : "none" }} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> + <div className="layout-container" style={{ height: 'max-content' }}> + <div className="layout" style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => { this.layout = 'blank'; this.createNewSlide(this.layout); })} /> + <div className="layout" style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => { this.layout = 'title'; this.createNewSlide(this.layout); })}> + <div className="title">Title</div> + <div className="subtitle">Subtitle</div> + </div> + <div className="layout" style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => { this.layout = 'header'; this.createNewSlide(this.layout); })}> + <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}>Section header</div> + </div> + <div className="layout" style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => { this.layout = 'content'; this.createNewSlide(this.layout); })}> + <div className="title" style={{ alignSelf: 'center' }}>Title</div> + <div className="content">Text goes here</div> + </div> + </div> + </div> + </div > + ); + } + + @observable openLayouts: boolean = false; + @observable addFreeform: boolean = true; + @observable layout: string = ""; + @observable title: string = ""; + + @computed get newDocumentDropdown() { + return ( + <div> + <div className={"presBox-ribbon"} onClick={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> + <div className="ribbon-box"> + Slide Title: <br></br> + <input className="ribbon-textInput" placeholder="..." type="text" name="fname" + onChange={(e) => { + e.stopPropagation(); + e.preventDefault(); + runInAction(() => this.title = e.target.value); + }}> + </input> + </div> + <div className="ribbon-box"> + Choose type: + <div className="ribbon-doubleButton"> + <div title="Text" className={'ribbon-toggle'} style={{ background: this.addFreeform ? "" : Colors.LIGHT_BLUE }} onClick={action(() => this.addFreeform = !this.addFreeform)}>Text</div> + <div title="Freeform" className={'ribbon-toggle'} style={{ background: this.addFreeform ? Colors.LIGHT_BLUE : "" }} onClick={action(() => this.addFreeform = !this.addFreeform)}>Freeform</div> + </div> + </div> + <div className="ribbon-box" style={{ display: this.addFreeform ? "grid" : "none" }}> + Preset layouts: + <div className="layout-container" style={{ height: this.openLayouts ? 'max-content' : '75px' }}> + <div className="layout" style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => this.layout = 'blank')} /> + <div className="layout" style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => this.layout = 'title')}> + <div className="title">Title</div> + <div className="subtitle">Subtitle</div> + </div> + <div className="layout" style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => this.layout = 'header')}> + <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}>Section header</div> + </div> + <div className="layout" style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => this.layout = 'content')}> + <div className="title" style={{ alignSelf: 'center' }}>Title</div> + <div className="content">Text goes here</div> + </div> + <div className="layout" style={{ border: this.layout === 'twoColumns' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => this.layout = 'twoColumns')}> + <div className="title" style={{ alignSelf: 'center', gridColumn: '1/3' }}>Title</div> + <div className="content" style={{ gridColumn: 1, gridRow: 2 }}>Column one text</div> + <div className="content" style={{ gridColumn: 2, gridRow: 2 }}>Column two text</div> + </div> + </div> + <div className="open-layout" onClick={action(() => this.openLayouts = !this.openLayouts)}> + <FontAwesomeIcon style={{ transition: 'all 0.3s', transform: this.openLayouts ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={"caret-down"} size={"lg"} /> + </div> + </div> + <div className="ribbon-final-box"> + <div className={this.title !== "" && (this.addFreeform && this.layout !== "" || !this.addFreeform) ? "ribbon-final-button-hidden" : "ribbon-final-button"} onClick={() => this.createNewSlide(this.layout, this.title, this.addFreeform)}> + Create New Slide + </div> + </div> + </div> + </div > + ); + } + + createNewSlide = (layout?: string, title?: string, freeform?: boolean) => { + let doc = undefined; + if (layout) doc = this.createTemplate(layout); + if (freeform && layout) doc = this.createTemplate(layout, title); + if (!freeform && !layout) doc = Docs.Create.TextDocument("", { _nativeWidth: 400, _width: 225, title: title }); + if (doc) { + const presCollection = Cast(this.layoutDoc.presCollection, Doc, null); + const data = Cast(presCollection?.data, listSpec(Doc)); + const presData = Cast(this.rootDoc.data, listSpec(Doc)); + if (data && presData) { + data.push(doc); + TabDocView.PinDoc(doc); + this.gotoDocument(this.childDocs.length, this.activeItem); + } else { + this.props.addDocTab(doc, "add:right"); + } + } + } + + createTemplate = (layout: string, input?: string) => { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + let x = 0; + let y = 0; + if (activeItem && targetDoc) { + x = NumCast(targetDoc.x); + y = NumCast(targetDoc.y) + NumCast(targetDoc._height) + 20; + } + let doc = undefined; + const title = Docs.Create.TextDocument("Click to change title", { title: "Slide title", _width: 380, _height: 60, x: 10, y: 58, _fontSize: "24pt", }); + const subtitle = Docs.Create.TextDocument("Click to change subtitle", { title: "Slide subtitle", _width: 380, _height: 50, x: 10, y: 118, _fontSize: "16pt" }); + const header = Docs.Create.TextDocument("Click to change header", { title: "Slide header", _width: 380, _height: 65, x: 10, y: 80, _fontSize: "20pt" }); + const contentTitle = Docs.Create.TextDocument("Click to change title", { title: "Slide title", _width: 380, _height: 60, x: 10, y: 10, _fontSize: "24pt" }); + const content = Docs.Create.TextDocument("Click to change text", { title: "Slide text", _width: 380, _height: 145, x: 10, y: 70, _fontSize: "14pt" }); + const content1 = Docs.Create.TextDocument("Click to change text", { title: "Column 1", _width: 185, _height: 140, x: 10, y: 80, _fontSize: "14pt" }); + const content2 = Docs.Create.TextDocument("Click to change text", { title: "Column 2", _width: 185, _height: 140, x: 205, y: 80, _fontSize: "14pt" }); + switch (layout) { + case 'blank': + doc = Docs.Create.FreeformDocument([], { title: input ? input : "Blank slide", _width: 400, _height: 225, x: x, y: y }); + break; + case 'title': + doc = Docs.Create.FreeformDocument([title, subtitle], { title: input ? input : "Title slide", _width: 400, _height: 225, _fitToBox: true, x: x, y: y }); + break; + case 'header': + doc = Docs.Create.FreeformDocument([header], { title: input ? input : "Section header", _width: 400, _height: 225, _fitToBox: true, x: x, y: y }); + break; + case 'content': + doc = Docs.Create.FreeformDocument([contentTitle, content], { title: input ? input : "Title and content", _width: 400, _height: 225, _fitToBox: true, x: x, y: y }); + break; + case 'twoColumns': + doc = Docs.Create.FreeformDocument([contentTitle, content1, content2], { title: input ? input : "Title and two columns", _width: 400, _height: 225, _fitToBox: true, x: x, y: y }); + break; + default: + break; + } + return doc; + } + + // Dropdown that appears when the user wants to begin presenting (either minimize or sidebar view) + @computed get presentDropdown() { + return ( + <div className={`dropdown-play ${this.presentTools ? "active" : ""}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> + <div className="dropdown-play-button" onClick={undoBatch(action(() => { this.updateMinimize(); this.turnOffEdit(true); this.gotoDocument(this.itemIndex, this.activeItem); }))}> + Mini-player + </div> + <div className="dropdown-play-button" onClick={undoBatch(action(() => { this.layoutDoc.presStatus = "manual"; this.turnOffEdit(true); this.gotoDocument(this.itemIndex, this.activeItem); }))}> + Sidebar player + </div> + </div> + ); + } + + // Case in which the document has keyframes to navigate to next key frame + @action + nextKeyframe = (tagDoc: Doc, curDoc: Doc): void => { + const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]); + const currentFrame = Cast(tagDoc._currentFrame, "number", null); + if (currentFrame === undefined) { + tagDoc._currentFrame = 0; + // CollectionFreeFormDocumentView.setupScroll(tagDoc, 0); + // CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); + } + // if (tagDoc.editScrollProgressivize) CollectionFreeFormDocumentView.updateScrollframe(tagDoc, currentFrame); + CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, tagDoc); + tagDoc._currentFrame = Math.max(0, (currentFrame || 0) + 1); + tagDoc.lastFrame = Math.max(NumCast(tagDoc._currentFrame), NumCast(tagDoc.lastFrame)); + } + + @action + prevKeyframe = (tagDoc: Doc, actItem: Doc): void => { + const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]); + const currentFrame = Cast(tagDoc._currentFrame, "number", null); + if (currentFrame === undefined) { + tagDoc._currentFrame = 0; + // CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); + } + CollectionFreeFormDocumentView.gotoKeyframe(childDocs.slice()); + tagDoc._currentFrame = Math.max(0, (currentFrame || 0) - 1); + } + + /** + * Returns the collection type as a string for headers + */ + @computed get stringType(): string { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + let type: string = ''; + if (activeItem) { + switch (targetDoc.type) { + case DocumentType.PDF: type = "PDF"; break; + case DocumentType.RTF: type = "Text node"; break; + case DocumentType.COL: type = "Collection"; break; + case DocumentType.AUDIO: type = "Audio"; break; + case DocumentType.VID: type = "Video"; break; + case DocumentType.IMG: type = "Image"; break; + case DocumentType.WEB: type = "Web page"; break; + default: type = "Other node"; break; + } + } + return type; + } + + @observable private openActiveColorPicker: boolean = false; + @observable private openViewedColorPicker: boolean = false; + + + + @computed get progressivizeDropdown() { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + if (activeItem && targetDoc) { + const activeFontColor = targetDoc["pres-text-color"] ? StrCast(targetDoc["pres-text-color"]) : "Black"; + const viewedFontColor = targetDoc["pres-text-viewed-color"] ? StrCast(targetDoc["pres-text-viewed-color"]) : "Black"; + return ( + <div> + <div className={`presBox-ribbon ${this.progressivizeTools && this.layoutDoc.presStatus === "edit" ? "active" : ""}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> + <div className="ribbon-box"> + {this.stringType} selected + <div className="ribbon-doubleButton" style={{ borderTop: 'solid 1px darkgrey', display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.RTF ? "inline-flex" : "none" }}> + <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presProgressivize ? Colors.LIGHT_BLUE : "" }} onClick={this.progressivizeChild}>Contents</div> + <div className="ribbon-toggle" style={{ opacity: activeItem.presProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editProgressivize ? Colors.LIGHT_BLUE : "" }} onClick={this.editProgressivize}>Edit</div> + </div> + <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? "inline-flex" : "none" }}> + <div className="presBox-subheading">Active text color</div> + <div className="ribbon-colorBox" style={{ backgroundColor: activeFontColor, height: 15, width: 15 }} onClick={action(() => { this.openActiveColorPicker = !this.openActiveColorPicker; })}> + </div> + </div> + {this.activeColorPicker} + <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? "inline-flex" : "none" }}> + <div className="presBox-subheading">Viewed font color</div> + <div className="ribbon-colorBox" style={{ backgroundColor: viewedFontColor, height: 15, width: 15 }} onClick={action(() => this.openViewedColorPicker = !this.openViewedColorPicker)}> + </div> + </div> + {this.viewedColorPicker} + <div className="ribbon-doubleButton" style={{ borderTop: 'solid 1px darkgrey', display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG ? "inline-flex" : "none" }}> + <div className="ribbon-toggle" style={{ backgroundColor: activeItem.zoomProgressivize ? Colors.LIGHT_BLUE : "" }} onClick={this.progressivizeZoom}>Zoom</div> + <div className="ribbon-toggle" style={{ opacity: activeItem.zoomProgressivize ? 1 : 0.4, backgroundColor: activeItem.editZoomProgressivize ? Colors.LIGHT_BLUE : "" }} onClick={this.editZoomProgressivize}>Edit</div> + </div> + <div className="ribbon-doubleButton" style={{ borderTop: 'solid 1px darkgrey', display: targetDoc._viewType === "stacking" || targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF ? "inline-flex" : "none" }}> + <div className="ribbon-toggle" style={{ backgroundColor: activeItem.scrollProgressivize ? Colors.LIGHT_BLUE : "" }} onClick={this.progressivizeScroll}>Scroll</div> + <div className="ribbon-toggle" style={{ opacity: activeItem.scrollProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editScrollProgressivize ? Colors.LIGHT_BLUE : "" }} onClick={this.editScrollProgressivize}>Edit</div> + </div> + </div> + <div className="ribbon-final-box"> + Frames + <div className="ribbon-doubleButton"> + <div className="ribbon-frameSelector"> + <div key="back" title="back frame" className="backKeyframe" onClick={e => { e.stopPropagation(); this.prevKeyframe(targetDoc, activeItem); }}> + <FontAwesomeIcon icon={"caret-left"} size={"lg"} /> + </div> + <div key="num" title="toggle view all" className="numKeyframe" style={{ color: targetDoc.keyFrameEditing ? "white" : "black", backgroundColor: targetDoc.keyFrameEditing ? Colors.MEDIUM_BLUE : Colors.LIGHT_BLUE }} + onClick={action(() => targetDoc.keyFrameEditing = !targetDoc.keyFrameEditing)} > + {NumCast(targetDoc._currentFrame)} + </div> + <div key="fwd" title="forward frame" className="fwdKeyframe" onClick={e => { e.stopPropagation(); this.nextKeyframe(targetDoc, activeItem); }}> + <FontAwesomeIcon icon={"caret-right"} size={"lg"} /> + </div> + </div> + <Tooltip title={<><div className="dash-tooltip">{"Last frame"}</div></>}><div className="ribbon-property">{NumCast(targetDoc.lastFrame)}</div></Tooltip> + </div> + <div className="ribbon-frameList"> + {this.frameListHeader} + {this.frameList} + </div> + <div className="ribbon-toggle" style={{ height: 20, backgroundColor: Colors.LIGHT_BLUE }} onClick={() => console.log(" TODO: play frames")}>Play</div> + </div> + </div> + </div> + ); + } + } + + @undoBatch + @action + switchActive = (color: ColorState) => { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + const val = String(color.hex); + targetDoc["pres-text-color"] = val; + return true; + } + @undoBatch + @action + switchPresented = (color: ColorState) => { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + const val = String(color.hex); + targetDoc["pres-text-viewed-color"] = val; + return true; + } + + @computed get activeColorPicker() { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + return !this.openActiveColorPicker ? (null) : <SketchPicker onChange={this.switchActive} + presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', + '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', + '#FFFFFF', '#f1efeb', 'transparent']} + color={StrCast(targetDoc["pres-text-color"])} />; + } + + @computed get viewedColorPicker() { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + return !this.openViewedColorPicker ? (null) : <SketchPicker onChange={this.switchPresented} + presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', + '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', + '#FFFFFF', '#f1efeb', 'transparent']} + color={StrCast(targetDoc["pres-text-viewed-color"])} />; + } + + @action + turnOffEdit = (paths?: boolean) => { + // Turn off paths + if (paths) { + const srcContext = Cast(this.rootDoc.presCollection, Doc, null); + if (srcContext) this.togglePath(srcContext, true); + } + // Turn off the progressivize editors for each document + this.childDocs.forEach((doc) => { + doc.editSnapZoomProgressivize = false; + doc.editZoomProgressivize = false; + const targetDoc = Cast(doc.presentationTargetDoc, Doc, null); + if (targetDoc) { + targetDoc.editZoomProgressivize = false; + // targetDoc.editScrollProgressivize = false; + } + }); + } + + //Toggle whether the user edits or not + @action + editZoomProgressivize = (e: React.MouseEvent) => { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + if (!targetDoc.editZoomProgressivize) { + if (!activeItem.zoomProgressivize) activeItem.zoomProgressivize = true; targetDoc.zoomProgressivize = true; + targetDoc.editZoomProgressivize = true; + activeItem.editZoomProgressivize = true; + } else { + targetDoc.editZoomProgressivize = false; + activeItem.editZoomProgressivize = false; + } + } + + //Toggle whether the user edits or not + @action + editScrollProgressivize = (e: React.MouseEvent) => { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + if (!targetDoc.editScrollProgressivize) { + if (!targetDoc.scrollProgressivize) { targetDoc.scrollProgressivize = true; activeItem.scrollProgressivize = true; } + targetDoc.editScrollProgressivize = true; + } else { + targetDoc.editScrollProgressivize = false; + } + } + + //Progressivize Zoom + @action + progressivizeScroll = (e: React.MouseEvent) => { + e.stopPropagation(); + const activeItem: Doc = this.activeItem; + activeItem.scrollProgressivize = !activeItem.scrollProgressivize; + const targetDoc: Doc = this.targetDoc; + targetDoc.scrollProgressivize = !targetDoc.scrollProgressivize; + // CollectionFreeFormDocumentView.setupScroll(targetDoc, NumCast(targetDoc._currentFrame)); + if (targetDoc.editScrollProgressivize) { + targetDoc.editScrollProgressivize = false; + targetDoc._currentFrame = 0; + targetDoc.lastFrame = 0; + } + } + + //Progressivize Zoom + @action + progressivizeZoom = (e: React.MouseEvent) => { + e.stopPropagation(); + const activeItem: Doc = this.activeItem; + activeItem.zoomProgressivize = !activeItem.zoomProgressivize; + const targetDoc: Doc = this.targetDoc; + targetDoc.zoomProgressivize = !targetDoc.zoomProgressivize; + CollectionFreeFormDocumentView.setupZoom(activeItem, targetDoc); + if (activeItem.editZoomProgressivize) { + activeItem.editZoomProgressivize = false; + targetDoc._currentFrame = 0; + targetDoc.lastFrame = 0; + } + } + + //Progressivize Child Docs + @action + editProgressivize = (e: React.MouseEvent) => { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + targetDoc._currentFrame = targetDoc.lastFrame; + if (!targetDoc.editProgressivize) { + if (!activeItem.presProgressivize) { activeItem.presProgressivize = true; targetDoc.presProgressivize = true; } + targetDoc.editProgressivize = true; + } else { + targetDoc.editProgressivize = false; + } + } + + @action + progressivizeChild = (e: React.MouseEvent) => { + e.stopPropagation(); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + const docs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]); + if (!activeItem.presProgressivize) { + targetDoc.keyFrameEditing = false; + activeItem.presProgressivize = true; + targetDoc.presProgressivize = true; + targetDoc._currentFrame = 0; + docs.forEach((doc, i) => CollectionFreeFormDocumentView.setupKeyframes([doc], i, true)); + targetDoc.lastFrame = targetDoc.lastFrame ? NumCast(targetDoc.lastFrame) : docs.length - 1; + } else { + // targetDoc.editProgressivize = false; + activeItem.presProgressivize = false; + targetDoc.presProgressivize = false; + targetDoc._currentFrame = 0; + targetDoc.keyFrameEditing = true; + } + } + + @action + checkMovementLists = (doc: Doc, xlist: any, ylist: any) => { + const x: List<number> = xlist; + const y: List<number> = ylist; + const tags: JSX.Element[] = []; + let pathPoints = ""; //List of all of the pathpoints that need to be added + for (let i = 0; i < x.length - 1; i++) { + if (y[i] || x[i]) { + if (i === 0) pathPoints = (x[i] - 11) + "," + (y[i] + 33); + else pathPoints = pathPoints + " " + (x[i] - 11) + "," + (y[i] + 33); + tags.push(<div className="progressivizeMove-frame" style={{ position: 'absolute', top: y[i], left: x[i] }}>{i}</div>); + } + } + tags.push(<svg style={{ overflow: 'visible', position: 'absolute' }}><polyline + points={pathPoints} + style={{ + position: 'absolute', + opacity: 1, + stroke: "#000000", + strokeWidth: 2, + strokeDasharray: '10 5', + }} + fill="none" + /></svg>); + return tags; + } + + @observable + toggleDisplayMovement = (doc: Doc) => { + if (doc.displayMovement) doc.displayMovement = false; + else doc.displayMovement = true; + } + + @action + checkList = (doc: Doc, list: any): number => { + const x: List<number> = list; + if (x && x.length >= NumCast(doc._currentFrame) + 1) { + return x[NumCast(doc._currentFrame)]; + } else if (x) { + x.length = NumCast(doc._currentFrame) + 1; + x[NumCast(doc._currentFrame)] = x[NumCast(doc._currentFrame) - 1]; + return x[NumCast(doc._currentFrame)]; + } else return 100; + } + + @computed get progressivizeChildDocs() { + const targetDoc: Doc = this.targetDoc; + const docs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]); + const tags: JSX.Element[] = []; + docs.forEach((doc, index) => { + if (doc["x-indexed"] && doc["y-indexed"]) { + tags.push(<div style={{ position: 'absolute', display: doc.displayMovement ? "block" : "none" }}>{this.checkMovementLists(doc, doc["x-indexed"], doc["y-indexed"])}</div>); + } + tags.push( + <div className="progressivizeButton" key={index} onPointerLeave={() => { if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0; }} onPointerOver={() => { if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0.5; }} onClick={e => { this.toggleDisplayMovement(doc); e.stopPropagation(); }} style={{ backgroundColor: doc.displayMovement ? Colors.LIGHT_BLUE : "#c8c8c8", top: NumCast(doc.y), left: NumCast(doc.x) }}> + <div className="progressivizeButton-prev"><FontAwesomeIcon icon={"caret-left"} size={"lg"} onClick={e => { e.stopPropagation(); this.prevAppearFrame(doc, index); }} /></div> + <div className="progressivizeButton-frame">{doc.appearFrame}</div> + <div className="progressivizeButton-next"><FontAwesomeIcon icon={"caret-right"} size={"lg"} onClick={e => { e.stopPropagation(); this.nextAppearFrame(doc, index); }} /></div> + </div>); + }); + return tags; + } + + @action + nextAppearFrame = (doc: Doc, i: number): void => { + // const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + // const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const appearFrame = Cast(doc.appearFrame, "number", null); + if (appearFrame === undefined) { + doc.appearFrame = 0; + } + doc.appearFrame = appearFrame + 1; + this.updateOpacityList(doc["opacity-indexed"], NumCast(doc.appearFrame)); + } + + @action + prevAppearFrame = (doc: Doc, i: number): void => { + // const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + // const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const appearFrame = Cast(doc.appearFrame, "number", null); + if (appearFrame === undefined) { + doc.appearFrame = 0; + } + doc.appearFrame = Math.max(0, appearFrame - 1); + this.updateOpacityList(doc["opacity-indexed"], NumCast(doc.appearFrame)); + } + + @action + updateOpacityList = (list: any, frame: number) => { + const x: List<number> = list; + if (x && x.length >= frame) { + for (let i = 0; i < x.length; i++) { + if (i < frame) { + x[i] = 0; + } else if (i >= frame) { + x[i] = 1; + } + } + list = x; + } else { + x.length = frame + 1; + for (let i = 0; i < x.length; i++) { + if (i < frame) { + x[i] = 0; + } else if (i >= frame) { + x[i] = 1; + } + } + list = x; + } + } + + @computed get moreInfoDropdown() { + return (<div></div>); + } + + @computed + get toolbarWidth(): number { + const width = this.props.PanelWidth(); + return width; + } + + @action + toggleProperties = () => { + if (CurrentUserUtils.propertiesWidth > 0) { + CurrentUserUtils.propertiesWidth = 0; + } else { + CurrentUserUtils.propertiesWidth = 250; + } + } + + @computed get toolbar() { + const propIcon = CurrentUserUtils.propertiesWidth > 0 ? "angle-double-right" : "angle-double-left"; + const propTitle = CurrentUserUtils.propertiesWidth > 0 ? "Close Presentation Panel" : "Open Presentation Panel"; + const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; + const isMini: boolean = this.toolbarWidth <= 100; + const presKeyEvents: boolean = (this.isPres && this._presKeyEventsActive && this.rootDoc === Doc.UserDoc().activePresentation); + const activeColor = Colors.LIGHT_BLUE; + const inactiveColor = Colors.WHITE; + return (mode === CollectionViewType.Carousel3D) ? (null) : ( + <div id="toolbarContainer" className={'presBox-toolbar'}> + {/* <Tooltip title={<><div className="dash-tooltip">{"Add new slide"}</div></>}><div className={`toolbar-button ${this.newDocumentTools ? "active" : ""}`} onClick={action(() => this.newDocumentTools = !this.newDocumentTools)}> + <FontAwesomeIcon icon={"plus"} /> + <FontAwesomeIcon className={`dropdown ${this.newDocumentTools ? "active" : ""}`} icon={"angle-down"} /> + </div></Tooltip> */} + <Tooltip title={<><div className="dash-tooltip">{"View paths"}</div></>}> + <div style={{ opacity: this.childDocs.length > 1 && this.layoutDoc.presCollection ? 1 : 0.3, color: this._pathBoolean ? Colors.MEDIUM_BLUE : 'white', width: isMini ? "100%" : undefined }} className={"toolbar-button"} onClick={this.childDocs.length > 1 && this.layoutDoc.presCollection ? this.viewPaths : undefined}> + <FontAwesomeIcon icon={"exchange-alt"} /> + </div> + </Tooltip> + {isMini ? (null) : + <> + <div className="toolbar-divider" /> + {/* <Tooltip title={<><div className="dash-tooltip">{this._expandBoolean ? "Minimize all" : "Expand all"}</div></>}> + <div className={"toolbar-button"} + style={{ color: this._expandBoolean ? Colors.MEDIUM_BLUE : 'white' }} + onClick={this.toggleExpandMode}> + <FontAwesomeIcon icon={"eye"} /> + </div> + </Tooltip> + <div className="toolbar-divider" /> */} + <Tooltip title={<><div className="dash-tooltip">{presKeyEvents ? "Keys are active" : "Keys are not active - click anywhere on the presentation trail to activate keys"}</div></>}> + <div className="toolbar-button" style={{ cursor: presKeyEvents ? 'default' : 'pointer', position: 'absolute', right: 30, fontSize: 16 }}> + <FontAwesomeIcon className={"toolbar-thumbtack"} icon={"keyboard"} style={{ color: presKeyEvents ? activeColor : inactiveColor }} /> + </div> + </Tooltip> + <Tooltip title={<><div className="dash-tooltip">{propTitle}</div></>}> + <div className="toolbar-button" style={{ position: 'absolute', right: 4, fontSize: 16 }} onClick={this.toggleProperties}> + <FontAwesomeIcon className={"toolbar-thumbtack"} icon={propIcon} style={{ color: CurrentUserUtils.propertiesWidth > 0 ? activeColor : inactiveColor }} /> + </div> + </Tooltip> + </> + } + </div> + ); + } + + /** + * Top panel containes: + * viewPicker: The option to choose between List and Slides view for the presentaiton trail + * presentPanel: The button to start the presentation / open minimized view of the presentation + */ + @computed get topPanel() { + const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; + const isMini: boolean = this.toolbarWidth <= 100; + return ( + <div className="presBox-buttons" style={{ display: !this.rootDoc._chromeHidden ? "none" : undefined }}> + {isMini ? (null) : <select className="presBox-viewPicker" + style={{ display: this.layoutDoc.presStatus === "edit" ? "block" : "none" }} + onPointerDown={e => e.stopPropagation()} + onChange={this.viewChanged} + value={mode}> + <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Stacking}>List</option> + <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Carousel3D}>3D Carousel</option> + </select>} + <div className="presBox-presentPanel" style={{ opacity: this.childDocs.length ? 1 : 0.3 }}> + <span className={`presBox-button ${this.layoutDoc.presStatus === "edit" ? "present" : ""}`}> + <div className="presBox-button-left" + onClick={undoBatch(() => { + if (this.childDocs.length) { + this.layoutDoc.presStatus = "manual"; + this.gotoDocument(this.itemIndex, this.activeItem); + } + })}> + <FontAwesomeIcon icon={"play-circle"} /> + <div style={{ display: this.props.PanelWidth() > 200 ? "inline-flex" : "none" }}> Present</div> + </div> + {(mode === CollectionViewType.Carousel3D || isMini) ? (null) : <div className={`presBox-button-right ${this.presentTools ? "active" : ""}`} + onClick={(action(() => { + if (this.childDocs.length) this.presentTools = !this.presentTools; + }))}> + <FontAwesomeIcon className="dropdown" style={{ margin: 0, transform: this.presentTools ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={"angle-down"} /> + {this.presentDropdown} + </div>} + </span> + {this.playButtons} + </div> + </div> + ); + } + + @action + getList = (list: any): List<number> => { + const x: List<number> = list; + return x; + } + + @action + updateList = (list: any): List<number> => { + const targetDoc: Doc = this.targetDoc; + const x: List<number> = list; + x.length + 1; + x[x.length - 1] = NumCast(targetDoc._scrollY); + return x; + } + + @action + newFrame = () => { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + const type: string = StrCast(targetDoc.type); + if (!activeItem.frameList) activeItem.frameList = new List<number>(); + switch (type) { + case (DocumentType.PDF || DocumentType.RTF || DocumentType.WEB): + this.updateList(activeItem.frameList); + break; + case DocumentType.COL: + break; + default: + break; + } + } + + @computed get frameListHeader() { + return (<div className="frameList-header"> + Frames {this.panable ? <i>Panable</i> : this.scrollable ? <i>Scrollable</i> : (null)} + <div className={"frameList-headerButtons"}> + <Tooltip title={<><div className="dash-tooltip">{"Add frame by example"}</div></>}><div className={"headerButton"} onClick={e => { e.stopPropagation(); this.newFrame(); }}> + <FontAwesomeIcon icon={"plus"} onPointerDown={e => e.stopPropagation()} /> + </div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Edit in collection"}</div></>}><div className={"headerButton"} onClick={e => { e.stopPropagation(); console.log('New frame'); }}> + <FontAwesomeIcon icon={"edit"} onPointerDown={e => e.stopPropagation()} /> + </div></Tooltip> + </div> + </div>); + } + + @computed get frameList() { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + const frameList: List<number> = this.getList(activeItem.frameList); + if (frameList) { + const frameItems = frameList.map((value) => + <div className="framList-item"> + + </div> + ); + return ( + + <div className="frameList-container"> + {frameItems} + </div> + ); + } else return (null); + + } + + @computed get playButtonFrames() { + const targetDoc: Doc = this.targetDoc; + return ( + <> + {this.targetDoc ? <div className="presPanel-button-frame" style={{ display: targetDoc.lastFrame !== undefined && targetDoc.lastFrame >= 0 ? "inline-flex" : "none" }}> + <div>{targetDoc._currentFrame}</div> + <div className="presPanel-divider" style={{ border: 'solid 0.5px white', height: '60%' }}></div> + <div>{targetDoc.lastFrame}</div> + </div> : null} + </> + ); + } + + @computed get playButtons() { + const presEnd: boolean = !this.layoutDoc.presLoop && (this.itemIndex === this.childDocs.length - 1); + const presStart: boolean = !this.layoutDoc.presLoop && (this.itemIndex === 0); + // Case 1: There are still other frames and should go through all frames before going to next slide + return (<div className="presPanelOverlay" style={{ display: this.layoutDoc.presStatus !== "edit" ? "inline-flex" : "none" }}> + <Tooltip title={<><div className="dash-tooltip">{"Loop"}</div></>}><div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : 'white' }} onClick={() => this.layoutDoc.presLoop = !this.layoutDoc.presLoop}><FontAwesomeIcon icon={"redo-alt"} /></div></Tooltip> + <div className="presPanel-divider"></div> + <div className="presPanel-button" style={{ opacity: presStart ? 0.4 : 1 }} onClick={() => { this.back(); if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; } }}><FontAwesomeIcon icon={"arrow-left"} /></div> + <Tooltip title={<><div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? "Pause" : "Autoplay"}</div></>}><div className="presPanel-button" onClick={this.startOrPause}><FontAwesomeIcon icon={this.layoutDoc.presStatus === PresStatus.Autoplay ? "pause" : "play"} /></div></Tooltip> + <div className="presPanel-button" style={{ opacity: presEnd ? 0.4 : 1 }} onClick={() => { this.next(); if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; } }}><FontAwesomeIcon icon={"arrow-right"} /></div> + <div className="presPanel-divider"></div> + <Tooltip title={<><div className="dash-tooltip">{"Click to return to 1st slide"}</div></>}><div className="presPanel-button" style={{ border: 'solid 1px white' }} onClick={() => this.gotoDocument(0, this.activeItem)}><b>1</b></div></Tooltip> + <div + className="presPanel-button-text" + onClick={() => this.gotoDocument(0, this.activeItem)} + style={{ display: this.props.PanelWidth() > 250 ? "inline-flex" : "none" }}> + Slide {this.itemIndex + 1} / {this.childDocs.length} + {this.playButtonFrames} + </div> + <div className="presPanel-divider"></div> + {this.props.PanelWidth() > 250 ? <div className="presPanel-button-text" onClick={undoBatch(action(() => { this.layoutDoc.presStatus = "edit"; clearTimeout(this._presTimer); }))}>EXIT</div> + : <div className="presPanel-button" onClick={undoBatch(action(() => this.layoutDoc.presStatus = "edit"))}> + <FontAwesomeIcon icon={"times"} /> + </div>} + </div>); + } + + @action + startOrPause = () => { + if (this.layoutDoc.presStatus === PresStatus.Manual || this.layoutDoc.presStatus === PresStatus.Edit) this.startAutoPres(this.itemIndex); + else this.pauseAutoPres(); + } + + render() { + // calling this method for keyEvents + this.isPres; + // needed to ensure that the childDocs are loaded for looking up fields + this.childDocs.slice(); + const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; + const presKeyEvents: boolean = (this.isPres && this._presKeyEventsActive && this.rootDoc === Doc.UserDoc().activePresentation); + const presEnd: boolean = !this.layoutDoc.presLoop && (this.itemIndex === this.childDocs.length - 1); + const presStart: boolean = !this.layoutDoc.presLoop && (this.itemIndex === 0); + return CurrentUserUtils.OverlayDocs.includes(this.rootDoc) ? + <div className="miniPres"> + <div className="presPanelOverlay" style={{ display: "inline-flex", height: 30, background: '#323232', top: 0, zIndex: 3000000, boxShadow: presKeyEvents ? '0 0 0px 3px ' + Colors.MEDIUM_BLUE : undefined }}> + <Tooltip title={<><div className="dash-tooltip">{"Loop"}</div></>}><div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : undefined }} onClick={() => this.layoutDoc.presLoop = !this.layoutDoc.presLoop}><FontAwesomeIcon icon={"redo-alt"} /></div></Tooltip> + <div className="presPanel-divider"></div> + <div className="presPanel-button" style={{ opacity: presStart ? 0.4 : 1 }} onClick={() => { this.back(); if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; } }}><FontAwesomeIcon icon={"arrow-left"} /></div> + <Tooltip title={<><div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? "Pause" : "Autoplay"}</div></>}><div className="presPanel-button" onClick={this.startOrPause}><FontAwesomeIcon icon={this.layoutDoc.presStatus === "auto" ? "pause" : "play"} /></div></Tooltip> + <div className="presPanel-button" style={{ opacity: presEnd ? 0.4 : 1 }} onClick={() => { this.next(); if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; } }}><FontAwesomeIcon icon={"arrow-right"} /></div> + <div className="presPanel-divider"></div> + <Tooltip title={<><div className="dash-tooltip">{"Click to return to 1st slide"}</div></>}><div className="presPanel-button" style={{ border: 'solid 1px white' }} onClick={() => this.gotoDocument(0, this.activeItem)}><b>1</b></div></Tooltip> + <div className="presPanel-button-text"> + Slide {this.itemIndex + 1} / {this.childDocs.length} + {this.playButtonFrames} + </div> + <div className="presPanel-divider"></div> + <div className="presPanel-button-text" onClick={undoBatch(action(() => { this.updateMinimize(); this.layoutDoc.presStatus = PresStatus.Edit; clearTimeout(this._presTimer); }))}>EXIT</div> + </div> + </div> + : + <div className="presBox-cont" style={{ minWidth: CurrentUserUtils.OverlayDocs.includes(this.layoutDoc) ? 240 : undefined }} > + {this.topPanel} + {this.toolbar} + {this.newDocumentToolbarDropdown} + <div className="presBox-listCont"> + {mode !== CollectionViewType.Invalid ? + <CollectionView {...this.props} + ContainingCollectionDoc={this.props.Document} + PanelWidth={this.props.PanelWidth} + PanelHeight={this.panelHeight} + childIgnoreNativeSize={true} + moveDocument={returnFalse} + childFitWidth={returnTrue} + childOpacity={returnOne} + childLayoutTemplate={this.childLayoutTemplate} + filterAddDocument={this.addDocumentFilter} + removeDocument={returnFalse} + dontRegisterView={true} + focus={this.selectElement} + ScreenToLocalTransform={this.getTransform} + /> + : (null) + } + </div> + </div>; + } +} +Scripting.addGlobal(function lookupPresBoxField(container: Doc, field: string, data: Doc) { + if (field === 'indexInPres') return DocListCast(container[StrCast(container.presentationFieldKey)]).indexOf(data); + if (field === 'presCollapsedHeight') return container._viewType === CollectionViewType.Stacking ? 35 : 31; + if (field === 'presStatus') return container.presStatus; + if (field === '_itemIndex') return container._itemIndex; + if (field === 'presBox') return container; + return undefined; +});
\ No newline at end of file |