From 87484331f805f583c98226c5a8c6b1ec5fc88925 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 13 May 2025 13:37:54 -0400
Subject: fixed setting up link anchors for video to be added to Doc. fixed
 imageTemplate to work with templates in tabs.  fixed link following from link
 menu.  cleaned up getView with sidebars to open them when needed properly..

---
 .../collections/collectionFreeForm/CollectionFreeFormView.tsx      | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index bb3c59eae..77615c650 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1738,10 +1738,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         );
 
         if (addAsAnnotation) {
-            if (Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), null) !== undefined) {
-                Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), [])?.push(anchor);
+            const fieldKey = this._props.isAnnotationOverlay ? this._props.fieldKey : this._props.fieldKey + '_annotations';
+            if (Cast(this.dataDoc[fieldKey], listSpec(Doc), null) !== undefined) {
+                Cast(this.dataDoc[fieldKey], listSpec(Doc), [])?.push(anchor);
             } else {
-                this.dataDoc[this._props.fieldKey + '_annotations'] = new List<Doc>([anchor]);
+                this.dataDoc[fieldKey] = new List<Doc>([anchor]);
             }
         }
         return anchor;
-- 
cgit v1.2.3-70-g09d2


From 2c26d7c96e56344a1a73a7c6f6dec3d6b0f0ee7e Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 16 May 2025 11:46:22 -0400
Subject: changed isGroup to freeform_isGroup to fix issues with groups that
 are changed into other collection types.  fixed toggleTarget links to work
 when opening in a light box (previously they flickered on, then off).  fixed
 text boxes with a number field as data to act like a number input box.

---
 src/client/documents/Documents.ts                  |  2 +-
 src/client/util/DocumentManager.ts                 |  1 +
 src/client/views/DocumentDecorations.tsx           |  8 ++--
 src/client/views/PropertiesButtons.tsx             |  4 +-
 src/client/views/PropertiesView.tsx                |  6 +--
 src/client/views/StyleProvider.tsx                 |  8 ++--
 src/client/views/collections/CollectionView.tsx    |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 42 +++++++++--------
 .../collections/collectionFreeForm/MarqueeView.tsx |  4 +-
 src/client/views/nodes/DocumentLinksButton.tsx     |  1 -
 src/client/views/nodes/DocumentView.tsx            |  2 +-
 src/client/views/nodes/KeyValueBox.tsx             |  8 ++--
 .../views/nodes/formattedText/FormattedTextBox.tsx | 12 ++---
 src/client/views/nodes/trails/PresBox.tsx          |  2 +-
 src/fields/Doc.ts                                  | 54 +++++++++++++---------
 15 files changed, 85 insertions(+), 71 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 2df6f3e23..9aa1d53c2 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -361,7 +361,6 @@ export class DocumentOptions {
     isBaseProto?: BOOLt = new BoolInfo('is doc a base level prototype for data documents as opposed to data documents which are prototypes for layout documents.  base protos are not cloned during a deep');
     isTemplateForField?: string; // the field key for which the containing document is a rendering template
     isTemplateDoc?: BOOLt = new BoolInfo('is the document a template for creating other documents');
-    isGroup?: BOOLt = new BoolInfo('should collection use a grouping UI behavior');
     isFolder?: BOOLt = new BoolInfo('is document a tree view folder');
     _isTimelineLabel?: BOOLt = new BoolInfo('is document a timeline label');
     isLightbox?: BOOLt = new BoolInfo('whether a collection acts as a lightbox by opening lightbox links by hiding all other documents in collection besides link target');
@@ -401,6 +400,7 @@ export class DocumentOptions {
 
     // freeform properties
     freeform?: STRt = new StrInfo('');
+    freeform_isGroup?: BOOLt = new BoolInfo('should collection use a grouping UI behavior');
     _freeform_backgroundGrid?: BOOLt = new BoolInfo('whether background grid is shown on freeform collections');
     _freeform_scale_min?: NUMt = new NumInfo('how far out a view can zoom (used by image/videoBoxes that are clipped');
     _freeform_scale_max?: NUMt = new NumInfo('how far in a view can zoom (used by sidebar freeform views');
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 422613968..1c2ffab1b 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -274,6 +274,7 @@ export class DocumentManager {
             // even if we found the document view, if the target is a lightbox, we try to open it in the lightbox to preserve lightbox semantics (eg, there's only one active doc in the lightbox)
             const target = DocCast(targetDoc.annotationOn, targetDoc)!;
             const compView = this.getDocumentView(DocCast(target.embedContainer))?.ComponentView;
+            options.didMove = target.hidden || options.didMove ? true : false;
             if ((compView?.addDocTab ?? compView?._props.addDocTab)?.(target, options.openLocation)) {
                 await new Promise<void>(waitres => {
                     setTimeout(() => waitres());
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index ab665e984..1e7f4fb52 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -524,7 +524,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     resizeViewForOutpainting = (docView: DocumentView, refPt: number[], scale: { x: number; y: number }, opts: { dragHdl: string; shiftKey: boolean }) => {
         const doc = docView.Document;
 
-        if (doc.isGroup) {
+        if (Doc.IsFreeformGroup(doc)) {
             DocListCast(doc.data)
                 .map(member => DocumentView.getDocumentView(member, docView)!)
                 .forEach(member => this.resizeViewForOutpainting(member, refPt, scale, opts));
@@ -598,14 +598,14 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     //
     // determines if anything being dragged directly or via a group has a fixed aspect ratio (in which case we resize uniformly)
     //
-    hasFixedAspect = (doc: Doc): boolean => (doc.isGroup ? DocListCast(doc.data).some(this.hasFixedAspect) : !BoolCast(doc._layout_nativeDimEditable));
+    hasFixedAspect = (doc: Doc): boolean => (Doc.IsFreeformGroup(doc) ? DocListCast(doc.data).some(this.hasFixedAspect) : !BoolCast(doc._layout_nativeDimEditable));
 
     //
     // resize a single DocumentView about the specified reference point, possibly setting/updating the native dimensions of the Doc
     //
     resizeView = (docView: DocumentView, refPt: number[], scale: { x: number; y: number }, opts: { dragHdl: string; freezeNativeDims: boolean }) => {
         const doc = docView.Document;
-        if (doc.isGroup) {
+        if (Doc.IsFreeformGroup(doc)) {
             DocListCast(doc.data)
                 .map(member => DocumentView.getDocumentView(member, docView)!)
                 .forEach(member => this.resizeView(member, refPt, scale, opts));
@@ -737,7 +737,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             hideDecorations ||
             seldocview._props.hideOpenButton ||
             seldocview.Document.layout_hideOpenButton ||
-            DocumentView.Selected().some(docView => docView.Document._dragOnlyWithinContainer || docView.Document.isGroup || docView.Document.layout_hideOpenButton) ||
+            DocumentView.Selected().some(docView => docView.Document._dragOnlyWithinContainer || docView.Document.layout_hideOpenButton) ||
             this._isRounding ||
             this._isRotating;
         const hideDeleteButton =
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index ded342df0..2c84d7fe7 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -238,8 +238,8 @@ export class PropertiesButtons extends React.Component {
     //         on => `Display collection as a Group`,
     //         on => 'object-group',
     //         (dv, doc) => {
-    //             doc.isGroup = !doc.isGroup;
-    //             doc.forceActive = doc.isGroup;
+    //             docfreeform_isGroup = !docfreeform_isGroup;
+    //             doc.forceActive = docfreeform_isGroup;
     //         }
     //     );
     // }
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index acf6f928a..06463b2a2 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -81,7 +81,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
 
     @computed get selectedDoc() {
-        return DocumentView.SelectedSchemaDoc() || this.selectedDocumentView?.Document || Doc.ActiveDashboard;
+        return DocumentView.SelectedSchemaDoc() ?? this.selectedDocumentView?.Document ?? Doc.ActiveDashboard;
     }
 
     @computed get selectedLink() {
@@ -89,7 +89,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
 
     @computed get selectedLayoutDoc() {
-        return DocumentView.SelectedSchemaDoc() || this.selectedDocumentView?.layoutDoc || Doc.ActiveDashboard;
+        return DocumentView.SelectedSchemaDoc() ?? this.selectedDocumentView?.layoutDoc ?? Doc.ActiveDashboard;
     }
     @computed get selectedDocumentView() {
         return DocumentView.Selected().lastElement();
@@ -149,7 +149,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         return this.selectedDoc?.type === DocumentType.INK;
     }
     @computed get isGroup() {
-        return this.selectedDoc?.isGroup;
+        return Doc.IsFreeformGroup(this.selectedDoc);
     }
     @computed get isStack() {
         return [
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 9110e198f..b52f17102 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -154,7 +154,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                 const highlightStyle = ['solid', 'dashed', 'solid', 'solid', 'solid'][highlightIndex];
                 if (highlightIndex) {
                     return {
-                        highlightStyle: doc.isGroup ? "dotted": highlightStyle,
+                        highlightStyle: Doc.IsFreeformGroup(doc) ? "dotted": highlightStyle,
                         highlightColor,
                         highlightIndex,
                         highlightStroke: BoolCast(layoutDoc?.layout_isSvg),
@@ -284,7 +284,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                             ? SnappingManager.userBackgroundColor
                             : doc?.annotationOn
                             ? '#00000010' // faint interior for collections on PDFs, images, etc
-                            : doc?.isGroup
+                            : doc && Doc.IsFreeformGroup(doc)
                             ? undefined
                             : doc?._type_collection === CollectionViewType.Stacking ?
                             (Colors.DARK_GRAY)
@@ -308,7 +308,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                         doc?.layout_boxShadow,
                         doc?._type_collection === CollectionViewType.Pile
                             ? '4px 4px 10px 2px'
-                            : lockedPosition() || doc?.isGroup || LayoutTemplateString
+                            : lockedPosition() || (doc && Doc.IsFreeformGroup(doc)) || LayoutTemplateString
                             ? undefined // groups have no drop shadow -- they're supposed to be "invisible".  LayoutString's imply collection is being rendered as something else (e.g., title of a Slide)
                             : `${Colors.DARK_GRAY} ${StrCast(doc.layout_boxShadow, '0.2vw 0.2vw 0.8vw')}`
                     );
@@ -338,7 +338,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
             if (SnappingManager.ExploreMode || doc?.layout_unrendered) return isInk() ? 'visiblePainted' : 'all';
             if (pointerEvents?.() === 'none') return 'none';
             if (opacity() === 0) return 'none';
-            if (isGroupActive?.() ) return isInk()  ? 'visiblePainted':  (doc?.isGroup ) ? undefined: 'all';
+            if (isGroupActive?.()) return isInk()  ? 'visiblePainted':  doc && Doc.IsFreeformGroup(doc) ? undefined: 'all';
             if (isDocumentActive?.()) return isInk() ? 'visiblePainted' : 'all';
             return undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active
         case StyleProp.Decorations: {
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index eb9caf29d..72c8c3f8c 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -113,7 +113,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
     };
 
     setupViewTypes(category: string, func: (type_collection: CollectionViewType) => Doc) {
-        if (!Doc.IsSystem(this.Document) && this.Document._type_collection !== CollectionViewType.Docking && !this.dataDoc.isGroup && !this.Document.annotationOn) {
+        if (!Doc.IsSystem(this.Document) && this.Document._type_collection !== CollectionViewType.Docking && !this.Document.annotationOn) {
             // prettier-ignore
             const subItems: ContextMenuProps[] = [
                 { description: 'Freeform',    event: () => func(CollectionViewType.Freeform), icon: 'signature' },
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 77615c650..3571dab1a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -176,7 +176,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return renderableEles;
     }
     @computed get fitContentsToBox() {
-        return (this._props.fitContentsToBox?.() || this.Document._freeform_fitContentsToBox) && !this.isAnnotationOverlay;
+        return (this._props.fitContentsToBox?.() || this.Document._freeform_fitContentsToBox || this.Document.freeform_isGroup) && !this.isAnnotationOverlay;
     }
     @computed get nativeWidth() {
         return this._props.NativeWidth?.() || Doc.NativeWidth(this.Document);
@@ -342,7 +342,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
      */
     focusOnPoint = (options: FocusViewOptions) => {
         const { pointFocus, zoomTime, didMove } = options;
-        if (!this.Document.isGroup && pointFocus && !didMove) {
+        if (!this.Document.freeform_isGroup && pointFocus && !didMove) {
             const dfltScale = this.isAnnotationOverlay ? 1 : 0.25;
             if (this.layoutDoc[this.scaleFieldKey] !== dfltScale) {
                 this.zoomSmoothlyAboutPt(this.screenToFreeformContentsXf.transformPoint(pointFocus.X, pointFocus.Y), dfltScale, zoomTime);
@@ -380,7 +380,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
      * @returns
      */
     focus = (anchor: Doc, options: FocusViewOptions) => {
-        if (anchor.isGroup && !options.docTransform && options.contextPath?.length) {
+        if (Doc.IsFreeformGroup(anchor) && !options.docTransform && options.contextPath?.length) {
             // don't focus on group if there's a context path because we're about to focus on a group item
             // which will override any group focus. (If we allowed the group to focus, it would mark didMove even if there were no net movement)
             return undefined;
@@ -395,7 +395,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
         const xfToCollection = options?.docTransform ?? Transform.Identity();
         const savedState = { panX: NumCast(this.Document[this.panXFieldKey]), panY: NumCast(this.Document[this.panYFieldKey]), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined };
-        const cantTransform = this.fitContentsToBox || ((this.Document.isGroup || this.layoutDoc._lockedTransform) && !DocumentView.LightboxDoc());
+        const cantTransform = this.fitContentsToBox || ((this.Document.freeform_isGroup || this.layoutDoc._lockedTransform) && !DocumentView.LightboxDoc());
         const { panX, panY, scale } = cantTransform || (!options.willPan && !options.willZoomCentered) ? savedState : this.calculatePanIntoView(anchor, xfToCollection, options?.willZoomCentered ? (options?.zoomScale ?? 0.75) : undefined);
 
         // focus on the document in the collection
@@ -514,7 +514,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         this._downTime = Date.now();
         const scrollMode = e.altKey ? (Doc.UserDoc().freeformScrollMode === freeformScrollMode.Pan ? freeformScrollMode.Zoom : freeformScrollMode.Pan) : Doc.UserDoc().freeformScrollMode;
         if (e.button === 0 && (!(e.ctrlKey && !e.metaKey) || scrollMode !== freeformScrollMode.Pan) && this._props.isContentActive()) {
-            if (!this.Document.isGroup) {
+            if (!this.Document.freeform_isGroup) {
                 // group freeforms don't pan when dragged -- instead let the event go through to allow the group itself to drag
                 // prettier-ignore
                 const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
@@ -1271,7 +1271,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @action
     zoom = (pointX: number, pointY: number, deltaY: number): void => {
-        if (this.Document.isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return;
+        if (this.Document.freeform_isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return;
         let deltaScale = deltaY > 0 ? 1 / 1.05 : 1.05;
         if (deltaScale < 0) deltaScale = -deltaScale;
         const [x, y] = this.screenToFreeformContentsXf.transformPoint(pointX, pointY);
@@ -1298,7 +1298,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @action
     onPointerWheel = (e: React.WheelEvent): void => {
-        if (this.Document.isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom
+        if (this.Document.freeform_isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom
         SnappingManager.TriggerUserPanned();
         if (this.layoutDoc._Transform || this.Document.treeView_OutlineMode === TreeViewType.outline) return;
         e.stopPropagation();
@@ -1422,7 +1422,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @action
     zoomSmoothlyAboutPt(docpt: number[], scale: number, transitionTime = 500) {
-        if (this.Document.isGroup) return;
+        if (this.Document.freeform_isGroup) return;
         this.setPanZoomTransition(transitionTime);
         const screenXY = this.screenToFreeformContentsXf.inverse().transformPoint(docpt[0], docpt[1]);
         this.layoutDoc[this.scaleFieldKey] = scale;
@@ -1505,7 +1505,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     removeDocument = (docs: Doc | Doc[], annotationKey?: string | undefined) => {
         const ret = !!this._props.removeDocument?.(docs, annotationKey);
         // if this is a group and we have fewer than 2 Docs, then just promote what's left to our parent and get rid of the group.
-        if (ret && DocListCast(this.dataDoc[annotationKey ?? this.fieldKey]).length < 2 && this.Document.isGroup) {
+        if (ret && DocListCast(this.dataDoc[annotationKey ?? this.fieldKey]).length < 2 && this.Document.freeform_isGroup) {
             this.promoteCollection();
         }
         return ret;
@@ -1548,7 +1548,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 searchFilterDocs={this.searchFilterDocs}
                 isDocumentActive={childLayout.pointerEvents === 'none' ? returnFalse : this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this.isContentActive}
                 isContentActive={this.childContentsActive}
-                focus={this.Document.isGroup ? this.groupFocus : this.isAnnotationOverlay ? this._props.focus : this.focus}
+                focus={this.Document.freeform_isGroup ? this.groupFocus : this.isAnnotationOverlay ? this._props.focus : this.focus}
                 addDocTab={this.addDocTab}
                 addDocument={this._props.addDocument}
                 removeDocument={this.removeDocument}
@@ -1733,7 +1733,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         });
         PinDocView(
             anchor,
-            { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ? { ...pinProps.pinData, poslayoutview: pinProps.pinData.dataview } : {}), pannable: !this.Document.isGroup, collectionType: true, filters: true } },
+            { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ? { ...pinProps.pinData, poslayoutview: pinProps.pinData.dataview } : {}), pannable: !this.Document.freeform_isGroup, collectionType: true, filters: true } },
             this.Document
         );
 
@@ -1767,7 +1767,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 this._firstRender = false;
                 this._disposers.groupBounds = reaction(
                     () => {
-                        if (this.Document.isGroup && this.childDocs.length === this.childDocList?.length) {
+                        if (this.Document.freeform_isGroup && this.childDocs.length === this.childDocList?.length) {
                             const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: NumCast(cd._width), height: NumCast(cd._height) }));
                             return aggregateBounds(clist, NumCast(this.layoutDoc._xMargin), NumCast(this.layoutDoc._yMargin));
                         }
@@ -1929,8 +1929,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
         const appearance = ContextMenu.Instance.findByDescription('Appearance...');
         const appearanceItems = appearance?.subitems ?? [];
-        !this.Document.isGroup && appearanceItems.push({ description: 'Reset View', event: this.resetView, icon: 'compress-arrows-alt' });
-        !this.Document.isGroup && appearanceItems.push({ description: 'Toggle Auto Reset View', event: this.toggleResetView, icon: 'compress-arrows-alt' });
+        !this.Document.freeform_isGroup && appearanceItems.push({ description: 'Reset View', event: this.resetView, icon: 'compress-arrows-alt' });
+        !this.Document.freeform_isGroup && appearanceItems.push({ description: 'Toggle Auto Reset View', event: this.toggleResetView, icon: 'compress-arrows-alt' });
         if (this._props.setContentViewBox === emptyFunction) {
             !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' });
             return;
@@ -1948,7 +1948,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         !Doc.noviceMode && appearanceItems.push({ description: `update icon`, event: () => this.updateIcon(), icon: 'compress-arrows-alt' });
         this._props.renderDepth && appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' });
 
-        this.Document.isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: this.transcribeStrokes, icon: 'font' });
+        this.Document.freeform_isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: this.transcribeStrokes, icon: 'font' });
 
         !Doc.noviceMode ? appearanceItems.push({ description: 'Arrange contents in grid', event: this.layoutDocsInGrid, icon: 'table' }) : null;
         !Doc.noviceMode ? appearanceItems.push({ description: (this.Document._freeform_useClusters ? 'Hide' : 'Show') + ' Clusters', event: () => this._clusters.updateClusters(!this.Document._freeform_useClusters), icon: 'braille' }) : null;
@@ -2007,7 +2007,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     transcribeStrokes = undoable(() => {
-        if (this.Document.isGroup && this.Document.transcription) {
+        if (this.Document.freeform_isGroup && this.Document.transcription) {
             const text = StrCast(this.Document.transcription);
             const lines = text.split('\n');
             const height = 30 + 15 * lines.length;
@@ -2025,7 +2025,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     dragStarting = (snapToDraggedDoc: boolean = false, showGroupDragTarget: boolean = true, visited = new Set<Doc>()) => {
         if (visited.has(this.Document)) return;
         visited.add(this.Document);
-        showGroupDragTarget && (this.GroupChildDrag = BoolCast(this.Document.isGroup));
+        showGroupDragTarget && (this.GroupChildDrag = BoolCast(this.Document.freeform_isGroup));
         const activeDocs = this.getActiveDocuments();
         const size = this.screenToFreeformContentsXf.transformDirection(this._props.PanelWidth(), this._props.PanelHeight());
         const selRect = { left: this.panX() - size[0] / 2, top: this.panY() - size[1] / 2, width: size[0], height: size[1] };
@@ -2033,13 +2033,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const isDocInView = (doc: Doc, rect: { left: number; top: number; width: number; height: number }) => intersectRect(docDims(doc), rect);
 
         const snappableDocs = activeDocs.filter(doc => doc.z === undefined && isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to
-        activeDocs.filter(doc => doc.isGroup && SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc)).forEach(doc => DocumentView.getDocumentView(doc)?.ComponentView?.dragStarting?.(snapToDraggedDoc, false, visited));
+        activeDocs
+            .filter(doc => Doc.IsFreeformGroup(doc) && SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc))
+            .forEach(doc => DocumentView.getDocumentView(doc)?.ComponentView?.dragStarting?.(snapToDraggedDoc, false, visited));
 
         const horizLines: number[] = [];
         const vertLines: number[] = [];
         const invXf = this.screenToFreeformContentsXf.inverse();
         snappableDocs
-            .filter(doc => !doc.isGroup && (snapToDraggedDoc || (SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc))))
+            .filter(doc => !Doc.IsFreeformGroup(doc) && (snapToDraggedDoc || (SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc))))
             .forEach(doc => {
                 const { left, top, width, height } = docDims(doc);
                 const topLeftInScreen = invXf.transformPoint(left, top);
@@ -2131,7 +2133,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 {...this._props}
                 ref={this._marqueeViewRef}
                 Doc={this.Document}
-                ungroup={this.Document.isGroup ? this.promoteCollection : undefined}
+                ungroup={this.Document.freeform_isGroup ? this.promoteCollection : undefined}
                 nudge={this.isAnnotationOverlay || this._props.renderDepth > 0 ? undefined : this.nudge}
                 addDocTab={this.addDocTab}
                 slowLoadDocuments={this.slowLoadDocuments}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 3cc7c0f2d..37959a5f0 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -372,7 +372,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             ? creator(selected, { title: 'nested stack' })
             : ((doc: Doc) => {
                   doc.$data = new List<Doc>(selected);
-                  doc.$isGroup = makeGroup;
+                  doc.$freeform_isGroup = makeGroup;
                   doc.$title = makeGroup ? 'grouping' : 'nested freeform';
                   doc._freeform_panX = doc._freeform_panY = 0;
                   return doc;
@@ -508,7 +508,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             _layout_showSidebar: true,
             title: 'overview',
         });
-        const portal = Docs.Create.FreeformDocument(selected, { title: 'summarized documents', x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: 'transparent' });
+        const portal = Docs.Create.FreeformDocument(selected, { title: 'summarized documents', x: this.Bounds.left + 200, y: this.Bounds.top, freeform_isGroup: true, backgroundColor: 'transparent' });
         DocUtils.MakeLink(summary, portal, { link_relationship: 'summary of:summarized by' });
 
         portal.hidden = true;
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index c35a329c9..d30f00829 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -38,7 +38,6 @@ export class DocButtonState {
     // eslint-disable-next-line no-use-before-define
     public static _instance: DocButtonState | undefined;
     public static get Instance() {
-        // eslint-disable-next-line no-return-assign
         return DocButtonState._instance ?? (DocButtonState._instance = new DocButtonState());
     }
     constructor() {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index b0415ef18..9b73cc073 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -744,7 +744,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field
     @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
-        const noBackground = this.Document.isGroup && !this._componentView?.isUnstyledView?.() && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent');
+        const noBackground = Doc.IsFreeformGroup(this.Document) && !this._componentView?.isUnstyledView?.() && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent');
         return (
             <>
                 <div
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index aa66b5ba9..606f63d6d 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -273,15 +273,15 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() {
     };
 
     createFieldView = (templateDoc: Doc, row: KeyValuePair) => {
-        const metaKey = row._props.keyName;
-        const fieldTempDoc = Doc.IsDelegateField(templateDoc, metaKey) ? Doc.MakeDelegate(templateDoc) : Doc.MakeEmbedding(templateDoc);
-        fieldTempDoc.title = metaKey;
+        const keyName = row._props.keyName;
+        const fieldTempDoc = Doc.IsDelegateField(templateDoc, keyName) ? Doc.MakeDelegate(templateDoc) : Doc.MakeEmbedding(templateDoc);
+        fieldTempDoc.title = keyName;
         fieldTempDoc.layout_fitWidth = true;
         fieldTempDoc._xMargin = 10;
         fieldTempDoc._yMargin = 10;
         fieldTempDoc._width = 100;
         fieldTempDoc._height = 40;
-        fieldTempDoc.layout = this.inferType(templateDoc[metaKey], metaKey);
+        fieldTempDoc.layout = this.inferType(templateDoc[keyName], keyName);
         return fieldTempDoc;
     };
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index bab97f681..4189fcc0c 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -428,10 +428,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         const newAutoLinks = new Set<Doc>();
         const oldAutoLinks = Doc.Links(this.Document).filter(
             link => 
-                ((!Doc.isTemplateForField(this.Document) &&
-                    ((DocCast(link.link_anchor_1) && !Doc.isTemplateForField(DocCast(link.link_anchor_1)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) &&
-                    ((DocCast(link.link_anchor_2) && !Doc.isTemplateForField(DocCast(link.link_anchor_2)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) ||
-                 (Doc.isTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) &&
+                ((!Doc.IsTemplateForField(this.Document) &&
+                    ((DocCast(link.link_anchor_1) && !Doc.IsTemplateForField(DocCast(link.link_anchor_1)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) &&
+                    ((DocCast(link.link_anchor_2) && !Doc.IsTemplateForField(DocCast(link.link_anchor_2)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) ||
+                 (Doc.IsTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) &&
                 link.link_relationship === LinkManager.AutoKeywords
         ); // prettier-ignore
         if (this.EditorView?.state.doc.textContent) {
@@ -1216,7 +1216,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 const protoTime = protoData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? (DateCast(DocCast(this.dataDoc.proto)?.[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0;
                 const recentData = dataTime >= layoutTime ? (protoTime >= dataTime ? protoData : dataData) : layoutTime >= protoTime ? layoutData : protoData;
                 const whichData = recentData ?? (this.layoutDoc.isTemplateDoc ? layoutData : protoData) ?? protoData;
-                return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? StrCast(whichData)) };
+                return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? NumCast(whichData)?.toString() ?? StrCast(whichData)) };
             },
             incomingValue => {
                 if (this.EditorView && this.ApplyingChange !== this.fieldKey) {
@@ -1767,7 +1767,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         if (this.ProseRef?.children[0] !== e.nativeEvent.target) return;
         if (!(this.EditorView?.state.selection instanceof NodeSelection) || this.EditorView.state.selection.node.type !== this.EditorView.state.schema.nodes.footnote) {
             const stordMarks = this.EditorView?.state.storedMarks?.slice();
-            if (!(this.EditorView?.state.selection instanceof NodeSelection)) {
+            if (!(this.EditorView?.state.selection instanceof NodeSelection) && typeof this.dataDoc[this.fieldKey] !== 'number') {
                 this.autoLink();
                 if (this.EditorView?.state.tr) {
                     const tr = stordMarks?.reduce((tr2, m) => {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 11f35b8ef..cb2a1f13f 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -702,7 +702,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 transTime + 10
             );
         }
-        if ((pinDataTypes?.pannable || (!pinDataTypes && (activeItem.config_viewBounds !== undefined || activeItem.config_panX !== undefined || activeItem.config_viewScale !== undefined))) && !bestTarget.isGroup) {
+        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] };
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 0d11b9743..e110550e6 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -536,23 +536,35 @@ export namespace Doc {
     export function GetT<T extends FieldType>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): FieldResult<T> {
         return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult<T>;
     }
-    export function isTemplateDoc(doc: Doc) {
-        return GetT(doc, 'isTemplateDoc', 'boolean', true);
-    }
-    export function isTemplateForField(doc: Doc) {
-        return GetT(doc, 'isTemplateForField', 'string', true);
-    }
-    export function IsDataProto(doc: Doc) {
-        return GetT(doc, 'isDataDoc', 'boolean', true);
-    }
-    export function IsBaseProto(doc: Doc) {
-        return GetT(doc, 'isBaseProto', 'boolean', true);
-    }
-    export function IsSystem(doc: Doc) {
-        return GetT(doc, 'isSystem', 'boolean', true);
-    }
-    export function IsDelegateField(doc: Doc, fieldKey: string) {
-        return doc && Get(doc, fieldKey, true) !== undefined;
+    /**
+     * Tests whether the Doc is flagged as being a template.
+     * Templates can be set as a layout for another target Doc. When rendered by the target Doc, the template
+     * creates an instance of itself that holds rendering data specific to the target
+     * @param doc
+     */
+    export function IsTemplateDoc(doc: Doc)      { return GetT(doc, 'isTemplateDoc', 'boolean', true); } // prettier-ignore
+    /**
+     * Tests whether the Doc is flagged as being a template for rendering a specific field  of a Doc
+     * When flagged as field template and rendered, the template will redirect its componentView to write to the
+     * specified template field.  In general, a compound Doc template will contain multiple field templates, one for each of the
+     * data fields rendered by the compound template.
+     * @param doc
+     * @returns
+     */
+    export function IsTemplateForField(doc: Doc) { return GetT(doc, 'isTemplateForField', 'string', true); } // prettier-ignore
+    export function IsDataProto(doc: Doc)        { return GetT(doc, 'isDataDoc', 'boolean', true); } // prettier-ignore
+    export function IsBaseProto(doc: Doc)        { return GetT(doc, 'isBaseProto', 'boolean', true); } // prettier-ignore
+    export function IsSystem(doc: Doc)           { return GetT(doc, 'isSystem', 'boolean', true); } // prettier-ignore
+    export function IsDelegateField(doc: Doc, fieldKey: string) { return doc && Get(doc, fieldKey, true) !== undefined; } // prettier-ignore
+    /**
+     * Tests whether a doc is a freeform collection that renders as a group.
+     * The group variant of a collection automatically resizes so that none of its contents
+     * are ever hidden.
+     * @param doc doc to test for being a freeform group
+     * @returns boolean
+     */
+    export function IsFreeformGroup(doc: Doc) {
+        return doc.freeform_isGroup && doc.type_collection === CollectionViewType.Freeform;
     }
     //
     // this will write the value to the key on either the data doc or the embedding doc.  The choice
@@ -734,7 +746,7 @@ export namespace Doc {
     const FindDocsInRTF = new RegExp(/(audioId|textId|anchorId|docId)"\s*:\s*"(.*?)"/g);
 
     export function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<string, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], pruneDocs: Doc[], cloneLinks: boolean, cloneTemplates: boolean): Doc {
-        if (Doc.IsBaseProto(doc) || ((Doc.isTemplateDoc(doc) || Doc.isTemplateForField(doc)) && !cloneTemplates)) {
+        if (Doc.IsBaseProto(doc) || ((Doc.IsTemplateDoc(doc) || Doc.IsTemplateForField(doc)) && !cloneTemplates)) {
             return doc;
         }
         if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
@@ -808,7 +820,7 @@ export namespace Doc {
                 const docAtKey = DocCast(clone[key]);
                 if (docAtKey && !Doc.IsSystem(docAtKey)) {
                     if (!Array.from(cloneMap.values()).includes(docAtKey)) {
-                        clone[key] = !cloneTemplates && (Doc.isTemplateDoc(docAtKey) || Doc.isTemplateForField(docAtKey)) ? docAtKey : cloneMap.get(docAtKey[Id]);
+                        clone[key] = !cloneTemplates && (Doc.IsTemplateDoc(docAtKey) || Doc.IsTemplateForField(docAtKey)) ? docAtKey : cloneMap.get(docAtKey[Id]);
                     } else {
                         repairClone(docAtKey, cloneMap, cloneTemplates, visited);
                     }
@@ -859,7 +871,7 @@ export namespace Doc {
      */
     export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc, layoutFieldKey?: string) {
         // nothing to do if the layout isn't a template or we don't have a target that's different than the template
-        if (!targetDoc || templateLayoutDoc === targetDoc || (!Doc.isTemplateForField(templateLayoutDoc) && !Doc.isTemplateDoc(templateLayoutDoc))) {
+        if (!targetDoc || templateLayoutDoc === targetDoc || (!Doc.IsTemplateForField(templateLayoutDoc) && !Doc.IsTemplateDoc(templateLayoutDoc))) {
             return templateLayoutDoc;
         }
 
@@ -922,7 +934,7 @@ export namespace Doc {
             console.log('Warning: GetLayoutDataDocPair childDoc not defined');
             return { layout: childDoc, data: childDoc };
         }
-        const data = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!Doc.isTemplateDoc(childDoc) && !Doc.isTemplateForField(childDoc)) ? undefined : containerDataDoc;
+        const data = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!Doc.IsTemplateDoc(childDoc) && !Doc.IsTemplateForField(childDoc)) ? undefined : containerDataDoc;
         const templateRoot = DocCast(containerDoc?.rootDocument);
         return { layout: Doc.expandTemplateLayout(childDoc, templateRoot, layoutFieldKey), data };
     }
-- 
cgit v1.2.3-70-g09d2


From dd84952de3381049aca6742054f28bf502df2e16 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 19 Jun 2025 15:21:53 -0400
Subject: removing references to Box types from documentdecorations - added
 showBorderRounding to viewBoxInterface.

---
 src/client/views/ViewBoxInterface.ts                                  | 1 +
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx   | 3 ++-
 src/client/views/nodes/ImageBox.tsx                                   | 3 ++-
 src/client/views/nodes/formattedText/FormattedTextBox.tsx             | 3 ++-
 src/client/views/nodes/scrapbook/ScrapbookBox.tsx                     | 4 +++-
 5 files changed, 10 insertions(+), 4 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
index 83e395867..514dc4ae8 100644
--- a/src/client/views/ViewBoxInterface.ts
+++ b/src/client/views/ViewBoxInterface.ts
@@ -26,6 +26,7 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React
     docEditorView?: () => void;
     autoTag?: () => void; // auto tag the document
     isOutpaintable?: () => boolean; // can document be resized and outpainted
+    showBorderRounding?: () => boolean; // can document borders be rounded
     showSmartDraw?: (x: number, y: number, regenerate?: boolean) => void;
     updateIcon?: (usePanelDimensions?: boolean) => Promise<void>; // updates the icon representation of the document
     getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 3571dab1a..32ace463d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -8,7 +8,7 @@ import { computedFn } from 'mobx-utils';
 import * as React from 'react';
 import { AiOutlineSend } from 'react-icons/ai';
 import ReactLoading from 'react-loading';
-import { ClientUtils, DashColor, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../../ClientUtils';
+import { ClientUtils, DashColor, lightOrDark, OmitKeys, returnFalse, returnTrue, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../../ClientUtils';
 import { DateField } from '../../../../fields/DateField';
 import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc';
 import { DocData, DocLayout, Height, Width } from '../../../../fields/DocSymbols';
@@ -2065,6 +2065,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
         this.childDocs.some(doc => !this._renderCutoffData.get(doc[Id])) && setTimeout(this.incrementalRender, 1);
     });
+    showBorderRounding = returnTrue;
     showPresPaths = () => SnappingManager.ShowPresPaths;
     brushedView = () => this._brushedView;
     gridColor = () => DashColor(lightOrDark(this.backgroundColor)).fade(0.5).toString(); // prettier-ignore
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 356f9e13f..4d8d486b1 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -8,7 +8,7 @@ import { extname } from 'path';
 import * as React from 'react';
 import { AiOutlineSend } from 'react-icons/ai';
 import ReactLoading from 'react-loading';
-import { ClientUtils, imageUrlToBase64, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils';
+import { ClientUtils, imageUrlToBase64, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon, returnTrue } from '../../../ClientUtils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
@@ -994,6 +994,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return { width, height };
     };
     savedAnnotations = () => this._savedAnnotations;
+    showBorderRounding = returnTrue;
     rejectDrop = (de: DragManager.DropEvent, subView?: DocumentView | undefined) => (this.dataDoc[this.fieldKey] === undefined ? true : (this._props.rejectDrop?.(de, subView) ?? false));
     render() {
         TraceMobx();
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index d700ce9f8..07cb795f1 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -13,7 +13,7 @@ import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transacti
 import { EditorView, NodeViewConstructor } from 'prosemirror-view';
 import * as React from 'react';
 import { BsMarkdownFill } from 'react-icons/bs';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, removeStyleSheet, returnFalse, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, removeStyleSheet, returnFalse, returnTrue, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils';
 import { DateField } from '../../../../fields/DateField';
 import { CreateLinkToActiveAudio, Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc';
 import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols';
@@ -1088,6 +1088,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         return anchorDoc ?? this.Document;
     }
 
+    showBorderRounding = returnTrue;
     getView = (doc: Doc, options: FocusViewOptions) => {
         if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
             return SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, () => this.toggleSidebar(false), doc, options);
diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
index ff757af88..d0ae6194f 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
@@ -24,6 +24,7 @@ import { ScrapbookItemConfig } from './ScrapbookPreset';
 import { createPreset, getPresetNames } from './ScrapbookPresetRegistry';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { DocUtils } from '../../../documents/DocUtils';
+import { returnTrue } from '../../../../ClientUtils';
 
 function createPlaceholder(cfg: ScrapbookItemConfig, doc: Doc) {
     const placeholder = new Doc();
@@ -175,7 +176,8 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         );
     }
 
-    isOutpaintable = () => true;
+    isOutpaintable = returnTrue;
+    showBorderRounding = returnTrue;
 
     @action
     generateAiImage = (prompt: string) => {
-- 
cgit v1.2.3-70-g09d2


From 35bd9e51f7cef551382025a5459d68eddd8f028b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 23 Jun 2025 13:26:59 -0400
Subject: fixed invalidations to not trigger creating new refs when ref= was
 assigned to an anonymous function.  fixed scribble erase to not delete
 everything it overlaps, just things it intersects with or contains.  fixed
 ink to have a Math mode and fixed math recognition myscript calls.

---
 src/Utils.ts                                       |   3 +
 src/client/util/CurrentUserUtils.ts                |   4 +
 src/client/views/EditableView.tsx                  |   5 +-
 src/client/views/GestureOverlay.tsx                |   4 +-
 src/client/views/InkTranscription.tsx              | 182 +++++++++++----------
 src/client/views/InkingStroke.tsx                  |   2 +-
 src/client/views/LightboxView.tsx                  |   3 +-
 src/client/views/MainView.tsx                      |  36 ++--
 src/client/views/UndoStack.tsx                     |   3 +-
 src/client/views/animationtimeline/Timeline.tsx    |   3 +-
 .../views/collections/CollectionNoteTakingView.tsx |   3 +-
 .../views/collections/CollectionStackingView.tsx   |  10 +-
 .../CollectionStackingViewFieldColumn.tsx          |  12 +-
 .../views/collections/CollectionTreeView.tsx       |   3 +-
 src/client/views/collections/TreeView.tsx          |   3 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  53 +++---
 .../collectionFreeForm/FaceCollectionBox.tsx       |   3 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |  39 ++---
 .../collectionLinear/CollectionLinearView.tsx      |   5 +-
 .../collectionSchema/CollectionSchemaView.tsx      |  24 ++-
 .../collectionSchema/SchemaCellField.tsx           |   4 +-
 .../collectionSchema/SchemaColumnHeader.tsx        |   9 +-
 .../collectionSchema/SchemaTableCell.tsx           |   3 +-
 src/client/views/global/globalScripts.ts           |   2 +-
 src/client/views/nodes/DataVizBox/DataVizBox.tsx   |   7 +-
 .../DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx   |   3 +-
 .../nodes/DataVizBox/components/Histogram.tsx      |  11 +-
 .../nodes/DataVizBox/components/LineChart.tsx      |  11 +-
 .../views/nodes/DataVizBox/components/PieChart.tsx |  15 +-
 src/client/views/nodes/DiagramBox.tsx              |   6 +-
 src/client/views/nodes/DocumentContentsView.tsx    |   4 +-
 src/client/views/nodes/DocumentView.tsx            |  22 +--
 src/client/views/nodes/EquationBox.tsx             |   3 +-
 src/client/views/nodes/FunctionPlotBox.tsx         |   3 +-
 src/client/views/nodes/LabelBox.tsx                |  38 +++--
 src/client/views/nodes/LinkBox.tsx                 |  23 +--
 src/client/views/nodes/LinkDocPreview.tsx          |   9 +-
 src/client/views/nodes/ScreenshotBox.tsx           |  18 +-
 src/client/views/nodes/calendarBox/CalendarBox.tsx |  35 ++--
 .../nodes/chatbot/chatboxcomponents/ChatBox.tsx    |  14 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   8 +-
 src/client/views/nodes/trails/PresBox.tsx          |  51 +++---
 src/client/views/pdf/GPTPopup/GPTPopup.tsx         |   3 +-
 src/fields/InkField.ts                             |   1 +
 44 files changed, 358 insertions(+), 345 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index 724725c23..e96b8e231 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -202,6 +202,9 @@ export function aggregateBounds(boundsList: { x: number; y: number; width?: numb
 export function intersectRect(r1: { left: number; top: number; width: number; height: number }, r2: { left: number; top: number; width: number; height: number }) {
     return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top);
 }
+export function rectContains(r1: { left: number; top: number; width: number; height: number }, r2: { left: number; top: number; width: number; height: number }) {
+    if (r1.left < r2.left && r1.left + r1.width > r2.left + r2.width && r1.top < r2.top && r1.top + r1.height > r2.top + r2.height) return true;
+}
 
 export function stringHash(s?: string) {
     return !s ? undefined : Math.abs(s.split('').reduce((a, b) => (n => n & n)((a << 5) - a + b.charCodeAt(0)), 0));
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 45feb0b2f..9fbc82bef 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -796,6 +796,7 @@ pie title Minerals in my tap water
                     { title: "Pen",      toolTip: "Pen (Ctrl+P)",           btnType: ButtonType.ToggleButton, icon: "pen-nib",     toolType: InkInkTool.Pen,         ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }},
                     { title: "Highlight",toolTip: "Highlight (Ctrl+H)",     btnType: ButtonType.ToggleButton, icon: "highlighter", toolType: InkInkTool.Highlight,   ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }},
                     { title: "Write",    toolTip: "Write (Ctrl+Shift+P)",   btnType: ButtonType.ToggleButton, icon: "pen",         toolType: InkInkTool.Write,       ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' },  funcs: {hidden:"IsNoviceMode()" }},      
+                    { title: "Math",     toolTip: "Math",                   btnType: ButtonType.ToggleButton, icon: "calculator",  toolType: InkInkTool.Math,        ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' },  funcs: {hidden:"IsNoviceMode()" }},      
                 ]},
             { title: "Width",      toolTip: "Stroke width",                 btnType: ButtonType.NumberSliderButton,                toolType: InkProperty.StrokeWidth,ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"!activeInkTool()"}, numBtnMin: 1, linearView_btnWidth:40},
             { title: "Color",      toolTip: "Stroke color",                 btnType: ButtonType.ColorButton,  icon: "pen",         toolType: InkProperty.StrokeColor,ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"!activeInkTool()"}},
@@ -1032,6 +1033,9 @@ pie title Minerals in my tap water
         doc[`active${InkInkTool.Write}Color`]      ?? (doc[`active${InkInkTool.Write}Color`] = "rgb(255, 0, 0)");
         doc[`active${InkInkTool.Write}Width`]      ?? (doc[`active${InkInkTool.Write}Width`] = 1);
         doc[`active${InkInkTool.Write}Bezier`]     ?? (doc[`active${InkInkTool.Write}Bezier`] = "0");
+        doc[`active${InkInkTool.Math}Color`]       ?? (doc[`active${InkInkTool.Math}Color`] = "rgb(0, 0, 255)");
+        doc[`active${InkInkTool.Math}Width`]       ?? (doc[`active${InkInkTool.Math}Width`] = 1);
+        doc[`active${InkInkTool.Math}Bezier`]      ?? (doc[`active${InkInkTool.Math}Bezier`] = "0");
         doc[`active${InkInkTool.Highlight}Color`]  ?? (doc[`active${InkInkTool.Highlight}Color`] = 'transparent');
         doc[`active${InkInkTool.Highlight}Width`]  ?? (doc[`active${InkInkTool.Highlight}Width`] = 20);
         doc[`active${InkInkTool.Highlight}Bezier`] ?? (doc[`active${InkInkTool.Highlight}Bezier`] = "0");
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index d9447b7ec..deeabaa28 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -236,6 +236,7 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
         return this._editing;
     };
 
+    setInputRef = (r: HTMLInputElement | HTMLTextAreaElement | null) => (this._inputref = r);
     renderEditor() {
         return this._props.autosuggestProps ? (
             <Autosuggest
@@ -255,7 +256,7 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
         ) : this._props.oneLine !== false && this._props.GetValue()?.toString().indexOf('\n') === -1 ? (
             <input
                 className="editableView-input"
-                ref={r => { this._inputref = r; }} // prettier-ignore
+                ref={this.setInputRef}
                 style={{ display: this._props.display, overflow: 'auto', fontSize: this._props.fontSize, minWidth: 20, background: this._props.background }}
                 placeholder={this._props.placeholder}
                 onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)}
@@ -270,7 +271,7 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
         ) : (
             <textarea
                 className="editableView-input"
-                ref={r => { this._inputref = r; }} // prettier-ignore
+                ref={this.setInputRef}
                 style={{ display: this._props.display, overflow: 'auto', fontSize: this._props.fontSize, minHeight: `min(100%, ${(this._props.GetValue()?.split('\n').length || 1) * 15})`, minWidth: 20, background: this._props.background }}
                 placeholder={this._props.placeholder}
                 onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)}
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 777a34ebc..8488c5293 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -3,7 +3,7 @@ import { action, computed, makeObservable, observable, runInAction } from 'mobx'
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { setupMoveUpEvents } from '../../ClientUtils';
-import { emptyFunction, intersectRect } from '../../Utils';
+import { emptyFunction, intersectRect, rectContains } from '../../Utils';
 import { Doc } from '../../fields/Doc';
 import { InkData, InkField, InkTool } from '../../fields/InkField';
 import { NumCast } from '../../fields/Types';
@@ -166,7 +166,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
      */
     docsInBoundingBox = (boundingBox: { topLeft: number[]; bottomRight: number[] }, childDocs: Doc[]): Doc[] => {
         const rect = { left: boundingBox.topLeft[0], top: boundingBox.topLeft[1], width: boundingBox.bottomRight[0] - boundingBox.topLeft[0], height: boundingBox.bottomRight[1] - boundingBox.topLeft[1] };
-        return childDocs.filter(doc => intersectRect(rect, { left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }));
+        return childDocs.filter(doc => rectContains(rect, { left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }));
     };
     /**
      * Determines if what the array of cusp/intersection data corresponds to a scribble.
diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx
index 6220033d4..32ca5f56b 100644
--- a/src/client/views/InkTranscription.tsx
+++ b/src/client/views/InkTranscription.tsx
@@ -32,7 +32,7 @@ export class InkTranscription extends React.Component {
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     @observable _iinkEditor: any = undefined;
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    private lastJiix: any;
+    @observable _iinkMathEditor: any = undefined;
     private currGroup?: Doc;
     private collectionFreeForm?: CollectionFreeFormView;
 
@@ -44,7 +44,7 @@ export class InkTranscription extends React.Component {
     @action
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     setMathRef = async (r: any) => {
-        if (!this._textRegister && r) {
+        if (!this._mathRegister && r) {
             const options = {
                 configuration: {
                     server: {
@@ -55,19 +55,17 @@ export class InkTranscription extends React.Component {
                         protocol: 'WEBSOCKET',
                     },
                     recognition: {
-                        type: 'TEXT',
-                        lang: 'en_US',
-                        text: {
-                            mimeTypes: ['application/vnd.myscript.jiix'] as 'application/vnd.myscript.jiix'[],
+                        type: 'MATH',
+                        math: {
+                            mimeTypes: ['application/x-latex'] as unknown as 'application/vnd.myscript.jiix'[],
                         },
                     },
                 },
             };
-            await iink.Editor.load(r, 'INKV2', options);
-
-            this._textRegister = r;
+            this._iinkMathEditor = await iink.Editor.load(r, 'INKV2', options);
+            this._mathRegister = r;
             // eslint-disable-next-line @typescript-eslint/no-explicit-any
-            r?.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef));
+            r?.addEventListener('exported', (e: any) => this.exportInk(e));
 
             return (this._textRef = r);
         }
@@ -97,12 +95,14 @@ export class InkTranscription extends React.Component {
             this._iinkEditor = await iink.Editor.load(r, 'INKV2', options);
             this._textRegister = r;
             // eslint-disable-next-line @typescript-eslint/no-explicit-any
-            r?.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef));
+            r?.addEventListener('exported', (e: any) => this.exportInk(e));
 
             return (this._textRef = r);
         }
     };
 
+    _ffview: CollectionFreeFormView | undefined;
+
     /**
      * Handles processing Dash Doc data for ink transcription.
      *
@@ -110,7 +110,7 @@ export class InkTranscription extends React.Component {
      * @param inkDocs the ink docs contained within the selected group
      * @param math boolean whether to do math transcription or not
      */
-    transcribeInk = (groupDoc: Doc | undefined, inkDocs: Doc[], math: boolean) => {
+    transcribeInk = (ffview: CollectionFreeFormView, groupDoc: Doc | undefined, inkDocs: Doc[], math: boolean) => {
         if (!groupDoc) return;
         const validInks = inkDocs.filter(s => s.type === DocumentType.INK);
 
@@ -126,13 +126,10 @@ export class InkTranscription extends React.Component {
                 strokes.push(d.inkData.map(pd => inkStroke.ptToScreen({ X: pd.X, Y: pd.Y })));
                 times.push(authorTime);
             });
+        this._ffview = ffview;
         this.currGroup = groupDoc;
         const pointerData = strokes.map((stroke, i) => this.inkJSON(stroke, times[i]));
-        if (math) {
-            this._iinkEditor.importPointEvents(pointerData);
-        } else {
-            this._iinkEditor.importPointEvents(pointerData);
-        }
+        (math ? this._iinkMathEditor : this._iinkEditor).importPointEvents(pointerData);
     };
     convertPointsToString(points: InkData[]): string {
         return points[0].map(point => `new Point(${point.X}, ${point.Y})`).join(',');
@@ -204,83 +201,92 @@ export class InkTranscription extends React.Component {
      * @param ref the ref to the editor
      */
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    exportInk = async (e: any, ref: any) => {
-        const exports = e.detail['application/vnd.myscript.jiix'];
-        if (exports) {
-            if (exports['type'] == 'Math') {
-                const latex = exports['application/x-latex'];
-                if (this.currGroup) {
-                    this.currGroup.text = latex;
-                    this.currGroup.title = latex;
-                }
+    exportInk = (e: { detail: { [key: string]: string } }) => {
+        const exports = e.detail;
+        if (exports['application/x-latex']) {
+            const latex = exports['application/x-latex'];
+            if (this.currGroup) {
+                this.currGroup.text = latex;
+                this.currGroup.title = latex;
+            }
 
-                ref.editor.clear();
-            } else if (exports['type'] == 'Text') {
-                if (exports['application/vnd.myscript.jiix']) {
-                    this.lastJiix = JSON.parse(exports['application/vnd.myscript.jiix']);
-                    // map timestamp to strokes
-                    const timestampWord = new Map<number, string>();
-                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-                    this.lastJiix.words.map((word: any) => {
-                        if (word.items) {
-                            word.items.forEach((i: { id: string; timestamp: string; X: Array<number>; Y: Array<number>; F: Array<number> }) => {
-                                const ms = Date.parse(i.timestamp);
-                                timestampWord.set(ms, word.label);
-                            });
-                        }
+            this._ffview?.addDocument(
+                Docs.Create.EquationDocument(latex, {
+                    title: '',
+                    x: this.currGroup?.x as number,
+                    y: (this.currGroup?.y as number) + (this.currGroup?.height as number),
+                    nativeHeight: 40,
+                    _height: 50,
+                    nativeWidth: 40,
+                    _width: 50,
+                })
+            );
+            // this.showRecogBox(latex as string);
+            this._iinkMathEditor.clear();
+        } else if (exports['application/vnd.myscript.jiix']) {
+            const lastJiix = exports['application/vnd.myscript.jiix'] as unknown as { words: string[]; label: string };
+            // map timestamp to strokes
+            const timestampWord = new Map<number, string>();
+            // eslint-disable-next-line @typescript-eslint/no-explicit-any
+            lastJiix.words.map((word: any) => {
+                if (word.items) {
+                    word.items.forEach((i: { id: string; timestamp: string; X: Array<number>; Y: Array<number>; F: Array<number> }) => {
+                        const ms = Date.parse(i.timestamp);
+                        timestampWord.set(ms, word.label);
                     });
-
-                    const wordInkDocMap = new Map<string, Doc[]>();
-                    if (this.currGroup) {
-                        const docList = DocListCast(this.currGroup.data);
-                        docList.forEach((inkDoc: Doc) => {
-                            // just having the times match up and be a unique value (actual timestamp doesn't matter)
-                            const ms = (DateCast(inkDoc.author_date)?.getDate().getTime() ?? 0) + 14400000;
-                            const word = timestampWord.get(ms);
-                            if (!word) {
-                                return;
-                            }
-                            const entry = wordInkDocMap.get(word);
-                            if (entry) {
-                                entry.push(inkDoc);
-                                wordInkDocMap.set(word, entry);
-                            } else {
-                                const newEntry = [inkDoc];
-                                wordInkDocMap.set(word, newEntry);
-                            }
-                        });
-                        if (this.lastJiix.words.length > 1) this.subgroupsTranscriptions(wordInkDocMap);
-                    }
                 }
-                const text = exports['label'];
+            });
 
-                if (this.currGroup && text) {
-                    DocumentView.getDocumentView(this.currGroup)?.ComponentView?.updateIcon?.();
-                    const image = await this.getIcon();
-                    const { href } = (image as URLField).url;
-                    const hrefParts = href.split('.');
-                    const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
-                    let response;
-                    try {
-                        const hrefBase64 = await imageUrlToBase64(hrefComplete);
-                        response = await gptHandwriting(hrefBase64);
-                    } catch {
-                        console.error('Error getting image');
-                    }
-                    const textBoxText = 'iink: ' + text + '\n' + '\n' + 'ChatGPT: ' + response;
-                    this.currGroup.transcription = response;
-                    this.currGroup.title = response;
-                    if (!this.currGroup.hasTextBox) {
-                        const newDoc = Docs.Create.TextDocument(textBoxText, { title: '', x: this.currGroup.x as number, y: (this.currGroup.y as number) + (this.currGroup.height as number) });
-                        newDoc.height = 200;
-                        this.collectionFreeForm?.addDocument(newDoc);
-                        this.currGroup.hasTextBox = true;
+            const wordInkDocMap = new Map<string, Doc[]>();
+            if (this.currGroup) {
+                DocListCast(this.currGroup.data).forEach((inkDoc: Doc) => {
+                    // just having the times match up and be a unique value (actual timestamp doesn't matter)
+                    const ms = (DateCast(inkDoc.author_date)?.getDate().getTime() ?? 0) + 14400000;
+                    const word = timestampWord.get(ms);
+                    if (word) {
+                        const entry = wordInkDocMap.get(word);
+                        if (entry) {
+                            entry.push(inkDoc);
+                            wordInkDocMap.set(word, entry);
+                        } else {
+                            const newEntry = [inkDoc];
+                            wordInkDocMap.set(word, newEntry);
+                        }
                     }
-                    ref.editor.clear();
-                }
+                });
+                if (lastJiix.words.length > 1) this.subgroupsTranscriptions(wordInkDocMap);
             }
+            this.showRecogBox(lastJiix.label);
+            this._iinkEditor.clear();
         }
     };
+    private showRecogBox(text: string) {
+        if (this.currGroup) {
+            let response;
+            // DocumentView.getDocumentView(this.currGroup)?.ComponentView?.updateIcon?.();
+            // const image = await this.getIcon();
+            // const { href } = (image as URLField).url;
+            // const hrefParts = href.split('.');
+            // const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
+            // try {
+            //     const hrefBase64 = await imageUrlToBase64(hrefComplete);
+            //     response = await gptHandwriting(hrefBase64);
+            // } catch {
+            //     console.error('Error getting image');
+            // }
+            const textBoxText = 'iink: ' + text + '\n' + '\n' + 'ChatGPT: ' + response;
+
+            this.currGroup.transcription = response;
+            this.currGroup.title = response;
+            if (!this.currGroup.hasTextBox) {
+                const newDoc = Docs.Create.TextDocument(textBoxText, { title: '', x: this.currGroup.x as number, y: (this.currGroup.y as number) + (this.currGroup.height as number) });
+                newDoc.height = 200;
+                this.collectionFreeForm?.addDocument(newDoc);
+                this.currGroup.hasTextBox = true;
+            }
+        }
+    }
+
     /**
      * gets the icon of the collection that was just made
      * @returns the image of the collection
@@ -299,7 +305,7 @@ export class InkTranscription extends React.Component {
      */
     createInkGroup() {
         // TODO nda - if document being added to is a inkGrouping then we can just add to that group
-        if (Doc.ActiveTool === InkTool.Ink && Doc.ActiveInk === InkInkTool.Write) {
+        if (Doc.ActiveTool === InkTool.Ink && [InkInkTool.Write, InkInkTool.Math].includes(Doc.ActiveInk)) {
             CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => {
                 // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those
                 const selected = ffView.unprocessedDocs;
@@ -309,7 +315,7 @@ export class InkTranscription extends React.Component {
                 );
                 ffView.unprocessedDocs = [];
 
-                InkTranscription.Instance.transcribeInk(newCollection, selected, false);
+                InkTranscription.Instance.transcribeInk(ffView, newCollection, selected, Doc.ActiveInk === InkInkTool.Math);
             });
         }
         CollectionFreeFormView.collectionsWithUnprocessedInk.clear();
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 253db08de..4b651af7d 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -124,7 +124,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
             );
             ffView.unprocessedDocs = [];
 
-            InkTranscription.Instance.transcribeInk(newCollection, selected, false);
+            InkTranscription.Instance.transcribeInk(ffView, newCollection, selected, false);
         }
     };
 
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 0eb21b943..62b1180ec 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -244,6 +244,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
             </div>
         </div>
     );
+    setStickerRef = (r: StickerPalette | null) => (this._annoPaletteView = r);
     render() {
         let downx = 0;
         let downy = 0;
@@ -317,7 +318,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
                             </GestureOverlay>
                         </div>
 
-                        {this._showPalette && <StickerPalette ref={r => (this._annoPaletteView = r)} Doc={DocCast(Doc.UserDoc().myLightboxDrawings)} />}
+                        {this._showPalette && DocCast(Doc.UserDoc().myLightboxDrawings) && <StickerPalette ref={this.setStickerRef} Doc={DocCast(Doc.UserDoc().myLightboxDrawings)!} />}
                         {this.renderNavBtn(0, undefined, this._props.PanelHeight / 2 - 12.5, 'chevron-left', this._doc && this._history.length ? true : false, this.previous)}
                         {this.renderNavBtn(
                             this._props.PanelWidth - Math.min(this._props.PanelWidth / 4, this._props.maxBorder[0]),
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 13b14617c..867a5a304 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -921,19 +921,19 @@ export class MainView extends ObservableReactComponent<object> {
         );
     }
 
+    setMainDashRef = (r: HTMLDivElement | null) =>
+        r &&
+        new ResizeObserver(
+            action(() => {
+                this._dashUIWidth = r.getBoundingClientRect().width;
+                this._dashUIHeight = r.getBoundingClientRect().height;
+            })
+        ).observe(r);
     @computed get mainDashboardArea() {
         return !this.userDoc ? null : (
             <div
                 className="mainView-dashboardArea"
-                ref={r => {
-                    r &&
-                        new ResizeObserver(
-                            action(() => {
-                                this._dashUIWidth = r.getBoundingClientRect().width;
-                                this._dashUIHeight = r.getBoundingClientRect().height;
-                            })
-                        ).observe(r);
-                }}
+                ref={this.setMainDashRef}
                 style={{
                     color: 'black',
                     height: `calc(100% - ${this.topOfDashUI + this.topMenuHeight()}px)`,
@@ -1072,6 +1072,14 @@ export class MainView extends ObservableReactComponent<object> {
     };
 
     lightboxMaxBorder = [200, 50];
+    setMainViewRef = (r: HTMLDivElement | null) =>
+        r &&
+        new ResizeObserver(
+            action(() => {
+                this._windowWidth = r.getBoundingClientRect().width;
+                this._windowHeight = r.getBoundingClientRect().height;
+            })
+        ).observe(r);
     render() {
         return (
             <div
@@ -1085,15 +1093,7 @@ export class MainView extends ObservableReactComponent<object> {
                         ele.scrollTop = ele.scrollLeft = 0;
                     })(document.getElementById('root')!)
                 }
-                ref={r => {
-                    r &&
-                        new ResizeObserver(
-                            action(() => {
-                                this._windowWidth = r.getBoundingClientRect().width;
-                                this._windowHeight = r.getBoundingClientRect().height;
-                            })
-                        ).observe(r);
-                }}>
+                ref={this.setMainViewRef}>
                 {this.inkResources}
                 <DictationOverlay />
                 <SharingManager />
diff --git a/src/client/views/UndoStack.tsx b/src/client/views/UndoStack.tsx
index 067020a62..3755b1ec1 100644
--- a/src/client/views/UndoStack.tsx
+++ b/src/client/views/UndoStack.tsx
@@ -9,6 +9,7 @@ import './UndoStack.scss';
 
 @observer
 export class UndoStack extends React.Component<object> {
+    setRef = (r: HTMLDivElement | null) => r?.scroll({ behavior: 'auto', top: (r?.scrollHeight ?? 0) + 20 });
     render() {
         const background = UndoManager.batchCounter.get() ? 'yellow' : SettingsManager.userVariantColor;
         const color = UndoManager.batchCounter.get() ? 'black' : SettingsManager.userColor;
@@ -25,7 +26,7 @@ export class UndoStack extends React.Component<object> {
                             popup={
                                 <div
                                     className="undoStack-commandsContainer"
-                                    ref={r => r?.scroll({ behavior: 'auto', top: (r?.scrollHeight ?? 0) + 20 })}
+                                    ref={this.setRef}
                                     style={{
                                         background,
                                         color,
diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx
index 814e9a7a0..c2bd01334 100644
--- a/src/client/views/animationtimeline/Timeline.tsx
+++ b/src/client/views/animationtimeline/Timeline.tsx
@@ -533,6 +533,7 @@ export class Timeline extends ObservableReactComponent<FieldViewProps & { Doc: D
         this._totalLength = RegionHelpers.convertPixelTime(this._time, 'mili', 'pixel', this._tickSpacing, this._tickIncrement);
     };
 
+    setTrackRef = (r: Track) => this.mapOfTracks.push(r);
     /**
      * if you have any question here, just shoot me an email or text.
      * basically the only thing you need to edit besides render methods in track (individual track lines) and keyframe (green region)
@@ -554,7 +555,7 @@ export class Timeline extends ObservableReactComponent<FieldViewProps & { Doc: D
                                 {[...this.children, this._props.Doc].map(doc => (
                                     <Track
                                         key={doc[Id]}
-                                        ref={ref => this.mapOfTracks.push(ref)}
+                                        ref={this.setTrackRef}
                                         timeline={this}
                                         animatedDoc={doc}
                                         currentBarX={this._currentBarX}
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index 7f639a11e..7a4408931 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -256,9 +256,10 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         const height = () => this.getDocHeight(doc);
         let dref: Opt<DocumentView>;
         const noteTakingDocTransform = () => this.getDocTransform(doc, dref);
+        const setRef = (r: DocumentView | null) => (dref = r || undefined);
         return (
             <DocumentView
-                ref={r => (dref = r || undefined)}
+                ref={setRef}
                 Document={doc}
                 TemplateDataDocument={doc.isTemplateDoc || doc.isTemplateForField ? this._props.TemplateDataDocument : undefined}
                 pointerEvents={this.blockPointerEventsWhenDragging}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index bdeb7d944..fbdd23315 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -1,6 +1,6 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import * as CSS from 'csstype';
-import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { ClientUtils, DivHeight, returnNone, returnZero, setupMoveUpEvents, smoothScroll } from '../../../ClientUtils';
@@ -391,10 +391,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     });
 
     // This following three functions must be from the view Mehek showed
-    columnDividerDown = (e: React.PointerEvent) => {
-        runInAction(() => {
-            this._cursor = 'grabbing';
-        });
+    columnDividerDown = action((e: React.PointerEvent) => {
+        this._cursor = 'grabbing';
         const batch = UndoManager.StartBatch('stacking width');
         setupMoveUpEvents(
             this,
@@ -406,7 +404,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
             }),
             emptyFunction
         );
-    };
+    });
     @action
     onDividerMove = (e: PointerEvent) => {
         this.Document._layout_columnWidth = Math.max(10, (this._props.DocumentView?.().screenToViewTransform().transformPoint(e.clientX, 0)[0] ?? 0) - this.xMargin);
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 8c535534a..b5efa7a72 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -304,6 +304,12 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
         ContextMenu.Instance.displayMenu(pt[0], pt[1], undefined, true);
     };
 
+    setRef = (r: HTMLDivElement | null) => {
+        if (this._headerRef && this._props.colHeaderRefs.includes(this._headerRef)) this._props.colHeaderRefs.splice(this._props.colStackRefs.indexOf(this._headerRef), 1);
+        r && this._props.colHeaderRefs.push(r);
+        this._headerRef = r;
+    };
+
     @computed get innards() {
         TraceMobx();
         const key = this._props.pivotField;
@@ -315,11 +321,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
             <div
                 key={heading}
                 className="collectionStackingView-sectionHeader"
-                ref={r => {
-                    if (this._headerRef && this._props.colHeaderRefs.includes(this._headerRef)) this._props.colHeaderRefs.splice(this._props.colStackRefs.indexOf(this._headerRef), 1);
-                    r && this._props.colHeaderRefs.push(r);
-                    this._headerRef = r;
-                }}
+                ref={this.setRef}
                 style={{
                     marginTop: 0,
                     marginBottom: this._props.gridGap,
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index bee5d016d..4625965b4 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -407,6 +407,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
     remAnnotationDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, `${this._props.fieldKey}_annotations`) || false;
     moveAnnotationDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[], annotationKey?: string) => boolean) => this.moveDocument(doc, targetCollection, addDocument) || false;
 
+    setRef = (r: HTMLDivElement | null) => !this.Document.treeView_HasOverlay && r && this.createTreeDropTarget(r);
     @observable _headerHeight = 0;
     @computed get content() {
         const background = () => this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string;
@@ -428,7 +429,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
                 <div
                     className="collectionTreeView-contents"
                     key="tree"
-                    ref={r => !this.Document.treeView_HasOverlay && r && this.createTreeDropTarget(r)}
+                    ref={this.setRef}
                     style={{
                         ...(!titleBar ? { marginLeft: this.marginX(), paddingTop: this.marginTop() } : {}),
                         color: color(),
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 5b2f1ff81..f84c7d3c0 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -1294,6 +1294,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                     return null;
                 }
 
+                const setRef = (r: TreeView | null) => treeViewRefs.set(child, r || undefined);
                 const dentDoc = (editTitle: boolean, newParent: Doc, addAfter: Doc | undefined, parent: TreeView | CollectionTreeView | undefined) => {
                     if (parent instanceof TreeView && parent._props.treeView.fileSysMode && !newParent.isFolder) return;
                     const fieldKey = Doc.LayoutDataKey(newParent);
@@ -1317,7 +1318,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                 return (
                     <TreeView
                         key={child[Id]}
-                        ref={r => treeViewRefs.set(child, r || undefined)}
+                        ref={setRef}
                         Document={pair.layout}
                         dataDoc={pair.data}
                         treeViewParent={treeViewParent}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 32ace463d..6e9e503f4 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -562,7 +562,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 const B = this.screenToFreeformContentsXf.transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
                 const inkDoc = this.createInkDoc(points, B);
                 if (Doc.ActiveInk === InkInkTool.Highlight) inkDoc.$backgroundColor = 'transparent';
-                if (Doc.ActiveInk === InkInkTool.Write) {
+                if ([InkInkTool.Write, InkInkTool.Math].includes(Doc.ActiveInk)) {
                     this.unprocessedDocs.push(inkDoc);
                     CollectionFreeFormView.collectionsWithUnprocessedInk.add(this);
                 }
@@ -2220,18 +2220,41 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         );
     };
 
+    @computed get inkEraser() {
+        return (
+            Doc.ActiveTool === InkTool.Eraser &&
+            Doc.ActiveEraser === InkEraserTool.Radius &&
+            this._showEraserCircle && (
+                <div
+                    onPointerMove={this.onCursorMove}
+                    style={{
+                        position: 'fixed',
+                        left: this._eraserX,
+                        top: this._eraserY,
+                        width: (ActiveEraserWidth() + 5) * 2,
+                        height: (ActiveEraserWidth() + 5) * 2,
+                        borderRadius: '50%',
+                        border: '1px solid gray',
+                        transform: 'translate(-50%, -50%)',
+                    }}
+                />
+            )
+        );
+    }
+
+    setRef = (r: HTMLDivElement | null) => {
+        this.createDashEventsTarget(r);
+        this.fixWheelEvents(r, this._props.isContentActive, this.onPassiveWheel);
+        r?.addEventListener('mouseleave', this.onMouseLeave);
+        r?.addEventListener('mouseenter', this.onMouseEnter);
+    };
     render() {
         TraceMobx();
         return (
             <div
                 className="collectionfreeformview-container"
                 id={this._paintedId}
-                ref={r => {
-                    this.createDashEventsTarget(r);
-                    this.fixWheelEvents(r, this._props.isContentActive, this.onPassiveWheel);
-                    r?.addEventListener('mouseleave', this.onMouseLeave);
-                    r?.addEventListener('mouseenter', this.onMouseEnter);
-                }}
+                ref={this.setRef}
                 onWheel={this.onPointerWheel}
                 onClick={this.onClick}
                 onPointerDown={this.onPointerDown}
@@ -2246,21 +2269,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     width: `${100 / this.nativeDimScaling}%`,
                     height: this._props.getScrollHeight?.() ?? `${100 / this.nativeDimScaling}%`,
                 }}>
-                {Doc.ActiveTool === InkTool.Eraser && Doc.ActiveEraser === InkEraserTool.Radius && this._showEraserCircle && (
-                    <div
-                        onPointerMove={this.onCursorMove}
-                        style={{
-                            position: 'fixed',
-                            left: this._eraserX,
-                            top: this._eraserY,
-                            width: (ActiveEraserWidth() + 5) * 2,
-                            height: (ActiveEraserWidth() + 5) * 2,
-                            borderRadius: '50%',
-                            border: '1px solid gray',
-                            transform: 'translate(-50%, -50%)',
-                        }}
-                    />
-                )}
+                {this.inkEraser}
                 {this.paintFunc ? (
                     <FormattedTextBox {...this.props} /> // need this so that any live dashfieldviews will update the underlying text that the code eval reads
                 ) : this._lightboxDoc ? (
diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
index 142085e14..c31558dff 100644
--- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
@@ -150,6 +150,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() {
         FaceRecognitionHandler.UniqueFaceRemoveFaceImage(imgDoc, this.Document);
     }, 'remove doc from face');
 
+    setRef = (r: HTMLDivElement | null) => this.fixWheelEvents(r, this._props.isContentActive);
     render() {
         return (
             <div className="face-document-item" ref={ele => this.createDropTarget(ele!)}>
@@ -176,7 +177,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         style={{
                             pointerEvents: this._props.isContentActive() ? undefined : 'none',
                         }}
-                        ref={r => this.fixWheelEvents(r, this._props.isContentActive)}>
+                        ref={this.setRef}>
                         {FaceRecognitionHandler.UniqueFaceImages(this.Document).map((doc, i) => {
                             const [name, type] = ImageCastToNameType(doc?.[Doc.LayoutDataKey(doc)]) ?? ['-missing-', '.png'];
                             return (
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index ff78b332a..4191aaca8 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -762,29 +762,26 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         }
         e.stopPropagation();
     };
+    setRef = (r: HTMLDivElement | null) => {
+        r?.addEventListener('dashDragMovePause', this.onDragMovePause as EventListenerOrEventListenerObject);
+        this.MarqueeRef = r;
+    };
     render() {
         return (
-            <>
-                <div
-                    className="marqueeView"
-                    ref={r => {
-                        r?.addEventListener('dashDragMovePause', this.onDragMovePause as EventListenerOrEventListenerObject);
-                        this.MarqueeRef = r;
-                    }}
-                    style={{
-                        overflow: StrCast(this._props.Document._overflow),
-                        cursor: Doc.ActiveTool === InkTool.Ink || this._visible ? 'crosshair' : 'pointer',
-                    }}
-                    onDragOver={e => e.preventDefault()}
-                    onScroll={e => {
-                        e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0;
-                    }}
-                    onClick={this.onClick}
-                    onPointerDown={this.onPointerDown}>
-                    {this._visible ? this.marqueeDiv : null}
-                    {this.props.children}
-                </div>
-            </>
+            <div
+                className="marqueeView"
+                ref={this.setRef}
+                style={{
+                    overflow: StrCast(this._props.Document._overflow),
+                    cursor: Doc.ActiveTool === InkTool.Ink || this._visible ? 'crosshair' : 'pointer',
+                }}
+                onDragOver={e => e.preventDefault()}
+                onScroll={e => (e.currentTarget.scrollTop = (e.currentTarget.scrollLeft = 0))} // prettier-ignore
+                onClick={this.onClick}
+                onPointerDown={this.onPointerDown}>
+                {this._visible ? this.marqueeDiv : null}
+                {this.props.children}
+            </div>
         );
     }
 }
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index d0a1e6f0d..435f618d9 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -158,14 +158,13 @@ export class CollectionLinearView extends CollectionSubView() {
 
         let dref: Opt<HTMLDivElement>;
         const docXf = () => this.getTransform(dref);
+        const setRef = (r: HTMLDivElement | null) => (dref = r || undefined);
         // const scalable = pair.layout.onClick || pair.layout.onDragStart;
         return hidden ? null : (
             <div
                 className={preview ? 'preview' : `collectionLinearView-docBtn`}
                 key={doc[Id]}
-                ref={r => {
-                    dref = r || undefined;
-                }}
+                ref={setRef}
                 style={{
                     pointerEvents: 'all',
                     width: NumCast(doc._width),
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index 6442385c0..2576bdf9b 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -1043,17 +1043,16 @@ export class CollectionSchemaView extends CollectionSubView() {
         if (!this._oldKeysWheel?.scrollTop && e.deltaY <= 0) e.preventDefault();
         e.stopPropagation();
     };
+    setRef = (r: HTMLDivElement | null) => {
+        this._oldKeysWheel?.removeEventListener('wheel', this.onKeysPassiveWheel);
+        this._oldKeysWheel = r;
+        r?.addEventListener('wheel', this.onKeysPassiveWheel, { passive: false });
+    };
     _oldKeysWheel: HTMLDivElement | null = null;
     @computed get keysDropdown() {
         return (
             <div className="schema-key-search">
-                <div
-                    className="schema-key-list"
-                    ref={r => {
-                        this._oldKeysWheel?.removeEventListener('wheel', this.onKeysPassiveWheel);
-                        this._oldKeysWheel = r;
-                        r?.addEventListener('wheel', this.onKeysPassiveWheel, { passive: false });
-                    }}>
+                <div className="schema-key-list" ref={this.setRef}>
                     {this._menuKeys.map(key => (
                         <div
                             key={key}
@@ -1294,6 +1293,9 @@ export class CollectionSchemaView extends CollectionSubView() {
     screenToLocal = () => this.ScreenToLocalBoxXf().translate(-this.tableWidth, 0);
     previewWidthFunc = () => this.previewWidth;
     displayedDocsFunc = () => this.docsWithDrag.docs;
+    setColHdrRef = (r: SchemaColumnHeader | null) => r && this._headerRefs.push(r);
+    setPreviewRef = (r: HTMLDivElement | null) => (this._previewRef = r);
+
     render() {
         return (
             <div
@@ -1331,7 +1333,7 @@ export class CollectionSchemaView extends CollectionSubView() {
                         {this.columnKeys.map((key, index) => (
                             <SchemaColumnHeader
                                 //cleanupField={this.cleanupComputedField}
-                                ref={r => r && this._headerRefs.push(r)}
+                                ref={this.setColHdrRef}
                                 keysDropdown={this.keysDropdown}
                                 schemaView={this}
                                 columnWidth={() => CollectionSchemaView._minColWidth} //TODO: update
@@ -1379,11 +1381,7 @@ export class CollectionSchemaView extends CollectionSubView() {
                 </div>
                 {this.previewWidth > 0 && <div className="schema-preview-divider" style={{ width: CollectionSchemaView._previewDividerWidth }} onPointerDown={this.onDividerDown} />}
                 {this.previewWidth > 0 && (
-                    <div
-                        style={{ width: `${this.previewWidth}px` }}
-                        ref={ref => {
-                            this._previewRef = ref;
-                        }}>
+                    <div style={{ width: `${this.previewWidth}px` }} ref={this.setPreviewRef}>
                         {Array.from(this._selectedDocs).lastElement() && (
                             <DocumentView
                                 Document={Array.from(this._selectedDocs).lastElement()}
diff --git a/src/client/views/collections/collectionSchema/SchemaCellField.tsx b/src/client/views/collections/collectionSchema/SchemaCellField.tsx
index 9ad94cb31..412daa105 100644
--- a/src/client/views/collections/collectionSchema/SchemaCellField.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaCellField.tsx
@@ -341,12 +341,14 @@ export class SchemaCellField extends ObservableReactComponent<SchemaCellFieldPro
         return <span className="editableView-static">{this._props.fieldContents ? <FieldView {...this._props.fieldContents} /> : ''}</span>;
     };
 
+    setRef = (r: HTMLDivElement | null) => (this._inputref = r);
+
     renderEditor = () => {
         return (
             <div
                 contentEditable
                 className="schemaField-editing"
-                ref={r => (this._inputref = r)}
+                ref={this.setRef}
                 style={{ minHeight: `min(100%, ${(this._props.GetValue()?.split('\n').length || 1) * 15})`, minWidth: 20 }}
                 onBlur={() => (this._props.refSelectModeInfo.enabled ? setTimeout(() => this.setIsFocused(true), 1000) : this.finalizeEdit(false, true, false))}
                 onInput={this.onChange}
diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
index 134f2ed31..64bfab856 100644
--- a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
@@ -118,6 +118,10 @@ export class SchemaColumnHeader extends ObservableReactComponent<SchemaColumnHea
         return { color, fieldProps, cursor };
     };
 
+    setRef = (r: EditableView | null) => {
+        this._inputRef = r;
+        this._props.autoFocus && r?.setIsFocused(true);
+    };
     @computed get editableView() {
         const { color, fieldProps } = this.renderProps(this._props);
 
@@ -133,10 +137,7 @@ export class SchemaColumnHeader extends ObservableReactComponent<SchemaColumnHea
                     width: '100%',
                 }}>
                 <EditableView
-                    ref={r => {
-                        this._inputRef = r;
-                        this._props.autoFocus && r?.setIsFocused(true);
-                    }}
+                    ref={this.setRef}
                     oneLine={true}
                     allowCRs={false}
                     contents={''}
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index 8b34b4139..02e0d8100 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -185,6 +185,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
         return eqSymbol + modField;
     };
 
+    setRef = (r: SchemaCellField | null) => selectedCell(this._props) && this._props.autoFocus && r?.setIsFocused(true);
     @computed get defaultCellContent() {
         const { color, textDecoration, fieldProps, pointerEvents } = SchemaTableCell.renderProps(this._props);
 
@@ -204,7 +205,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
                     Doc={this._props.Doc}
                     highlightCells={(text: string) => this._props.highlightCells(this.adjustSelfReference(text))}
                     getCells={(text: string) => this._props.eqHighlightFunc(this.adjustSelfReference(text))}
-                    ref={r => selectedCell(this._props) && this._props.autoFocus && r?.setIsFocused(true)}
+                    ref={this.setRef}
                     oneLine={this._props.oneLine}
                     contents={undefined}
                     fieldContents={fieldProps}
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index cb3adae10..e098d50d8 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -408,7 +408,7 @@ function setActiveTool(tool: InkTool | InkEraserTool | InkInkTool | Gestures, ke
     }
     runInAction(() => {
         const eraserTool = tool === InkTool.Eraser ? Doc.ActiveEraser : [InkEraserTool.Stroke, InkEraserTool.Radius, InkEraserTool.Segment].includes(tool as InkEraserTool) ? (tool as InkEraserTool) : undefined;
-        const inkTool = tool === InkTool.Ink ? Doc.ActiveInk : [InkInkTool.Pen, InkInkTool.Write, InkInkTool.Highlight].includes(tool as InkInkTool) ? (tool as InkInkTool) : undefined;
+        const inkTool = tool === InkTool.Ink ? Doc.ActiveInk : [InkInkTool.Pen, InkInkTool.Write, InkInkTool.Math, InkInkTool.Highlight].includes(tool as InkInkTool) ? (tool as InkInkTool) : undefined;
         if (GestureOverlay.Instance) {
             SnappingManager.SetKeepGestureMode(keepPrim);
         }
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 9369ff98a..0b7033e57 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -420,6 +420,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
     };
 
+    setRef = (r: LineChart | Histogram | PieChart | null) => (this._vizRenderer = r ?? undefined);
     // toggles for user to decide which chart type to view the data in
     @computed get renderVizView() {
         const scale = this._props.NativeDimScaling?.() || 1;
@@ -437,9 +438,9 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         if (!this.records.length) return 'no data/visualization';
         switch (this.dataVizView) {
             case DataVizView.TABLE:     return <TableBox  {...sharedProps} Doc={this.Document} specHighlightedRow={this._specialHighlightedRow} docView={this.DocumentView} selectAxes={this.selectAxes} selectTitleCol={this.selectTitleCol}/>;
-            case DataVizView.LINECHART: return <LineChart {...sharedProps} Doc={this.Document} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} vizBox={this} />;
-            case DataVizView.HISTOGRAM: return <Histogram {...sharedProps} Doc={this.Document} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} />;
-            case DataVizView.PIECHART:  return <PieChart  {...sharedProps} Doc={this.Document} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} 
+            case DataVizView.LINECHART: return <LineChart {...sharedProps} Doc={this.Document} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={this.setRef} vizBox={this} />;
+            case DataVizView.HISTOGRAM: return <Histogram {...sharedProps} Doc={this.Document} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={this.setRef} />;
+            case DataVizView.PIECHART:  return <PieChart  {...sharedProps} Doc={this.Document} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={this.setRef} 
             margin={{ top: 10, right: 15, bottom: 15, left: 15 }} />;
             default:
         } // prettier-ignore
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx
index fb083ea75..8f6ecab57 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx
@@ -540,6 +540,7 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
         ]; //prettier-ignore
     }
 
+    setRef = (r: HTMLDivElement) => (this._ref = r);
     render() {
         const topButton = (icon: string, opt: string, func: () => void, tag: string) => (
             <div className={`top-button-container ${tag} ${opt === this._menuContent ? 'selected' : ''}`}>
@@ -558,7 +559,7 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
                 {!this._shouldDisplay ? undefined : (
                     <div
                         className="docCreatorMenu-cont"
-                        ref={r => (this._ref = r)}
+                        ref={this.setRef}
                         style={{
                             display: '',
                             left: this._pageX,
diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
index a7c4a00b0..f51683991 100644
--- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx
+++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
@@ -413,6 +413,10 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
         });
     };
 
+    setChartRef = (r: HTMLDivElement | null) => {
+        this._histogramRef = r;
+        r && this.drawChart(this._histogramData, this.width, this.height);
+    };
     render() {
         if (!this.selectedBins) this.layoutDoc.dataViz_histogram_selectedBins = new List<string>();
 
@@ -446,12 +450,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
                             size={Size.XSMALL}
                         />
                     </div>
-                    <div
-                        ref={r => {
-                            this._histogramRef = r;
-                            r && this.drawChart(this._histogramData, this.width, this.height);
-                        }}
-                    />
+                    <div ref={this.setChartRef} />
                     {selected !== 'none' ? (
                         <div className="selected-data">
                             Selected: {selected}
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
index 80fadf178..732681e05 100644
--- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -347,6 +347,10 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
             .style('pointer-events', 'none')
             .style('transform', `translate(${xScale(d0.x) - this.width}px,${yScale(d0.y)}px)`);
     }
+    setLineRef = (r: HTMLDivElement | null) => {
+        this._lineChartRef = r;
+        this.drawChart([this._lineChartData], this.rangeVals, this.width, this.height);
+    };
 
     render() {
         const selectedPt = this._currSelected ? `{ ${this._props.axes[0]}: ${this._currSelected.x} ${this._props.axes[1]}: ${this._currSelected.y} }` : 'none';
@@ -378,12 +382,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
                             fillWidth
                         />
                     </div>
-                    <div
-                        ref={r => {
-                            this._lineChartRef = r;
-                            this.drawChart([this._lineChartData], this.rangeVals, this.width, this.height);
-                        }}
-                    />
+                    <div ref={this.setLineRef} />
                     {selectedPt !== 'none' ? (
                         <div className="selected-data">
                             {`Selected: ${selectedPt}`}
diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx
index 0ae70786f..cf476b8d0 100644
--- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx
@@ -373,10 +373,10 @@ export class PieChart extends ObservableReactComponent<PieChartProps> {
         const sliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/%/g, '').replace(/#/g, '').replace(/</g, '') : sliceTitle;
 
         const sliceColors = Cast(this._props.layoutDoc.dataViz_pie_sliceColors, listSpec('string'), null);
-        sliceColors.forEach(each => {
+        sliceColors?.forEach(each => {
             if (each.split('::')[0] === sliceName) sliceColors.splice(sliceColors.indexOf(each), 1);
         });
-        sliceColors.push(StrCast(sliceName + '::' + color));
+        sliceColors?.push(StrCast(sliceName + '::' + color));
     };
 
     @action changeHistogramCheckBox = () => {
@@ -384,6 +384,10 @@ export class PieChart extends ObservableReactComponent<PieChartProps> {
         this.drawChart(this._pieChartData, this.width, this.height);
     };
 
+    setChartRef = (r: HTMLDivElement | null) => {
+        this._piechartRef = r;
+        this.drawChart(this._pieChartData, this.width, this.height);
+    };
     render() {
         let titleAccessor = 'dataViz_pie_title';
         if (this._props.axes.length === 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1];
@@ -443,12 +447,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> {
                             Organize data as histogram
                         </div>
                     ) : null}
-                    <div
-                        ref={r => {
-                            this._piechartRef = r;
-                            this.drawChart(this._pieChartData, this.width, this.height);
-                        }}
-                    />
+                    <div ref={this.setChartRef} />
                     {selected !== 'none' ? (
                         <div className="selected-data">
                             Selected: {selected}
diff --git a/src/client/views/nodes/DiagramBox.tsx b/src/client/views/nodes/DiagramBox.tsx
index 7cfccf0dc..6a31f64ce 100644
--- a/src/client/views/nodes/DiagramBox.tsx
+++ b/src/client/views/nodes/DiagramBox.tsx
@@ -185,6 +185,8 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return '( )';
     };
 
+    setRef = (r: HTMLDivElement | null) => this.fixWheelEvents(r, this._props.isContentActive);
+    setDiagramBoxRef = (r: HTMLDivElement | null) => r && this.renderMermaidAsync.call(this, this.removeWords(this.mermaidcode), r);
     render() {
         return (
             <div
@@ -192,7 +194,7 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 style={{
                     pointerEvents: this._props.isContentActive() ? undefined : 'none',
                 }}
-                ref={r => this.fixWheelEvents(r, this._props.isContentActive)}>
+                ref={this.setRef}>
                 <div className="DIYNodeBox-searchbar">
                     <input type="text" value={this._inputValue} onKeyDown={action(e => e.key === 'Enter' && this.generateMermaidCode())} onChange={action(e => (this._inputValue = e.target.value))} />
                     <button type="button" onClick={this.generateMermaidCode}>
@@ -208,7 +210,7 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     ) : this._generating ? (
                         <div className="loading-circle" />
                     ) : (
-                        <div className="diagramBox" ref={r => r && this.renderMermaidAsync.call(this, this.removeWords(this.mermaidcode), r)}>
+                        <div className="diagramBox" ref={this.setDiagramBoxRef}>
                             {this._errorMessage || 'Type a prompt to generate a diagram'}
                         </div>
                     )}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 504c1491e..32741a0fe 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -261,9 +261,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
                 jsx={layoutFrame}
                 showWarnings
                 // eslint-disable-next-line @typescript-eslint/no-explicit-any
-                onError={(test: any) => {
-                    console.log('DocumentContentsView:' + test, bindings, layoutFrame);
-                }}
+                onError={(test: any) => console.log('DocumentContentsView:' + test, bindings, layoutFrame)}
             />
         );
     }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index fe95f15af..bd71115db 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -701,16 +701,18 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field
 
     aiContentsWidth = () => (this.aiContentsHeight() * (this._props.NativeWidth?.() || 1)) / (this._props.NativeHeight?.() || 1);
     aiContentsHeight = () => Math.max(10, this._props.PanelHeight() - (this._aiWinHeight + (this.tagsOverlayFunc() ? 22 : 0)) * this.uiBtnScaling);
+    setAiRef = action((r: HTMLDivElement | null) => this.historyRef(this._oldHistoryWheel, (this._oldHistoryWheel = r)));
+
     @computed get aiEditor() {
         return (
             <div
                 className="documentView-editorView"
+                ref={this.setAiRef}
                 style={{
                     background: SnappingManager.userVariantColor,
                     width: `${100 / this.uiBtnScaling}%`, //
                     transform: `scale(${this.uiBtnScaling})`,
-                }}
-                ref={r => this.historyRef(this._oldHistoryWheel, (this._oldHistoryWheel = r))}>
+                }}>
                 <div className="documentView-editorView-resizer" />
                 {this._componentView?.componentAIView?.() ?? null}
                 {this._props.DocumentView?.() ? <TagsView background={this.backgroundBoxColor} Views={[this._props.DocumentView?.()]} /> : null}
@@ -1507,18 +1509,16 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
             .translate(-(this._docViewInternal?.aiShift() ?? 0), 0)
             .scale((this._docViewInternal?.aiScale() ?? 1) / this.nativeScaling);
 
+    setHtmlOverlayRef = (r: HTMLDivElement | null) => {
+        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)));
+        }
+    };
     htmlOverlay = () => {
         const effect = StrCast(this._htmlOverlayEffect?.presentation_effect, StrCast(this._htmlOverlayEffect?.followLinkAnimEffect));
         return (
-            <div
-                className="documentView-htmlOverlay"
-                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)));
-                    }
-                }}
-                style={{ display: !this._htmlOverlayText ? 'none' : undefined }}>
+            <div className="documentView-htmlOverlay" ref={this.setHtmlOverlayRef} style={{ display: !this._htmlOverlayText ? 'none' : undefined }}>
                 <div className="documentView-htmlOverlayInner" style={{ transition: `all 500ms`, opacity: this._enableHtmlOverlayTransitions ? 0.9 : 0 }}>
                     {DocumentViewInternal.AnimationEffect(
                         <div className="webBox-textHighlight">
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index e0ab04692..2ce24b688 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -116,12 +116,13 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
             this.layoutDoc._nativeHeight = mathHeight;
         }
     };
+    setRef = (r: HTMLDivElement) => r && this._ref.current?.element.current && this.updateSize(this._ref.current?.element.current);
     render() {
         TraceMobx();
         const scale = this._props.NativeDimScaling?.() || 1;
         return (
             <div
-                ref={r => r && this._ref.current?.element.current && this.updateSize(this._ref.current?.element.current)}
+                ref={this.setRef}
                 className="equationBox-cont"
                 onKeyDown={e => e.stopPropagation()}
                 onPointerDown={e => !e.ctrlKey && e.stopPropagation()}
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 8e4b64851..e4d37e006 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -121,11 +121,12 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
         }
         // if (this.layout_autoHeight) this.tryUpdateScrollHeight();
     };
+    setRef = (r: HTMLDivElement | null) => r && this.createGraph(r);
     @computed get theGraph() {
         return (
             <div
                 id={`${this._plotId}`}
-                ref={r => r && this.createGraph(r)}
+                ref={this.setRef}
                 style={{ position: 'absolute', width: '100%', height: '100%' }}
                 onPointerDown={e => {
                     e.stopPropagation();
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index e1ecc2018..c5948cbbd 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -169,6 +169,25 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
     };
 
+    setRef = (r: HTMLDivElement | null) => {
+        this._divRef?.removeEventListener('beforeinput', this.beforeInput);
+        this._divRef = r;
+        if (this._divRef) {
+            this._divRef.addEventListener('beforeinput', this.beforeInput);
+
+            if (DocumentView.SelectOnLoad === this.Document) {
+                DocumentView.SetSelectOnLoad(undefined);
+                this._liveTextUndo = FormattedTextBox.LiveTextUndo;
+                FormattedTextBox.LiveTextUndo = undefined;
+                this._divRef.focus();
+            }
+            this.fitTextToBox(this._divRef);
+            if (this.Title) {
+                this.resetCursor();
+            }
+        } else this._timeout && clearTimeout(this._timeout);
+    };
+
     render() {
         TraceMobx();
         const boxParams = this.fitTextToBox(undefined); // this causes mobx to trigger re-render when data changes
@@ -228,24 +247,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             __html: `<span class="textFitted textFitAlignVert" style="display: inline-block; text-align: center; font-size: 100px; height: 0px;">${this.Title?.startsWith('#') ? '' : (this.Title ?? '')}</span>`,
                         }}
                         contentEditable={this._props.onClickScript?.() ? undefined : true}
-                        ref={r => {
-                            this._divRef?.removeEventListener('beforeinput', this.beforeInput);
-                            this._divRef = r;
-                            if (this._divRef) {
-                                this._divRef.addEventListener('beforeinput', this.beforeInput);
-
-                                if (DocumentView.SelectOnLoad === this.Document) {
-                                    DocumentView.SetSelectOnLoad(undefined);
-                                    this._liveTextUndo = FormattedTextBox.LiveTextUndo;
-                                    FormattedTextBox.LiveTextUndo = undefined;
-                                    this._divRef.focus();
-                                }
-                                this.fitTextToBox(this._divRef);
-                                if (this.Title) {
-                                    this.resetCursor();
-                                }
-                            } else this._timeout && clearTimeout(this._timeout);
-                        }}
+                        ref={this.setRef}
                     />
                 </div>
             </div>
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 78c8a686c..d31fadf77 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -99,6 +99,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
     };
 
+    setRef = (r: HTMLDivElement | null) => (this._divRef = r);
     render() {
         TraceMobx();
 
@@ -149,28 +150,18 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const aid = targetAhyperlinks?.find(alink => container?.contains(alink))?.id ?? targetAhyperlinks?.lastElement()?.id;
             const bid = targetBhyperlinks?.find(blink => container?.contains(blink))?.id ?? targetBhyperlinks?.lastElement()?.id;
             if (!aid || !bid) {
-                setTimeout(
-                    action(() => {
-                        this._forceAnimate += 0.01;
-                    })
-                );
+                setTimeout(action(() => (this._forceAnimate += 0.01)));
                 return null;
             }
             if (foundParent) {
                 setTimeout(
-                    action(() => {
-                        this._forceAnimate += 0.01;
-                    }),
+                    action(() => (this._forceAnimate += 0.01)),
                     1
                 );
             }
-
-            if (at || bt)
-                setTimeout(
-                    action(() => {
-                        this._forceAnimate += 0.01;
-                    })
-                ); // this forces an update during a transition animation
+            if (at || bt) {
+                setTimeout(action(() => (this._forceAnimate += 0.01))); // this forces an update during a transition animation
+            }
             const highlight = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Highlighting) as { highlightStyle: string; highlightColor: string; highlightIndex: number; highlightStroke: boolean };
             const highlightColor = highlight?.highlightIndex ? highlight?.highlightColor : undefined;
             const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string;
@@ -217,7 +208,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 id={this.DocumentView?.().DocUniqueId}
                                 className="linkBox-label"
                                 tabIndex={-1}
-                                ref={r => (this._divRef = r)}
+                                ref={this.setRef}
                                 onPointerDown={e => e.stopPropagation()}
                                 onFocus={() => {
                                     RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, this.dataDoc);
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 5b07b303a..1d6f41c65 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -250,6 +250,10 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps
         );
     }
 
+    setDocViewRef = (r: DocumentView | null) => {
+        const targetanchor = this._linkDoc && this._linkSrc && Doc.getOppositeAnchor(this._linkDoc, this._linkSrc);
+        targetanchor && this._targetDoc !== targetanchor && r?._props.focus?.(targetanchor, {});
+    };
     @computed get docPreview() {
         return (!this._linkDoc || !this._targetDoc || !this._linkSrc) && !this._toolTipText ? null : (
             <div className="linkDocPreview-inner">
@@ -279,10 +283,7 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps
                         this._toolTipText
                     ) : (
                         <DocumentView
-                            ref={r => {
-                                const targetanchor = this._linkDoc && this._linkSrc && Doc.getOppositeAnchor(this._linkDoc, this._linkSrc);
-                                targetanchor && this._targetDoc !== targetanchor && r?._props.focus?.(targetanchor, {});
-                            }}
+                            ref={this.setDocViewRef}
                             Document={this._targetDoc!}
                             moveDocument={returnFalse}
                             styleProvider={this._props.styleProvider}
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 603dcad5c..4677e0e61 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -171,19 +171,21 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         ContextMenu.Instance.addItem({ description: 'Options...', subitems, icon: 'video' });
     };
 
+    setRef = (r: HTMLVideoElement | null) => {
+        this._videoRef = r;
+        setTimeout(() => {
+            if (this.layoutDoc.mediaState === mediaState.PendingRecording && this._videoRef) {
+                this.toggleRecording();
+            }
+        }, 100);
+    };
+
     @computed get content() {
         return (
             <video
                 className="videoBox-content"
                 key="video"
-                ref={r => {
-                    this._videoRef = r;
-                    setTimeout(() => {
-                        if (this.layoutDoc.mediaState === mediaState.PendingRecording && this._videoRef) {
-                            this.toggleRecording();
-                        }
-                    }, 100);
-                }}
+                ref={this.setRef}
                 autoPlay={this._screenCapture}
                 style={{ width: this._screenCapture ? '100%' : undefined, height: this._screenCapture ? '100%' : undefined }}
                 onCanPlay={this.videoLoad}
diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx
index a4183a11a..a2fa83b5a 100644
--- a/src/client/views/nodes/calendarBox/CalendarBox.tsx
+++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx
@@ -298,10 +298,9 @@ export class CalendarBox extends CollectionSubView() {
                         ev.preventDefault();
                     });
                 }}
-
                 // for dragging and dropping (mirror)
 
-                eventDragStart={(arg) => {
+                eventDragStart={arg => {
                     const mirror = arg.el.cloneNode(true) as HTMLElement;
                     const rect = arg.el.getBoundingClientRect();
 
@@ -312,25 +311,24 @@ export class CalendarBox extends CollectionSubView() {
                     mirror.classList.add('custom-drag-mirror');
                     mirror.style.width = `${rect.width}px`;
                     mirror.style.height = `${rect.height}px`;
-                  
+
                     document.body.appendChild(mirror);
-                  
+
                     const moveListener = (ev: MouseEvent) => {
-                      mirror.style.left = `${ev.clientX}px`;
-                      mirror.style.top = `${ev.clientY}px`;
+                        mirror.style.left = `${ev.clientX}px`;
+                        mirror.style.top = `${ev.clientY}px`;
                     };
-                  
+
                     window.addEventListener('mousemove', moveListener);
 
                     // hide the actual box
                     arg.el.style.visibility = 'hidden';
                     arg.el.style.opacity = '0';
-                  
+
                     (arg.el as any)._mirrorElement = mirror;
                     (arg.el as any)._moveListener = moveListener;
                 }}
-                
-                eventDragStop={(arg) => {
+                eventDragStop={arg => {
                     const el = arg.el as any;
                     const mirror = el._mirrorElement;
                     const moveListener = el._moveListener;
@@ -338,15 +336,18 @@ export class CalendarBox extends CollectionSubView() {
                     // show the actual box
                     el.style.visibility = 'visible';
                     el.style.opacity = '1';
-                    
+
                     if (mirror) document.body.removeChild(mirror);
                     if (moveListener) window.removeEventListener('mousemove', moveListener);
                 }}
-                  
             />
         );
     }
 
+    setRef = (r: HTMLDivElement | null) => {
+        this.createDashEventsTarget(r);
+        this.fixWheelEvents(r, this._props.isContentActive);
+    };
     render() {
         const scale = this._props.ScreenToLocalTransform().Scale;
         const scaledWidth = this._props.PanelWidth();
@@ -361,11 +362,8 @@ export class CalendarBox extends CollectionSubView() {
                     height: scaledHeight,
                     overflow: 'hidden',
                     position: 'relative',
-                  }}
-                ref={r => {
-                    this.createDashEventsTarget(r);
-                    this.fixWheelEvents(r, this._props.isContentActive);
                 }}
+                ref={this.setRef}
                 onPointerDown={e => {
                     setTimeout(
                         action(() => {
@@ -383,9 +381,8 @@ export class CalendarBox extends CollectionSubView() {
                         transformOrigin: 'top left',
                         width: scaledWidth / scale,
                         height: scaledHeight / scale,
-                    }}
-                >
-                {this.renderCalendar}
+                    }}>
+                    {this.renderCalendar}
                 </div>
             </div>
         );
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
index 6c3da8977..8043111b9 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
@@ -974,6 +974,8 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     };
 
     _dictation: DictationButton | null = null;
+    setInputRef = (r: HTMLInputElement) => (this._textInputRef = r);
+    setDictationRef = (r: DictationButton) => (this._dictation = r);
     /**
      * Renders the chat interface, including the message list, input field, and other UI elements.
      */
@@ -1002,9 +1004,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
                 <form onSubmit={this.askGPT} className="chat-input">
                     <input
-                        ref={r => {
-                            this._textInputRef = r;
-                        }}
+                        ref={this.setInputRef}
                         type="text"
                         name="messageInput"
                         autoComplete="off"
@@ -1023,13 +1023,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                             </svg>
                         )}
                     </button>
-                    <DictationButton
-                        ref={r => {
-                            this._dictation = r;
-                        }}
-                        setInput={this.setChatInput}
-                        inputRef={this._textInputRef}
-                    />
+                    <DictationButton ref={this.setDictationRef} setInput={this.setChatInput} inputRef={this._textInputRef} />
                 </form>
                 {/* Popup for citation */}
                 {this._citationPopup.visible && (
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 07cb795f1..255ee1afe 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -2129,6 +2129,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         }
     };
 
+    setRef = (r: HTMLDivElement | null) => this.fixWheelEvents(r, this._props.isContentActive, this.onPassiveWheel);
+    setScrollRef = (r: HTMLDivElement | null) => (this._scrollRef = r);
     render() {
         TraceMobx();
         const scale = this.nativeScaling();
@@ -2143,7 +2145,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         ) : styleFromLayout?.height === '0px' ? null : (
             <div
                 className="formattedTextBox"
-                ref={r => this.fixWheelEvents(r, this._props.isContentActive, this.onPassiveWheel)}
+                ref={this.setRef}
                 style={{
                     ...(this._props.dontScale
                         ? {}
@@ -2181,9 +2183,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     onDoubleClick={this.onDoubleClick}>
                     <div
                         className="formattedTextBox-outer"
-                        ref={r => {
-                            this._scrollRef = r;
-                        }}
+                        ref={this.setScrollRef}
                         style={{
                             width: this.noSidebar ? '100%' : `calc(100% - ${this.sidebarWidthPercent})`,
                             overflow: this.layoutDoc._createDocOnCR || this.layoutDoc._layout_hideScroll ? 'hidden' : this.layout_autoHeight ? 'visible' : undefined,
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index cb2a1f13f..04b312ca5 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -1743,6 +1743,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return <div />;
     }
 
+    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
      */
@@ -1755,14 +1764,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         <ReactTextareaAutosize
                             placeholder="Use AI to suggest effects. Leave blank for random results."
                             className="pres-chatbox"
-                            ref={r => {
-                                setTimeout(() => {
-                                    if (r && !r.textContent) {
-                                        r.style.height = '';
-                                        r.style.height = r.scrollHeight + 'px';
-                                    }
-                                });
-                            }}
+                            ref={this.setAiEffectsRef}
                             value={this._animationChat}
                             onChange={e => {
                                 e.currentTarget.style.height = '';
@@ -1784,12 +1786,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         color={SnappingManager.userVariantColor}
                         onClick={this.customizeAnimations}
                     />
-                    <DictationButton
-                        ref={r => {
-                            this._animationDictation = r;
-                        }}
-                        setInput={this.setAnimationChat}
-                    />
+                    <DictationButton ref={this.setAnimDictationRef} setInput={this.setAnimationChat} />
                 </div>
                 <div style={{ alignItems: 'center' }}>
                     Click a box to use the effect.
@@ -1821,6 +1818,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         );
     }
 
+    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
@@ -1855,14 +1862,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 <ReactTextareaAutosize
                                     placeholder="Describe how to modify the slide properties."
                                     className="pres-chatbox"
-                                    ref={r => {
-                                        setTimeout(() => {
-                                            if (r && !r.textContent) {
-                                                r.style.height = '';
-                                                r.style.height = r.scrollHeight + 'px';
-                                            }
-                                        });
-                                    }}
+                                    ref={this.setPropertiesRef}
                                     value={this._chatInput}
                                     onChange={e => {
                                         e.currentTarget.style.height = '';
@@ -1874,12 +1874,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                         e.stopPropagation();
                                     }}
                                 />
-                                <DictationButton
-                                    ref={r => {
-                                        this._slideDictation = r;
-                                    }}
-                                    setInput={this.setChatInput}
-                                />
+                                <DictationButton ref={this.setSlideDictationRef} setInput={this.setChatInput} />
                             </div>
                             <Button
                                 style={{ alignSelf: 'flex-end' }}
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 568e48edf..660284397 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -515,6 +515,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
         </div>
     );
 
+    setDictationRef = (r: DictationButton | null) => (this._askDictation = r);
     promptBox = (heading: string, value: string, onChange: (e: string) => string, placeholder: string) => (
         <>
             <div className="gptPopup-sortBox">
@@ -541,7 +542,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
                     background={SettingsManager.userVariantColor}
                     onClick={() => this.callGpt(this._mode)}
                 />
-                <DictationButton ref={r => (this._askDictation = r)} setInput={onChange} />
+                <DictationButton ref={this.setDictationRef} setInput={onChange} />
             </div>
         </>
     );
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index d1dda106a..f2c09b6b7 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -18,6 +18,7 @@ export enum InkInkTool {
     Pen = 'Pen',
     Highlight = 'Highlight',
     Write = 'Write',
+    Math = 'Math',
 }
 
 export enum InkEraserTool {
-- 
cgit v1.2.3-70-g09d2

