From d818ef151ca65008e5c6bb5e92b709decb3026d8 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 14 Apr 2025 18:35:49 -0400 Subject: fixed how templates are expanded to avoid template sub-component conflicts by changing how field keys are named. fixed various Cast functions to be more typesafe by including undefined as part of return type. overhaul of Doc.MakeClone, MakeCopy, FindRefernces - makeClone is no longer async. fixed inlined docs in text docs. --- src/client/views/nodes/AudioBox.tsx | 2 +- .../views/nodes/CollectionFreeFormDocumentView.tsx | 4 +- src/client/views/nodes/DataVizBox/DataVizBox.tsx | 2 +- src/client/views/nodes/DocumentContentsView.tsx | 4 +- src/client/views/nodes/DocumentView.tsx | 100 ++-- src/client/views/nodes/FieldView.tsx | 2 +- src/client/views/nodes/IconTagBox.tsx | 2 +- src/client/views/nodes/ImageBox.tsx | 24 +- src/client/views/nodes/KeyValueBox.tsx | 8 +- src/client/views/nodes/KeyValuePair.tsx | 4 +- src/client/views/nodes/PDFBox.tsx | 2 +- .../views/nodes/RecordingBox/RecordingBox.tsx | 2 +- src/client/views/nodes/ScreenshotBox.tsx | 2 +- src/client/views/nodes/ScriptingBox.tsx | 13 +- src/client/views/nodes/WebBox.tsx | 18 +- .../nodes/chatbot/chatboxcomponents/ChatBox.tsx | 2 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 55 +- .../views/nodes/formattedText/RichTextMenu.tsx | 6 +- .../views/nodes/formattedText/RichTextRules.ts | 19 +- src/client/views/nodes/trails/PresBox.tsx | 187 +++--- src/client/views/nodes/trails/PresElementBox.scss | 308 ---------- src/client/views/nodes/trails/PresElementBox.tsx | 628 --------------------- src/client/views/nodes/trails/PresSlideBox.scss | 308 ++++++++++ src/client/views/nodes/trails/PresSlideBox.tsx | 628 +++++++++++++++++++++ src/client/views/nodes/trails/index.ts | 2 +- 25 files changed, 1163 insertions(+), 1169 deletions(-) delete mode 100644 src/client/views/nodes/trails/PresElementBox.scss delete mode 100644 src/client/views/nodes/trails/PresElementBox.tsx create mode 100644 src/client/views/nodes/trails/PresSlideBox.scss create mode 100644 src/client/views/nodes/trails/PresSlideBox.tsx (limited to 'src/client/views/nodes') diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index e0d59cc9d..d4c512342 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -396,7 +396,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent() { returnFalse, action(() => { const newDoc = DocUtils.GetNewTextDoc('', NumCast(this.Document.x), NumCast(this.Document.y) + NumCast(this.layoutDoc._height) + 10, NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height)); - const textField = Doc.LayoutFieldKey(newDoc); + const textField = Doc.LayoutDataKey(newDoc); const newDocData = newDoc[DocData]; newDocData[`${textField}_recordingSource`] = this.dataDoc; newDocData[`${textField}_recordingStart`] = ComputedField.MakeFunction(`this.${textField}_recordingSource.${this.fieldKey}_recordingStart`); diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 53a3d3631..940c4cb99 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -71,7 +71,7 @@ export class CollectionFreeFormDocumentView extends DocComponent (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames + public static animDataFields = (doc: Doc) => (Doc.LayoutDataKey(doc) ? [Doc.LayoutDataKey(doc)] : []); // fields that are configured to be animatable using animation frames public static from(dv?: DocumentView): CollectionFreeFormDocumentView | undefined { return dv?._props.reactParent instanceof CollectionFreeFormDocumentView ? dv._props.reactParent : undefined; } @@ -187,7 +187,7 @@ export class CollectionFreeFormDocumentView extends DocComponent() { const getFrom = DocCast(this.layoutDoc.dataViz_asSchema); if (!getFrom?.schema_columnKeys) return undefined; const keys = StrListCast(getFrom?.schema_columnKeys).filter(key => key !== 'text'); - const children = DocListCast(getFrom?.[Doc.LayoutFieldKey(getFrom)]); + const children = DocListCast(getFrom?.[Doc.LayoutDataKey(getFrom)]); const current: { [key: string]: string }[] = []; children .filter(child => child) diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 6f004bed3..504c1491e 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -188,8 +188,8 @@ export class DocumentContentsView extends ObservableReactComponent, onInput: Opt): JsxBindings { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index d88d8c44d..521934152 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -169,9 +169,7 @@ export class DocumentViewInternal extends DocComponent { - this._mounted = true; - }); + runInAction(() => (this._mounted = true)); this.setupHandlers(); this._disposers.contentActive = reaction( () => @@ -183,16 +181,12 @@ export class DocumentViewInternal extends DocComponent { - this._isContentActive = active; - }, + active => (this._isContentActive = active), { fireImmediately: true } ); this._disposers.pointerevents = reaction( () => this.style(this.Document, StyleProp.PointerEvents) as Property.PointerEvents | undefined, - pointerevents => { - this._pointerEvents = pointerevents; - }, + pointerevents => (this._pointerEvents = pointerevents), { fireImmediately: true } ); } @@ -305,7 +299,7 @@ export class DocumentViewInternal extends DocComponent (this.onClickFunc?.()?.script.run(scriptProps, console.log).result as Opt<{ select: boolean }>)?.select && this._props.select(false) : undefined; if (!clickFunc) { - // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part + // onDragStart implies a button doc that we don't want to select when clicking. if (this.layoutDoc.onDragStart && !(e.ctrlKey || e.button > 0)) stopPropagate = false; preventDefault = false; } @@ -502,7 +496,7 @@ export class DocumentViewInternal extends DocComponent ContextMenu.Instance.addItem(item)); - const customScripts = Cast(this.Document.contextMenuScripts, listSpec(ScriptField), []); + const customScripts = Cast(this.Document.contextMenuScripts, listSpec(ScriptField), [])!; StrListCast(this.Document.contextMenuLabels).forEach((label, i) => cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.Document, scriptContext: this._props.scriptContext }), icon: 'sticky-note' }) ); @@ -536,9 +530,7 @@ export class DocumentViewInternal extends DocComponent { - this.layoutDoc._keepZWhenDragged = !this.layoutDoc._keepZWhenDragged; - }), + action(() => (this.layoutDoc._keepZWhenDragged = !this.layoutDoc._keepZWhenDragged)), 'set zIndex drag' ), icon: 'hand-point-up', @@ -663,7 +655,7 @@ export class DocumentViewInternal extends DocComponent (this.disableClickScriptFunc ? undefined : this.onClickHdlr); setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height + 2 * NumCast(this.Document.borderWidth))); } // prettier-ignore - setContentView = action((view: ViewBoxInterface) => { this._componentView = view; }); // prettier-ignore + setContentView = action((view: ViewBoxInterface) => (this._componentView = view)); isContentActive = (): boolean | undefined => this._isContentActive; childFilters = () => [...this._props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)]; @@ -763,6 +755,7 @@ export class DocumentViewInternal extends DocComponent (this.widgetDecorations ? this.widgetOverlay : null); viewingAiEditor = () => (this._props.showAIEditor && this._componentView?.componentAIView?.() !== undefined ? this.aiEditor : null); + _contentsRef = React.createRef(); @computed get viewBoxContents() { TraceMobx(); const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString; @@ -778,6 +771,7 @@ export class DocumentViewInternal extends DocComponent , props: Opt, property: string) => this._props?.styleProvider?.(doc, props, property + ':caption'); fieldsDropdown = (placeholder: string) => (
{ r && runInAction(() => (this._titleDropDownInnerWidth = DivWidth(r)));}} // prettier-ignore - onPointerDown={action(() => { this._changingTitleField = true; })} // prettier-ignore + ref={action((r:HTMLDivElement|null) => r && (this._titleDropDownInnerWidth = DivWidth(r)))} // prettier-ignore + onPointerDown={action(() => (this._changingTitleField = true))} style={{ width: 'max-content', background: SnappingManager.userBackgroundColor, color: SnappingManager.userColor, transformOrigin: 'left', transform: `scale(${this.titleHeight / 30 /* height of Dropdown */})` }}> { this._changingTitleField = false; })} // prettier-ignore + menuClose={action(() => (this._changingTitleField = false))} />
); @@ -842,7 +836,7 @@ export class DocumentViewInternal extends DocComponent u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, - StrCast(Doc.SharingDoc().headingColor, SnappingManager.userBackgroundColor) + StrCast(Doc.SharingDoc()?.headingColor, SnappingManager.userBackgroundColor) // ) ); const dropdownWidth = this._titleRef.current?._editing || this._changingTitleField ? Math.max(10, (this._titleDropDownInnerWidth * this.titleHeight) / 30) : 0; @@ -1028,7 +1022,7 @@ export class DocumentViewInternal extends DocComponent() { public static GetDocImage(doc?: Doc) { return DocumentView.getDocumentView(doc) ?.ComponentView?.updateIcon?.() - .then(() => ImageCast(doc!.icon, ImageCast(doc![Doc.LayoutFieldKey(doc!)]))); + .then(() => ImageCast(doc!.icon, ImageCast(doc![Doc.LayoutDataKey(doc!)]))); } public get displayName() { return 'DocumentView(' + (this.Document?.title??"") + ')'; } // prettier-ignore @@ -1181,6 +1175,24 @@ export class DocumentView extends DocComponent() { @observable public TagPanelHeight = 0; @observable public TagPanelEditing = false; + /** + * Tests whether the component Doc being rendered matches the Doc that this view thinks its rendering. + * When switching the layout_fieldKey, component views may react before the internal DocumentView has re-rendered. + * If this happens, the component view may try to write invalid data, such as when expanding a template (which + * depends on the DocumentView being in synch with the subcomponents). + * @param renderDoc sub-component Doc being rendered + * @returns boolean whether sub-component Doc is in synch with the layoutDoc that this view thinks its rendering + */ + IsInvalid = (renderDoc?: Doc): boolean => { + const docContents = this._docViewInternal?._contentsRef.current; + return !( + (!renderDoc || + (docContents?.layoutDoc === renderDoc && // + !this.docViewPath().some(dv => dv.IsInvalid()))) && + docContents?._props.layoutFieldKey === this.Document.layout_fieldKey + ); + }; + @computed get showTags() { return this.Document._layout_showTags || this._props.showTags; } @@ -1250,7 +1262,7 @@ export class DocumentView extends DocComponent() { !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentView.removeView(this); } - public set IsSelected(val) { runInAction(() => { this._selected = val; }); } // prettier-ignore + public set IsSelected(val) { runInAction(() => (this._selected = val)) } // prettier-ignore public get IsSelected() { return this._selected; } // prettier-ignore public get IsContentActive(){ return this._docViewInternal?.isContentActive(); } // prettier-ignore public get topMost() { return this._props.renderDepth === 0; } // prettier-ignore @@ -1262,7 +1274,7 @@ export class DocumentView extends DocComponent() { public get HasAIEditor() { return !!this._docViewInternal?._componentView?.componentAIView?.(); } // prettier-ignore get LayoutFieldKey() { - return Doc.LayoutFieldKey(this.Document, this._props.LayoutTemplateString); + return Doc.LayoutDataKey(this.Document, this._props.LayoutTemplateString); } @computed get layout_fitWidth() { @@ -1316,10 +1328,10 @@ export class DocumentView extends DocComponent() { public toggleNativeDimensions = () => this._docViewInternal && this.Document.type !== DocumentType.INK && Doc.toggleNativeDimensions(this.layoutDoc, this.NativeDimScaling() ?? 1, this._props.PanelWidth(), this._props.PanelHeight()); - public iconify(finished?: () => void, animateTime?: number) { + public iconify = action((finished?: () => void, animateTime?: number) => { this.ComponentView?.updateIcon?.(); const animTime = this._docViewInternal?.animateScaleTime(); - runInAction(() => { this._docViewInternal && animateTime !== undefined && (this._docViewInternal._animateScaleTime = animateTime); }); // prettier-ignore + this._docViewInternal && animateTime !== undefined && (this._docViewInternal._animateScaleTime = animateTime); const finalFinished = action(() => { finished?.(); this._docViewInternal && (this._docViewInternal._animateScaleTime = animTime); @@ -1329,12 +1341,12 @@ export class DocumentView extends DocComponent() { this.switchViews(true, 'icon', finalFinished); if (layoutFieldKey && layoutFieldKey !== 'layout' && layoutFieldKey !== 'layout_icon') this.Document.deiconifyLayout = layoutFieldKey.replace('layout_', ''); } else { - const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null); + const deiconifyLayout = StrCast(this.Document.deiconifyLayout); this.switchViews(!!deiconifyLayout, deiconifyLayout, finalFinished, true); this.Document.deiconifyLayout = undefined; this._props.bringToFront?.(this.Document); } - } + }); public playAnnotation = () => { const audioAnnoState = this.Document._audioAnnoState ?? AudioAnnoState.stopped; @@ -1349,7 +1361,7 @@ export class DocumentView extends DocComponent() { autoplay: true, loop: false, volume: 0.5, - onend: action(() => { this.Document._audioAnnoState = AudioAnnoState.stopped; }), // prettier-ignore + onend: action(() => (this.Document._audioAnnoState = AudioAnnoState.stopped)), }); this.Document._audioAnnoState = AudioAnnoState.playing; break; @@ -1405,14 +1417,10 @@ export class DocumentView extends DocComponent() { tempDoc = view.Document; MakeTemplate(tempDoc); Doc.AddDocToList(Doc.UserDoc(), 'template_user', tempDoc); - Doc.AddDocToList(DocListCast(Doc.MyTools.data)[1], 'data', makeUserTemplateButtonOrImage(tempDoc)); - tempDoc && Doc.AddDocToList(Cast(Doc.UserDoc().template_user, Doc, null), 'data', tempDoc); + Doc.AddDocToList(DocListCast(Doc.MyTools?.data)[1], 'data', makeUserTemplateButtonOrImage(tempDoc)); + DocCast(Doc.UserDoc().template_user) && tempDoc && Doc.AddDocToList(DocCast(Doc.UserDoc().template_user)!, 'data', tempDoc); } else { - tempDoc = DocCast(view.Document[StrCast(view.Document.layout_fieldKey)]); - if (!tempDoc) { - tempDoc = view.Document; - while (tempDoc && !Doc.isTemplateDoc(tempDoc)) tempDoc = DocCast(tempDoc.proto); - } + tempDoc = DocCast(Doc.LayoutField(view.Document)); } } Doc.UserDoc().defaultTextLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined; @@ -1434,10 +1442,10 @@ export class DocumentView extends DocComponent() { if (this.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) this.switchViews(!!defaultLayout, defaultLayout, undefined, true); else this.switchViews(true, detailLayoutKeySuffix, undefined, true); }; - public switchViews = (custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => { + public switchViews = action((custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => { const batch = UndoManager.StartBatch('switchView:' + view); // shrink doc first.. - runInAction(() => { this._docViewInternal && (this._docViewInternal._animateScalingTo = 0.1); }); // prettier-ignore + this._docViewInternal && (this._docViewInternal._animateScalingTo = 0.1); setTimeout( action(() => { if (useExistingLayout && custom && this.Document['layout_' + view]) { @@ -1457,7 +1465,7 @@ export class DocumentView extends DocComponent() { }), Math.max(0, (this._docViewInternal?.animateScaleTime() ?? 0) - 10) ); - }; + }); /** * @returns a hierarchy path through the nested DocumentViews that display this view. The last element of the path is this view. */ @@ -1510,7 +1518,7 @@ export class DocumentView extends DocComponent() { ref={r => { const val = r?.style.display !== 'none'; // if the outer overlay has been displayed, trigger the innner div to start it's opacity fade in transition if (r && val !== this._enableHtmlOverlayTransitions) { - setTimeout(action(() => { this._enableHtmlOverlayTransitions = val; })); // prettier-ignore + setTimeout(action(() => (this._enableHtmlOverlayTransitions = val))); } }} style={{ display: !this._htmlOverlayText ? 'none' : undefined }}> @@ -1537,12 +1545,8 @@ export class DocumentView extends DocComponent() {
{ - this._isHovering = true; - })} - onPointerLeave={action(() => { - this._isHovering = false; - })}> + onPointerEnter={action(() => (this._isHovering = true))} // + onPointerLeave={action(() => (this._isHovering = false))}> {!this.Document || !this._props.PanelWidth() ? null : (
() { fitWidth={this.layout_fitWidthFunc} ScreenToLocalTransform={this.screenToContentsTransform} focus={this._props.focus || emptyFunction} - ref={action((r: DocumentViewInternal | null) => { - r && (this._docViewInternal = r); - })} + ref={action((r: DocumentViewInternal | null) => r && (this._docViewInternal = r))} /> {this.htmlOverlay()} {this.ComponentView?.infoUI?.()} @@ -1616,7 +1618,7 @@ export class DocumentView extends DocComponent() { if (dv && (!containingDoc || dv.containerViewPath?.().lastElement()?.Document === containingDoc)) { DocumentView.showDocumentView(dv, options).then(() => dv && Doc.linkFollowHighlight(dv.Document)); } else { - const container = DocCast(containingDoc ?? doc.embedContainer ?? Doc.BestEmbedding(doc)); + const container = DocCast(containingDoc ?? doc.embedContainer ?? Doc.BestEmbedding(doc))!; const showDoc = !Doc.IsSystem(container) && !cv ? container : doc; options.toggleTarget = undefined; DocumentView.showDocument(showDoc, options, () => DocumentView.showDocument(doc, { ...options, openLocation: undefined })).then(() => { diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index ad6d93d43..de49f502f 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -80,7 +80,7 @@ export interface FieldViewSharedProps { setTitleFocus?: () => void; focus: FocusFuncType; onClickScript?: () => ScriptField | undefined; - onDoubleClickScript?: () => ScriptField; + onDoubleClickScript?: () => ScriptField | undefined; onPointerDownScript?: () => ScriptField; onPointerUpScript?: () => ScriptField; onKey?: (e: KeyboardEvent, textBox: FormattedTextBox) => boolean | undefined; diff --git a/src/client/views/nodes/IconTagBox.tsx b/src/client/views/nodes/IconTagBox.tsx index e3924eca7..0bbd6a0d3 100644 --- a/src/client/views/nodes/IconTagBox.tsx +++ b/src/client/views/nodes/IconTagBox.tsx @@ -108,7 +108,7 @@ export class IconTagBox extends ObservableReactComponent { )); // prettier-ignore - const audioannos = StrListCast(this.View.Document[Doc.LayoutFieldKey(this.View.Document) + '_audioAnnotations_text']); + const audioannos = StrListCast(this.View.Document[Doc.LayoutDataKey(this.View.Document) + '_audioAnnotations_text']); return !buttons.length && !audioannos.length ? null : (
{audioannos.length ? this.renderAudioButtons(this.View, audioannos.lastElement()) : null} diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 2b9a78596..0c475b7bb 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -216,7 +216,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { } else if (hitDropTarget(e.target as HTMLElement, this._regenerateIconRef.current)) { this._regenerateLoading = true; const drag = de.complete.docDragData.draggedDocuments.lastElement(); - const dragField = drag[Doc.LayoutFieldKey(drag)]; + const dragField = drag[Doc.LayoutDataKey(drag)]; const descText = RTFCast(dragField)?.Text || StrCast(dragField) || RTFCast(drag.text)?.Text || StrCast(drag.text) || StrCast(this.Document.title); const oldPrompt = StrCast(this.Document.ai_firefly_prompt, StrCast(this.Document.title)); const newPrompt = (text: string) => (oldPrompt ? `${oldPrompt} ~~~ ${text}` : text); @@ -224,7 +224,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { added = false; } else if (de.altKey || !this.dataDoc[this.fieldKey]) { const layoutDoc = de.complete.docDragData?.draggedDocuments[0]; - const targetField = Doc.LayoutFieldKey(layoutDoc); + const targetField = Doc.LayoutDataKey(layoutDoc); const targetDoc = layoutDoc[DocData]; if (targetDoc[targetField] instanceof ImageField) { added = true; @@ -264,7 +264,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { DocListCast(this.dataDoc[this.annotationKey]).forEach(doc => { doc.x = (NumCast(doc.x) / oldnativeWidth) * newnativeWidth; doc.y = (NumCast(doc.y) / oldnativeWidth) * newnativeWidth; - if (!RTFCast(doc[Doc.LayoutFieldKey(doc)])) { + if (!RTFCast(doc[Doc.LayoutDataKey(doc)])) { doc.width = (NumCast(doc.width) / oldnativeWidth) * newnativeWidth; doc.height = (NumCast(doc.height) / oldnativeWidth) * newnativeWidth; } @@ -351,9 +351,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { event: () => { Networking.PostToServer('/queryFireflyImageText', { file: (file => { - const ext = extname(file); - return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); - })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href), + const ext = file ? extname(file) : ''; + return file?.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); + })(ImageCast(this.Document[Doc.LayoutDataKey(this.Document)])?.url.href), }).then(text => alert(text)); }, icon: 'expand-arrows-alt', @@ -364,9 +364,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { Networking.PostToServer('/expandImage', { prompt: 'sunny skies', file: (file => { - const ext = extname(file); - return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); - })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href), + const ext = file ? extname(file) : ''; + return file?.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); + })(ImageCast(this.Document[Doc.LayoutDataKey(this.Document)])?.url.href), }).then(res => { const info = res as Upload.ImageInformation; const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title }); @@ -510,7 +510,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() {
DocumentView.showDocument(DocCast(this.Document.ai_firefly_generatedDocs), { openLocation: OpenWhere.addRight })} + onClick={() => DocCast(this.Document.ai_firefly_generatedDocs) && DocumentView.showDocument(DocCast(this.Document.ai_firefly_generatedDocs)!, { openLocation: OpenWhere.addRight })} style={{ display: (this._props.isContentActive() && (SnappingManager.CanEmbed || this.Document.ai_firefly_generatedDocs)) || this._regenerateLoading ? 'block' : 'none', transform: `scale(${this.uiBtnScaling})`, @@ -532,7 +532,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { const defaultUrl = new URL(ClientUtils.prepend(DefaultPath)); const altpaths = alts - ?.map(doc => (doc instanceof Doc ? (ImageCast(doc[Doc.LayoutFieldKey(doc)])?.url ?? defaultUrl) : defaultUrl)) + ?.map(doc => (doc instanceof Doc ? (ImageCast(doc[Doc.LayoutDataKey(doc)])?.url ?? defaultUrl) : defaultUrl)) .filter(url => url) .map(url => this.choosePath(url)) ?? []; // acc ess the primary layout data of the alternate documents const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; @@ -664,7 +664,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { const url = firstImg.pathname; const imgField = new ImageField(url); this._prevImgs.length === 0 && - this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field.url.pathname }); + this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field?.url.pathname ?? '' }); this._prevImgs.unshift({ prompt: firstImg.prompt, seed: firstImg.seed, pathname: url }); this.dataDoc.ai_firefly_history = JSON.stringify(this._prevImgs); this.dataDoc.ai_firefly_prompt = firstImg.prompt; diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index be897b3f3..706607fe1 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -96,7 +96,7 @@ export class KeyValueBox extends ViewBoxBaseComponent() { const { script, type, onDelegate } = kvpScript; const chooseDelegate = forceOnDelegate || onDelegate || keyIn.startsWith('_'); const key = chooseDelegate && keyIn.startsWith('$') ? keyIn.slice(1) : keyIn; - const target = chooseDelegate ? doc : DocCast(doc.proto, doc); + const target = chooseDelegate ? doc : DocCast(doc.proto, doc)!; let field: FieldType | undefined; switch (type) { case 'computed': field = new ComputedField(script); break; // prettier-ignore @@ -261,15 +261,15 @@ export class KeyValueBox extends ViewBoxBaseComponent() { getFieldView = () => { const rows = this.rows.filter(row => row.isChecked); if (rows.length > 1) { - const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this.Document).title}`, _chromeHidden: true }); + const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${this.Document.title}`, _chromeHidden: true }); rows.forEach(row => { - const field = this.createFieldView(DocCast(this.Document), row); + const field = this.createFieldView(this.Document, row); field && Doc.AddDocToList(parent, 'data', field); row.uncheck(); }); return parent; } - return rows.length ? this.createFieldView(DocCast(this.Document), rows.lastElement()) : undefined; + return rows.length ? this.createFieldView(this.Document, rows.lastElement()) : undefined; }; createFieldView = (templateDoc: Doc, row: KeyValuePair) => { diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index c9e0aea5a..d8f968a40 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -61,7 +61,7 @@ export class KeyValuePair extends ObservableReactComponent { }; render() { - let doc = this._props.keyName.startsWith('_') ? this._props.doc[DocLayout] : this._props.doc; + let doc: Doc | undefined = this._props.keyName.startsWith('_') ? this._props.doc[DocLayout] : this._props.doc; const layoutField = doc !== this._props.doc; const key = layoutField ? this._props.keyName.replace(/^_/, '') : this._props.keyName; let protoCount = 0; @@ -85,7 +85,7 @@ export class KeyValuePair extends ObservableReactComponent { style={hover} className="keyValuePair-td-key-delete" onClick={undoable(() => { - delete (Object.keys(doc).indexOf(key) !== -1 ? doc : DocCast(this._props.doc.proto))[key]; + doc && delete (Object.keys(doc).indexOf(key) !== -1 ? doc : DocCast(this._props.doc.proto)!)[key]; }, 'set key value')}> X diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 36d260fb9..83c44c80f 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -210,7 +210,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent() { this._disposers.scroll = reaction( () => this.layoutDoc.layout_scrollTop, () => { - if (!(ComputedField.WithoutComputed(() => FieldValue(this.Document[this.SidebarKey + '_panY'])) instanceof ComputedField)) { + if (!(ComputedField.DisableCompute(() => FieldValue(this.Document[this.SidebarKey + '_panY'])) instanceof ComputedField)) { this.Document[this.SidebarKey + '_panY'] = ComputedField.MakeFunction('this.layout_scrollTop'); } this.layoutDoc[this.SidebarKey + '_freeform_scale'] = 1; diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx index 53783e8a3..25c708da8 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -98,7 +98,7 @@ export class RecordingBox extends ViewBoxBaseComponent() { }); screengrabber.overlayX = 70; // was -400 screengrabber.overlayY = 590; // was 0 - screengrabber['$' + Doc.LayoutFieldKey(screengrabber) + '_trackScreen'] = true; + screengrabber['$' + Doc.LayoutDataKey(screengrabber) + '_trackScreen'] = true; Doc.AddToMyOverlay(screengrabber); // just adds doc to overlay DocumentView.addViewRenderedCb(screengrabber, docView => { RecordingBox.screengrabber = docView.ComponentView as RecordingBox; diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 999f9c1cd..4f02d68d6 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -284,7 +284,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent() setupDictation = () => { if (this.dataDoc[this.fieldKey + '_dictation']) return; const dictationText = DocUtils.GetNewTextDoc('dictation', NumCast(this.Document.x), NumCast(this.Document.y) + NumCast(this.layoutDoc._height) + 10, NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height)); - const textField = Doc.LayoutFieldKey(dictationText); + const textField = Doc.LayoutDataKey(dictationText); dictationText._layout_autoHeight = false; const dictationTextProto = dictationText[DocData]; dictationTextProto[`${textField}_recordingSource`] = this.dataDoc; diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index 8da422039..38a43e8d4 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -1,14 +1,12 @@ -/* eslint-disable react/button-has-type */ import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import ResizeObserver from 'resize-observer-polyfill'; import { returnAlways, returnEmptyString } from '../../../ClientUtils'; -import { Doc } from '../../../fields/Doc'; +import { Doc, StrListCast } from '../../../fields/Doc'; import { List } from '../../../fields/List'; -import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; @@ -26,10 +24,8 @@ import './ScriptingBox.scss'; import * as ts from 'typescript'; import { FieldType } from '../../../fields/ObjectField'; -// eslint-disable-next-line @typescript-eslint/no-var-requires const getCaretCoordinates = require('textarea-caret'); -// eslint-disable-next-line @typescript-eslint/no-var-requires const ReactTextareaAutocomplete = require('@webscopeio/react-textarea-autocomplete').default; @observer @@ -105,7 +101,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent() this.dataDoc[this.fieldKey + '-functionDescription'] = value; } @computed({ keepAlive: true }) get compileParams() { - return Cast(this.dataDoc[this.fieldKey + '-params'], listSpec('string'), []); + return StrListCast(this.dataDoc[this.fieldKey + '-params']); } set compileParams(value) { this.dataDoc[this.fieldKey + '-params'] = new List(value); @@ -426,7 +422,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent() onChange={e => this.viewChanged(e, parameter)} value={typeof this.dataDoc[parameter] === 'string' ? 'S' + StrCast(this.dataDoc[parameter]) : typeof this.dataDoc[parameter] === 'number' ? 'N' + NumCast(this.dataDoc[parameter]) : 'B' + BoolCast(this.dataDoc[parameter])}> {types.map((type, i) => ( - // eslint-disable-next-line react/no-array-index-key
}> -
- 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.PRESELEMENT, { - layout: { view: PresElementBox, dataField: 'data' }, - options: { acl: '', title: 'pres element template', _layout_fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: 'data' }, -}); diff --git a/src/client/views/nodes/trails/PresSlideBox.scss b/src/client/views/nodes/trails/PresSlideBox.scss new file mode 100644 index 000000000..9ac2b5a94 --- /dev/null +++ b/src/client/views/nodes/trails/PresSlideBox.scss @@ -0,0 +1,308 @@ +$light-blue: #aeddf8; +$dark-blue: #5b9fdd; +$light-background: #ececec; +$slide-background: #d5dce2; +$slide-active: #5b9fdd; + +.testingv2 { + background-color: red; +} + +.presItem-container { + cursor: grab; + display: flex; + grid-template-columns: 20px auto; + font-family: Roboto; + letter-spacing: normal; + position: relative; + pointer-events: all; + width: 100%; + height: 100%; + font-weight: 400; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + align-items: center; + + // .presItem-number { + // margin-top: 3.5px; + // font-size: 12px; + // font-weight: 700; + // text-align: center; + // justify-self: center; + // align-self: flex-start; + // position: relative; + // display: inline-block; + // overflow: hidden; + // } +} + +.presItem-slide { + position: relative; + height: 100%; + width: 100%; + border-bottom: 0.5px solid grey; + display: flex; + align-items: center; + + .presItem-number { + cursor: pointer; + &:hover { + background-color: $light-blue; + } + } + .presItem-name { + display: flex; + min-width: 20px; + z-index: 300; + top: 2px; + align-self: center; + font-size: 11px; + font-family: Roboto; + font-weight: 500; + position: relative; + padding-left: 10px; + padding-right: 10px; + letter-spacing: normal; + width: max-content; + text-overflow: ellipsis; + overflow: hidden; + white-space: pre; + } + + .presItem-docName { + min-width: 20px; + z-index: 300; + align-self: center; + font-size: 9px; + font-family: Roboto; + font-weight: 300; + position: relative; + padding-left: 10px; + padding-right: 10px; + letter-spacing: normal; + width: max-content; + text-overflow: ellipsis; + overflow: hidden; + white-space: pre; + grid-row: 2; + grid-column: 1/6; + } + + .presItem-time { + align-self: center; + position: relative; + padding-right: 10px; + top: 1px; + font-size: 10; + font-weight: 300; + font-family: Roboto; + z-index: 300; + letter-spacing: normal; + } + + .presItem-embedded { + overflow: hidden; + grid-row: 3; + grid-column: 1/8; + position: relative; + display: inline-block; + } + + .presItem-embeddedMask { + width: 100%; + height: 100%; + position: absolute; + border-radius: 3px; + top: 0; + left: 0; + z-index: 1; + overflow: hidden; + } + + .presItem-slideButtons { + display: flex; + position: absolute; + width: max-content; + justify-self: right; + justify-content: flex-end; + + .slideButton { + cursor: pointer; + position: relative; + border-radius: 100px; + z-index: 300; + width: 18px; + height: 18px; + display: flex; + font-size: 12px; + justify-self: center; + align-self: center; + background-color: rgba(0, 0, 0, 0.5); + color: white; + justify-content: center; + align-items: center; + transition: 0.2s; + margin-right: 3px; + } + + .slideButton:hover { + background-color: rgba(0, 0, 0, 1); + transform: scale(1.2); + } + } +} + +// .presItem-slide:hover { +// .presItem-slideButtons { +// display: flex; +// grid-column: 7; +// grid-row: 1/3; +// width: max-content; +// justify-self: right; +// justify-content: flex-end; + +// .slideButton { +// cursor: pointer; +// position: relative; +// border-radius: 100px; +// z-index: 300; +// width: 18px; +// height: 18px; +// display: flex; +// font-size: 12px; +// justify-self: center; +// align-self: center; +// background-color: rgba(0, 0, 0, 0.5); +// color: white; +// justify-content: center; +// align-items: center; +// transition: 0.2s; +// margin-right: 3px; +// } + +// .slideButton:hover { +// background-color: rgba(0, 0, 0, 1); +// transform: scale(1.2); +// } +// } +// } + +.presItem-slide.active { + //box-shadow: 0 0 0px 2.5px $dark-blue; + border: $dark-blue solid 2.5px; +} + +.presItem-slide.group { + border-radius: 5px; +} + +.presItem-slide.activeGroup { + border-radius: 5px; + box-shadow: 0 0 0px 2.5px $dark-blue; +} + +.presItem-groupSlideContainer { + position: absolute; + /* grid-row: 3; */ + /* grid-column: 1/8; */ + top: 28; + display: block; + background: #92adb9; + width: 100%; + height: 10px; + border-radius: 0px 0px 5px 5px; + height: calc(100% - 28px); + display: grid; + grid-template-rows: auto auto auto; + grid-template-columns: 100%; + justify-items: left; + align-items: center; +} + +.presItem-groupSlide { + position: relative; + background-color: #d5dce2; + border-radius: 5px; + height: calc(100% - 7px); + width: calc(100% - 20px); + left: 15px; + /* height: 20px; */ + /* width: calc(100% - 19px); */ + display: flex; + grid-template-rows: 16px 10px auto; + grid-template-columns: max-content max-content max-content max-content auto; +} + +.presItem-multiDrag { + font-family: Roboto; + font-weight: 600; + color: white; + text-align: center; + justify-content: center; + align-content: center; + width: 100px; + height: 30px; + position: absolute; + background-color: $dark-blue; + z-index: 4000; + border-radius: 10px; + box-shadow: black 0.4vw 0.4vw 0.8vw; + line-height: 30px; +} + +.presItem-miniSlide { + font-weight: 700; + font-size: 12; + grid-column: 1/8; + align-self: center; + justify-self: center; + background-color: #d5dce2; + width: 26px; + text-align: center; + height: 26px; + line-height: 28px; + border-radius: 100%; +} + +.presItem-miniSlide.active { + box-shadow: 0 0 0px 1.5px $dark-blue; +} + +.expandButton { + cursor: pointer; + position: absolute; + border-radius: 100px; + bottom: 0; + left: -18; + z-index: 300; + width: 15px; + height: 15px; + display: flex; + font-size: 12px; + justify-self: center; + align-self: center; + background-color: #92adb9; + color: white; + justify-content: center; + align-items: center; + transition: 0.2s; + margin-right: 3px; +} + +.expandButton:hover { + background-color: rgba(0, 0, 0, 1); + transform: scale(1.2); +} + +.presItem-groupNum { + color: #d5dce2; + position: absolute; + left: -15px; + top: 1; + font-weight: 600; + font-size: 12; +} diff --git a/src/client/views/nodes/trails/PresSlideBox.tsx b/src/client/views/nodes/trails/PresSlideBox.tsx new file mode 100644 index 000000000..3dbb3da88 --- /dev/null +++ b/src/client/views/nodes/trails/PresSlideBox.tsx @@ -0,0 +1,628 @@ +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 }, +}); diff --git a/src/client/views/nodes/trails/index.ts b/src/client/views/nodes/trails/index.ts index 7b18974df..a5bc55221 100644 --- a/src/client/views/nodes/trails/index.ts +++ b/src/client/views/nodes/trails/index.ts @@ -1,3 +1,3 @@ export * from './PresBox'; -export * from './PresElementBox'; +export * from './PresSlideBox'; export * from './PresEnums'; -- cgit v1.2.3-70-g09d2