import { Button, Dropdown, DropdownType, IconButton, Size, Type } from '@dash/components'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import Slider from '@mui/material/Slider'; import _ from 'lodash'; import { IReactionDisposer, ObservableSet, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { AiOutlineSend } from 'react-icons/ai'; import { FaArrowDown, FaArrowLeft, FaArrowRight, FaArrowUp } from 'react-icons/fa'; import ReactLoading from 'react-loading'; import ReactTextareaAutosize from 'react-textarea-autosize'; import { StopEvent, lightOrDark, returnFalse, returnOne, setupMoveUpEvents } from '../../../../ClientUtils'; import { emptyFunction, stringHash } from '../../../../Utils'; import { Doc, DocListCast, Field, FieldResult, FieldType, NumListCast, Opt, StrListCast } from '../../../../fields/Doc'; import { Animation, DocData, TransitionTimer } from '../../../../fields/DocSymbols'; import { Copy, Id } from '../../../../fields/FieldSymbols'; import { InkField } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; import { ObjectField } from '../../../../fields/ObjectField'; import { listSpec } from '../../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, StrCast, toList } from '../../../../fields/Types'; import { DocServer } from '../../../DocServer'; import { getSlideTransitionSuggestions, gptSlideProperties, gptTrailSlideCustomization } from '../../../apis/gpt/PresCustomization'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { Docs } from '../../../documents/Documents'; import { dropActionType } from '../../../util/DropActionTypes'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; import { SerializationHelper } from '../../../util/SerializationHelper'; import { SnappingManager } from '../../../util/SnappingManager'; import { UndoManager, undoBatch, undoable } from '../../../util/UndoManager'; import { DictationButton } from '../../DictationButton'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { pinDataTypes as dataTypes } from '../../PinFuncs'; import { CollectionView } from '../../collections/CollectionView'; import { TreeView } from '../../collections/TreeView'; import { CollectionFreeFormView } from '../../collections/collectionFreeForm'; import { CollectionFreeFormPannableContents } from '../../collections/collectionFreeForm/CollectionFreeFormPannableContents'; import { Colors } from '../../global/globalEnums'; import { DocumentView } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; import { FocusEffectDelay, FocusViewOptions } from '../FocusViewOptions'; import { OpenWhere, OpenWhereMod } from '../OpenWhere'; import { ScriptingBox } from '../ScriptingBox'; import CubicBezierEditor, { EaseFuncToPoints, TIMING_DEFAULT_MAPPINGS } from './CubicBezierEditor'; import './PresBox.scss'; import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums'; import SlideEffect from './SlideEffect'; import { AnimationSettings, SpringSettings, SpringType, easeItems, effectItems, effectTimings, movementItems, presEffectDefaultTimings, springMappings, springPreviewColors } from './SpringUtils'; @observer export class PresBox extends ViewBoxBaseComponent() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresBox, fieldKey); } private static _getTabDocs: () => Doc[]; public static Init(tabDocs: () => Doc[]) { PresBox._getTabDocs = tabDocs; } static navigateToDocScript: ScriptField; public static PanelName = 'PRESBOX'; // name of dockingview tab where presentations get added constructor(props: FieldViewProps) { super(props); makeObservable(this); if (!PresBox.navigateToDocScript) { PresBox.navigateToDocScript = ScriptField.MakeFunction('navigateToDoc(this.presentation_targetDoc, this)')!; } CollectionFreeFormPannableContents.SetOverlayPlugin((fform: Doc) => PresBox.Instance.pathLines(fform)); } private _disposers: { [name: string]: IReactionDisposer } = {}; public selectedArray = new ObservableSet(); public slideToModify: Doc | null = null; _batch: UndoManager.Batch | undefined = undefined; // undo batch for dragging sliders which generate multiple scene edit events as the cursor moves _keyTimer: NodeJS.Timeout | undefined; // timer for turning off transition flag when key frame change has completed. Need to clear this if you do a second navigation before first finishes, or else first timer can go off during second naviation. _unmounting = false; // flag that view is unmounting used to block RemFromMap from deleting things _presTimer: NodeJS.Timeout | undefined; _animationDictation: DictationButton | null = null; _slideDictation: DictationButton | null = null; // eslint-disable-next-line no-use-before-define @observable public static Instance: PresBox; @observable _isChildActive = false; @observable _moveOnFromAudio: boolean = true; @observable _eleArray: HTMLElement[] = []; @observable _dragArray: HTMLElement[] = []; @observable _pathBoolean: boolean = false; @observable _expandBoolean: boolean = false; @observable _transitionTools: boolean = false; @observable _newDocumentTools: boolean = false; @observable _openMovementDropdown: boolean = false; @observable _openEffectDropdown: boolean = false; @observable _openBulletEffectDropdown: boolean = false; @observable _presentTools: boolean = false; @observable _treeViewMap: Map = new Map(); @observable _presKeyEvents: boolean = false; @observable _forceKeyEvents: boolean = false; @observable _showAIGallery = false; @observable _showPreview = true; @observable _easeDropdownVal = 'ease'; // GPT @observable _chatActive: boolean = false; @observable _animationChat: string = ''; @observable _chatInput: string = ''; @observable _isLoading: boolean = false; @observable generatedAnimations: AnimationSettings[] = [ // default presets { effect: PresEffect.Bounce, direction: PresEffectDirection.Left, stiffness: 400, damping: 15, mass: 1, }, { effect: PresEffect.Fade, direction: PresEffectDirection.Left, stiffness: 100, damping: 15, mass: 1, }, { effect: PresEffect.Flip, direction: PresEffectDirection.Left, stiffness: 100, damping: 15, mass: 1, }, { effect: PresEffect.Rotate, direction: PresEffectDirection.Left, stiffness: 100, damping: 15, mass: 1, }, ]; setGeneratedAnimations = action((input: AnimationSettings[]) => { this.generatedAnimations = input; }) // prettier-ignore setChatInput = action((input: string) => { this._chatInput = input; }); // prettier-ignore setAnimationChat = action((input: string) => { this._animationChat = input; }); // prettier-ignore setIsLoading = action((input?: boolean) => { this._isLoading = !!input; }); // prettier-ignore setShowAIGalleryVisibilty = action((visible: boolean) => { this._showAIGallery = visible; }); // prettier-ignore setBezierControlPoints = action((newPoints: { p1: number[]; p2: number[] }) => { this.activeItem && this.setEaseFunc(this.activeItem, `cubic-bezier(${newPoints.p1[0]}, ${newPoints.p1[1]}, ${newPoints.p2[0]}, ${newPoints.p2[1]})`); }); @computed get showEaseFunctions() { return ![PresMovement.None, PresMovement.Jump, ''].includes(StrCast(this.activeItem?.presentation_movement) as PresMovement); } @computed get currCPoints() { return EaseFuncToPoints(this.activeItem?.presentation_easeFunc ? StrCast(this.activeItem.presentation_easeFunc) : 'ease'); } @computed get isTreeOrStack() { return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(StrCast(this.layoutDoc._type_collection) as CollectionViewType); } @computed get isTree() { return this.layoutDoc._type_collection === CollectionViewType.Tree; } @computed get presFieldKey() { return StrCast(this.layoutDoc.presFieldKey, 'data'); } @computed get childDocs() { return DocListCast(this.Document[this.presFieldKey]); } @computed get tagDocs() { return this.childDocs.map(doc => DocCast(doc.presentation_targetDoc)!).filter(doc => doc); } @computed get itemIndex() { return NumCast(this.Document._itemIndex); } @computed get activeItem() { return DocCast(this.childDocs[NumCast(this.Document._itemIndex)]); } @computed get targetDoc() { return DocCast(this.activeItem?.presentation_targetDoc); } public static targetRenderedDoc = (doc: Doc) => { const targetDoc = DocCast(doc?.presentation_targetDoc); return targetDoc?.layout_unrendered ? DocCast(targetDoc.annotationOn) : targetDoc; }; @computed get scrollable() { if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc?.type as DocumentType) || this.targetDoc?._type_collection === CollectionViewType.Stacking) return true; return false; } @computed get selectedDocumentView() { if (DocumentView.Selected().length) return DocumentView.Selected()[0]; if (this.selectedArray.size) return DocumentView.getDocumentView(this.Document); return undefined; } componentWillUnmount() { this._unmounting = true; if (this._presTimer) clearTimeout(this._presTimer); document.removeEventListener('keydown', PresBox.keyEventsWrapper, true); this.resetPresentation(); this.turnOffEdit(true); Object.values(this._disposers).forEach(disposer => disposer?.()); } componentDidMount() { this._props.setContentViewBox?.(this); this._disposers.pause = reaction( () => SnappingManager.UserPanned, () => this.pauseAutoPres() ); this._disposers.keyboard = reaction( () => this.selectedDocumentView?.Document, selected => { document.removeEventListener('keydown', PresBox.keyEventsWrapper, true); (this._presKeyEvents = selected === this.Document) && document.addEventListener('keydown', PresBox.keyEventsWrapper, true); }, { fireImmediately: true } ); this._disposers.forcekeyboard = reaction( () => this._forceKeyEvents, force => { if (force) { document.removeEventListener('keydown', PresBox.keyEventsWrapper, true); document.addEventListener('keydown', PresBox.keyEventsWrapper, true); this._presKeyEvents = true; } this._forceKeyEvents = false; }, { fireImmediately: true } ); this._props.setContentViewBox?.(this); this._unmounting = false; this.turnOffEdit(true); this._disposers.selection = reaction( () => DocumentView.Selected().slice(), views => (!PresBox.Instance || views.some(view => view.Document === this.Document)) && this.updateCurrentPresentation(), { fireImmediately: true } ); this._disposers.editing = reaction( () => this.layoutDoc.presentation_status === PresStatus.Edit, editing => editing && this.childDocs.filter(doc => doc.presentation_indexed !== undefined).forEach(doc => { this.progressivizedItems(doc)?.forEach(indexedDoc => { indexedDoc.opacity = undefined; }); doc.presentation_indexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, 1); }) // prettier-ignore ); } clearSelectedArray = () => this.selectedArray.clear(); addToSelectedArray = action((doc: Doc) => this.selectedArray.add(doc)); removeFromSelectedArray = action((doc: Doc) => this.selectedArray.delete(doc)); @action updateCurrentPresentation = (pres?: Doc) => { Doc.ActivePresentation = pres ?? this.Document; PresBox.Instance = this; }; // '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.config_clipEnd) - NumCast(activeItem.config_clipStart); if ([DocumentType.VID, DocumentType.AUDIO].includes(targetDoc.type as DocumentType)) { const targMedia = DocumentView.getDocumentView(targetDoc); targMedia?.ComponentView?.playFrom?.(NumCast(activeItem.config_clipStart), NumCast(activeItem.config_clipStart) + duration); } }; stopTempMedia = (targetDocField: FieldResult) => { const targetDoc = DocCast(DocCast(targetDocField)?.annotationOn) ?? DocCast(targetDocField); if ([DocumentType.VID, DocumentType.AUDIO].includes(targetDoc?.type as DocumentType)) { const targMedia = DocumentView.getDocumentView(targetDoc); targMedia?.ComponentView?.Pause?.(); } }; setDictationContent = (value: string) => this.setChatInput(value); customizeAnimations = action(() => { this.setIsLoading(true); getSlideTransitionSuggestions(this._animationChat) .then(res => this.setGeneratedAnimations(JSON.parse(res) as AnimationSettings[])) .catch(err => console.error(err)) .finally(this.setIsLoading); }); customizeWithGPT = action((input: string) => { // const testInput = 'change title to Customized Slide, transition for 2.3s with fade in effect'; this.setIsLoading(true); const slideDefaults: { [key: string]: FieldResult } = { presentation_transition: 500, config_zoom: 1 }; const currSlideProperties = gptSlideProperties.reduce( (prev, key) => { prev[key] = Field.toString(this.activeItem?.[key]) ?? prev[key]; return prev; }, slideDefaults); // prettier-ignore gptTrailSlideCustomization(input, JSON.stringify(currSlideProperties)) .then(res => (Object.entries(JSON.parse(res)) as string[][]).forEach(([key, val]) => { this.activeItem && (this.activeItem[key] = (+val).toString() === val ? +val : (val ?? this.activeItem[key])); }) ) .catch(e => console.error(e)) .finally(this.setIsLoading); }); // TODO: al: it seems currently that tempMedia doesn't stop onslidechange after clicking the button; the time the tempmedia stop depends on the start & end time // TODO: to handle child slides (entering into subtrail and exiting), also the next() and back() functions // No more frames in current doc and next slide is defined, therefore move to next slide nextSlide = (slideNum?: number) => { const nextSlideInd = slideNum ?? this.itemIndex + 1; let curSlideInd = nextSlideInd; // CollectionStackedTimeline.CurrentlyPlaying?.map(clipView => clipView?.ComponentView?.Pause?.()); this.clearSelectedArray(); const doGroupWithUp = (nextSelected: number, force = false) => () => { if (nextSelected < this.childDocs.length) { if (force || this.childDocs[nextSelected].presentation_groupWithUp) { this.addToSelectedArray(this.childDocs[nextSelected]); const serial = nextSelected + 1 < this.childDocs.length && NumCast(this.childDocs[nextSelected + 1].presentation_groupWithUp) > 1; if (serial) { this.gotoDocument(nextSelected, this.activeItem, true, async () => { const waitTime = NumCast(this.activeItem?.presentation_duration); await new Promise(res => { setTimeout(res, Math.max(0, waitTime)); }); doGroupWithUp(nextSelected + 1)(); }); } else { this.gotoDocument(nextSelected, this.activeItem, true); curSlideInd = this.itemIndex; doGroupWithUp(nextSelected + 1)(); } } } }; doGroupWithUp(curSlideInd, true)(); }; // docs within a slide target that will be progressively revealed progressivizedItems = (doc: Doc) => { const targetList = PresBox.targetRenderedDoc(doc); if (doc.presentation_indexed !== undefined && targetList) { const listItems = (Cast(targetList[Doc.LayoutDataKey(targetList)], listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[]) ?? DocListCast(targetList[Doc.LayoutDataKey(targetList) + '_annotations']); return listItems.filter(ldoc => !ldoc.layout_unrendered); } return undefined; }; // go to documents chain runSubroutines = (childrenToRun: Opt, normallyNextSlide: Doc) => { if (childrenToRun && childrenToRun[0] !== normallyNextSlide) { childrenToRun.forEach(child => DocumentView.showDocument(child, {})); } }; // Called when the user activates 'next' - to move to the next part of the pres. trail @action next = () => { const progressiveReveal = (first: boolean) => { const presIndexed = Cast(this.activeItem?.presentation_indexed, 'number', null); if (presIndexed !== undefined && this.activeItem) { const listItems = this.progressivizedItems(this.activeItem); const listItemDoc = listItems?.[presIndexed]; if (listItems && listItemDoc) { if (!first) { const presBulletTiming = 500; // bcz: hardwired for now Doc.linkFollowUnhighlight(); Doc.linkFollowHighlight(listItemDoc); listItemDoc.presentation_effect = this.activeItem.presBulletEffect; listItemDoc.presentation_transition = presBulletTiming; listItemDoc.opacity = undefined; const targetView = DocumentView.getFirstDocumentView(listItemDoc); if (targetView) { targetView.setAnimEffect(listItemDoc, presBulletTiming); if (this.activeItem.presBulletExpand) { targetView.setAnimateScaling(1.2, presBulletTiming * 0.8); Doc.AddUnHighlightWatcher(() => targetView.setAnimateScaling(0, undefined)); } } this.activeItem.presentation_indexed = presIndexed + 1; } return true; } } return undefined; }; if (progressiveReveal(false)) return true; if (this.childDocs[this.itemIndex + 1] !== undefined) { // Case 1: No more frames in current doc and next slide is defined, therefore move to next slide const slides = DocListCast(this.Document[StrCast(this.presFieldKey, 'data')]); const curLast = this.selectedArray.size ? Math.max(...Array.from(this.selectedArray).map(d => slides.indexOf(DocCast(d) ?? d))) : this.itemIndex; // before moving onto next slide, run the subroutines :) const currentDoc = this.childDocs[this.itemIndex]; // could i do this.childDocs[this.itemIndex] for first arg? this.runSubroutines(TreeView.GetRunningChildren.get(currentDoc)?.(), this.childDocs[this.itemIndex + 1]); this.nextSlide(curLast + 1 === this.childDocs.length ? (this.layoutDoc.presLoop ? 0 : curLast) : curLast + 1); progressiveReveal(true); // shows first progressive document, but without a transition effect } else { if (this.childDocs[this.itemIndex + 1] === undefined && (this.layoutDoc.presLoop || this.layoutDoc.presentation_status === PresStatus.Edit)) { // Case 2: Last slide and presLoop is toggled ON or it is in Edit mode this.nextSlide(0); progressiveReveal(true); // shows first progressive document, but without a transition effect return 0; } return false; } return this.itemIndex; }; // Called when the user activates 'back' - to move to the previous part of the pres. trail @action back = () => { const { activeItem } = this; let prevSelected = this.itemIndex; // Functionality for group with up let didZoom = activeItem?.presentation_movement; for (; prevSelected > 0 && this.childDocs[Math.max(0, prevSelected - 1)].presentation_groupWithUp; prevSelected--) { didZoom = didZoom === 'none' ? this.childDocs[prevSelected].presentation_movement : didZoom; } 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.nextSlide(prevSelected); this.Document._itemIndex = prevSelected; } 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.nextSlide(this.childDocs.length - 1); } return this.itemIndex; }; // 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. @undoBatch public gotoDocument = action((index: number, from?: Doc, group?: boolean, finished?: () => void) => { Doc.UnBrushAllDocs(); if (index >= 0 && index < this.childDocs.length) { this.Document._itemIndex = index; if (from?.mediaStopTriggerList && this.layoutDoc.presentation_status !== PresStatus.Edit) { DocListCast(from.mediaStopTriggerList).forEach(this.stopTempMedia); } if (from?.presentation_mediaStop === 'auto' && this.layoutDoc.presentation_status !== PresStatus.Edit) { this.stopTempMedia(from.presentation_targetDoc); } // If next slide is audio / video 'Play automatically' then the next slide should be played if (this.layoutDoc.presentation_status !== PresStatus.Edit && (this.targetDoc?.type === DocumentType.AUDIO || this.targetDoc?.type === DocumentType.VID) && this.activeItem?.presentation_mediaStart === 'auto') { this.startTempMedia(this.targetDoc, this.activeItem); } if (!group) this.clearSelectedArray(); this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); // Update selected array this.turnOffEdit(); this.navigateToActiveItem((options: FocusViewOptions) => { setTimeout(this.doHideBeforeAfter, FocusEffectDelay(options)); // Handles hide after/before finished?.(); }); // Handles movement to element only when presentationTrail is list } }); static pinDataTypes(target?: Doc): dataTypes { const targetType = target?.type as DocumentType; const inkable = [DocumentType.INK].includes(targetType); const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._type_collection === CollectionViewType.Stacking; const pannable = [DocumentType.IMG, DocumentType.PDF].includes(targetType) || (targetType === DocumentType.COL && target?._type_collection === CollectionViewType.Freeform); const map = [DocumentType.MAP].includes(targetType); const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(targetType); const clippable = [DocumentType.COMPARISON].includes(targetType); const datarange = [DocumentType.FUNCPLOT].includes(targetType); const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG, DocumentType.RTF].includes(targetType) && target?.activeFrame === undefined; const poslayoutview = [DocumentType.COL].includes(targetType) && target?.activeFrame === undefined; const collectionType = targetType === DocumentType.COL; const filters = true; const pivot = true; const dataannos = false; return { scrollable, pannable, inkable, collectionType, pivot, map, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos }; } @action playAnnotation = (/* anno: AudioField */) => { /* empty */ }; @action static restoreTargetDocView(bestTargetView: Opt, activeItem: Doc, transTime: number, pinDocLayout: boolean = BoolCast(activeItem.config_pinLayout), pinDataTypes?: dataTypes, targetDoc?: Doc) { const bestTarget = bestTargetView?.Document ?? (targetDoc?.layout_unrendered ? DocCast(targetDoc?.annotationOn) : targetDoc); if (!bestTarget) return undefined; let changed = false; if (pinDocLayout) { if ( bestTarget.x !== NumCast(activeItem.config_x, NumCast(bestTarget.x)) || bestTarget.y !== NumCast(activeItem.config_y, NumCast(bestTarget.y)) || bestTarget.rotation !== NumCast(activeItem.config_rotation, NumCast(bestTarget.rotation)) || bestTarget.width !== NumCast(activeItem.config_width, NumCast(bestTarget.width)) || bestTarget.height !== NumCast(activeItem.config_height, NumCast(bestTarget.height)) ) { bestTarget._dataTransition = `all ${transTime}ms`; bestTarget.x = NumCast(activeItem.config_x, NumCast(bestTarget.x)); bestTarget.y = NumCast(activeItem.config_y, NumCast(bestTarget.y)); bestTarget.rotation = NumCast(activeItem.config_rotation, NumCast(bestTarget.rotation)); bestTarget.width = NumCast(activeItem.config_width, NumCast(bestTarget.width)); bestTarget.height = NumCast(activeItem.config_height, NumCast(bestTarget.height)); bestTarget[TransitionTimer] && clearTimeout(bestTarget[TransitionTimer]); bestTarget[TransitionTimer] = setTimeout(() => { bestTarget[TransitionTimer] = bestTarget._dataTransition = undefined; }, transTime + 10); changed = true; } } const activeFrame = activeItem.config_activeFrame ?? activeItem.config_currentFrame; if (activeFrame !== undefined) { const frameTime = NumCast(activeItem.presentation_transition, 500); const acontext = activeItem.config_activeFrame !== undefined ? DocCast(DocCast(activeItem?.presentation_targetDoc)?.embedContainer) : DocCast(activeItem.presentation_targetDoc); const context = DocCast(acontext)?.annotationOn ? DocCast(DocCast(acontext)?.annotationOn) : acontext; if (context) { const ffview = CollectionFreeFormView.from(DocumentView.getFirstDocumentView(context)); if (ffview?.childDocs) { PresBox.Instance._keyTimer = CollectionFreeFormView.gotoKeyframe(PresBox.Instance._keyTimer, ffview.childDocs, frameTime); ffview.layoutDoc._currentFrame = NumCast(activeFrame); } } } if ((pinDataTypes?.dataview && activeItem.config_data !== undefined) || (!pinDataTypes && activeItem.config_data !== undefined)) { bestTarget._dataTransition = `all ${transTime}ms`; const fkey = Doc.LayoutDataKey(bestTarget); const setData = bestTargetView?.ComponentView?.setData; if (setData) setData(activeItem.config_data); else { const current = bestTarget['$' + fkey]; const hash = bestTarget['$' + fkey] ? stringHash(Field.toString(bestTarget['$' + fkey] as FieldType)) : undefined; if (hash) bestTarget['$' + fkey + '_' + hash] = current instanceof ObjectField ? current[Copy]() : current; bestTarget['$' + fkey] = activeItem.config_data instanceof ObjectField ? activeItem.config_data[Copy]() : activeItem.config_data; } bestTarget[fkey + '_usePath'] = activeItem.config_usePath; setTimeout(() => { bestTarget._dataTransition = undefined; }, transTime + 10); } if (pinDataTypes?.datarange || (!pinDataTypes && activeItem.config_xRange !== undefined)) { if (bestTarget.xRange !== activeItem.config_xRange) { bestTarget.xRange = (activeItem.config_xRange as ObjectField)?.[Copy](); changed = true; } if (bestTarget.yRange !== activeItem.config_yRange) { bestTarget.yRange = (activeItem.config_yRange as ObjectField)?.[Copy](); changed = true; } } if (pinDataTypes?.clippable || (!pinDataTypes && activeItem.config_clipWidth !== undefined)) { const fkey = '_' + Doc.LayoutDataKey(bestTarget); if (bestTarget[fkey + '_clipWidth'] !== activeItem.config_clipWidth) { bestTarget[fkey + '_clipWidth'] = activeItem.config_clipWidth; changed = true; } } if (pinDataTypes?.map || (!pinDataTypes && activeItem.config_latitude !== undefined)) { if (bestTarget.latitude !== activeItem.config_latitude) { Doc.SetInPlace(bestTarget, 'latitude', NumCast(activeItem.config_latitude), true); changed = true; } if (bestTarget.longitude !== activeItem.config_longitude) { Doc.SetInPlace(bestTarget, 'longitude', NumCast(activeItem.config_longitude), true); changed = true; } if (bestTarget.zoom !== activeItem.config_map_zoom) { Doc.SetInPlace(bestTarget, 'map_zoom', NumCast(activeItem.config_map_zoom), true); changed = true; } if (bestTarget.map_type !== activeItem.config_map_type) { Doc.SetInPlace(bestTarget, 'map_type', StrCast(activeItem.config_map_type), true); changed = true; } if (bestTarget.map !== activeItem.config_map) { Doc.SetInPlace(bestTarget, 'map', StrCast(activeItem.config_map), true); changed = true; } } if (pinDataTypes?.temporal || (!pinDataTypes && activeItem.config_clipStart !== undefined)) { if (bestTarget._layout_currentTimecode !== activeItem.config_clipStart) { bestTarget._layout_currentTimecode = activeItem.config_clipStart; changed = true; } } if (pinDataTypes?.temporal || (!pinDataTypes && activeItem.timecodeToShow !== undefined)) { if (bestTarget._layout_currentTimecode !== activeItem.timecodeToShow) { bestTarget._layout_currentTimecode = activeItem.timecodeToShow; changed = true; } } if (pinDataTypes?.inkable || (!pinDataTypes && (activeItem.config_fillColor !== undefined || activeItem.color !== undefined))) { if (bestTarget.fillColor !== activeItem.config_fillColor) { bestTarget.$fillColor = StrCast(activeItem.config_fillColor, StrCast(bestTarget.fillColor)); changed = true; } if (bestTarget.color !== activeItem.config_color) { bestTarget.$color = StrCast(activeItem.config_color, StrCast(bestTarget.color)); changed = true; } if (bestTarget.width !== activeItem.width) { bestTarget._width = NumCast(activeItem.config_width, NumCast(bestTarget.width)); changed = true; } if (bestTarget.height !== activeItem.height) { bestTarget._height = NumCast(activeItem.config_height, NumCast(bestTarget.height)); changed = true; } } if ((pinDataTypes?.collectionType && activeItem.config_card_curDoc !== undefined) || (!pinDataTypes && activeItem.config_card_curDoc !== undefined)) { if (bestTarget._card_curDoc !== activeItem.config_card_curDoc) { bestTarget._card_curDoc = activeItem.config_card_curDoc; changed = true; } } if ((pinDataTypes?.collectionType && activeItem.config_carousel_index !== undefined) || (!pinDataTypes && activeItem.config_carousel_index !== undefined)) { if (bestTarget._carousel_index !== activeItem.config_carousel_index) { bestTarget._carousel_index = activeItem.config_carousel_index; changed = true; } } if ((pinDataTypes?.collectionType && activeItem.config_type_collection !== undefined) || (!pinDataTypes && activeItem.config_type_collection !== undefined)) { if (bestTarget._type_collection !== activeItem.config_type_collection) { bestTarget._type_collection = activeItem.config_type_collection; changed = true; } } if ((pinDataTypes?.filters && activeItem.config_docFilters !== undefined) || (!pinDataTypes && activeItem.config_docFilters !== undefined)) { if (!_.isEqual(Array.from(StrListCast(bestTarget.childFilters)), Array.from(StrListCast(activeItem.config_docFilters)))) { bestTarget.childFilters = ObjectField.MakeCopy(activeItem.config_docFilters as ObjectField) || new List([]); changed = true; } } if ((pinDataTypes?.pivot && activeItem.config_pivotField !== undefined) || (!pinDataTypes && activeItem.config_pivotField !== undefined)) { if (bestTarget.pivotField !== activeItem.config_pivotField) { bestTarget.pivotField = activeItem.config_pivotField; bestTarget._prevFilterIndex = 1; // need to revisit this...see CollectionTimeView changed = true; } } if (bestTargetView?.ComponentView?.restoreView?.(activeItem)) { changed = true; } if (pinDataTypes?.scrollable || (!pinDataTypes && activeItem.config_scrollTop !== undefined)) { if (bestTarget._layout_scrollTop !== activeItem.config_scrollTop) { bestTarget._layout_scrollTop = activeItem.config_scrollTop; changed = true; } } if (pinDataTypes?.dataannos || (!pinDataTypes && activeItem.config_annotations !== undefined)) { const fkey = Doc.LayoutDataKey(bestTarget); const oldItems = DocListCast(bestTarget[fkey + '_annotations']).filter(doc => doc.layout_unrendered); const newItems = DocListCast(activeItem.config_annotations).map(doc => { doc.hidden = false; return doc; }); const hiddenItems = DocListCast(bestTarget[fkey + '_annotations']) .filter(doc => !doc.layout_unrendered && !newItems.includes(doc)) .map(doc => { doc.hidden = true; return doc; }); const newList = new List([...oldItems, ...hiddenItems, ...newItems]); bestTarget['$' + fkey + '_annotations'] = newList; } if (pinDataTypes?.poslayoutview || (!pinDataTypes && activeItem.config_pinLayoutData !== undefined)) { changed = true; const layoutField = Doc.LayoutDataKey(bestTarget); const transitioned = new Set(); StrListCast(activeItem.config_pinLayoutData) .map(str => JSON.parse(str) as { id: string; x: number; y: number; back: string; fill: string; w: number; h: number; data: string; text: string }) .forEach(async data => { const doc = DocCast(DocServer.GetCachedRefField(data.id)); if (doc) { transitioned.add(doc); const field = !data.data ? undefined : ((await SerializationHelper.Deserialize(data.data)) as FieldType); const tfield = !data.text ? undefined : ((await SerializationHelper.Deserialize(data.text)) as FieldType); doc._dataTransition = `all ${transTime}ms`; doc.x = data.x; doc.y = data.y; data.back && (doc._backgroundColor = data.back); data.fill && (doc._fillColor = data.fill); doc._width = data.w; doc._height = data.h; data.data && (doc.$data = field); data.text && (doc.$text = tfield); Doc.AddDocToList(bestTarget[DocData], layoutField, doc); } }); setTimeout( () => Array.from(transitioned).forEach(doc => { doc._dataTransition = undefined; }), transTime + 10 ); } if ((pinDataTypes?.pannable || (!pinDataTypes && (activeItem.config_viewBounds !== undefined || activeItem.config_panX !== undefined || activeItem.config_viewScale !== undefined))) && !Doc.IsFreeformGroup(bestTarget)) { const contentBounds = Cast(activeItem.config_viewBounds, listSpec('number')); if (contentBounds) { const viewport = { panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] }; bestTarget._freeform_panX = viewport.panX; bestTarget._freeform_panY = viewport.panY; const dv = DocumentView.getDocumentView(bestTarget); if (dv) { changed = true; const computedScale = NumCast(activeItem.config_zoom, 1) * Math.min(dv._props.PanelWidth() / viewport.width, dv._props.PanelHeight() / viewport.height); activeItem.presentation_movement === PresMovement.Zoom && (bestTarget._freeform_scale = computedScale); dv.ComponentView?.brushView?.(viewport, transTime, 2500); } } else if (bestTarget._freeform_panX !== activeItem.config_panX || bestTarget._freeform_panY !== activeItem.config_panY || bestTarget._freeform_scale !== activeItem.config_viewScale) { bestTarget._freeform_panX = activeItem.config_panX ?? bestTarget._freeform_panX; bestTarget._freeform_panY = activeItem.config_panY ?? bestTarget._freeform_panY; bestTarget._freeform_scale = activeItem.config_viewScale ?? bestTarget._freeform_scale; changed = true; } } if (changed) { return bestTargetView?.setViewTransition('all', transTime); } return undefined; } /** * 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. */ navigateToActiveItem = (afterNav?: (options: FocusViewOptions) => void) => { const { activeItem, targetDoc } = this; const finished = (options: FocusViewOptions) => { afterNav?.(options); targetDoc && (targetDoc[Animation] = undefined); }; const selViewCache = Array.from(this.selectedArray); const dragViewCache = Array.from(this._dragArray); const eleViewCache = Array.from(this._eleArray); const resetSelection = action((options: FocusViewOptions) => { if (!this._props.isSelected()) { const presDocView = DocumentView.getDocumentView(this.Document); if (presDocView) DocumentView.SelectView(presDocView, false); this.clearSelectedArray(); selViewCache.forEach(doc => this.addToSelectedArray(doc)); this._dragArray.splice(0, this._dragArray.length, ...dragViewCache); this._eleArray.splice(0, this._eleArray.length, ...eleViewCache); } finished(options); }); targetDoc && activeItem && PresBox.NavigateToTarget(targetDoc, activeItem, resetSelection); }; static NavigateToTarget(targetDoc: Doc, activeItem: Doc, finished?: (options: FocusViewOptions) => void) { if (activeItem.presentation_movement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) { (DocumentView.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.(); return; } const effect = activeItem.presentation_effect && activeItem.presentation_effect !== PresEffect.None ? activeItem.presentation_effect : undefined; // default with effect: 750ms else 500ms const presTime = NumCast(activeItem.presentation_transition, effect ? 750 : 500); const options: FocusViewOptions = { willPan: activeItem.presentation_movement !== PresMovement.None, willZoomCentered: activeItem.presentation_movement === PresMovement.Zoom || activeItem.presentation_movement === PresMovement.Jump || activeItem.presentation_movement === PresMovement.Center, zoomScale: activeItem.presentation_movement === PresMovement.Center ? 0 : NumCast(activeItem.config_zoom, 1), zoomTime: activeItem.presentation_movement === PresMovement.Jump ? 0 : Math.min(Math.max(effect ? 750 : 500, (effect ? 0.2 : 1) * presTime), presTime), effect: activeItem, noSelect: true, openLocation: targetDoc.type === DocumentType.PRES ? ((OpenWhere.replace + ':' + PresBox.PanelName) as OpenWhere) : OpenWhere.addLeft, easeFunc: StrCast(activeItem.presentation_easeFunc, 'ease') as 'linear' | 'ease', zoomTextSelections: BoolCast(activeItem.presentation_zoomText), playAudio: BoolCast(activeItem.presentation_playAudio), playMedia: activeItem.presentation_mediaStart === 'auto', }; if (activeItem.presentation_openInLightbox) { const context = DocCast(targetDoc.annotationOn) ?? targetDoc; if (!DocumentView.getLightboxDocumentView(context)) { DocumentView.SetLightboxDoc(context); } } if (targetDoc) { if (activeItem.presentation_targetDoc instanceof Doc) activeItem.presentation_targetDoc[Animation] = undefined; DocumentView.addViewRenderedCb(DocumentView.LightboxDoc(), () => { // if target or the doc it annotates is not in the lightbox, then close the lightbox if (!DocumentView.getLightboxDocumentView(DocCast(targetDoc.annotationOn) ?? targetDoc)) { DocumentView.SetLightboxDoc(undefined); } DocumentView.showDocument(targetDoc, options, () => finished?.(options)); }); } else finished?.(options); } /** * For 'Hide Before' and 'Hide After' buttons making sure that * they are hidden each time the presentation is updated. */ @action doHideBeforeAfter = () => { this.childDocs.forEach((doc, index) => { const curDoc = DocCast(doc); if (curDoc) { const tagDoc = PresBox.targetRenderedDoc(curDoc) ?? curDoc; const itemIndexes = this.getAllIndexes(this.tagDocs, curDoc); let opacity = index === this.itemIndex ? 1 : undefined; if (curDoc.presentation_hide) { if (index !== this.itemIndex) { opacity = 1; } } const hidingIndBef = itemIndexes.find(item => item >= this.itemIndex) ?? itemIndexes.slice().reverse().lastElement(); if (curDoc.presentation_hideBefore && index === hidingIndBef) { if (index > this.itemIndex) { opacity = 0; } else if (index === this.itemIndex || !curDoc.presentation_hideAfter) { opacity = 1; } } const hidingIndAft = itemIndexes .slice() .reverse() .find(item => item <= this.itemIndex) ?? itemIndexes.lastElement(); if (curDoc.presentation_hideAfter && index === hidingIndAft) { if (index < this.itemIndex) { opacity = 0; } else if (index === this.itemIndex || !curDoc.presentation_hideBefore) { opacity = 1; } } const hidingInd = itemIndexes.find(item => item === this.itemIndex); if (curDoc.presentation_hide && index === hidingInd) { if (index === this.itemIndex) { opacity = 0; } } opacity !== undefined && (tagDoc.opacity = opacity === 1 ? undefined : opacity); } }); }; _exitTrail: Opt<() => void>; playTrail = (docs: Doc[]) => { const savedStates = docs.map(doc => { switch (doc.type) { case DocumentType.COL: if (doc._type_collection === CollectionViewType.Freeform) { return { type: CollectionViewType.Freeform, doc, x: NumCast(doc.freeform_panX), y: NumCast(doc.freeform_panY), s: NumCast(doc.freeform_scale) }; } break; case DocumentType.INK: if (doc.data instanceof InkField) { return { type: doc.type, doc, data: doc.data?.[Copy](), fillColor: doc.fillColor, color: doc.color, x: NumCast(doc.x), y: NumCast(doc.y) }; } break; default: } return undefined; }); this.startPresentation(0); this._exitTrail = () => { savedStates .filter(savedState => savedState) .forEach(savedState => { switch (savedState?.type) { case CollectionViewType.Freeform: { const { x, y, s, doc } = savedState!; doc._freeform_panX = x; doc._freeform_panY = y; doc._freeform_scale = s; } break; case DocumentType.INK: { const { data, fillColor, color, x, y, doc } = savedState!; doc.x = x; doc.y = y; doc.data = data; doc.fillColor = fillColor; doc.color = color; } break; default: } }); DocumentView.SetLightboxDoc(undefined); Doc.RemFromMyOverlay(this.Document); return PresStatus.Edit; }; }; // The function pauses the auto presentation @action pauseAutoPres = () => { if (this.layoutDoc.presentation_status === PresStatus.Autoplay) { if (this._presTimer) clearTimeout(this._presTimer); this.layoutDoc.presentation_status = PresStatus.Manual; 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.childDocs .map(doc => PresBox.targetRenderedDoc(doc)) .filter(doc => doc instanceof Doc) .forEach(doc => { try { doc.opacity = 1; } catch (e) { console.log('Reset presentation error: ', e); } }); }; // The function allows for viewing the pres path on toggle @action togglePath = (off?: boolean) => { this._pathBoolean = off ? false : !this._pathBoolean; SnappingManager.SetShowPresPaths(this._pathBoolean); }; // The function allows for expanding the view of pres on toggle @action toggleExpandMode = () => { runInAction(() => { this._expandBoolean = !this._expandBoolean; }); this.Document.expandBoolean = this._expandBoolean; this.childDocs.forEach(doc => { doc.presentation_expandInlineButton = this._expandBoolean; }); }; initializePresState = (startIndex: number) => { this.childDocs.forEach((doc, index) => { const tagDoc = PresBox.targetRenderedDoc(doc) ?? doc; if (doc.presentation_hideBefore && index > startIndex) tagDoc.opacity = 0; if (doc.presentation_hideAfter && index < startIndex) tagDoc.opacity = 0; if (doc.presentation_indexed !== undefined && index >= startIndex) { const startInd = NumCast(doc.presentation_indexedStart); this.progressivizedItems(doc) ?.slice(startInd) .forEach(indexedDoc => { indexedDoc.opacity = 0; }); doc.presentation_indexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, startInd); } // if (doc.presentation_hide && this.childDocs.indexOf(doc) === startIndex) tagDoc.opacity = 0; }); }; /** * 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 */ @action startPresentation = (startIndex: number) => { PresBox.Instance = this; clearTimeout(this._presTimer); if (this.childDocs.length) { this.layoutDoc.presentation_status = PresStatus.Autoplay; this.initializePresState(startIndex); const func = () => { const delay = NumCast(this.activeItem?.presentation_duration, this.activeItem?.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem?.presentation_transition); this._presTimer = setTimeout(() => { if (this.next() === false) this.layoutDoc.presentation_status = this._exitTrail?.() ?? PresStatus.Manual; this.layoutDoc.presentation_status === PresStatus.Autoplay && func(); }, delay); }; this.gotoDocument(startIndex, this.activeItem, undefined, func); } }; /** * The method called to open the presentation as a minimized view */ @action enterMinimize = () => { this.updateCurrentPresentation(this.Document); clearTimeout(this._presTimer); const pt = this.ScreenToLocalBoxXf().inverse().transformPoint(0, 0); this._props.removeDocument?.(this.layoutDoc); return PresBox.OpenPresMinimized(this.Document, [pt[0] + (this._props.PanelWidth() - 250), pt[1] + 10]); }; exitMinimize = () => { if (Doc.IsInMyOverlay(this.layoutDoc)) { Doc.RemFromMyOverlay(this.Document); DocumentView.addSplit(this.Document, OpenWhereMod.right); } return PresStatus.Edit; }; public static minimizedWidth = 198; public static OpenPresMinimized(doc: Doc, pt: number[]) { [doc.overlayX, doc.overlayY] = pt; doc._height = 30; doc._width = PresBox.minimizedWidth; Doc.AddToMyOverlay(doc); PresBox.Instance?.initializePresState(PresBox.Instance.itemIndex); doc.presentation_status = PresStatus.Manual; return doc.presentation_status; } /** * Called when the user changes the view type * Either 'List' (stacking) or 'Slides' (carousel) */ @undoBatch viewChanged = action((e: React.ChangeEvent) => { const typeCollection = (e.target as HTMLSelectElement).selectedOptions[0].value as CollectionViewType; this.layoutDoc.presFieldKey = this.fieldKey + (typeCollection === CollectionViewType.Tree ? '-linearized' : ''); // pivot field may be set by the user in timeline view (or some other way) -- need to reset it here [CollectionViewType.Tree || CollectionViewType.Stacking].includes(typeCollection) && (this.Document._pivotField = undefined); this.Document._type_collection = typeCollection; if (this.isTreeOrStack) { this.layoutDoc._gridGap = 0; } }); movementName = action((activeItem: Doc) => { if (![PresMovement.Zoom, PresMovement.Pan, PresMovement.Center, PresMovement.Jump, PresMovement.None].includes(StrCast(activeItem.presentation_movement) as PresMovement)) { return PresMovement.Zoom; } return StrCast(activeItem.presentation_movement); }); whenChildContentsActiveChanged = action((isActive: boolean) => { this._props.whenChildContentsActiveChanged((this._isChildActive = isActive)); }); // For dragging documents into the presentation trail addDocumentFilter = (docs: Doc[]) => { const results = docs.map(doc => { if (doc.presentation_targetDoc) return true; if (doc.type === DocumentType.LABEL) { const audio = Cast(doc.annotationOn, Doc, null); if (audio) { audio.config_clipStart = NumCast(doc._timecodeToShow /* audioStart */, NumCast(doc._timecodeToShow /* videoStart */)); audio.config_clipEnd = NumCast(doc._timecodeToHide /* audioEnd */, NumCast(doc._timecodeToHide /* videoEnd */)); audio.presentation_duration = audio.config_clipStart - audio.config_clipEnd; this._props.pinToPres(audio, { audioRange: true }); setTimeout(() => this.removeDocument(doc), 0); return false; } } else if (doc.type !== DocumentType.PRES) { if (!doc.presentation_targetDoc) doc.title = doc.title + ' - Slide'; doc.presentation_targetDoc = doc.createdFrom ?? doc; // dropped document will be a new embedding of an embedded document somewhere else. doc.presentation_movement = PresMovement.Zoom; if (this._expandBoolean) doc.presentation_expandInlineButton = true; } return false; }); return !results.some(r => !r); }; childLayoutTemplate = () => Docs.Create.PresSlideDocument(); removeDocument = (doc: Doc | Doc[]) => !toList(doc) .map(d => Doc.RemoveDocFromList(this.Document, this.fieldKey, d)) .some(p => !p); getTransform = () => this.ScreenToLocalBoxXf().translate(-5, -65); // listBox padding-left and pres-box-cont minHeight panelHeight = () => this._props.PanelHeight() - 40; /** * For sorting the array so that the order is maintained when it is dropped. */ sortArray = () => 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() { return Array.from(this.selectedArray).map((doc, index) => { const curDoc = DocCast(doc); if (!curDoc) return null; const tagDoc = Cast(curDoc.presentation_targetDoc, Doc, null); if (curDoc && curDoc === this.activeItem) return (
{index + 1}. {StrCast(curDoc.title)}
); if (tagDoc) return (
{index + 1}. {StrCast(curDoc.title)}
); if (curDoc) return (
{index + 1}. {StrCast(curDoc.title)}
); return null; }); } @action selectPres = () => { const presDocView = DocumentView.getDocumentView(this.Document); presDocView && DocumentView.SelectView(presDocView, false); }; focusElement = (doc: Doc) => { this.selectElement(doc); return undefined; }; // Regular click @action selectElement = (doc: Doc, noNav = false) => { DocumentView.CurrentlyPlaying?.map(clip => clip?.ComponentView?.Pause?.()); if (noNav) { const index = this.childDocs.indexOf(doc); if (index >= 0 && index < this.childDocs.length) { this.Document._itemIndex = index; } } else { this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem); } this.updateCurrentPresentation(DocCast(doc.embedContainer)); }; // Command click @action multiSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement) => { if (!this.selectedArray.has(doc)) { this.addToSelectedArray(doc); this._eleArray.push(ref); this._dragArray.push(drag); } else { this.removeFromSelectedArray(doc); this._eleArray.splice(this._eleArray.indexOf(ref)); this._dragArray.splice(this._dragArray.indexOf(drag)); } this.selectPres(); }; // Shift click @action shiftSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement) => { this.clearSelectedArray(); // 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.addToSelectedArray(this.childDocs[i]); this._eleArray.push(ref); this._dragArray.push(drag); } } this.selectPres(); }; // regular click @action regularSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, noNav: boolean, selectPres = true) => { this.clearSelectedArray(); this.addToSelectedArray(doc); this._eleArray.splice(0, this._eleArray.length, ref); this._dragArray.splice(0, this._dragArray.length, drag); this.selectElement(doc, noNav); selectPres && this.selectPres(); }; modifierSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, noNav: 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, noNav); }; 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; if (e.target instanceof HTMLTextAreaElement) 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.presentation_status === 'edit') { undoable( action(() => { Array.from(this.selectedArray).forEach(doc => this.removeDocument(doc)); this.clearSelectedArray(); this._eleArray.length = 0; this._dragArray.length = 0; }), 'delete slides' )(); handled = true; } break; case 'Escape': if (Doc.IsInMyOverlay(this.layoutDoc)) { this.exitClicked(); } else if (this.layoutDoc.presentation_status === PresStatus.Edit) { this.clearSelectedArray(); this._eleArray.length = this._dragArray.length = 0; } else { this.layoutDoc.presentation_status = 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.Document._itemIndex = NumCast(this.Document._itemIndex) + 1; this.addToSelectedArray(this.childDocs[this.Document._itemIndex]); } else { this.next(); if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presentation_status = 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.Document._itemIndex = NumCast(this.Document._itemIndex) - 1; this.addToSelectedArray(this.childDocs[this.Document._itemIndex]); } else { this.back(); if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presentation_status = PresStatus.Manual; } } handled = true; break; case 'Spacebar': case ' ': if (this.layoutDoc.presentation_status === PresStatus.Manual) this.startOrPause(true); else if (this.layoutDoc.presentation_status === PresStatus.Autoplay) if (this._presTimer) clearTimeout(this._presTimer); handled = true; break; case 'a': if ((e.metaKey || e.altKey) && this.layoutDoc.presentation_status === 'edit') { this.clearSelectedArray(); this.childDocs.forEach(doc => this.addToSelectedArray(doc)); handled = true; } break; default: } if (handled) { e.stopPropagation(); e.preventDefault(); } }; getAllIndexes = (arr: Doc[], val: Doc) => arr.map((doc, i) => (doc === val ? i : -1)).filter(i => i !== -1); // Adds the index in the pres path graphically orderedPathLabels = (collection: Doc) => { const order: JSX.Element[] = []; const docs = new Set(); const presCollection = collection; const dv = DocumentView.getDocumentView(presCollection); this.childDocs.forEach((doc, index) => { const tagDoc = PresBox.targetRenderedDoc(doc) ?? doc; const srcContext = Cast(tagDoc.embedContainer, Doc, null); const labelCreator = (top: number, left: number, edge: number, fontSize: number) => (
this.selectElement(doc)}>
{index + 1}
); if (presCollection === srcContext) { const gap = 2; 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; // Case A: Document is contained within the collection if (docs.has(tagDoc)) { const prevOccurences = this.getAllIndexes(Array.from(docs), tagDoc).length; order.push(labelCreator(NumCast(tagDoc.y) + (prevOccurences * (edge + gap) - edge / 2), NumCast(tagDoc.x) - edge / 2, edge, fontSize)); } else { order.push(labelCreator(NumCast(tagDoc.y) - edge / 2, NumCast(tagDoc.x) - edge / 2, edge, fontSize)); } } else if (doc.config_pinView && presCollection === tagDoc && dv) { // Case B: Document is presPinView and is presCollection const scale = 1 / NumCast(doc.config_viewScale); const viewBounds = NumListCast(doc.config_viewBounds, [0, 0, dv._props.PanelWidth(), dv._props.PanelHeight()]); const height = (viewBounds[3] - viewBounds[1]) * scale; const width = (viewBounds[2] - viewBounds[0]) * scale; const indWidth = width / 10; const indHeight = Math.max(height / 10, 15); const indEdge = Math.max(indWidth, indHeight); const indFontSize = indEdge * 0.8; const left = NumCast(doc.config_panX) - width / 2; const top = NumCast(doc.config_panY) - height / 2; order.push( <> {labelCreator(top - indEdge / 2, left - indEdge / 2, indEdge, indFontSize)}
); } docs.add(tagDoc); }); 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 _freeform_fitContentsToBox 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) */ pathLines = (collection: Doc) => { let pathPoints = ''; this.childDocs .filter(doc => PresBox.targetRenderedDoc(doc)?.embedContainer === collection) .forEach((doc, index) => { const tagDoc = PresBox.targetRenderedDoc(doc); const [n1x, n1y] = tagDoc // ? [NumCast(tagDoc.x) + NumCast(tagDoc._width) / 2, NumCast(tagDoc.y) + NumCast(tagDoc._height) / 2] : [NumCast(doc.config_panX), NumCast(doc.config_panY)]; if (index === 0) pathPoints = n1x + ',' + n1y; else pathPoints = pathPoints + ' ' + n1x + ',' + n1y; }); return ( <>
{PresBox.Instance?.orderedPathLabels(collection)}
); }; // Converts seconds to ms and updates presentation_transition public static SetTransitionTime = (number: string, setter: (timeInMS: number) => void, change?: number) => { let timeInMS = Number(number) * 1000; if (change) timeInMS += change; if (timeInMS < 100) timeInMS = 100; if (timeInMS > 100000) timeInMS = 100000; setter(timeInMS); }; @undoBatch updateTransitionTime = (number: string, change?: number) => { PresBox.SetTransitionTime( number, (timeInMS: number) => this.selectedArray.forEach(doc => { doc.presentation_transition = timeInMS; }), change ); }; // Converts seconds to ms and updates presentation_transition @undoBatch updateZoom = (number: string, change?: number) => { let scale = Number(number) / 100; if (change) scale += change; if (scale < 0.01) scale = 0.01; if (scale > 1) scale = 1; this.selectedArray.forEach(doc => { doc.config_zoom = scale; }); }; /* * Converts seconds to ms and updates presentation_duration */ @undoBatch updateDurationTime = (number: string, change?: number) => { let timeInMS = Number(number) * 1000; if (change) timeInMS += change; if (timeInMS < 100) timeInMS = 100; if (timeInMS > 20000) timeInMS = 20000; this.selectedArray.forEach(doc => { doc.presentation_duration = timeInMS; }); }; @undoBatch updateMovement = action((movement: PresMovement, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => { doc.presentation_movement = movement; }) ); @undoBatch updateHideBefore = (activeItem: Doc) => { activeItem.presentation_hideBefore = !activeItem.presentation_hideBefore; this.selectedArray.forEach(doc => { doc.presentation_hideBefore = activeItem.presentation_hideBefore; }); }; @undoBatch updateHide = (activeItem: Doc) => { activeItem.presentation_hide = !activeItem.presentation_hide; this.selectedArray.forEach(doc => { doc.presentation_hide = activeItem.presentation_hide; }); }; @undoBatch updateHideAfter = (activeItem: Doc) => { activeItem.presentation_hideAfter = !activeItem.presentation_hideAfter; this.selectedArray.forEach(doc => { doc.presentation_hideAfter = activeItem.presentation_hideAfter; }); }; @undoBatch updateOpenDoc = (activeItem: Doc) => { activeItem.presentation_openInLightbox = !activeItem.presentation_openInLightbox; this.selectedArray.forEach(doc => { doc.presentation_openInLightbox = activeItem.presentation_openInLightbox; }); }; @undoBatch updateEaseFunc = (activeItem: Doc) => { activeItem.presentation_easeFunc = activeItem.presentation_easeFunc === 'linear' ? 'ease' : 'linear'; this.selectedArray.forEach(doc => { doc.presentation_easeFunc = activeItem.presentation_easeFunc; }); }; setEaseFunc = (activeItem: Doc, easeFunc: string) => { activeItem.presentation_easeFunc = easeFunc; this.selectedArray.forEach(doc => { doc.presentation_easeFunc = activeItem.presentation_easeFunc; }); }; @undoBatch updateEffectDirection = (effect: PresEffectDirection, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => { doc.presentation_effectDirection = effect; }); @undoBatch updateEffect = (effect: PresEffect, bullet: boolean, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => { bullet ? (doc.presBulletEffect = effect) : (doc.presentation_effect = effect); }); @undoBatch updateEffectTiming = (activeItem: Doc, timing: SpringSettings) => { activeItem.presentation_effectTiming = JSON.stringify(timing); this.selectedArray.forEach(doc => { doc.presentation_effectTiming = activeItem.presentation_effectTiming; }); }; static _sliderBatch: UndoManager.Batch | undefined; static endBatch = () => { PresBox._sliderBatch?.end(); document.removeEventListener('pointerup', PresBox.endBatch, true); }; public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void, hmargin?: number) => ( { PresBox._sliderBatch = UndoManager.StartBatch('pres slider'); document.addEventListener('pointerup', PresBox.endBatch, true); e.stopPropagation(); }} onChange={e => { e.stopPropagation(); change(e.target.value); }} /> ); // Applies the slide transiiton settings to all docs in the array @undoBatch applyTo = (array: Doc[]) => { if (this.activeItem) { this.updateMovement(this.activeItem.presentation_movement as PresMovement, true); this.updateEffect(this.activeItem.presentation_effect as PresEffect, false, true); this.updateEffect(this.activeItem.presBulletEffect as PresEffect, true, true); this.updateEffectDirection(this.activeItem.presentation_effectDirection as PresEffectDirection, true); const { presentation_transition: pt, presentation_duration: pd, presentation_hideBefore: ph, presentation_hideAfter: pa } = this.activeItem; array.forEach(curDoc => { curDoc.presentation_transition = pt; curDoc.presentation_duration = pd; curDoc.presentation_hideBefore = ph; curDoc.presentation_hideAfter = pa; }); } }; @computed get visibilityDurationDropdown() { const { activeItem } = this; if (activeItem && this.targetDoc) { const targetType = this.targetDoc.type; let duration = activeItem.presentation_duration ? NumCast(activeItem.presentation_duration) / 1000 : 0; if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration); return (
Hide before presented
}>
this.updateHideBefore(activeItem)}> Hide before
Hide while presented
}>
this.updateHide(activeItem)}> Hide
Hide after presented
}>
this.updateHideAfter(activeItem)}> Hide after
Open in lightbox view
}>
this.updateOpenDoc(activeItem)}> Lightbox
{[DocumentType.AUDIO, DocumentType.VID].includes(targetType as DocumentType) ? null : (
How long to view the slide before transitioning to the next slide
}>
DURATION
{PresBox.inputter('0.1', '0.1', '10', duration, targetType !== DocumentType.AUDIO, this.updateDurationTime)}
Short
Long
this.updateDurationTime(e.target.value))} /> s
)} ); } return undefined; } @computed get mediaDropdown() { const { activeItem } = this; if (activeItem && this.targetDoc) { return (
Should first bullet be progressively disclosed or does it appear with slide.
}>
{ activeItem.presentation_playAudio = !BoolCast(activeItem.presentation_playAudio); }}> Play Audio Annotation
Should first bullet be progressively disclosed or does it appear with slide.
}>
{ activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText); }}> Zoom Text Selections
); } return null; } @computed get progressivizeDropdown() { const { activeItem } = this; if (activeItem && this.targetDoc) { return (
whether progressivization is active for this slide
}>
{ activeItem.presentation_indexed = activeItem.presentation_indexed === undefined ? 0 : undefined; activeItem.presentation_hideBefore = activeItem.presentation_indexed !== undefined; const tagDoc = PresBox.targetRenderedDoc(activeItem) ?? activeItem; const type = DocCast(tagDoc?.annotationOn)?.type ?? tagDoc.type; activeItem.presentation_indexedStart = type === DocumentType.COL ? 1 : 0; // a progressivized slide doesn't have sub-slides, but rather iterates over the data list of the target being progressivized. // to avoid creating a new slide to correspond to each of the target's data list, we create a computedField to refernce the target's data list. let dataField = Doc.LayoutDataKey(tagDoc); if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField += '_annotations'; if (DocCast(activeItem.presentation_targetDoc)?.annotationOn) activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc.annotationOn?.["${dataField}"]`); else activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc?.["${dataField}"]`); }}> Enable
Should first bullet be progressively disclosed or does it appear with slide.
}>
{ activeItem.presentation_indexedStart = activeItem.presentation_indexedStart ? 0 : 1; }}> All Bullets
Whether the active bullet expands when active.}>
{ activeItem.presBulletExpand = !activeItem.presBulletExpand; }}> Expand Active
({ text: v.toString(), val: v }))} selectedVal={StrCast(activeItem.presBulletEffect, PresMovement.None)} setSelectedVal={val => this.updateEffect(val as PresEffect, true)} dropdownType={DropdownType.SELECT} type={Type.TERT} /> ); } return null; } @computed get gptDropdown() { return
; } setAiEffectsRef = (r: HTMLTextAreaElement | null) => setTimeout(() => { if (r && !r.textContent) { r.style.height = ''; r.style.height = r.scrollHeight + 'px'; } }); setAnimDictationRef = (r: DictationButton | null) => (this._animationDictation = r); /** * This chatbox is for getting slide effect transition suggestions from gpt and visualizing them */ @computed get aiEffects() { return (
{/* Custom */}
{ e.currentTarget.style.height = ''; e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px'; this.setAnimationChat(e.target.value); }} onKeyDown={e => { this._animationDictation?.stopDictation(); e.stopPropagation(); }} />
Click a box to use the effect. {/* Preview Animations */}
{this.generatedAnimations.map((elem, i) => (
{ this.updateEffect(elem.effect, false); this.updateEffectDirection(elem.direction); this.activeItem && this.updateEffectTiming(this.activeItem, { type: SpringType.CUSTOM, stiffness: elem.stiffness, damping: elem.damping, mass: elem.mass, }); }}>
))}
); } setPropertiesRef = (r: HTMLTextAreaElement | null) => setTimeout(() => { if (r && !r.textContent) { r.style.height = ''; r.style.height = r.scrollHeight + 'px'; } }); setSlideDictationRef = (r: DictationButton | null) => (this._slideDictation = r); @computed get transitionDropdown() { const { activeItem } = this; // Retrieving spring timing properties const timing = StrCast(activeItem?.presentation_effectTiming); const timingConfig: SpringSettings = timing ? JSON.parse(timing) : { type: SpringType.GENTLE, stiffness: 100, damping: 15, mass: 1, }; if (activeItem && this.targetDoc) { const transitionSpeed = activeItem.presentation_transition ? NumCast(activeItem.presentation_transition) / 1000 : 0.5; const zoom = NumCast(activeItem.config_zoom, 1) * 100; const effect = StrCast(activeItem.presentation_effect) ? (StrCast(activeItem.presentation_effect) as PresEffect) : PresEffect.None; const direction = StrCast(activeItem.presentation_effectDirection) as PresEffectDirection; return ( <> {/* This chatbox is for customizing the properties of trails, like transition time, movement type (zoom, pan) using GPT */}
Customize Slide Properties{' '}
window.open('https://brown-dash.github.io/Dash-Documentation/features/trails/#slide-customization')}> } color={SnappingManager.userColor} />
{ e.currentTarget.style.height = ''; e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px'; this.setChatInput(e.target.value); }} onKeyDown={e => { this._slideDictation?.stopDictation(); e.stopPropagation(); }} />
{/* Movement */}
e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onClick={action(e => { e.stopPropagation(); this._openMovementDropdown = false; this._openEffectDropdown = false; this._openBulletEffectDropdown = false; })}>
How long the transition (view navigation and slide animation effect) lasts
}>
SPEED
{PresBox.inputter('0.1', '0.1', '10', transitionSpeed, true, this.updateTransitionTime)}
Fast
Slow
e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s
this.updateMovement(val as PresMovement)} dropdownType={DropdownType.SELECT} type={Type.TERT} />
How much (%) of screen target should occupy
}>
ZOOM %
{PresBox.inputter('0', '1', '100', zoom, activeItem.presentation_movement === PresMovement.Zoom, this.updateZoom)}
this.updateZoom(e.target.value)} /> %
{/* Easing function */} {!this.showEaseFunctions ? null : ( typeof val === 'string' && this.activeItem && this.setEaseFunc(this.activeItem, val !== 'custom' ? val : TIMING_DEFAULT_MAPPINGS.ease)} dropdownType={DropdownType.SELECT} type={Type.TERT} /> )}
{/* Cubic bezier editor */} {!this.showEaseFunctions || !StrCast(activeItem.presentation_easeFunc).includes('cubic-bezier') ? null : (
)}
e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onClick={action(e => { e.stopPropagation(); this._openMovementDropdown = false; this._openEffectDropdown = false; this._openBulletEffectDropdown = false; })}>
{/* Effect dropdown */}
{ this.updateEffect(val as PresEffect, false); // set default spring options for that effect this.updateEffectTiming(activeItem, presEffectDefaultTimings[val as keyof typeof presEffectDefaultTimings]); }} dropdownType={DropdownType.SELECT} type={Type.TERT} />
this.setShowAIGalleryVisibilty(!this._showAIGallery)}> MORE
{this.aiEffects}
{/* Effect direction */} {/* Only applies to certain effects */} {(effect === PresEffect.Flip || effect === PresEffect.Bounce || effect === PresEffect.Roll) && (
DIRECTION
} onClick={() => this.updateEffectDirection(PresEffectDirection.Left)} /> } onClick={() => this.updateEffectDirection(PresEffectDirection.Right)} /> {effect !== PresEffect.Roll && ( <> } onClick={() => this.updateEffectDirection(PresEffectDirection.Top)} /> } onClick={() => this.updateEffectDirection(PresEffectDirection.Bottom)} /> )}
)} {![PresEffect.Lightspeed, PresEffect.Fade, PresEffect.None, ''].includes(effect) && ( <> this.updateEffectTiming(activeItem, { type: val as SpringType, ...springMappings[val] })} dropdownType={DropdownType.SELECT} type={Type.TERT} />
{/* No spring settings for jackinthebox (lightspeed) */} {StrCast(activeItem.presentation_effectTiming).includes('custom') && effect !== PresEffect.None && ( <>
Tension
e.stopPropagation()}> {/* prettier-ignore */} timingConfig && this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, stiffness: val as number })} valueLabelDisplay="auto" />
Damping
e.stopPropagation()}> {/* prettier-ignore */} timingConfig && this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, damping: val as number })} valueLabelDisplay="auto" />
Mass
e.stopPropagation()}> {/* prettier-ignore */} timingConfig && this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, mass: val as number })} valueLabelDisplay="auto" />
)}
)}
{[PresEffect.None, PresEffect.Fade, ''].includes(effect) ? null : (