import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { BoolCast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { DragManager } from '../../../util/DragManager'; import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { undoable, undoBatch } from '../../../util/UndoManager'; import { TreeView } from '../../collections/TreeView'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { EditableView } from '../../EditableView'; import { Colors } from '../../global/globalEnums'; import { PinDocView } from '../../PinFuncs'; import { StyleProp } from '../../StyleProp'; import { returnEmptyDocViewList } from '../../StyleProvider'; import { DocumentView } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; import { PresBox } from './PresBox'; import './PresSlideBox.scss'; import { PresMovement } from './PresEnums'; /** * This class models the view a document added to presentation will have in the presentation. * It involves some functionality for its buttons and options. */ @observer export class PresSlideBox extends ViewBoxBaseComponent() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresSlideBox, fieldKey); } private _itemRef: React.RefObject = React.createRef(); private _dragRef: React.RefObject = React.createRef(); private _titleRef: React.RefObject = React.createRef(); private _heightDisposer: IReactionDisposer | undefined; readonly expandViewHeight = 100; readonly collapsedHeight = 35; constructor(props: FieldViewProps) { super(props); makeObservable(this); } @observable _dragging = false; // the presentation view that renders this slide @computed get presBoxView() { return this.DocumentView?.() .containerViewPath?.() .slice() .reverse() .find(dv => dv?.ComponentView instanceof PresBox)?.ComponentView as Opt; } // the presentation view document that renders this slide @computed get presBox() { return this.presBoxView?.Document; } // Since this node is being rendered with a template, this method retrieves // the actual slide being rendered from the auto-generated rendering template @computed get slideDoc() { return this.rootDoc; } // this is the document in the workspaces that is targeted by the slide @computed get targetDoc() { return DocCast(this.slideDoc.presentation_targetDoc, this.slideDoc)!; } // computes index of this presentation slide in the presBox list @computed get indexInPres() { return this.presBoxView?.SlideIndex(this.slideDoc) ?? 0; } @computed get selectedArray() { return this.presBoxView?.selectedArray; } @computed get videoRecordingIsInOverlay() { return Doc.MyOverlayDocs.some(doc => doc.slides === this.slideDoc); } componentDidMount() { this.layoutDoc.layout_hideLinkButton = true; this._heightDisposer = reaction( () => ({ expand: this.slideDoc.presentation_expandInlineButton, height: this.collapsedHeight }), ({ expand, height }) => { this.layoutDoc._height = height + (expand ? this.expandViewHeight : 0); }, { fireImmediately: true } ); } componentWillUnmount() { this._heightDisposer?.(); } presExpandDocumentClick = () => { this.slideDoc.presentation_expandInlineButton = !this.slideDoc.presentation_expandInlineButton; }; embedHeight = () => this.collapsedHeight + this.expandViewHeight; embedWidth = () => this._props.PanelWidth() / 2; // prettier-ignore styleProvider = ( doc: Doc | undefined, props: Opt, property: string ) => (property === StyleProp.Opacity ? 1 : this._props.styleProvider?.(doc, props, property)); /** * The function that is responsible for rendering a preview or not for this * presentation element. */ @computed get renderEmbeddedInline() { return !this.slideDoc.presentation_expandInlineButton || !this.targetDoc ? null : (
); } @computed get renderGroupSlides() { const childDocs = DocListCast(this.targetDoc.data); const groupSlides = childDocs.map((doc: Doc, ind: number) => (
{ e.stopPropagation(); e.preventDefault(); this.presBoxView?.modifierSelect(doc, this._itemRef.current!, this._dragRef.current!, e.shiftKey || e.ctrlKey || e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey); this.presExpandDocumentClick(); }}>
{`${ind + 1}.`}
StrCast(doc.title)} SetValue={(value: string) => { doc.title = !value.trim().length ? '-untitled-' : value; return true; }} />
)); return groupSlides; } @computed get transition() { let transitionInS: number; if (this.slideDoc.presentation_transition) transitionInS = NumCast(this.slideDoc.presentation_transition) / 1000; else transitionInS = 0.5; return this.slideDoc.presentation_movement === PresMovement.Jump || this.slideDoc.presentation_movement === PresMovement.None ? null : 'M: ' + transitionInS + 's'; } @action headerDown = (e: React.PointerEvent) => { const element = e.target as HTMLDivElement; e.stopPropagation(); e.preventDefault(); if (element && !(e.ctrlKey || e.metaKey || e.button === 2)) { this.presBoxView?.regularSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, true, false); setupMoveUpEvents(this, e, this.startDrag, emptyFunction, clickEv => { clickEv.stopPropagation(); clickEv.preventDefault(); this.presBoxView?.modifierSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, clickEv.shiftKey || clickEv.ctrlKey || clickEv.metaKey, clickEv.ctrlKey || clickEv.metaKey, clickEv.shiftKey); this.presBoxView?.activeItem && this.showRecording(this.presBoxView?.activeItem); }); } }; /** * Function to drag and drop the pres element to a diferent location */ startDrag = (e: PointerEvent) => { this.presBoxView?.regularSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, true, false); const miniView: boolean = this.toolbarWidth <= 100; const activeItem = this.slideDoc; const dragArray = this.presBoxView?._dragArray ?? []; const dragData = new DragManager.DocumentDragData(this.presBoxView?.sortArray() ?? []); if (!dragData.draggedDocuments.length) dragData.draggedDocuments.push(this.slideDoc); dragData.treeViewDoc = this.presBox?._type_collection === CollectionViewType.Tree ? this.presBox : undefined; // this.DocumentView?.()?._props.treeViewDoc; dragData.moveDocument = this._props.moveDocument; const dragItem: HTMLElement[] = []; const classesToRestore = new Map(); if (dragArray.length === 1) { const doc = this._itemRef.current || dragArray[0]; if (doc) { classesToRestore.set(doc, doc.className); doc.className = miniView ? 'presItem-miniSlide' : 'presItem-slide'; dragItem.push(doc); } } else if (dragArray.length >= 1) { const doc = document.createElement('div'); doc.className = 'presItem-multiDrag'; doc.innerText = 'Move ' + (this.selectedArray?.size ?? 0) + ' slides'; doc.style.position = 'absolute'; doc.style.top = e.clientY + 'px'; doc.style.left = e.clientX - 50 + 'px'; dragItem.push(doc); } if (activeItem) { runInAction(() => { this._dragging = true; }); DragManager.StartDocumentDrag( dragItem.map(ele => ele), dragData, e.clientX, e.clientY, undefined, action(() => { Array.from(classesToRestore).forEach(pair => (pair[0].className = pair[1])); this._dragging = false; }) ); return true; } return false; }; onPointerOver = () => { document.removeEventListener('pointermove', this.onPointerMove); document.addEventListener('pointermove', this.onPointerMove); }; onPointerMove = (e: PointerEvent) => { const slide = this._itemRef.current; const dragIsPresItem = DragManager.docsBeingDragged.some(d => d.presentation_targetDoc); if (slide && dragIsPresItem) { const rect = slide.getBoundingClientRect(); const y = e.clientY - rect.top; // y position within the element. const height = slide.clientHeight; const halfLine = height / 2; if (y <= halfLine) { slide.style.borderTop = `solid 2px ${Colors.MEDIUM_BLUE}`; slide.style.borderBottom = '0px'; } else if (y > halfLine) { slide.style.borderTop = '0px'; slide.style.borderBottom = `solid 2px ${Colors.MEDIUM_BLUE}`; } } document.removeEventListener('pointermove', this.onPointerMove); }; onPointerLeave = () => { const slide = this._itemRef.current; if (slide) { slide.style.borderTop = '0px'; slide.style.borderBottom = '0px'; } document.removeEventListener('pointermove', this.onPointerMove); }; @action toggleProperties = () => { if (SnappingManager.PropertiesWidth < 5) { SnappingManager.SetPropertiesWidth(250); } }; removePresentationItem = undoable( action((e: React.MouseEvent) => { e.stopPropagation(); if (this.presBox && this.indexInPres < (this.presBoxView?.itemIndex || 0)) { this.presBox.itemIndex = (this.presBoxView?.itemIndex || 0) - 1; } this._props.removeDocument?.(this.slideDoc); this.presBoxView?.removeFromSelectedArray(this.slideDoc); this.removeAllRecordingInOverlay(); }), 'Remove doc from pres trail' ); // set title of the individual pres slide onSetValue = undoable( action((value: string) => { this.slideDoc.title = !value.trim().length ? '-untitled-' : value; return true; }), 'set title of pres element' ); /** * Method called for updating the view of the currently selected document * * @param targetDoc * @param activeItem */ @undoBatch updateCapturedContainerLayout = (presTargetDoc: Doc, activeItem: Doc) => { const targetDoc = DocCast(presTargetDoc.annotationOn) ?? presTargetDoc; activeItem.config_x = NumCast(targetDoc.x); activeItem.config_y = NumCast(targetDoc.y); activeItem.config_rotation = NumCast(targetDoc.rotation); activeItem.config_width = NumCast(targetDoc.width); activeItem.config_height = NumCast(targetDoc.height); activeItem.config_pinLayout = !activeItem.config_pinLayout; // activeItem.config_pinLayout = true; }; /** * Method called for updating the view of the currently selected document * * @param presTargetDoc * @param activeItem */ updateCapturedViewContents = undoable( action((presTargetDoc: Doc, activeItem: Doc) => { const target = DocCast(presTargetDoc.annotationOn) ?? presTargetDoc; PinDocView(activeItem, { pinData: PresBox.pinDataTypes(target) }, target); }), 'updated captured view contents' ); // a previously recorded video will have timecode defined static videoIsRecorded = (activeItem: Opt) => 'layout_currentTimecode' in (DocCast(activeItem?.recording) ?? {}); removeAllRecordingInOverlay = () => Doc.MyOverlayDocs.filter(doc => doc.slides === this.slideDoc).forEach(Doc.RemFromMyOverlay); /// remove all videos that have been recorded from overlay (leave videso that are being recorded to avoid losing data) static removeEveryExistingRecordingInOverlay = () => { Doc.MyOverlayDocs.filter(doc => doc.slides !== null && PresSlideBox.videoIsRecorded(DocCast(doc.slides))) // .forEach(Doc.RemFromMyOverlay); }; hideRecording = undoable( action((e: React.MouseEvent) => { e.stopPropagation(); this.removeAllRecordingInOverlay(); }), 'hide video recording' ); showRecording = undoable( action((activeItem: Doc, iconClick: boolean = false) => { // remove the overlays on switch *IF* not opened from the specific icon if (!iconClick) PresSlideBox.removeEveryExistingRecordingInOverlay(); DocCast(activeItem.recording) && Doc.AddToMyOverlay(DocCast(activeItem.recording)!); }), 'show video recording' ); startRecording = undoable( action((e: React.MouseEvent, activeItem: Doc) => { e.stopPropagation(); if (PresSlideBox.videoIsRecorded(activeItem)) { // if we already have an existing recording this.showRecording(activeItem, true); // // if we already have an existing recording // Doc.AddToMyOverlay(Cast(activeItem.recording, Doc, null)); } else { // we dont have any recording // Remove every recording that already exists in overlay view // this is a design decision to clear to focus in on the recoding mode PresSlideBox.removeEveryExistingRecordingInOverlay(); // create and add a recording to the slide // make recording box appear in the bottom right corner of the screen Doc.AddToMyOverlay( (activeItem.recording = Docs.Create.WebCamDocument('', { _width: 384, _height: 216, overlayX: window.innerWidth - 384 - 20, overlayY: window.innerHeight - 216 - 20, layout_hideDocumentButtonBar: true, layout_hideDecorationTitle: true, layout_hideOpenButton: true, cloneFieldFilter: new List(['isSystem']), slides: activeItem, // attach the slide to the recording })) ); } }), 'start video recording' ); @undoBatch lfg = (e: React.MouseEvent) => { e.stopPropagation(); // TODO: fix this bug // const { toggleChildrenRun } = this.slideDoc; TreeView.ToggleChildrenRun.get(this.slideDoc)?.(); // call this.slideDoc.recurChildren() to get all the children // if (iconClick) PresSlideBox.showVideo = false; }; @computed get toolbarWidth(): number { const presBoxDocView = DocumentView.getDocumentView(this.presBox); const width = NumCast(this.presBox?._width); return presBoxDocView ? presBoxDocView._props.PanelWidth() : width || 300; } @computed get presButtons() { const { presBox, targetDoc, slideDoc: activeItem } = this; const presBoxColor = StrCast(presBox?._backgroundColor); const presColorBool = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false; const hasChildren = BoolCast(this.slideDoc?.hasChildren); const items: JSX.Element[] = []; items.push( Update captured doc layout}>
setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.updateCapturedContainerLayout(targetDoc, activeItem), true)} style={{ opacity: activeItem.config_pinLayout ? 1 : 0.5, fontWeight: 700, display: 'flex' }}> L
); items.push( Update captured doc content}>
setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.updateCapturedViewContents(targetDoc, activeItem))} style={{ opacity: activeItem.config_pinData || activeItem.config_pinView ? 1 : 0.5, fontWeight: 700, display: 'flex' }}> C
); items.push( {this.videoRecordingIsInOverlay ? 'Hide Recording' : `${PresSlideBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}}>
(this.videoRecordingIsInOverlay ? this.hideRecording(e) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}> e.stopPropagation()} />
); if (this.indexInPres !== 0) { items.push( {!activeItem.presentation_groupWithUp ? 'Not grouped with previous slide (click to group)' : activeItem.presentation_groupWithUp === 1 ? 'Run simultaneously with previous slide (click again to run after)' : 'Run after previous slide (click to ungroup from previous)'} }>
{ activeItem.presentation_groupWithUp = (NumCast(activeItem.presentation_groupWithUp) + 1) % 3; }} style={{ zIndex: 1000 - this.indexInPres, fontWeight: 700, backgroundColor: activeItem.presentation_groupWithUp ? (presColorBool ? presBoxColor : Colors.MEDIUM_BLUE) : undefined, outline: NumCast(activeItem.presentation_groupWithUp) > 1 ? 'solid black 1px' : undefined, height: activeItem.presentation_groupWithUp ? 53 : 18, transform: activeItem.presentation_groupWithUp ? 'translate(0, -17px)' : undefined, }}>
e.stopPropagation()} />
); } items.push( {this.slideDoc.presentation_expandInlineButton ? 'Minimize' : 'Expand'}}>
{ e.stopPropagation(); this.presExpandDocumentClick(); }}> e.stopPropagation()} />
); if (!Doc.noviceMode && hasChildren) { // TODO: replace with if treeveiw, has childrenDocs items.push( Run child processes (tree only)}>
{ e.stopPropagation(); this.lfg(e); }} style={{ fontWeight: 700 }}> e.stopPropagation()} />
); } items.push( Remove from presentation}>
e.stopPropagation()} />
); items.push( Customize Slide}>
{ this.presBoxView?.regularSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, true, false); PresBox.Instance.navigateToActiveItem(); PresBox.Instance.openProperties(); PresBox.Instance.slideToModify = this.Document; }}> e.stopPropagation()} />
); return items; } @computed get mainItem() { const { presBox, slideDoc: activeItem } = this; const isSelected: boolean = !!this.selectedArray?.has(activeItem); const isCurrent: boolean = this.presBox?._itemIndex === this.indexInPres; const miniView: boolean = this.toolbarWidth <= 110; const presBoxColor: string = StrCast(presBox?._backgroundColor); const presColorBool: boolean = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false; return (
{ this.toggleProperties(); this.presBoxView?.regularSelect(activeItem, this._itemRef.current!, this._dragRef.current!, false); })} onPointerOver={this.onPointerOver} onPointerLeave={this.onPointerLeave} onPointerDown={this.headerDown}> {miniView ? (
{`${this.indexInPres + 1}.`}
) : (
{ e.stopPropagation(); if (this._itemRef.current && this._dragRef.current) { this.presBoxView?.modifierSelect(activeItem, this._itemRef.current, this._dragRef.current, true, false, false); } }} onClick={e => e.stopPropagation()}>{`${this.indexInPres + 1}. `}
StrCast(activeItem.title)} SetValue={this.onSetValue} />
{/*
{"Movement speed"}
}>
{this.transition}
*/} {/*
{"Duration"}
}>
{this.duration}
*/}
{...this.presButtons}
{this.renderEmbeddedInline}
)}
); } render() { return !(this.slideDoc instanceof Doc) || this.targetDoc instanceof Promise ? null : this.mainItem; } } Docs.Prototypes.TemplateMap.set(DocumentType.PRESSLIDE, { layout: { view: PresSlideBox, dataField: 'data' }, options: { acl: '', title: 'presSlide', _layout_fitWidth: true, _xMargin: 0, isTemplateDoc: true }, });