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..

---
 .../views/nodes/formattedText/FormattedTextBox.tsx      | 17 +++++++----------
 1 file changed, 7 insertions(+), 10 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 57720baae..bab97f681 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -270,13 +270,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             e.preventDefault();
             e.stopPropagation();
             const targetCreator = (annotationOn?: Doc) => {
-                const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn);
+                const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
+                target.layout_fitWidth = true;
                 DocumentView.SetSelectOnLoad(target);
                 return target;
             };
 
+            const sourceAnchorCreator = () => this.getAnchor(true);
+
             const docView = this.DocumentView?.();
-            docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, () => this.getAnchor(true), targetCreator), e.pageX, e.pageY);
+            docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY);
         });
 
         AnchorMenu.Instance.AddDrawingAnnotation = (drawing: Doc) => {
@@ -1070,15 +1073,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
     getView = (doc: Doc, options: FocusViewOptions) => {
         if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
-            if (!this.SidebarShown) {
-                this.toggleSidebar(false);
-                options.didMove = true;
-            }
-            setTimeout(() => this._sidebarRef?.current?.makeDocUnfiltered(doc));
+            return SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, () => this.toggleSidebar(false), doc, options);
         }
-        return new Promise<Opt<DocumentView>>(res => {
-            DocumentView.addViewRenderedCb(doc, dv => res(dv));
-        });
+        return new Promise<Opt<DocumentView>>(res => DocumentView.addViewRenderedCb(doc, res));
     };
     focus = (textAnchor: Doc, options: FocusViewOptions) => {
         const focusSpeed = options.zoomTime ?? 500;
-- 
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/nodes/formattedText/FormattedTextBox.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 51b4b64eae4749542590785bca9d33d3ccdded69 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 19 May 2025 19:43:28 -0400
Subject: fixed text boxes to calculate height correctly.  removed usertag
 modification time in text.  fixed masonry and stacking views with pivots.

---
 src/ClientUtils.ts                                 |   3 +-
 .../collections/CollectionMasonryViewFieldRow.tsx  |  42 +++---
 .../views/collections/CollectionStackingView.scss  |   5 +-
 .../views/collections/CollectionStackingView.tsx   | 154 ++++++++-------------
 .../CollectionStackingViewFieldColumn.tsx          |  30 ++--
 .../views/nodes/formattedText/FormattedTextBox.tsx |  25 ++--
 src/client/views/nodes/formattedText/marks_rts.ts  |  14 --
 7 files changed, 111 insertions(+), 162 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts
index cc8b715b4..8eb1a39ec 100644
--- a/src/ClientUtils.ts
+++ b/src/ClientUtils.ts
@@ -670,7 +670,8 @@ export function setupMoveUpEvents(
 }
 
 export function DivHeight(ele: HTMLElement | null): number {
-    return ele ? Number(getComputedStyle(ele).height.replace('px', '')) : 0;
+    const hgt = ele ? Number(getComputedStyle(ele).height.replace('px', '')) : 0;
+    return Number.isNaN(hgt) ? 0 : hgt;
 }
 export function DivWidth(ele: HTMLElement | null): number {
     return ele ? Number(getComputedStyle(ele).width.replace('px', '')) : 0;
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index 89ccf5a0f..d1f7971d4 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -28,12 +28,14 @@ interface CMVFieldRowProps {
     headingObject: SchemaHeaderField | undefined;
     docList: Doc[];
     parent: CollectionStackingView;
+    panelWidth: () => number;
+    columnWidth: () => number;
     pivotField: string;
     type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined;
     createDropTarget: (ele: HTMLDivElement) => void;
     screenToLocalTransform: () => Transform;
     setDocHeight: (key: string, thisHeight: number) => void;
-    refList: Element[];
+    sectionRefs: Element[];
     showHandle: boolean;
 }
 
@@ -74,7 +76,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
     createRowDropRef = (ele: HTMLDivElement | null) => {
         this._dropDisposer?.();
         if (ele) this._dropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this), this._props.Doc);
-        else if (this._ele) this.props.refList.splice(this.props.refList.indexOf(this._ele), 1);
+        else if (this._ele) this.props.sectionRefs.splice(this.props.sectionRefs.indexOf(this._ele), 1);
         this._ele = ele;
     };
     @action
@@ -82,10 +84,10 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
         this.heading = this._props.headingObject?.heading || '';
         this.color = this._props.headingObject?.color || '#f1efeb';
         this.collapsed = this._props.headingObject?.collapsed || false;
-        this._ele && this.props.refList.push(this._ele);
+        this._ele && this.props.sectionRefs.push(this._ele);
     }
     componentWillUnmount() {
-        this._ele && this.props.refList.splice(this.props.refList.indexOf(this._ele), 1);
+        this._ele && this.props.sectionRefs.splice(this.props.sectionRefs.indexOf(this._ele), 1);
         this._ele = null;
     }
 
@@ -128,10 +130,8 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
         const key = this._props.pivotField;
         const castedValue = this.getValue(value);
         if (castedValue) {
-            if (this._props.parent.colHeaderData) {
-                if (this._props.parent.colHeaderData.map(i => i.heading).indexOf(castedValue.toString()) > -1) {
-                    return false;
-                }
+            if (this._props.parent.colHeaderData?.map(i => i.heading).indexOf(castedValue.toString()) || 0 > -1) {
+                return false;
             }
             key && this._props.docList.forEach(d => Doc.SetInPlace(d, key, castedValue, true));
             this._heading = castedValue.toString();
@@ -251,20 +251,11 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
     textCallback = (/* char: string */) => this.addDocument('', false);
 
     @computed get contentLayout() {
-        const rows = Math.max(1, Math.min(this._props.docList.length, Math.floor((this._props.parent._props.PanelWidth() - 2 * this._props.parent.xMargin) / (this._props.parent.columnWidth + this._props.parent.gridGap))));
-        const showChrome = !this._props.chromeHidden;
-        const stackPad = showChrome ? `0px ${this._props.parent.xMargin}px` : `${this._props.parent.yMargin}px ${this._props.parent.xMargin}px 0px ${this._props.parent.xMargin}px `;
+        const rows = Math.max(1, Math.min(this._props.docList.length, Math.floor(this._props.panelWidth() / this._props.columnWidth())));
         return this.collapsed ? null : (
             <div style={{ position: 'relative' }}>
-                {showChrome ? (
-                    <div
-                        className="collectionStackingView-addDocumentButton"
-                        style={
-                            {
-                                // width: style.columnWidth / style.numGroupColumns,
-                                // padding: `${NumCast(this._props.parent.layoutDoc._yPadding, this._props.parent.yMargin)}px 0px 0px 0px`,
-                            }
-                        }>
+                {!this._props.chromeHidden ? (
+                    <div className="collectionStackingView-addDocumentButton">
                         <EditableView GetValue={returnEmptyString} SetValue={this.addDocument} textCallback={this.textCallback} contents="+ NEW" />
                     </div>
                 ) : null}
@@ -272,11 +263,9 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
                     className="collectionStackingView-masonryGrid"
                     ref={this._contRef}
                     style={{
-                        padding: stackPad,
                         minHeight: this._props.showHandle && this._props.parent._props.isContentActive() ? '10px' : undefined,
-                        width: this._props.parent.NodeWidth,
                         gridGap: this._props.parent.gridGap,
-                        gridTemplateColumns: numberRange(rows).reduce(list => list + ` ${this._props.parent.columnWidth}px`, ''),
+                        gridTemplateColumns: numberRange(rows).reduce(list => list + ` ${this._props.columnWidth()}px`, ''),
                     }}>
                     {this._props.parent.children(this._props.docList)}
                 </div>
@@ -339,7 +328,12 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
     render() {
         const background = this._background;
         return (
-            <div className="collectionStackingView-masonrySection" style={{ width: this._props.parent.NodeWidth, background }} ref={this.createRowDropRef} onPointerEnter={this.pointerEnteredRow} onPointerLeave={this.pointerLeaveRow}>
+            <div
+                className="collectionStackingView-masonrySection"
+                style={{ width: this._props.pivotField ? this._props.panelWidth() : '100%', background }}
+                ref={this.createRowDropRef}
+                onPointerEnter={this.pointerEnteredRow}
+                onPointerLeave={this.pointerLeaveRow}>
                 {this.headingView}
                 {this.contentLayout}
             </div>
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 05ac52ff9..d6e4943ff 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -2,10 +2,12 @@
 
 .collectionMasonryView {
     display: inline;
+    flex-wrap: wrap;
 }
 
 .collectionStackingView {
     display: flex;
+    justify-content: space-between;
 }
 
 .collectionStackingMasonry-cont {
@@ -56,7 +58,6 @@
         top: 0;
         overflow-y: auto;
         overflow-x: hidden;
-        flex-wrap: wrap;
         transition: top 0.5s;
 
         > div {
@@ -210,7 +211,6 @@
     .collectionStackingView-sectionHeader {
         text-align: center;
         margin: auto;
-        margin-bottom: 10px;
         background: global.$medium-gray;
         // overflow: hidden; overflow is visible so the color menu isn't hidden -ftong
 
@@ -367,7 +367,6 @@
     .collectionStackingView-addGroupButton {
         display: flex;
         overflow: hidden;
-        margin: auto;
         width: 90%;
         overflow: ellipses;
 
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 25a222cbb..cc514f262 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -4,7 +4,7 @@ import { action, computed, IReactionDisposer, makeObservable, observable, Observ
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { ClientUtils, DivHeight, returnNone, returnZero, setupMoveUpEvents, smoothScroll } from '../../../ClientUtils';
-import { Doc, Opt } from '../../../fields/Doc';
+import { Doc, Field, Opt } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
@@ -35,6 +35,7 @@ import { CollectionStackingViewFieldColumn } from './CollectionStackingViewField
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 import { computedFn } from 'mobx-utils';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
+import { FieldsDropdown } from '../FieldsDropdown';
 
 export type collectionStackingViewProps = {
     sortFunc?: (a: Doc, b: Doc) => number;
@@ -48,15 +49,15 @@ export type collectionStackingViewProps = {
 @observer
 export class CollectionStackingView extends CollectionSubView<Partial<collectionStackingViewProps>>() {
     _disposers: { [key: string]: IReactionDisposer } = {};
+    _addGroupRef = React.createRef<HTMLDivElement>();
     _masonryGridRef: HTMLDivElement | null = null;
     // used in a column dragger, likely due for the masonry grid view. We want to use this
     _draggerRef = React.createRef<HTMLDivElement>();
     // keeping track of documents. Updated on internal and external drops. What's the difference?
     _docXfs: { height: () => number; width: () => number; stackedDocTransform: () => Transform }[] = [];
-    // Doesn't look like this field is being used anywhere. Obsolete?
-    _columnStart: number = 0;
 
-    @observable _refList: HTMLElement[] = [];
+    @observable _colStackRefs: HTMLElement[] = [];
+    @observable _colHdrRefs: HTMLElement[] = [];
     // map of node headers to their heights. Used in Masonry
     @observable _heightMap = new Map<string, number>();
     // Assuming that this is the current css cursor style
@@ -67,10 +68,25 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     @computed get chromeHidden() {
         return this._props.chromeHidden || BoolCast(this.layoutDoc.chromeHidden);
     }
-    // it looks like this gets the column headers that Mehek was showing just now
     @computed get colHeaderData() {
         return Cast(this.dataDoc['_' + this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null);
     }
+
+    @computed get Sections() {
+        return this.filteredChildren.reduce(
+            (map, d) => {
+                const docHeader = d[this.pivotField] ? d[this.pivotField] : `NO ${this.pivotField.toUpperCase()} VALUE`;
+                const docHeaderString = docHeader !== undefined ? Field.toString(docHeader) : `NO ${this.pivotField.toUpperCase()} VALUE`;
+
+                // find existing header or create
+                const existingHeader = Array.from(map.keys()).find(sh => sh.heading === docHeaderString);
+                if (!existingHeader) map.set(new SchemaHeaderField(docHeaderString), [d]);
+                else map.get(existingHeader)!.push(d);
+                return map;
+            },
+            new ObservableMap<SchemaHeaderField, Doc[]>(this.colHeaderData?.map(hdata => [hdata, []] as [SchemaHeaderField, Doc[]]) ?? [])
+        );
+    }
     // Still not sure what a pivot is, but it appears that we can actually filter docs somehow?
     @computed get pivotField() {
         return StrCast(this.layoutDoc._pivotField);
@@ -107,9 +123,12 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     @computed get showAddAGroup() {
         return this.pivotField && !this.chromeHidden;
     }
+    @computed get availableWidth() {
+        return this._props.PanelWidth() - 2 * this.xMargin - (this.isStackingView ? this.gridGap * ((this.numGroupColumns || 1) - 1) : 0);
+    }
     // columnWidth handles the margin on the left and right side of the documents
     @computed get columnWidth() {
-        const availableWidth = this._props.PanelWidth() - 2 * this.xMargin;
+        const availableWidth = this.availableWidth;
         const cwid = availableWidth / (NumCast(this.Document._layout_columnCount) || this._props.PanelWidth() / NumCast(this.Document._layout_columnWidth, this._props.PanelWidth() / 4));
         return Math.min(availableWidth, this.isStackingView ? availableWidth / (this.numGroupColumns || 1) : cwid - this.gridGap);
     }
@@ -121,28 +140,17 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     constructor(props: SubCollectionViewProps) {
         super(props);
         makeObservable(this);
-        if (this.colHeaderData === undefined) {
-            // TODO: what is a layout doc? Is it literally how this document is supposed to be layed out?
-            // here we're making an empty list of column headers (again, what Mehek showed us)
-            this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>();
-        }
     }
 
+    availableWidthFn = () => this.availableWidth;
     columnWidthFn = () => this.columnWidth;
     columnDocHeightFn = (doc: Doc) => () => (this.isStackingView ? this.getDocHeight(doc)() : Math.min(this.getDocHeight(doc)(), this._props.PanelHeight()));
 
-    // TODO: plj - these are the children
     children = (docs: Doc[]) => {
-        // TODO: can somebody explain me to what exactly TraceMobX is?
         TraceMobx();
-        // appears that we are going to reset the _docXfs. TODO: what is Xfs?
         this._docXfs.length = 0;
-        this._renderCount < docs.length &&
-            setTimeout(
-                action(() => {
-                    this._renderCount = Math.min(docs.length, this._renderCount + 5);
-                })
-            );
+        this._renderCount < docs.length && 
+            setTimeout(action(() => (this._renderCount = Math.min(docs.length, this._renderCount + 5)))); // prettier-ignore
         return docs.map((d, i) => {
             // assuming we need to get rowSpan because we might be dealing with many columns. Grid gap makes sense if multiple columns
             const rowSpan = Math.ceil((this.getDocHeight(d)() + this.gridGap) / this.gridGap);
@@ -153,76 +161,28 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                       margin: undefined,
                       transition: this.getDocTransition(d)(),
                       width: this.columnWidth,
-                      marginTop: i ? this.gridGap : 0,
                       height: this.getDocHeight(d)(),
                       zIndex: DocumentView.getFirstDocumentView(d)?.IsSelected ? 1000 : 0,
                   }
                 : { gridRowEnd: `span ${rowSpan}`, zIndex: DocumentView.getFirstDocumentView(d)?.IsSelected ? 1000 : 0 };
             // So we're choosing whether we're going to render a column or a masonry doc
             return (
-                <div className={`collectionStackingView-${this.isStackingView ? 'columnDoc' : 'masonryDoc'}`} key={d[Id]} style={style}>
+                <div className={`collectionStackingView${this.isStackingView ? '-columnDoc' : '-masonryDoc'}`} key={d[Id]} style={style}>
                     {this.getDisplayDoc(d, this.getDocTransition(d), i)}
                 </div>
             );
         });
     };
     @action
-    setDocHeight = (key: string, sectionHeight: number) => {
-        this._heightMap.set(key, sectionHeight);
+    setDocHeight = (key: string, sectionHeight: number) => this._heightMap.set(key, sectionHeight);
+
+    setAutoHeight = () => {
+        const maxHeader = this.isStackingView ? this._colHdrRefs.reduce((p, r) => Math.max(p, DivHeight(r)), 0) + (this._colHdrRefs.length ? this.gridGap : 0) : 0;
+        const maxCol = this.isStackingView
+            ? this._colStackRefs.reduce((p, r) => Math.max(p, DivHeight(r)), 0) + this.gridGap
+            : this._colStackRefs.reduce((p, r) => p + DivHeight(r), this._addGroupRef.current ? DivHeight(this._addGroupRef.current) : 0);
+        this._props.setHeight?.(this.headerMargin + 2 * this.yMargin + maxCol + maxHeader);
     };
-
-    // is sections that all collections inherit? I think this is how we show the masonry/columns
-    // TODO: this seems important
-    get Sections() {
-        // appears that pivot field IS actually for sorting
-        if (!this.pivotField || this.colHeaderData instanceof Promise) return new Map<SchemaHeaderField, Doc[]>();
-
-        if (this.colHeaderData === undefined) {
-            setTimeout(() => {
-                this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>();
-            });
-            return new Map<SchemaHeaderField, Doc[]>();
-        }
-        const colHeaderData = Array.from(this.colHeaderData);
-        const fields = new Map<SchemaHeaderField, Doc[]>(colHeaderData.map(sh => [sh, []] as [SchemaHeaderField, []]));
-        let changed = false;
-        this.filteredChildren.forEach(d => {
-            const sectionValue = (d[this.pivotField] ? d[this.pivotField] : `NO ${this.pivotField.toUpperCase()} VALUE`) as object;
-            // the next five lines ensures that floating point rounding errors don't create more than one section -syip
-            const parsed = parseInt(sectionValue.toString());
-            const castedSectionValue = !isNaN(parsed) ? parsed : sectionValue;
-
-            // look for if header exists already
-            const existingHeader = colHeaderData.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`));
-            if (existingHeader) {
-                fields.get(existingHeader)!.push(d);
-            } else {
-                const newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`);
-                fields.set(newSchemaHeader, [d]);
-                colHeaderData.push(newSchemaHeader);
-                changed = true;
-            }
-        });
-        // remove all empty columns if hideHeadings is set
-        // we will want to have something like this, so that we can hide columns and add them back in
-        if (this.layoutDoc._columnsHideIfEmpty) {
-            Array.from(fields.keys())
-                .filter(key => !fields.get(key)!.length)
-                .forEach(header => {
-                    fields.delete(header);
-                    colHeaderData.splice(colHeaderData.indexOf(header), 1);
-                    changed = true;
-                });
-        }
-        changed &&
-            setTimeout(
-                action(() => this.colHeaderData?.splice(0, this.colHeaderData.length, ...colHeaderData)),
-                0
-            );
-        return fields;
-    }
-
-    setAutoHeight = () => this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : 2 * this.yMargin + this._refList.reduce((p, r) => p + DivHeight(r), 0)));
     observer = new ResizeObserver(this.setAutoHeight);
 
     componentDidMount() {
@@ -232,9 +192,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         // reset section headers when a new filter is inputted
         this._disposers.pivotField = reaction(
             () => this.pivotField,
-            () => {
-                this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List();
-            }
+            () => (this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List())
         );
         // reset section headers when a new filter is inputted
         this._disposers.width = reaction(
@@ -252,7 +210,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         );
 
         this._disposers.refList = reaction(
-            () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !DocumentView.LightboxContains(this.DocumentView?.()) }),
+            () => ({ refList: this._colStackRefs.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !DocumentView.LightboxContains(this.DocumentView?.()) }),
             ({ refList, autoHeight }) => {
                 this.observer.disconnect();
                 if (autoHeight) refList.forEach(r => this.observer.observe(r));
@@ -409,11 +367,10 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     getDocWidth = computedFn((d?: Doc) => () => {
         if (!d) return 0;
         const childLayoutDoc = Doc.LayoutDoc(d, this._props.childLayoutTemplate?.());
-        const maxWidth = this.columnWidth / this.numGroupColumns;
         if (!this.layoutDoc._columnsFill && !this.childFitWidth(childLayoutDoc)) {
-            return Math.min(NumCast(d._width), maxWidth);
+            return Math.min(NumCast(d._width), this.columnWidth);
         }
-        return maxWidth;
+        return this.columnWidth;
     });
     getDocTransition = computedFn((d?: Doc) => () => StrCast(d?.dataTransition));
     getDocHeight = computedFn((d?: Doc) => () => {
@@ -424,7 +381,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!this.childFitWidth(childLayoutDoc) ? NumCast(d._width) : 0);
         const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!this.childFitWidth(childLayoutDoc) ? NumCast(d._height) : 0);
         if (nw && nh) {
-            const colWid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
+            const colWid = this.columnWidth;
             const docWid = this.layoutDoc._columnsFill ? colWid : Math.min(this.getDocWidth(d)(), colWid);
             return Math.min(maxHeight, (docWid * nh) / nw);
         }
@@ -571,7 +528,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         }
         return (
             <CollectionStackingViewFieldColumn
-                refList={this._refList}
+                colStackRefs={this._colStackRefs}
+                colHeaderRefs={this._colHdrRefs}
                 addDocument={this.addDocument}
                 chromeHidden={this.chromeHidden}
                 colHeaderData={this.colHeaderData}
@@ -613,8 +571,10 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                         showHandle={first}
                         Doc={this.Document}
                         chromeHidden={this.chromeHidden}
+                        panelWidth={this.availableWidthFn}
+                        columnWidth={this.columnWidthFn}
                         pivotField={this.pivotField}
-                        refList={this._refList}
+                        sectionRefs={this._colStackRefs}
                         key={heading ? heading.heading : ''}
                         rows={rows}
                         headings={this.headings}
@@ -636,8 +596,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     @action
     addGroup = (value: string) => {
         if (value && this.colHeaderData) {
-            const schemaHdrField = new SchemaHeaderField(value);
-            this.colHeaderData.push(schemaHdrField);
+            this.colHeaderData.push(new SchemaHeaderField(value));
             return true;
         }
         return false;
@@ -745,7 +704,11 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                             this.fixWheelEvents(ele, this._props.isContentActive);
                         }}
                         style={{
-                            overflowY: this.isContentActive() ? 'auto' : 'hidden',
+                            paddingBottom: this.yMargin,
+                            paddingTop: this.yMargin,
+                            paddingLeft: this.xMargin,
+                            paddingRight: this.xMargin,
+                            overflowY: this.isContentActive() && !this.layoutDoc._layout_autoHeight ? 'auto' : 'hidden',
                             background: this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string,
                             pointerEvents: this._props.pointerEvents?.() ?? this.backgroundEvents,
                         }}
@@ -756,11 +719,12 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                         onContextMenu={this.onContextMenu}
                         onWheel={e => this.isContentActive() && e.stopPropagation()}>
                         {this.renderedSections}
-                        {!this.showAddAGroup ? null : (
-                            <div key={`${this.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" style={{ width: !this.isStackingView ? '100%' : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}>
-                                <EditableView {...editableViewProps} />
-                            </div>
-                        )}
+                        <div className="collectionStackingView-addGroupButton" ref={this._addGroupRef} style={{ width: !this.isStackingView ? '100%' : this.columnWidth, display: this.showAddAGroup ? undefined : 'none' }}>
+                            <EditableView {...editableViewProps} />
+                        </div>
+                        <div style={{ right: 0, top: 0, position: 'absolute', display: !this.layoutDoc._pivotField ? 'none' : undefined }}>
+                            <FieldsDropdown Doc={this.Document} selectFunc={fieldKey => (this.layoutDoc._pivotField = fieldKey)} placeholder={StrCast(this.layoutDoc._pivotField)} />
+                        </div>
                     </div>
                 </div>
             </>
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 345f60e75..8c7cb8276 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { DivHeight, DivWidth, returnEmptyString, returnTrue, setupMoveUpEvents } from '../../../ClientUtils';
@@ -50,14 +50,14 @@ interface CSVFieldColumnProps {
     addDocument: (doc: Doc | Doc[]) => boolean;
     createDropTarget: (ele: HTMLDivElement) => void;
     screenToLocalTransform: () => Transform;
-    refList: HTMLElement[];
+    colStackRefs: HTMLElement[];
+    colHeaderRefs: HTMLElement[];
 }
 
 @observer
 export class CollectionStackingViewFieldColumn extends ObservableReactComponent<CSVFieldColumnProps> {
     private dropDisposer?: DragManager.DragDropDisposer;
     private _disposers: { [name: string]: IReactionDisposer } = {};
-    private _headerRef: React.RefObject<HTMLDivElement> = React.createRef();
     @observable _background = 'inherit';
     @observable _paletteOn = false;
     @observable _heading = '';
@@ -72,6 +72,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
 
     _ele: HTMLElement | null = null;
     _eleMasonrySingle = React.createRef<HTMLDivElement>();
+    _headerRef: HTMLDivElement | null = null;
 
     protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetDropAction: dropActionType) => {
         const dragData = de.complete.docDragData;
@@ -93,13 +94,13 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
     createColumnDropRef = (ele: HTMLDivElement | null) => {
         this.dropDisposer?.();
         if (ele) this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this), this._props.Doc, this.onInternalPreDrop.bind(this));
-        else if (this._eleMasonrySingle.current) this.props.refList.splice(this.props.refList.indexOf(this._eleMasonrySingle.current), 1);
+        else if (this._eleMasonrySingle.current) runInAction(() => this.props.colStackRefs.splice(this.props.colStackRefs.indexOf(this._eleMasonrySingle.current!), 1));
         this._ele = ele;
     };
 
     @action
     componentDidMount() {
-        this._eleMasonrySingle.current && this.props.refList.push(this._eleMasonrySingle.current);
+        this._eleMasonrySingle.current && this.props.colStackRefs.push(this._eleMasonrySingle.current);
         this._disposers.collapser = reaction(
             () => this._props.headingObject?.collapsed,
             collapsed => { this.collapsed = collapsed !== undefined ? BoolCast(collapsed) : false; }, // prettier-ignore
@@ -108,7 +109,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
     }
     componentWillUnmount() {
         this._disposers.collapser?.();
-        this._ele && this.props.refList.splice(this.props.refList.indexOf(this._ele), 1);
+        this._ele && runInAction(() => this.props.colStackRefs.splice(this.props.colStackRefs.indexOf(this._ele!), 1));
         this._ele = null;
     }
 
@@ -192,7 +193,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
         value = typeof value === 'string' ? `"${value}"` : value;
         embedding.viewSpecScript = ScriptField.MakeFunction(`doc.${this._props.pivotField} === ${value}`, { doc: Doc.name });
         if (embedding.viewSpecScript) {
-            DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([embedding]), e.clientX, e.clientY);
+            DragManager.StartDocumentDrag([this._headerRef!], new DragManager.DocumentDragData([embedding]), e.clientX, e.clientY);
             return true;
         }
         return false;
@@ -314,9 +315,14 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
             <div
                 key={heading}
                 className="collectionStackingView-sectionHeader"
-                ref={this._headerRef}
+                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;
+                }}
                 style={{
-                    marginTop: this._props.yMargin,
+                    marginTop: 0,
+                    marginBottom: this._props.gridGap,
                     width: this._props.columnWidth,
                 }}>
                 {/* the default bucket (no key value) has a tooltip that describes what it is.
@@ -367,7 +373,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
                             ref={this._eleMasonrySingle}
                             className="collectionStackingView-masonrySingle"
                             style={{
-                                padding: `${columnYMargin}px ${0}px ${this._props.yMargin}px ${0}px`,
+                                padding: `${columnYMargin}px ${0}px ${0}px ${0}px`,
                                 margin: this._props.dontCenter.includes('x') ? undefined : 'auto',
                                 height: 'max-content',
                                 position: 'relative',
@@ -400,15 +406,13 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
 
     render() {
         TraceMobx();
-        const headings = this._props.headings();
         const heading = this._heading;
-        const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
         return (
             <div
                 className="collectionStackingViewFieldColumn"
                 key={heading}
                 style={{
-                    width: `${100 / (uniqueHeadings.length + (this._props.chromeHidden ? 0 : 1) || 1)}%`,
+                    width: this._props.columnWidth,
                     height: undefined,
                     background: this._background,
                 }}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 4189fcc0c..fea2986c7 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1522,7 +1522,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         }
         if (this.EditorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
             const { $from } = this.EditorView.state.selection;
-            const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) });
+            const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() });
             const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? [];
             const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
             if (selLoadChar === 'Enter') {
@@ -1549,7 +1549,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             const { text, paragraph } = schema.nodes;
             const selNode = this.EditorView.state.selection.$anchor.node();
             if (this.EditorView.state.selection.from === 1 && this.EditorView.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) {
-                const docDefaultMarks = [schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })];
+                const docDefaultMarks = [schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() })];
                 this.EditorView.state.selection.empty && this.EditorView.state.selection.from === 1 && this.EditorView?.dispatch(this.EditorView?.state.tr.setStoredMarks(docDefaultMarks).removeStoredMark(schema.marks.pFontColor));
             }
         }
@@ -1685,7 +1685,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 if (e.clientX > boundsRect.left && e.clientX < boundsRect.right && e.clientY > boundsRect.bottom) {
                     // if we clicked below the last prosemirror div, then set the selection to be the end of the document
                     editorView.focus();
-                    editorView.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, editorView.state.doc.content.size)));
+                    // editorView.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, editorView.state.doc.content.size)));
                 }
             } else if (node && [editorView.state.schema.nodes.ordered_list, editorView.state.schema.nodes.listItem].includes(node.type) && node !== (editorView.state.selection as NodeSelection)?.node && pcords) {
                 editorView.dispatch(editorView.state.tr.setSelection(NodeSelection.create(editorView.state.doc, pcords.pos)));
@@ -1814,7 +1814,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             return;
         }
         if (this._enteringStyle && 'tix!'.includes(e.key)) {
-            const tag = e.key === 't' ? 'todo' : e.key === 'i' ? 'ignore' : e.key === 'x' ? 'disagree' : e.key === '!' ? 'important' : '??';
             const node = state.selection.$from.nodeAfter;
             const start = state.selection.from;
             const end = state.selection.to;
@@ -1823,9 +1822,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 StopEvent(e);
                 _editorView.dispatch(
                     state.tr
-                        .removeMark(start, end, schema.marks.user_mark)
-                        .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }))
-                        .addMark(start, end, schema.marks.user_tag.create({ userid: ClientUtils.CurrentUserEmail(), tag, modified: Math.round(Date.now() / 1000 / 60) }))
+                        .removeMark(start, end, schema.marks.user_mark) //
+                        .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() }))
                 );
                 return;
             }
@@ -1858,9 +1856,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 break;
             default:
                 if ([AclEdit, AclAugment, AclAdmin].includes(GetEffectiveAcl(this.Document))) {
-                    const modified = Math.floor(Date.now() / 1000);
-                    const mark = state.selection.$to.marks().find(m => m.type === schema.marks.user_mark && m.attrs.modified === modified);
-                    _editorView.dispatch(state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified })));
+                    const mark = state.selection.$to.marks().find(m => m.type === schema.marks.user_mark);
+                    _editorView.dispatch(
+                        state.tr
+                            .removeStoredMark(schema.marks.user_mark) //
+                            .addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() }))
+                    );
                 }
                 break;
         }
@@ -1884,14 +1885,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yMargin || 0);
         const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
         if (this.EditorView && children && !SnappingManager.IsDragging) {
-            const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0;
+            const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), 0) ?? 0;
             const toNum = (val: string) => Number(val.replace('px', ''));
             const toHgt = (node: Element): number => {
                 const { height, marginTop, marginBottom } = getComputedStyle(node);
                 const childHeight = height === 'auto' ? getChildrenHeights(Array.from(node.children)) : toNum(height);
                 return childHeight + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom));
             };
-            const proseHeight = !this.ProseRef ? 0 : getChildrenHeights(children);
+            const proseHeight = !this.ProseRef ? 0 : getChildrenHeights(children) + margins;
             const scrollHeight = this.ProseRef && proseHeight;
             if (this._props.setHeight && !this._props.suppressSetHeight && scrollHeight && !this._props.dontRegisterView) {
                 // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index b7dae1ca3..333ee6be8 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -330,20 +330,6 @@ export const marks: { [index: string]: MarkSpec } = {
             return ['span', { class: 'UM-' + uid + remote + ' UM-min-' + min + ' UM-hr-' + hr + ' UM-day-' + day }, 0];
         },
     },
-    // the id of the user who entered the text
-    user_tag: {
-        attrs: {
-            userid: { default: '' },
-            modified: { default: 'when?' }, // 1 second intervals since 1970
-            tag: { default: '' },
-        },
-        group: 'inline',
-        inclusive: false,
-        toDOM: node => {
-            const uid = node.attrs.userid.replace('.', '').replace('@', '');
-            return ['span', { class: 'UT-' + uid + ' UT-' + node.attrs.tag }, 0];
-        },
-    },
 
     // :: MarkSpec Code font mark. Represented as a `<code>` element.
     code: {
-- 
cgit v1.2.3-70-g09d2


From 3efe20d11332df95237c13a11fa99918552ed290 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 27 May 2025 11:29:47 -0400
Subject: fixed resetting autoheight by dragging up to work when nativewidth's
 are set.  fixed autoheight on text to work properly when native dims are set.
  fixed templates to support native witdths and heights.

---
 src/client/views/DocumentDecorations.tsx           | 20 ++++++++---------
 .../views/nodes/formattedText/FormattedTextBox.tsx | 25 ++++++++++++----------
 src/fields/Doc.ts                                  |  5 ++---
 3 files changed, 26 insertions(+), 24 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index a94bee7cb..69c2467a3 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -607,8 +607,10 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             if (nwidth && nheight && !cornerReflow && !horizontalReflow && !verticalReflow) {
                 scale.x === 1 ? (scale.x = scale.y) : (scale.y = scale.x);
             }
+            if (NumCast(doc._height) * scale.y < NumCast(doc._height_min, 10)) scale.y = NumCast(doc._height_min, 10) / NumCast(doc._height);
+            if (NumCast(doc._width) * scale.x < NumCast(doc._width_min, 25)) scale.x = NumCast(doc._width_min, 25) / NumCast(doc._width);
 
-            if ((horizontalReflow || cornerReflow) && Doc.NativeWidth(doc)) {
+            if ((horizontalReflow || cornerReflow) && Doc.NativeWidth(doc) && scale.x > 0) {
                 const setData = Doc.NativeWidth(doc[DocData]) === doc.nativeWidth;
                 doc._nativeWidth = scale.x * Doc.NativeWidth(doc);
                 if (setData) Doc.SetNativeWidth(doc[DocData], NumCast(doc.nativeWidth));
@@ -616,25 +618,23 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                     doc._nativeHeight = (initHeight / initWidth) * nwidth; // initializes the nativeHeight for a PDF
                 }
             }
-            if ((verticalReflow || cornerReflow) && Doc.NativeHeight(doc)) {
+            if ((verticalReflow || cornerReflow) && Doc.NativeHeight(doc) && scale.y > 0) {
                 const setData = Doc.NativeHeight(doc[DocData]) === doc.nativeHeight && !doc.layout_reflowVertical;
                 doc._nativeHeight = scale.y * Doc.NativeHeight(doc);
                 if (setData) Doc.SetNativeHeight(doc[DocData], NumCast(doc._nativeHeight));
             }
 
-            doc._width = Math.max(NumCast(doc._width_min, 25), NumCast(doc._width) * scale.x);
-            doc._height = Math.max(NumCast(doc._height_min, 10), NumCast(doc._height) * scale.y);
+            doc._width = NumCast(doc._width) * scale.x;
+            doc._height = NumCast(doc._height) * scale.y;
             const { deltaX, deltaY } = this.realignRefPt(doc, refCent, initWidth || 1, initHeight || 1);
             doc.x = NumCast(doc.x) + deltaX;
             doc.y = NumCast(doc.y) + deltaY;
 
             doc._layout_modificationDate = new DateField();
-            if (scale.y !== 1) {
-                const docLayout = docView.layoutDoc;
-                docLayout._layout_autoHeight = undefined;
-                if (docView.layoutDoc._layout_autoHeight) {
-                    // if autoHeight is still on because of a prototype
-                    docLayout._layout_autoHeight = false; // then don't inherit, but explicitly set it to false
+            if (scale.y !== 1 && !opts.freezeNativeDims) {
+                doc._layout_autoHeight = undefined;
+                if (doc._layout_autoHeight) {
+                    doc._layout_autoHeight = false; // set explicitly to false if inherited from parent of layout
                 }
             }
         }
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index fea2986c7..1827cd2dc 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1142,13 +1142,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         return undefined;
     };
 
-    // if the scroll height has changed and we're in layout_autoHeight mode, then we need to update the textHeight component of the doc.
-    // Since we also monitor all component height changes, this will update the document's height.
-    resetNativeHeight = action((scrollHeight: number) => {
-        this.layoutDoc['_' + this.fieldKey + '_height'] = scrollHeight;
-        if (!this.layoutDoc.isTemplateForField && NumCast(this.layoutDoc._nativeHeight)) this.layoutDoc._nativeHeight = scrollHeight;
-    });
-
     addPlugin = (plugin: Plugin) => {
         const editorView = this.EditorView;
         if (editorView) {
@@ -1179,21 +1172,31 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         this._disposers.width = reaction(this._props.PanelWidth, this.tryUpdateScrollHeight);
         this._disposers.scrollHeight = reaction(
             () => ({ scrollHeight: this.scrollHeight, layoutAutoHeight: this.layout_autoHeight, width: NumCast(this.layoutDoc._width) }),
-            ({ width, scrollHeight, layoutAutoHeight }) => width && layoutAutoHeight && this.resetNativeHeight(scrollHeight),
+            ({ width, scrollHeight, layoutAutoHeight }) => width && layoutAutoHeight && (this.layoutDoc['_' + this.fieldKey + '_height'] = scrollHeight),
             { fireImmediately: true }
         );
         this._disposers.componentHeights = reaction(
             // set the document height when one of the component heights changes and layout_autoHeight is on
-            () => ({ border: this._props.PanelHeight(), sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layoutAutoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }),
-            ({ border, sidebarHeight, textHeight, layoutAutoHeight, marginsHeight }) => {
+            () => ({
+                border: this._props.PanelHeight(),
+                scrollHeight: NumCast(this.layoutDoc['_' + this.fieldKey + '_height']),
+                sidebarHeight: this.sidebarHeight,
+                textHeight: this.textHeight,
+                layoutAutoHeight: this.layout_autoHeight,
+                marginsHeight: this.layout_autoHeightMargins,
+            }),
+            ({ border, sidebarHeight, scrollHeight, textHeight, layoutAutoHeight, marginsHeight }) => {
                 const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight));
                 if (
                     (!Array.from(this._curHighlights).includes('Bold Text') || this._props.isSelected()) && //
                     layoutAutoHeight &&
                     newHeight &&
-                    (newHeight !== this.layoutDoc.height || border < NumCast(this.layoutDoc.height)) &&
+                    (newHeight !== this.layoutDoc.height || border < NumCast(this.layoutDoc.height) || this.layoutDoc._nativeHeight !== scrollHeight) &&
                     !this._props.dontRegisterView
                 ) {
+                    if (NumCast(this.layoutDoc.nativeHeight)) {
+                        this.layoutDoc._nativeHeight = scrollHeight;
+                    }
                     this._props.setHeight?.(newHeight);
                 }
             },
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index e110550e6..366dc6a20 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1202,14 +1202,14 @@ export namespace Doc {
     }
     export function NativeWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) {
         // if this is a field template, then don't use the doc's nativeWidth/height
-        return !doc ? 0 : NumCast(doc.isTemplateForField ? undefined : doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeWidth'], !doc.isTemplateForField && useWidth ? NumCast(doc._width) : 0));
+        return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeWidth'], useWidth ? NumCast(doc._width) : 0));
     }
     export function NativeHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) {
         if (!doc) return 0;
         const nheight = (Doc.NativeWidth(doc, dataDoc, useHeight) / NumCast(doc._width)) * NumCast(doc._height); // divide before multiply to avoid floating point errrorin case nativewidth = width
         const dheight = NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeHeight'], useHeight ? NumCast(doc._height) : 0);
         // if this is a field template, then don't use the doc's nativeWidth/height
-        return NumCast(doc.isTemplateForField ? undefined : doc._nativeHeight, nheight || dheight);
+        return NumCast(doc._nativeHeight, nheight || dheight);
     }
 
     export function OutpaintingWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) {
@@ -1479,7 +1479,6 @@ export namespace Doc {
                 layoutDoc._nativeWidth = undefined;
                 layoutDoc._nativeHeight = undefined;
             } else {
-                layoutDoc._layout_autoHeight = false;
                 if (!Doc.NativeWidth(layoutDoc)) {
                     layoutDoc._nativeWidth = NumCast(layoutDoc._width, panelWidth);
                     layoutDoc._nativeHeight = NumCast(layoutDoc._height, panelHeight);
-- 
cgit v1.2.3-70-g09d2


From 76a21badf70c82388872ec5485858ab06e303bca Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 30 May 2025 09:57:46 -0400
Subject: fixed using empty images in templates to support assigning a new
 image and getting native dimensions right.  fixed native dimensions of images
 within templates to not use native dims of template. fixed undo for text
 templates that don't have a text box.  fixed image dropping on images that
 are empty (to assign the dropped image).

---
 src/client/documents/DocUtils.ts                   | 20 +++---
 .../collections/collectionFreeForm/MarqueeView.tsx |  1 +
 src/client/views/nodes/EquationBox.tsx             |  9 +--
 src/client/views/nodes/ImageBox.tsx                | 84 +++++++++++++---------
 src/client/views/nodes/LabelBox.tsx                |  8 ++-
 .../views/nodes/formattedText/FormattedTextBox.tsx | 11 +--
 src/fields/Doc.ts                                  |  4 +-
 7 files changed, 80 insertions(+), 57 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index 41a61b154..5aae50ee8 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -642,27 +642,27 @@ export namespace DocUtils {
         return dd;
     }
 
-    export function assignUploadInfo(result: Upload.FileInformation, protoIn: Doc) {
+    export function assignUploadInfo(result: Upload.FileInformation, protoIn: Doc, fieldKey: string = 'data') {
         const proto = protoIn;
 
         if (Upload.isTextInformation(result)) {
             proto.text = result.rawText;
         }
         if (Upload.isVideoInformation(result)) {
-            proto.data_duration = result.duration;
+            proto[`${fieldKey}_duration`] = result.duration;
         }
         if (Upload.isImageInformation(result)) {
             const maxNativeDim = Math.max(result.nativeHeight, result.nativeWidth);
             const exifRotation = StrCast(result.exifData?.data?.Orientation).toLowerCase();
-            proto.data_nativeOrientation = result.exifData?.data?.image?.Orientation ?? (exifRotation.includes('rotate 90') || exifRotation.includes('rotate 270') ? 5 : undefined);
-            proto.data_nativeWidth = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim;
-            proto.data_nativeHeight = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight);
-            if (NumCast(proto.data_nativeOrientation) >= 5) {
-                proto.data_nativeHeight = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim;
-                proto.data_nativeWidth = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight);
+            proto[`${fieldKey}_nativeOrientation`] = result.exifData?.data?.image?.Orientation ?? (exifRotation.includes('rotate 90') || exifRotation.includes('rotate 270') ? 5 : undefined);
+            proto[`${fieldKey}_nativeWidth`] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim;
+            proto[`${fieldKey}_nativeHeight`] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight);
+            if (NumCast(proto[`${fieldKey}_nativeOrientation`]) >= 5) {
+                proto[`${fieldKey}_nativeHeight`] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim;
+                proto[`${fieldKey}_nativeWidth`] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight);
             }
-            proto.data_exif = JSON.stringify(result.exifData?.data);
-            proto.data_contentSize = result.contentSize;
+            proto[`${fieldKey}_exif`] = JSON.stringify(result.exifData?.data);
+            proto[`${fieldKey}_contentSize`] = result.contentSize;
             // exif gps data coordinates are stored in DMS (Degrees Minutes Seconds), the following operation converts that to decimal coordinates
             const latitude = result.exifData?.data?.GPSLatitude;
             const latitudeDirection = result.exifData?.data?.GPSLatitudeRef;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 37959a5f0..c120cddf0 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -190,6 +190,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             FormattedTextBox.SelectOnLoadChar = Doc.UserDoc().defaultTextLayout && !this._props.childLayoutString ? e.key : '';
             FormattedTextBox.LiveTextUndo = UndoManager.StartBatch('type new note');
             this._props.addLiveTextDocument(DocUtils.GetNewTextDoc('-typed text-', x, y, 200, 100));
+            setTimeout(() => FormattedTextBox.LiveTextUndo?.end(), 100);
             e.stopPropagation();
         }
     };
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index 3cacb6692..8e48a0b54 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -6,7 +6,7 @@ import { TraceMobx } from '../../../fields/util';
 import { DocUtils } from '../../documents/DocUtils';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
-import { undoBatch } from '../../util/UndoManager';
+import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { StyleProp } from '../StyleProp';
 import { DocumentView } from './DocumentView';
@@ -21,6 +21,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return FieldView.LayoutString(EquationBox, fieldKey);
     }
     _ref: React.RefObject<EquationEditor> = React.createRef();
+    _liveTextUndo: UndoManager.Batch | undefined; // captured undo batch when typing a new text note into a collection
 
     constructor(props: FieldViewProps) {
         super(props);
@@ -30,6 +31,8 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
     componentDidMount() {
         this._props.setContentViewBox?.(this);
         if (DocumentView.SelectOnLoad === this.Document && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.()))) {
+            this._liveTextUndo = FormattedTextBox.LiveTextUndo;
+            FormattedTextBox.LiveTextUndo = undefined;
             this._props.select(false);
 
             this._ref.current?.mathField.focus();
@@ -113,9 +116,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 className="equationBox-cont"
                 onKeyDown={e => e.stopPropagation()}
                 onPointerDown={e => !e.ctrlKey && e.stopPropagation()}
-                onBlur={() => {
-                    FormattedTextBox.LiveTextUndo?.end();
-                }}
+                onBlur={() => this._liveTextUndo?.end()}
                 style={{
                     transform: `scale(${scale})`,
                     minWidth: `${100 / scale}%`,
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 6380dec17..30fc44f62 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -112,6 +112,28 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         this._props.setContentViewBox?.(this);
     }
 
+    @computed get oupaintOriginalSize(): { width: number; height: number } {
+        return {
+            width: NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']),
+            height: NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']),
+        };
+    }
+    set outpaintOriginalSize(prop: { width: number; height: number } | undefined) {
+        this.Document[this.fieldKey + '_outpaintOriginalWidth'] = prop?.width;
+        this.Document[this.fieldKey + '_outpaintOriginalHeight'] = prop?.height;
+    }
+
+    @computed get imgNativeSize() {
+        return {
+            nativeWidth: NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth'], 500)),
+            nativeHeight: NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight'], 500)),
+        };
+    }
+    set imgNativeSize(prop: { nativeWidth: number; nativeHeight: number }) {
+        this.dataDoc[this.fieldKey + '_nativeWidth'] = prop.nativeWidth;
+        this.dataDoc[this.fieldKey + '_nativeHeight'] = prop.nativeHeight;
+    }
+
     protected createDropTarget = (ele: HTMLDivElement) => {
         this._mainCont = ele;
         this._dropDisposer?.();
@@ -153,7 +175,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width), height: this.layoutDoc._height }),
             ({ nativeSize, width, height }) => {
                 if (!this.layoutDoc._layout_nativeDimEditable || !height || this.layoutDoc.layout_resetNativeDim) {
-                    this.layoutDoc.layout_resetNativeDim = undefined; // template images need to reset their dimensions when they are rendered with content.  afterwards, remove this flag.
+                    this.layoutDoc.layout_resetNativeDim = undefined; // reset dimensions of templates rendered with content or if image changes.  afterwards, remove this flag.
                     this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth;
                 }
             },
@@ -170,7 +192,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             { fireImmediately: true }
         );
         this._disposers.outpaint = reaction(
-            () => this.Document[this.fieldKey + '_outpaintOriginalWidth'] !== undefined && !SnappingManager.ShiftKey,
+            () => this.outpaintOriginalSize?.width && !SnappingManager.ShiftKey,
             complete => complete && this.openOutpaintPrompt(),
             { fireImmediately: true }
         );
@@ -231,14 +253,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     DrawingFillHandler.drawingToImage(this.Document, 90, newPrompt(descText), drag)?.then(action(() => (this._regenerateLoading = false)));
                     added = false;
                 } else if (de.altKey || !this.dataDoc[this.fieldKey]) {
-                    const layoutDoc = de.complete.docDragData?.draggedDocuments[0];
-                    const targetField = Doc.LayoutDataKey(layoutDoc);
-                    const targetDoc = layoutDoc[DocData];
-                    if (targetDoc[targetField] instanceof ImageField) {
+                    const dropDoc = de.complete.docDragData?.draggedDocuments[0];
+                    const dropDocFieldKey = Doc.LayoutDataKey(dropDoc);
+                    const dropDataDoc = dropDoc[DocData];
+                    if (dropDataDoc[dropDocFieldKey] instanceof ImageField) {
                         added = true;
-                        this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField);
-                        Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(targetDoc), this.fieldKey);
-                        Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(targetDoc), this.fieldKey);
+                        this.dataDoc.layout_resetNativeDim = true;
+                        this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(dropDataDoc[dropDocFieldKey] as ImageField);
+                        Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(dropDataDoc), this.fieldKey);
+                        Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(dropDataDoc), this.fieldKey);
                     }
                 }
                 added === false && e.preventDefault();
@@ -257,18 +280,17 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     @undoBatch
     setNativeSize = action(() => {
-        const oldnativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']);
+        const oldnativeWidth = this.imgNativeSize.nativeWidth;
         const nscale = NumCast(this._props.PanelWidth()) * NumCast(this.layoutDoc._freeform_scale, 1);
         const nw = nscale / oldnativeWidth;
-        this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nw;
-        this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nw;
+        this.imgNativeSize = { nativeWidth: this.imgNativeSize.nativeWidth * nw, nativeHeight: this.imgNativeSize.nativeHeight * nw };
         this.dataDoc.freeform_panX = nw * NumCast(this.dataDoc.freeform_panX);
         this.dataDoc.freeform_panY = nw * NumCast(this.dataDoc.freeform_panY);
         this.dataDoc.freeform_panX_max = this.dataDoc.freeform_panX_max ? nw * NumCast(this.dataDoc.freeform_panX_max) : undefined;
         this.dataDoc.freeform_panX_min = this.dataDoc.freeform_panX_min ? nw * NumCast(this.dataDoc.freeform_panX_min) : undefined;
         this.dataDoc.freeform_panY_max = this.dataDoc.freeform_panY_max ? nw * NumCast(this.dataDoc.freeform_panY_max) : undefined;
         this.dataDoc.freeform_panY_min = this.dataDoc.freeform_panY_min ? nw * NumCast(this.dataDoc.freeform_panY_min) : undefined;
-        const newnativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']);
+        const newnativeWidth = this.imgNativeSize.nativeWidth;
         DocListCast(this.dataDoc[this.annotationKey]).forEach(doc => {
             doc.x = (NumCast(doc.x) / oldnativeWidth) * newnativeWidth;
             doc.y = (NumCast(doc.y) / oldnativeWidth) * newnativeWidth;
@@ -280,13 +302,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     });
     @undoBatch
     rotate = action(() => {
-        const nw = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']);
-        const nh = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']);
+        const nativeSize = this.imgNativeSize;
         const w = this.layoutDoc._width;
         const h = this.layoutDoc._height;
         this.dataDoc[this.fieldKey + '_rotation'] = (NumCast(this.dataDoc[this.fieldKey + '_rotation']) + 90) % 360;
-        this.dataDoc[this.fieldKey + '_nativeWidth'] = nh;
-        this.dataDoc[this.fieldKey + '_nativeHeight'] = nw;
+        this.imgNativeSize = { nativeWidth: nativeSize.nativeHeight, nativeHeight: nativeSize.nativeWidth }; // swap width and height
         this.layoutDoc._width = h;
         this.layoutDoc._height = w;
     });
@@ -302,7 +322,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         const anchy = NumCast(cropping.y);
         const anchw = NumCast(cropping._width);
         const anchh = NumCast(cropping._height);
-        const viewScale = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) / anchw;
+        const viewScale = this.nativeSize.nativeWidth / anchw;
         cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width);
         cropping.y = NumCast(this.Document.y);
         cropping.onClick = undefined;
@@ -364,12 +384,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     @action
     cancelOutpaintPrompt = () => {
-        const origWidth = NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']);
-        const origHeight = NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']);
-        this.Document._width = origWidth;
-        this.Document._height = origHeight;
+        [this.Document._width, this.Document._height] = [this.oupaintOriginalSize.width, this.oupaintOriginalSize.height];
         this._outpaintingInProgress = false;
-        this.Document[this.fieldKey + '_outpaintOriginalWidth'] = undefined;
+        this.outpaintOriginalSize = undefined;
         this.closeOutpaintPrompt();
     };
 
@@ -417,8 +434,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             loadingOverlay.innerHTML = '<div style="color: white; font-size: 16px;">Generating outpainted image...</div>';
             this._mainCont?.appendChild(loadingOverlay);
 
-            const origWidth = NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']);
-            const origHeight = NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']);
+            const { width: origWidth, height: origHeight } = this.oupaintOriginalSize;
             const response = await Networking.PostToServer('/outpaintImage', {
                 imageUrl: currentPath,
                 prompt: customPrompt,
@@ -455,8 +471,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     this.Document.$ai = true;
                     this.Document.$ai_outpainted = true;
                     this.Document.$ai_outpaint_prompt = customPrompt;
-                    this.Document[this.fieldKey + '_outpaintOriginalWidth'] = undefined;
-                    this.Document[this.fieldKey + '_outpaintOriginalHeight'] = undefined;
+                    this.outpaintOriginalSize = undefined;
                 } else {
                     this.cancelOutpaintPrompt();
                     alert('Failed to receive a valid image URL from server.');
@@ -669,8 +684,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @computed get nativeSize() {
         TraceMobx();
         if (this.paths.length && this.paths[0].includes(DefaultPath)) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0 };
-        const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth'], 500));
-        const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight'], 500));
+        const { nativeWidth, nativeHeight } = this.imgNativeSize;
         const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '_nativeOrientation'], 1);
         return { nativeWidth, nativeHeight, nativeOrientation };
     }
@@ -821,7 +835,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                             transform,
                             transformOrigin,
                             width: this._outpaintAlign ? 'max-content' : this._outpaintAlign ? '100%' : undefined,
-                            height: this._outpaintVAlign ? 'max-content' : this.Document[this.fieldKey + '_outpaintOriginalWidth'] !== undefined ? '100%' : undefined,
+                            height: this._outpaintVAlign ? 'max-content' : this.outpaintOriginalSize?.width ? '100%' : undefined,
                         }}
                         onError={action(e => (this._error = e.toString()))}
                         draggable={false}
@@ -944,6 +958,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return { width, height };
     };
     savedAnnotations = () => this._savedAnnotations;
+    rejectDrop = (de: DragManager.DropEvent, subView?: DocumentView | undefined) => (this.dataDoc[this.fieldKey] === undefined ? true : (this._props.rejectDrop?.(de, subView) ?? false));
     render() {
         TraceMobx();
         const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string;
@@ -987,7 +1002,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         ScreenToLocalTransform={this.screenToLocalTransform}
                         select={emptyFunction}
                         focus={this.focus}
-                        rejectDrop={this._props.rejectDrop}
+                        rejectDrop={this.rejectDrop}
                         getScrollHeight={this.getScrollHeight}
                         NativeDimScaling={returnOne}
                         isAnyChildContentActive={returnFalse}
@@ -1049,8 +1064,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 if (result instanceof Error) {
                     alert('Error uploading files - possibly due to unsupported file types');
                 } else {
-                    this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client);
-                    !(result instanceof Error) && DocUtils.assignUploadInfo(result, this.dataDoc);
+                    runInAction(() => {
+                        this.dataDoc.layout_resetNativeDim = true;
+                        !(result instanceof Error) && DocUtils.assignUploadInfo(result, this.dataDoc, this.fieldKey);
+                        this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client);
+                    });
                 }
                 disposer();
             } else {
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 4cbe01b82..e1ecc2018 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -9,7 +9,7 @@ import { TraceMobx } from '../../../fields/util';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
-import { undoable } from '../../util/UndoManager';
+import { undoable, UndoManager } from '../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { StyleProp } from '../StyleProp';
@@ -28,6 +28,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
     private _timeout: NodeJS.Timeout | undefined;
     private _divRef: HTMLDivElement | null = null;
     private _disposers: { [key: string]: IReactionDisposer } = {};
+    private _liveTextUndo: UndoManager.Batch | undefined; // captured undo batch when typing a new text note into a collection
 
     constructor(props: FieldViewProps) {
         super(props);
@@ -220,8 +221,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             this.setText(this._divRef?.innerText ?? '');
                             if (!FormattedTextBox.tryKeepingFocus(e.relatedTarget, () => this._divRef?.focus())) {
                                 RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
-                                FormattedTextBox.LiveTextUndo?.end();
-                                FormattedTextBox.LiveTextUndo = undefined;
+                                this._liveTextUndo?.end();
                             }
                         }}
                         dangerouslySetInnerHTML={{
@@ -236,6 +236,8 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
                                 if (DocumentView.SelectOnLoad === this.Document) {
                                     DocumentView.SetSelectOnLoad(undefined);
+                                    this._liveTextUndo = FormattedTextBox.LiveTextUndo;
+                                    FormattedTextBox.LiveTextUndo = undefined;
                                     this._divRef.focus();
                                 }
                                 this.fitTextToBox(this._divRef);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 1827cd2dc..c8df6e50f 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -98,7 +98,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
     public static PasteOnLoad: ClipboardEvent | undefined;
     public static SelectOnLoadChar = '';
-    public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch when typing a new text note into a collection
+    public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch request when typing a new text note into a collection
+    private _liveTextUndo: UndoManager.Batch | undefined; // captured undo batch when typing a new text note into a collection
 
     private static _nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor };
     private _curHighlights = new ObservableSet<string>(['Audio Tags']);
@@ -1536,6 +1537,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data
         }
         if (selectOnLoad) {
+            this._liveTextUndo = FormattedTextBox.LiveTextUndo;
+            FormattedTextBox.LiveTextUndo = undefined;
             this.EditorView!.focus();
         }
         if (this._props.isContentActive()) this.prepareForTyping();
@@ -1565,8 +1568,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         removeStyleSheet(this._userStyleSheetElement);
         Object.values(this._disposers).forEach(disposer => disposer?.());
         this.endUndoTypingBatch();
-        FormattedTextBox.LiveTextUndo?.end();
-        FormattedTextBox.LiveTextUndo = undefined;
+        this._liveTextUndo?.end();
         this.unhighlightSearchTerms();
         this.EditorView?.destroy();
         RichTextMenu.Instance?.TextView === this && RichTextMenu.Instance.updateMenu(undefined, undefined, undefined, undefined);
@@ -1795,8 +1797,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
         this.endUndoTypingBatch();
 
-        FormattedTextBox.LiveTextUndo?.end();
-        FormattedTextBox.LiveTextUndo = undefined;
+        this._liveTextUndo?.end();
 
         // if the text box blurs and none of its contents are focused(), then pass the blur along
         setTimeout(() => !this.ProseRef?.contains(document.activeElement) && this._props.onBlur?.());
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 366dc6a20..25e70d950 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1202,12 +1202,12 @@ export namespace Doc {
     }
     export function NativeWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) {
         // if this is a field template, then don't use the doc's nativeWidth/height
-        return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeWidth'], useWidth ? NumCast(doc._width) : 0));
+        return !doc ? 0 : NumCast(doc._nativeWidth, doc[DocLayout].isTemplateDoc ? 0 : NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeWidth'], useWidth ? NumCast(doc._width) : 0));
     }
     export function NativeHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) {
         if (!doc) return 0;
         const nheight = (Doc.NativeWidth(doc, dataDoc, useHeight) / NumCast(doc._width)) * NumCast(doc._height); // divide before multiply to avoid floating point errrorin case nativewidth = width
-        const dheight = NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeHeight'], useHeight ? NumCast(doc._height) : 0);
+        const dheight = doc[DocLayout].isTemplateDoc ? 0 : NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeHeight'], useHeight ? NumCast(doc._height) : 0);
         // if this is a field template, then don't use the doc's nativeWidth/height
         return NumCast(doc._nativeHeight, nheight || dheight);
     }
-- 
cgit v1.2.3-70-g09d2

