From 3a3e496dda5f9a1f5286a2c9f62524de59794ade Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 24 Mar 2023 03:37:41 -0400
Subject: overhaul of selection api so that schema and other views behave like
 freeform and use document views onClick for selection

---
 .../collectionFreeForm/CollectionFreeFormLayoutEngines.tsx         | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index fa0695fb2..81b0c4d8a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -116,6 +116,8 @@ export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc
             zIndex: NumCast(layout.zIndex),
             pair: { layout, data },
             replica: '',
+            color: 'white',
+            backgroundColor: 'white',
         });
     });
     const divider = { type: 'div', color: 'transparent', x: -burstRadius[0], y: 0, width: 15, height: 15, payload: undefined };
@@ -408,7 +410,7 @@ function normalizeResults(
         .map(ele => {
             const newPosRaw = ele[1];
             if (newPosRaw) {
-                const newPos = {
+                const newPos: PoolData = {
                     x: newPosRaw.x * scale,
                     y: newPosRaw.y * scale,
                     z: newPosRaw.z,
@@ -417,6 +419,9 @@ function normalizeResults(
                     zIndex: newPosRaw.zIndex,
                     width: (newPosRaw.width || 0) * scale,
                     height: newPosRaw.height! * scale,
+                    backgroundColor: newPosRaw.backgroundColor,
+                    opacity: newPosRaw.opacity,
+                    color: newPosRaw.color,
                     pair: ele[1].pair,
                 };
                 poolData.set(newPos.pair.layout[Id] + (newPos.replica || ''), { transition: 'all 1s', ...newPos });
-- 
cgit v1.2.3-70-g09d2


From 33299a19d86948051eef442825c0b3241d1ac619 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 24 Apr 2023 11:55:53 -0400
Subject: fixed isContentActive=false to apply to stacking collections. fixed
 pile view to be faster and to work in fit content panels. fixed issues with
 hidden docs and freeformviews that act as lightboxes - hidden docs can be
 shown as the lightbox doc without modifying the hidden flag to allow
 collection state to be restored.

---
 src/client/documents/Documents.ts                  |   2 +-
 src/client/util/CurrentUserUtils.ts                |  12 +--
 src/client/util/DocumentManager.ts                 |  15 ++-
 src/client/views/DocComponent.tsx                  | 104 ++++++++++----------
 src/client/views/LightboxView.tsx                  |   6 +-
 src/client/views/MainView.tsx                      |  18 ++--
 src/client/views/PropertiesButtons.tsx             |   7 +-
 src/client/views/StyleProvider.tsx                 |   5 +-
 .../views/collections/CollectionPileView.tsx       |  35 +++----
 .../views/collections/CollectionStackingView.tsx   |  15 ++-
 .../views/collections/CollectionTimeView.tsx       |   2 +-
 src/client/views/collections/CollectionView.tsx    |   1 +
 src/client/views/collections/TreeView.tsx          |   2 -
 .../CollectionFreeFormLayoutEngines.tsx            |  23 ++---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 107 ++++++++++++++-------
 .../collections/collectionFreeForm/MarqueeView.tsx |   8 +-
 src/client/views/nodes/DocumentView.tsx            |  57 ++++++-----
 src/client/views/nodes/ImageBox.scss               |   4 +-
 src/client/views/nodes/ImageBox.tsx                |   1 +
 src/client/views/nodes/WebBox.scss                 |   1 +
 .../views/nodes/formattedText/DashDocView.tsx      |   2 +
 .../nodes/formattedText/FormattedTextBox.scss      |  30 ++++++
 .../views/nodes/formattedText/FormattedTextBox.tsx |  47 ++++++++-
 src/fields/util.ts                                 |   3 +
 24 files changed, 315 insertions(+), 192 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index a0149eadf..dee5feebc 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1066,7 +1066,7 @@ export namespace Docs {
         }
 
         export function PileDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _overflow: 'visible', _forceActive: true, _noAutoscroll: true, ...options, _viewType: CollectionViewType.Pile }, id);
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _overflow: 'visible', enableDragWhenActive: true, _forceActive: true, _noAutoscroll: true, ...options, _viewType: CollectionViewType.Pile }, id);
         }
 
         export function LinearDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index abf7313a4..cdb12624f 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -619,12 +619,12 @@ export class CurrentUserUtils {
     }
     static viewTools(): Button[] {
         return [
-            { title: "Grid",         icon: "border-all",  toolTip: "Show Grid",           btnType: ButtonType.ToggleButton, expertMode: false, toolType:"grid",       funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
-            { title: "Snap\xA0Lines",icon: "th",          toolTip: "Show Snap Lines",     btnType: ButtonType.ToggleButton, expertMode: false, toolType:"snap lines", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
-            { title: "View\xA0All",  icon: "object-group",toolTip: "Fit all Docs to View",btnType: ButtonType.ToggleButton, expertMode: false, toolType:"viewAll",    funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
-            { title: "Clusters",     icon: "braille",     toolTip: "Show Doc Clusters",   btnType: ButtonType.ToggleButton, expertMode: false, toolType:"clusters",   funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
-            { title: "Arrange",      icon: "window",      toolTip: "Toggle Auto Arrange", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"arrange",    funcs: {hidden: 'IsNoviceMode()'}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
-            { title: "Reset",        icon: "check",       toolTip: "Reset View",          btnType: ButtonType.ClickButton,  expertMode: false, backgroundColor:"transparent",    scripts: { onClick: 'resetView()'}}, // Only when floating document is selected in freeform
+            { title: "Grid",          icon: "border-all",   toolTip: "Show Grid",           btnType: ButtonType.ToggleButton, expertMode: false, toolType:"grid",       funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
+            { title: "Snap\xA0Lines", icon: "th",           toolTip: "Show Snap Lines",     btnType: ButtonType.ToggleButton, expertMode: false, toolType:"snap lines", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
+            { title: "View\xA0All",   icon: "object-group", toolTip: "Fit all Docs to View",btnType: ButtonType.ToggleButton, expertMode: false, toolType:"viewAll",    funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
+            { title: "Clusters",      icon: "braille",      toolTip: "Show Doc Clusters",   btnType: ButtonType.ToggleButton, expertMode: false, toolType:"clusters",   funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
+            { title: "Arrange",icon: "arrow-down-short-wide",toolTip: "Toggle Auto Arrange",btnType: ButtonType.ToggleButton, expertMode: false, toolType:"arrange",    funcs: {hidden: 'IsNoviceMode()'}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
+            { title: "Reset",         icon: "check",        toolTip: "Reset View",          btnType: ButtonType.ClickButton,  expertMode: false, backgroundColor:"transparent",    scripts: { onClick: 'resetView()'}}, // Only when floating document is selected in freeform
         ]
     }
     static textTools():Button[] {
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index e01457b4f..4542c1c05 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -8,8 +8,7 @@ import { CollectionViewType } from '../documents/DocumentTypes';
 import { CollectionDockingView } from '../views/collections/CollectionDockingView';
 import { TabDocView } from '../views/collections/TabDocView';
 import { LightboxView } from '../views/LightboxView';
-import { MainView } from '../views/MainView';
-import { DocFocusOptions, DocumentView, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewInternal, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView';
 import { FormattedTextBox } from '../views/nodes/formattedText/FormattedTextBox';
 import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox';
 import { PresBox } from '../views/nodes/trails';
@@ -227,7 +226,7 @@ export class DocumentManager {
         let rootContextView = docViewPath.shift();
         await (rootContextView && this.focusViewsInPath(rootContextView, options, async () => ({ childDocView: docViewPath.shift(), viewSpec: undefined })));
         if (options.toggleTarget && (!options.didMove || targetDocView.rootDoc.hidden)) targetDocView.rootDoc.hidden = !targetDocView.rootDoc.hidden;
-        else if (options.openLocation?.startsWith(OpenWhere.toggle) && !options.didMove && rootContextView) MainView.addDocTabFunc(rootContextView.rootDoc, options.openLocation);
+        else if (options.openLocation?.startsWith(OpenWhere.toggle) && !options.didMove && rootContextView) DocumentViewInternal.addDocTabFunc(rootContextView.rootDoc, options.openLocation);
     };
 
     // shows a document by first:
@@ -247,9 +246,17 @@ export class DocumentManager {
             const viewIndex = docContextPath.findIndex(doc => this.getDocumentView(doc));
             if (viewIndex !== -1) return res(this.getDocumentView(docContextPath[viewIndex])!);
             options.didMove = true;
-            docContextPath.some(doc => TabDocView.Activate(doc)) || MainView.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight);
+            docContextPath.some(doc => TabDocView.Activate(doc)) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight);
             this.AddViewRenderedCb(docContextPath[0], dv => res(dv));
         });
+        if (options.openLocation === OpenWhere.lightbox) {
+            // 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 contextView = this.getDocumentView(DocCast(target.context));
+            if (contextView?.docView?._componentView?.addDocTab?.(target, OpenWhere.lightbox)) {
+                await new Promise<void>(waitres => setTimeout(() => waitres()));
+            }
+        }
         docContextPath.shift();
         const childViewIterator = async (docView: DocumentView) => {
             const innerDoc = docContextPath.shift();
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index a59189fd2..d60ad68c6 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -141,29 +141,25 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
             const effectiveAcl = GetEffectiveAcl(this.dataDoc);
             const indocs = doc instanceof Doc ? [doc] : doc;
             const docs = indocs.filter(doc => [AclEdit, AclAdmin].includes(effectiveAcl) || GetEffectiveAcl(doc) === AclAdmin);
-            if (docs.length) {
-                docs.map(doc => {
-                    Doc.SetInPlace(doc, 'followLinkToggle', undefined, true);
-                    doc.annotationOn === this.props.Document && Doc.SetInPlace(doc, 'annotationOn', undefined, true);
+
+            docs.forEach(doc => doc.annotationOn === this.props.Document && Doc.SetInPlace(doc, 'annotationOn', undefined, true));
+            const targetDataDoc = this.dataDoc;
+            const value = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]);
+            const toRemove = value.filter(v => docs.includes(v));
+
+            if (toRemove.length !== 0) {
+                const recent = Doc.MyRecentlyClosed;
+                toRemove.forEach(doc => {
+                    leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey);
+                    Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc);
+                    doc.context = undefined;
+                    if (recent) {
+                        Doc.RemoveDocFromList(recent, 'data', doc);
+                        doc.type !== DocumentType.LOADING && Doc.AddDocToList(recent, 'data', doc, undefined, true, true);
+                    }
                 });
-                const targetDataDoc = this.dataDoc;
-                const value = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]);
-                const toRemove = value.filter(v => docs.includes(v));
-
-                if (toRemove.length !== 0) {
-                    const recent = Doc.MyRecentlyClosed;
-                    toRemove.forEach(doc => {
-                        leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey);
-                        Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc);
-                        doc.context = undefined;
-                        if (recent) {
-                            Doc.RemoveDocFromList(recent, 'data', doc);
-                            doc.type !== DocumentType.LOADING && Doc.AddDocToList(recent, 'data', doc, undefined, true, true);
-                        }
-                    });
-                    this.isAnyChildContentActive() && this.props.select(false);
-                    return true;
-                }
+                this.isAnyChildContentActive() && this.props.select(false);
+                return true;
             }
 
             return false;
@@ -190,46 +186,44 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
                 return false;
             }
             const targetDataDoc = this.props.Document[DataSym];
-            const docList = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]);
-            const added = docs.filter(d => !docList.includes(d));
             const effectiveAcl = GetEffectiveAcl(targetDataDoc);
 
+            if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) {
+                return false;
+            }
+            const added = docs;
             if (added.length) {
-                if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) {
-                    return false;
-                } else {
-                    if (this.props.Document[AclSym] && Object.keys(this.props.Document[AclSym]).length) {
-                        added.forEach(d => {
-                            for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
-                                if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d);
-                            }
-                        });
-                    }
+                if (this.props.Document[AclSym] && Object.keys(this.props.Document[AclSym]).length) {
+                    added.forEach(d => {
+                        for (const key of Object.keys(this.props.Document[AclSym])) {
+                            if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d);
+                        }
+                    });
+                }
 
-                    if (effectiveAcl === AclAugment) {
-                        added.map(doc => {
-                            if ([AclAdmin, AclEdit].includes(GetEffectiveAcl(doc)) && Doc.ActiveDashboard) inheritParentAcls(Doc.ActiveDashboard, doc);
+                if (effectiveAcl === AclAugment) {
+                    added.map(doc => {
+                        if ([AclAdmin, AclEdit].includes(GetEffectiveAcl(doc)) && Doc.ActiveDashboard) inheritParentAcls(Doc.ActiveDashboard, doc);
+                        doc.context = this.props.Document;
+                        if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.props.Document;
+                        Doc.AddDocToList(targetDataDoc, annotationKey ?? this.annotationKey, doc);
+                    });
+                } else {
+                    added
+                        .filter(doc => [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc)))
+                        .map(doc => {
+                            // only make a pushpin if we have acl's to edit the document
+                            //DocUtils.LeavePushpin(doc);
+                            doc._stayInCollection = undefined;
                             doc.context = this.props.Document;
                             if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.props.Document;
-                            Doc.AddDocToList(targetDataDoc, annotationKey ?? this.annotationKey, doc);
+
+                            Doc.ActiveDashboard && inheritParentAcls(Doc.ActiveDashboard, doc);
                         });
-                    } else {
-                        added
-                            .filter(doc => [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc)))
-                            .map(doc => {
-                                // only make a pushpin if we have acl's to edit the document
-                                //DocUtils.LeavePushpin(doc);
-                                doc._stayInCollection = undefined;
-                                doc.context = this.props.Document;
-                                if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.props.Document;
-
-                                Doc.ActiveDashboard && inheritParentAcls(Doc.ActiveDashboard, doc);
-                            });
-                        const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>;
-                        if (annoDocs instanceof List) annoDocs.push(...added);
-                        else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>(added);
-                        targetDataDoc[(annotationKey ?? this.annotationKey) + '-lastModified'] = new DateField(new Date(Date.now()));
-                    }
+                    const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>;
+                    if (annoDocs instanceof List) annoDocs.push(...added);
+                    else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>(added);
+                    targetDataDoc[(annotationKey ?? this.annotationKey) + '-lastModified'] = new DateField(new Date(Date.now()));
                 }
             }
             return true;
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 69eec8456..c18a89481 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -63,10 +63,8 @@ export class LightboxView extends React.Component<LightboxViewProps> {
             Doc.ActiveTool = InkTool.None;
             MainView.Instance._exploreMode = false;
         } else {
-            if (doc) {
-                const l = DocUtils.MakeLinkToActiveAudio(() => doc).lastElement();
-                l && (Cast(l.anchor2, Doc, null).backgroundColor = 'lightgreen');
-            }
+            const l = DocUtils.MakeLinkToActiveAudio(() => doc).lastElement();
+            l && (Cast(l.anchor2, Doc, null).backgroundColor = 'lightgreen');
             CollectionStackedTimeline.CurrentlyPlaying?.forEach(dv => dv.ComponentView?.Pause?.());
             //TabDocView.PinDoc(doc, { hidePresBox: true });
             this._history ? this._history.push({ doc, target }) : (this._history = [{ doc, target }]);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 4cbf8a811..e4554c339 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -49,7 +49,7 @@ import { LinkMenu } from './linking/LinkMenu';
 import './MainView.scss';
 import { AudioBox } from './nodes/AudioBox';
 import { DocumentLinksButton } from './nodes/DocumentLinksButton';
-import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
+import { DocumentView, DocumentViewInternal, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
 import { DashFieldViewMenu } from './nodes/formattedText/DashFieldView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { RichTextMenu } from './nodes/formattedText/RichTextMenu';
@@ -222,6 +222,7 @@ export class MainView extends React.Component {
 
     constructor(props: Readonly<{}>) {
         super(props);
+        DocumentViewInternal.addDocTabFunc = MainView.addDocTabFunc_impl;
         MainView.Instance = this;
         DashboardView._urlState = HistoryUtil.parseUrl(window.location) || ({} as any);
 
@@ -245,6 +246,7 @@ export class MainView extends React.Component {
             ...[
                 fa.faExclamationCircle,
                 fa.faEdit,
+                fa.faArrowDownShortWide,
                 fa.faTrash,
                 fa.faTrashAlt,
                 fa.faShare,
@@ -584,7 +586,7 @@ export class MainView extends React.Component {
                     Document={this.headerBarDoc}
                     DataDoc={undefined}
                     addDocument={undefined}
-                    addDocTab={MainView.addDocTabFunc}
+                    addDocTab={DocumentViewInternal.addDocTabFunc}
                     pinToPres={emptyFunction}
                     docViewPath={returnEmptyDoclist}
                     styleProvider={DefaultStyleProvider}
@@ -619,7 +621,7 @@ export class MainView extends React.Component {
                     Document={this.mainContainer!}
                     DataDoc={undefined}
                     addDocument={undefined}
-                    addDocTab={MainView.addDocTabFunc}
+                    addDocTab={DocumentViewInternal.addDocTabFunc}
                     pinToPres={emptyFunction}
                     docViewPath={returnEmptyDoclist}
                     styleProvider={this._hideUI ? DefaultStyleProvider : undefined}
@@ -688,7 +690,7 @@ export class MainView extends React.Component {
 
     sidebarScreenToLocal = () => new Transform(0, -this.topOfSidebarDoc, 1);
     mainContainerXf = () => this.sidebarScreenToLocal().translate(-this.leftScreenOffsetOfMainDocView, 0);
-    static addDocTabFunc = (doc: Doc, location: OpenWhere): boolean => {
+    static addDocTabFunc_impl = (doc: Doc, location: OpenWhere): boolean => {
         const whereFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':');
         const keyValue = whereFields[1]?.includes('KeyValue');
         const whereMods: OpenWhereMod = whereFields.length > 1 ? (whereFields[1].replace('KeyValue', '') as OpenWhereMod) : OpenWhereMod.none;
@@ -716,7 +718,7 @@ export class MainView extends React.Component {
                         Document={this._sidebarContent.proto || this._sidebarContent}
                         DataDoc={undefined}
                         addDocument={undefined}
-                        addDocTab={MainView.addDocTabFunc}
+                        addDocTab={DocumentViewInternal.addDocTabFunc}
                         pinToPres={emptyFunction}
                         docViewPath={returnEmptyDoclist}
                         styleProvider={this._sidebarContent.proto === Doc.MyDashboards || this._sidebarContent.proto === Doc.MyFilesystem ? DashboardStyleProvider : DefaultStyleProvider}
@@ -748,7 +750,7 @@ export class MainView extends React.Component {
                     Document={Doc.MyLeftSidebarMenu}
                     DataDoc={undefined}
                     addDocument={undefined}
-                    addDocTab={MainView.addDocTabFunc}
+                    addDocTab={DocumentViewInternal.addDocTabFunc}
                     pinToPres={emptyFunction}
                     rootSelected={returnTrue}
                     removeDocument={returnFalse}
@@ -810,7 +812,7 @@ export class MainView extends React.Component {
                             </div>
                         )}
                         <div className="properties-container" style={{ width: this.propertiesWidth() }}>
-                            {this.propertiesWidth() < 10 ? null : <PropertiesView styleProvider={DefaultStyleProvider} addDocTab={MainView.addDocTabFunc} width={this.propertiesWidth()} height={this.propertiesHeight()} />}
+                            {this.propertiesWidth() < 10 ? null : <PropertiesView styleProvider={DefaultStyleProvider} addDocTab={DocumentViewInternal.addDocTabFunc} width={this.propertiesWidth()} height={this.propertiesHeight()} />}
                         </div>
                     </div>
                 </div>
@@ -889,7 +891,7 @@ export class MainView extends React.Component {
                     docViewPath={returnEmptyDoclist}
                     moveDocument={this.moveButtonDoc}
                     addDocument={this.addButtonDoc}
-                    addDocTab={MainView.addDocTabFunc}
+                    addDocTab={DocumentViewInternal.addDocTabFunc}
                     pinToPres={emptyFunction}
                     removeDocument={this.remButtonDoc}
                     ScreenToLocalTransform={this.buttonBarXf}
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 98dcf4f21..cf808f801 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -136,10 +136,11 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                     // containerDoc.noShadow =
                     // containerDoc.disableDocBrushing =
                     // containerDoc._forceActive =
-                    containerDoc._fitContentsToBox = containerDoc._isLightbox = !containerDoc._isLightbox;
-                    containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined;
+                    //containerDoc._fitContentsToBox =
+                    containerDoc._isLightbox = !containerDoc._isLightbox;
+                    //containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined;
                     const containerContents = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]);
-                    dv.rootDoc.onClick = ScriptField.MakeScript('{self.data = undefined; documentView.select(false)}', { documentView: 'any' });
+                    //dv.rootDoc.onClick = ScriptField.MakeScript('{self.data = undefined; documentView.select(false)}', { documentView: 'any' });
                     containerContents.forEach(doc => LinkManager.Links(doc).forEach(link => (link.linkDisplay = false)));
                 });
             }
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 5f16e0ebd..d98e5aa80 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -30,7 +30,6 @@ export enum StyleProp {
     TreeViewSortings = 'treeViewSortings', // options for how to sort tree view items
     DocContents = 'docContents', // when specified, the JSX returned will replace the normal rendering of the document view
     Opacity = 'opacity', // opacity of the document view
-    Hidden = 'hidden', // whether the document view should not be isplayed
     BoxShadow = 'boxShadow', // box shadow - used for making collections standout and for showing clusters in free form views
     BorderRounding = 'borderRounding', // border radius of the document view
     Color = 'color', // foreground color of Document view items
@@ -172,8 +171,6 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
             const backColor = backgroundCol() || docView?.props.styleProvider?.(docView.props.treeViewDoc, docView.props, StyleProp.BackgroundColor);
             if (!backColor) return undefined;
             return lightOrDark(backColor);
-        case StyleProp.Hidden:
-            return props?.LayoutTemplateString?.includes(KeyValueBox.name) ? false : BoolCast(doc?.hidden);
         case StyleProp.BorderRounding:
             return StrCast(doc?.[fieldKey + 'borderRounding'], StrCast(doc?.borderRounding, doc?._viewType === CollectionViewType.Pile ? '50%' : ''));
         case StyleProp.TitleHeight:
@@ -257,7 +254,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
                     if (StrCast(Doc.LayoutField(doc)).includes(SliderBox.name)) break;
                     docColor =
                         docColor ||
-                        (doc?._viewType === CollectionViewType.Pile || Doc.IsSystem(doc)
+                        (Doc.IsSystem(doc)
                             ? darkScheme()
                                 ? Colors.DARK_GRAY
                                 : Colors.LIGHT_GRAY // system docs (seen in treeView) get a grayish background
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index fd9b0c0ce..e5d89865b 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -1,6 +1,6 @@
 import { action, computed, IReactionDisposer, reaction } from 'mobx';
 import { observer } from 'mobx-react';
-import { Doc, HeightSym, WidthSym } from '../../../fields/Doc';
+import { Doc, DocListCast, HeightSym, WidthSym } from '../../../fields/Doc';
 import { NumCast, StrCast } from '../../../fields/Types';
 import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
@@ -26,12 +26,6 @@ export class CollectionPileView extends CollectionSubView() {
         }
         this._originalChrome = this.layoutDoc._chromeHidden;
         this.layoutDoc._chromeHidden = true;
-
-        // pileups are designed to go away when they are empty.
-        this._disposers.selected = reaction(
-            () => this.childDocs.length,
-            num => !num && this.props.CollectionFreeFormDocumentView?.().props.removeDocument?.(this.props.Document)
-        );
     }
     componentWillUnmount() {
         this.layoutDoc._chromeHidden = this._originalChrome;
@@ -48,13 +42,15 @@ export class CollectionPileView extends CollectionSubView() {
 
     @undoBatch
     removePileDoc = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => {
-        (doc instanceof Doc ? [doc] : doc).map(undoBatch(d => Doc.deiconifyView(d)));
-        return this.props.moveDocument?.(doc, targetCollection, addDoc) || false;
+        (doc instanceof Doc ? [doc] : doc).forEach(d => Doc.deiconifyView(d));
+        const ret = this.props.moveDocument?.(doc, targetCollection, addDoc) || false;
+        if (ret && !DocListCast(this.rootDoc[this.fieldKey ?? 'data']).length) this.props.DocumentView?.().props.removeDocument?.(this.rootDoc);
+        return ret;
     };
 
-    toggleIcon = () => {
+    @computed get toggleIcon() {
         return ScriptField.MakeScript('documentView.iconify()', { documentView: 'any' });
-    };
+    }
 
     // returns the contents of the pileup in a CollectionFreeFormView
     @computed get contents() {
@@ -63,11 +59,11 @@ export class CollectionPileView extends CollectionSubView() {
             <div className="collectionPileView-innards" style={{ pointerEvents: isStarburst || SnappingManager.GetIsDragging() ? undefined : 'none' }}>
                 <CollectionFreeFormView
                     {...this.props}
+                    childContentsActive={returnFalse}
                     layoutEngine={this.layoutEngine}
-                    childDocumentsActive={isStarburst ? returnTrue : undefined}
                     addDocument={this.addPileDoc}
                     childCanEmbedOnDrag={true}
-                    childClickScript={this.toggleIcon()}
+                    childClickScript={this.toggleIcon}
                     moveDocument={this.removePileDoc}
                 />
             </div>
@@ -87,15 +83,12 @@ export class CollectionPileView extends CollectionSubView() {
             this.layoutDoc._panY = -10;
             this.props.Document._pileLayoutEngine = computePassLayout.name;
         } else {
-            const defaultSize = 25;
-            !this.layoutDoc._starburstRadius && (this.layoutDoc._starburstRadius = 250);
+            const defaultSize = 500;
             !this.layoutDoc._starburstDocScale && (this.layoutDoc._starburstDocScale = 2.5);
-            if (this.layoutEngine() === computePassLayout.name) {
-                this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - defaultSize / 2;
-                this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - defaultSize / 2;
-                this.layoutDoc._starburstPileWidth = this.layoutDoc[WidthSym]();
-                this.layoutDoc._starburstPileHeight = this.layoutDoc[HeightSym]();
-            }
+            this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - defaultSize / 2;
+            this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - defaultSize / 2;
+            this.layoutDoc._starburstPileWidth = this.layoutDoc[WidthSym]();
+            this.layoutDoc._starburstPileHeight = this.layoutDoc[HeightSym]();
             this.layoutDoc._panX = this.layoutDoc._panY = 0;
             this.layoutDoc._width = this.layoutDoc._height = defaultSize;
             this.props.Document._pileLayoutEngine = computeStarburstLayout.name;
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index bdad325d5..020fe1cb4 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -10,7 +10,7 @@ import { listSpec } from '../../../fields/Schema';
 import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
 import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnEmptyDoclist, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
+import { emptyFunction, returnEmptyDoclist, returnFalse, returnNone, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { CollectionViewType } from '../../documents/DocumentTypes';
 import { DragManager, dropActionType } from '../../util/DragManager';
@@ -238,9 +238,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         this.createDashEventsTarget(ele!); //so the whole grid is the drop target?
     };
 
-    @computed get onChildClickHandler() {
-        return () => this.props.childClickScript || ScriptCast(this.Document.onChildClick);
-    }
+    onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick);
     @computed get onChildDoubleClickHandler() {
         return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
     }
@@ -300,7 +298,13 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
 
     @observable _renderCount = 5;
     isChildContentActive = () =>
-        this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive)) ? true : this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false ? false : undefined;
+        this.props.isContentActive?.() === false
+            ? false
+            : this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive))
+            ? true
+            : this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false
+            ? false
+            : undefined;
     isChildButtonContentActive = () => (this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false ? false : undefined);
     // this is what renders the document that you see on the screen
     // called in Children: this actually adds a document to our children list
@@ -320,6 +324,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                 renderDepth={this.props.renderDepth + 1}
                 PanelWidth={width}
                 PanelHeight={height}
+                pointerEvents={this.props.DocumentView?.().props.onClick ? returnNone : undefined} // if the stack has an onClick, then we don't want the contents to be interactive (see CollectionPileView)
                 styleProvider={this.styleProvider}
                 docViewPath={this.props.docViewPath}
                 fitWidth={this.props.childFitWidth}
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 4d5978548..03c010703 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -277,7 +277,7 @@ export class CollectionTimeView extends CollectionSubView() {
 }
 
 ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBounds) {
-    const pivotField = StrCast(pivotDoc._pivotField) || 'author';
+    const pivotField = StrCast(pivotDoc._pivotField, 'author');
     let prevFilterIndex = NumCast(pivotDoc._prevFilterIndex);
     const originalFilter = StrListCast(ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField));
     pivotDoc['_prevDocFilter' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField);
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 790aa765d..b76033a0c 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -46,6 +46,7 @@ interface CollectionViewProps_ extends FieldViewProps {
     // property overrides for child documents
     childDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox)
     childDocumentsActive?: () => boolean | undefined; // whether child documents can be dragged if collection can be dragged (eg., in a when a Pile document is in startburst mode)
+    childContentsActive?: () => boolean | undefined;
     childFitWidth?: (child: Doc) => boolean;
     childShowTitle?: () => string;
     childOpacity?: () => number;
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 8fb610b87..4adf86683 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -801,7 +801,6 @@ export class TreeView extends React.Component<TreeViewProps> {
             case StyleProp.Opacity:         return this.props.treeView.outlineMode ? undefined : 1;
             case StyleProp.BackgroundColor: return this.selected ? '#7089bb' : StrCast(doc._backgroundColor, StrCast(doc.backgroundColor));
             case StyleProp.Highlighting:    if (this.props.treeView.outlineMode) return undefined;
-            case StyleProp.Hidden:          return false;
             case StyleProp.BoxShadow:       return undefined;
             case StyleProp.DocContents:
                 const highlightIndex = this.props.treeView.outlineMode ? Doc.DocBrushStatus.unbrushed : Doc.isBrushedHighlightedDegree(doc);
@@ -827,7 +826,6 @@ export class TreeView extends React.Component<TreeViewProps> {
     };
     embeddedStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
         if (property.startsWith(StyleProp.Decorations)) return null;
-        if (property.startsWith(StyleProp.Hidden)) return false;
         return this.props?.treeView?.props.styleProvider?.(doc, props, property); // properties are inherited from the CollectionTreeView, not the hierarchical parent in the treeView
     };
     onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 81b0c4d8a..2549ee0b3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -93,6 +93,7 @@ export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc
             width: layout[WidthSym](),
             height: layout[HeightSym](),
             pair: { layout, data },
+            transition: 'all .3s',
             replica: '',
         });
     });
@@ -100,28 +101,27 @@ export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc
 }
 
 export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
-    const mustFit = pivotDoc[WidthSym]() !== panelDim[0]; // if a panel size is set that's not the same as the pivot doc's size, then assume this is in a panel for a content fitting view (like a grid) in which case everything must be scaled to stay within the panel
     const docMap = new Map<string, PoolData>();
-    const docSize = mustFit ? panelDim[0] * 0.33 : 75; // assume an icon sized at 75
-    const burstRadius = mustFit ? panelDim : [NumCast(pivotDoc._starburstRadius, panelDim[0]) - docSize, NumCast(pivotDoc._starburstRadius, panelDim[1]) - docSize];
-    const scaleDim = [burstRadius[0] * 2 + docSize, burstRadius[1] * 2 + docSize];
+    const burstDiam = [NumCast(pivotDoc._width), NumCast(pivotDoc._height)];
     childPairs.forEach(({ layout, data }, i) => {
-        const docSize = layout.layoutKey === 'layout_icon' ? (mustFit ? panelDim[0] * 0.33 : 75) : 400; // assume a icon sized at 75
+        const aspect = layout[HeightSym]() / layout[WidthSym]();
+        const docSize = Math.min(Math.min(400, layout[WidthSym]()), Math.min(400, layout[WidthSym]()) / aspect);
         const deg = (i / childPairs.length) * Math.PI * 2;
         docMap.set(layout[Id], {
-            x: Math.cos(deg) * burstRadius[0] - docSize / 2,
-            y: Math.sin(deg) * burstRadius[1] - (docSize * layout[HeightSym]()) / layout[WidthSym]() / 2,
-            width: docSize, //layout[WidthSym](),
-            height: (docSize * layout[HeightSym]()) / layout[WidthSym](),
+            x: Math.min(burstDiam[0] / 2 - docSize, Math.max(-burstDiam[0] / 2, (Math.cos(deg) * burstDiam[0]) / 2 - docSize / 2)),
+            y: Math.min(burstDiam[1] / 2 - docSize * aspect, Math.max(-burstDiam[1] / 2, (Math.sin(deg) * burstDiam[1]) / 2 - (docSize / 2) * aspect)),
+            width: docSize,
+            height: docSize * aspect,
             zIndex: NumCast(layout.zIndex),
             pair: { layout, data },
             replica: '',
             color: 'white',
             backgroundColor: 'white',
+            transition: 'all 0.3s',
         });
     });
-    const divider = { type: 'div', color: 'transparent', x: -burstRadius[0], y: 0, width: 15, height: 15, payload: undefined };
-    return normalizeResults(scaleDim, 12, docMap, poolData, viewDefsToJSX, [], 0, [divider]);
+    const divider = { type: 'div', color: 'transparent', x: -burstDiam[0] / 2, y: -burstDiam[1] / 2, width: 15, height: 15, payload: undefined };
+    return normalizeResults(burstDiam, 12, docMap, poolData, viewDefsToJSX, [], 0, [divider]);
 }
 
 export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
@@ -424,6 +424,7 @@ function normalizeResults(
                     color: newPosRaw.color,
                     pair: ele[1].pair,
                 };
+                if (newPosRaw.transition) newPos.transition = newPosRaw.transition;
                 poolData.set(newPos.pair.layout[Id] + (newPos.replica || ''), { transition: 'all 1s', ...newPos });
             }
         });
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index ff0d01f29..1fc4d9259 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -15,7 +15,7 @@ import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } fro
 import { ImageField } from '../../../../fields/URLField';
 import { TraceMobx } from '../../../../fields/util';
 import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
-import { aggregateBounds, emptyFunction, intersectRect, returnFalse, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { aggregateBounds, emptyFunction, intersectRect, returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils';
 import { CognitiveServices } from '../../../cognitive_services/CognitiveServices';
 import { Docs, DocUtils } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
@@ -308,6 +308,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     focus = (anchor: Doc, options: DocFocusOptions) => {
+        if (this._lightboxDoc) return;
         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.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc);
@@ -327,7 +328,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     getView = async (doc: Doc): Promise<Opt<DocumentView>> => {
         return new Promise<Opt<DocumentView>>(res => {
-            doc.hidden && (doc.hidden = false);
+            if (doc.hidden && this._lightboxDoc !== doc) doc.hidden = false;
             const findDoc = (finish: (dv: DocumentView) => void) => DocumentManager.Instance.AddViewRenderedCb(doc, dv => finish(dv));
             findDoc(dv => res(dv));
         });
@@ -778,7 +779,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         this._batch?.end();
     };
 
+    @action
     onClick = (e: React.MouseEvent) => {
+        if (this._lightboxDoc) this._lightboxDoc = undefined;
         if (this.onBrowseClickHandler()) {
             if (this.props.DocumentView?.()) {
                 this.onBrowseClickHandler().script.run({ documentView: this.props.DocumentView(), clientX: e.clientX, clientY: e.clientY });
@@ -1274,7 +1277,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 docRangeFilters={this.childDocRangeFilters}
                 searchFilterDocs={this.searchFilterDocs}
                 isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive}
-                isContentActive={emptyFunction}
+                isContentActive={this.props.childContentsActive ?? emptyFunction}
                 focus={this.Document._isGroup ? this.groupFocus : this.isAnnotationOverlay ? this.props.focus : this.focus}
                 addDocTab={this.addDocTab}
                 addDocument={this.props.addDocument}
@@ -1320,13 +1323,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             case undefined:
             case OpenWhere.lightbox:
                 if (this.layoutDoc._isLightbox) {
-                    // _isLightbox docs have a script that will unset this overlay onClick
-                    this.layoutDoc[this.props.fieldKey] = new List<Doc>(doc instanceof Doc ? [doc] : doc);
+                    this._lightboxDoc = doc;
+                    return true;
+                } else if (this.childDocList?.includes(doc)) {
+                    if (doc.hidden) doc.hidden = false;
                     return true;
                 }
         }
         return this.props.addDocTab(doc, where);
     });
+    @observable _lightboxDoc: Opt<Doc>;
 
     getCalculatedPositions(params: { pair: { layout: Doc; data?: Doc }; index: number; collection: Doc }): PoolData {
         const childDoc = params.pair.layout;
@@ -1936,7 +1942,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             1000
         );
     };
-
+    lightboxPanelWidth = () => Math.max(0, this.props.PanelWidth() - 30);
+    lightboxPanelHeight = () => Math.max(0, this.props.PanelHeight() - 30);
+    lightboxScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-15, -15);
     render() {
         TraceMobx();
         return (
@@ -1965,36 +1973,65 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     width: `${100 / (this.nativeDimScaling || 1)}%`,
                     height: this.props.getScrollHeight?.() ?? `${100 / (this.nativeDimScaling || 1)}%`,
                 }}>
-                {this._firstRender ? this.placeholder : this.marqueeView}
-                {this.props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />}
-
-                {/* // uncomment to show snap lines */}
-                <div className="snapLines" style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' }}>
-                    <svg style={{ width: '100%', height: '100%' }}>
-                        {this._hLines?.map(l => (
-                            <line x1="0" y1={l} x2="1000" y2={l} stroke="black" />
-                        ))}
-                        {this._vLines?.map(l => (
-                            <line y1="0" x1={l} y2="1000" x2={l} stroke="black" />
-                        ))}
-                    </svg>
-                </div>
+                {this._lightboxDoc ? (
+                    <div style={{ padding: 15, width: '100%', height: '100%' }}>
+                        <DocumentView
+                            {...this.props}
+                            Document={this._lightboxDoc}
+                            DataDoc={undefined}
+                            PanelWidth={this.lightboxPanelWidth}
+                            PanelHeight={this.lightboxPanelHeight}
+                            NativeWidth={returnZero}
+                            NativeHeight={returnZero}
+                            onClick={this.onChildClickHandler}
+                            onKey={this.onKeyDown}
+                            onDoubleClick={this.onChildDoubleClickHandler}
+                            onBrowseClick={this.onBrowseClickHandler}
+                            docFilters={this.childDocFilters}
+                            docRangeFilters={this.childDocRangeFilters}
+                            searchFilterDocs={this.searchFilterDocs}
+                            isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive}
+                            isContentActive={this.props.childContentsActive ?? emptyFunction}
+                            addDocTab={this.addDocTab}
+                            ScreenToLocalTransform={this.lightboxScreenToLocal}
+                            fitContentsToBox={undefined}
+                            focus={this.focus}
+                        />
+                    </div>
+                ) : (
+                    <>
+                        {this._firstRender ? this.placeholder : this.marqueeView}
+                        {this.props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />}
+
+                        {/* // uncomment to show snap lines */}
+                        <div className="snapLines" style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' }}>
+                            <svg style={{ width: '100%', height: '100%' }}>
+                                {this._hLines?.map(l => (
+                                    <line x1="0" y1={l} x2="1000" y2={l} stroke="black" />
+                                ))}
+                                {this._vLines?.map(l => (
+                                    <line y1="0" x1={l} y2="1000" x2={l} stroke="black" />
+                                ))}
+                            </svg>
+                        </div>
 
-                {this.props.Document._isGroup && SnappingManager.GetIsDragging() && this.ChildDrag ? (
-                    <div
-                        className="collectionFreeForm-groupDropper"
-                        ref={this.createGroupEventsTarget}
-                        style={{
-                            width: this.ChildDrag ? '10000' : '100%',
-                            height: this.ChildDrag ? '10000' : '100%',
-                            left: this.ChildDrag ? '-5000' : 0,
-                            top: this.ChildDrag ? '-5000' : 0,
-                            position: 'absolute',
-                            background: '#0009930',
-                            pointerEvents: 'all',
-                        }}
-                    />
-                ) : null}
+                        {this.props.Document._isGroup && SnappingManager.GetIsDragging() && this.ChildDrag ? (
+                            <div
+                                className="collectionFreeForm-groupDropper"
+                                ref={this.createGroupEventsTarget}
+                                style={{
+                                    width: this.ChildDrag ? '10000' : '100%',
+                                    height: this.ChildDrag ? '10000' : '100%',
+                                    left: this.ChildDrag ? '-5000' : 0,
+                                    top: this.ChildDrag ? '-5000' : 0,
+                                    position: 'absolute',
+                                    background: '#0009930',
+                                    pointerEvents: 'all',
+                                }}
+                            />
+                        ) : null}
+                    </>
+                )}
             </div>
         );
     }
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index d443df0f3..eaeb5f933 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -371,10 +371,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
 
     @undoBatch
     @action
-    delete = () => {
+    delete = (e?: React.PointerEvent<Element> | KeyboardEvent | undefined, hide?: boolean) => {
         const selected = this.marqueeSelect(false);
         SelectionManager.DeselectAll();
-        selected.forEach(doc => this.props.removeDocument?.(doc));
+        selected.forEach(doc => (hide ? (doc.hidden = true) : this.props.removeDocument?.(doc)));
 
         this.cleanupInteractions(false);
         MarqueeOptionsMenu.Instance.fadeOut(true);
@@ -550,11 +550,11 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         if (this._commandExecuted || (e as any).propagationIsStopped) {
             return;
         }
-        if (e.key === 'Backspace' || e.key === 'Delete' || e.key === 'd') {
+        if (e.key === 'Backspace' || e.key === 'Delete' || e.key === 'd' || e.key === 'h') {
             this._commandExecuted = true;
             e.stopPropagation();
             (e as any).propagationIsStopped = true;
-            this.delete();
+            this.delete(e, e.key === 'h');
             e.stopPropagation();
         }
         if ('cbtsSpg'.indexOf(e.key) !== -1) {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index dc508d95f..2ea0a6f19 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -262,7 +262,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     @observable _animateScalingTo = 0;
 
     public get animateScaleTime() {
-        return this._animateScaleTime ?? 300;
+        return this._animateScaleTime ?? 100;
     }
     public get displayName() {
         return 'DocumentView(' + this.props.Document.title + ')';
@@ -295,8 +295,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Decorations + (this.props.isSelected() ? ':selected' : ''));
     }
     @computed get backgroundBoxColor() {
-        const thumb = ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb));
-        return thumb ? undefined : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor + ':box');
+        return this.thumbShown() ? undefined : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor + ':box');
     }
     @computed get docContents() {
         return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.DocContents);
@@ -413,6 +412,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         setTimeout(() => this._titleRef.current?.setIsFocused(true)); // use timeout in case title wasn't shown to allow re-render so that titleref will be defined
     };
 
+    public static addDocTabFunc: (doc: Doc, location: OpenWhere) => boolean = returnFalse;
+
     onClick = action((e: React.MouseEvent | React.PointerEvent) => {
         if (!this.Document.ignoreClick && this.pointerEvents !== 'none' && this.props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) {
             let stopPropagate = true;
@@ -446,24 +447,33 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 let clickFunc: undefined | (() => any);
                 if (!this.disableClickScriptFunc && this.onClickHandler?.script) {
                     const { clientX, clientY, shiftKey, altKey, metaKey } = e;
-                    const func = () =>
-                        this.onClickHandler?.script.run(
-                            {
-                                this: this.layoutDoc,
-                                self: this.rootDoc,
-                                _readOnly_: false,
-                                scriptContext: this.props.scriptContext,
-                                documentView: this.props.DocumentView(),
-                                clientX,
-                                clientY,
-                                shiftKey,
-                                altKey,
-                                metaKey,
-                            },
-                            console.log
-                        ).result?.select === true
-                            ? this.props.select(false)
-                            : '';
+                    const func = () => {
+                        // replace default add doc func with this view's add doc func.
+                        // to allow override behaviors for how to display links to undisplayed documents.
+                        // e.g., if this document is part of a labeled 'lightbox' container, then documents will be shown in place
+                        // instead of in the global lightbox
+                        const oldFunc = DocumentViewInternal.addDocTabFunc;
+                        DocumentViewInternal.addDocTabFunc = this.props.addDocTab;
+                        const res =
+                            this.onClickHandler?.script.run(
+                                {
+                                    this: this.layoutDoc,
+                                    self: this.rootDoc,
+                                    _readOnly_: false,
+                                    scriptContext: this.props.scriptContext,
+                                    documentView: this.props.DocumentView(),
+                                    clientX,
+                                    clientY,
+                                    shiftKey,
+                                    altKey,
+                                    metaKey,
+                                },
+                                console.log
+                            ).result?.select === true
+                                ? this.props.select(false)
+                                : '';
+                        DocumentViewInternal.addDocTabFunc = oldFunc;
+                    };
                     clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'on click'));
                 } else {
                     // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part
@@ -1318,9 +1328,6 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         const hideCount = this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton;
         return hideCount ? null : <DocumentLinksButton View={this} scaling={this.scaleToScreenSpace} OnHover={true} Bottom={this.topMost} ShowCount={true} />;
     }
-    @computed get hidden() {
-        return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Hidden);
-    }
     @computed get docViewPath(): DocumentView[] {
         return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this];
     }
@@ -1497,7 +1504,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         const xshift = Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined;
         const yshift = Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined;
 
-        return this.hidden ? null : (
+        return (
             <div className="contentFittingDocumentView" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))}>
                 {!this.props.Document || !this.props.PanelWidth() ? null : (
                     <div
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 22dbc1e80..29943e156 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -2,7 +2,9 @@
     border-radius: inherit;
     width: 100%;
     height: 100%;
-    position: relative;
+    position: absolute;
+    top: 0;
+    left: 0;
     transform-origin: top left;
 
     .imageBox-annotationLayer {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 98df777cb..20e5f5eca 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -505,6 +505,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                     }
                 })}
                 style={{
+                    display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined,
                     width: this.props.PanelWidth() ? undefined : `100%`,
                     height: this.props.PanelWidth() ? undefined : `100%`,
                     pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index 6f578a9fc..75847c100 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -183,6 +183,7 @@
                 height: 100%;
                 position: absolute;
                 top: 0;
+                left: 0;
                 body {
                     ::selection {
                         color: white;
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
index b31fc01ff..648c579d0 100644
--- a/src/client/views/nodes/formattedText/DashDocView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -181,6 +181,8 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
                     height: this._height,
                     position: 'absolute',
                     display: 'inline-block',
+                    left: 0,
+                    top: 0,
                 }}
                 onPointerLeave={this.onPointerLeave}
                 onPointerEnter={this.onPointerEnter}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index fd7fbb333..3b42c41a5 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -24,6 +24,27 @@ audiotag:hover {
     transform: scale(2);
     transform-origin: bottom center;
 }
+.formattedTextBox {
+    touch-action: none;
+    background: inherit;
+    padding: 0;
+    border-width: 0px;
+    border-radius: inherit;
+    border-color: $medium-gray;
+    box-sizing: border-box;
+    background-color: inherit;
+    border-style: solid;
+    overflow-y: auto;
+    overflow-x: hidden;
+    color: inherit;
+    display: flex;
+    flex-direction: row;
+    transition: opacity 1s;
+    width: 100%;
+    position: absolute;
+    top: 0;
+    left: 0;
+}
 
 .formattedTextBox-cont {
     touch-action: none;
@@ -51,6 +72,15 @@ audiotag:hover {
         position: absolute;
     }
 }
+.formattedTextBox-alternateButton {
+    position: absolute;
+    color: white;
+    background: black;
+    right: 0;
+    bottom: 0;
+    width: 15;
+    height: 15;
+}
 
 .formattedTextBox-outer-selected,
 .formattedTextBox-outer {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index bbe38cf99..2755d5100 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1,9 +1,9 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@material-ui/core';
 import { isEqual } from 'lodash';
 import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
-import { Configuration, OpenAIApi } from 'openai';
 import { baseKeymap, selectAll } from 'prosemirror-commands';
 import { history } from 'prosemirror-history';
 import { inputRules } from 'prosemirror-inputrules';
@@ -1928,6 +1928,45 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             </div>
         );
     }
+    @computed get overlayAlternateIcon() {
+        const usePath = this.rootDoc[`${this.props.fieldKey}-usePath`];
+        return (
+            <Tooltip
+                title={
+                    <div className="dash-tooltip">
+                        toggle between
+                        <span style={{ color: usePath === undefined ? 'black' : undefined }}>
+                            <em> primary, </em>
+                        </span>
+                        <span style={{ color: usePath === 'alternate' ? 'black' : undefined }}>
+                            <em>alternate, </em>
+                        </span>
+                        and show
+                        <span style={{ color: usePath === 'alternate:hover' ? 'black' : undefined }}>
+                            <em> alternate on hover</em>
+                        </span>
+                    </div>
+                }>
+                <div
+                    className="formattedTextBox-alternateButton"
+                    onPointerDown={e =>
+                        setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => (this.rootDoc[`_${this.props.fieldKey}-usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined))
+                    }
+                    style={{
+                        display: this.props.isContentActive() ? 'block' : 'none',
+                        background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray',
+                        color: usePath === undefined ? 'black' : 'white',
+                    }}>
+                    <FontAwesomeIcon icon="turn-up" size="lg" />
+                </div>
+            </Tooltip>
+        );
+    }
+    @computed get fieldKey() {
+        const usePath = StrCast(this.rootDoc[`${this.props.fieldKey}-usePath`]);
+        return this.props.fieldKey + (usePath && (!usePath.includes(':hover') || this._isHovering) ? `-${usePath.replace(':hover', '')}` : '');
+    }
+    @observable _isHovering = false;
     render() {
         TraceMobx();
         const active = this.props.isContentActive() || this.props.isSelected();
@@ -1944,7 +1983,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         const styleFromLayoutString = Doc.styleFromLayoutString(this.rootDoc, this.layoutDoc, this.props, scale); // this converts any expressions in the format string to style props.  e.g., <FormattedTextBox height='{this._headerHeight}px' >
         return styleFromLayoutString?.height === '0px' ? null : (
             <div
-                className="formattedTextBox-cont"
+                className="formattedTextBox"
+                onPointerEnter={action(() => (this._isHovering = true))}
+                onPointerLeave={action(() => (this._isHovering = false))}
                 ref={r =>
                     r?.addEventListener(
                         'wheel', // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this)
@@ -1966,6 +2007,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                               width: `${100 / scale}%`,
                               height: `${100 / scale}%`,
                           }),
+                    display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined,
                     transition: 'inherit',
                     // overflowY: this.layoutDoc._autoHeight ? "hidden" : undefined,
                     color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color),
@@ -2017,6 +2059,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     {this.noSidebar || this.props.dontSelectOnLoad || !this.SidebarShown || this.sidebarWidthPercent === '0%' ? null : this.sidebarCollection}
                     {this.noSidebar || this.Document._noSidebar || this.props.dontSelectOnLoad || this.Document._singleLine ? null : this.sidebarHandle}
                     {this.audioHandle}
+                    {this.overlayAlternateIcon}
                 </div>
             </div>
         );
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 70d9ed61f..92f3a69eb 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -107,8 +107,11 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
                 redo: () => (receiver[prop] = value),
                 undo: () => {
                     const wasUpdate = receiver[UpdatingFromServer];
+                    const wasForce = receiver[ForceServerWrite];
+                    receiver[ForceServerWrite] = true; // needed since writes aren't propagated to server if UpdatingFromServerIsSet
                     receiver[UpdatingFromServer] = true; // needed if the event caused ACL's to change such that the doc is otherwise no longer editable.
                     receiver[prop] = curValue;
+                    receiver[ForceServerWrite] = wasForce;
                     receiver[UpdatingFromServer] = wasUpdate;
                 },
                 prop: prop?.toString(),
-- 
cgit v1.2.3-70-g09d2


From c83c44313b0d3e6168408fadd0c01d99057c325a Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 24 Apr 2023 12:45:38 -0400
Subject: fixed pile view to be resizable.

---
 src/client/views/collections/CollectionPileView.tsx                 | 6 ++++--
 .../collectionFreeForm/CollectionFreeFormLayoutEngines.tsx          | 3 ++-
 src/client/views/nodes/ImageBox.tsx                                 | 2 +-
 3 files changed, 7 insertions(+), 4 deletions(-)

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

diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index e5d89865b..5b96a8682 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -73,6 +73,9 @@ export class CollectionPileView extends CollectionSubView() {
     // toggles the pileup between starburst to compact
     toggleStarburst = action(() => {
         if (this.layoutEngine() === computeStarburstLayout.name) {
+            if (this.rootDoc[WidthSym]() !== NumCast(this.rootDoc._starburstDiameter, 500)) {
+                this.rootDoc._starburstDiameter = this.rootDoc[WidthSym]();
+            }
             const defaultSize = 110;
             this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - NumCast(this.layoutDoc._starburstPileWidth, defaultSize) / 2;
             this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - NumCast(this.layoutDoc._starburstPileHeight, defaultSize) / 2;
@@ -83,8 +86,7 @@ export class CollectionPileView extends CollectionSubView() {
             this.layoutDoc._panY = -10;
             this.props.Document._pileLayoutEngine = computePassLayout.name;
         } else {
-            const defaultSize = 500;
-            !this.layoutDoc._starburstDocScale && (this.layoutDoc._starburstDocScale = 2.5);
+            const defaultSize = NumCast(this.rootDoc._starburstDiameter, 500);
             this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - defaultSize / 2;
             this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - defaultSize / 2;
             this.layoutDoc._starburstPileWidth = this.layoutDoc[WidthSym]();
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 2549ee0b3..c1f3c5aa6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -103,9 +103,10 @@ export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc
 export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
     const docMap = new Map<string, PoolData>();
     const burstDiam = [NumCast(pivotDoc._width), NumCast(pivotDoc._height)];
+    const burstScale = NumCast(pivotDoc._starburstDocScale, 1);
     childPairs.forEach(({ layout, data }, i) => {
         const aspect = layout[HeightSym]() / layout[WidthSym]();
-        const docSize = Math.min(Math.min(400, layout[WidthSym]()), Math.min(400, layout[WidthSym]()) / aspect);
+        const docSize = Math.min(Math.min(400, layout[WidthSym]()), Math.min(400, layout[WidthSym]()) / aspect) * burstScale;
         const deg = (i / childPairs.length) * Math.PI * 2;
         docMap.set(layout[Id], {
             x: Math.min(burstDiam[0] / 2 - docSize, Math.max(-burstDiam[0] / 2, (Math.cos(deg) * burstDiam[0]) / 2 - docSize / 2)),
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 20e5f5eca..c9be10d3a 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -407,7 +407,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             .filter(url => url)
             .map(url => this.choosePath(url)); // access the primary layout data of the alternate documents
         const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths;
-        return paths.length ? paths : [Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')];
+        return paths.length ? paths : [Utils.CorsProxy('https://cs.brown.edu/~bcz/noImage.png')];
     }
 
     @observable _isHovering = false; // flag to switch between primary and alternate images on hover
-- 
cgit v1.2.3-70-g09d2


From 2255f2ffd4d9c5818d8d26f1814b315ad914eaa9 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 8 May 2023 11:34:29 -0400
Subject: fixed dragging inkMask strokes.  fixed background color/fill for
 strokes.  fixed send to back.  changed #tags to be a list

---
 src/client/views/FilterPanel.tsx                   |   4 +-
 src/client/views/PropertiesView.tsx                | 150 +++++++--------------
 src/client/views/StyleProvider.tsx                 |   4 +-
 .../CollectionFreeFormLayoutEngines.tsx            |  15 +--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   5 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   7 +-
 src/client/views/nodes/DocumentView.tsx            |   2 +-
 src/client/views/nodes/button/FontIconBox.tsx      |  20 +--
 .../views/nodes/formattedText/FormattedTextBox.tsx |   2 +
 .../views/nodes/formattedText/RichTextRules.ts     |  10 +-
 src/fields/Doc.ts                                  |   3 +-
 11 files changed, 85 insertions(+), 137 deletions(-)

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

diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx
index a237249c1..d17a4ea25 100644
--- a/src/client/views/FilterPanel.tsx
+++ b/src/client/views/FilterPanel.tsx
@@ -145,8 +145,8 @@ export class FilterPanel extends React.Component<filterProps> {
         const set = new Set<string>([String.fromCharCode(127) + '--undefined--']);
         if (facetHeader === 'tags')
             allCollectionDocs.forEach(child =>
-                Field.toString(child[facetHeader] as Field)
-                    .split(':')
+                StrListCast(child[facetHeader])
+                    .filter(h => h)
                     .forEach(key => set.add(key))
             );
         else
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 6582c3f2a..fff8390b3 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -7,7 +7,7 @@ import { intersection } from 'lodash';
 import { action, computed, Lambda, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { ColorState, SketchPicker } from 'react-color';
-import { AclAdmin, AclSym, DataSym, Doc, Field, HeightSym, HierarchyMapping, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc';
+import { AclAdmin, AclSym, DataSym, Doc, Field, FieldResult, HeightSym, HierarchyMapping, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { InkField } from '../../fields/InkField';
 import { List } from '../../fields/List';
@@ -152,116 +152,55 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         return 0;
     };
 
-    @computed get expandedField() {
+    editableFields = (filter: (key: string) => boolean, reqdKeys: string[]) => {
+        const rows: JSX.Element[] = [];
         if (this.dataDoc && this.selectedDoc) {
-            const ids: { [key: string]: string } = {};
-            const docs = SelectionManager.Views().length < 2 ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc] : SelectionManager.Views().map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc));
-            docs.forEach(doc => Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)));
-            const rows: JSX.Element[] = [];
-            for (const key of Object.keys(ids).slice().sort()) {
-                const docvals = new Set<any>();
-                docs.forEach(doc => docvals.add(doc[key]));
-                const contents = Array.from(docvals.keys()).length > 1 ? '-multiple' : docs[0][key];
-                if (key[0] === '#') {
-                    rows.push(
-                        <div style={{ display: 'flex', overflowY: 'visible', marginBottom: '2px' }} key={key}>
-                            <span style={{ fontWeight: 'bold', whiteSpace: 'nowrap' }}>{key}</span>
-                            &nbsp;
-                        </div>
-                    );
-                } else {
-                    const contentElement = (
-                        <EditableView
-                            key="editableView"
-                            contents={contents !== undefined ? Field.toString(contents as Field) : 'null'}
-                            height={13}
-                            fontSize={10}
-                            GetValue={() => (contents !== undefined ? Field.toString(contents as Field) : 'null')}
-                            SetValue={(value: string) => {
-                                docs.map(doc => KeyValueBox.SetField(doc, key, value, true));
-                                return true;
-                            }}
-                        />
-                    );
-                    rows.push(
-                        <div style={{ display: 'flex', overflowY: 'visible', marginBottom: '-1px' }} key={key}>
-                            <span style={{ fontWeight: 'bold', whiteSpace: 'nowrap' }}>{key + ':'}</span>
-                            &nbsp;
-                            {contentElement}
-                        </div>
-                    );
-                }
-            }
+            const ids = new Set<string>(reqdKeys);
+            const docs: Doc[] = SelectionManager.Views().length < 2 ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc] : SelectionManager.Views().map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc));
+            docs.forEach(doc => Object.keys(doc).forEach(key => doc[key] !== ComputedField.undefined && ids.add(key)));
+
+            // prettier-ignore
+            Array.from(ids).filter(filter).sort().map(key => {
+                const multiple = Array.from(docs.reduce((set,doc) => set.add(doc[key]), new Set<FieldResult>()).keys()).length > 1;
+                const editableContents = multiple ? '-multiple-' : Field.toKeyValueString(docs[0], key);
+                const displayContents = multiple ? '-multiple-' : Field.toString(docs[0][key] as Field);
+                const contentElement = key[0] === '#'  ? <></> : (
+                    <EditableView
+                        key="editableView"
+                        contents={displayContents}
+                        height={13}
+                        fontSize={10}
+                        GetValue={() => editableContents}
+                        SetValue={(value: string) => {
+                            value !== '-multiple-' && docs.map(doc => KeyValueBox.SetField(doc, key, value, true));
+                            return true;
+                        }}
+                    />);
+                rows.push(
+                    <div style={{ display: 'flex', overflowY: 'visible', marginBottom: '-1px' }} key={key}>
+                        <span style={{ fontWeight: 'bold', whiteSpace: 'nowrap' }}>{key + ':'}</span>
+                        &nbsp;
+                        {contentElement}
+                    </div>
+                );
+            });
+
             rows.push(
-                <div className="propertiesView-field" key={'newKeyValue'} style={{ marginTop: '3px' }}>
+                <div className="propertiesView-field" key="newKeyValue" style={{ marginTop: '3px' }}>
                     <EditableView key="editableView" oneLine contents={'add key:value or #tags'} height={13} fontSize={10} GetValue={() => ''} SetValue={this.setKeyValue} />
                 </div>
             );
-            return rows;
         }
+        return rows;
+    };
+
+    @computed get expandedField() {
+        return this.editableFields(returnTrue, []);
     }
 
     @computed get noviceFields() {
-        if (this.dataDoc) {
-            const ids: { [key: string]: string } = {};
-            const docs = SelectionManager.Views().length < 2 ? [this.dataDoc] : SelectionManager.Views().map(dv => dv.dataDoc);
-            docs.forEach(doc => Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)));
-            const rows: JSX.Element[] = [];
-            const noviceReqFields = ['author', 'creationDate', 'tags'];
-            const noviceLayoutFields = ['_curPage'];
-            const noviceKeys = [...Array.from(Object.keys(ids)).filter(key => key[0] === '#' || key.indexOf('lastModified') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('acl'))), ...noviceReqFields, ...noviceLayoutFields];
-            for (const key of noviceKeys.sort()) {
-                const docvals = new Set<any>();
-                docs.forEach(doc => docvals.add(doc[key]));
-                const contents = Array.from(docvals.keys()).length > 1 ? '-multiple' : docs[0][key];
-                if (key[0] === '#') {
-                    rows.push(
-                        <div className="propertiesView-uneditable-field" key={key}>
-                            <span style={{ fontWeight: 'bold', whiteSpace: 'nowrap' }}>{key}</span>
-                            &nbsp;
-                        </div>
-                    );
-                } else if (contents !== undefined) {
-                    const value = Field.toString(contents as Field);
-                    if (noviceReqFields.includes(key) || key.indexOf('lastModified') !== -1) {
-                        rows.push(
-                            <div className="propertiesView-uneditable-field" key={key}>
-                                <span style={{ fontWeight: 'bold', whiteSpace: 'nowrap' }}>{key + ': '}</span>
-                                <div style={{ whiteSpace: 'nowrap', overflowX: 'hidden' }}>{value}</div>
-                            </div>
-                        );
-                    } else {
-                        const contentElement = (
-                            <EditableView
-                                key="editableView"
-                                contents={value}
-                                height={13}
-                                fontSize={10}
-                                GetValue={() => (contents !== undefined ? Field.toString(contents as Field) : 'null')}
-                                SetValue={(value: string) => {
-                                    docs.map(doc => KeyValueBox.SetField(doc, key, value, true));
-                                    return true;
-                                }}
-                            />
-                        );
-
-                        rows.push(
-                            <div style={{ display: 'flex', overflowY: 'visible', marginBottom: '-1px' }} key={key}>
-                                <span style={{ fontWeight: 'bold', whiteSpace: 'nowrap' }}>{key + ':'}</span>
-                                &nbsp;
-                                {contentElement}
-                            </div>
-                        );
-                    }
-                }
-            }
-            rows.push(
-                <div className="propertiesView-field" key={'newKeyValue'} style={{ marginTop: '3px' }}>
-                    <EditableView key="editableView" oneLine contents={'add key:value or #tags'} height={13} fontSize={10} GetValue={() => ''} SetValue={this.setKeyValue} />
-                </div>
-            );
-            return rows;
-        }
+        const noviceReqFields = ['author', 'creationDate', 'tags', '_curPage'];
+        return this.editableFields(key => key[0] === '#' || key.indexOf('lastModified') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('acl')), noviceReqFields);
     }
 
     @undoBatch
@@ -280,9 +219,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
             } else if (value[0] === '#') {
                 const newVal = value + `:'${value}'`;
                 doc[DataSym][value] = value;
-                const tags = StrCast(doc.tags, ':');
-                if (!tags.includes(`${value}:`)) {
-                    doc[DataSym].tags = `${tags + value + ':'}`;
+                const tags = StrListCast(doc.tags);
+                if (!tags.includes(value)) {
+                    tags.push(value);
+                    doc[DataSym].tags = tags.length ? new List<string>(tags) : undefined;
                 }
                 return true;
             }
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index d13052f71..192a501f2 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -303,13 +303,13 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
             }
         }
         case StyleProp.PointerEvents:
-            const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name);
+            const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name) && !props?.LayoutTemplateString;
             if (docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.() !== undefined) return docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.();
             if (MainView.Instance._exploreMode || doc?.unrendered) return isInk ? 'visiblePainted' : 'all';
             if (doc?.pointerEvents) return StrCast(doc.pointerEvents);
             if (props?.contentPointerEvents) return StrCast(props.contentPointerEvents);
             if (props?.pointerEvents?.() === 'none') return 'none';
-            if (opacity() === 0 || doc?.isInkMask) return 'none';
+            if (opacity() === 0) return 'none';
             if (props?.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/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index c1f3c5aa6..fee4705e6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -133,20 +133,15 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do
     let nonNumbers = 0;
     const pivotFieldKey = toLabel(engineProps?.pivotField ?? pivotDoc._pivotField) || 'author';
     childPairs.map(pair => {
-        const lval =
-            pivotFieldKey === '#' || pivotFieldKey === 'tags'
-                ? Array.from(Object.keys(Doc.GetProto(pair.layout)))
-                      .filter(k => k.startsWith('#'))
-                      .map(k => k.substring(1))
-                : Cast(pair.layout[pivotFieldKey], listSpec('string'), null);
+        const listValue = Cast(pair.layout[pivotFieldKey], listSpec('string'), null);
 
         const num = toNumber(pair.layout[pivotFieldKey]);
         if (num === undefined || Number.isNaN(num)) {
             nonNumbers++;
         }
         const val = Field.toString(pair.layout[pivotFieldKey] as Field);
-        if (lval) {
-            lval.forEach((val, i) => {
+        if (listValue) {
+            listValue.forEach((val, i) => {
                 !pivotColumnGroups.get(val) && pivotColumnGroups.set(val, { docs: [], filters: [val], replicas: [] });
                 pivotColumnGroups.get(val)!.docs.push(pair.layout);
                 pivotColumnGroups.get(val)!.replicas.push(i.toString());
@@ -159,8 +154,8 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do
             docMap.set(pair.layout[Id], {
                 x: 0,
                 y: 0,
-                zIndex: -99,
-                width: 0,
+                zIndex: 0,
+                width: 0, // should make doc hidden in CollectionFreefromDocumentView
                 height: 0,
                 pair,
                 replica: '',
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 9cc732008..25da868e0 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1158,7 +1158,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @action
     bringToFront = (doc: Doc, sendToBack?: boolean) => {
         if (sendToBack) {
-            doc.zIndex = 0;
+            const docs = this.childLayoutPairs.map(pair => pair.layout).slice();
+            docs.sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex));
+            let zfirst = docs.length ? NumCast(docs[0].zIndex) : 0;
+            doc.zIndex = zfirst - 1;
         } else if (doc.isInkMask) {
             doc.zIndex = 5000;
         } else {
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 24b9f3b25..b68bcc263 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -16,6 +16,8 @@ import { StyleProp } from '../StyleProvider';
 import './CollectionFreeFormDocumentView.scss';
 import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView';
 import React = require('react');
+import { DocumentType } from '../../documents/DocumentTypes';
+import { InkingStroke } from '../InkingStroke';
 
 export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
     dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; rotation?: number; color?: string; backgroundColor?: string; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined;
@@ -201,9 +203,10 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
             PanelWidth: this.panelWidth,
             PanelHeight: this.panelHeight,
         };
+        const isInk = StrCast(this.layoutDoc.layout).includes(InkingStroke.name) && !this.props.LayoutTemplateString && !this.layoutDoc._isInkMask;
         return (
             <div
-                className={'collectionFreeFormDocumentView-container'}
+                className="collectionFreeFormDocumentView-container"
                 style={{
                     width: this.panelWidth(),
                     height: this.panelHeight(),
@@ -211,7 +214,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                     transformOrigin: '50% 50%',
                     transition: this.dataProvider?.transition ?? (this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition)),
                     zIndex: this.ZInd,
-                    display: this.ZInd === -99 ? 'none' : undefined,
+                    pointerEvents: isInk ? 'none' : undefined,
                 }}>
                 {this.props.renderCutoffProvider(this.props.Document) ? (
                     <div style={{ position: 'absolute', width: this.panelWidth(), height: this.panelHeight(), background: 'lightGreen' }} />
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 3bdd2bf6e..f1f5f7e10 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -870,7 +870,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     contentPointerEvents = () => (!this.disableClickScriptFunc && this.onClickHandler ? 'none' : this.pointerEvents);
     @computed get contents() {
         TraceMobx();
-        const isInk = StrCast(this.layoutDoc.layout).includes(InkingStroke.name);
+        const isInk = StrCast(this.layoutDoc.layout).includes(InkingStroke.name) && !this.props.LayoutTemplateString;
         return (
             <div
                 className="documentView-contentsView"
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index f04678d8b..26515da30 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -224,12 +224,12 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                             </div>
                             <div
                                 className="dropbox-background"
-                                onClick={e => {
+                                onClick={action(e => {
                                     e.stopPropagation();
                                     this.rootDoc.dropDownOpen = false;
                                     this.noTooltip = false;
                                     Doc.UnBrushAllDocs();
-                                }}
+                                })}
                             />
                         </div>
                     ) : null}
@@ -336,11 +336,11 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                 style={{ backgroundColor: this.rootDoc.dropDownOpen ? Colors.MEDIUM_BLUE : backgroundColor, color: color, display: dropdown ? undefined : 'flex' }}
                 onClick={
                     dropdown
-                        ? () => {
+                        ? action(() => {
                               this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen;
                               this.noTooltip = this.rootDoc.dropDownOpen;
                               Doc.UnBrushAllDocs();
-                          }
+                          })
                         : undefined
                 }>
                 {dropdown ? null : <FontAwesomeIcon style={{ marginLeft: 5 }} className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />}
@@ -358,12 +358,12 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                         </div>
                         <div
                             className="dropbox-background"
-                            onClick={e => {
+                            onClick={action(e => {
                                 e.stopPropagation();
                                 this.rootDoc.dropDownOpen = false;
                                 this.noTooltip = false;
                                 Doc.UnBrushAllDocs();
-                            }}
+                            })}
                         />
                     </div>
                 ) : null}
@@ -582,17 +582,19 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b
     } else if (selectedViews.length) {
         if (checkResult) {
             const selView = selectedViews.lastElement();
+            const fieldKey = selView.rootDoc.type === DocumentType.INK ? 'fillColor' : 'backgroundColor';
             const layoutFrameNumber = Cast(selView.props.docViewPath().lastElement()?.rootDoc?._currentFrame, 'number'); // frame number that container is at which determines layout frame values
             const contentFrameNumber = Cast(selView.rootDoc?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
-            return CollectionFreeFormDocumentView.getStringValues(selView?.rootDoc, contentFrameNumber).backgroundColor ?? 'transparent';
+            return CollectionFreeFormDocumentView.getStringValues(selView?.rootDoc, contentFrameNumber)[fieldKey] ?? 'transparent';
         }
         selectedViews.forEach(dv => {
+            const fieldKey = dv.rootDoc.type === DocumentType.INK ? 'fillColor' : 'backgroundColor';
             const layoutFrameNumber = Cast(dv.props.docViewPath().lastElement()?.rootDoc?._currentFrame, 'number'); // frame number that container is at which determines layout frame values
             const contentFrameNumber = Cast(dv.rootDoc?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
             if (contentFrameNumber !== undefined) {
-                CollectionFreeFormDocumentView.setStringValues(contentFrameNumber, dv.rootDoc, { backgroundColor: color });
+                CollectionFreeFormDocumentView.setStringValues(contentFrameNumber, dv.rootDoc, { fieldKey: color });
             } else {
-                dv.rootDoc._backgroundColor = color;
+                dv.rootDoc['_' + fieldKey] = color;
             }
         });
     } else {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index eb28ffbd7..1d668d6a9 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -69,6 +69,7 @@ import { SummaryView } from './SummaryView';
 import applyDevTools = require('prosemirror-dev-tools');
 import React = require('react');
 import { RTFMarkup } from '../../../util/RTFMarkup';
+import { List } from '../../../../fields/List';
 const translateGoogleApi = require('translate-google-api');
 export const GoogleRef = 'googleDocId';
 type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void;
@@ -318,6 +319,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                 const removed = curTags.filter(tag => !accumTags.includes(tag));
                 removed.forEach(r => (dataDoc[r] = undefined));
                 added.forEach(a => (dataDoc[a] = a));
+                dataDoc.tags = curTags.length ? new List<string>(curTags) : undefined;
 
                 let unchanged = true;
                 if (this._applyingChange !== this.fieldKey && removeSelection(json) !== removeSelection(curProto?.Data)) {
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index cc19d12bd..68b209332 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -1,7 +1,8 @@
 import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from 'prosemirror-inputrules';
 import { NodeSelection, TextSelection } from 'prosemirror-state';
-import { DataSym, Doc } from '../../../../fields/Doc';
+import { DataSym, Doc, StrListCast } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
+import { List } from '../../../../fields/List';
 import { ComputedField } from '../../../../fields/ScriptField';
 import { NumCast, StrCast } from '../../../../fields/Types';
 import { normalizeEmail } from '../../../../fields/util';
@@ -325,9 +326,10 @@ export class RichTextRules {
                 const tag = match[1];
                 if (!tag) return state.tr;
                 this.Document[DataSym]['#' + tag] = '#' + tag;
-                const tags = StrCast(this.Document[DataSym].tags, ':');
-                if (!tags.includes(`#${tag}:`)) {
-                    this.Document[DataSym].tags = `${tags + '#' + tag + ':'}`;
+                const tags = StrListCast(this.Document[DataSym].tags);
+                if (!tags.includes(tag)) {
+                    tags.push(tag);
+                    this.Document[DataSym].tags = new List<string>(tags);
                 }
                 const fieldView = state.schema.nodes.dashField.create({ fieldKey: '#' + tag });
                 return state.tr
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index a9be24c8b..03ae91c50 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1387,7 +1387,8 @@ export namespace Doc {
         if (typeof value === 'string') {
             value = value.replace(`,${Utils.noRecursionHack}`, '');
         }
-        const fieldVal = key === '#' ? (StrCast(doc.tags).includes(':#' + value + ':') ? StrCast(doc.tags) : undefined) : doc[key];
+        const tagsString = doc.tags ? ':' + StrListCast(doc.tags).join(':') + ':' : '';
+        const fieldVal = key === '#' ? (tagsString.includes(':#' + value + ':') ? tagsString : undefined) : doc[key];
         if (Cast(fieldVal, listSpec('string'), []).length) {
             const vals = Cast(fieldVal, listSpec('string'), []);
             const docs = vals.some(v => (v as any) instanceof Doc);
-- 
cgit v1.2.3-70-g09d2

