From 534532aebbab49b00dc76ad6a0cf6c20368d818c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 10 May 2025 00:01:39 -0400
Subject: added an imageTemplate ui button.

---
 src/client/views/nodes/DocumentView.tsx | 38 ++++++++++++++++++++-------------
 1 file changed, 23 insertions(+), 15 deletions(-)

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

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index f9c21451e..05706fe6b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1389,28 +1389,36 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         custom && DocUtils.makeCustomViewClicked(this.Document, Docs.Create.StackingDocument, layout, undefined);
     }, 'set custom view');
 
-    public static setDefaultTemplate(checkResult?: boolean) {
-        if (checkResult) {
-            return Doc.UserDoc().defaultTextLayout;
+    private static getTemplate(view: DocumentView | undefined) {
+        if (view) {
+            if (!view.layoutDoc.isTemplateDoc) {
+                MakeTemplate(view.Document);
+                Doc.AddDocToList(Doc.UserDoc(), 'template_user', view.Document);
+                Doc.AddDocToList(DocListCast(Doc.MyTools?.data)[1], 'data', makeUserTemplateButtonOrImage(view.Document));
+                DocCast(Doc.UserDoc().template_user) && view.Document && Doc.AddDocToList(DocCast(Doc.UserDoc().template_user)!, 'data', view.Document);
+                return view.Document;
+            }
+            return DocCast(Doc.LayoutField(view.Document)) ?? view.Document;
         }
+    }
+    public static setDefaultTemplate(checkResult?: boolean) {
+        if (checkResult) return Doc.UserDoc().defaultTextLayout;
         const view = DocumentView.Selected()[0]?._props.renderDepth > 0 ? DocumentView.Selected()[0] : undefined;
         undoable(() => {
-            let tempDoc: Opt<Doc>;
-            if (view) {
-                if (!view.layoutDoc.isTemplateDoc) {
-                    tempDoc = view.Document;
-                    MakeTemplate(tempDoc);
-                    Doc.AddDocToList(Doc.UserDoc(), 'template_user', tempDoc);
-                    Doc.AddDocToList(DocListCast(Doc.MyTools?.data)[1], 'data', makeUserTemplateButtonOrImage(tempDoc));
-                    DocCast(Doc.UserDoc().template_user) && tempDoc && Doc.AddDocToList(DocCast(Doc.UserDoc().template_user)!, 'data', tempDoc);
-                } else {
-                    tempDoc = DocCast(Doc.LayoutField(view.Document));
-                }
-            }
+            const tempDoc = DocumentView.getTemplate(view);
             Doc.UserDoc().defaultTextLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined;
         }, 'set default template')();
         return undefined;
     }
+    public static setDefaultImageTemplate(checkResult?: boolean) {
+        if (checkResult) return Doc.UserDoc().defaultImageLayout;
+        const view = DocumentView.Selected()[0]?._props.renderDepth > 0 ? DocumentView.Selected()[0] : undefined;
+        undoable(() => {
+            const tempDoc = DocumentView.getTemplate(view);
+            Doc.UserDoc().defaultImageLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined;
+        }, 'set default image template')();
+        return undefined;
+    }
 
     /**
      * This switches between the current view of a Doc and a specified alternate layout view.
-- 
cgit v1.2.3-70-g09d2


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

---
 src/client/documents/DocUtils.ts                   |  2 +-
 src/client/util/DocumentManager.ts                 |  2 +-
 src/client/views/MarqueeAnnotator.tsx              |  3 +-
 src/client/views/SidebarAnnos.tsx                  | 29 +++++++++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  7 +--
 src/client/views/linking/LinkMenuItem.tsx          | 18 +++----
 src/client/views/nodes/DocumentView.tsx            |  2 +-
 src/client/views/nodes/MapBox/MapBox.tsx           | 59 +++++++---------------
 .../views/nodes/MapboxMapBox/MapboxContainer.tsx   | 51 ++++++++++---------
 src/client/views/nodes/PDFBox.tsx                  | 17 +++----
 src/client/views/nodes/VideoBox.tsx                | 17 ++++---
 src/client/views/nodes/WebBox.tsx                  | 26 +++++-----
 .../views/nodes/formattedText/FormattedTextBox.tsx | 17 +++----
 13 files changed, 124 insertions(+), 126 deletions(-)

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

diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index 36e03daed..41a61b154 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -724,7 +724,7 @@ export namespace DocUtils {
                                 _width: width || BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 * 6 : 200,
                                 _height: BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 : 35,
                                 _layout_autoHeight: true,
-                                backgroundColor: StrCast(Doc.UserDoc().textBackgroundColor),
+                                backgroundColor: backgroundColor ?? StrCast(Doc.UserDoc().textBackgroundColor),
                                 borderColor: Doc.UserDoc().borderColor as string,
                                 borderWidth: Doc.UserDoc().borderWidth as number,
                                 text_centered: BoolCast(Doc.UserDoc().textCentered),
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index fc8f22cf3..422613968 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -250,7 +250,7 @@ export class DocumentManager {
         const options = optionsIn;
         Doc.MyRecentlyClosed && Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, targetDoc);
         const docContextPath = DocumentManager.GetContextPath(targetDoc, true);
-        if (docContextPath.some(doc => doc.hidden)) options.toggleTarget = false;
+        if (docContextPath.some(doc => doc !== targetDoc && doc.hidden)) options.toggleTarget = false;
         let activatedTab = false;
         if (DocumentView.activateTabView(docContextPath[0])) {
             options.toggleTarget = false;
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index b2e42652d..d354dd2c0 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -194,14 +194,13 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
         AnchorMenu.Instance.StartDrag = action((e: PointerEvent, ele: HTMLElement) => {
             e.preventDefault();
             e.stopPropagation();
-            const sourceAnchorCreator = () => this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true); // hyperlink color
-
             const targetCreator = (annotationOn: Doc | undefined) => {
                 const target = DocUtils.GetNewTextDoc('Note linked to ' + this.props.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
                 target.layout_fitWidth = true;
                 DocumentView.SetSelectOnLoad(target);
                 return target;
             };
+            const sourceAnchorCreator = () => this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true); // hyperlink color
             DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView(), sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
                 dragComplete: dragEv => {
                     if (!dragEv.aborted && dragEv.annoDragData && dragEv.annoDragData.linkSourceDoc && dragEv.annoDragData.dropDocument && dragEv.linkDocument) {
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 573c28ccf..71b479a22 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -3,7 +3,7 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { ClientUtils, returnFalse, returnOne, returnZero } from '../../ClientUtils';
 import { emptyFunction } from '../../Utils';
-import { Doc, DocListCast, Field, FieldResult, FieldType, StrListCast } from '../../fields/Doc';
+import { Doc, DocListCast, Field, FieldResult, FieldType, Opt, StrListCast } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { List } from '../../fields/List';
 import { RichTextField } from '../../fields/RichTextField';
@@ -19,6 +19,7 @@ import { StyleProp } from './StyleProp';
 import { CollectionStackingView } from './collections/CollectionStackingView';
 import { DocumentView } from './nodes/DocumentView';
 import { FieldViewProps } from './nodes/FieldView';
+import { FocusViewOptions } from './nodes/FocusViewOptions';
 
 interface ExtraProps {
     fieldKey: string;
@@ -149,15 +150,33 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr
     };
     makeDocUnfiltered = (doc: Doc) => {
         if (DocListCast(this._props.Doc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
-            if (this.childFilters()) {
+            if (this.childFilters().length) {
                 // if any child filters exist, get rid of them
                 this._props.layoutDoc._childFilters = new List<string>();
+                return true;
             }
-            return true;
         }
         return false;
     };
 
+    public static getView(sidebar: SidebarAnnos | null, sidebarShown: boolean, toggleSidebar: () => void, doc: Doc, options: FocusViewOptions) {
+        if (!sidebarShown) {
+            options.didMove = true;
+            toggleSidebar();
+        }
+        options.didMove = sidebar?.makeDocUnfiltered(doc) || options.didMove;
+
+        if (!doc.hidden) {
+            if (!options.didMove && options.toggleTarget) {
+                options.toggleTarget = false;
+                options.didMove = doc.hidden = true;
+            }
+        } else {
+            options.didMove = !(doc.hidden = false);
+        }
+        return new Promise<Opt<DocumentView>>(res => DocumentView.addViewRenderedCb(doc, res));
+    }
+
     get sidebarKey() {
         return this._props.fieldKey + '_sidebar';
     }
@@ -181,8 +200,8 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr
     layout_showTitle = () => 'title';
     setHeightCallback = (height: number) => this._props.setHeight?.(height + this.filtersHeight());
     sortByLinkAnchorY = (a: Doc, b: Doc) => {
-        const ay = Doc.Links(a).length && DocCast(Doc.Links(a)[0].link_anchor_1).y;
-        const by = Doc.Links(b).length && DocCast(Doc.Links(b)[0].link_anchor_1).y;
+        const ay = Doc.Links(a).length && DocCast(Doc.Links(a)[0].link_anchor_1)?.y;
+        const by = Doc.Links(b).length && DocCast(Doc.Links(b)[0].link_anchor_1)?.y;
         return NumCast(ay) - NumCast(by);
     };
     render() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index bb3c59eae..77615c650 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1738,10 +1738,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         );
 
         if (addAsAnnotation) {
-            if (Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), null) !== undefined) {
-                Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), [])?.push(anchor);
+            const fieldKey = this._props.isAnnotationOverlay ? this._props.fieldKey : this._props.fieldKey + '_annotations';
+            if (Cast(this.dataDoc[fieldKey], listSpec(Doc), null) !== undefined) {
+                Cast(this.dataDoc[fieldKey], listSpec(Doc), [])?.push(anchor);
             } else {
-                this.dataDoc[this._props.fieldKey + '_annotations'] = new List<Doc>([anchor]);
+                this.dataDoc[fieldKey] = new List<Doc>([anchor]);
             }
         }
         return anchor;
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index c984a7053..6c1ad568d 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -105,11 +105,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
                     LinkManager.Instance.currentLinkAnchor = LinkManager.Instance.currentLink ? this.sourceAnchor : undefined;
 
                     if ((SnappingManager.PropertiesWidth ?? 0) < 100) {
-                        setTimeout(
-                            action(() => {
-                                SnappingManager.SetPropertiesWidth(250);
-                            })
-                        );
+                        setTimeout(action(() => SnappingManager.SetPropertiesWidth(250)));
                     }
                 }
             })
@@ -136,14 +132,14 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
                     this._props.itemHandler?.(this._props.linkDoc);
                 } else {
                     const focusDoc =
-                        Cast(this._props.linkDoc.link_anchor_1, Doc, null)?.annotationOn === this._props.sourceDoc
-                            ? Cast(this._props.linkDoc.link_anchor_1, Doc, null)
-                            : Cast(this._props.linkDoc.link_anchor_2, Doc, null)?.annotationOn === this._props.sourceDoc
-                              ? Cast(this._props.linkDoc.link_anchor_12, Doc, null)
-                              : undefined;
+                        DocCast(this._props.linkDoc.link_anchor_1)?.annotationOn === this._props.sourceDoc
+                            ? DocCast(this._props.linkDoc.link_anchor_1)
+                      : DocCast(this._props.linkDoc.link_anchor_2)?.annotationOn === this._props.sourceDoc
+                            ? DocCast(this._props.linkDoc.link_anchor_2)
+                      : undefined; // prettier-ignore
 
                     if (focusDoc) this._props.docView._props.focus(focusDoc, { instant: true });
-                    DocumentView.FollowLink(this._props.linkDoc, this._props.sourceDoc, false);
+                    DocumentView.FollowLink(this._props.linkDoc, focusDoc ?? this._props.sourceDoc, false);
                 }
             }
         );
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 05706fe6b..b0415ef18 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1412,7 +1412,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     }
     public static setDefaultImageTemplate(checkResult?: boolean) {
         if (checkResult) return Doc.UserDoc().defaultImageLayout;
-        const view = DocumentView.Selected()[0]?._props.renderDepth > 0 ? DocumentView.Selected()[0] : undefined;
+        const view = DocumentView.Selected()[0]?._props.renderDepth > 0 || DocumentView.Selected()[0]?.Document.isTemplateDoc ? DocumentView.Selected()[0] : undefined;
         undoable(() => {
             const tempDoc = DocumentView.getTemplate(view);
             Doc.UserDoc().defaultImageLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined;
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index a563b7c1b..a279ccc48 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -13,7 +13,7 @@ import { CirclePicker, ColorResult } from 'react-color';
 import { Layer, MapProvider, MapRef, Map as MapboxMap, Marker, Source, ViewState, ViewStateChangeEvent } from 'react-map-gl/mapbox';
 import { ClientUtils, setupMoveUpEvents } from '../../../../ClientUtils';
 import { emptyFunction } from '../../../../Utils';
-import { Doc, DocListCast, Field, LinkedTo, Opt, StrListCast } from '../../../../fields/Doc';
+import { Doc, DocListCast, Field, LinkedTo, StrListCast } from '../../../../fields/Doc';
 import { List } from '../../../../fields/List';
 import { RichTextField } from '../../../../fields/RichTextField';
 import { DocCast, NumCast, StrCast, toList } from '../../../../fields/Types';
@@ -111,12 +111,12 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     // this list contains pushpins and configs
     @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } // prettier-ignore
-    @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); } // prettier-ignore
+    @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.sidebarKey]); } // prettier-ignore
     @computed get allPushpins() { return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); } // prettier-ignore
     @computed get allRoutes() { return this.allAnnotations.filter(anno => anno.type === DocumentType.MAPROUTE); } // prettier-ignore
     @computed get SidebarShown() { return !!this.layoutDoc._layout_showSidebar; } // prettier-ignore
     @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } // prettier-ignore
-    @computed get SidebarKey() {  return this.fieldKey + '_sidebar'; } // prettier-ignore
+    @computed get sidebarKey() {  return this.fieldKey + '_sidebar'; } // prettier-ignore
     @computed get sidebarColor() {
         return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4'));
     }
@@ -260,7 +260,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     removeMapDocument = (doc: Doc | Doc[], annotationKey?: string) => {
         this.allAnnotations
-            .filter(anno => toList(doc).includes(DocCast(anno.mapPin)))
+            .filter(anno => toList(doc).includes(DocCast(anno.mapPin)!))
             .forEach(anno => {
                 anno.mapPin = undefined;
             });
@@ -339,27 +339,19 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     startAnchorDrag = (e: PointerEvent, ele: HTMLElement) => {
         e.preventDefault();
         e.stopPropagation();
-
-        const sourceAnchorCreator = action(() => {
-            const note = this.getAnchor(true);
-            if (note && this._selectedPinOrRoute) {
-                note.latitude = this._selectedPinOrRoute.latitude;
-                note.longitude = this._selectedPinOrRoute.longitude;
-                note.map = this._selectedPinOrRoute.map;
-            }
-            return note as Doc;
-        });
-
         const targetCreator = (annotationOn: Doc | undefined) => {
             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, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
                 dragComplete: dragEv => {
-                    if (!dragEv.aborted && dragEv.annoDragData && dragEv.annoDragData.linkSourceDoc && dragEv.annoDragData.dropDocument && dragEv.linkDocument) {
+                    if (!dragEv.aborted && dragEv.annoDragData?.linkSourceDoc && dragEv.annoDragData.dropDocument && dragEv.linkDocument) {
                         dragEv.annoDragData.linkSourceDoc.followLinkToggle = dragEv.annoDragData.dropDocument.annotationOn === this.Document;
                         dragEv.annoDragData.linkSourceDoc.followLinkZoom = false;
                     }
@@ -368,17 +360,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     };
 
     createNoteAnnotation = () => {
-        const createFunc = undoable(
-            action(() => {
-                const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(true), ['latitude', 'longitude', LinkedTo]);
-                if (note && this._selectedPinOrRoute) {
-                    note.latitude = this._selectedPinOrRoute.latitude;
-                    note.longitude = this._selectedPinOrRoute.longitude;
-                    note.map = this._selectedPinOrRoute.map;
-                }
-            }),
-            'create note annotation'
-        );
+        const createFunc = undoable(() => this._sidebarRef.current?.anchorMenuClick(this.getAnchor(true), ['latitude', 'longitude', LinkedTo]), 'create note annotation');
         if (!this.layoutDoc.layout_showSidebar) {
             this.toggleSidebar();
             setTimeout(createFunc);
@@ -428,14 +410,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
     };
 
-    getView = (doc: Doc, options: FocusViewOptions) => {
-        if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) {
-            this.toggleSidebar();
-            options.didMove = true;
+    getView = async (doc: Doc, options: FocusViewOptions) => {
+        if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
+            SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, this.toggleSidebar, doc, options);
         }
-        return new Promise<Opt<DocumentView>>(res => {
-            DocumentView.addViewRenderedCb(doc, dv => res(dv));
-        });
+        return undefined;
     };
 
     /*
@@ -476,13 +455,14 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     @action
     deleteSelectedPinOrRoute = undoable(() => {
-        if (this._selectedPinOrRoute) {
+        const selPin = DocCast(this._selectedPinOrRoute);
+        if (selPin) {
             // Removes filter
-            Doc.setDocFilter(this.Document, 'latitude', NumCast(this._selectedPinOrRoute.latitude), 'remove');
-            Doc.setDocFilter(this.Document, 'longitude', NumCast(this._selectedPinOrRoute.longitude), 'remove');
-            Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this._selectedPinOrRoute))}`, 'remove');
+            Doc.setDocFilter(this.Document, 'latitude', NumCast(selPin.latitude), 'remove');
+            Doc.setDocFilter(this.Document, 'longitude', NumCast(selPin.longitude), 'remove');
+            Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(selPin)}`, 'remove');
 
-            this.removePushpinOrRoute(this._selectedPinOrRoute);
+            this.removePushpinOrRoute(selPin);
         }
         MapAnchorMenu.Instance.fadeOut(true);
         document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
@@ -1299,7 +1279,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     <SidebarAnnos
                         ref={this._sidebarRef}
                         {...this._props}
-                        fieldKey={this.fieldKey}
                         Doc={this.Document}
                         layoutDoc={this.layoutDoc}
                         dataDoc={this.dataDoc}
diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
index e0efab576..0beefcb67 100644
--- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
+++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
@@ -145,7 +145,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
     removeMapDocument = (docsIn: Doc | Doc[], annotationKey?: string) => {
         const docs = toList(docsIn);
         this.allAnnotations
-            .filter(anno => docs.includes(DocCast(anno.mapPin)))
+            .filter(anno => docs.includes(DocCast(anno.mapPin)!))
             .forEach(anno => {
                 anno.mapPin = undefined;
             });
@@ -224,6 +224,12 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
     startAnchorDrag = (e: PointerEvent, ele: HTMLElement) => {
         e.preventDefault();
         e.stopPropagation();
+        const targetCreator = (annotationOn: Doc | undefined) => {
+            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 = action(() => {
             const note = this.getAnchor(true);
@@ -235,11 +241,6 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
             return note as Doc;
         });
 
-        const targetCreator = (annotationOn: Doc | undefined) => {
-            const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
-            DocumentView.SetSelectOnLoad(target);
-            return target;
-        };
         const docView = this.DocumentView?.();
         docView &&
             DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
@@ -362,22 +363,22 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
 
     @action
     deselectPin = () => {
-        if (this.selectedPin) {
+        const selPin = DocCast(this.selectedPin);
+        if (selPin) {
             // Removes filter
-            Doc.setDocFilter(this.Document, 'latitude', NumCast(this.selectedPin.latitude), 'remove');
-            Doc.setDocFilter(this.Document, 'longitude', NumCast(this.selectedPin.longitude), 'remove');
-            Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove');
+            Doc.setDocFilter(this.Document, 'latitude', NumCast(selPin.latitude), 'remove');
+            Doc.setDocFilter(this.Document, 'longitude', NumCast(selPin.longitude), 'remove');
+            Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(selPin)}`, 'remove');
 
-            const temp = this.selectedPin;
             if (!this._unmounting) {
-                this._bingMap.current.entities.remove(this.map_docToPinMap.get(temp));
+                this._bingMap.current.entities.remove(this.map_docToPinMap.get(selPin));
             }
-            const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(temp.latitude, temp.longitude));
-            this.MicrosoftMaps.Events.addHandler(newpin, 'click', () => this.pushpinClicked(temp as Doc));
+            const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(selPin.latitude, selPin.longitude));
+            this.MicrosoftMaps.Events.addHandler(newpin, 'click', () => this.pushpinClicked(selPin));
             if (!this._unmounting) {
                 this._bingMap.current.entities.push(newpin);
             }
-            this.map_docToPinMap.set(temp, newpin);
+            this.map_docToPinMap.set(selPin, newpin);
             this.selectedPin = undefined;
             this.bingSearchBarContents = this.Document.map;
         }
@@ -388,9 +389,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
             this.toggleSidebar();
             options.didMove = true;
         }
-        return new Promise<Opt<DocumentView>>(res => {
-            DocumentView.addViewRenderedCb(doc, dv => res(dv));
-        });
+        return new Promise<Opt<DocumentView>>(res => DocumentView.addViewRenderedCb(doc, res));
     };
     /*
      * Pushpin onclick
@@ -535,13 +534,14 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
 
     @action
     deleteSelectedPin = undoable(() => {
-        if (this.selectedPin) {
+        const selPin = this.selectedPin;
+        if (selPin) {
             // Removes filter
-            Doc.setDocFilter(this.Document, 'latitude', NumCast(this.selectedPin.latitude), 'remove');
-            Doc.setDocFilter(this.Document, 'longitude', NumCast(this.selectedPin.longitude), 'remove');
-            Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove');
+            Doc.setDocFilter(this.Document, 'latitude', NumCast(selPin.latitude), 'remove');
+            Doc.setDocFilter(this.Document, 'longitude', NumCast(selPin.longitude), 'remove');
+            Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(selPin)}`, 'remove');
 
-            this.removePushpin(this.selectedPin);
+            this.removePushpin(selPin);
         }
         MapAnchorMenu.Instance.fadeOut(true);
         document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
@@ -638,7 +638,10 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
         this._disposers.highlight = reaction(
             () => this.allAnnotations.map(doc => doc[Highlight]),
             () => {
-                const allConfigPins = this.allAnnotations.map(doc => ({ doc, pushpin: DocCast(doc.mapPin) })).filter(pair => pair.pushpin);
+                const allConfigPins = this.allAnnotations
+                    .map(doc => ({ doc, pushpin: DocCast(doc.mapPin) }))
+                    .filter(pair => pair.pushpin)
+                    .map(pair => ({ doc: pair.doc, pushpin: pair.pushpin! }));
                 allConfigPins.forEach(({ pushpin }) => {
                     if (!pushpin[Highlight] && this.map_pinHighlighted.get(pushpin)) {
                         this.recolorPin(pushpin);
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 55e6d5596..e4050a3c3 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -10,7 +10,7 @@ import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { ComputedField } from '../../../fields/ScriptField';
-import { Cast, FieldValue, NumCast, StrCast, toList } from '../../../fields/Types';
+import { Cast, DocCast, FieldValue, NumCast, StrCast, toList } from '../../../fields/Types';
 import { ImageField, PdfField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction } from '../../../Utils';
@@ -27,7 +27,6 @@ import { Colors } from '../global/globalEnums';
 import { PDFViewer } from '../pdf/PDFViewer';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { SidebarAnnos } from '../SidebarAnnos';
-import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
 import { ImageBox } from './ImageBox';
@@ -56,6 +55,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy> = undefined;
     @observable private _pageControls = false;
 
+    @computed get sidebarKey() {
+        return this.fieldKey + '_sidebar';
+    }
     @computed get pdfUrl() {
         return Cast(this.dataDoc[this._props.fieldKey], PdfField);
     }
@@ -232,14 +234,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return this._pdfViewer?.scrollFocus(anchor, NumCast(anchor.y, NumCast(anchor.config_scrollTop)), options);
     };
 
-    getView = (doc: Doc, options: FocusViewOptions) => {
-        if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) {
-            options.didMove = true;
-            this.toggleSidebar(false);
+    getView = async (doc: Doc, options: FocusViewOptions) => {
+        if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
+            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 undefined;
     };
 
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index fa099178c..b3cb0e1db 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -338,12 +338,17 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         const timecode = Cast(this.layoutDoc._layout_currentTimecode, 'number', null);
         const marquee = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation);
+        const docAnchor = () =>
+            Docs.Create.ConfigDocument({
+                title: '#' + timecode,
+                _timecodeToShow: timecode,
+                annotationOn: this.Document,
+            });
         if (!addAsAnnotation && marquee) marquee.backgroundColor = 'transparent';
-        const anchor =
-            addAsAnnotation && marquee
-                ? CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode || undefined, undefined, marquee, addAsAnnotation) || this.Document
-                : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.Document });
+        const visibleAnchor = () => addAsAnnotation && marquee && (CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode || undefined, undefined, marquee, addAsAnnotation) || this.Document);
+        const anchor = visibleAnchor() || docAnchor();
         PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true, pannable: true } }, this.Document);
+        addAsAnnotation && this.addDocument(anchor);
         return anchor;
     };
 
@@ -376,9 +381,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             }
             return this._stackedTimeline.getView(doc, options);
         }
-        return new Promise<Opt<DocumentView>>(res => {
-            DocumentView.addViewRenderedCb(doc, dv => res(dv));
-        });
+        return new Promise<Opt<DocumentView>>(res => DocumentView.addViewRenderedCb(doc, res));
     };
 
     // extracts video thumbnails and saves them as field of doc
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 838dbea9d..779ccab40 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -13,7 +13,7 @@ import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
 import { RefField } from '../../../fields/RefField';
 import { listSpec } from '../../../fields/Schema';
-import { Cast, NumCast, StrCast, toList, WebCast } from '../../../fields/Types';
+import { Cast, DocCast, NumCast, StrCast, toList, WebCast } from '../../../fields/Types';
 import { ImageField, WebField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, stringHash } from '../../../Utils';
@@ -104,6 +104,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @computed get webField() {
         return Cast(this.Document[this._props.fieldKey], WebField)?.url;
     }
+    @computed get sidebarKey() {
+        return this.fieldKey + '_sidebar';
+    }
 
     constructor(props: FieldViewProps) {
         super(props);
@@ -308,18 +311,17 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     };
 
     @action
-    getView = (doc: Doc /* , options: FocusViewOptions */) => {
-        if (Doc.AreProtosEqual(doc, this.Document))
-            return new Promise<Opt<DocumentView>>(res => {
-                res(this.DocumentView?.());
-            });
+    getView = async (doc: Doc, options: FocusViewOptions) => {
+        if (Doc.AreProtosEqual(doc, this.Document)) return new Promise<Opt<DocumentView>>(res => res(this.DocumentView?.()));
+
         if (this.Document.layout_fieldKey === 'layout_icon') this.DocumentView?.().iconify();
         const webUrl = WebCast(doc.config_data)?.url;
         if (this._url && webUrl && webUrl.href !== this._url) this.setData(webUrl.href);
-        if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false);
-        return new Promise<Opt<DocumentView>>(res => {
-            DocumentView.addViewRenderedCb(doc, dv => res(dv));
-        });
+
+        if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
+            return SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, () => this.toggleSidebar(false), doc, options);
+        }
+        return undefined;
     };
 
     sidebarAddDocTab = (doc: Doc, where: OpenWhere) => {
@@ -454,7 +456,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     iframeDown = (e: PointerEvent) => {
         this._textAnnotationCreator = undefined;
         const sel = this._url ? this._iframe?.contentDocument?.getSelection() : window.document.getSelection();
-        if (sel?.empty && !(e.target as any).textContent)
+        if (sel?.empty && !(e.target as HTMLElement).textContent)
             sel.empty(); // Chrome
         else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox
 
@@ -565,7 +567,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 'click',
                 undoable(
                     action((e: MouseEvent) => {
-                        let eleHref = (e.target as any)?.outerHTML?.split('"="')[1]?.split('"')[0];
+                        let eleHref = (e.target as HTMLElement)?.outerHTML?.split('"="')[1]?.split('"')[0];
                         for (let ele = e.target as HTMLElement | Element | null; ele; ele = ele.parentElement) {
                             if ('href' in ele) {
                                 eleHref = (typeof ele.href === 'string' ? ele.href : eleHref) || (ele.parentElement && 'href' in ele.parentElement ? (ele.parentElement.href as string) : eleHref);
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/DocumentView.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

