From f83e5d34794ef675d4627ecef2ed7042b17b1b06 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 18 Apr 2023 10:05:01 -0400
Subject: cleaning up zip/unzip of files

---
 src/client/views/nodes/DocumentView.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 7107707d1..6686f142f 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -693,7 +693,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this.props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
             !appearance && appearanceItems.length && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' });
 
-            if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && this.props.docViewPath().lastElement()?.rootDoc?._viewType !== CollectionViewType.Tree) {
+            if (!Doc.IsSystem(this.rootDoc) && this.rootDoc.type !== DocumentType.PRES && ![CollectionViewType.Docking, CollectionViewType.Tree].includes(this.rootDoc._viewType as any)) {
                 const existingOnClick = cm.findByDescription('OnClick...');
                 const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : [];
 
-- 
cgit v1.2.3-70-g09d2


From 9a968d6d96428172e0627e80ba551f5f2cbdbbe4 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 18 Apr 2023 12:32:13 -0400
Subject: fixed selection issues: shift does multiselect again, doc decoration
 background isn't drawn anymore for schema view, rotated objects in a
 multiselect don't cause artifacts,  clusters in freeform don't cause images
 to dim.

---
 src/client/util/DragManager.ts                                | 11 ++++++-----
 src/client/views/DocumentDecorations.tsx                      |  4 ++--
 .../collections/collectionFreeForm/CollectionFreeFormView.tsx |  4 ++--
 src/client/views/nodes/DocumentView.tsx                       |  2 +-
 4 files changed, 11 insertions(+), 10 deletions(-)

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

diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 7e6de5e67..404c85eb2 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -355,7 +355,7 @@ export namespace DragManager {
             top: Number.MAX_SAFE_INTEGER,
             bottom: Number.MIN_SAFE_INTEGER,
         };
-        let rot = 0;
+        let rot: number[] = [];
         const docsToDrag = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : [];
         const dragElements = eles.map(ele => {
             // bcz: very hacky -- if dragged element is a freeForm view with a rotation, then extract the rotation in order to apply it to the dragged element
@@ -363,9 +363,10 @@ export namespace DragManager {
             // if the parent isn't a freeform view, then the element's width and height are presumed to match the acutal doc's dimensions (eg, dragging from import sidebar menu)
             if (ele?.parentElement?.parentElement?.parentElement?.className === 'collectionFreeFormDocumentView-container') {
                 ele = ele.parentElement.parentElement.parentElement;
-                rot = Number(ele.style.transform.replace(/.*rotate\(([-0-9.e]*)deg\).*/, '$1') || 0);
+                rot.push(Number(ele.style.transform.replace(/.*rotate\(([-0-9.e]*)deg\).*/, '$1') || 0));
             } else {
                 useDim = true;
+                rot.push(0);
             }
             if (!ele.parentNode) dragDiv.appendChild(ele);
             const dragElement = ele.parentNode === dragDiv ? ele : (ele.cloneNode(true) as HTMLElement);
@@ -388,7 +389,7 @@ export namespace DragManager {
             const rect = ele.getBoundingClientRect();
             const w = ele.offsetWidth || rect.width;
             const h = ele.offsetHeight || rect.height;
-            const rotR = -((rot < 0 ? rot + 360 : rot) / 180) * Math.PI;
+            const rotR = -((rot.lastElement() < 0 ? rot.lastElement() + 360 : rot.lastElement()) / 180) * Math.PI;
             const tl = [0, 0];
             const tr = [Math.cos(rotR) * w, Math.sin(-rotR) * w];
             const bl = [Math.sin(rotR) * h, Math.cos(-rotR) * h];
@@ -422,7 +423,7 @@ export namespace DragManager {
                 transformOrigin: '0 0',
                 width,
                 height,
-                transform: `translate(${xs[0]}px, ${ys[0]}px) rotate(${rot}deg) scale(${scaling})`,
+                transform: `translate(${xs[0]}px, ${ys[0]}px) rotate(${rot.lastElement()}deg) scale(${scaling})`,
             });
             dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`;
 
@@ -563,7 +564,7 @@ export namespace DragManager {
             const moveVec = { x: x - lastPt.x, y: y - lastPt.y };
             lastPt = { x, y };
 
-            dragElements.map((dragElement, i) => (dragElement.style.transform = `translate(${(xs[i] += moveVec.x)}px, ${(ys[i] += moveVec.y)}px) rotate(${rot}deg)  scale(${scalings[i]})`));
+            dragElements.map((dragElement, i) => (dragElement.style.transform = `translate(${(xs[i] += moveVec.x)}px, ${(ys[i] += moveVec.y)}px) rotate(${rot[i]}deg)  scale(${scalings[i]})`));
             dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`;
         };
         const upHandler = (e: PointerEvent) => {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 9a4aaf00e..042e8f6d0 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -781,7 +781,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
 
         const useLock = bounds.r - bounds.x > 135 && seldocview.props.CollectionFreeFormDocumentView;
         const useRotation = !hideResizers && seldocview.rootDoc.type !== DocumentType.EQUATION && seldocview.props.CollectionFreeFormDocumentView; // when do we want an object to not rotate?
-        const rotation = NumCast(seldocview.rootDoc._rotation);
+        const rotation = SelectionManager.Views().length == 1 ? NumCast(seldocview.rootDoc._rotation) : 0;
 
         const resizerScheme = colorScheme ? 'documentDecorations-resizer' + colorScheme : '';
 
@@ -828,7 +828,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                         left: bounds.x - this._resizeBorderWidth / 2,
                         top: bounds.y - this._resizeBorderWidth / 2,
                         pointerEvents: DocumentDecorations.Instance.AddToSelection || this.Interacting ? 'none' : 'all',
-                        display: SelectionManager.Views().length <= 1 ? 'none' : undefined,
+                        display: SelectionManager.Views().length <= 1 || hideDecorations ? 'none' : undefined,
                     }}
                     onPointerDown={this.onBackgroundDown}
                     onContextMenu={e => {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 33fc2ddf3..ff0d01f29 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -565,7 +565,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         switch (property) {
             case StyleProp.BackgroundColor:
                 const cluster = NumCast(doc?.cluster);
-                if (this.Document._useClusters) {
+                if (this.Document._useClusters && doc?.type !== DocumentType.IMG) {
                     if (this._clusterSets.length <= cluster) {
                         setTimeout(() => doc && this.updateCluster(doc));
                     } else {
@@ -786,7 +786,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             e.stopPropagation();
             e.preventDefault();
         } else if (Math.abs(e.pageX - this._downX) < 3 && Math.abs(e.pageY - this._downY) < 3) {
-            if (e.shiftKey) {
+            if (e.shiftKey && (this.props.renderDepth === 0 || this.isContentActive())) {
                 if (Date.now() - this._lastTap < 300) {
                     // reset zoom of freeform view to 1-to-1 on a shift + double click
                     this.zoomSmoothlyAboutPt(this.getTransform().transformPoint(e.clientX, e.clientY), 1);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 6686f142f..dc508d95f 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -473,7 +473,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     preventDefault = false;
                 }
 
-                this._singleClickFunc = clickFunc ?? (() => (this._componentView?.select ?? this.props.select)(e.ctrlKey || e.metaKey, e.shiftKey));
+                this._singleClickFunc = clickFunc ?? (() => this._componentView?.select?.(e.ctrlKey || e.metaKey, e.shiftKey) ?? this.props.select(e.ctrlKey || e.metaKey || e.shiftKey));
                 const waitFordblclick = this.props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick;
                 if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') {
                     this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout);
-- 
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/nodes/DocumentView.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 4846ff09a02cce1da4f0b8984e387d7d204837f1 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 26 Apr 2023 10:58:58 -0400
Subject: fixed filters - checkboxes generated when options are less than 20,
 added -undefined- as distinct value option. fixed pointer evcents for docs on
 pdfs

---
 src/client/util/CurrentUserUtils.ts                |   7 +-
 src/client/util/DragManager.ts                     |  18 +-
 src/client/views/FilterPanel.scss                  |  12 +-
 src/client/views/FilterPanel.tsx                   |  41 +++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   1 +
 src/client/views/nodes/DocumentView.tsx            |   2 +
 src/client/views/nodes/FilterBox.scss              | 189 ---------------------
 src/client/views/nodes/FilterBox.tsx               |   0
 .../views/nodes/formattedText/FormattedTextBox.tsx |   6 +-
 src/client/views/pdf/PDFViewer.tsx                 |   3 +-
 src/fields/Doc.ts                                  |   2 +-
 11 files changed, 57 insertions(+), 224 deletions(-)
 delete mode 100644 src/client/views/nodes/FilterBox.scss
 delete mode 100644 src/client/views/nodes/FilterBox.tsx

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 4a90edd49..5475873a9 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -49,6 +49,7 @@ interface Button {
     ignoreClick?: boolean;
     buttonText?: string;
     backgroundColor?: string;
+    waitForDoubleClickToClick?: boolean;
 
     // fields that do not correspond to DocumentOption fields
     scripts?: { script?: string; onClick?: string; onDoubleClick?: string }
@@ -613,8 +614,9 @@ export class CurrentUserUtils {
 
     static freeTools(): Button[] {
         return [
-            { title: "Bottom",    icon: "arrows-down-to-line",toolTip: "Make doc topmost",    btnType: ButtonType.ClickButton,  expertMode: false,                        funcs: {}, scripts: { onClick: 'sendToBack()'}}, // Only when floating document is selected in freeform
-            { title: "Top",       icon: "arrows-up-to-line",  toolTip: "Make doc bottommost", btnType: ButtonType.ClickButton,  expertMode: false,                        funcs: {}, scripts: { onClick: 'bringToFront()'}}, // Only when floating document is selected in freeform
+            { title: "Bottom",  icon: "arrows-down-to-line",toolTip: "Make doc topmost",                                   btnType: ButtonType.ClickButton,  expertMode: false, funcs: {}, scripts: { onClick: 'sendToBack()'}}, // Only when floating document is selected in freeform
+            { title: "Top",     icon: "arrows-up-to-line",  toolTip: "Make doc bottommost",                                btnType: ButtonType.ClickButton,  expertMode: false, funcs: {}, scripts: { onClick: 'bringToFront()'}}, // Only when floating document is selected in freeform
+            { title: "Z order", icon: "z",                  toolTip: "Bring Forward on Drag (double click to set for all)",waitForDoubleClickToClick:true, btnType: ButtonType.ToggleButton, expertMode: false, funcs: {}, scripts: { onClick: 'toggleRaiseOnDrag(false, _readOnly_)', onDoubleClick:`{ return toggleRaiseOnDrag(true, _readOnly_)`}}, // Only when floating document is selected in freeform
         ]
     }
     static viewTools(): Button[] {
@@ -689,7 +691,6 @@ export class CurrentUserUtils {
             { title: "Fill",    icon: "fill-drip",     toolTip: "Background Fill Color",btnType: ButtonType.ColorButton,         expertMode: false,                                                                                                          ignoreClick: true, width: 20,  scripts: { script:  'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected
             { title: "Header",  icon: "heading",       toolTip: "Header Color",         btnType: ButtonType.ColorButton,         expertMode: false,                                                                                                          ignoreClick: true,             scripts: { script:  'return setHeaderColor(value, _readOnly_)'}},
             { title: "Overlay", icon: "layer-group",   toolTip: "Overlay",              btnType: ButtonType.ToggleButton,        expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'},       scripts: { onClick: 'toggleOverlay(_readOnly_)'}}, // Only when floating document is selected in freeform
-            { title: "Z order", icon: "z",             toolTip: "Bring Forward on Drag",btnType: ButtonType.ToggleButton,        expertMode: false, toolType:CollectionViewType.Freeform, funcs: {},                                                                                        scripts: { onClick: 'toggleRaiseOnDrag(_readOnly_)'}}, // Only when floating document is selected in freeform
             { title: "Back",    icon: "chevron-left",  toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton,         expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 20,  scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
             { title: "Num", icon:"",toolTip: "Frame Number (click to toggle edit mode)",btnType: ButtonType.TextButton,          expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
             { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,         expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 20,  scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 404c85eb2..b6de5604d 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -11,6 +11,7 @@ import * as globalCssVariables from '../views/global/globalCssVariables.scss';
 import { Colors } from '../views/global/globalEnums';
 import { DocumentView } from '../views/nodes/DocumentView';
 import { ScriptingGlobals } from './ScriptingGlobals';
+import { SelectionManager } from './SelectionManager';
 import { SnappingManager } from './SnappingManager';
 import { UndoManager } from './UndoManager';
 
@@ -597,7 +598,18 @@ export namespace DragManager {
     }
 }
 
-ScriptingGlobals.add(function toggleRaiseOnDrag(readOnly?: boolean) {
-    if (readOnly) return DragManager.GetRaiseWhenDragged() ? Colors.MEDIUM_BLUE : 'transparent';
-    DragManager.SetRaiseWhenDragged(!DragManager.GetRaiseWhenDragged());
+ScriptingGlobals.add(function toggleRaiseOnDrag(forAllDocs: boolean, readOnly?: boolean) {
+    if (readOnly) {
+        if (SelectionManager.Views().length)
+            return SelectionManager.Views().some(dv => dv.rootDoc.raiseWhenDragged)
+                ? Colors.MEDIUM_BLUE
+                : SelectionManager.Views().some(dv => dv.rootDoc.raiseWhenDragged === false)
+                ? 'transparent'
+                : DragManager.GetRaiseWhenDragged()
+                ? Colors.MEDIUM_BLUE_ALT
+                : Colors.PINK;
+        return DragManager.GetRaiseWhenDragged() ? Colors.PINK : 'transparent';
+    }
+    if (!forAllDocs) SelectionManager.Views().map(dv => (dv.rootDoc.raiseWhenDragged ? (dv.rootDoc.raiseWhenDragged = undefined) : dv.rootDoc.raiseWhenDragged === false ? (dv.rootDoc.raiseWhenDragged = true) : (dv.rootDoc.raiseWhenDragged = false)));
+    else DragManager.SetRaiseWhenDragged(!DragManager.GetRaiseWhenDragged());
 });
diff --git a/src/client/views/FilterPanel.scss b/src/client/views/FilterPanel.scss
index 7f907c8d4..c903f29ee 100644
--- a/src/client/views/FilterPanel.scss
+++ b/src/client/views/FilterPanel.scss
@@ -33,9 +33,10 @@
 // }
 
 .filterBox-select {
-    // width: 90%;
+    display: flex;
+    width: 100%;
     margin-top: 5px;
-    // margin-bottom: 15px;
+    background: white;
 }
 
 .filterBox-saveBookmark {
@@ -150,8 +151,8 @@
 .filterBox-treeView {
     display: flex;
     flex-direction: column;
-    width: 200px;
-    position: absolute;
+    width: 100%;
+    position: relative;
     right: 0;
     top: 0;
     z-index: 1;
@@ -184,6 +185,7 @@
         display: inline-block;
         width: 100%;
         margin-bottom: 10px;
-        //height: calc(100% - 30px);
+        margin-left: 5px;
+        overflow: auto;
     }
 }
diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx
index d35494f26..a237249c1 100644
--- a/src/client/views/FilterPanel.tsx
+++ b/src/client/views/FilterPanel.tsx
@@ -51,10 +51,12 @@ export class FilterPanel extends React.Component<filterProps> {
 
         const keys = new Set<string>(noviceFields);
         this.allDocs.forEach(doc => SearchBox.documentKeys(doc).filter(key => keys.add(key)));
-        return Array.from(keys.keys())
+        const sortedKeys = Array.from(keys.keys())
             .filter(key => key[0])
             .filter(key => key[0] === '#' || key.indexOf('lastModified') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('_')) || noviceFields.includes(key) || !Doc.noviceMode)
             .sort();
+        noviceFields.forEach(key => sortedKeys.splice(sortedKeys.indexOf(key), 1));
+        return [...noviceFields, ...sortedKeys];
     }
 
     /**
@@ -129,7 +131,7 @@ export class FilterPanel extends React.Component<filterProps> {
                 maxVal = Math.max(num, maxVal);
             }
         });
-        if (facetHeader === 'text' || (facetValues.rtFields / allCollectionDocs.length > 0.1 && facetValues.rtFields > 20)) {
+        if (facetHeader === 'text' || (facetValues.rtFields / allCollectionDocs.length > 0.1 && facetValues.strings.length > 20)) {
             this._chosenFacets.set(facetHeader, 'text');
         } else if (facetHeader !== 'tags' && nonNumbers / facetValues.strings.length < 0.1) {
         } else {
@@ -140,7 +142,7 @@ export class FilterPanel extends React.Component<filterProps> {
     facetValues = (facetHeader: string) => {
         const allCollectionDocs = new Set<Doc>();
         SearchBox.foreachRecursiveDoc(this.targetDocChildren, (depth: number, doc: Doc) => allCollectionDocs.add(doc));
-        const set = new Set<string>();
+        const set = new Set<string>([String.fromCharCode(127) + '--undefined--']);
         if (facetHeader === 'tags')
             allCollectionDocs.forEach(child =>
                 Field.toString(child[facetHeader] as Field)
@@ -158,32 +160,29 @@ export class FilterPanel extends React.Component<filterProps> {
         let nonNumbers = 0;
 
         facetValues.map(val => Number.isNaN(Number(val)) && nonNumbers++);
-        const facetValueDocSet = (nonNumbers / facetValues.length > 0.1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2))).map(facetValue => {
-            return facetValue;
-        });
-        return facetValueDocSet;
+        return nonNumbers / facetValues.length > 0.1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2));
     };
 
     render() {
         const options = this._allFacets.filter(facet => this.currentFacets.indexOf(facet) === -1).map(facet => ({ value: facet, label: facet }));
         return (
-            <div className="filterBox-treeView" style={{ position: 'relative', width: '100%' }}>
-                <div className="filterBox-select-bool">
-                    <select className="filterBox-selection" onChange={action(e => this.targetDoc && (this.targetDoc._filterBoolean = (e.target as any).value))} defaultValue={StrCast(this.targetDoc?.filterBoolean)}>
-                        {['AND', 'OR'].map(bool => (
-                            <option value={bool} key={bool}>
-                                {bool}
-                            </option>
-                        ))}
-                    </select>
-                    <div className="filterBox-select-text">filters together</div>
-                </div>
-
+            <div className="filterBox-treeView">
                 <div className="filterBox-select">
-                    <Select placeholder="Add a filter..." options={options} isMulti={false} onChange={val => this.facetClick((val as UserOptions).value)} onKeyDown={e => e.stopPropagation()} value={null} closeMenuOnSelect={true} />
+                    <div style={{ width: '100%' }}>
+                        <Select placeholder="Add a filter..." options={options} isMulti={false} onChange={val => this.facetClick((val as UserOptions).value)} onKeyDown={e => e.stopPropagation()} value={null} closeMenuOnSelect={true} />
+                    </div>
+                    <div className="filterBox-select-bool">
+                        <select className="filterBox-selection" onChange={action(e => this.targetDoc && (this.targetDoc._filterBoolean = (e.target as any).value))} defaultValue={StrCast(this.targetDoc?.filterBoolean)}>
+                            {['AND', 'OR'].map(bool => (
+                                <option value={bool} key={bool}>
+                                    {bool}
+                                </option>
+                            ))}
+                        </select>
+                    </div>{' '}
                 </div>
 
-                <div className="filterBox-tree" key="tree" style={{ overflow: 'auto' }}>
+                <div className="filterBox-tree" key="tree">
                     {Array.from(this.activeFacets.keys()).map(facetHeader => (
                         <div>
                             {facetHeader}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 3333befc6..719a39e8d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1972,6 +1972,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                             : SnappingManager.GetIsDragging() && this.childDocs.includes(DragManager.docsBeingDragged.lastElement())
                             ? 'all'
                             : (this.props.pointerEvents?.() as any),
+                    textAlign: this.isAnnotationOverlay ? 'initial' : undefined,
                     transform: `scale(${this.nativeDimScaling || 1})`,
                     width: `${100 / (this.nativeDimScaling || 1)}%`,
                     height: this.props.getScrollHeight?.() ?? `${100 / (this.nativeDimScaling || 1)}%`,
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 2ea0a6f19..38aa8db55 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -868,6 +868,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             ? true
             : false;
     };
+    docFilters = () => [...this.props.docFilters(), ...StrListCast(this.layoutDoc.docFilters)];
     contentPointerEvents = () => (!this.disableClickScriptFunc && this.onClickHandler ? 'none' : this.pointerEvents);
     @computed get contents() {
         TraceMobx();
@@ -898,6 +899,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     docViewPath={this.props.viewPath}
                     thumbShown={this.thumbShown}
                     setContentView={this.setContentView}
+                    docFilters={this.docFilters}
                     NativeDimScaling={this.props.NativeDimScaling}
                     PanelHeight={this.panelHeight}
                     setHeight={!this.props.suppressSetHeight ? this.setHeight : undefined}
diff --git a/src/client/views/nodes/FilterBox.scss b/src/client/views/nodes/FilterBox.scss
deleted file mode 100644
index 7f907c8d4..000000000
--- a/src/client/views/nodes/FilterBox.scss
+++ /dev/null
@@ -1,189 +0,0 @@
-.filterBox-flyout {
-    display: block;
-    text-align: left;
-    font-weight: 100;
-
-    .filterBox-flyout-facet {
-        background-color: white;
-        text-align: left;
-        display: inline-block;
-        position: relative;
-        width: 100%;
-
-        .filterBox-flyout-facet-check {
-            margin-right: 6px;
-        }
-    }
-}
-
-.filter-bookmark {
-    //display: flex;
-
-    .filter-bookmark-icon {
-        float: right;
-        margin-right: 10px;
-        margin-top: 7px;
-    }
-}
-
-// .filterBox-bottom {
-//     // position: fixed;
-//     // bottom: 0;
-//     // width: 100%;
-// }
-
-.filterBox-select {
-    // width: 90%;
-    margin-top: 5px;
-    // margin-bottom: 15px;
-}
-
-.filterBox-saveBookmark {
-    background-color: #e9e9e9;
-    border-radius: 11px;
-    padding-left: 8px;
-    padding-right: 8px;
-    padding-top: 5px;
-    padding-bottom: 5px;
-    margin: 8px;
-    display: flex;
-    font-size: 11px;
-    cursor: pointer;
-
-    &:hover {
-        background-color: white;
-    }
-
-    .filterBox-saveBookmark-icon {
-        margin-right: 6px;
-        margin-top: 4px;
-        margin-left: 2px;
-    }
-}
-
-.filterBox-select-scope,
-.filterBox-select-bool,
-.filterBox-addWrapper,
-.filterBox-select-matched,
-.filterBox-saveWrapper {
-    font-size: 10px;
-    justify-content: center;
-    justify-items: center;
-    padding-bottom: 10px;
-    display: flex;
-}
-
-.filterBox-addWrapper {
-    font-size: 11px;
-    width: 100%;
-}
-
-.filterBox-saveWrapper {
-    width: 100%;
-}
-
-// .filterBox-top {
-//     padding-bottom: 20px;
-//     border-bottom: 2px solid black;
-//     position: fixed;
-//     top: 0;
-//     width: 100%;
-// }
-
-.filterBox-select-scope {
-    padding-bottom: 20px;
-    border-bottom: 2px solid black;
-}
-
-.filterBox-title {
-    font-size: 15;
-    // border: 2px solid black;
-    width: 100%;
-    align-self: center;
-    text-align: center;
-    background-color: #d3d3d3;
-}
-
-.filterBox-select-bool {
-    margin-top: 6px;
-}
-
-.filterBox-select-text {
-    margin-right: 8px;
-    margin-left: 8px;
-    margin-top: 3px;
-}
-
-.filterBox-select-box {
-    margin-right: 2px;
-    font-size: 30px;
-    border: 0;
-    background: transparent;
-}
-
-.filterBox-selection {
-    border-radius: 6px;
-    border: none;
-    background-color: #e9e9e9;
-    padding: 2px;
-
-    &:hover {
-        background-color: white;
-    }
-}
-
-.filterBox-addFilter {
-    width: 120px;
-    background-color: #e9e9e9;
-    border-radius: 12px;
-    padding: 5px;
-    margin: 5px;
-    display: flex;
-    text-align: center;
-    justify-content: center;
-
-    &:hover {
-        background-color: white;
-    }
-}
-
-.filterBox-treeView {
-    display: flex;
-    flex-direction: column;
-    width: 200px;
-    position: absolute;
-    right: 0;
-    top: 0;
-    z-index: 1;
-    background-color: #9f9f9f;
-
-    .filterBox-tree {
-        z-index: 0;
-    }
-
-    .filterBox-addfacet {
-        display: inline-block;
-        width: 200px;
-        height: 30px;
-        text-align: left;
-
-        .filterBox-addFacetButton {
-            display: flex;
-            margin: auto;
-            cursor: pointer;
-        }
-
-        > div,
-        > div > div {
-            width: 100%;
-            height: 100%;
-        }
-    }
-
-    .filterBox-tree {
-        display: inline-block;
-        width: 100%;
-        margin-bottom: 10px;
-        //height: calc(100% - 30px);
-    }
-}
diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 82a26fa86..399e6bfdf 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -852,7 +852,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         const optionItems = options && 'subitems' in options ? options.subitems : [];
         optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' });
         optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' });
-        optionItems.push({ description: !this.Document._singleLine ? 'Make Single Line' : 'Make Multi Line', event: () => (this.layoutDoc._singleLine = !this.layoutDoc._singleLine), icon: !this.Document._singleLine ? 'grip-lines' : 'bars' });
+        optionItems.push({
+            description: !this.Document._singleLine ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns',
+            event: () => (this.layoutDoc._singleLine = !this.layoutDoc._singleLine),
+            icon: !this.Document._singleLine ? 'grip-lines' : 'bars',
+        });
         optionItems.push({ description: `${this.Document._autoHeight ? 'Lock' : 'Auto'} Height`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: this.Document._autoHeight ? 'lock' : 'unlock' });
         !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
         this._downX = this._downY = Number.NaN;
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 68241e61f..20803bba8 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -536,7 +536,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
                 NativeWidth={returnZero}
                 NativeHeight={returnZero}
                 setContentView={emptyFunction} // override setContentView to do nothing
-                pointerEvents={SnappingManager.GetIsDragging() ? returnAll : returnNone}
+                pointerEvents={SnappingManager.GetIsDragging() ? returnAll : returnNone} // freeform view doesn't get events unless something is being dragged onto it.
+                childPointerEvents={'all'} // but freeform children need to get events to allow text editing, etc
                 renderDepth={this.props.renderDepth + 1}
                 isAnnotationOverlay={true}
                 fieldKey={this.props.fieldKey + '-annotations'}
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index b0033b977..0c2ee35fd 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1391,7 +1391,7 @@ export namespace Doc {
             return vals.some(v => v.includes(value)); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
         }
         const fieldStr = Field.toString(fieldVal as Field);
-        return fieldStr.includes(value); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
+        return fieldStr.includes(value) || (value === String.fromCharCode(127) + '--undefined--' && fieldVal === undefined); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
     }
 
     export function deiconifyView(doc: Doc) {
-- 
cgit v1.2.3-70-g09d2


From 1150296b546bb3628c3dac5704150dfb4295434b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 26 Apr 2023 16:03:49 -0400
Subject: added cheatsheet and shortcut for viewing text shortcuts. changed
 menu organization slightly for viewability.

---
 src/client/util/RTFMarkup.tsx                      | 137 +++++++++++++++++++++
 src/client/util/ServerStats.tsx                    |   3 +-
 src/client/views/MainView.tsx                      |   8 +-
 src/client/views/MainViewModal.tsx                 |  37 +++---
 src/client/views/nodes/DocumentView.tsx            |   6 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   2 +
 .../views/nodes/formattedText/RichTextRules.ts     |   7 ++
 7 files changed, 177 insertions(+), 23 deletions(-)
 create mode 100644 src/client/util/RTFMarkup.tsx

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

diff --git a/src/client/util/RTFMarkup.tsx b/src/client/util/RTFMarkup.tsx
new file mode 100644
index 000000000..306d151d7
--- /dev/null
+++ b/src/client/util/RTFMarkup.tsx
@@ -0,0 +1,137 @@
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { MainViewModal } from '../views/MainViewModal';
+
+@observer
+export class RTFMarkup extends React.Component<{}> {
+    static Instance: RTFMarkup;
+    @observable private isOpen = false; // whether the SharingManager modal is open or not
+
+    // private get linkVisible() {
+    //     return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false;
+    // }
+
+    @action
+    public open = () => (this.isOpen = true);
+
+    @action
+    public close = () => (this.isOpen = false);
+
+    constructor(props: {}) {
+        super(props);
+        RTFMarkup.Instance = this;
+    }
+
+    @observable _stats: { [key: string]: any } | undefined;
+
+    /**
+     * @returns the main interface of the SharingManager.
+     */
+    @computed get cheatSheet() {
+        return (
+            <div style={{ textAlign: 'initial', height: '100%' }}>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`wiki:phrase`}</b>
+                    {` display wikipedia page for entered text (terminate with carriage return)`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`#tag `}</b>
+                    {`  add hashtag metadata to document. e.g, #idea`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`#, ## ... ###### `}</b>
+                    {`  set heading style based on number of '#'s between 1 and 6`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`#tag `}</b>
+                    {`  add hashtag metadata to document. e.g, #idea`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`>> `}</b>
+                    {`  add a sidebar text document inline`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`\`\` `}</b>
+                    {`  create a code snippet block`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`%% `}</b>
+                    {`  restore default styling`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`%color `}</b>
+                    {`  changes text color styling. e.g., %green.`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`%num `}</b>
+                    {`  set font size. e.g., %10 for 10pt font`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`%eq `}</b>
+                    {`  creates an equation block for typeset math`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`%alt `}</b>
+                    {`  switch between primary and alternate text (see bottom right Button for hover options).`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`%f `}</b>
+                    {`  create an inline footnote`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`%> `}</b>
+                    {`  create a bockquote section. Terminate with 2 carriage returns`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`%( `}</b>
+                    {`  start a section of inline elidable text. Terminate the inline text with %)`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`%q `}</b>
+                    {`  start a quoted block of text that’s indented on the left and right. Terminate with %q`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`%d `}</b>
+                    {`  start a block text where the first line is indented`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`%h `}</b>
+                    {`  start a block of text that begins with a hanging indent`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`%[ `}</b>
+                    {`  left justify text`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`%^ `}</b>
+                    {`  center text`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`%] `}</b>
+                    {`  right justify text`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`[:doctitle]] `}</b>
+                    {`  hyperlink to document specified by it’s title`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`[[fieldname]] `}</b>
+                    {`  display value of fieldname`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`[[fieldname=value]] `}</b>
+                    {`  assign value to fieldname of document and display it`}
+                </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`[[fieldname:doctitle]] `}</b>
+                    {`  show value of fieldname from doc specified by it’s title`}
+                </p>
+            </div>
+        );
+    }
+
+    render() {
+        return <MainViewModal contents={this.cheatSheet} isDisplayed={this.isOpen} interactive={true} closeOnExternalClick={this.close} />;
+    }
+}
diff --git a/src/client/util/ServerStats.tsx b/src/client/util/ServerStats.tsx
index 0ab411bff..f84ad8598 100644
--- a/src/client/util/ServerStats.tsx
+++ b/src/client/util/ServerStats.tsx
@@ -1,8 +1,7 @@
-import { action, computed, observable, runInAction } from 'mobx';
+import { action, computed, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { MainViewModal } from '../views/MainViewModal';
-import { DocumentView } from '../views/nodes/DocumentView';
 import './SharingManager.scss';
 
 @observer
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 43f56dd8c..5af9a9f9a 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -14,7 +14,7 @@ import { StrCast } from '../../fields/Types';
 import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../Utils';
 import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
 import { DocServer } from '../DocServer';
-import { Docs, DocUtils } from '../documents/Documents';
+import { Docs } from '../documents/Documents';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
 import { CaptureManager } from '../util/CaptureManager';
 import { DocumentManager } from '../util/DocumentManager';
@@ -22,11 +22,12 @@ import { GroupManager } from '../util/GroupManager';
 import { HistoryUtil } from '../util/History';
 import { Hypothesis } from '../util/HypothesisUtils';
 import { ReportManager } from '../util/ReportManager';
+import { RTFMarkup } from '../util/RTFMarkup';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
 import { SelectionManager } from '../util/SelectionManager';
+import { ServerStats } from '../util/ServerStats';
 import { ColorScheme, SettingsManager } from '../util/SettingsManager';
 import { SharingManager } from '../util/SharingManager';
-import { ServerStats } from '../util/ServerStats';
 import { SnappingManager } from '../util/SnappingManager';
 import { Transform } from '../util/Transform';
 import { TimelineMenu } from './animationtimeline/TimelineMenu';
@@ -41,7 +42,7 @@ import { DashboardView } from './DashboardView';
 import { DictationOverlay } from './DictationOverlay';
 import { DocumentDecorations } from './DocumentDecorations';
 import { GestureOverlay } from './GestureOverlay';
-import { TOPBAR_HEIGHT, LEFT_MENU_WIDTH } from './global/globalCssVariables.scss';
+import { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } from './global/globalCssVariables.scss';
 import { Colors } from './global/globalEnums';
 import { KeyManager } from './GlobalKeyHandler';
 import { InkTranscription } from './InkTranscription';
@@ -971,6 +972,7 @@ export class MainView extends React.Component {
                 <DictationOverlay />
                 <SharingManager />
                 <ServerStats />
+                <RTFMarkup />
                 <SettingsManager />
                 <ReportManager />
                 <CaptureManager />
diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx
index 55dee005d..32997a944 100644
--- a/src/client/views/MainViewModal.tsx
+++ b/src/client/views/MainViewModal.tsx
@@ -1,5 +1,5 @@
 import * as React from 'react';
-import "./MainViewModal.scss";
+import './MainViewModal.scss';
 import { observer } from 'mobx-react';
 
 export interface MainViewOverlayProps {
@@ -15,31 +15,38 @@ export interface MainViewOverlayProps {
 
 @observer
 export class MainViewModal extends React.Component<MainViewOverlayProps> {
-
     render() {
         const p = this.props;
         const dialogueOpacity = p.dialogueBoxDisplayedOpacity || 1;
         const overlayOpacity = p.overlayDisplayedOpacity || 0.4;
-        return !p.isDisplayed ? (null) : (
-            <div className="mainViewModal-cont" style={{
-                pointerEvents: p.isDisplayed && p.interactive ? "all" : "none"
-            }}>
-                <div className="dialogue-box" style={{
-                    borderColor: "black",
-                    ...(p.dialogueBoxStyle || {}),
-                    opacity: p.isDisplayed ? dialogueOpacity : 0
-                }} >
+        return !p.isDisplayed ? null : (
+            <div
+                className="mainViewModal-cont"
+                style={{
+                    pointerEvents: p.isDisplayed && p.interactive ? 'all' : 'none',
+                }}>
+                <div
+                    className="dialogue-box"
+                    style={{
+                        borderColor: 'black',
+                        height: 'max-content',
+                        overflow: 'auto',
+                        maxHeight: '80%',
+                        ...(p.dialogueBoxStyle || {}),
+                        opacity: p.isDisplayed ? dialogueOpacity : 0,
+                    }}>
                     {p.contents}
                 </div>
-                <div className="overlay"
+                <div
+                    className="overlay"
                     onClick={this.props?.closeOnExternalClick}
                     style={{
-                        backgroundColor: "black",
+                        backgroundColor: 'black',
                         ...(p.overlayStyle || {}),
-                        opacity: p.isDisplayed ? overlayOpacity : 0
+                        opacity: p.isDisplayed ? overlayOpacity : 0,
                     }}
                 />
             </div>
         );
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 38aa8db55..d58a4c199 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -767,6 +767,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         const constantItems: ContextMenuProps[] = [];
 
         if (!Doc.IsSystem(this.rootDoc)) {
+            constantItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(this.props.Document, (OpenWhere.addRight.toString() + 'KeyValue') as OpenWhere), icon: 'layer-group' });
             constantItems.push({ description: 'Export as Zip file', icon: 'download', event: async () => Doc.Zip(this.props.Document) });
             constantItems.push({ description: 'Import Zipped file', icon: 'upload', event: ({ x, y }) => this.importDocument() });
             (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' });
@@ -774,11 +775,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions)
                 constantItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' });
             }
-            cm.addItem({ description: 'General...', noexpand: false, subitems: constantItems, icon: 'question' });
+            cm.addItem({ description: 'General...', noexpand: true, subitems: constantItems, icon: 'question' });
         }
         const help = cm.findByDescription('Help...');
         const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : [];
-        helpItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(this.props.Document, (OpenWhere.addRight.toString() + 'KeyValue') as OpenWhere), icon: 'layer-group' });
         !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'keyboard' });
         !Doc.noviceMode && helpItems.push({ description: 'Print Document in Console', event: () => console.log(this.props.Document), icon: 'hand-point-right' });
         !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.props.Document[DataSym]), icon: 'hand-point-right' });
@@ -825,7 +825,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 icon: 'book',
             });
         }
-        if (!help) cm.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'question' });
+        if (!help) cm.addItem({ description: 'Help...', noexpand: !Doc.noviceMode, subitems: helpItems, icon: 'question' });
         else cm.moveAfter(help);
 
         e?.stopPropagation(); // DocumentViews should stop propagation of this event
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 399e6bfdf..0610d1e45 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -68,6 +68,7 @@ import { schema } from './schema_rts';
 import { SummaryView } from './SummaryView';
 import applyDevTools = require('prosemirror-dev-tools');
 import React = require('react');
+import { RTFMarkup } from '../../../util/RTFMarkup';
 const translateGoogleApi = require('translate-google-api');
 export const GoogleRef = 'googleDocId';
 type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void;
@@ -858,6 +859,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             icon: !this.Document._singleLine ? 'grip-lines' : 'bars',
         });
         optionItems.push({ description: `${this.Document._autoHeight ? 'Lock' : 'Auto'} Height`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: this.Document._autoHeight ? 'lock' : 'unlock' });
+        optionItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: 'text' });
         !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
         this._downX = this._downY = Number.NaN;
     };
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 599f96bca..20ce929ad 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -8,6 +8,7 @@ import { normalizeEmail } from '../../../../fields/util';
 import { Utils } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
 import { Docs, DocUtils } from '../../../documents/Documents';
+import { RTFMarkup } from '../../../util/RTFMarkup';
 import { FormattedTextBox } from './FormattedTextBox';
 import { wrappingInputRule } from './prosemirrorPatches';
 import { RichTextMenu } from './RichTextMenu';
@@ -228,6 +229,12 @@ export class RichTextRules {
                 return null;
             }),
 
+            // show markup help
+            new InputRule(new RegExp(/%\?$/), (state, match, start, end) => {
+                setTimeout(RTFMarkup.Instance.open);
+                return state.tr.deleteRange(start, end);
+            }),
+
             // stop using active style
             new InputRule(new RegExp(/%alt$/), (state, match, start, end) => {
                 setTimeout(() => (this.Document[this.TextBox.props.fieldKey + '-usePath'] = this.Document[this.TextBox.props.fieldKey + '-usePath'] ? undefined : 'alternate'));
-- 
cgit v1.2.3-70-g09d2


From d9cb66b5368a1d7d90b1874f928cf69ab62d976d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 26 Apr 2023 16:10:46 -0400
Subject: from last

---
 src/client/views/nodes/DocumentView.tsx | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

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

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index d58a4c199..c957bc778 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -717,7 +717,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                         event: undoBatch(action(() => (this.rootDoc._raiseWhenDragged = this.rootDoc._raiseWhenDragged === undefined ? false : undefined))),
                         icon: 'hand-point-up',
                     });
-                    !zorders && cm.addItem({ description: 'ZOrder...', noexpand: true, subitems: zorderItems, icon: 'compass' });
+                    !zorders && cm.addItem({ description: 'Z Order...', addDivider: true, noexpand: true, subitems: zorderItems, icon: 'compass' });
                 }
 
                 onClicks.push({ description: 'Enter Portal', event: this.makeIntoPortal, icon: 'window-restore' });
@@ -730,11 +730,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
 
                     onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' });
                     onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' });
-                    !existingOnClick && cm.addItem({ description: 'OnClick...', addDivider: true, noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
+                    !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
                 } else if (LinkManager.Links(this.Document).length) {
                     onClicks.push({ description: 'Select on Click', event: () => this.noOnClick(), icon: 'link' });
                     onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' });
-                    !existingOnClick && cm.addItem({ description: 'OnClick...', addDivider: true, subitems: onClicks, icon: 'mouse-pointer' });
+                    !existingOnClick && cm.addItem({ description: 'OnClick...', subitems: onClicks, icon: 'mouse-pointer' });
                 }
             }
 
-- 
cgit v1.2.3-70-g09d2


From 7342eb81f241ce0b2a5a33ddfb0c865eab6a492f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 27 Apr 2023 09:10:36 -0400
Subject: added proper scaling of multi-selections and groups.  fixed
 pinViewport to work again.

---
 src/client/util/CurrentUserUtils.ts                |   2 +-
 src/client/views/DocumentButtonBar.tsx             |   4 -
 src/client/views/DocumentDecorations.tsx           | 112 ++++++++++++---------
 src/client/views/collections/TabDocView.tsx        |   1 +
 .../collections/collectionFreeForm/MarqueeView.tsx |   5 +-
 src/client/views/nodes/DocumentView.tsx            |  10 +-
 src/client/views/nodes/button/FontIconBox.tsx      |   6 +-
 .../views/nodes/formattedText/RichTextRules.ts     |   2 +-
 src/client/views/nodes/formattedText/marks_rts.ts  |   2 +-
 src/client/views/nodes/trails/PresBox.tsx          |   1 +
 10 files changed, 80 insertions(+), 65 deletions(-)

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 5475873a9..479fcd011 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -621,8 +621,8 @@ 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: "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: "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
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 8c2ced55a..30e41b06c 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -303,10 +303,6 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                             onPointerEnter={action(e => (this.subEndLink = (pinLayout ? 'Layout' : '') + (pinLayout && pinContent ? ' &' : '') + (pinContent ? ' Content' : '')))}
                             onPointerLeave={action(e => (this.subEndLink = ''))}
                             onClick={e => {
-                                const docs = this.props
-                                    .views()
-                                    .filter(v => v)
-                                    .map(dv => dv!.rootDoc);
                                 this.view0 &&
                                     DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.view0.props.Document, true, this.view0, {
                                         pinDocLayout: pinLayout,
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 042e8f6d0..2811c96eb 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -12,7 +12,7 @@ import { InkField } from '../../fields/InkField';
 import { ScriptField } from '../../fields/ScriptField';
 import { Cast, NumCast, StrCast } from '../../fields/Types';
 import { GetEffectiveAcl } from '../../fields/util';
-import { emptyFunction, numberValue, returnFalse, setupMoveUpEvents, Utils } from '../../Utils';
+import { aggregateBounds, emptyFunction, numberValue, returnFalse, setupMoveUpEvents, Utils } from '../../Utils';
 import { Docs } from '../documents/Documents';
 import { DocumentType } from '../documents/DocumentTypes';
 import { DragManager } from '../util/DragManager';
@@ -34,6 +34,7 @@ import React = require('react');
 import { RichTextField } from '../../fields/RichTextField';
 import { LinkFollower } from '../util/LinkFollower';
 import _ = require('lodash');
+import { DocumentManager } from '../util/DocumentManager';
 
 @observer
 export class DocumentDecorations extends React.Component<{ PanelWidth: number; PanelHeight: number; boundsLeft: number; boundsTop: number }, { value: string }> {
@@ -509,73 +510,86 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
             dragRight = false,
             dragBotRight = false,
             dragTop = false;
-        let dX = 0,
-            dY = 0,
-            dW = 0,
-            dH = 0;
+        let dXin = 0,
+            dYin = 0,
+            dWin = 0,
+            dHin = 0;
         switch (this._resizeHdlId.split(' ')[0]) {
             case '':
                 break;
             case 'documentDecorations-topLeftResizer':
-                dX = -1;
-                dY = -1;
-                dW = -move[0];
-                dH = -move[1];
+                dXin = -1;
+                dYin = -1;
+                dWin = -move[0];
+                dHin = -move[1];
                 break;
             case 'documentDecorations-topRightResizer':
-                dW = move[0];
-                dY = -1;
-                dH = -move[1];
+                dWin = move[0];
+                dYin = -1;
+                dHin = -move[1];
                 break;
             case 'documentDecorations-topResizer':
-                dY = -1;
-                dH = -move[1];
+                dYin = -1;
+                dHin = -move[1];
                 dragTop = true;
                 break;
             case 'documentDecorations-bottomLeftResizer':
-                dX = -1;
-                dW = -move[0];
-                dH = move[1];
+                dXin = -1;
+                dWin = -move[0];
+                dHin = move[1];
                 break;
             case 'documentDecorations-bottomRightResizer':
-                dW = move[0];
-                dH = move[1];
+                dWin = move[0];
+                dHin = move[1];
                 dragBotRight = true;
                 break;
             case 'documentDecorations-bottomResizer':
-                dH = move[1];
+                dHin = move[1];
                 dragBottom = true;
                 break;
             case 'documentDecorations-leftResizer':
-                dX = -1;
-                dW = -move[0];
+                dXin = -1;
+                dWin = -move[0];
                 break;
             case 'documentDecorations-rightResizer':
-                dW = move[0];
+                dWin = move[0];
                 dragRight = true;
                 break;
         }
 
-        SelectionManager.Views().forEach(
+        const isGroup = first.rootDoc._isGroup ? first.rootDoc : undefined;
+        const scaleViews = isGroup ? DocListCast(isGroup.data).map(doc => DocumentManager.Instance.getFirstDocumentView(doc)!) : SelectionManager.Views();
+        const aggBounds = aggregateBounds(scaleViews.map(view => view.rootDoc) as any, 0, 0);
+        const refWidth = aggBounds.r - aggBounds.x;
+        const refHeight = aggBounds.b - aggBounds.y;
+        const scaleRefPt = first.props
+            .ScreenToLocalTransform()
+            .inverse()
+            .transformPoint(
+                NumCast(isGroup?._xPadding) + (dXin ? refWidth : 0), //
+                NumCast(isGroup?._yPadding) + (dYin ? refHeight : 0)
+            );
+        scaleViews.forEach(
             action((docView: DocumentView) => {
                 if (e.ctrlKey && !Doc.NativeHeight(docView.props.Document)) docView.toggleNativeDimensions();
-                if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
-                    const doc = Document(docView.rootDoc);
+                if (dXin !== 0 || dYin !== 0 || dWin !== 0 || dHin !== 0) {
+                    const doc = docView.rootDoc;
+                    const refCent = docView.props.ScreenToLocalTransform().transformPoint(scaleRefPt[0], scaleRefPt[1]);
+
                     if (doc.nativeHeightUnfrozen && !NumCast(doc.nativeHeight)) {
                         doc._nativeHeight = (NumCast(doc._height) / NumCast(doc._width, 1)) * docView.nativeWidth;
                     }
                     const nwidth = docView.nativeWidth;
                     const nheight = docView.nativeHeight;
-                    let docheight = doc._height || 0;
-                    let docwidth = doc._width || 0;
-                    const width = docwidth;
-                    let height = docheight || (nheight / nwidth) * width;
-                    height = !height || isNaN(height) ? 20 : height;
+                    const docwidth = NumCast(doc._width);
+                    let docheight = (hgt => (!hgt || isNaN(hgt) ? 20 : hgt))(NumCast(doc._height) || (nheight / nwidth) * docwidth);
+                    let dW = docwidth * (dWin / refWidth);
+                    let dH = docheight * (dHin / refHeight);
                     const scale = docView.props.ScreenToLocalTransform().Scale;
                     const modifyNativeDim = (e.ctrlKey || doc.forceReflow) && doc.nativeDimModifiable && ((!dragBottom && !dragTop) || e.ctrlKey || doc.nativeHeightUnfrozen);
                     if (nwidth && nheight) {
-                        if (nwidth / nheight !== width / height && !dragBottom && !dragTop) {
-                            height = (nheight / nwidth) * width;
+                        if (nwidth / nheight !== docwidth / docheight && !dragBottom && !dragTop) {
+                            docheight = (nheight / nwidth) * docwidth;
                         }
                         if (modifyNativeDim && !dragBottom && !dragTop) {
                             // ctrl key enables modification of the nativeWidth or nativeHeight durin the interaction
@@ -583,21 +597,25 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                             else dW = (dH * nwidth) / nheight;
                         }
                     }
-                    let actualdW = Math.max(width + dW * scale, 20);
-                    let actualdH = Math.max(height + dH * scale, 20);
+                    let actualdW = Math.max(docwidth + dW * scale, 20);
+                    let actualdH = Math.max(docheight + dH * scale, 20);
+                    let dX = !dWin ? 0 : scale * refCent[0] * (1 - (1 + dWin / refWidth));
+                    let dY = !dHin ? 0 : scale * refCent[1] * (1 - (1 + dHin / refHeight));
                     const preserveNativeDim = doc._nativeHeightUnfrozen === false && doc._nativeDimModifiable === false;
                     const fixedAspect = nwidth && nheight && (!doc._fitWidth || preserveNativeDim || e.ctrlKey || doc.nativeHeightUnfrozen || doc.nativeDimModifiable);
                     if (fixedAspect) {
                         if ((Math.abs(dW) > Math.abs(dH) && ((!dragBottom && !dragTop) || !modifyNativeDim)) || dragRight) {
                             if (dragRight && modifyNativeDim) {
                                 if (Doc.NativeWidth(doc)) {
-                                    doc._nativeWidth = (actualdW / (doc._width || 1)) * Doc.NativeWidth(doc);
+                                    doc._nativeWidth = (actualdW / (docwidth || 1)) * Doc.NativeWidth(doc);
                                 }
                             } else {
                                 if (!doc._fitWidth || preserveNativeDim) {
                                     actualdH = (nheight / nwidth) * actualdW;
                                     doc._height = actualdH;
-                                } else if (!modifyNativeDim || dragBotRight) doc._height = actualdH;
+                                } else if (!modifyNativeDim || dragBotRight) {
+                                    doc._height = actualdH;
+                                }
                             }
                             doc._width = actualdW;
                         } else {
@@ -605,21 +623,23 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                 // frozen web pages, PDFs, and some RTFS have frozen nativewidth/height.  But they are marked to allow their nativeHeight
                                 // to be explicitly modified with fitWidth and vertical resizing. (ie, with fitWidth they can't grow horizontally to match
                                 // a vertical resize so it makes more sense to change their nativeheight even if the ctrl key isn't used)
-                                doc._nativeHeight = (actualdH / (doc._height || 1)) * Doc.NativeHeight(doc);
+                                doc._nativeHeight = (actualdH / (docheight || 1)) * Doc.NativeHeight(doc);
                                 doc._autoHeight = false;
                             } else {
                                 if (!doc._fitWidth || preserveNativeDim) {
                                     actualdW = (nwidth / nheight) * actualdH;
                                     doc._width = actualdW;
-                                } else if (!modifyNativeDim || dragBotRight) doc._width = actualdW;
+                                } else if (!modifyNativeDim || dragBotRight) {
+                                    doc._width = actualdW;
+                                }
                             }
                             if (!modifyNativeDim) {
-                                actualdH = Math.min((nheight / nwidth) * NumCast(doc._width), actualdH);
-                                doc._height = actualdH;
-                            } else doc._height = actualdH;
+                                actualdH = Math.min((nheight / nwidth) * docwidth, actualdH);
+                            }
+                            doc._height = actualdH;
                         }
                     } else {
-                        const rotCtr = [NumCast(doc._width) / 2, NumCast(doc._height) / 2];
+                        const rotCtr = [docwidth / 2, docheight / 2];
                         const tlRotated = Utils.rotPt(-rotCtr[0], -rotCtr[1], (NumCast(doc._rotation) / 180) * Math.PI);
 
                         const maxHeight = doc.nativeHeightUnfrozen || !nheight ? 0 : Math.max(nheight, NumCast(doc.scrollHeight, NumCast(doc[docView.LayoutFieldKey + '-scrollHeight']))) * docView.NativeDimScaling();
@@ -632,8 +652,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                         doc.x = NumCast(doc.x) + tlRotated.x + rotCtr[0] - (tlRotated2.x + rotCtr2[0]); // doc shifts by amount topleft moves because rotation is about center of doc
                         doc.y = NumCast(doc.y) + tlRotated.y + rotCtr[1] - (tlRotated2.y + rotCtr2[1]);
                     }
-                    doc.x = (doc.x || 0) + dX * (actualdW - docwidth);
-                    doc.y = (doc.y || 0) + (dragBottom ? 0 : dY * (actualdH - docheight));
+                    doc.x = NumCast(doc.x) + dX;
+                    doc.y = NumCast(doc.y) + dY;
                     doc._lastModified = new DateField();
                 }
                 const val = this._dragHeights.get(docView.layoutDoc);
@@ -726,7 +746,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         }
         // hide the decorations if the parent chooses to hide it or if the document itself hides it
         const hideDecorations = seldocview.props.hideDecorations || seldocview.rootDoc.hideDecorations;
-        const hideResizers = hideDecorations || seldocview.props.hideResizeHandles || seldocview.rootDoc.hideResizeHandles || seldocview.rootDoc._isGroup || this._isRounding || this._isRotating;
+        const hideResizers = hideDecorations || seldocview.props.hideResizeHandles || seldocview.rootDoc.hideResizeHandles || this._isRounding || this._isRotating;
         const hideTitle = hideDecorations || seldocview.props.hideDecorationTitle || seldocview.rootDoc.hideDecorationTitle || this._isRounding || this._isRotating;
         const hideDocumentButtonBar = hideDecorations || seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.hideDocumentButtonBar || this._isRounding || this._isRotating;
         // if multiple documents have been opened at the same time, then don't show open button
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 4bbc3bb44..45604c1bf 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -266,6 +266,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
             pinDoc.treeViewHideHeaderIfTemplate = true; // this will force the document to render itself as the tree view header
             const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], null);
 
+            if (pinProps.pinViewport) PresBox.pinDocView(pinDoc, pinProps, anchorDoc ?? doc);
             if (!pinProps?.audioRange && duration !== undefined) {
                 pinDoc.mediaStart = 'manual';
                 pinDoc.mediaStop = 'manual';
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index eaeb5f933..e5f47823c 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -425,9 +425,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
      */
     @undoBatch
     @action
-    pinWithView = async () => {
-        const doc = this.props.Document;
-        TabDocView.PinDoc(doc, { pinViewport: this.Bounds });
+    pinWithView = () => {
+        TabDocView.PinDoc(this.props.Document, { pinViewport: this.Bounds });
         MarqueeOptionsMenu.Instance.fadeOut(true);
         this.hideMarquee();
     };
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c957bc778..a25e5c42d 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -765,9 +765,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' });
         }
         const constantItems: ContextMenuProps[] = [];
-
+        constantItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(this.props.Document, (OpenWhere.addRight.toString() + 'KeyValue') as OpenWhere), icon: 'layer-group' });
         if (!Doc.IsSystem(this.rootDoc)) {
-            constantItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(this.props.Document, (OpenWhere.addRight.toString() + 'KeyValue') as OpenWhere), icon: 'layer-group' });
             constantItems.push({ description: 'Export as Zip file', icon: 'download', event: async () => Doc.Zip(this.props.Document) });
             constantItems.push({ description: 'Import Zipped file', icon: 'upload', event: ({ x, y }) => this.importDocument() });
             (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' });
@@ -775,8 +774,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions)
                 constantItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' });
             }
-            cm.addItem({ description: 'General...', noexpand: true, subitems: constantItems, icon: 'question' });
         }
+        cm.addItem({ description: 'General...', noexpand: !Doc.IsSystem(this.rootDoc), subitems: constantItems, icon: 'question' });
+
         const help = cm.findByDescription('Help...');
         const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : [];
         !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'keyboard' });
@@ -819,9 +819,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         if (documentationDescription && documentationLink) {
             helpItems.push({
                 description: documentationDescription,
-                event: () => {
-                    window.open(documentationLink, '_blank');
-                },
+                event: () => window.open(documentationLink, '_blank'),
                 icon: 'book',
             });
         }
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 1a75a7e76..4f570b5fc 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -610,16 +610,16 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
     selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log('[FontIconBox.tsx] toggleOverlay failed');
 });
 
-ScriptingGlobals.add(function showFreeform(attr: 'grid' | 'snap lines' | 'clusters' | 'arrange' | 'viewAll', checkResult?: boolean) {
+ScriptingGlobals.add(function showFreeform(attr: 'grid' | 'snapline' | 'clusters' | 'arrange' | 'viewAll', checkResult?: boolean) {
     const selected = SelectionManager.Docs().lastElement();
     // prettier-ignore
-    const map: Map<'grid' | 'snap lines' | 'clusters' | 'arrange'| 'viewAll', { undo: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([
+    const map: Map<'grid' | 'snapline' | 'clusters' | 'arrange'| 'viewAll', { undo: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([
         ['grid', {
             undo: false,
             checkResult: (doc:Doc) => doc._backgroundGridShow,
             setDoc: (doc:Doc) => doc._backgroundGridShow = !doc._backgroundGridShow,
         }],
-        ['snap lines', {
+        ['snapline', {
             undo: false,
             checkResult: (doc:Doc) => doc.showSnapLines,
             setDoc: (doc:Doc) => doc._showSnapLines = !doc._showSnapLines,
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 20ce929ad..e5943f257 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -391,7 +391,7 @@ export class RichTextRules {
                 const content = selected.selection.content();
                 const replaced = node ? selected.replaceRangeWith(start, end, schema.nodes.summary.create({ visibility: true, text: content, textslice: content.toJSON() })) : state.tr;
 
-                return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1))).setStoredMarks([...node.marks, ...sm]);
+                return replaced.setSelection(new TextSelection(replaced.doc.resolve(end))).setStoredMarks([...node.marks, ...sm]);
             }),
 
             new InputRule(new RegExp(/%\)/), (state, match, start, end) => {
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index 3898490d3..5b47e8a70 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -349,7 +349,7 @@ export const marks: { [index: string]: MarkSpec } = {
         group: 'inline',
         toDOM(node: any) {
             const uid = node.attrs.userid.replace('.', '').replace('@', '');
-            const min = Math.round(node.attrs.modified / 12);
+            const min = Math.round(node.attrs.modified / 60);
             const hr = Math.round(min / 60);
             const day = Math.round(hr / 60 / 24);
             const remote = node.attrs.userid !== Doc.CurrentUserEmail ? ' UM-remote' : '';
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index f8c47aafe..0b780f589 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -567,6 +567,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 bestTarget._panY = viewport.panY;
                 const dv = DocumentManager.Instance.getDocumentView(bestTarget);
                 if (dv) {
+                    changed = true;
                     const computedScale = NumCast(activeItem.presZoom, 1) * Math.min(dv.props.PanelWidth() / viewport.width, dv.props.PanelHeight() / viewport.height);
                     activeItem.presMovement === PresMovement.Zoom && (bestTarget._viewScale = computedScale);
                     dv.ComponentView?.brushView?.(viewport);
-- 
cgit v1.2.3-70-g09d2


From ae038471f7eda76ecd985e277dda2dc6d27a3be7 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 28 Apr 2023 10:57:37 -0400
Subject: fixed open in lightbox from doc decorations.  fixed resizing text
 docs that are not fitWidth => artifact is that pdf's don't preserve aspect
 until they are resize vertically to establish a nativeHeight unless they are
 created with nativeHeight set.

---
 src/client/views/DocumentDecorations.tsx                   | 14 +++++++-------
 src/client/views/collections/CollectionTimeView.tsx        |  2 --
 .../collectionFreeForm/CollectionFreeFormView.tsx          |  5 ++---
 src/client/views/nodes/DocumentView.tsx                    |  3 +--
 4 files changed, 10 insertions(+), 14 deletions(-)

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

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 2811c96eb..85d36dbf8 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -35,6 +35,7 @@ import { RichTextField } from '../../fields/RichTextField';
 import { LinkFollower } from '../util/LinkFollower';
 import _ = require('lodash');
 import { DocumentManager } from '../util/DocumentManager';
+import { isUndefined } from 'lodash';
 
 @observer
 export class DocumentDecorations extends React.Component<{ PanelWidth: number; PanelHeight: number; boundsLeft: number; boundsTop: number }, { value: string }> {
@@ -289,12 +290,11 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                     openDoc = DocListCast(openDoc.aliases).find(alias => !alias.context) ?? Doc.MakeAlias(openDoc);
                     Doc.deiconifyView(openDoc);
                 }
-                selectedDocs[0].props.addDocTab(openDoc, OpenWhere.lightbox);
-                // LightboxView.SetLightboxDoc(
-                //     openDoc,
-                //     undefined,
-                //     selectedDocs.slice(1).map(view => view.props.Document)
-                // );
+                LightboxView.SetLightboxDoc(
+                    openDoc,
+                    undefined,
+                    selectedDocs.slice(1).map(view => view.props.Document)
+                );
             }
         }
         SelectionManager.DeselectAll();
@@ -576,7 +576,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                     const doc = docView.rootDoc;
                     const refCent = docView.props.ScreenToLocalTransform().transformPoint(scaleRefPt[0], scaleRefPt[1]);
 
-                    if (doc.nativeHeightUnfrozen && !NumCast(doc.nativeHeight)) {
+                    if (doc.nativeHeightUnfrozen && !NumCast(doc.nativeHeight) && doc._nativeWidth !== undefined) {
                         doc._nativeHeight = (NumCast(doc._height) / NumCast(doc._width, 1)) * docView.nativeWidth;
                     }
                     const nwidth = docView.nativeWidth;
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 03c010703..d0f3f2ca5 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -141,7 +141,6 @@ export class CollectionTimeView extends CollectionSubView() {
         }
     };
 
-    dontScaleFilter = (doc: Doc) => doc.type === DocumentType.RTF;
     @computed get contents() {
         return (
             <div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this.props.isContentActive() ? undefined : 'none' }} onClick={this.contentsDown}>
@@ -151,7 +150,6 @@ export class CollectionTimeView extends CollectionSubView() {
                     fitContentsToBox={returnTrue}
                     childClickScript={this._childClickedScript}
                     viewDefDivClick={this.layoutEngine() === computeTimelineLayout.name ? undefined : this._viewDefDivClick}
-                    //dontScaleFilter={this.dontScaleFilter}
                     layoutEngine={this.layoutEngine}
                 />
             </div>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 719a39e8d..bab42e560 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -64,7 +64,6 @@ export type collectionFreeformViewProps = {
     noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale)
     engineProps?: any;
     getScrollHeight?: () => number | undefined;
-    dontScaleFilter?: (doc: Doc) => boolean; // whether this collection should scale documents to fit their panel vs just scrolling them
     dontRenderDocuments?: boolean; // used for annotation overlays which need to distribute documents into different freeformviews with different mixBlendModes depending on whether they are transparent or not.
     // However, this screws up interactions since only the top layer gets events.  so we render the freeformview a 3rd time with all documents in order to get interaction events (eg., marquee) but we don't actually want to display the documents.
 };
@@ -1296,7 +1295,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 dropAction={StrCast(this.props.Document.childDropAction) as dropActionType}
                 bringToFront={this.bringToFront}
                 showTitle={this.props.childShowTitle}
-                dontScaleFilter={this.props.dontScaleFilter}
                 dontRegisterView={this.props.dontRenderDocuments || this.props.dontRegisterView}
                 pointerEvents={this.pointerEvents}
                 //rotation={this.props.styleProvider?.(childLayout, this.props, StyleProp.JitterRotation) || 0}
@@ -1328,7 +1326,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 if (this.layoutDoc._isLightbox) {
                     this._lightboxDoc = doc;
                     return true;
-                } else if (this.childDocList?.includes(doc)) {
+                }
+                if (this.childDocList?.includes(doc)) {
                     if (doc.hidden) doc.hidden = false;
                     return true;
                 }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a25e5c42d..4876ef300 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -215,7 +215,6 @@ export interface DocumentViewProps extends DocumentViewSharedProps {
     radialMenu?: String[];
     LayoutTemplateString?: string;
     dontCenter?: 'x' | 'y' | 'xy';
-    dontScaleFilter?: (doc: Doc) => boolean; // decides whether a document can be scaled to fit its container vs native size with scrolling
     NativeWidth?: () => number;
     NativeHeight?: () => number;
     NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NOTE: Must also be added to FieldViewProps
@@ -1341,7 +1340,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, !this.fitWidth));
     }
     @computed get shouldNotScale() {
-        return (this.fitWidth && !this.nativeWidth) || this.props.dontScaleFilter?.(this.Document) || [CollectionViewType.Docking].includes(this.Document._viewType as any);
+        return (this.fitWidth && !this.nativeWidth) || [CollectionViewType.Docking].includes(this.Document._viewType as any);
     }
     @computed get effectiveNativeWidth() {
         return this.shouldNotScale ? 0 : this.nativeWidth || NumCast(this.layoutDoc.width);
-- 
cgit v1.2.3-70-g09d2


From 2660a8b454ec86250fc7679df95f76441fb84aad Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 2 May 2023 12:37:04 -0400
Subject: fixed ink erasing when stroke intersects at endpoint.  attempted
 performance improvement as well.  bug fix for selecting strokes that are not
 active.

---
 src/client/views/GestureOverlay.tsx                | 12 ++++++------
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 22 ++++++++++++++++++++--
 src/client/views/nodes/DocumentView.tsx            |  4 +++-
 3 files changed, 29 insertions(+), 9 deletions(-)

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

diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 13faae783..b04769c1e 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -658,9 +658,9 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                                 }
                                 possibilities.push(...wR?.alternates?.map((a: any) => a.recognizedString));
                             }
-                            const r = Math.max(this.svgBounds.right, ...this._strokes.map(s => this.getBounds(s).right));
-                            const l = Math.min(this.svgBounds.left, ...this._strokes.map(s => this.getBounds(s).left));
-                            const t = Math.min(this.svgBounds.top, ...this._strokes.map(s => this.getBounds(s).top));
+                            const r = Math.max(this.svgBounds.right, ...this._strokes.map(s => GestureOverlay.getBounds(s).right));
+                            const l = Math.min(this.svgBounds.left, ...this._strokes.map(s => GestureOverlay.getBounds(s).left));
+                            const t = Math.min(this.svgBounds.top, ...this._strokes.map(s => GestureOverlay.getBounds(s).top));
 
                             // if we receive any word results from cognitive services, display them
                             runInAction(() => {
@@ -891,7 +891,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                     detail: {
                         points,
                         gesture,
-                        bounds: this.getBounds(points),
+                        bounds: GestureOverlay.getBounds(points),
                         text,
                     },
                 })
@@ -899,7 +899,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
         );
     };
 
-    getBounds = (stroke: InkData, pad?: boolean) => {
+    public static getBounds = (stroke: InkData, pad?: boolean) => {
         const padding = pad ? [-20000, 20000] : [];
         const xs = [...padding, ...stroke.map(p => p.X)];
         const ys = [...padding, ...stroke.map(p => p.Y)];
@@ -911,7 +911,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
     };
 
     @computed get svgBounds() {
-        return this.getBounds(this._points);
+        return GestureOverlay.getBounds(this._points);
     }
 
     @computed get elements() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index bab42e560..9cc732008 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -821,6 +821,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         this._lastY = e.clientY;
     };
 
+    _eraserLock = 0;
     /**
      * Erases strokes by intersecting them with an invisible "eraser stroke".
      *  By default this iterates through all intersected ink strokes, determines their segmentation, draws back the non-intersected segments,
@@ -830,6 +831,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @action
     onEraserMove = (e: PointerEvent, down: number[], delta: number[]) => {
         const currPoint = { X: e.clientX, Y: e.clientY };
+        if (this._eraserLock) return false; // bcz: should be fixed by putting it on a queue to be processed after the last eraser movement is processed.
         this.getEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint).forEach(intersect => {
             if (!this._deleteList.includes(intersect.inkView)) {
                 this._deleteList.push(intersect.inkView);
@@ -837,12 +839,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 SetActiveInkColor(StrCast(intersect.inkView.rootDoc.color?.toString()) || 'black');
                 // create a new curve by appending all curves of the current segment together in order to render a single new stroke.
                 if (!e.shiftKey) {
+                    this._eraserLock++;
                     this.segmentInkStroke(intersect.inkView, intersect.t).forEach(segment =>
-                        GestureOverlay.Instance.dispatchGesture(
+                        this.forceStrokeGesture(
+                            e,
                             GestureUtils.Gestures.Stroke,
                             segment.reduce((data, curve) => [...data, ...curve.points.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[])
                         )
                     );
+                    setTimeout(() => this._eraserLock--);
                 }
                 // Lower ink opacity to give the user a visual indicator of deletion.
                 intersect.inkView.layoutDoc.opacity = 0.5;
@@ -851,6 +856,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         });
         return false;
     };
+    forceStrokeGesture = (e: PointerEvent, gesture: GestureUtils.Gestures, points: InkData, text?: any) => {
+        this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, GestureOverlay.getBounds(points), text));
+    };
 
     @action
     onPointerMove = (e: PointerEvent): boolean => {
@@ -929,7 +937,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     const docCurveTVal = t + Math.floor(i / 4);
                     if (excludeT < startSegmentT || excludeT > docCurveTVal) {
                         const localStartTVal = startSegmentT - Math.floor(i / 4);
-                        segment.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, t));
+                        t !== (localStartTVal < 0 ? 0 : localStartTVal) && segment.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, t));
                         segment.length && segments.push(segment);
                     }
                     // start a new segment from the intersection t value
@@ -979,11 +987,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     if (ink?.Document === otherInk.props.Document && neighboringSegment) continue;
 
                     const otherCurve = new Bezier(otherCtrlPts.slice(j, j + 4).map(p => ({ x: p.X, y: p.Y })));
+                    const c0 = otherCurve.get(0);
+                    const c1 = otherCurve.get(1);
+                    const apt = curve.project(c0);
+                    const bpt = curve.project(c1);
+                    if (apt.d !== undefined && apt.d < 1 && apt.t !== undefined && !tVals.includes(apt.t)) {
+                        tVals.push(apt.t);
+                    }
                     this.bintersects(curve, otherCurve).forEach((val: string | number, i: number) => {
                         // Converting the Bezier.js Split type to a t-value number.
                         const t = +val.toString().split('/')[0];
                         if (i % 2 === 0 && !tVals.includes(t)) tVals.push(t); // bcz: Hack!  don't know why but intersection points are doubled from bezier.js (but not identical).
                     });
+                    if (bpt.d !== undefined && bpt.d < 1 && bpt.t !== undefined && !tVals.includes(bpt.t)) {
+                        tVals.push(bpt.t);
+                    }
                 }
             });
         return tVals;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 4876ef300..3bdd2bf6e 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -47,6 +47,7 @@ import { LinkAnchorBox } from './LinkAnchorBox';
 import { PresEffect, PresEffectDirection } from './trails';
 import { PinProps, PresBox } from './trails/PresBox';
 import React = require('react');
+import { InkingStroke } from '../InkingStroke';
 const { Howl } = require('howler');
 
 interface Window {
@@ -869,11 +870,12 @@ 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);
         return (
             <div
                 className="documentView-contentsView"
                 style={{
-                    pointerEvents: (this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents) ?? 'all',
+                    pointerEvents: (isInk ? 'none' : this.pointerEvents) ?? 'all',
                     height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
                 }}>
                 {!this._retryThumb || !this.thumbShown() ? null : (
-- 
cgit v1.2.3-70-g09d2

