From 081632757af3c2ec2e4482c1e5fe710c8ee6cad8 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 11 Aug 2022 19:01:37 -0400
Subject: added better ui and backend support for mask ink strokes. added frame
 #'s ui for seeing current animation frame.

---
 .../collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx   | 1 +
 1 file changed, 1 insertion(+)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 3d85d32a0..ee01c341b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -45,6 +45,7 @@ export interface PoolData {
 export interface ViewDefResult {
     ele: JSX.Element;
     bounds?: ViewDefBounds;
+    inkMask?: boolean;
 }
 function toLabel(target: FieldResult<Field>) {
     if (typeof target === 'number' || Number(target)) {
-- 
cgit v1.2.3-70-g09d2


From d8f2a45a95a14151d3484c3c14f72d217a818786 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 22 Aug 2022 12:50:52 -0400
Subject: made inkMasks animate when they are turned on /off.    fixed
 inkStrokes to be interpolated when they have an activeFrame set.

---
 src/client/util/CurrentUserUtils.ts                |  4 ++--
 src/client/views/DocumentDecorations.tsx           |  4 ++--
 src/client/views/InkStrokeProperties.ts            |  1 -
 src/client/views/InkingStroke.tsx                  | 22 +++++++++++++++----
 .../CollectionFreeFormLayoutEngines.tsx            |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.scss |  9 ++++++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  9 ++++----
 src/client/views/nodes/DocumentView.tsx            |  1 -
 src/client/views/nodes/button/FontIconBox.tsx      | 25 +++++++++++-----------
 9 files changed, 47 insertions(+), 30 deletions(-)

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index f7d072d80..20c57c617 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -633,7 +633,7 @@ export class CurrentUserUtils {
             { title: "Left",   toolTip: "Left align",   btnType: ButtonType.ToggleButton, icon: "align-left",   scripts: {onClick:'{ return setAlignment("left", _readOnly_);}' }},
             { title: "Center", toolTip: "Center align", btnType: ButtonType.ToggleButton, icon: "align-center", scripts: {onClick:'{ return setAlignment("center", _readOnly_);}'} },
             { title: "Right",  toolTip: "Right align",  btnType: ButtonType.ToggleButton, icon: "align-right",  scripts: {onClick:'{ return setAlignment("right", _readOnly_);}'} },
-            { title: "NoLink", toolTip: "Auto Link",    btnType: ButtonType.ToggleButton, icon: "link",         scripts: {onClick:'{ return toggleNoAutoLinkAnchor(_readOnly_);}'}},
+            { title: "NoLink", toolTip: "Auto Link",    btnType: ButtonType.ToggleButton, icon: "link",         scripts: {onClick:'{ return toggleNoAutoLinkAnchor(_readOnly_);}'}, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}},
             { title: "Dictate",toolTip: "Dictate",      btnType: ButtonType.ToggleButton, icon: "microphone",   scripts: {onClick:'{ return toggleDictation(_readOnly_);}'}},
         ];
     }
@@ -676,7 +676,7 @@ export class CurrentUserUtils {
                     CollectionViewType.Grid, CollectionViewType.NoteTaking]),
               title: "Perspective",                    toolTip: "View",                 btnType: ButtonType.DropdownList,                                                                                    ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}},
             { title: "Back",    icon: "chevron-left",  toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20,  scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
-            { title: "Num",     icon: "",           toolTip: "Frame Number",         btnType: ButtonType.TextButton,          funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement().currentFrame.toString()'}, width: 20,  scripts: {}},
+            { title: "Num",     icon: "",              toolTip: "Frame Number",         btnType: ButtonType.TextButton,          funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement().currentFrame.toString()'}, width: 20,  scripts: {}},
             { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20,  scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
             { title: "Fill",    icon: "fill-drip",     toolTip: "Background Fill Color",btnType: ButtonType.ColorButton,         funcs: {hidden: '!SelectionManager_selectedDocType()'},                     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,         funcs: {hidden: '!SelectionManager_selectedDocType()'},                     ignoreClick: true,             scripts: { script:  'return setHeaderColor(value, _readOnly_)'}},
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 6d1397395..ab77af0f4 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -203,8 +203,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                 if (this._deleteAfterIconify) {
                     views.forEach(iconView => {
                         Doc.setNativeView(iconView.props.Document);
-                        if (iconView.props.Document.isInkMask && iconView.props.Document.activeFrame !== undefined) {
-                            iconView.props.Document.opacity = 0; // bcz: hacky ... allows inkMaks to be "turned off" without removing them from the collection which allows them to function properly in a presenation.
+                        if (iconView.props.Document.activeFrame) {
+                            iconView.props.Document.opacity = 0; // bcz: hacky ... allows inkMasks and other documents to be "turned off" without removing them from the animated collection which allows them to function properly in a presenation.
                         } else {
                             iconView.props.removeDocument?.(iconView.props.Document);
                         }
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index b32c9d54c..1f5f16592 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -68,7 +68,6 @@ export class InkStrokeProperties {
                                 doc.x = oldXrange.coord + (newXrange.min - oldXrange.min) * ptsXscale;
                                 doc.y = oldYrange.coord + (newYrange.min - oldYrange.min) * ptsYscale;
                                 if (doc.activeFrame !== undefined) {
-                                    doc.data = ComputedField.MakeInterpolated('data', 'activeFrame', doc, NumCast(doc.activeFrame));
                                     const findexed = Cast(doc[`data-indexed`], listSpec(InkField), []).slice();
                                     findexed[NumCast(doc.activeFrame)] = new InkField(newPoints);
                                     doc[`data-indexed`] = new List<InkField>(findexed);
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 52efbdfd7..520d40abf 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -25,7 +25,7 @@ import { action, IReactionDisposer, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc, HeightSym, WidthSym } from '../../fields/Doc';
 import { InkData, InkField, InkTool } from '../../fields/InkField';
-import { BoolCast, Cast, NumCast, RTFCast, StrCast } from '../../fields/Types';
+import { BoolCast, Cast, FieldValue, NumCast, RTFCast, StrCast } from '../../fields/Types';
 import { TraceMobx } from '../../fields/util';
 import { OmitKeys, returnFalse, setupMoveUpEvents } from '../../Utils';
 import { CognitiveServices } from '../cognitive_services/CognitiveServices';
@@ -45,6 +45,9 @@ import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { INK_MASK_SIZE } from './global/globalCssVariables.scss';
 import './InkStroke.scss';
 import Color = require('color');
+import { ComputedField } from '../../fields/ScriptField';
+import { listSpec } from '../../fields/Schema';
+import { List } from '../../fields/List';
 
 @observer
 export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -56,7 +59,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
         return inkData && inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y;
     }
     private _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated
-    private _selDisposer?: IReactionDisposer;
+    private _disposers: { [key: string]: IReactionDisposer } = {};
 
     @observable _nearestSeg?: number; // nearest Bezier segment along the ink stroke to the cursor (used for displaying the Add Point highlight)
     @observable _nearestT?: number; // nearest t value within the nearest Bezier segment "
@@ -64,13 +67,24 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
 
     componentDidMount() {
         this.props.setContentView?.(this);
-        this._selDisposer = reaction(
+        this._disposers.activeFrame = reaction(
+            () => this.rootDoc.activeFrame !== undefined && !(ComputedField.WithoutComputed(() => FieldValue(this.rootDoc[this.fieldKey])) instanceof ComputedField),
+            () => {
+                const newPoints = Cast(this.rootDoc[this.fieldKey], InkField, null).inkData;
+                this.rootDoc[this.fieldKey] = ComputedField.MakeInterpolated(this.fieldKey, 'activeFrame', this.rootDoc, NumCast(this.rootDoc.activeFrame));
+                const findexed = Cast(this.rootDoc[`data-indexed`], listSpec(InkField), []).slice();
+                findexed[NumCast(this.rootDoc.activeFrame)] = new InkField(newPoints);
+                this.rootDoc[this.fieldKey + '-indexed'] = new List<InkField>(findexed);
+            },
+            { fireImmediately: true }
+        );
+        this._disposers.selfDisper = reaction(
             () => this.props.isSelected(), // react to stroke being deselected by turning off ink handles
             selected => !selected && (InkStrokeProperties.Instance._controlButton = false)
         );
     }
     componentWillUnmount() {
-        this._selDisposer?.();
+        Object.keys(this._disposers).forEach(key => this._disposers[key]());
     }
 
     // transform is the inherited screentolocal xf plus any scaling that was done to make the stroke
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index ee01c341b..b210e7d9a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -45,7 +45,7 @@ export interface PoolData {
 export interface ViewDefResult {
     ele: JSX.Element;
     bounds?: ViewDefBounds;
-    inkMask?: boolean;
+    inkMask?: number; //sort elements into either the mask layer (which has a mixedBlendMode appropriate for transparent masks), or the regular documents layer;   -1 = no mask, 0 = mask layer but stroke is transprent (hidden), >0 = mask layer and not hidden
 }
 function toLabel(target: FieldResult<Field>) {
     if (typeof target === 'number' || Number(target)) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 010132aa5..d80fcdfc3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -20,15 +20,20 @@
     pointer-events: none;
 }
 
+.collectionfreeformview-mask-empty,
 .collectionfreeformview-mask {
-    mix-blend-mode: multiply;
     z-index: 5000;
     width: $INK_MASK_SIZE;
     height: $INK_MASK_SIZE;
     transform: translate($INK_MASK_SIZE_HALF, $INK_MASK_SIZE_HALF);
-    background-color: rgba(0, 0, 0, 0.7);
     pointer-events: none;
     position: absolute;
+    background-color: transparent;
+    transition: background-color 1s ease 0s;
+}
+.collectionfreeformview-mask {
+    mix-blend-mode: multiply;
+    background-color: rgba(0, 0, 0, 0.7);
 }
 
 .collectionfreeformview-viewdef {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 0fd326091..1d518076a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -119,9 +119,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @observable ChildDrag: DocumentView | undefined; // child document view being dragged.  needed to update drop areas of groups when a group item is dragged.
 
     @computed get views() {
-        const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask).map(ele => ele.ele);
-        const renderableEles = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && !ele.inkMask).map(ele => ele.ele);
-        if (viewsMask.length) renderableEles.push(<div className="collectionfreeformview-mask">{viewsMask}</div>);
+        const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask !== -1).map(ele => ele.ele);
+        const renderableEles = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask === -1).map(ele => ele.ele);
+        if (viewsMask.length) renderableEles.push(<div className={`collectionfreeformview-mask${this._layoutElements.some(ele => (ele.inkMask ?? 0) > 0) ? '' : '-empty'}`}>{viewsMask}</div>);
         return renderableEles;
     }
     @computed get fitToContentVals() {
@@ -251,7 +251,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     isCurrent(doc: Doc) {
-        if (doc.isInkMask && doc.opacity === 0) return false; // bcz: hacky --- allows inkMasks to be "turned off" in a presentation without removing them from the collection.  otherwise, they still render a gray background.. need to come back to this and fix.
         const dispTime = NumCast(doc._timecodeToShow, -1);
         const endTime = NumCast(doc._timecodeToHide, dispTime + 1.5);
         const curTime = NumCast(this.Document._currentTimecode, -1);
@@ -1465,7 +1464,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 elements.push({
                     ele: this.getChildDocView(entry[1]),
                     bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica),
-                    inkMask: BoolCast(entry[1].pair.layout.isInkMask),
+                    inkMask: BoolCast(entry[1].pair.layout.isInkMask) ? NumCast(entry[1].pair.layout.opacity) : -1,
                 })
             );
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 172adcafe..74143a731 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1656,7 +1656,6 @@ export class DocumentView extends React.Component<DocumentViewProps> {
                         ref={this.ContentRef}
                         style={{
                             transition: this.props.dataTransition,
-                            //position: this.props.Document.isInkMask ? 'absolute' : undefined,
                             transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`,
                             width: isButton || isPresTreeElement ? '100%' : xshift() ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`,
                             height:
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index cb68c1ac3..fc5bf86f4 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -90,9 +90,11 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
     @computed get label() {
         return StrCast(this.rootDoc.label, StrCast(this.rootDoc.title));
     }
-    @computed get icon() {
-        return StrCast(this.dataDoc.icon, 'user') as any;
-    }
+    Icon = (color: string) => {
+        const icon = StrCast(this.dataDoc.icon, 'user') as any;
+        const trailsIcon = () => <img src={`/assets/${'presTrails.png'}`} style={{ width: 30, height: 30, filter: `invert(${color === Colors.DARK_GRAY ? '0%' : '100%'})` }} />;
+        return !icon ? null : icon === 'pres-trail' ? trailsIcon() : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />;
+    };
     @computed get dropdown() {
         return BoolCast(this.rootDoc.dropDownOpen);
     }
@@ -228,7 +230,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                 className={`menuButton ${this.type} ${active}`}
                 style={{ color: color, backgroundColor: backgroundColor, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
                 onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}>
-                <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
+                {this.Icon(color)}
                 {!this.label || !FontIconBox.GetShowLabels() ? null : (
                     <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}>
                         {' '}
@@ -384,7 +386,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                 style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
                 onClick={action(() => (this.colorPickerClosed = !this.colorPickerClosed))}
                 onPointerDown={e => e.stopPropagation()}>
-                <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
+                {this.Icon(color)}
                 <div className="colorButton-color" style={{ backgroundColor: curColor }} />
                 {label}
                 {/* {dropdownCaret} */}
@@ -436,7 +438,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
         } else {
             return (
                 <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ opacity: 1, backgroundColor, color }}>
-                    <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
+                    {this.Icon(color)}
                     {label}
                 </div>
             );
@@ -453,7 +455,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
         return (
             <div className={`menuButton ${this.type}`} onContextMenu={this.specificContextMenu} style={{ backgroundColor: 'transparent', borderBottomLeftRadius: this.dropdown ? 0 : undefined }}>
                 <div className="menuButton-wrap">
-                    <FontAwesomeIcon className={`menuButton-icon-${this.type}`} icon={this.icon} color={'black'} size={'sm'} />
+                    {this.Icon(color)}
                     {!this.label || !FontIconBox.GetShowLabels() ? null : (
                         <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}>
                             {' '}
@@ -506,7 +508,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
             case ButtonType.TextButton:
                 button = (
                     <div className={`menuButton ${this.type}`} style={{ color, backgroundColor, opacity: 1, gridAutoColumns: `${NumCast(this.rootDoc._height)} auto` }}>
-                        <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
+                        {this.Icon(color)}
                         {StrCast(this.rootDoc.buttonText) ? <div className="button-text">{StrCast(this.rootDoc.buttonText)}</div> : null}
                         {label}
                     </div>
@@ -531,7 +533,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
             case ButtonType.ToolButton:
                 button = (
                     <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ opacity: 1, backgroundColor, color }}>
-                        <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
+                        {this.Icon(color)}
                         {label}
                     </div>
                 );
@@ -543,16 +545,15 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
             case ButtonType.ClickButton:
                 button = (
                     <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ color, backgroundColor, opacity: 1 }}>
-                        <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
+                        {this.Icon(color)}
                         {label}
                     </div>
                 );
                 break;
             case ButtonType.MenuButton:
-                const trailsIcon = <img src={`/assets/${'presTrails.png'}`} style={{ width: 30, height: 30, filter: `invert(${color === Colors.DARK_GRAY ? '0%' : '100%'})` }} />;
                 button = (
                     <div className={`menuButton ${this.type}`} style={{ color, backgroundColor }}>
-                        {this.icon === 'pres-trail' ? trailsIcon : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />}
+                        {this.Icon(color)}
                         {menuLabel}
                         <FontIconBadge value={Cast(this.Document.badgeValue, 'string', null)} />
                     </div>
-- 
cgit v1.2.3-70-g09d2


From 440f5c296a599d5c3f9ed781b58c5b00da6edb5a Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 22 Aug 2022 17:18:10 -0400
Subject: added animation of background color, and streamlined/extended a
 variety of props-related things.

---
 src/client/util/CurrentUserUtils.ts                |  6 +--
 src/client/views/DocComponent.tsx                  | 14 -------
 src/client/views/StyleProvider.tsx                 |  9 +++--
 .../CollectionFreeFormLayoutEngines.tsx            |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 33 ++++++++--------
 .../views/nodes/CollectionFreeFormDocumentView.tsx | 30 ++++++++++++---
 src/client/views/nodes/FieldView.tsx               | 11 ++++--
 .../views/nodes/formattedText/FormattedTextBox.tsx | 45 ++++++++++------------
 src/fields/Doc.ts                                  | 14 +++++++
 src/fields/ScriptField.ts                          | 12 +++++-
 10 files changed, 102 insertions(+), 74 deletions(-)

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 20c57c617..17d58595c 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -235,9 +235,9 @@ export class CurrentUserUtils {
             const header = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { ...opts, title: "text", 
                 layout:
                 "<HTMLdiv transformOrigin='top left' width='{100/scale}%' height='{100/scale}%' transform='scale({scale})'>" +
-                `    <FormattedTextBox {...props} dontScale='true' fieldKey={'text'}  height='calc(100% - ${headerBtnHgt}px - {this._headerHeight||0}px)'/>` +
-                "    <FormattedTextBox {...props} dontScale='true' fieldKey={'header'}  dontSelectOnLoad='true' ignoreAutoHeight='true' fontSize='{this._headerFontSize||9}px' height='{(this._headerHeight||0)}px' background='{this._headerColor || MySharedDocs().userColor||`lightGray`}' />" +
-                `    <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' background='yellow' onClick={‘(this._headerHeight=scale*Math.min(Math.max(0,this._height-30),this._headerHeight===0?50:0)) + (this._autoHeightMargins=this._headerHeight ? this._headerHeight+${headerBtnHgt}:0)’}  >Metadata</HTMLdiv>` +
+                `    <FormattedTextBox {...props} dontScale='true' fieldKey={'text'} height='calc(100% - ${headerBtnHgt}px - {this._headerHeight||0}px)'/>` +
+                "    <FormattedTextBox {...props} dontScale='true' fieldKey={'header'} dontSelectOnLoad='true' ignoreAutoHeight='true' fontSize='{this._headerFontSize||9}px' height='{(this._headerHeight||0)}px' backgroundColor='{this._headerColor || MySharedDocs().userColor||`lightGray`}' />" +
+                `    <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' backgroundColor='yellow' onClick={‘(this._headerHeight=scale*Math.min(Math.max(0,this._height-30),this._headerHeight===0?50:0)) + (this._autoHeightMargins=this._headerHeight ? this._headerHeight+${headerBtnHgt}:0)’}  >Metadata</HTMLdiv>` +
                 "</HTMLdiv>"
             }, "header");
 
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 280ca8a8c..886dd974b 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -132,20 +132,6 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
 
         lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result;
 
-        styleFromLayoutString = (scale: number) => {
-            const style: { [key: string]: any } = {};
-            const divKeys = ['width', 'height', 'fontSize', 'transform', 'left', 'background', 'left', 'right', 'top', 'bottom', 'pointerEvents', 'position'];
-            const replacer = (match: any, expr: string, offset: any, string: any) => {
-                // bcz: this executes a script to convert a property expression string:  { script }  into a value
-                return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ self: this.rootDoc, this: this.layoutDoc, scale }).result?.toString() ?? '';
-            };
-            divKeys.map((prop: string) => {
-                const p = (this.props as any)[prop];
-                typeof p === 'string' && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer));
-            });
-            return style;
-        };
-
         protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
 
         @computed public get annotationKey() {
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 3bd4f5152..c0ba170c6 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -40,7 +40,8 @@ export enum StyleProp {
     JitterRotation = 'jitterRotation', // whether documents should be randomly rotated
     BorderPath = 'customBorder', // border path for document view
     FontSize = 'fontSize', // size of text font
-    FontFamily = 'fontFamily', // size of text font
+    FontFamily = 'fontFamily', // font family of text
+    FontWeight = 'fontWeight', // font weight of text
 }
 
 function darkScheme() {
@@ -117,9 +118,11 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
         case StyleProp.HideLinkButton:
             return props?.hideLinkButton || (!selected && (doc?.isLinkButton || doc?.hideLinkButton));
         case StyleProp.FontSize:
-            return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(doc?.fontSize, StrCast(Doc.UserDoc().fontSize)));
+            return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(doc?._fontSize, StrCast(Doc.UserDoc().fontSize)));
         case StyleProp.FontFamily:
-            return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(doc?.fontFamily, StrCast(Doc.UserDoc().fontFamily)));
+            return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(doc?._fontFamily, StrCast(Doc.UserDoc().fontFamily)));
+        case StyleProp.FontWeight:
+            return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(doc?._fontWeight, StrCast(Doc.UserDoc().fontWeight)));
         case StyleProp.ShowTitle:
             return (
                 (doc &&
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index b210e7d9a..d84717b95 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -34,7 +34,7 @@ export interface PoolData {
     zIndex?: number;
     width?: number;
     height?: number;
-    color?: string;
+    backgroundColor?: string;
     opacity?: number;
     transition?: string;
     highlight?: boolean;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 1d518076a..a807ba4ea 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -115,7 +115,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @observable _timelineRef = React.createRef<Timeline>();
     @observable _marqueeRef = React.createRef<HTMLDivElement>();
     @observable _marqueeViewRef = React.createRef<MarqueeView>();
-    @observable _keyframeEditing = false;
     @observable ChildDrag: DocumentView | undefined; // child document view being dragged.  needed to update drop areas of groups when a group item is dragged.
 
     @computed get views() {
@@ -185,8 +184,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             this.Document.lastFrame = Math.max(NumCast(this.Document._currentFrame), NumCast(this.Document.lastFrame));
         }
     };
-    @action setKeyFrameEditing = (set: boolean) => (this._keyframeEditing = set);
-    getKeyFrameEditing = () => this._keyframeEditing;
     onBrowseClickHandler = () => this.props.onBrowseClick?.() || ScriptCast(this.layoutDoc.onBrowseClick);
     onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick);
     onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
@@ -272,15 +269,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             .sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex));
         zsorted.forEach((doc, index) => (doc.zIndex = doc.isInkMask ? 5000 : index + 1));
         const dvals = CollectionFreeFormDocumentView.getValues(refDoc, NumCast(refDoc.activeFrame, 1000));
-        const dropPos = this.Document._currentFrame !== undefined ? [dvals.x || 0, dvals.y || 0] : [NumCast(refDoc.x), NumCast(refDoc.y)];
+        const dropPos = this.Document._currentFrame !== undefined ? [NumCast(dvals.x), NumCast(dvals.y)] : [NumCast(refDoc.x), NumCast(refDoc.y)];
         for (let i = 0; i < docDragData.droppedDocuments.length; i++) {
             const d = docDragData.droppedDocuments[i];
             const layoutDoc = Doc.Layout(d);
             if (this.Document._currentFrame !== undefined) {
                 CollectionFreeFormDocumentView.setupKeyframes([d], NumCast(this.Document._currentFrame), false);
                 const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000));
-                vals.x = x + (vals.x || 0) - dropPos[0];
-                vals.y = y + (vals.y || 0) - dropPos[1];
+                vals.x = x + NumCast(vals.x) - dropPos[0];
+                vals.y = y + NumCast(vals.y) - dropPos[1];
                 vals._scrollTop = this.Document.editScrollProgressivize ? vals._scrollTop : undefined;
                 CollectionFreeFormDocumentView.setValues(NumCast(this.Document._currentFrame), d, vals);
             } else {
@@ -1317,22 +1314,22 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     });
 
     getCalculatedPositions(params: { pair: { layout: Doc; data?: Doc }; index: number; collection: Doc }): PoolData {
-        const layoutDoc = Doc.Layout(params.pair.layout);
-        const { z, color, zIndex } = params.pair.layout;
-        const { x, y, opacity } =
-            this.Document._currentFrame === undefined
-                ? { x: params.pair.layout.x, y: params.pair.layout.y, opacity: this.props.childOpacity ? this.props.childOpacity() : this.props.styleProvider?.(params.pair.layout, this.props, StyleProp.Opacity) }
-                : CollectionFreeFormDocumentView.getValues(params.pair.layout, NumCast(this.Document._currentFrame));
+        const curFrame = NumCast(this.Document._currentFrame);
+        const childDoc = params.pair.layout;
+        const childDocLayout = Doc.Layout(childDoc);
+        const { z, zIndex } = childDoc;
+        const { backgroundColor } = curFrame === undefined ? { backgroundColor: undefined } : CollectionFreeFormDocumentView.getStringValues(childDoc, curFrame);
+        const { x, y, opacity } = curFrame === undefined ? { x: childDoc.x, y: childDoc.y, opacity: this.props.childOpacity?.() } : CollectionFreeFormDocumentView.getValues(childDoc, curFrame);
         return {
             x: NumCast(x),
             y: NumCast(y),
             z: Cast(z, 'number'),
-            color: StrCast(color),
+            backgroundColor: Cast(backgroundColor, 'string') ?? this.props.styleProvider?.(params.pair.layout, this.props, StyleProp.BackgroundColor),
+            opacity: Cast(opacity, 'number') ?? this.props.styleProvider?.(childDoc, this.props, StyleProp.Opacity),
             zIndex: Cast(zIndex, 'number'),
-            transition: StrCast(layoutDoc.dataTransition),
-            opacity: this._keyframeEditing ? 1 : Cast(opacity, 'number', null),
-            width: Cast(layoutDoc._width, 'number'),
-            height: Cast(layoutDoc._height, 'number'),
+            width: Cast(childDocLayout._width, 'number'),
+            height: Cast(childDocLayout._height, 'number'),
+            transition: StrCast(childDocLayout.dataTransition),
             pair: params.pair,
             replica: '',
         };
@@ -1447,7 +1444,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             for (const entry of array) {
                 const lastPos = this._cachedPool.get(entry[0]); // last computed pos
                 const newPos = entry[1];
-                if (!lastPos || newPos.opacity !== lastPos.opacity || newPos.x !== lastPos.x || newPos.y !== lastPos.y || newPos.z !== lastPos.z || newPos.zIndex !== lastPos.zIndex) {
+                if (!lastPos || newPos.backgroundColor !== lastPos.backgroundColor || newPos.opacity !== lastPos.opacity || newPos.x !== lastPos.x || newPos.y !== lastPos.y || newPos.z !== lastPos.z || newPos.zIndex !== lastPos.zIndex) {
                     this._layoutPoolData.set(entry[0], newPos);
                 }
                 if (!lastPos || newPos.height !== lastPos.height || newPos.width !== lastPos.width) {
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index e19e2d525..7dc8a7fbb 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -18,7 +18,7 @@ import { DocumentView, DocumentViewProps } from './DocumentView';
 import React = require('react');
 
 export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
-    dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined;
+    dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; backgroundColor?: string; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined;
     sizeProvider?: (doc: Doc, replica: string) => { width: number; height: number } | undefined;
     renderCutoffProvider: (doc: Doc) => boolean;
     zIndex?: number;
@@ -32,6 +32,7 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
 @observer
 export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps>() {
     public static animFields = ['_height', '_width', 'x', 'y', '_scrollTop', 'opacity']; // fields that are configured to be animatable using animation frames
+    public static animStringFields = ['backgroundColor']; // fields that are configured to be animatable using animation frames
     @observable _animPos: number[] | undefined = undefined;
     @observable _contentView: DocumentView | undefined | null;
     get displayName() {
@@ -41,16 +42,19 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         return `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Document.jitterRotation, this.props.jitterRotation)}deg)`;
     }
     get X() {
-        return this.dataProvider ? this.dataProvider.x : NumCast(this.Document.x);
+        return this.dataProvider?.x ?? NumCast(this.Document.x);
     }
     get Y() {
-        return this.dataProvider ? this.dataProvider.y : NumCast(this.Document.y);
+        return this.dataProvider?.y ?? NumCast(this.Document.y);
     }
     get ZInd() {
-        return this.dataProvider ? this.dataProvider.zIndex : NumCast(this.Document.zIndex);
+        return this.dataProvider?.zIndex ?? NumCast(this.Document.zIndex);
     }
     get Opacity() {
-        return this.dataProvider ? this.dataProvider.opacity : undefined;
+        return this.dataProvider?.opacity;
+    }
+    get BackgroundColor() {
+        return this.dataProvider?.backgroundColor;
     }
     get Highlight() {
         return this.dataProvider?.highlight;
@@ -67,6 +71,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
 
     styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => {
         if (property === StyleProp.Opacity && doc === this.layoutDoc) return this.Opacity; // only change the opacity for this specific document, not its children
+        if (property === StyleProp.BackgroundColor) {
+            return this.BackgroundColor; // only change the opacity for this specific document, not its children
+        }
         return this.props.styleProvider?.(doc, props, property);
     };
 
@@ -77,6 +84,13 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         }, {} as { [val: string]: Opt<number> });
     }
 
+    public static getStringValues(doc: Doc, time: number) {
+        return CollectionFreeFormDocumentView.animStringFields.reduce((p, val) => {
+            p[val] = Cast(`${val}-indexed`, listSpec('string'), [StrCast(doc[val])]).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as string);
+            return p;
+        }, {} as { [val: string]: Opt<string> });
+    }
+
     public static setValues(time: number, d: Doc, vals: { [val: string]: Opt<number> }) {
         const timecode = Math.round(time);
         Object.keys(vals).forEach(val => {
@@ -95,6 +109,10 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                     const findexed = Cast(doc[`${val}-indexed`], listSpec('number'), null);
                     findexed?.length <= timecode + 1 && findexed.push(undefined as any as number);
                 });
+                CollectionFreeFormDocumentView.animStringFields.forEach(val => {
+                    const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null);
+                    findexed?.length <= timecode + 1 && findexed.push(undefined as any as string);
+                });
             })
         );
         setTimeout(
@@ -142,6 +160,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                 doc['opacity-indexed'] = new List<number>(numberRange(currTimecode + 1).map(t => (!doc.z && makeAppear && t < NumCast(doc.appearFrame) ? 0 : 1)));
             }
             CollectionFreeFormDocumentView.animFields.forEach(val => (doc[val] = ComputedField.MakeInterpolated(val, 'activeFrame', doc, currTimecode)));
+            CollectionFreeFormDocumentView.animStringFields.forEach(val => (doc[val] = ComputedField.MakeInterpolatedString(val, 'activeFrame', doc, currTimecode)));
             doc.activeFrame = ComputedField.MakeFunction('self.context?._currentFrame||0');
             doc.dataTransition = 'inherit';
         });
@@ -189,7 +208,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
             PanelWidth: this.panelWidth,
             PanelHeight: this.panelHeight,
         };
-        const background = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
         const mixBlendMode = undefined; // (StrCast(this.layoutDoc.mixBlendMode) as any) || (typeof background === 'string' && background && !background.startsWith('linear') && DashColor(background).alpha() !== 1 ? 'multiply' : undefined);
         return (
             <div
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index dd2c13391..77aaa4441 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -26,16 +26,21 @@ export interface FieldViewProps extends DocumentViewSharedProps {
     NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NOTE: Must also be added to DocumentViewInternalsProps
     onBrowseClick?: () => ScriptField | undefined;
     onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => boolean | undefined;
+    pointerEvents?: () => Opt<string>;
 
     // properties intended to be used from within layout strings (otherwise use the function equivalents that work more efficiently with React)
-    pointerEvents?: () => Opt<string>;
+    // See currentUserUtils headerTemplate for examples of creating text boxes from html which set some of these fields
+    // Also, see InkingStroke for examples of creating text boxes from render() methods which set some of these fields
+    backgroundColor?: string;
+    color?: string;
     fontSize?: number;
     height?: number;
     width?: number;
-    background?: string;
-    color?: string;
     xPadding?: number;
     yPadding?: number;
+    noSidebar?: boolean;
+    dontScale?: boolean;
+    dontSelectOnLoad?: boolean; // suppress selecting (e.g.,. text box) when loaded (and mark as not being associated with scrollTop document field)
 }
 
 @observer
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index f61533619..81ac45521 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -11,7 +11,7 @@ import { Fragment, Mark, Node, Slice } from 'prosemirror-model';
 import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state';
 import { EditorView } from 'prosemirror-view';
 import { DateField } from '../../../../fields/DateField';
-import { AclAdmin, AclAugment, AclEdit, AclReadonly, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from '../../../../fields/Doc';
+import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
 import { InkTool } from '../../../../fields/InkField';
 import { PrefetchProxy } from '../../../../fields/Proxy';
@@ -35,6 +35,7 @@ import { SnappingManager } from '../../../util/SnappingManager';
 import { undoBatch, UndoManager } from '../../../util/UndoManager';
 import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView';
 import { CollectionStackingView } from '../../collections/CollectionStackingView';
+import { CollectionTreeView } from '../../collections/CollectionTreeView';
 import { ContextMenu } from '../../ContextMenu';
 import { ContextMenuProps } from '../../ContextMenuItem';
 import { ViewBoxAnnotatableComponent } from '../../DocComponent';
@@ -44,6 +45,7 @@ import { LightboxView } from '../../LightboxView';
 import { AnchorMenu } from '../../pdf/AnchorMenu';
 import { SidebarAnnos } from '../../SidebarAnnos';
 import { StyleProp } from '../../StyleProvider';
+import { DocumentViewInternal } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
 import { LinkDocPreview } from '../LinkDocPreview';
 import { DashDocCommentView } from './DashDocCommentView';
@@ -61,25 +63,14 @@ import { schema } from './schema_rts';
 import { SummaryView } from './SummaryView';
 import applyDevTools = require('prosemirror-dev-tools');
 import React = require('react');
-import { text } from 'body-parser';
-import { CollectionTreeView } from '../../collections/CollectionTreeView';
-import { DocumentViewInternal } from '../DocumentView';
 const translateGoogleApi = require('translate-google-api');
 
-export interface FormattedTextBoxProps {
-    makeLink?: () => Opt<Doc>; // bcz: hack: notifies the text document when the container has made a link.  allows the text doc to react and setup a hyeprlink for any selected text
-    xPadding?: number; // used to override document's settings for xMargin --- see CollectionCarouselView
-    yPadding?: number;
-    noSidebar?: boolean;
-    dontScale?: boolean;
-    dontSelectOnLoad?: boolean; // suppress selecting the text box when loaded (and mark as not being associated with scrollTop document field)
-}
 export const GoogleRef = 'googleDocId';
 
 type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void;
 
 @observer
-export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps & FormattedTextBoxProps>() {
+export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     public static LayoutString(fieldStr: string) {
         return FieldView.LayoutString(FormattedTextBox, fieldStr);
     }
@@ -1843,18 +1834,27 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         const paddingY = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
         const selPad = (selected && !this.layoutDoc._singleLine) || minimal ? Math.min(paddingY, Math.min(paddingX, 10)) : 0;
         const selPaddingClass = selected && !this.layoutDoc._singleLine && paddingY >= 10 ? '-selected' : '';
-        const styleFromString = this.styleFromLayoutString(scale); // this converts any expressions in the format string to style props.  e.g., <FormattedTextBox height='{this._headerHeight}px' >
-        return styleFromString?.height === '0px' ? null : (
+        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"
                 onWheel={e => this.props.isContentActive() && e.stopPropagation()}
                 style={{
-                    transform: this.props.dontScale ? undefined : `scale(${scale})`,
-                    transformOrigin: this.props.dontScale ? undefined : 'top left',
-                    width: this.props.dontScale ? undefined : `${100 / scale}%`,
-                    height: this.props.dontScale ? undefined : `${100 / scale}%`,
+                    ...(this.props.dontScale
+                        ? {}
+                        : {
+                              transform: `scale(${scale})`,
+                              transformOrigin: 'top left',
+                              width: `${100 / scale}%`,
+                              height: `${100 / scale}%`,
+                          }),
+                    transition: 'inherit',
                     // overflowY: this.layoutDoc._autoHeight ? "hidden" : undefined,
-                    ...styleFromString,
+                    color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color),
+                    fontSize: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize),
+                    fontFamily: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontFamily),
+                    fontWeight: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontWeight),
+                    ...styleFromLayoutString,
                 }}>
                 <div
                     className={`formattedTextBox-cont`}
@@ -1862,11 +1862,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     style={{
                         overflow: this.autoHeight ? 'hidden' : undefined,
                         height: this.props.height || (this.autoHeight && this.props.renderDepth && !this.props.suppressSetHeight ? 'max-content' : undefined),
-                        background: this.props.background ? this.props.background : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
-                        color: this.props.color ? this.props.color : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color),
-                        fontSize: this.props.fontSize ? this.props.fontSize : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize),
-                        fontWeight: Cast(this.layoutDoc._fontWeight, 'string', null) as any,
-                        fontFamily: StrCast(this.layoutDoc._fontFamily, 'inherit'),
                         pointerEvents: interactive ? undefined : 'none',
                     }}
                     onContextMenu={this.specificContextMenu}
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 8d56ebf8c..612fc7fb8 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1389,6 +1389,20 @@ export namespace Doc {
         return !curPres ? false : DocListCast(curPres.data).findIndex(val => Doc.AreProtosEqual(val, doc)) !== -1;
     }
 
+    export function styleFromLayoutString(rootDoc: Doc, layoutDoc: Doc, props: any, scale: number) {
+        const style: { [key: string]: any } = {};
+        const divKeys = ['width', 'height', 'fontSize', 'transform', 'left', 'backgroundColor', 'left', 'right', 'top', 'bottom', 'pointerEvents', 'position'];
+        const replacer = (match: any, expr: string, offset: any, string: any) => {
+            // bcz: this executes a script to convert a property expression string:  { script }  into a value
+            return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ self: rootDoc, this: layoutDoc, scale }).result?.toString() ?? '';
+        };
+        divKeys.map((prop: string) => {
+            const p = props[prop];
+            typeof p === 'string' && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer));
+        });
+        return style;
+    }
+
     // prettier-ignore
     export function toIcon(doc?: Doc, isOpen?: boolean) {
         switch (StrCast(doc?.type)) {
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 68fb45987..0fd992d3b 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -9,7 +9,7 @@ import { Doc, Field, Opt } from './Doc';
 import { Copy, Id, ToScriptString, ToString } from './FieldSymbols';
 import { List } from './List';
 import { ObjectField } from './ObjectField';
-import { Cast, NumCast } from './Types';
+import { Cast, NumCast, StrCast } from './Types';
 import { Plugins } from './util';
 
 function optional(propSchema: PropSchema) {
@@ -199,6 +199,16 @@ export class ComputedField extends ScriptField {
         const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {});
         return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined;
     }
+    public static MakeInterpolatedString(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) {
+        if (!doc[`${fieldKey}-indexed`]) {
+            const flist = new List<string>(numberRange(curTimecode + 1).map(i => undefined) as any as string[]);
+            flist[curTimecode] = StrCast(doc[fieldKey]);
+            doc[`${fieldKey}-indexed`] = flist;
+        }
+        const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey})`, {}, true, {});
+        const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {});
+        return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined;
+    }
 }
 export namespace ComputedField {
     let useComputed = true;
-- 
cgit v1.2.3-70-g09d2


From 267bb35a555ac0f8b67041346213d9a06386785f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 23 Aug 2022 11:57:58 -0400
Subject: added color to animated properties.  changed doc decorations to stop
 before menu bar.  changed color of tab bar so that doc decorations is visible
 when overlapping.

---
 src/client/views/InkingStroke.tsx                  |  3 ++-
 src/client/views/MainView.tsx                      |  4 ++--
 src/client/views/_nodeModuleOverrides.scss         |  3 ++-
 .../CollectionFreeFormLayoutEngines.tsx            |  1 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 14 ++++++++++--
 .../views/nodes/CollectionFreeFormDocumentView.tsx | 25 +++++++++++++---------
 6 files changed, 34 insertions(+), 16 deletions(-)

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

diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 520d40abf..ceaabd0e1 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -48,6 +48,7 @@ import Color = require('color');
 import { ComputedField } from '../../fields/ScriptField';
 import { listSpec } from '../../fields/Schema';
 import { List } from '../../fields/List';
+import { StyleProp } from './StyleProvider';
 
 @observer
 export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -370,7 +371,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
         const closed = InkingStroke.IsClosed(inkData);
         const isInkMask = BoolCast(this.layoutDoc.isInkMask);
         const fillColor = isInkMask ? '#aaaaaa' : StrCast(this.layoutDoc.fillColor, 'transparent');
-        const strokeColor = !closed && fillColor && fillColor !== 'transparent' ? fillColor : StrCast(this.layoutDoc.color);
+        const strokeColor = !closed && fillColor && fillColor !== 'transparent' ? fillColor : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color) ?? StrCast(this.layoutDoc.color);
 
         // bcz: Hack!! Not really sure why, but having fractional values for width/height of mask ink strokes causes the dragging clone (see DragManager) to be offset from where it should be.
         if (isInkMask && (this.layoutDoc[WidthSym]() !== Math.round(this.layoutDoc[WidthSym]()) || this.layoutDoc[HeightSym]() !== Math.round(this.layoutDoc[HeightSym]()))) {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index d2b0e10c1..06be4d194 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -981,7 +981,7 @@ export class MainView extends React.Component {
                 <CaptureManager />
                 <GroupManager />
                 <GoogleAuthenticationManager />
-                <DocumentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfHeaderBarDoc} PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} />
+                <DocumentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfSidebarDoc} PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} />
                 <ComponentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfMainDocContent} />
                 {this._hideUI ? null : <TopBar />}
                 {LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null}
@@ -994,7 +994,7 @@ export class MainView extends React.Component {
                         default:
                             return (
                                 <>
-                                    <div style={{ position: 'relative', display: this._hideUI || LightboxView.LightboxDoc ? 'none' : undefined, zIndex: 1999 }}>
+                                    <div style={{ position: 'relative', display: this._hideUI || LightboxView.LightboxDoc ? 'none' : undefined, zIndex: 2001 }}>
                                         <CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} />
                                     </div>
                                     {this.mainDashboardArea}
diff --git a/src/client/views/_nodeModuleOverrides.scss b/src/client/views/_nodeModuleOverrides.scss
index 17eff022f..b1cce8705 100644
--- a/src/client/views/_nodeModuleOverrides.scss
+++ b/src/client/views/_nodeModuleOverrides.scss
@@ -44,7 +44,8 @@ div .lm_header {
     position: absolute;
     width: calc(100% - 60px);
     overflow: scroll;
-    background: $dark-gray;
+    background: #6b6b6b6b; //$dark-gray;
+    border-radius: 5px;
 }
 
 .lm_tab {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index d84717b95..89cc22d07 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -35,6 +35,7 @@ export interface PoolData {
     width?: number;
     height?: number;
     backgroundColor?: string;
+    color?: string;
     opacity?: number;
     transition?: string;
     highlight?: boolean;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 0234d303f..03beaf65e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1318,12 +1318,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const childDoc = params.pair.layout;
         const childDocLayout = Doc.Layout(childDoc);
         const { z, zIndex } = childDoc;
-        const { backgroundColor } = curFrame === undefined ? { backgroundColor: undefined } : CollectionFreeFormDocumentView.getStringValues(childDoc, curFrame);
+        const { backgroundColor, color } = curFrame === undefined ? { backgroundColor: undefined, color: undefined } : CollectionFreeFormDocumentView.getStringValues(childDoc, curFrame);
         const { x, y, opacity } = curFrame === undefined ? { x: childDoc.x, y: childDoc.y, opacity: this.props.childOpacity?.() } : CollectionFreeFormDocumentView.getValues(childDoc, curFrame);
         return {
             x: NumCast(x),
             y: NumCast(y),
             z: Cast(z, 'number'),
+            color: Cast(color, 'string') ? StrCast(color) : this.props.styleProvider?.(childDoc, this.props, StyleProp.Color),
             backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this.props.styleProvider?.(childDoc, this.props, StyleProp.BackgroundColor),
             opacity: Cast(opacity, 'number') ?? this.props.styleProvider?.(childDoc, this.props, StyleProp.Opacity),
             zIndex: Cast(zIndex, 'number'),
@@ -1444,7 +1445,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             for (const entry of array) {
                 const lastPos = this._cachedPool.get(entry[0]); // last computed pos
                 const newPos = entry[1];
-                if (!lastPos || newPos.backgroundColor !== lastPos.backgroundColor || newPos.opacity !== lastPos.opacity || newPos.x !== lastPos.x || newPos.y !== lastPos.y || newPos.z !== lastPos.z || newPos.zIndex !== lastPos.zIndex) {
+                if (
+                    !lastPos ||
+                    newPos.color !== lastPos.color ||
+                    newPos.backgroundColor !== lastPos.backgroundColor ||
+                    newPos.opacity !== lastPos.opacity ||
+                    newPos.x !== lastPos.x ||
+                    newPos.y !== lastPos.y ||
+                    newPos.z !== lastPos.z ||
+                    newPos.zIndex !== lastPos.zIndex
+                ) {
                     this._layoutPoolData.set(entry[0], newPos);
                 }
                 if (!lastPos || newPos.height !== lastPos.height || newPos.width !== lastPos.width) {
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 7dc8a7fbb..45f68e0f0 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -18,7 +18,7 @@ import { DocumentView, DocumentViewProps } from './DocumentView';
 import React = require('react');
 
 export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
-    dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; backgroundColor?: string; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined;
+    dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; color?: string; backgroundColor?: string; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined;
     sizeProvider?: (doc: Doc, replica: string) => { width: number; height: number } | undefined;
     renderCutoffProvider: (doc: Doc) => boolean;
     zIndex?: number;
@@ -32,12 +32,14 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
 @observer
 export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps>() {
     public static animFields = ['_height', '_width', 'x', 'y', '_scrollTop', 'opacity']; // fields that are configured to be animatable using animation frames
-    public static animStringFields = ['backgroundColor']; // fields that are configured to be animatable using animation frames
+    public static animStringFields = ['backgroundColor', 'color']; // fields that are configured to be animatable using animation frames
     @observable _animPos: number[] | undefined = undefined;
     @observable _contentView: DocumentView | undefined | null;
     get displayName() {
+        // this makes mobx trace() statements more descriptive
         return 'CollectionFreeFormDocumentView(' + this.rootDoc.title + ')';
-    } // this makes mobx trace() statements more descriptive
+    }
+
     get transform() {
         return `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Document.jitterRotation, this.props.jitterRotation)}deg)`;
     }
@@ -56,12 +58,12 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     get BackgroundColor() {
         return this.dataProvider?.backgroundColor;
     }
+    get Color() {
+        return this.dataProvider?.color;
+    }
     get Highlight() {
         return this.dataProvider?.highlight;
     }
-    @computed get ShowTitle() {
-        return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.ShowTitle) as Opt<string>;
-    }
     @computed get dataProvider() {
         return this.props.dataProvider?.(this.props.Document, this.props.replica);
     }
@@ -70,10 +72,13 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     }
 
     styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => {
-        if (property === StyleProp.Opacity && doc === this.layoutDoc) return this.Opacity; // only change the opacity for this specific document, not its children
-        if (property === StyleProp.BackgroundColor) {
-            return this.BackgroundColor; // only change the opacity for this specific document, not its children
-        }
+        if (doc === this.layoutDoc)
+            // prettier-ignore
+            switch (property) {
+                case StyleProp.Opacity:         return this.Opacity; // only change the opacity for this specific document, not its children
+                case StyleProp.BackgroundColor: return this.BackgroundColor;
+                case StyleProp.Color:           return this.Color;
+            }
         return this.props.styleProvider?.(doc, props, property);
     };
 
-- 
cgit v1.2.3-70-g09d2


From e7995c366381e5f813ddcb3babc90c8b51e407b6 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 10 Nov 2022 22:54:02 -0500
Subject: fixed inkMaskStrokes

---
 src/client/views/InkingStroke.tsx                                     | 4 +++-
 .../collectionFreeForm/CollectionFreeFormLayoutEngines.tsx            | 2 +-
 2 files changed, 4 insertions(+), 2 deletions(-)

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

diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index b83ba97e7..8fbe6d7c1 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -391,7 +391,9 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
             false
         );
         const highlightIndex = /*BoolCast(this.props.Document.isLinkButton) && */ Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString
-        const highlightColor = !highlightIndex
+        const highlightColor = isInkMask
+            ? 'transparent'
+            : !highlightIndex
             ? StrCast(this.layoutDoc.strokeOutlineColor, !closed && fillColor && fillColor !== 'transparent' ? StrCast(this.layoutDoc.color, 'transparent') : 'transparent')
             : ['transparent', 'rgb(68, 118, 247)', 'rgb(68, 118, 247)', 'yellow', 'magenta', 'cyan', 'orange'][highlightIndex];
         // Invisible polygonal line that enables the ink to be selected by the user.
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 89cc22d07..54be6ba0f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -46,7 +46,7 @@ export interface PoolData {
 export interface ViewDefResult {
     ele: JSX.Element;
     bounds?: ViewDefBounds;
-    inkMask?: number; //sort elements into either the mask layer (which has a mixedBlendMode appropriate for transparent masks), or the regular documents layer;   -1 = no mask, 0 = mask layer but stroke is transprent (hidden), >0 = mask layer and not hidden
+    inkMask?: number; //sort elements into either the mask layer (which has a mixedBlendMode appropriate for transparent masks), or the regular documents layer;   -1 = no mask, 0 = mask layer but stroke is transparent (hidden, as in during a presentation when you want to smoothly animate it into being a mask), >0 = mask layer and not hidden
 }
 function toLabel(target: FieldResult<Field>) {
     if (typeof target === 'number' || Number(target)) {
-- 
cgit v1.2.3-70-g09d2


From b7d4f932d826d48aca4c7c058e05ceaea9c43057 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 17 Nov 2022 14:35:13 -0500
Subject: mostly changing strings to enums

---
 src/client/util/DictationManager.ts                |   6 +-
 src/client/util/LinkFollower.ts                    |   4 +-
 src/client/views/LightboxView.tsx                  |  16 +-
 src/client/views/MainView.tsx                      |  28 +-
 .../views/PropertiesDocBacklinksSelector.tsx       |  45 ++-
 src/client/views/PropertiesDocContextSelector.tsx  |   6 +-
 src/client/views/PropertiesView.tsx                |   4 +-
 .../views/collections/CollectionDockingView.tsx    |  26 +-
 .../views/collections/CollectionNoteTakingView.tsx |  12 +-
 .../views/collections/CollectionPileView.tsx       |  22 +-
 .../views/collections/CollectionStackingView.tsx   |   4 +-
 .../views/collections/CollectionTimeView.tsx       | 351 +++++++++-------
 src/client/views/collections/CollectionView.tsx    |  11 +-
 src/client/views/collections/TabDocView.tsx        |  34 +-
 src/client/views/collections/TreeView.tsx          |   8 +-
 .../CollectionFreeFormLayoutEngines.tsx            |   4 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  23 +-
 .../CollectionMulticolumnView.tsx                  |   9 +-
 .../collectionSchema/CollectionSchemaCells.tsx     | 447 +++++++++++----------
 .../CollectionSchemaMovableRow.tsx                 |   3 +-
 .../collections/collectionSchema/SchemaTable.tsx   |   6 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   4 +-
 src/client/views/nodes/DocumentView.tsx            |  35 +-
 src/client/views/nodes/KeyValueBox.tsx             |   5 +-
 src/client/views/nodes/KeyValuePair.tsx            |  65 +--
 src/client/views/nodes/LinkAnchorBox.tsx           |   5 +-
 src/client/views/nodes/LinkDocPreview.tsx          |   4 +-
 src/client/views/nodes/VideoBox.tsx                |   3 +-
 src/client/views/nodes/button/FontIconBox.tsx      |   3 +-
 .../views/nodes/formattedText/DashFieldView.tsx    |   3 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   4 +-
 .../formattedText/ProsemirrorExampleTransfer.ts    |   3 +-
 src/client/views/nodes/trails/PresBox.tsx          |  13 +-
 src/mobile/MobileInterface.tsx                     |   2 +-
 34 files changed, 668 insertions(+), 550 deletions(-)

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

diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 0a61f3478..203d4ad62 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -11,7 +11,7 @@ import { Utils } from '../../Utils';
 import { Docs } from '../documents/Documents';
 import { DocumentType } from '../documents/DocumentTypes';
 import { DictationOverlay } from '../views/DictationOverlay';
-import { DocumentView } from '../views/nodes/DocumentView';
+import { DocumentView, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView';
 import { SelectionManager } from './SelectionManager';
 import { UndoManager } from './UndoManager';
 
@@ -328,7 +328,7 @@ export namespace DictationManager {
                 {
                     action: (target: DocumentView) => {
                         const kvp = Docs.Create.KVPDocument(target.props.Document, { _width: 300, _height: 300 });
-                        target.props.addDocTab(kvp, 'add:right');
+                        target.props.addDocTab(kvp, OpenWhere.addRight);
                     },
                 },
             ],
@@ -345,7 +345,7 @@ export namespace DictationManager {
                         const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"ordered_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`;
                         proto.data = new RichTextField(proseMirrorState);
                         proto.backgroundColor = '#eeffff';
-                        target.props.addDocTab(newBox, 'add:right');
+                        target.props.addDocTab(newBox, OpenWhere.addRight);
                     },
                 },
             ],
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 68716a207..a3eb7ed7a 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -4,7 +4,7 @@ import { BoolCast, Cast, DocCast, StrCast } from '../../fields/Types';
 import { DocumentType } from '../documents/DocumentTypes';
 import { DocumentDecorations } from '../views/DocumentDecorations';
 import { LightboxView } from '../views/LightboxView';
-import { DocumentViewSharedProps, ViewAdjustment } from '../views/nodes/DocumentView';
+import { DocumentViewSharedProps, OpenWhere, ViewAdjustment } from '../views/nodes/DocumentView';
 import { DocumentManager } from './DocumentManager';
 import { LinkManager } from './LinkManager';
 import { UndoManager } from './UndoManager';
@@ -32,7 +32,7 @@ export class LinkFollower {
         const createViewFunc = (doc: Doc, followLoc: string, finished?: Opt<() => void>) => {
             const createTabForTarget = (didFocus: boolean) =>
                 new Promise<ViewAdjustment>(res => {
-                    const where = LightboxView.LightboxDoc ? 'inPlace' : StrCast(sourceDoc.followLinkLocation, followLoc);
+                    const where = LightboxView.LightboxDoc ? OpenWhere.inPlace : (StrCast(sourceDoc.followLinkLocation, followLoc) as OpenWhere);
                     docViewProps.addDocTab(doc, where);
                     setTimeout(() => {
                         const targDocView = DocumentManager.Instance.getFirstDocumentView(doc); // get first document view available within the lightbox if that's open, or anywhere otherwise.
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 5660a34e9..1f58763d1 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -18,7 +18,7 @@ import { TabDocView } from './collections/TabDocView';
 import { GestureOverlay } from './GestureOverlay';
 import './LightboxView.scss';
 import { MainView } from './MainView';
-import { DocumentView } from './nodes/DocumentView';
+import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
 import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider';
 
 interface LightboxViewProps {
@@ -141,11 +141,13 @@ export class LightboxView extends React.Component<LightboxViewProps> {
             this._docFilters = (f => (this._docFilters ? [this._docFilters.push(f) as any, this._docFilters][1] : [f]))(`cookies:${cookie}:provide`);
         }
     }
-    public static AddDocTab = (doc: Doc, location: string, layoutTemplate?: Doc, openInTabFunc?: any) => {
-        const inPlaceView = DocCast(doc.context) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.context)) : undefined;
-        if (inPlaceView) {
-            inPlaceView.dataDoc[Doc.LayoutFieldKey(inPlaceView.rootDoc)] = new List<Doc>([doc]);
-            return true;
+    public static AddDocTab = (doc: Doc, location: OpenWhere, layoutTemplate?: Doc, openInTabFunc?: any) => {
+        if (location !== OpenWhere.lightbox) {
+            const inPlaceView = DocCast(doc.context) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.context)) : undefined;
+            if (inPlaceView) {
+                inPlaceView.dataDoc[Doc.LayoutFieldKey(inPlaceView.rootDoc)] = new List<Doc>([doc]);
+                return true;
+            }
         }
         LightboxView.openInTabFunc = openInTabFunc;
         SelectionManager.DeselectAll();
@@ -360,7 +362,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                     title={'open in tab'}
                     onClick={e => {
                         e.stopPropagation();
-                        CollectionDockingView.AddSplit(LightboxView._docTarget || LightboxView._doc!, '');
+                        CollectionDockingView.AddSplit(LightboxView._docTarget || LightboxView._doc!, OpenWhereMod.none);
                         //LightboxView.openInTabFunc(LightboxView._docTarget || LightboxView._doc!, "inPlace");
                         SelectionManager.DeselectAll();
                         LightboxView.SetLightboxDoc(undefined);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 392b4eeeb..c151aebcd 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 } from './nodes/DocumentView';
+import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
 import { DashFieldViewMenu } from './nodes/formattedText/DashFieldView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { RichTextMenu } from './nodes/formattedText/RichTextMenu';
@@ -538,7 +538,7 @@ export class MainView extends React.Component {
     @action
     createNewPresentation = () => {
         const pres = Doc.MakeCopy(Doc.UserDoc().emptyTrail as Doc, true);
-        CollectionDockingView.AddSplit(pres, 'right');
+        CollectionDockingView.AddSplit(pres, OpenWhereMod.right);
         Doc.MyTrails && Doc.AddDocToList(Doc.MyTrails, 'data', pres); // Doc.MyTrails should be created in createDashboard
         Doc.ActivePresentation = pres;
     };
@@ -546,7 +546,7 @@ export class MainView extends React.Component {
     @action
     openPresentation = (pres: Doc) => {
         if (pres.type === DocumentType.PRES) {
-            CollectionDockingView.AddSplit(pres, 'right');
+            CollectionDockingView.AddSplit(pres, OpenWhereMod.right);
             Doc.MyTrails && (Doc.ActivePresentation = pres);
             Doc.AddDocToList(Doc.MyTrails, 'data', pres);
             this.closeFlyout();
@@ -683,20 +683,20 @@ export class MainView extends React.Component {
 
     sidebarScreenToLocal = () => new Transform(0, -this.topOfSidebarDoc, 1);
     mainContainerXf = () => this.sidebarScreenToLocal().translate(-this.leftScreenOffsetOfMainDocView, 0);
-    addDocTabFunc = (doc: Doc, location: string): boolean => {
-        const locationFields = doc._viewType === CollectionViewType.Docking ? ['dashboard'] : location.split(':');
-        const locationParams = locationFields.length > 1 ? locationFields[1] : '';
+    addDocTabFunc = (doc: Doc, location: OpenWhere): boolean => {
+        const whereFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':');
+        const whereMods = whereFields.length > 1 ? whereFields[1] : '';
         if (doc.dockingConfig) return DashboardView.openDashboard(doc);
         // prettier-ignore
-        switch (locationFields[0]) {
+        switch (whereFields[0]) {
             default:
-            case 'inPlace':
-            case 'lightbox':   return LightboxView.AddDocTab(doc, location);
-            case 'add':        return CollectionDockingView.AddSplit(doc, locationParams);
-            case 'dashboard':  return DashboardView.openDashboard(doc);
-            case 'close':      return CollectionDockingView.CloseSplit(doc, locationParams);
-            case 'fullScreen': return CollectionDockingView.OpenFullScreen(doc);
-            case 'toggle':     return CollectionDockingView.ToggleSplit(doc, locationParams);
+            case OpenWhere.inPlace:
+            case OpenWhere.lightbox:   return LightboxView.AddDocTab(doc, location);
+            case OpenWhere.add:        return CollectionDockingView.AddSplit(doc, whereMods as OpenWhereMod);
+            case OpenWhere.dashboard:  return DashboardView.openDashboard(doc);
+            case OpenWhere.close:      return CollectionDockingView.CloseSplit(doc, whereMods);
+            case OpenWhere.fullScreen: return CollectionDockingView.OpenFullScreen(doc);
+            case OpenWhere.toggle:     return CollectionDockingView.ToggleSplit(doc, whereMods as OpenWhereMod);
         }
     };
 
diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx
index 4ead8eaf0..25ac44078 100644
--- a/src/client/views/PropertiesDocBacklinksSelector.tsx
+++ b/src/client/views/PropertiesDocBacklinksSelector.tsx
@@ -1,20 +1,21 @@
-import { computed } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import { Doc, DocListCast } from "../../fields/Doc";
-import { Cast } from "../../fields/Types";
-import { emptyFunction } from "../../Utils";
-import { DocumentType } from "../documents/DocumentTypes";
-import { LinkManager } from "../util/LinkManager";
-import { SelectionManager } from "../util/SelectionManager";
-import { LinkMenu } from "./linking/LinkMenu";
+import { computed } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc, DocListCast } from '../../fields/Doc';
+import { Cast } from '../../fields/Types';
+import { emptyFunction } from '../../Utils';
+import { DocumentType } from '../documents/DocumentTypes';
+import { LinkManager } from '../util/LinkManager';
+import { SelectionManager } from '../util/SelectionManager';
+import { LinkMenu } from './linking/LinkMenu';
+import { OpenWhere, OpenWhereMod } from './nodes/DocumentView';
 import './PropertiesDocBacklinksSelector.scss';
 
 type PropertiesDocBacklinksSelectorProps = {
-    Document: Doc,
-    Stack?: any,
-    hideTitle?: boolean,
-    addDocTab(doc: Doc, location: string): void
+    Document: Doc;
+    Stack?: any;
+    hideTitle?: boolean;
+    addDocTab(doc: Doc, location: OpenWhere): void;
 };
 
 @observer
@@ -40,14 +41,16 @@ export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDo
 
         if (otherdoc) {
             otherdoc.hidden = false;
-            this.props.addDocTab(Doc.IsPrototype(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, "toggle:right");
+            this.props.addDocTab(Doc.IsPrototype(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, (OpenWhere.toggle + ':' + OpenWhereMod.right) as OpenWhere);
         }
-    }
+    };
 
     render() {
-        return !SelectionManager.Views().length ? (null) : <div>
-            {this.props.hideTitle ? (null) : <p key="contexts">Contexts:</p>}
-            <LinkMenu docView={SelectionManager.Views().lastElement()} clearLinkEditor={emptyFunction} itemHandler={this.getOnClick} position={{ x: 0 }} />
-        </div>;
+        return !SelectionManager.Views().length ? null : (
+            <div>
+                {this.props.hideTitle ? null : <p key="contexts">Contexts:</p>}
+                <LinkMenu docView={SelectionManager.Views().lastElement()} clearLinkEditor={emptyFunction} itemHandler={this.getOnClick} position={{ x: 0 }} />
+            </div>
+        );
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx
index 9d89ee036..2c7da5931 100644
--- a/src/client/views/PropertiesDocContextSelector.tsx
+++ b/src/client/views/PropertiesDocContextSelector.tsx
@@ -7,14 +7,14 @@ import { Cast, NumCast, StrCast } from '../../fields/Types';
 import { CollectionViewType } from '../documents/DocumentTypes';
 import { DocFocusOrOpen } from '../util/DocumentManager';
 import { CollectionDockingView } from './collections/CollectionDockingView';
-import { DocumentView } from './nodes/DocumentView';
+import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
 import './PropertiesDocContextSelector.scss';
 
 type PropertiesDocContextSelectorProps = {
     DocView?: DocumentView;
     Stack?: any;
     hideTitle?: boolean;
-    addDocTab(doc: Doc, location: string): void;
+    addDocTab(doc: Doc, location: OpenWhere): void;
 };
 
 @observer
@@ -53,7 +53,7 @@ export class PropertiesDocContextSelector extends React.Component<PropertiesDocC
             col._panY = NumCast(target.y) + NumCast(target._height) / 2;
         }
         col.hidden = false;
-        this.props.addDocTab(col, 'toggle:right');
+        this.props.addDocTab(col, (OpenWhere.toggle + ':' + OpenWhereMod.right) as OpenWhere);
         setTimeout(() => DocFocusOrOpen(Doc.GetProto(this.props.DocView!.props.Document), col), 100);
     };
 
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index e5ff9e267..93a3fd253 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -24,7 +24,7 @@ import { Transform } from '../util/Transform';
 import { undoBatch, UndoManager } from '../util/UndoManager';
 import { EditableView } from './EditableView';
 import { InkStrokeProperties } from './InkStrokeProperties';
-import { DocumentView, StyleProviderFunc } from './nodes/DocumentView';
+import { DocumentView, OpenWhere, StyleProviderFunc } from './nodes/DocumentView';
 import { FilterBox } from './nodes/FilterBox';
 import { KeyValueBox } from './nodes/KeyValueBox';
 import { PresBox } from './nodes/trails';
@@ -42,7 +42,7 @@ interface PropertiesViewProps {
     width: number;
     height: number;
     styleProvider?: StyleProviderFunc;
-    addDocTab: (doc: Doc, where: string) => boolean;
+    addDocTab: (doc: Doc, where: OpenWhere) => boolean;
 }
 
 @observer
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 434466505..8cbe548c7 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -25,6 +25,7 @@ import { CollectionFreeFormView } from './collectionFreeForm';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 import { TabDocView } from './TabDocView';
 import React = require('react');
+import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
 const _global = (window /* browser */ || global) /* node */ as any;
 
 @observer
@@ -142,7 +143,7 @@ export class CollectionDockingView extends CollectionSubView() {
 
     @undoBatch
     @action
-    public static ReplaceTab(document: Doc, panelName: string, stack: any, addToSplit?: boolean): boolean {
+    public static ReplaceTab(document: Doc, panelName: OpenWhereMod, stack: any, addToSplit?: boolean): boolean {
         const instance = CollectionDockingView.Instance;
         if (!instance) return false;
         const newConfig = CollectionDockingView.makeDocumentConfig(document, panelName);
@@ -164,7 +165,7 @@ export class CollectionDockingView extends CollectionSubView() {
     }
 
     @undoBatch
-    public static ToggleSplit(doc: Doc, location: string, stack?: any, panelName?: string) {
+    public static ToggleSplit(doc: Doc, location: OpenWhereMod, stack?: any, panelName?: string) {
         return CollectionDockingView.Instance && Array.from(CollectionDockingView.Instance.tabMap.keys()).findIndex(tab => tab.DashDoc === doc) !== -1
             ? CollectionDockingView.CloseSplit(doc)
             : CollectionDockingView.AddSplit(doc, location, stack, panelName);
@@ -175,7 +176,7 @@ export class CollectionDockingView extends CollectionSubView() {
     //
     @undoBatch
     @action
-    public static AddSplit(document: Doc, pullSide: string, stack?: any, panelName?: string) {
+    public static AddSplit(document: Doc, pullSide: OpenWhereMod, stack?: any, panelName?: string) {
         if (document?._viewType === CollectionViewType.Docking) return DashboardView.openDashboard(document);
         if (!CollectionDockingView.Instance) return false;
         const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === document);
@@ -208,14 +209,15 @@ export class CollectionDockingView extends CollectionSubView() {
                 // if row
                 switch (pullSide) {
                     default:
-                    case 'right':
+                    case OpenWhereMod.none:
+                    case OpenWhereMod.right:
                         glayRoot.contentItems[0].addChild(newContentItem());
                         break;
-                    case 'left':
+                    case OpenWhereMod.left:
                         glayRoot.contentItems[0].addChild(newContentItem(), 0);
                         break;
-                    case 'top':
-                    case 'bottom':
+                    case OpenWhereMod.top:
+                    case OpenWhereMod.bottom:
                         // if not going in a row layout, must add already existing content into column
                         const rowlayout = glayRoot.contentItems[0];
                         const newColumn = rowlayout.layoutManager.createContentItem({ type: 'column' }, instance._goldenLayout);
@@ -496,7 +498,7 @@ export class CollectionDockingView extends CollectionSubView() {
                     title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`,
                 });
                 this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd);
-                CollectionDockingView.AddSplit(docToAdd, '', stack);
+                CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack);
             }
         });
 
@@ -539,7 +541,7 @@ export class CollectionDockingView extends CollectionSubView() {
                             title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`,
                         });
                         this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd);
-                        CollectionDockingView.AddSplit(docToAdd, '', stack);
+                        CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack);
                     }
                 })
             );
@@ -568,14 +570,14 @@ export class CollectionDockingView extends CollectionSubView() {
 
 ScriptingGlobals.add(
     function openInLightbox(doc: any) {
-        LightboxView.AddDocTab(doc, 'lightbox');
+        LightboxView.AddDocTab(doc, OpenWhere.lightbox);
     },
     'opens up document in a lightbox',
     '(doc: any)'
 );
 ScriptingGlobals.add(
     function openOnRight(doc: any) {
-        return CollectionDockingView.AddSplit(doc, 'right');
+        return CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
     },
     'opens up document in tab on right side of the screen',
     '(doc: any)'
@@ -588,5 +590,5 @@ ScriptingGlobals.add(
     '(doc: any)'
 );
 ScriptingGlobals.add(function useRightSplit(doc: any, shiftKey?: boolean) {
-    CollectionDockingView.ReplaceTab(doc, 'right', undefined, shiftKey);
+    CollectionDockingView.ReplaceTab(doc, OpenWhereMod.right, undefined, shiftKey);
 });
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index b0f64ed60..29670a1a7 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -19,7 +19,7 @@ import { undoBatch } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { LightboxView } from '../LightboxView';
-import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment } from '../nodes/DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, ViewAdjustment } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { StyleProp } from '../StyleProvider';
@@ -180,14 +180,6 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
     }
 
-    addDocTab = (doc: Doc, where: string) => {
-        if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) {
-            this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]);
-            return true;
-        }
-        return this.props.addDocTab(doc, where);
-    };
-
     scrollToBottom = () => {
         smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight);
     };
@@ -274,7 +266,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
                 removeDocument={this.props.removeDocument}
                 contentPointerEvents={StrCast(this.layoutDoc.contentPointerEvents)}
                 whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
-                addDocTab={this.addDocTab}
+                addDocTab={this.props.addDocTab}
                 bringToFront={returnFalse}
                 scriptContext={this.props.scriptContext}
                 pinToPres={this.props.pinToPres}
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index 38e240ac6..e95622630 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -12,6 +12,8 @@ import './CollectionPileView.scss';
 import { CollectionSubView } from './CollectionSubView';
 import React = require('react');
 import { ScriptField } from '../../../fields/ScriptField';
+import { OpenWhere } from '../nodes/DocumentView';
+import { computePassLayout, computeStarburstLayout } from './collectionFreeForm';
 
 @observer
 export class CollectionPileView extends CollectionSubView() {
@@ -19,8 +21,8 @@ export class CollectionPileView extends CollectionSubView() {
     _disposers: { [name: string]: IReactionDisposer } = {};
 
     componentDidMount() {
-        if (this.layoutEngine() !== 'pass' && this.layoutEngine() !== 'starburst') {
-            this.Document._pileLayoutEngine = 'pass';
+        if (this.layoutEngine() !== computePassLayout.name && this.layoutEngine() !== computeStarburstLayout.name) {
+            this.Document._pileLayoutEngine = computePassLayout.name;
         }
         this._originalChrome = this.layoutDoc._chromeHidden;
         this.layoutDoc._chromeHidden = true;
@@ -56,7 +58,7 @@ export class CollectionPileView extends CollectionSubView() {
 
     // returns the contents of the pileup in a CollectionFreeFormView
     @computed get contents() {
-        const isStarburst = this.layoutEngine() === 'starburst';
+        const isStarburst = this.layoutEngine() === computeStarburstLayout.name;
         return (
             <div className="collectionPileView-innards" style={{ pointerEvents: isStarburst || SnappingManager.GetIsDragging() ? undefined : 'none' }}>
                 <CollectionFreeFormView
@@ -74,7 +76,7 @@ export class CollectionPileView extends CollectionSubView() {
 
     // toggles the pileup between starburst to compact
     toggleStarburst = action(() => {
-        if (this.layoutEngine() === 'starburst') {
+        if (this.layoutEngine() === computeStarburstLayout.name) {
             const defaultSize = 110;
             this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - NumCast(this.layoutDoc._starburstPileWidth, defaultSize) / 2;
             this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - NumCast(this.layoutDoc._starburstPileHeight, defaultSize) / 2;
@@ -83,12 +85,12 @@ export class CollectionPileView extends CollectionSubView() {
             DocUtils.pileup(this.childDocs, undefined, undefined, NumCast(this.layoutDoc._width) / 2, false);
             this.layoutDoc._panX = 0;
             this.layoutDoc._panY = -10;
-            this.props.Document._pileLayoutEngine = 'pass';
+            this.props.Document._pileLayoutEngine = computePassLayout.name;
         } else {
             const defaultSize = 25;
             !this.layoutDoc._starburstRadius && (this.layoutDoc._starburstRadius = 250);
             !this.layoutDoc._starburstDocScale && (this.layoutDoc._starburstDocScale = 2.5);
-            if (this.layoutEngine() === 'pass') {
+            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]();
@@ -96,7 +98,7 @@ export class CollectionPileView extends CollectionSubView() {
             }
             this.layoutDoc._panX = this.layoutDoc._panY = 0;
             this.layoutDoc._width = this.layoutDoc._height = defaultSize;
-            this.props.Document._pileLayoutEngine = 'starburst';
+            this.props.Document._pileLayoutEngine = computeStarburstLayout.name;
         }
     });
 
@@ -118,7 +120,7 @@ export class CollectionPileView extends CollectionSubView() {
                         const doc = this.childDocs[0];
                         doc.x = e.clientX;
                         doc.y = e.clientY;
-                        this.props.addDocTab(doc, 'inParent') && (this.props.removeDocument?.(doc) || false);
+                        this.props.addDocTab(doc, OpenWhere.inParent) && (this.props.removeDocument?.(doc) || false);
                         dist = 0;
                     }
                 }
@@ -130,8 +132,8 @@ export class CollectionPileView extends CollectionSubView() {
                 SnappingManager.SetIsDragging(false);
             },
             emptyFunction,
-            e.shiftKey && this.layoutEngine() === 'pass',
-            this.layoutEngine() === 'pass' && e.shiftKey
+            e.shiftKey && this.layoutEngine() === computePassLayout.name,
+            this.layoutEngine() === computePassLayout.name && e.shiftKey
         ); // this sets _doubleTap
     };
 
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 175051d5c..aa4583af6 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -22,7 +22,7 @@ import { ContextMenuProps } from '../ContextMenuItem';
 import { EditableView } from '../EditableView';
 import { LightboxView } from '../LightboxView';
 import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
-import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment } from '../nodes/DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, ViewAdjustment } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { StyleProp } from '../StyleProvider';
@@ -241,7 +241,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
     }
 
-    addDocTab = (doc: Doc, where: string) => {
+    addDocTab = (doc: Doc, where: OpenWhere) => {
         if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) {
             this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]);
             return true;
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 3dd9d2d84..ac896a8fd 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -1,32 +1,32 @@
-import { toUpper } from "lodash";
-import { action, computed, observable, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import { Doc, Opt, StrListCast } from "../../../fields/Doc";
-import { List } from "../../../fields/List";
-import { ObjectField } from "../../../fields/ObjectField";
-import { RichTextField } from "../../../fields/RichTextField";
-import { listSpec } from "../../../fields/Schema";
-import { ComputedField, ScriptField } from "../../../fields/ScriptField";
-import { Cast, NumCast, StrCast } from "../../../fields/Types";
-import { emptyFunction, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from "../../../Utils";
-import { Docs } from "../../documents/Documents";
-import { DocumentType } from "../../documents/DocumentTypes";
-import { DocumentManager } from "../../util/DocumentManager";
-import { ScriptingGlobals } from "../../util/ScriptingGlobals";
-import { ContextMenu } from "../ContextMenu";
-import { ContextMenuProps } from "../ContextMenuItem";
-import { EditableView } from "../EditableView";
-import { ViewSpecPrefix } from "../nodes/DocumentView";
-import { ViewDefBounds } from "./collectionFreeForm/CollectionFreeFormLayoutEngines";
-import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
-import { CollectionSubView } from "./CollectionSubView";
-import "./CollectionTimeView.scss";
-import React = require("react");
+import { toUpper } from 'lodash';
+import { action, computed, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import { Doc, Opt, StrListCast } from '../../../fields/Doc';
+import { List } from '../../../fields/List';
+import { ObjectField } from '../../../fields/ObjectField';
+import { RichTextField } from '../../../fields/RichTextField';
+import { listSpec } from '../../../fields/Schema';
+import { ComputedField, ScriptField } from '../../../fields/ScriptField';
+import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { emptyFunction, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils';
+import { Docs } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { DocumentManager } from '../../util/DocumentManager';
+import { ScriptingGlobals } from '../../util/ScriptingGlobals';
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { EditableView } from '../EditableView';
+import { ViewSpecPrefix } from '../nodes/DocumentView';
+import { computePivotLayout, computeTimelineLayout, ViewDefBounds } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
+import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
+import { CollectionSubView } from './CollectionSubView';
+import './CollectionTimeView.scss';
+import React = require('react');
 
 @observer
 export class CollectionTimeView extends CollectionSubView() {
     _changing = false;
-    @observable _layoutEngine = "pivot";
+    @observable _layoutEngine = computePivotLayout.name;
     @observable _collapsed: boolean = false;
     @observable _childClickedScript: Opt<ScriptField>;
     @observable _viewDefDivClick: Opt<ScriptField>;
@@ -35,7 +35,7 @@ export class CollectionTimeView extends CollectionSubView() {
     getAnchor = () => {
         const anchor = Docs.Create.HTMLAnchorDocument([], {
             title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as any,
-            annotationOn: this.rootDoc
+            annotationOn: this.rootDoc,
         });
 
         // save view spec information for anchor
@@ -43,81 +43,103 @@ export class CollectionTimeView extends CollectionSubView() {
         proto.pivotField = this.pivotField;
         proto.docFilters = ObjectField.MakeCopy(this.layoutDoc._docFilters as ObjectField) || new List<string>([]);
         proto.docRangeFilters = ObjectField.MakeCopy(this.layoutDoc._docRangeFilters as ObjectField) || new List<string>([]);
-        proto[ViewSpecPrefix + "_viewType"] = this.layoutDoc._viewType;
+        proto[ViewSpecPrefix + '_viewType'] = this.layoutDoc._viewType;
 
         // store anchor in annotations list of document (not technically needed since these anchors are never drawn)
-        if (Cast(this.dataDoc[this.props.fieldKey + "-annotations"], listSpec(Doc), null) !== undefined) {
-            Cast(this.dataDoc[this.props.fieldKey + "-annotations"], listSpec(Doc), []).push(anchor);
+        if (Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), null) !== undefined) {
+            Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), []).push(anchor);
         } else {
-            this.dataDoc[this.props.fieldKey + "-annotations"] = new List<Doc>([anchor]);
+            this.dataDoc[this.props.fieldKey + '-annotations'] = new List<Doc>([anchor]);
         }
         return anchor;
-    }
+    };
 
     async componentDidMount() {
         this.props.setContentView?.(this);
         //const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || DocUtils.findTemplate("detailView", StrCast(this.rootDoc.type), "");
         ///const childText = "const alias = getAlias(self); switchView(alias, detailView); alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); ";
         runInAction(() => {
-            this._childClickedScript = ScriptField.MakeScript("openInLightbox(self)", { this: Doc.name });
-            this._viewDefDivClick = ScriptField.MakeScript("pivotColumnClick(this,payload)", { payload: "any" });
+            this._childClickedScript = ScriptField.MakeScript('openInLightbox(self)', { this: Doc.name });
+            this._viewDefDivClick = ScriptField.MakeScript('pivotColumnClick(this,payload)', { payload: 'any' });
         });
     }
 
-    get pivotField() { return this._focusPivotField || StrCast(this.layoutDoc._pivotField); }
+    get pivotField() {
+        return this._focusPivotField || StrCast(this.layoutDoc._pivotField);
+    }
     @action
     setViewSpec = (anchor: Doc, preview: boolean) => {
-        if (preview) {   // if in preview, then override document's fields with view spec
+        if (preview) {
+            // if in preview, then override document's fields with view spec
             this._focusFilters = StrListCast(Doc.GetProto(anchor).docFilters);
             this._focusRangeFilters = StrListCast(Doc.GetProto(anchor).docRangeFilters);
             this._focusPivotField = StrCast(anchor.pivotField);
-        } else if (anchor.pivotField !== undefined) {  // otherwise set document's fields based on anchor view spec
+        } else if (anchor.pivotField !== undefined) {
+            // otherwise set document's fields based on anchor view spec
             this.layoutDoc._prevFilterIndex = 1;
             this.layoutDoc._pivotField = StrCast(anchor.pivotField);
             this.layoutDoc._docFilters = new List<string>(StrListCast(anchor.docFilters));
             this.layoutDoc._docRangeFilters = new List<string>(StrListCast(anchor.docRangeFilters));
         }
         return 0;
-    }
+    };
 
     layoutEngine = () => this._layoutEngine;
-    toggleVisibility = action(() => this._collapsed = !this._collapsed);
+    toggleVisibility = action(() => (this._collapsed = !this._collapsed));
 
     onMinDown = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => {
-            const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0));
-            const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10));
-            this.props.Document[this.props.fieldKey + "-timelineMinReq"] = minReq + (maxReq - minReq) * delta[0] / this.props.PanelWidth();
-            this.props.Document[this.props.fieldKey + "-timelineSpan"] = undefined;
-            return false;
-        }), returnFalse, emptyFunction);
-    }
+        setupMoveUpEvents(
+            this,
+            e,
+            action((e: PointerEvent, down: number[], delta: number[]) => {
+                const minReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMinReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMin'], 0));
+                const maxReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMaxReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMax'], 10));
+                this.props.Document[this.props.fieldKey + '-timelineMinReq'] = minReq + ((maxReq - minReq) * delta[0]) / this.props.PanelWidth();
+                this.props.Document[this.props.fieldKey + '-timelineSpan'] = undefined;
+                return false;
+            }),
+            returnFalse,
+            emptyFunction
+        );
+    };
 
     onMaxDown = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => {
-            const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0));
-            const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10));
-            this.props.Document[this.props.fieldKey + "-timelineMaxReq"] = maxReq + (maxReq - minReq) * delta[0] / this.props.PanelWidth();
-            return false;
-        }), returnFalse, emptyFunction);
-    }
+        setupMoveUpEvents(
+            this,
+            e,
+            action((e: PointerEvent, down: number[], delta: number[]) => {
+                const minReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMinReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMin'], 0));
+                const maxReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMaxReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMax'], 10));
+                this.props.Document[this.props.fieldKey + '-timelineMaxReq'] = maxReq + ((maxReq - minReq) * delta[0]) / this.props.PanelWidth();
+                return false;
+            }),
+            returnFalse,
+            emptyFunction
+        );
+    };
 
     onMidDown = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => {
-            const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0));
-            const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10));
-            this.props.Document[this.props.fieldKey + "-timelineMinReq"] = minReq - (maxReq - minReq) * delta[0] / this.props.PanelWidth();
-            this.props.Document[this.props.fieldKey + "-timelineMaxReq"] = maxReq - (maxReq - minReq) * delta[0] / this.props.PanelWidth();
-            return false;
-        }), returnFalse, emptyFunction);
-    }
+        setupMoveUpEvents(
+            this,
+            e,
+            action((e: PointerEvent, down: number[], delta: number[]) => {
+                const minReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMinReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMin'], 0));
+                const maxReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMaxReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMax'], 10));
+                this.props.Document[this.props.fieldKey + '-timelineMinReq'] = minReq - ((maxReq - minReq) * delta[0]) / this.props.PanelWidth();
+                this.props.Document[this.props.fieldKey + '-timelineMaxReq'] = maxReq - ((maxReq - minReq) * delta[0]) / this.props.PanelWidth();
+                return false;
+            }),
+            returnFalse,
+            emptyFunction
+        );
+    };
 
     goTo = (prevFilterIndex: number) => {
-        this.layoutDoc._pivotField = this.layoutDoc["_prevPivotFields" + prevFilterIndex];
-        this.layoutDoc._docFilters = ObjectField.MakeCopy(this.layoutDoc["_prevDocFilter" + prevFilterIndex] as ObjectField);
-        this.layoutDoc._docRangeFilters = ObjectField.MakeCopy(this.layoutDoc["_prevDocRangeFilters" + prevFilterIndex] as ObjectField);
+        this.layoutDoc._pivotField = this.layoutDoc['_prevPivotFields' + prevFilterIndex];
+        this.layoutDoc._docFilters = ObjectField.MakeCopy(this.layoutDoc['_prevDocFilter' + prevFilterIndex] as ObjectField);
+        this.layoutDoc._docRangeFilters = ObjectField.MakeCopy(this.layoutDoc['_prevDocRangeFilters' + prevFilterIndex] as ObjectField);
         this.layoutDoc._prevFilterIndex = prevFilterIndex;
-    }
+    };
 
     @action
     contentsDown = (e: React.MouseEvent) => {
@@ -127,37 +149,58 @@ export class CollectionTimeView extends CollectionSubView() {
         } else {
             this.layoutDoc._docFilters = new List([]);
         }
-    }
+    };
 
     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}>
-            <CollectionFreeFormView {...this.props}
-                engineProps={{ pivotField: this.pivotField, docFilters: this.childDocFilters, docRangeFilters: this.childDocRangeFilters }}
-                fitContentsToBox={returnTrue}
-                childClickScript={this._childClickedScript}
-                viewDefDivClick={this._viewDefDivClick}
-                //dontScaleFilter={this.dontScaleFilter}
-                layoutEngine={this.layoutEngine} />
-        </div>;
+        return (
+            <div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this.props.isContentActive() ? undefined : 'none' }} onClick={this.contentsDown}>
+                <CollectionFreeFormView
+                    {...this.props}
+                    engineProps={{ pivotField: this.pivotField, docFilters: this.childDocFilters, docRangeFilters: this.childDocRangeFilters }}
+                    fitContentsToBox={returnTrue}
+                    childClickScript={this._childClickedScript}
+                    viewDefDivClick={this._viewDefDivClick}
+                    //dontScaleFilter={this.dontScaleFilter}
+                    layoutEngine={this.layoutEngine}
+                />
+            </div>
+        );
     }
 
     public static SyncTimelineToPresentation(doc: Doc) {
         const fieldKey = Doc.LayoutFieldKey(doc);
-        doc[fieldKey + "-timelineCur"] = ComputedField.MakeFunction("(activePresentationItem()[this._pivotField || 'year'] || 0)");
+        doc[fieldKey + '-timelineCur'] = ComputedField.MakeFunction("(activePresentationItem()[this._pivotField || 'year'] || 0)");
     }
     specificMenu = (e: React.MouseEvent) => {
         const layoutItems: ContextMenuProps[] = [];
         const doc = this.layoutDoc;
 
-        layoutItems.push({ description: "Force Timeline", event: () => { doc._forceRenderEngine = "timeline"; }, icon: "compress-arrows-alt" });
-        layoutItems.push({ description: "Force Pivot", event: () => { doc._forceRenderEngine = "pivot"; }, icon: "compress-arrows-alt" });
-        layoutItems.push({ description: "Auto Time/Pivot layout", event: () => { doc._forceRenderEngine = undefined; }, icon: "compress-arrows-alt" });
-        layoutItems.push({ description: "Sync with presentation", event: () => CollectionTimeView.SyncTimelineToPresentation(doc), icon: "compress-arrows-alt" });
+        layoutItems.push({
+            description: 'Force Timeline',
+            event: () => {
+                doc._forceRenderEngine = computeTimelineLayout.name;
+            },
+            icon: 'compress-arrows-alt',
+        });
+        layoutItems.push({
+            description: 'Force Pivot',
+            event: () => {
+                doc._forceRenderEngine = computePivotLayout.name;
+            },
+            icon: 'compress-arrows-alt',
+        });
+        layoutItems.push({
+            description: 'Auto Time/Pivot layout',
+            event: () => {
+                doc._forceRenderEngine = undefined;
+            },
+            icon: 'compress-arrows-alt',
+        });
+        layoutItems.push({ description: 'Sync with presentation', event: () => CollectionTimeView.SyncTimelineToPresentation(doc), icon: 'compress-arrows-alt' });
 
-        ContextMenu.Instance.addItem({ description: "Options...", subitems: layoutItems, icon: "eye" });
-    }
+        ContextMenu.Instance.addItem({ description: 'Options...', subitems: layoutItems, icon: 'eye' });
+    };
     @computed get _allFacets() {
         const facets = new Set<string>();
         this.childDocs.forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key)));
@@ -169,37 +212,40 @@ export class CollectionTimeView extends CollectionSubView() {
         const docItems: ContextMenuProps[] = [];
         const keySet: Set<string> = new Set();
 
-        this.childLayoutPairs.map(pair => this._allFacets.filter(fieldKey =>
-            pair.layout[fieldKey] instanceof RichTextField ||
-            typeof (pair.layout[fieldKey]) === "number" ||
-            typeof (pair.layout[fieldKey]) === "boolean" ||
-            typeof (pair.layout[fieldKey]) === "string").filter(fieldKey => fieldKey[0] !== "_" && (fieldKey[0] !== "#" || fieldKey === "#") && (fieldKey === "tags" || fieldKey[0] === toUpper(fieldKey)[0])).map(fieldKey => keySet.add(fieldKey)));
-        Array.from(keySet).map(fieldKey =>
-            docItems.push({ description: ":" + fieldKey, event: () => this.layoutDoc._pivotField = fieldKey, icon: "compress-arrows-alt" }));
-        docItems.push({ description: ":default", event: () => this.layoutDoc._pivotField = undefined, icon: "compress-arrows-alt" });
-        ContextMenu.Instance.addItem({ description: "Pivot Fields ...", subitems: docItems, icon: "eye" });
+        this.childLayoutPairs.map(pair =>
+            this._allFacets
+                .filter(fieldKey => pair.layout[fieldKey] instanceof RichTextField || typeof pair.layout[fieldKey] === 'number' || typeof pair.layout[fieldKey] === 'boolean' || typeof pair.layout[fieldKey] === 'string')
+                .filter(fieldKey => fieldKey[0] !== '_' && (fieldKey[0] !== '#' || fieldKey === '#') && (fieldKey === 'tags' || fieldKey[0] === toUpper(fieldKey)[0]))
+                .map(fieldKey => keySet.add(fieldKey))
+        );
+        Array.from(keySet).map(fieldKey => docItems.push({ description: ':' + fieldKey, event: () => (this.layoutDoc._pivotField = fieldKey), icon: 'compress-arrows-alt' }));
+        docItems.push({ description: ':default', event: () => (this.layoutDoc._pivotField = undefined), icon: 'compress-arrows-alt' });
+        ContextMenu.Instance.addItem({ description: 'Pivot Fields ...', subitems: docItems, icon: 'eye' });
         const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y);
-        ContextMenu.Instance.displayMenu(x, y, ":");
-    }
+        ContextMenu.Instance.displayMenu(x, y, ':');
+    };
 
     @computed get pivotKeyUI() {
-        return <div className={"pivotKeyEntry"}>
-            <EditableView
-                GetValue={returnEmptyString}
-                SetValue={(value: any) => {
-                    if (value?.length) {
-                        this.layoutDoc._pivotField = value;
-                        return true;
-                    }
-                    return false;
-                }}
-                toggle={this.toggleVisibility}
-                background={"#f1efeb"} // this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
-                contents={":" + StrCast(this.layoutDoc._pivotField)}
-                showMenuOnLoad={true}
-                display={"inline"}
-                menuCallback={this.menuCallback} />
-        </div>;
+        return (
+            <div className={'pivotKeyEntry'}>
+                <EditableView
+                    GetValue={returnEmptyString}
+                    SetValue={(value: any) => {
+                        if (value?.length) {
+                            this.layoutDoc._pivotField = value;
+                            return true;
+                        }
+                        return false;
+                    }}
+                    toggle={this.toggleVisibility}
+                    background={'#f1efeb'} // this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
+                    contents={':' + StrCast(this.layoutDoc._pivotField)}
+                    showMenuOnLoad={true}
+                    display={'inline'}
+                    menuCallback={this.menuCallback}
+                />
+            </div>
+        );
     }
 
     render() {
@@ -211,55 +257,62 @@ export class CollectionTimeView extends CollectionSubView() {
             }
         });
         const forceLayout = StrCast(this.layoutDoc._forceRenderEngine);
-        const doTimeline = forceLayout ? (forceLayout === "timeline") : nonNumbers / this.childDocs.length < 0.1 && this.props.PanelWidth() / this.props.PanelHeight() > 6;
-        if (doTimeline !== (this._layoutEngine === "timeline")) {
+        const doTimeline = forceLayout ? forceLayout === computeTimelineLayout.name : nonNumbers / this.childDocs.length < 0.1 && this.props.PanelWidth() / this.props.PanelHeight() > 6;
+        if (doTimeline !== (this._layoutEngine === computeTimelineLayout.name)) {
             if (!this._changing) {
                 this._changing = true;
-                setTimeout(action(() => {
-                    this._layoutEngine = doTimeline ? "timeline" : "pivot";
-                    this._changing = false;
-                }), 0);
+                setTimeout(
+                    action(() => {
+                        this._layoutEngine = doTimeline ? computeTimelineLayout.name : computePivotLayout.name;
+                        this._changing = false;
+                    }),
+                    0
+                );
             }
         }
 
-        return <div className={"collectionTimeView" + (doTimeline ? "" : "-pivot")} onContextMenu={this.specificMenu}
-            style={{ width: this.props.PanelWidth(), height: "100%" }}>
-            {this.pivotKeyUI}
-            {this.contents}
-            {!this.props.isSelected() || !doTimeline ? (null) : <>
-                <div className="collectionTimeView-thumb-min collectionTimeView-thumb" key="min" onPointerDown={this.onMinDown} />
-                <div className="collectionTimeView-thumb-max collectionTimeView-thumb" key="mid" onPointerDown={this.onMaxDown} />
-                <div className="collectionTimeView-thumb-mid collectionTimeView-thumb" key="max" onPointerDown={this.onMidDown} />
-            </>}
-        </div>;
+        return (
+            <div className={'collectionTimeView' + (doTimeline ? '' : '-pivot')} onContextMenu={this.specificMenu} style={{ width: this.props.PanelWidth(), height: '100%' }}>
+                {this.pivotKeyUI}
+                {this.contents}
+                {!this.props.isSelected() || !doTimeline ? null : (
+                    <>
+                        <div className="collectionTimeView-thumb-min collectionTimeView-thumb" key="min" onPointerDown={this.onMinDown} />
+                        <div className="collectionTimeView-thumb-max collectionTimeView-thumb" key="mid" onPointerDown={this.onMaxDown} />
+                        <div className="collectionTimeView-thumb-mid collectionTimeView-thumb" key="max" onPointerDown={this.onMidDown} />
+                    </>
+                )}
+            </div>
+        );
     }
 }
 
 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);
-    pivotDoc["_prevDocRangeFilters" + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docRangeFilters as ObjectField);
-    pivotDoc["_prevPivotFields" + prevFilterIndex] = pivotField;
+    pivotDoc['_prevDocFilter' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField);
+    pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docRangeFilters as ObjectField);
+    pivotDoc['_prevPivotFields' + prevFilterIndex] = pivotField;
     pivotDoc._prevFilterIndex = ++prevFilterIndex;
     pivotDoc._docFilters = new List();
-    setTimeout(action(() => {
-        const filterVals = (bounds.payload as string[]);
-        filterVals.map(filterVal => Doc.setDocFilter(pivotDoc, pivotField, filterVal, "check"));
-        const pivotView = DocumentManager.Instance.getDocumentView(pivotDoc);
-        if (pivotDoc && pivotView?.ComponentView instanceof CollectionTimeView && filterVals.length === 1) {
-            if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) {
-                pivotDoc._pivotField = filterVals[0];
+    setTimeout(
+        action(() => {
+            const filterVals = bounds.payload as string[];
+            filterVals.map(filterVal => Doc.setDocFilter(pivotDoc, pivotField, filterVal, 'check'));
+            const pivotView = DocumentManager.Instance.getDocumentView(pivotDoc);
+            if (pivotDoc && pivotView?.ComponentView instanceof CollectionTimeView && filterVals.length === 1) {
+                if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) {
+                    pivotDoc._pivotField = filterVals[0];
+                }
             }
-        }
-        const newFilters = StrListCast(pivotDoc._docFilters);
-        if (newFilters.length && originalFilter.length &&
-            newFilters.lastElement() === originalFilter.lastElement()) {
-            pivotDoc._prevFilterIndex = --prevFilterIndex;
-            pivotDoc["_prevDocFilter" + prevFilterIndex] = undefined;
-            pivotDoc["_prevDocRangeFilters" + prevFilterIndex] = undefined;
-            pivotDoc["_prevPivotFields" + prevFilterIndex] = undefined;
-        }
-    }));
-});
\ No newline at end of file
+            const newFilters = StrListCast(pivotDoc._docFilters);
+            if (newFilters.length && originalFilter.length && newFilters.lastElement() === originalFilter.lastElement()) {
+                pivotDoc._prevFilterIndex = --prevFilterIndex;
+                pivotDoc['_prevDocFilter' + prevFilterIndex] = undefined;
+                pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = undefined;
+                pivotDoc['_prevPivotFields' + prevFilterIndex] = undefined;
+            }
+        })
+    );
+});
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 625d4e9e5..917d7618c 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -16,6 +16,7 @@ import { InteractionUtils } from '../../util/InteractionUtils';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
+import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
 import { FieldView, FieldViewProps } from '../nodes/FieldView';
 import { CollectionCarousel3DView } from './CollectionCarousel3DView';
 import { CollectionCarouselView } from './CollectionCarouselView';
@@ -174,7 +175,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
                 vtype => {
                     const newRendition = Doc.MakeAlias(this.rootDoc);
                     newRendition._viewType = vtype;
-                    this.props.addDocTab(newRendition, 'add:right');
+                    this.props.addDocTab(newRendition, OpenWhere.addRight);
                     return newRendition;
                 },
                 false
@@ -184,17 +185,17 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
             const optionItems = options && 'subitems' in options ? options.subitems : [];
             !Doc.noviceMode ? optionItems.splice(0, 0, { description: `${this.rootDoc.forceActive ? 'Select' : 'Force'} Contents Active`, event: () => (this.rootDoc.forceActive = !this.rootDoc.forceActive), icon: 'project-diagram' }) : null;
             if (this.rootDoc.childLayout instanceof Doc) {
-                optionItems.push({ description: 'View Child Layout', event: () => this.props.addDocTab(this.rootDoc.childLayout as Doc, 'add:right'), icon: 'project-diagram' });
+                optionItems.push({ description: 'View Child Layout', event: () => this.props.addDocTab(this.rootDoc.childLayout as Doc, OpenWhere.addRight), icon: 'project-diagram' });
             }
             if (this.rootDoc.childClickedOpenTemplateView instanceof Doc) {
-                optionItems.push({ description: 'View Child Detailed Layout', event: () => this.props.addDocTab(this.rootDoc.childClickedOpenTemplateView as Doc, 'add:right'), icon: 'project-diagram' });
+                optionItems.push({ description: 'View Child Detailed Layout', event: () => this.props.addDocTab(this.rootDoc.childClickedOpenTemplateView as Doc, OpenWhere.addRight), icon: 'project-diagram' });
             }
             !Doc.noviceMode && optionItems.push({ description: `${this.rootDoc.isInPlaceContainer ? 'Unset' : 'Set'} inPlace Container`, event: () => (this.rootDoc.isInPlaceContainer = !this.rootDoc.isInPlaceContainer), icon: 'project-diagram' });
 
             if (!Doc.noviceMode && false) {
                 optionItems.push({
                     description: 'Create Branch',
-                    event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), 'add:right'),
+                    event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), OpenWhere.addRight),
                     icon: 'project-diagram',
                 });
                 optionItems.push({
@@ -225,7 +226,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
                         event: (obj: any) => {
                             const alias = Doc.MakeAlias(this.rootDoc);
                             DocUtils.makeCustomViewClicked(alias, undefined, func.key);
-                            this.props.addDocTab(alias, 'add:right');
+                            this.props.addDocTab(alias, OpenWhere.addRight);
                         },
                     })
                 );
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index e21649648..2cc588b78 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -26,7 +26,7 @@ import { DashboardView } from '../DashboardView';
 import { Colors, Shadows } from '../global/globalEnums';
 import { LightboxView } from '../LightboxView';
 import { MainView } from '../MainView';
-import { DocFocusOptions, DocumentView, DocumentViewProps } from '../nodes/DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
 import { DashFieldView } from '../nodes/formattedText/DashFieldView';
 import { PinProps, PresBox, PresMovement } from '../nodes/trails';
 import { DefaultStyleProvider, StyleProp } from '../StyleProvider';
@@ -296,7 +296,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
         ) {
             const docs = Cast(Doc.MyOverlayDocs.data, listSpec(Doc), []);
             if (docs.includes(curPres)) docs.splice(docs.indexOf(curPres), 1);
-            CollectionDockingView.AddSplit(curPres, 'right');
+            CollectionDockingView.AddSplit(curPres, OpenWhereMod.right);
             setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), false, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things
         }
         setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs
@@ -343,34 +343,30 @@ export class TabDocView extends React.Component<TabDocViewProps> {
     //                                  "replace:right" - will replace the stack on the right named "right" if it exists, or create a stack on the right with that name,
     //                                   "replace:monkeys" - will replace any tab that has the label 'monkeys', or a tab with that label will be created by default on the right
     //  inPlace - will add the document to any collection along the path from the document to the docking view that has a field isInPlaceContainer. if none is found, inPlace adds a tab to current stack
-    addDocTab = (doc: Doc, location: string) => {
+    addDocTab = (doc: Doc, location: OpenWhere) => {
         SelectionManager.DeselectAll();
-        const locationFields = doc._viewType === CollectionViewType.Docking ? ['dashboard'] : location.split(':');
-        const locationParams = locationFields.length > 1 ? locationFields[1] : '';
+        const locationFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':');
+        const locationParams: OpenWhereMod = locationFields.length > 1 ? (locationFields[1] as OpenWhereMod) : OpenWhereMod.none;
         switch (locationFields[0]) {
-            case 'dashboard':
+            case OpenWhere.dashboard:
                 return DashboardView.openDashboard(doc);
-            case 'close':
+            case OpenWhere.close:
                 return CollectionDockingView.CloseSplit(doc, locationParams);
-            case 'fullScreen':
+            case OpenWhere.fullScreen:
                 return CollectionDockingView.OpenFullScreen(doc);
-            case 'replace':
+            case OpenWhere.replace:
                 return CollectionDockingView.ReplaceTab(doc, locationParams, this.stack);
-            // case "lightbox": {
-            //     // TabDocView.PinDoc(doc, { hidePresBox: true });
-            //     return LightboxView.AddDocTab(doc, location, undefined, this.addDocTab);
-            // }
-            case 'inPlace':
+            case OpenWhere.inPlace:
                 const inPlaceView = DocCast(doc.context) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.context)) : undefined;
                 if (inPlaceView) {
                     inPlaceView.dataDoc[Doc.LayoutFieldKey(inPlaceView.rootDoc)] = new List<Doc>([doc]);
                     return true;
-                }
-            case 'lightbox':
+                } // fall through to lightbox
+            case OpenWhere.lightbox:
                 return LightboxView.AddDocTab(doc, location, undefined, this.addDocTab);
-            case 'toggle':
+            case OpenWhere.toggle:
                 return CollectionDockingView.ToggleSplit(doc, locationParams, this.stack);
-            case 'add':
+            case OpenWhere.add:
             default:
                 return CollectionDockingView.AddSplit(doc, locationParams, this.stack);
         }
@@ -509,7 +505,7 @@ interface TabMinimapViewProps {
     document: Doc;
     hideMinimap: () => boolean;
     tabView: () => DocumentView | undefined;
-    addDocTab: (doc: Doc, where: string) => boolean;
+    addDocTab: (doc: Doc, where: OpenWhere) => boolean;
     PanelWidth: () => number;
     PanelHeight: () => number;
     background: () => string;
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 13cf64558..bd326f917 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -22,7 +22,7 @@ import { Transform } from '../../util/Transform';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { EditableView } from '../EditableView';
 import { TREE_BULLET_WIDTH } from '../global/globalCssVariables.scss';
-import { DocumentView, DocumentViewInternal, DocumentViewProps, StyleProviderFunc } from '../nodes/DocumentView';
+import { DocumentView, DocumentViewInternal, DocumentViewProps, OpenWhere, StyleProviderFunc } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
@@ -44,7 +44,7 @@ export interface TreeViewProps {
     containerCollection: Doc;
     renderDepth: number;
     dropAction: dropActionType;
-    addDocTab: (doc: Doc, where: string) => boolean;
+    addDocTab: (doc: Doc, where: OpenWhere) => boolean;
     panelWidth: () => number;
     panelHeight: () => number;
     addDocument: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean;
@@ -236,7 +236,7 @@ export class TreeView extends React.Component<TreeViewProps> {
             const bestAlias =
                 docView.props.Document.author === Doc.CurrentUserEmail && !Doc.IsPrototype(docView.props.Document) ? docView.props.Document : DocListCast(this.props.document.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail);
             const nextBestAlias = DocListCast(this.props.document.aliases).find(doc => doc.author === Doc.CurrentUserEmail);
-            this.props.addDocTab(bestAlias ?? nextBestAlias ?? Doc.MakeAlias(this.props.document), 'lightbox');
+            this.props.addDocTab(bestAlias ?? nextBestAlias ?? Doc.MakeAlias(this.props.document), OpenWhere.lightbox);
         }
     };
 
@@ -1109,7 +1109,7 @@ export class TreeView extends React.Component<TreeViewProps> {
         remove: undefined | ((doc: Doc | Doc[]) => boolean),
         move: DragManager.MoveFunction,
         dropAction: dropActionType,
-        addDocTab: (doc: Doc, where: string) => boolean,
+        addDocTab: (doc: Doc, where: OpenWhere) => boolean,
         styleProvider: undefined | StyleProviderFunc,
         screenToLocalXf: () => Transform,
         isContentActive: (outsideReaction?: boolean) => boolean,
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 54be6ba0f..7dd9cdb8b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -82,7 +82,7 @@ interface PivotColumn {
     filters: string[];
 }
 
-export function computerPassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
+export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
     const docMap = new Map<string, PoolData>();
     childPairs.forEach(({ layout, data }, i) => {
         docMap.set(layout[Id], {
@@ -97,7 +97,7 @@ export function computerPassLayout(poolData: Map<string, PoolData>, pivotDoc: Do
     return normalizeResults(panelDim, 12, docMap, poolData, viewDefsToJSX, [], 0, []);
 }
 
-export function computerStarburstLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
+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
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 57cccec4a..8cabf060d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -37,7 +37,7 @@ import { GestureOverlay } from '../../GestureOverlay';
 import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke';
 import { LightboxView } from '../../LightboxView';
 import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView';
-import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from '../../nodes/DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, ViewAdjustment, ViewSpecPrefix } from '../../nodes/DocumentView';
 import { FieldViewProps } from '../../nodes/FieldView';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
 import { PresBox } from '../../nodes/trails/PresBox';
@@ -47,7 +47,7 @@ import { StyleProp } from '../../StyleProvider';
 import { CollectionSubView } from '../CollectionSubView';
 import { TreeViewType } from '../CollectionTreeView';
 import { TabDocView } from '../TabDocView';
-import { computePivotLayout, computerPassLayout, computerStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines';
+import { computePivotLayout, computePassLayout as computePassLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines';
 import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors';
 import './CollectionFreeFormView.scss';
 import { MarqueeView } from './MarqueeView';
@@ -1272,7 +1272,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
     pointerEvents = () => {
         const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine);
-        const pointerEvents = this.props.isContentActive() === false ? 'none' : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === 'pass' && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.());
+        const pointerEvents =
+            this.props.isContentActive() === false ? 'none' : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.());
         return pointerEvents;
     };
     getChildDocView(entry: PoolData) {
@@ -1328,8 +1329,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             />
         );
     }
-    addDocTab = action((doc: Doc, where: string) => {
-        if (where === 'inParent') {
+    addDocTab = action((doc: Doc, where: OpenWhere) => {
+        if (where === OpenWhere.inParent) {
             (doc instanceof Doc ? [doc] : doc).forEach(doc => {
                 const pt = this.getTransform().transformPoint(NumCast(doc.x), NumCast(doc.y));
                 doc.x = pt[0];
@@ -1337,7 +1338,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             });
             return this.props.addDocument?.(doc) || false;
         }
-        if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) {
+        if (where === OpenWhere.inPlace && this.layoutDoc.isInPlaceContainer) {
             this.dataDoc[this.props.fieldKey] = doc instanceof Doc ? doc : new List<Doc>(doc as any as Doc[]);
             return true;
         }
@@ -1457,10 +1458,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const newPool = new Map<string, PoolData>();
         // prettier-ignore
         switch (this.layoutEngine) {
-            case 'pass':      return { newPool, computedElementData: this.doEngineLayout(newPool, computerPassLayout) };
-            case 'timeline':  return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) };
-            case 'pivot':     return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) };
-            case 'starburst': return { newPool, computedElementData: this.doEngineLayout(newPool, computerStarburstLayout) };
+            case computePassLayout.name :      return { newPool, computedElementData: this.doEngineLayout(newPool, computePassLayout) };
+            case computeTimelineLayout.name:  return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) };
+            case computePivotLayout.name:     return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) };
+            case computeStarburstLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computeStarburstLayout) };
         }
         return { newPool, computedElementData: this.doFreeformLayout(newPool) };
     }
@@ -1724,7 +1725,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             doc.x = scr?.[0];
             doc.y = scr?.[1];
         });
-        this.props.addDocTab(childDocs as any as Doc, 'inParent');
+        this.props.addDocTab(childDocs as any as Doc, OpenWhere.inParent);
         this.props.ContainingCollectionView?.removeDocument(this.props.Document);
     };
 
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index e5a2d9007..b88da5191 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -234,13 +234,6 @@ export class CollectionMulticolumnView extends CollectionSubView() {
     onChildClickHandler = () => ScriptCast(this.Document.onChildClick);
     onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick);
 
-    addDocTab = (doc: Doc, where: string) => {
-        if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) {
-            this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]);
-            return true;
-        }
-        return this.props.addDocTab(doc, where);
-    };
     focusDocument = (doc: Doc, options: DocFocusOptions) => this.props.focus(this.rootDoc, options);
     isContentActive = () => this.props.isSelected() || this.props.isContentActive() || this.props.isAnyChildContentActive();
     isChildContentActive = () => (((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.isContentActive() ? true : false);
@@ -278,7 +271,7 @@ export class CollectionMulticolumnView extends CollectionSubView() {
                 moveDocument={this.props.moveDocument}
                 removeDocument={this.props.removeDocument}
                 whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
-                addDocTab={this.addDocTab}
+                addDocTab={this.props.addDocTab}
                 pinToPres={this.props.pinToPres}
                 bringToFront={returnFalse}
             />
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
index adcd9e1e3..ef75fb159 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
@@ -1,35 +1,36 @@
-import React = require("react");
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable } from "mobx";
-import { observer } from "mobx-react";
-import { extname } from "path";
-import DatePicker from "react-datepicker";
-import { CellInfo } from "react-table";
-import { DateField } from "../../../../fields/DateField";
-import { Doc, DocListCast, Field, Opt } from "../../../../fields/Doc";
-import { Id } from "../../../../fields/FieldSymbols";
-import { List } from "../../../../fields/List";
-import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField";
-import { ComputedField } from "../../../../fields/ScriptField";
-import { BoolCast, Cast, DateCast, FieldValue, StrCast } from "../../../../fields/Types";
-import { ImageField } from "../../../../fields/URLField";
-import { emptyFunction, Utils } from "../../../../Utils";
-import { Docs } from "../../../documents/Documents";
-import { DocumentType } from "../../../documents/DocumentTypes";
-import { DocumentManager } from "../../../util/DocumentManager";
-import { DragManager } from "../../../util/DragManager";
-import { KeyCodes } from "../../../util/KeyCodes";
-import { CompileScript } from "../../../util/Scripting";
-import { SearchUtil } from "../../../util/SearchUtil";
-import { SnappingManager } from "../../../util/SnappingManager";
-import { undoBatch } from "../../../util/UndoManager";
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import { extname } from 'path';
+import DatePicker from 'react-datepicker';
+import { CellInfo } from 'react-table';
+import { DateField } from '../../../../fields/DateField';
+import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc';
+import { Id } from '../../../../fields/FieldSymbols';
+import { List } from '../../../../fields/List';
+import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
+import { ComputedField } from '../../../../fields/ScriptField';
+import { BoolCast, Cast, DateCast, FieldValue, StrCast } from '../../../../fields/Types';
+import { ImageField } from '../../../../fields/URLField';
+import { emptyFunction, Utils } from '../../../../Utils';
+import { Docs } from '../../../documents/Documents';
+import { DocumentType } from '../../../documents/DocumentTypes';
+import { DocumentManager } from '../../../util/DocumentManager';
+import { DragManager } from '../../../util/DragManager';
+import { KeyCodes } from '../../../util/KeyCodes';
+import { CompileScript } from '../../../util/Scripting';
+import { SearchUtil } from '../../../util/SearchUtil';
+import { SnappingManager } from '../../../util/SnappingManager';
+import { undoBatch } from '../../../util/UndoManager';
 import '../../../views/DocumentDecorations.scss';
-import { EditableView } from "../../EditableView";
+import { EditableView } from '../../EditableView';
 import { MAX_ROW_HEIGHT } from '../../global/globalCssVariables.scss';
-import { DocumentIconContainer } from "../../nodes/DocumentIcon";
-import { OverlayView } from "../../OverlayView";
-import { CollectionView } from "../CollectionView";
-import "./CollectionSchemaView.scss";
+import { DocumentIconContainer } from '../../nodes/DocumentIcon';
+import { OverlayView } from '../../OverlayView';
+import { CollectionView } from '../CollectionView';
+import './CollectionSchemaView.scss';
+import { OpenWhere } from '../../nodes/DocumentView';
 
 // intialize cell properties
 export interface CellProps {
@@ -46,10 +47,9 @@ export interface CellProps {
     // currently unused
     renderDepth: number;
     // called when a button is pressed on the node itself
-    addDocTab: (document: Doc, where: string) => boolean;
+    addDocTab: (document: Doc, where: OpenWhere) => boolean;
     pinToPres: (document: Doc) => void;
-    moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined,
-        addDocument: (document: Doc | Doc[]) => boolean) => boolean;
+    moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
     isFocused: boolean;
     changeFocusedCellByIndex: (row: number, col: number) => void;
     // set whether the cell is in the isEditing mode
@@ -67,7 +67,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
     // return a field key that is corrected for whether it COMMENT
     public static resolvedFieldKey(column: string, rowDoc: Doc) {
         const fieldKey = column;
-        if (fieldKey.startsWith("*")) {
+        if (fieldKey.startsWith('*')) {
             const rootKey = fieldKey.substring(1);
             const allKeys = [...Array.from(Object.keys(rowDoc)), ...Array.from(Object.keys(Doc.GetProto(rowDoc)))];
             const matchedKeys = allKeys.filter(key => key.includes(rootKey));
@@ -82,33 +82,37 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
     protected _rowDataDoc = Doc.GetProto(this.props.rowProps.original);
     // methods for dragging and dropping
     protected _dropDisposer?: DragManager.DragDropDisposer;
-    @observable contents: string = "";
+    @observable contents: string = '';
 
-    componentDidMount() { document.addEventListener("keydown", this.onKeyDown); }
-    componentWillUnmount() { document.removeEventListener("keydown", this.onKeyDown); }
+    componentDidMount() {
+        document.addEventListener('keydown', this.onKeyDown);
+    }
+    componentWillUnmount() {
+        document.removeEventListener('keydown', this.onKeyDown);
+    }
 
     @action
     onKeyDown = (e: KeyboardEvent): void => {
         // If a cell is editable and clicked, hitting enter shoudl allow the user to edit it
         if (this.props.isFocused && this.props.isEditable && e.keyCode === KeyCodes.ENTER) {
-            document.removeEventListener("keydown", this.onKeyDown);
+            document.removeEventListener('keydown', this.onKeyDown);
             this._isEditing = true;
             this.props.setIsEditing(true);
         }
-    }
+    };
 
     @action
     isEditingCallback = (isEditing: boolean): void => {
         // a general method that takes a boolean that determines whether the cell should be in
         // is-editing mode
         // remove the event listener if it's there
-        document.removeEventListener("keydown", this.onKeyDown);
+        document.removeEventListener('keydown', this.onKeyDown);
         // it's not already in is-editing mode, re-add the event listener
-        isEditing && document.addEventListener("keydown", this.onKeyDown);
+        isEditing && document.addEventListener('keydown', this.onKeyDown);
         this._isEditing = isEditing;
         this.props.setIsEditing(isEditing);
         this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
-    }
+    };
 
     @action
     onPointerDown = async (e: React.PointerEvent): Promise<void> => {
@@ -119,19 +123,19 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
         this.props.setPreviewDoc(this.props.rowProps.original);
 
         let url: string;
-        if (url = StrCast(this.props.rowProps.row.href)) {
+        if ((url = StrCast(this.props.rowProps.row.href))) {
             // opens up the the doc in a new window, blurring the old one
             try {
                 new URL(url);
                 const temp = window.open(url)!;
                 temp.blur();
                 window.focus();
-            } catch { }
+            } catch {}
         }
 
         const doc = Cast(this._rowDoc[this.renderFieldKey], Doc, null);
         doc && this.props.setPreviewDoc(doc);
-    }
+    };
 
     @undoBatch
     applyToDoc = (doc: Doc, row: number, col: number, run: (args?: { [name: string]: any }) => any) => {
@@ -142,7 +146,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
         doc[this.renderFieldKey] = res.result;
         return true;
         // return whether the change was successful
-    }
+    };
 
     private drop = (e: Event, de: DragManager.DropEvent) => {
         // if the drag has data at its completion
@@ -151,41 +155,51 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
             if (de.complete.docDragData.draggedDocuments.length === 1) {
                 // update the renderFieldKey
                 this._rowDataDoc[this.renderFieldKey] = de.complete.docDragData.draggedDocuments[0];
-            }
-            else {
+            } else {
                 // create schema document reflecting the new column arrangement
-                const coll = Docs.Create.SchemaDocument([new SchemaHeaderField("title", "#f1efeb")], de.complete.docDragData.draggedDocuments, {});
+                const coll = Docs.Create.SchemaDocument([new SchemaHeaderField('title', '#f1efeb')], de.complete.docDragData.draggedDocuments, {});
                 this._rowDataDoc[this.renderFieldKey] = coll;
             }
             e.stopPropagation();
         }
-    }
+    };
 
     protected dropRef = (ele: HTMLElement | null) => {
         // if the drop disposer is not undefined, run its function
         this._dropDisposer?.();
         // if ele is not null, give ele a non-undefined drop disposer
         ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)));
-    }
+    };
 
     returnHighlights(contents: string, positions?: number[]) {
         if (positions) {
             const results = [];
             StrCast(this.props.Document._searchString);
             const length = StrCast(this.props.Document._searchString).length;
-            const color = contents ? "black" : "grey";
+            const color = contents ? 'black' : 'grey';
 
-            results.push(<span key="-1" style={{ color }}>{contents?.slice(0, positions[0])}</span>);
+            results.push(
+                <span key="-1" style={{ color }}>
+                    {contents?.slice(0, positions[0])}
+                </span>
+            );
             positions.forEach((num, cur) => {
-                results.push(<span key={"start" + cur} style={{ backgroundColor: "#FFFF00", color }}>{contents?.slice(num, num + length)}</span>);
+                results.push(
+                    <span key={'start' + cur} style={{ backgroundColor: '#FFFF00', color }}>
+                        {contents?.slice(num, num + length)}
+                    </span>
+                );
                 let end = 0;
-                cur === positions.length - 1 ? end = contents.length : end = positions[cur + 1];
-                results.push(<span key={"end" + cur} style={{ color }}>{contents?.slice(num + length, end)}</span>);
-            }
-            );
+                cur === positions.length - 1 ? (end = contents.length) : (end = positions[cur + 1]);
+                results.push(
+                    <span key={'end' + cur} style={{ color }}>
+                        {contents?.slice(num + length, end)}
+                    </span>
+                );
+            });
             return results;
         }
-        return <span style={{ color: contents ? "black" : "grey" }}>{contents ? contents?.valueOf() : "undefined"}</span>;
+        return <span style={{ color: contents ? 'black' : 'grey' }}>{contents ? contents?.valueOf() : 'undefined'}</span>;
     }
 
     @computed get renderFieldKey() {
@@ -199,10 +213,9 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
             const aliasdoc = await SearchUtil.GetAliasesOfDocument(this._rowDataDoc);
             const targetContext = aliasdoc.length <= 0 ? undefined : Cast(aliasdoc[0].context, Doc, null);
             // Jump to the this document
-            DocumentManager.Instance.jumpToDocument(this._rowDoc, false, emptyFunction, targetContext ? [targetContext] : [],
-                undefined, undefined, undefined, () => this.props.setPreviewDoc(this._rowDoc));
+            DocumentManager.Instance.jumpToDocument(this._rowDoc, false, emptyFunction, targetContext ? [targetContext] : [], undefined, undefined, undefined, () => this.props.setPreviewDoc(this._rowDoc));
         }
-    }
+    };
 
     renderCellWithType(type: string | undefined) {
         const dragRef: React.RefObject<HTMLDivElement> = React.createRef();
@@ -214,29 +227,29 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
 
         const onPointerEnter = (e: React.PointerEvent): void => {
             // e.buttons === 1 means the left moue pointer is down
-            if (e.buttons === 1 && SnappingManager.GetIsDragging() && (type === "document" || type === undefined)) {
-                dragRef.current!.className = "collectionSchemaView-cellContainer doc-drag-over";
+            if (e.buttons === 1 && SnappingManager.GetIsDragging() && (type === 'document' || type === undefined)) {
+                dragRef.current!.className = 'collectionSchemaView-cellContainer doc-drag-over';
             }
         };
         const onPointerLeave = (e: React.PointerEvent): void => {
             // change the class name to indicate that the cell is no longer being dragged
-            dragRef.current!.className = "collectionSchemaView-cellContainer";
+            dragRef.current!.className = 'collectionSchemaView-cellContainer';
         };
 
         let contents = Field.toString(field as Field);
         // display 2 hyphens instead of a blank box for empty cells
-        contents = contents === "" ? "--" : contents;
+        contents = contents === '' ? '--' : contents;
 
         // classname reflects the tatus of the cell
-        let className = "collectionSchemaView-cellWrapper";
-        if (this._isEditing) className += " editing";
-        if (this.props.isFocused && this.props.isEditable) className += " focused";
-        if (this.props.isFocused && !this.props.isEditable) className += " inactive";
+        let className = 'collectionSchemaView-cellWrapper';
+        if (this._isEditing) className += ' editing';
+        if (this.props.isFocused && this.props.isEditable) className += ' focused';
+        if (this.props.isFocused && !this.props.isEditable) className += ' inactive';
 
         const positions = [];
-        if (StrCast(this.props.Document._searchString).toLowerCase() !== "") {
+        if (StrCast(this.props.Document._searchString).toLowerCase() !== '') {
             // term is ...promise pending... if the field is a Promise, otherwise it is the cell's contents
-            let term = (field instanceof Promise) ? "...promise pending..." : contents.toLowerCase();
+            let term = field instanceof Promise ? '...promise pending...' : contents.toLowerCase();
             const search = StrCast(this.props.Document._searchString).toLowerCase();
             let start = term.indexOf(search);
             let tally = 0;
@@ -256,56 +269,60 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
                 positions.pop();
             }
         }
-        const placeholder = type === "number" ? "0" : contents === "" ? "--" : "undefined";
+        const placeholder = type === 'number' ? '0' : contents === '' ? '--' : 'undefined';
         return (
-            <div className="collectionSchemaView-cellContainer" style={{ cursor: field instanceof Doc ? "grab" : "auto" }}
-                ref={dragRef} onPointerDown={this.onPointerDown} onClick={action(e => this._isEditing = true)} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}>
+            <div
+                className="collectionSchemaView-cellContainer"
+                style={{ cursor: field instanceof Doc ? 'grab' : 'auto' }}
+                ref={dragRef}
+                onPointerDown={this.onPointerDown}
+                onClick={action(e => (this._isEditing = true))}
+                onPointerEnter={onPointerEnter}
+                onPointerLeave={onPointerLeave}>
                 <div className={className} ref={this._focusRef} tabIndex={-1}>
-                    <div className="collectionSchemaView-cellContents" ref={type === undefined || type === "document" ? this.dropRef : null}>
-                        {!this.props.Document._searchDoc ?
+                    <div className="collectionSchemaView-cellContents" ref={type === undefined || type === 'document' ? this.dropRef : null}>
+                        {!this.props.Document._searchDoc ? (
                             <EditableView
                                 editing={this._isEditing}
                                 isEditingCallback={this.isEditingCallback}
-                                display={"inline"}
+                                display={'inline'}
                                 contents={contents}
-                                height={"auto"}
+                                height={'auto'}
                                 maxHeight={Number(MAX_ROW_HEIGHT)}
                                 placeholder={placeholder}
                                 GetValue={() => {
                                     const cfield = ComputedField.WithoutComputed(() => FieldValue(field));
                                     const cscript = cfield instanceof ComputedField ? cfield.script.originalScript : undefined;
-                                    const cfinalScript = cscript?.split("return")[cscript.split("return").length - 1];
-                                    return cscript ? (cfinalScript?.endsWith(";") ? `:=${cfinalScript?.substring(0, cfinalScript.length - 2)}` : cfinalScript) :
-                                        Field.IsField(cfield) ? Field.toScriptString(cfield) : "";
+                                    const cfinalScript = cscript?.split('return')[cscript.split('return').length - 1];
+                                    return cscript ? (cfinalScript?.endsWith(';') ? `:=${cfinalScript?.substring(0, cfinalScript.length - 2)}` : cfinalScript) : Field.IsField(cfield) ? Field.toScriptString(cfield) : '';
                                 }}
                                 SetValue={action((value: string) => {
                                     // sets what is displayed after the user makes an input
                                     let retVal = false;
-                                    if (value.startsWith(":=") || value.startsWith("=:=")) {
+                                    if (value.startsWith(':=') || value.startsWith('=:=')) {
                                         // decides how to compute a value when given either of the above strings
-                                        const script = value.substring(value.startsWith("=:=") ? 3 : 2);
-                                        retVal = this.props.setComputed(script, value.startsWith(":=") ? this._rowDataDoc : this._rowDoc, this.renderFieldKey, this.props.row, this.props.col);
+                                        const script = value.substring(value.startsWith('=:=') ? 3 : 2);
+                                        retVal = this.props.setComputed(script, value.startsWith(':=') ? this._rowDataDoc : this._rowDoc, this.renderFieldKey, this.props.row, this.props.col);
                                     } else {
                                         // check if the input is a number
                                         let inputIsNum = true;
                                         for (const s of value) {
-                                            if (isNaN(parseInt(s)) && !(s === ".") && !(s === ",")) {
+                                            if (isNaN(parseInt(s)) && !(s === '.') && !(s === ',')) {
                                                 inputIsNum = false;
                                             }
                                         }
                                         // check if the input is a boolean
-                                        const inputIsBool: boolean = value === "false" || value === "true";
-                                        // what to do in the case 
-                                        if (!inputIsNum && !inputIsBool && !value.startsWith("=")) {
+                                        const inputIsBool: boolean = value === 'false' || value === 'true';
+                                        // what to do in the case
+                                        if (!inputIsNum && !inputIsBool && !value.startsWith('=')) {
                                             // if it's not a number, it's a string, and should be processed as such
-                                            // strips the string of quotes when it is edited to prevent quotes form being added to the text automatically 
+                                            // strips the string of quotes when it is edited to prevent quotes form being added to the text automatically
                                             // after each edit
                                             let valueSansQuotes = value;
                                             if (this._isEditing) {
                                                 const vsqLength = valueSansQuotes.length;
                                                 // get rid of outer quotes
-                                                valueSansQuotes = valueSansQuotes.substring(value.startsWith("\"") ? 1 : 0,
-                                                    valueSansQuotes.charAt(vsqLength - 1) === "\"" ? vsqLength - 1 : vsqLength);
+                                                valueSansQuotes = valueSansQuotes.substring(value.startsWith('"') ? 1 : 0, valueSansQuotes.charAt(vsqLength - 1) === '"' ? vsqLength - 1 : vsqLength);
                                             }
                                             let inputAsString = '"';
                                             // escape any quotes in the string
@@ -319,27 +336,27 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
                                             // add a closing quote
                                             inputAsString += '"';
                                             //two options here: we can strip off outer quotes or we can figure out what's going on with the script
-                                            const script = CompileScript(inputAsString, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
+                                            const script = CompileScript(inputAsString, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } });
                                             const changeMade = inputAsString.length !== value.length || inputAsString.length - 2 !== value.length;
                                             // change it if a change is made, otherwise, just compile using the old cell conetnts
                                             script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run));
                                             // handle numbers and expressions
-                                        } else if (inputIsNum || value.startsWith("=")) {
+                                        } else if (inputIsNum || value.startsWith('=')) {
                                             //TODO: make accept numbers
-                                            const inputscript = value.substring(value.startsWith("=") ? 1 : 0);
+                                            const inputscript = value.substring(value.startsWith('=') ? 1 : 0);
                                             // if commas are not stripped, the parser only considers the numbers after the last comma
-                                            let inputSansCommas = "";
+                                            let inputSansCommas = '';
                                             for (const s of inputscript) {
-                                                if (!(s === ",")) {
+                                                if (!(s === ',')) {
                                                     inputSansCommas += s;
                                                 }
                                             }
-                                            const script = CompileScript(inputSansCommas, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
+                                            const script = CompileScript(inputSansCommas, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } });
                                             const changeMade = value.length - 2 !== value.length;
                                             script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run));
                                             // handle booleans
                                         } else if (inputIsBool) {
-                                            const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
+                                            const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } });
                                             const changeMade = value.length - 2 !== value.length;
                                             script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run));
                                         }
@@ -352,33 +369,47 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
                                 })}
                                 OnFillDown={async (value: string) => {
                                     // computes all of the value preceded by :=
-                                    const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
-                                    script.compiled && DocListCast(this.props.Document[this.props.fieldKey]).
-                                        forEach((doc, i) => value.startsWith(":=") ?
-                                            this.props.setComputed(value.substring(2), Doc.GetProto(doc), this.renderFieldKey, i, this.props.col) :
-                                            this.applyToDoc(Doc.GetProto(doc), i, this.props.col, script.run));
+                                    const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } });
+                                    script.compiled &&
+                                        DocListCast(this.props.Document[this.props.fieldKey]).forEach((doc, i) =>
+                                            value.startsWith(':=') ? this.props.setComputed(value.substring(2), Doc.GetProto(doc), this.renderFieldKey, i, this.props.col) : this.applyToDoc(Doc.GetProto(doc), i, this.props.col, script.run)
+                                        );
                                 }}
                             />
-                            :
+                        ) : (
                             this.returnHighlights(contents, positions)
-                        }
-                    </div >
+                        )}
+                    </div>
                 </div>
             </div>
         );
     }
 
-    render() { return this.renderCellWithType(undefined); }
+    render() {
+        return this.renderCellWithType(undefined);
+    }
 }
 
 @observer
-export class CollectionSchemaNumberCell extends CollectionSchemaCell { render() { return this.renderCellWithType("number"); } }
+export class CollectionSchemaNumberCell extends CollectionSchemaCell {
+    render() {
+        return this.renderCellWithType('number');
+    }
+}
 
 @observer
-export class CollectionSchemaBooleanCell extends CollectionSchemaCell { render() { return this.renderCellWithType("boolean"); } }
+export class CollectionSchemaBooleanCell extends CollectionSchemaCell {
+    render() {
+        return this.renderCellWithType('boolean');
+    }
+}
 
 @observer
-export class CollectionSchemaStringCell extends CollectionSchemaCell { render() { return this.renderCellWithType("string"); } }
+export class CollectionSchemaStringCell extends CollectionSchemaCell {
+    render() {
+        return this.renderCellWithType('string');
+    }
+}
 
 @observer
 export class CollectionSchemaDateCell extends CollectionSchemaCell {
@@ -396,24 +427,24 @@ export class CollectionSchemaDateCell extends CollectionSchemaCell {
         // ^ DateCast is always undefined for some reason, but that is what the field should be set to
         this._rowDoc[this.renderFieldKey] = new DateField(date as Date);
         //}
-    }
+    };
 
     render() {
-        return !this.props.isFocused ? <span onPointerDown={this.onPointerDown}>{this._date ? Field.toString(this._date as Field) : "--"}</span> :
-            <DatePicker
-                selected={this._date?.date || new Date}
-                onSelect={date => this.handleChange(date)}
-                onChange={date => this.handleChange(date)}
-            />;
+        return !this.props.isFocused ? (
+            <span onPointerDown={this.onPointerDown}>{this._date ? Field.toString(this._date as Field) : '--'}</span>
+        ) : (
+            <DatePicker selected={this._date?.date || new Date()} onSelect={date => this.handleChange(date)} onChange={date => this.handleChange(date)} />
+        );
     }
 }
 
 @observer
 export class CollectionSchemaDocCell extends CollectionSchemaCell {
-
     _overlayDisposer?: () => void;
 
-    @computed get _doc() { return FieldValue(Cast(this._rowDoc[this.renderFieldKey], Doc)); }
+    @computed get _doc() {
+        return FieldValue(Cast(this._rowDoc[this.renderFieldKey], Doc));
+    }
 
     @action
     onSetValue = (value: string) => {
@@ -422,7 +453,7 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell {
         const script = CompileScript(value, {
             addReturn: true,
             typecheck: true,
-            transformer: DocumentIconContainer.getTransformer()
+            transformer: DocumentIconContainer.getTransformer(),
         });
         // compile the script
         const results = script.compiled && script.run();
@@ -432,44 +463,43 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell {
             return true;
         }
         return false;
-    }
+    };
 
-    componentWillUnmount() { this.onBlur(); }
+    componentWillUnmount() {
+        this.onBlur();
+    }
 
-    onBlur = () => { this._overlayDisposer?.(); };
+    onBlur = () => {
+        this._overlayDisposer?.();
+    };
     onFocus = () => {
         this.onBlur();
         this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
-    }
+    };
 
     @action
     isEditingCallback = (isEditing: boolean): void => {
         // the isEditingCallback from a general CollectionSchemaCell
-        document.removeEventListener("keydown", this.onKeyDown);
-        isEditing && document.addEventListener("keydown", this.onKeyDown);
+        document.removeEventListener('keydown', this.onKeyDown);
+        isEditing && document.addEventListener('keydown', this.onKeyDown);
         this._isEditing = isEditing;
         this.props.setIsEditing(isEditing);
         this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
-    }
+    };
 
     render() {
         // if there's a doc, render it
-        return !this._doc ? this.renderCellWithType("document") :
-            <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1}
-                onPointerDown={this.onPointerDown}
-            >
-                <div className="collectionSchemaView-cellContents-document"
-                    style={{ padding: "5.9px" }}
-                    ref={this.dropRef}
-                    onFocus={this.onFocus}
-                    onBlur={this.onBlur}
-                >
+        return !this._doc ? (
+            this.renderCellWithType('document')
+        ) : (
+            <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
+                <div className="collectionSchemaView-cellContents-document" style={{ padding: '5.9px' }} ref={this.dropRef} onFocus={this.onFocus} onBlur={this.onBlur}>
                     <EditableView
                         editing={this._isEditing}
                         isEditingCallback={this.isEditingCallback}
-                        display={"inline"}
-                        contents={this._doc.title || "--"}
-                        height={"auto"}
+                        display={'inline'}
+                        contents={this._doc.title || '--'}
+                        height={'auto'}
                         maxHeight={Number(MAX_ROW_HEIGHT)}
                         GetValue={() => StrCast(this._doc?.title)}
                         SetValue={action((value: string) => {
@@ -477,33 +507,36 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell {
                             return true;
                         })}
                     />
-                </div >
-                <div onClick={() => this._doc && this.props.addDocTab(this._doc, "add:right")} className="collectionSchemaView-cellContents-docButton">
+                </div>
+                <div onClick={() => this._doc && this.props.addDocTab(this._doc, OpenWhere.addRight)} className="collectionSchemaView-cellContents-docButton">
                     <FontAwesomeIcon icon="external-link-alt" size="lg" />
                 </div>
-            </div>;
+            </div>
+        );
     }
 }
 
 @observer
 export class CollectionSchemaImageCell extends CollectionSchemaCell {
-
     choosePath(url: URL) {
-        if (url.protocol === "data") return url.href; // if the url ises the data protocol, just return the href
+        if (url.protocol === 'data') return url.href; // if the url ises the data protocol, just return the href
         if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); // otherwise, put it through the cors proxy erver
-        if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href;//Why is this here — good question
+        if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href; //Why is this here — good question
 
         const ext = extname(url.href);
-        return url.href.replace(ext, "_o" + ext);
+        return url.href.replace(ext, '_o' + ext);
     }
 
     render() {
         const field = Cast(this._rowDoc[this.renderFieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc
-        const alts = DocListCast(this._rowDoc[this.renderFieldKey + "-alternates"]); // retrieve alternate documents that may be rendered as alternate images
-        const altpaths = alts.map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url).filter(url => url).map(url => this.choosePath(url)); // access the primary layout data of the alternate documents
+        const alts = DocListCast(this._rowDoc[this.renderFieldKey + '-alternates']); // retrieve alternate documents that may be rendered as alternate images
+        const altpaths = alts
+            .map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url)
+            .filter(url => url)
+            .map(url => this.choosePath(url)); // access the primary layout data of the alternate documents
         const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths;
         // If there is a path, follow it; otherwise, follow a link to a default image icon
-        const url = paths.length ? paths : [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")];
+        const url = paths.length ? paths : [Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')];
 
         const aspect = Doc.NativeAspect(this._rowDoc); // aspect ratio
         let width = Math.min(75, this.props.rowProps.width); // get a with that is no smaller than 75px
@@ -511,25 +544,28 @@ export class CollectionSchemaImageCell extends CollectionSchemaCell {
         width = height * aspect; // increase the width of the image if necessary to maintain proportionality
 
         const reference = React.createRef<HTMLDivElement>();
-        return <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
-            <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}>
-                <img src={url[0]}
-                    width={paths.length ? width : "20px"}
-                    height={paths.length ? height : "20px"} />
-            </div >
-        </div>;
+        return (
+            <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
+                <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}>
+                    <img src={url[0]} width={paths.length ? width : '20px'} height={paths.length ? height : '20px'} />
+                </div>
+            </div>
+        );
     }
 }
 
-
 @observer
 export class CollectionSchemaListCell extends CollectionSchemaCell {
     _overlayDisposer?: () => void;
 
-    @computed get _field() { return this._rowDoc[this.renderFieldKey]; }
-    @computed get _optionsList() { return this._field as List<any>; }
+    @computed get _field() {
+        return this._rowDoc[this.renderFieldKey];
+    }
+    @computed get _optionsList() {
+        return this._field as List<any>;
+    }
     @observable private _opened = false; // whether the list is opened
-    @observable private _text = "select an item";
+    @observable private _text = 'select an item';
     @observable private _selectedNum = 0; // the index of the list item selected
 
     @action
@@ -538,102 +574,109 @@ export class CollectionSchemaListCell extends CollectionSchemaCell {
         this._optionsList[this._selectedNum] = this._text = value;
 
         (this._field as List<any>).splice(this._selectedNum, 1, value);
-    }
+    };
 
     @action
     onSelected = (element: string, index: number) => {
         // if an item is selected, the private variables should update to reflect this
         this._text = element;
         this._selectedNum = index;
-    }
+    };
 
     onFocus = () => {
         this._overlayDisposer?.();
         this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
-    }
+    };
 
     render() {
         const link = false;
         const reference = React.createRef<HTMLDivElement>();
 
-        // if the list is not opened, don't display it; otherwise, do. 
+        // if the list is not opened, don't display it; otherwise, do.
         if (this._optionsList?.length) {
-            const options = !this._opened ? (null) :
+            const options = !this._opened ? null : (
                 <div>
                     {this._optionsList.map((element, index) => {
                         const val = Field.toString(element);
-                        return <div className="collectionSchemaView-dropdownOption" key={index} style={{ padding: "6px" }} onPointerDown={(e) => this.onSelected(StrCast(element), index)} >
-                            {val}
-                        </div>;
+                        return (
+                            <div className="collectionSchemaView-dropdownOption" key={index} style={{ padding: '6px' }} onPointerDown={e => this.onSelected(StrCast(element), index)}>
+                                {val}
+                            </div>
+                        );
                     })}
-                </div>;
-
-            const plainText = <div style={{ padding: "5.9px" }}>{this._text}</div>;
-            const textarea = <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} style={{ padding: "5.9px" }} ref={this.dropRef} >
-                <EditableView
-                    editing={this._isEditing}
-                    isEditingCallback={this.isEditingCallback}
-                    display={"inline"}
-                    contents={this._text}
-                    height={"auto"}
-                    maxHeight={Number(MAX_ROW_HEIGHT)}
-                    GetValue={() => this._text}
-                    SetValue={action((value: string) => {
-                        // add special for params 
-                        this.onSetValue(value);
-                        return true;
-                    })}
-                />
-            </div >;
+                </div>
+            );
+
+            const plainText = <div style={{ padding: '5.9px' }}>{this._text}</div>;
+            const textarea = (
+                <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} style={{ padding: '5.9px' }} ref={this.dropRef}>
+                    <EditableView
+                        editing={this._isEditing}
+                        isEditingCallback={this.isEditingCallback}
+                        display={'inline'}
+                        contents={this._text}
+                        height={'auto'}
+                        maxHeight={Number(MAX_ROW_HEIGHT)}
+                        GetValue={() => this._text}
+                        SetValue={action((value: string) => {
+                            // add special for params
+                            this.onSetValue(value);
+                            return true;
+                        })}
+                    />
+                </div>
+            );
 
             //☰
             return (
                 <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
                     <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}>
                         <div className="collectionSchemaView-dropDownWrapper">
-                            <button type="button" className="collectionSchemaView-dropdownButton" style={{ right: "length", position: "relative" }}
-                                onClick={action(e => this._opened = !this._opened)} >
-                                <FontAwesomeIcon icon={this._opened ? "caret-up" : "caret-down"} size="sm" />
+                            <button type="button" className="collectionSchemaView-dropdownButton" style={{ right: 'length', position: 'relative' }} onClick={action(e => (this._opened = !this._opened))}>
+                                <FontAwesomeIcon icon={this._opened ? 'caret-up' : 'caret-down'} size="sm" />
                             </button>
                             <div className="collectionSchemaView-dropdownText"> {link ? plainText : textarea} </div>
                         </div>
                         {options}
-                    </div >
+                    </div>
                 </div>
             );
         }
-        return this.renderCellWithType("list");
+        return this.renderCellWithType('list');
     }
 }
 
-
 @observer
 export class CollectionSchemaCheckboxCell extends CollectionSchemaCell {
-    @computed get _isChecked() { return BoolCast(this._rowDoc[this.renderFieldKey]); }
+    @computed get _isChecked() {
+        return BoolCast(this._rowDoc[this.renderFieldKey]);
+    }
 
     render() {
         const reference = React.createRef<HTMLDivElement>();
         return (
             <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
-                <input type="checkbox" checked={this._isChecked} onChange={e => this._rowDoc[this.renderFieldKey] = e.target.checked} />
+                <input type="checkbox" checked={this._isChecked} onChange={e => (this._rowDoc[this.renderFieldKey] = e.target.checked)} />
             </div>
         );
     }
 }
 
-
 @observer
 export class CollectionSchemaButtons extends CollectionSchemaCell {
     // the navigation buttons for schema view when it is used for search.
     render() {
-        return !this.props.Document._searchDoc || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this._rowDoc.type) as DocumentType) ? <></> :
-            <div style={{ paddingTop: 8, paddingLeft: 3 }} >
+        return !this.props.Document._searchDoc || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this._rowDoc.type) as DocumentType) ? (
+            <></>
+        ) : (
+            <div style={{ paddingTop: 8, paddingLeft: 3 }}>
                 <button style={{ padding: 2, left: 77 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, true)}>
                     <FontAwesomeIcon icon="arrow-up" size="sm" />
                 </button>
-                <button style={{ padding: 2 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, false)} >
+                <button style={{ padding: 2 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, false)}>
                     <FontAwesomeIcon icon="arrow-down" size="sm" />
                 </button>
-            </div>;
+            </div>
+        );
     }
 }
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx
index f872637e5..3cb2df7d3 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx
@@ -10,6 +10,7 @@ import { SnappingManager } from '../../../util/SnappingManager';
 import { Transform } from '../../../util/Transform';
 import { undoBatch } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
+import { OpenWhere } from '../../nodes/DocumentView';
 import './CollectionSchemaView.scss';
 
 export interface MovableRowProps {
@@ -138,7 +139,7 @@ export class MovableRow extends React.Component<React.PropsWithChildren<MovableR
                             <div className="row-option" style={{ cursor: 'grab' }} ref={reference} onPointerDown={onItemDown}>
                                 <FontAwesomeIcon icon="grip-vertical" size="sm" />
                             </div>
-                            <div className="row-option" onClick={() => this.props.addDocTab(this.props.rowInfo.original, 'add:right')}>
+                            <div className="row-option" onClick={() => this.props.addDocTab(this.props.rowInfo.original, OpenWhere.addRight)}>
                                 <FontAwesomeIcon icon="external-link-alt" size="sm" />
                             </div>
                         </div>
diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx
index fafea5ce3..45ad4f86b 100644
--- a/src/client/views/collections/collectionSchema/SchemaTable.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTable.tsx
@@ -23,7 +23,7 @@ import { undoBatch } from '../../../util/UndoManager';
 import '../../../views/DocumentDecorations.scss';
 import { ContextMenu } from '../../ContextMenu';
 import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../global/globalCssVariables.scss';
-import { DocumentView } from '../../nodes/DocumentView';
+import { DocumentView, OpenWhere } from '../../nodes/DocumentView';
 import { DefaultStyleProvider } from '../../StyleProvider';
 import { CollectionView } from '../CollectionView';
 import {
@@ -86,7 +86,7 @@ export interface SchemaTableProps {
     ScreenToLocalTransform: () => Transform;
     active: (outsideReaction: boolean | undefined) => boolean | undefined;
     onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void;
-    addDocTab: (document: Doc, where: string) => boolean;
+    addDocTab: (document: Doc, where: OpenWhere) => boolean;
     pinToPres: (document: Doc) => void;
     isSelected: (outsideReaction?: boolean) => boolean;
     isFocused: (document: Doc, outsideReaction: boolean) => boolean;
@@ -625,7 +625,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
     };
 
     onOpenClick = () => {
-        this._showDoc && this.props.addDocTab(this._showDoc, 'add:right');
+        this._showDoc && this.props.addDocTab(this._showDoc, OpenWhere.addRight);
     };
 
     getPreviewTransform = (): Transform => {
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 570039550..868822fbf 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -16,7 +16,7 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm/Collec
 import { DocComponent } from '../DocComponent';
 import { StyleProp } from '../StyleProvider';
 import './CollectionFreeFormDocumentView.scss';
-import { DocumentView, DocumentViewProps } from './DocumentView';
+import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView';
 import React = require('react');
 
 export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
@@ -199,7 +199,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                 topDoc.x = spt[0];
                 topDoc.y = spt[1];
                 this.props.removeDocument?.(topDoc);
-                this.props.addDocTab(topDoc, 'inParent');
+                this.props.addDocTab(topDoc, OpenWhere.inParent);
             } else {
                 const spt = this.screenToLocalTransform().inverse().transformPoint(0, 0);
                 const fpt = screenXf.transformPoint(spt[0], spt[1]);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index fb07f1033..2729a5047 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -69,6 +69,27 @@ export enum ViewAdjustment {
     doNothing = 0,
 }
 
+export enum OpenWhere {
+    inPlace = 'inPlace',
+    lightbox = 'lightbox',
+    add = 'add',
+    addRight = 'add:right',
+    addBottom = 'add:bottom',
+    dashboard = 'dashboard',
+    close = 'close',
+    fullScreen = 'fullScreen',
+    toggle = 'toggle',
+    replace = 'replace',
+    inParent = 'inParent',
+}
+export enum OpenWhereMod {
+    none = '',
+    left = 'left',
+    right = 'right',
+    top = 'top',
+    bottom = 'bottom',
+}
+
 export const ViewSpecPrefix = 'viewSpec'; // field prefix for anchor fields that are immediately copied over to the target document when link is followed.  Other anchor properties will be copied over in the specific setViewSpec() method on their view (which allows for seting preview values instead of writing to the document)
 
 export interface DocFocusOptions {
@@ -139,7 +160,7 @@ export interface DocumentViewSharedProps {
     showTitle?: () => string;
     whenChildContentsActiveChanged: (isActive: boolean) => void;
     rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected
-    addDocTab: (doc: Doc, where: string) => boolean;
+    addDocTab: (doc: Doc, where: OpenWhere) => boolean;
     filterAddDocument?: (doc: Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example)
     addDocument?: (doc: Doc | Doc[]) => boolean;
     removeDocument?: (doc: Doc | Doc[]) => boolean;
@@ -474,7 +495,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
         RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15);
 
-        // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "map-pin", selected: -1 });
+        // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), OpenWhere.addRight), icon: "map-pin", selected: -1 });
         const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]);
         (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) &&
             RadialMenu.Instance.addItem({
@@ -485,7 +506,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 icon: 'external-link-square-alt',
                 selected: -1,
             });
-        // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "add:right"), icon: "trash", selected: -1 });
+        // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, OpenWhere.addRight), icon: "trash", selected: -1 });
         RadialMenu.Instance.addItem({ description: 'Pin', event: () => this.props.pinToPres(this.props.Document, {}), icon: 'map-pin', selected: -1 });
         RadialMenu.Instance.addItem({ description: 'Open', event: () => MobileInterface.Instance.handleClick(this.props.Document), icon: 'trash', selected: -1 });
 
@@ -586,7 +607,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                         );
                     UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click');
                 } else if (!Doc.IsSystem(this.rootDoc) && !this.rootDoc.isLinkButton) {
-                    UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, 'lightbox', this.props.LayoutTemplate?.(), this.props.addDocTab), 'double tap');
+                    UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox, this.props.LayoutTemplate?.(), this.props.addDocTab), 'double tap');
                     SelectionManager.DeselectAll();
                     Doc.UnBrushDoc(this.props.Document);
                 }
@@ -857,7 +878,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null);
             const appearance = cm.findByDescription('UI Controls...');
             const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : [];
-            !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this.props.addDocTab(templateDoc, 'add:right'), icon: 'eye' });
+            !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this.props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
             !Doc.noviceMode &&
                 appearanceItems.push({
                     description: 'Add a Field',
@@ -957,8 +978,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         }
         const help = cm.findByDescription('Help...');
         const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : [];
-        helpItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), 'add:right'), 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 }), 'add:right'), icon: 'keyboard' });
+        helpItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), OpenWhere.addRight), 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' });
 
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 7d04c4b64..18c5b81ec 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -20,6 +20,7 @@ import { ContextMenuProps } from '../ContextMenuItem';
 import e = require('express');
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { ImageBox } from './ImageBox';
+import { OpenWhere } from './DocumentView';
 
 export type KVPScript = {
     script: CompiledScript;
@@ -259,8 +260,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
         openItems.push({
             description: 'Default Perspective',
             event: () => {
-                this.props.addDocTab(this.props.Document, 'close');
-                this.props.addDocTab(this.fieldDocToLayout, 'add:right');
+                this.props.addDocTab(this.props.Document, OpenWhere.close);
+                this.props.addDocTab(this.fieldDocToLayout, OpenWhere.addRight);
             },
             icon: 'image',
         });
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 80def3025..e74ef4a39 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -1,18 +1,19 @@
 import { action, observable } from 'mobx';
-import { observer } from "mobx-react";
+import { observer } from 'mobx-react';
 import { Doc, Field, Opt } from '../../../fields/Doc';
 import { emptyFunction, returnFalse, returnOne, returnZero, returnEmptyFilter, returnEmptyDoclist, emptyPath } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { Transform } from '../../util/Transform';
 import { undoBatch } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
-import { EditableView } from "../EditableView";
+import { EditableView } from '../EditableView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { KeyValueBox } from './KeyValueBox';
-import "./KeyValueBox.scss";
-import "./KeyValuePair.scss";
-import React = require("react");
+import './KeyValueBox.scss';
+import './KeyValuePair.scss';
+import React = require('react');
 import { DefaultStyleProvider } from '../StyleProvider';
+import { OpenWhere } from './DocumentView';
 
 // Represents one row in a key value plane
 
@@ -23,7 +24,7 @@ export interface KeyValuePairProps {
     keyWidth: number;
     PanelHeight: () => number;
     PanelWidth: () => number;
-    addDocTab: (doc: Doc, where: string) => boolean;
+    addDocTab: (doc: Doc, where: OpenWhere) => boolean;
 }
 @observer
 export class KeyValuePair extends React.Component<KeyValuePairProps> {
@@ -34,23 +35,23 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
     @action
     handleCheck = (e: React.ChangeEvent<HTMLInputElement>) => {
         this.isChecked = e.currentTarget.checked;
-    }
+    };
 
     @action
     uncheck = () => {
         this.checkbox.current!.checked = false;
         this.isChecked = false;
-    }
+    };
 
     onContextMenu = (e: React.MouseEvent) => {
         const value = this.props.doc[this.props.keyName];
         if (value instanceof Doc) {
             e.stopPropagation();
             e.preventDefault();
-            ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" });
+            ContextMenu.Instance.addItem({ description: 'Open Fields', event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'layer-group' });
             ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
         }
-    }
+    };
 
     render() {
         const props: FieldViewProps = {
@@ -68,7 +69,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
             isSelected: returnFalse,
             setHeight: returnFalse,
             select: emptyFunction,
-            dropAction: "alias",
+            dropAction: 'alias',
             bringToFront: emptyFunction,
             renderDepth: 1,
             isContentActive: returnFalse,
@@ -92,30 +93,30 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
             doc = doc.proto;
         }
         const parenCount = Math.max(0, protoCount - 1);
-        const keyStyle = protoCount === 0 ? "black" : "blue";
+        const keyStyle = protoCount === 0 ? 'black' : 'blue';
 
-        const hover = { transition: "0.3s ease opacity", opacity: this.isPointerOver || this.isChecked ? 1 : 0 };
+        const hover = { transition: '0.3s ease opacity', opacity: this.isPointerOver || this.isChecked ? 1 : 0 };
 
         return (
-            <tr className={this.props.rowStyle} onPointerEnter={action(() => this.isPointerOver = true)} onPointerLeave={action(() => this.isPointerOver = false)}>
+            <tr className={this.props.rowStyle} onPointerEnter={action(() => (this.isPointerOver = true))} onPointerLeave={action(() => (this.isPointerOver = false))}>
                 <td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}>
                     <div className="keyValuePair-td-key-container">
-                        <button style={hover} className="keyValuePair-td-key-delete" onClick={undoBatch(() => {
-                            if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) {
-                                delete props.Document[props.fieldKey];
-                            }
-                            else delete props.Document.proto![props.fieldKey];
-                        })}>
+                        <button
+                            style={hover}
+                            className="keyValuePair-td-key-delete"
+                            onClick={undoBatch(() => {
+                                if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) {
+                                    delete props.Document[props.fieldKey];
+                                } else delete props.Document.proto![props.fieldKey];
+                            })}>
                             X
                         </button>
-                        <input
-                            className={"keyValuePair-td-key-check"}
-                            type="checkbox"
-                            style={hover}
-                            onChange={this.handleCheck}
-                            ref={this.checkbox}
-                        />
-                        <div className="keyValuePair-keyField" style={{ color: keyStyle }}>{"(".repeat(parenCount)}{props.fieldKey}{")".repeat(parenCount)}</div>
+                        <input className={'keyValuePair-td-key-check'} type="checkbox" style={hover} onChange={this.handleCheck} ref={this.checkbox} />
+                        <div className="keyValuePair-keyField" style={{ color: keyStyle }}>
+                            {'('.repeat(parenCount)}
+                            {props.fieldKey}
+                            {')'.repeat(parenCount)}
+                        </div>
                     </div>
                 </td>
                 <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }} onContextMenu={this.onContextMenu}>
@@ -123,13 +124,13 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
                         <EditableView
                             contents={contents}
                             maxHeight={36}
-                            height={"auto"}
+                            height={'auto'}
                             GetValue={() => Field.toKeyValueString(props.Document, props.fieldKey)}
-                            SetValue={(value: string) =>
-                                KeyValueBox.SetField(props.Document, props.fieldKey, value)} />
+                            SetValue={(value: string) => KeyValueBox.SetField(props.Document, props.fieldKey, value)}
+                        />
                     </div>
                 </td>
             </tr>
         );
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index d6cf79f87..be9565452 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -17,6 +17,7 @@ import { FieldView, FieldViewProps } from './FieldView';
 import './LinkAnchorBox.scss';
 import { LinkDocPreview } from './LinkDocPreview';
 import React = require('react');
+import { OpenWhere } from './DocumentView';
 const higflyout = require('@hig/flyout');
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -88,13 +89,13 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
     };
 
     openLinkDocOnRight = (e: React.MouseEvent) => {
-        this.props.addDocTab(this.rootDoc, 'add:right');
+        this.props.addDocTab(this.rootDoc, OpenWhere.addRight);
     };
     openLinkTargetOnRight = (e: React.MouseEvent) => {
         const alias = Doc.MakeAlias(Cast(this.layoutDoc[this.fieldKey], Doc, null));
         alias._isLinkButton = undefined;
         alias.layoutKey = 'layout';
-        this.props.addDocTab(alias, 'add:right');
+        this.props.addDocTab(alias, OpenWhere.addRight);
     };
     @action
     openLinkEditor = action((e: React.MouseEvent) => {
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index a47577701..135fbca31 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -14,7 +14,7 @@ import { LinkFollower } from '../../util/LinkFollower';
 import { LinkManager } from '../../util/LinkManager';
 import { Transform } from '../../util/Transform';
 import { undoBatch } from '../../util/UndoManager';
-import { DocumentView, DocumentViewSharedProps } from './DocumentView';
+import { DocumentView, DocumentViewSharedProps, OpenWhere } from './DocumentView';
 import './LinkDocPreview.scss';
 import React = require('react');
 import { LinkEditor } from '../linking/LinkEditor';
@@ -156,7 +156,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
             LinkDocPreview.Clear();
             LinkFollower.FollowLink(this._linkDoc, this._linkSrc, this.props.docProps, false);
         } else if (this.props.hrefs?.length) {
-            this.props.docProps?.addDocTab(Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, useCors: true }), 'add:right');
+            this.props.docProps?.addDocTab(Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, useCors: true }), OpenWhere.addRight);
         }
     };
 
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 70ac84fa4..82d5b00f9 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -31,6 +31,7 @@ import { FieldView, FieldViewProps } from './FieldView';
 import { RecordingBox } from './RecordingBox';
 import './VideoBox.scss';
 import { ObjectField } from '../../../fields/ObjectField';
+import { OpenWhere } from './DocumentView';
 const path = require('path');
 
 /**
@@ -273,7 +274,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             this.player && this._contentRef && this._contentRef.requestFullscreen();
         }
         try {
-            this._youtubePlayer && this.props.addDocTab(this.rootDoc, 'add');
+            this._youtubePlayer && this.props.addDocTab(this.rootDoc, OpenWhere.add);
         } catch (e) {
             console.log('Video FullScreen Exception:', e);
         }
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 42d20ba99..c71552a6f 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -26,6 +26,7 @@ import { ActiveFillColor, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, SetAc
 import { InkTranscription } from '../../InkTranscription';
 import { StyleProp } from '../../StyleProvider';
 import { FieldView, FieldViewProps } from '.././FieldView';
+import { OpenWhere } from '../DocumentView';
 import { RichTextMenu } from '../formattedText/RichTextMenu';
 import { WebBox } from '../WebBox';
 import { FontIconBadge } from './FontIconBadge';
@@ -61,7 +62,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
     }
     showTemplate = (): void => {
         const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null);
-        dragFactory && this.props.addDocTab(dragFactory, 'add:right');
+        dragFactory && this.props.addDocTab(dragFactory, OpenWhere.addRight);
     };
     dragAsTemplate = (): void => {
         this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 1e7cb6ea5..63347015b 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -17,6 +17,7 @@ import { Tooltip } from '@material-ui/core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
 import { NodeSelection } from 'prosemirror-state';
+import { OpenWhere } from '../DocumentView';
 
 export class DashFieldView {
     dom: HTMLDivElement; // container for label and value
@@ -227,7 +228,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
             list.map(c => c.heading).indexOf(this._fieldKey) === -1 && list.push(new SchemaHeaderField(this._fieldKey, '#f1efeb'));
             list.map(c => c.heading).indexOf('text') === -1 && list.push(new SchemaHeaderField('text', '#f1efeb'));
             alias._pivotField = this._fieldKey.startsWith('#') ? '#' : this._fieldKey;
-            this.props.tbox.props.addDocTab(alias, 'add:right');
+            this.props.tbox.props.addDocTab(alias, OpenWhere.addRight);
         }
     };
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index fdd61463d..ce4639b76 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -45,7 +45,7 @@ import { LightboxView } from '../../LightboxView';
 import { AnchorMenu } from '../../pdf/AnchorMenu';
 import { SidebarAnnos } from '../../SidebarAnnos';
 import { StyleProp } from '../../StyleProvider';
-import { DocumentViewInternal } from '../DocumentView';
+import { DocumentViewInternal, OpenWhere } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
 import { LinkDocPreview } from '../LinkDocPreview';
 import { DashDocCommentView } from './DashDocCommentView';
@@ -1428,7 +1428,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     const func = () => {
                         const docView = DocumentManager.Instance.getDocumentView(audiodoc);
                         if (!docView) {
-                            this.props.addDocTab(audiodoc, 'add:bottom');
+                            this.props.addDocTab(audiodoc, OpenWhere.addBottom);
                             setTimeout(func);
                         } else docView.ComponentView?.playFrom?.(timecode, Cast(anchor.timecodeToHide, 'number', null)); // bcz: would be nice to find the next audio tag in the doc and play until that
                     };
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index 3d9bd6add..68b0488a2 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -9,6 +9,7 @@ import { GetEffectiveAcl } from '../../../../fields/util';
 import { Utils } from '../../../../Utils';
 import { Docs } from '../../../documents/Documents';
 import { SelectionManager } from '../../../util/SelectionManager';
+import { OpenWhere } from '../DocumentView';
 import { liftListItem, sinkListItem } from './prosemirrorPatches.js';
 
 const mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false;
@@ -135,7 +136,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
     //Command to create a new Tab with a PDF of all the command shortcuts
     bind('Mod-/', (state: EditorState, dispatch: (tx: Transaction) => void) => {
         const newDoc = Docs.Create.PdfDocument(Utils.prepend('/assets/cheat-sheet.pdf'), { _width: 300, _height: 300 });
-        props.addDocTab(newDoc, 'add:right');
+        props.addDocTab(newDoc, OpenWhere.addRight);
     });
 
     //Commands to modify BlockType
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index e19b53f50..adfd2fda1 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -23,7 +23,7 @@ import { SelectionManager } from '../../../util/SelectionManager';
 import { SettingsManager } from '../../../util/SettingsManager';
 import { undoBatch, UndoManager } from '../../../util/UndoManager';
 import { CollectionDockingView } from '../../collections/CollectionDockingView';
-import { CollectionFreeFormView, MarqueeViewBounds } from '../../collections/collectionFreeForm';
+import { CollectionFreeFormView, computeTimelineLayout, MarqueeViewBounds } from '../../collections/collectionFreeForm';
 import { CollectionView } from '../../collections/CollectionView';
 import { TabDocView } from '../../collections/TabDocView';
 import { ViewBoxBaseComponent } from '../../DocComponent';
@@ -35,6 +35,7 @@ import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresMovement, PresStatus } from './PresEnums';
 import { map } from 'bluebird';
+import { OpenWhere, OpenWhereMod } from '../DocumentView';
 const { Howl } = require('howler');
 
 export interface PinProps {
@@ -196,7 +197,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         );
         this.props.setContentView?.(this);
         this._unmounting = false;
-        this.rootDoc._forceRenderEngine = 'timeline';
+        this.rootDoc._forceRenderEngine = computeTimelineLayout.name;
         this.layoutDoc.presStatus = PresStatus.Edit;
         this.layoutDoc._gridGap = 0;
         this.layoutDoc._yMargin = 0;
@@ -563,7 +564,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             self._eleArray.splice(0, self._eleArray.length, ...eleViewCache);
         });
         const openInTab = (doc: Doc, finished?: () => void) => {
-            (collectionDocView ?? this).props.addDocTab(doc, '');
+            (collectionDocView ?? this).props.addDocTab(doc, OpenWhere.add);
             this.layoutDoc.presCollection = targetDoc;
             // this still needs some fixing
             setTimeout(resetSelection, 500);
@@ -725,7 +726,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
             this.layoutDoc.presStatus = PresStatus.Edit;
             Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, this.rootDoc);
-            CollectionDockingView.AddSplit(this.rootDoc, 'right');
+            CollectionDockingView.AddSplit(this.rootDoc, OpenWhereMod.right);
         } else {
             this.layoutDoc.presStatus = PresStatus.Edit;
             clearTimeout(this._presTimer);
@@ -1760,7 +1761,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 TabDocView.PinDoc(doc, {});
                 this.gotoDocument(this.childDocs.length, this.activeItem);
             } else {
-                this.props.addDocTab(doc, 'add:right');
+                this.props.addDocTab(doc, OpenWhere.addRight);
             }
         }
     };
@@ -2322,7 +2323,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     static NavigateToDoc(bestTarget: Doc, activeItem: Doc) {
         const srcContext = Cast(bestTarget.context, Doc, null) ?? Cast(Cast(bestTarget.annotationOn, Doc, null)?.context, Doc, null);
         const openInTab = (doc: Doc, finished?: () => void) => {
-            CollectionDockingView.AddSplit(doc, 'right');
+            CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
             finished?.();
         };
         PresBox.NavigateToTarget(bestTarget, activeItem, openInTab, srcContext);
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index 8265de445..2ae597b0b 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -700,7 +700,7 @@ export class MobileInterface extends React.Component {
                     className="docButton"
                     title={Doc.isDocPinned(this._activeDoc) ? 'Unpin from presentation' : 'Pin to presentation'}
                     style={{ backgroundColor: isPinned ? 'black' : 'white', color: isPinned ? 'white' : 'black' }}
-                    onClick={e => TabDocView.PinDoc(this._activeDoc)}>
+                    onClick={e => TabDocView.PinDoc(this._activeDoc, {})}>
                     <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" />
                 </div>
             );
-- 
cgit v1.2.3-70-g09d2


From af2a2c83868c87812e9ae54c8e3cced81374619a Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 27 Feb 2023 15:08:45 -0500
Subject: restructured getAnchor()/scrollFocus to be more consistent.  added
 setterscript for computedFields. restructed getFieldsImpl to avoid making
 multiple requests for the same document due to timing issues by 'locking' a
 document cache with a promise before sending the server request.  added
 rotation and fill color as animatable fields.  fixed image cropping for

---
 src/client/DocServer.ts                            | 143 +++++++-------
 src/client/documents/Documents.ts                  |  23 ++-
 src/client/util/DocumentManager.ts                 |   4 +-
 src/client/util/LinkManager.ts                     |   2 +-
 src/client/util/SharingManager.tsx                 |  30 ++-
 src/client/views/DocumentButtonBar.tsx             |  10 +-
 src/client/views/InkTangentHandles.tsx             |   9 +-
 src/client/views/InkingStroke.tsx                  |  32 +++-
 src/client/views/MainView.tsx                      |   1 -
 src/client/views/MarqueeAnnotator.tsx              |  35 ++--
 src/client/views/StyleProvider.tsx                 |   3 +
 .../collections/CollectionStackedTimeline.tsx      |  10 +-
 .../views/collections/CollectionTimeView.tsx       |  37 +---
 src/client/views/collections/TabDocView.tsx        |   8 +-
 .../CollectionFreeFormLayoutEngines.tsx            |   2 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  49 ++---
 src/client/views/nodes/AudioBox.tsx                |  12 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  23 ++-
 src/client/views/nodes/DocumentLinksButton.tsx     |  20 +-
 src/client/views/nodes/DocumentView.tsx            |  15 +-
 src/client/views/nodes/FunctionPlotBox.tsx         |   6 +-
 src/client/views/nodes/ImageBox.tsx                |  34 +---
 src/client/views/nodes/LabelBox.tsx                |   2 -
 src/client/views/nodes/PDFBox.tsx                  |  27 +--
 src/client/views/nodes/ScreenshotBox.tsx           |   2 +-
 src/client/views/nodes/ScriptingBox.tsx            |   5 +-
 src/client/views/nodes/VideoBox.tsx                |  23 +--
 src/client/views/nodes/WebBox.tsx                  |  40 ++--
 src/client/views/nodes/button/FontIconBox.tsx      |  28 ++-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   4 +-
 src/client/views/nodes/trails/PresBox.tsx          | 211 ++++++++++++++-------
 src/client/views/pdf/AnchorMenu.tsx                |   7 +-
 src/client/views/pdf/PDFViewer.tsx                 |   9 +-
 src/fields/Doc.ts                                  |  28 ++-
 src/fields/ScriptField.ts                          |   6 +-
 src/server/DashUploadUtils.ts                      |   2 +-
 src/server/websocket.ts                            |   2 +-
 37 files changed, 517 insertions(+), 387 deletions(-)

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

diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index cab90138f..8f79ebb03 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -234,7 +234,7 @@ export namespace DocServer {
      * the server if the document has not been cached.
      * @param id the id of the requested document
      */
-    const _GetRefFieldImpl = async (id: string, force: boolean = false): Promise<Opt<RefField>> => {
+    const _GetRefFieldImpl = (id: string, force: boolean = false): Promise<Opt<RefField>> => {
         // an initial pass through the cache to determine whether the document needs to be fetched,
         // is already in the process of being fetched or already exists in the
         // cache
@@ -309,8 +309,7 @@ export namespace DocServer {
     }
 
     export async function getYoutubeChannels() {
-        const apiKey = await Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.Channels });
-        return apiKey;
+        return await Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.Channels });
     }
 
     export function getYoutubeVideos(videoTitle: string, callBack: (videos: any[]) => void) {
@@ -329,10 +328,11 @@ export namespace DocServer {
      */
     const _GetRefFieldsImpl = async (ids: string[]): Promise<{ [id: string]: Opt<RefField> }> => {
         const requestedIds: string[] = [];
-        const waitingIds: string[] = [];
-        const promises: Promise<Opt<RefField>>[] = [];
-        const map: { [id: string]: Opt<RefField> } = {};
+        const promises: Promise<any>[] = [];
 
+        let defaultRes: any = undefined;
+        const defaultPromise = new Promise<any>(res => (defaultRes = res));
+        let defaultPromises: { p: Promise<any>; id: string }[] = [];
         // 1) an initial pass through the cache to determine
         // i) which documents need to be fetched
         // ii) which are already in the process of being fetched
@@ -340,6 +340,13 @@ export namespace DocServer {
         for (const id of ids) {
             const cached = _cache[id];
             if (cached === undefined) {
+                defaultPromises.push({
+                    id,
+                    p: (_cache[id] = new Promise<any>(async res => {
+                        await defaultPromise;
+                        res(_cache[id]);
+                    })),
+                });
                 // NOT CACHED => we'll have to send a request to the server
                 requestedIds.push(id);
             } else if (cached instanceof Promise) {
@@ -347,10 +354,10 @@ export namespace DocServer {
                 // and requested one of the documents I'm looking for. Shouldn't fetch again, just
                 // wait until this promise is resolved (see 7)
                 promises.push(cached);
-                waitingIds.push(id);
+                // waitingIds.push(id);
             } else {
                 // CACHED => great, let's just add it to the field map
-                map[id] = cached;
+                // map[id] = cached;
             }
         }
 
@@ -358,74 +365,65 @@ export namespace DocServer {
             // 2) synchronously, we emit a single callback to the server requesting the serialized (i.e. represented by a string)
             // fields for the given ids. This returns a promise, which, when resolved, indicates that all the JSON serialized versions of
             // the fields have been returned from the server
-            console.log('Requesting ' + requestedIds.length + ' fields');
+            console.log('Requesting ' + requestedIds.length);
             FieldLoader.active && runInAction(() => (FieldLoader.ServerLoadStatus.requested = requestedIds.length));
-            const getSerializedFields: Promise<any> = Utils.EmitCallback(_socket, MessageStore.GetRefFields, requestedIds);
+            const serializedFields = await Utils.EmitCallback(_socket, MessageStore.GetRefFields, requestedIds);
 
             // 3) when the serialized RefFields have been received, go head and begin deserializing them into objects.
             // Here, once deserialized, we also invoke .proto to 'load' the documents' prototypes, which ensures that all
             // future .proto calls on the Doc won't have to go farther than the cache to get their actual value.
-
-            let retrieved = 0;
-            const fields: { [id: string]: RefField } = {};
-            await getSerializedFields.then(async fieldvals => {
-                console.log('deserializing ' + fieldvals.length + ' fields');
-                const proms: Promise<void>[] = [];
-                await runInAction(async () => {
-                    for (const field of fieldvals) {
-                        const cached = _cache[field.id];
-                        if (!cached) {
-                            retrieved++;
-                            if (FieldLoader.active && retrieved % 150 === 0) {
-                                runInAction(() => (FieldLoader.ServerLoadStatus.retrieved = retrieved));
-                                await new Promise(res => setTimeout(res));
+            let processed = 0;
+            console.log('deserializing ' + serializedFields.length + ' fields');
+            for (const field of serializedFields) {
+                processed++;
+                if (FieldLoader.active && processed % 150 === 0) {
+                    runInAction(() => (FieldLoader.ServerLoadStatus.retrieved = processed));
+                    await new Promise(res => setTimeout(res)); // force loading to yield to splash screen rendering to update progress
+                }
+                const cached = _cache[field.id];
+                if (!cached || (cached instanceof Promise && defaultPromises.some(dp => dp.p === cached))) {
+                    // deserialize
+                    // adds to a list of promises that will be awaited asynchronously
+                    promises.push(
+                        (_cache[field.id] = SerializationHelper.Deserialize(field).then(deserialized => {
+                            //overwrite or delete any promises (that we inserted as flags
+                            // to indicate that the field was in the process of being fetched). Now everything
+                            // should be an actual value within or entirely absent from the cache.
+                            if (deserialized !== undefined) {
+                                _cache[field.id] = deserialized;
+                            } else {
+                                delete _cache[field.id];
                             }
-                            // deserialize
-                            const prom = SerializationHelper.Deserialize(field).then(async deserialized => {
-                                fields[field.id] = deserialized;
-
-                                //overwrite or delete any promises (that we inserted as flags
-                                // to indicate that the field was in the process of being fetched). Now everything
-                                // should be an actual value within or entirely absent from the cache.
-                                if (deserialized !== undefined) {
-                                    _cache[field.id] = deserialized;
-                                } else {
-                                    delete _cache[field.id];
-                                }
-                                return deserialized;
-                            });
-                            // 4) here, for each of the documents we've requested *ourselves* (i.e. weren't promises or found in the cache)
-                            // we set the value at the field's id to a promise that will resolve to the field.
-                            // When we find that promises exist at keys in the cache, THIS is where they were set, just by some other caller (method).
-                            // The mapping in the .then call ensures that when other callers await these promises, they'll
-                            // get the resolved field
-                            _cache[field.id] = prom;
-
-                            // adds to a list of promises that will be awaited asynchronously
-                            proms.push(prom);
-                        } else if (cached instanceof Promise) {
-                            console.log('.');
-                            proms.push(cached as any);
-                            cached.then((f: any) => (fields[field.id] = f));
-                        } else if (field) {
-                            console.log('-');
-                            proms.push(cached as any);
-                            fields[field.id] = DocServer.GetCachedRefField(field.id) || field;
-                        }
-                    }
-                });
-                return Promise.all(proms);
-            });
+                            const promInd = defaultPromises.findIndex(dp => dp.id === field.id);
+                            promInd !== -1 && defaultPromises.splice(promInd, 1);
+                            return deserialized;
+                        }))
+                    );
+                    // 4) here, for each of the documents we've requested *ourselves* (i.e. weren't promises or found in the cache)
+                    // we set the value at the field's id to a promise that will resolve to the field.
+                    // When we find that promises exist at keys in the cache, THIS is where they were set, just by some other caller (method).
+                    // The mapping in the .then call ensures that when other callers await these promises, they'll
+                    // get the resolved field
+                } else if (cached instanceof Promise) {
+                    console.log('.');
+                    //promises.push(cached);
+                } else if (field) {
+                    // console.log('-');
+                }
+            }
+        }
 
-            // 5) at this point, all fields have a) been returned from the server and b) been deserialized into actual Field objects whose
-            // prototype documents, if any, have also been fetched and cached.
-            console.log('Deserialized ' + Object.keys(fields).length + ' fields');
+        await Promise.all(promises);
+        defaultPromises.forEach(df => delete _cache[df.id]);
+        defaultRes();
 
-            // 6) with this confidence, we can now go through and update the cache at the ids of the fields that
-            // we explicitly had to fetch. To finish it off, we add whatever value we've come up with for a given
-            // id to the soon-to-be-returned field mapping.
-            requestedIds.forEach(id => (map[id] = fields[id]));
-        }
+        // 5) at this point, all fields have a) been returned from the server and b) been deserialized into actual Field objects whose
+        // prototype documents, if any, have also been fetched and cached.
+        console.log('Deserialized ' + (requestedIds.length - defaultPromises.length) + ' fields');
+        // 6) with this confidence, we can now go through and update the cache at the ids of the fields that
+        // we explicitly had to fetch. To finish it off, we add whatever value we've come up with for a given
+        // id to the soon-to-be-returned field mapping.
+        //ids.forEach(id => (map[id] = _cache[id] as any));
 
         // 7) those promises we encountered in the else if of 1), which represent
         // other callers having already submitted a request to the server for (a) document(s)
@@ -434,16 +432,19 @@ export namespace DocServer {
         //
         // fortunately, those other callers will also hit their own version of 6) and clean up
         // the shared cache when these promises resolve, so all we have to do is...
-        const otherCallersFetching = await Promise.all(promises);
+        //const otherCallersFetching = await Promise.all(promises);
         // ...extract the RefFields returned from the resolution of those promises and add them to our
         // own map.
-        waitingIds.forEach((id, index) => (map[id] = otherCallersFetching[index]));
+        // waitingIds.forEach((id, index) => (map[id] = otherCallersFetching[index]));
 
         // now, we return our completed mapping from all of the ids that were passed into the method
         // to their actual RefField | undefined values. This return value either becomes the input
         // argument to the caller's promise (i.e. GetRefFields(["_id1_", "_id2_", "_id3_"]).then(map => //do something with map...))
         // or it is the direct return result if the promise is awaited (i.e. let fields = await GetRefFields(["_id1_", "_id2_", "_id3_"])).
-        return map;
+        return ids.reduce((map, id) => {
+            map[id] = _cache[id] as any;
+            return map;
+        }, {} as { [id: string]: Opt<RefField> });
     };
 
     let _GetRefFields: (ids: string[]) => Promise<{ [id: string]: Opt<RefField> }> = errorFunc;
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index debb11066..d2ab67849 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -263,6 +263,7 @@ export class DocumentOptions {
     viewTransitionTime?: number; // transition duration for view parameters
     presTransition?: number; //the time taken for the transition TO a document
     presDuration?: number; //the duration of the slide in presentation view
+    presZoomText?: boolean; // whether text anchors should shown in a larger box when following links to make them stand out
     borderRounding?: string;
     boxShadow?: string; // box-shadow css string OR "standard" to use dash standard box shadow
     data?: any;
@@ -1043,6 +1044,9 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.MARKER), url, options, id);
         }
 
+        export function CollectionAnchorDocument(options: DocumentOptions = {}, id?: string) {
+            return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id);
+        }
         export function TextanchorDocument(options: DocumentOptions = {}, id?: string) {
             return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id);
         }
@@ -1051,6 +1055,10 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id);
         }
 
+        export function InkAnchorDocument(options: DocumentOptions, id?: string) {
+            return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id);
+        }
+
         export function HTMLAnchorDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
             return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), options, id);
         }
@@ -1407,12 +1415,15 @@ export namespace DocUtils {
                 }
             });
         funcs &&
-            Object.keys(funcs).map(key => {
-                const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
-                if (ScriptCast(cfield)?.script.originalScript !== funcs[key]) {
-                    doc[key] = funcs[key] ? ComputedField.MakeFunction(funcs[key], { dragData: DragManager.DocumentDragData.name }, { _readOnly_: true }) : undefined;
-                }
-            });
+            Object.keys(funcs)
+                .filter(key => key.endsWith('-setter'))
+                .map(key => {
+                    const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
+                    if (ScriptCast(cfield)?.script.originalScript !== funcs[key]) {
+                        const setFunc = Cast(funcs[key + '-setter'], 'string', null);
+                        doc[key] = funcs[key] ? ComputedField.MakeFunction(funcs[key], { dragData: DragManager.DocumentDragData.name }, { _readOnly_: true }, setFunc) : undefined;
+                    }
+                });
         return doc;
     }
     export function AssignOpts(doc: Doc | undefined, reqdOpts: DocumentOptions, items?: Doc[]) {
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index e4d48d4de..d9273c2c9 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -199,7 +199,7 @@ export class DocumentManager {
     static GetContextPath(doc: Opt<Doc>, includeExistingViews?: boolean) {
         if (!doc) return [];
         const srcContext = Cast(doc.context, Doc, null) ?? Cast(Cast(doc.annotationOn, Doc, null)?.context, Doc, null);
-        var containerDocContext = srcContext ? [srcContext] : [];
+        var containerDocContext = srcContext ? [srcContext, doc] : [doc];
         while (
             containerDocContext.length &&
             containerDocContext[0]?.context &&
@@ -286,7 +286,7 @@ export class DocumentManager {
                 return annoContainerView.iconify(() => DocumentManager.Instance.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(targetDoc, { ...options, originalTarget, toggleTarget: false }, createViewFunc, docContextPath, finished)), 30);
             }
             if (!docView && targetDoc.type !== DocumentType.MARKER) {
-                annoContainerView.focus(targetDoc, {}); // this allows something like a PDF view to remove its doc filters to expose the target so that it can be found in the retry code below
+                annoContainerView.focus(targetDoc, options); // this allows something like a PDF view to remove its doc filters to expose the target so that it can be found in the retry code below
             }
         }
 
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 7da16ca78..46d44fea4 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -48,7 +48,7 @@ export class LinkManager {
                         const a2 = DocCast(linkdata[1]);
                         a1 &&
                             a2 &&
-                            Promise.all([a1.proto, a2.proto]).then(
+                            Promise.all([Doc.GetProto(a1), Doc.GetProto(a2)]).then(
                                 action(protos => {
                                     (protos[0] as Doc)?.[DirectLinksSym].add(link);
                                     (protos[1] as Doc)?.[DirectLinksSym].add(link);
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 00ae85d12..36095700c 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -130,29 +130,21 @@ export class SharingManager extends React.Component<{}> {
         if (!this.populating && Doc.UserDoc()[Id] !== '__guest__') {
             this.populating = true;
             const userList = await RequestPromise.get(Utils.prepend('/getUsers'));
-            const raw = JSON.parse(userList) as User[];
-            const sharingDocs: ValidatedUser[] = [];
-            const evaluating = raw.map(async user => {
-                const isCandidate = user.email !== Doc.CurrentUserEmail;
-                if (isCandidate) {
-                    const sharingDoc = await DocServer.GetRefField(user.sharingDocumentId);
-                    const linkDatabase = await DocServer.GetRefField(user.linkDatabaseId);
+            const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== Doc.CurrentUserEmail);
+            const docs = await DocServer.GetRefFields(raw.reduce((list, user) => [...list, user.sharingDocumentId, user.linkDatabaseId], [] as string[]));
+            raw.map(
+                action((newUser: User) => {
+                    const sharingDoc = docs[newUser.sharingDocumentId];
+                    const linkDatabase = docs[newUser.linkDatabaseId];
                     if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) {
-                        sharingDocs.push({ user, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.userColor) });
-                    }
-                }
-            });
-            return Promise.all(evaluating).then(() => {
-                runInAction(async () => {
-                    for (const sharer of sharingDocs) {
-                        if (!this.users.find(user => user.user.email === sharer.user.email)) {
-                            this.users.push(sharer);
+                        if (!this.users.find(user => user.user.email === newUser.email)) {
+                            this.users.push({ user: newUser, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.userColor) });
                             // LinkManager.addLinkDB(sharer.linkDatabase);
                         }
                     }
-                });
-                this.populating = false;
-            });
+                })
+            );
+            this.populating = false;
         }
     };
 
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index f61d147cf..9298c881c 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -315,7 +315,13 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                                     .views()
                                     .filter(v => v)
                                     .map(dv => dv!.rootDoc);
-                                TabDocView.PinDoc(docs, { pinAudioPlay: true, pinDocLayout, pinDocContent, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null), currentFrame: Cast(docs.lastElement()?.currentFrame, 'number', null) });
+                                TabDocView.PinDoc(docs, {
+                                    pinAudioPlay: true,
+                                    pinDocLayout,
+                                    pinData: { dataview: true },
+                                    activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null),
+                                    currentFrame: Cast(docs.lastElement()?.currentFrame, 'number', null),
+                                });
                                 e.stopPropagation();
                             }}
                         />
@@ -496,7 +502,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
         const rootView = this.props.views()[0];
         const rootDoc = rootView?.rootDoc;
         if (rootDoc) {
-            const anchor = rootView.ComponentView?.getAnchor?.(true) ?? rootDoc;
+            const anchor = rootView.ComponentView?.getAnchor?.(false) ?? rootDoc;
             const trail = DocCast(anchor.presTrail) ?? Doc.MakeCopy(DocCast(Doc.UserDoc().emptyTrail), true);
             if (trail !== anchor.presTrail) {
                 DocUtils.MakeLink({ doc: anchor }, { doc: trail }, 'link trail');
diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx
index c4a2f603e..71e0ff63c 100644
--- a/src/client/views/InkTangentHandles.tsx
+++ b/src/client/views/InkTangentHandles.tsx
@@ -38,18 +38,19 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
         setupMoveUpEvents(
             this,
             e,
-            (e: PointerEvent, down: number[], delta: number[]) => {
+            action((e: PointerEvent, down: number[], delta: number[]) => {
                 if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('DocDecs move tangent');
                 if (e.altKey) this.onBreakTangent(controlIndex);
                 const inkMoveEnd = this.props.inkView.ptFromScreen({ X: delta[0], Y: delta[1] });
                 const inkMoveStart = this.props.inkView.ptFromScreen({ X: 0, Y: 0 });
                 InkStrokeProperties.Instance.moveTangentHandle(this.docView, -(inkMoveEnd.X - inkMoveStart.X), -(inkMoveEnd.Y - inkMoveStart.Y), handleIndex, oppositeHandleIndex, controlIndex);
                 return false;
-            },
-            () => {
+            }),
+            action(() => {
                 this.props.inkView.controlUndo?.end();
+                this.props.inkView.controlUndo = undefined;
                 UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']);
-            },
+            }),
             emptyFunction
         );
     };
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 76c1aa80a..17cf6a678 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -25,7 +25,7 @@ import { action, IReactionDisposer, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc, HeightSym, WidthSym } from '../../fields/Doc';
 import { InkData, InkField } from '../../fields/InkField';
-import { BoolCast, Cast, NumCast, RTFCast, StrCast } from '../../fields/Types';
+import { BoolCast, Cast, DocCast, NumCast, RTFCast, StrCast } from '../../fields/Types';
 import { TraceMobx } from '../../fields/util';
 import { DashColor, OmitKeys, returnFalse, setupMoveUpEvents } from '../../Utils';
 import { CognitiveServices } from '../cognitive_services/CognitiveServices';
@@ -41,11 +41,13 @@ import { InkControlPtHandles, InkEndPtHandles } from './InkControlPtHandles';
 import './InkStroke.scss';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { InkTangentHandles } from './InkTangentHandles';
-import { DocComponentView, DocFocusOptions } from './nodes/DocumentView';
+import { DocComponentView, DocFocusOptions, DocumentView } from './nodes/DocumentView';
 import { FieldView, FieldViewProps } from './nodes/FieldView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { StyleProp } from './StyleProvider';
 import Color = require('color');
+import { Docs } from '../documents/Documents';
+import { PinProps, PresBox } from './nodes/trails';
 
 @observer
 export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -78,12 +80,28 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
     // fit within its panel (e.g., for content fitting views like Lightbox or multicolumn, etc)
     screenToLocal = () => this.props.ScreenToLocalTransform().scale(this.props.NativeDimScaling?.() || 1);
 
-    getAnchor = (addAsAnnotation: boolean) => {
-        return this._subContentView?.getAnchor?.(addAsAnnotation) || this.rootDoc;
+    getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+        if (this._subContentView) {
+            return this._subContentView.getAnchor?.(addAsAnnotation) || this.rootDoc;
+        }
+
+        if (!addAsAnnotation && !pinProps) return this.rootDoc;
+
+        const anchor = Docs.Create.InkAnchorDocument({ title: 'Ink anchor:' + this.rootDoc.title, presDuration: 1100, presTransition: 1000, unrendered: true, annotationOn: this.rootDoc });
+        if (anchor) {
+            anchor.backgroundColor = 'transparent';
+            // /* addAsAnnotation &&*/ this.addDocument(anchor);
+            PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: true, inkable: true } }, this.rootDoc);
+            return anchor;
+        }
+        return this.rootDoc;
     };
 
-    scrollFocus = (textAnchor: Doc, options: DocFocusOptions) => {
-        return this._subContentView?.scrollFocus?.(textAnchor, options);
+    scrollFocus = (docView: DocumentView, anchor: Doc, options: DocFocusOptions) => {
+        if (this._subContentView) return this._subContentView?.scrollFocus?.(docView, anchor, options);
+
+        const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
+        return PresBox.restoreTargetDocView(docView, anchor, focusSpeed) ? focusSpeed : undefined;
     };
     /**
      * @returns the center of the ink stroke in the ink document's coordinate space (not screen space, and not the ink data coordinate space);
@@ -349,7 +367,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
         const markerScale = NumCast(this.layoutDoc.strokeMarkerScale, 1);
         const closed = InkingStroke.IsClosed(inkData);
         const isInkMask = BoolCast(this.layoutDoc.isInkMask);
-        const fillColor = isInkMask ? DashColor(StrCast(this.layoutDoc.fillColor, 'transparent')).blacken(0).rgb().toString() : StrCast(this.layoutDoc.fillColor, 'transparent');
+        const fillColor = isInkMask ? DashColor(StrCast(this.layoutDoc.fillColor, 'transparent')).blacken(0).rgb().toString() : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FillColor) ?? 'transparent';
         const strokeColor = !closed && fillColor && fillColor !== 'transparent' ? fillColor : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color) ?? StrCast(this.layoutDoc.color);
 
         // bcz: Hack!! Not really sure why, but having fractional values for width/height of mask ink strokes causes the dragging clone (see DragManager) to be offset from where it should be.
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 625dc2748..64f116a09 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -145,7 +145,6 @@ export class MainView extends React.Component {
         if (ele && prog) {
             // remove from DOM
             setTimeout(() => {
-                clearTimeout();
                 prog.style.transition = '1s';
                 prog.style.width = '100%';
             }, 0);
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 3bdf65d01..30867a774 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -8,7 +8,7 @@ import { GetEffectiveAcl } from '../../fields/util';
 import { unimplementedFunction, Utils } from '../../Utils';
 import { Docs, DocUtils } from '../documents/Documents';
 import { DragManager } from '../util/DragManager';
-import { undoBatch } from '../util/UndoManager';
+import { undoBatch, UndoManager } from '../util/UndoManager';
 import './MarqueeAnnotator.scss';
 import { DocumentView } from './nodes/DocumentView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
@@ -48,7 +48,11 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
     constructor(props: any) {
         super(props);
 
-        AnchorMenu.Instance.OnCrop = (e: PointerEvent) => this.props.anchorMenuCrop?.(this.highlight('', true), true);
+        AnchorMenu.Instance.OnCrop = (e: PointerEvent) => {
+            if (this.props.anchorMenuCrop) {
+                UndoManager.RunInBatch(() => this.props.anchorMenuCrop?.(this.highlight('', true, undefined, false), true), 'cropping');
+            }
+        };
         AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true));
         AnchorMenu.Instance.OnAudio = unimplementedFunction;
         AnchorMenu.Instance.Highlight = this.highlight;
@@ -87,11 +91,8 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
         AnchorMenu.Instance.StartDrag = action((e: PointerEvent, ele: HTMLElement) => {
             e.preventDefault();
             e.stopPropagation();
-            const sourceAnchorCreator = () => {
-                const annoDoc = this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true); // hyperlink color
-                annoDoc && this.props.addDocument(annoDoc);
-                return annoDoc;
-            };
+            const sourceAnchorCreator = () => this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true); // hyperlink color
+
             const targetCreator = (annotationOn: Doc | undefined) => {
                 const target = DocUtils.GetNewTextDoc('Note linked to ' + this.props.rootDoc.title, 0, 0, 100, 100, undefined, annotationOn, undefined, 'yellow');
                 FormattedTextBox.SelectOnLoad = target[Id];
@@ -116,11 +117,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
                   e.preventDefault();
                   e.stopPropagation();
                   var cropRegion: Doc | undefined;
-                  const sourceAnchorCreator = () => {
-                      cropRegion = this.highlight('', true); // hyperlink color
-                      cropRegion && this.props.addDocument(cropRegion);
-                      return cropRegion;
-                  };
+                  const sourceAnchorCreator = () => (cropRegion = this.highlight('', true, undefined, true)); // hyperlink color
                   const targetCreator = (annotationOn: Doc | undefined) => this.props.anchorMenuCrop!(cropRegion, false)!;
                   DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
                       dragComplete: e => {
@@ -159,7 +156,15 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
             return marqueeAnno;
         }
 
-        const textRegionAnno = Docs.Create.HTMLAnchorDocument([], { annotationOn: this.props.rootDoc, text: this.props.selectionText(), backgroundColor: 'transparent', title: 'Selection on ' + this.props.rootDoc.title });
+        const textRegionAnno = Docs.Create.HTMLAnchorDocument([], {
+            annotationOn: this.props.rootDoc,
+            text: this.props.selectionText(),
+            backgroundColor: 'transparent',
+            presDuration: 2100,
+            presTransition: 500,
+            presZoomText: true,
+            title: 'Selection on ' + this.props.rootDoc.title,
+        });
         let minX = Number.MAX_VALUE;
         let maxX = -Number.MAX_VALUE;
         let minY = Number.MAX_VALUE;
@@ -262,10 +267,6 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
 
             AnchorMenu.Instance.jumpTo(cliX, cliY);
 
-            if (AnchorMenu.Instance.Highlighting) {
-                // when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up
-                this.highlight('rgba(245, 230, 95, 0.75)', false); // yellowish highlight color for highlighted text (should match AnchorMenu's highlight color)
-            }
             this.props.finishMarquee(undefined, undefined, e);
             runInAction(() => (this._width = this._height = 0));
         } else {
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 5f87f0697..153c30052 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -31,6 +31,7 @@ export enum StyleProp {
     BorderRounding = 'borderRounding', // border radius of the document view
     Color = 'color', // foreground color of Document view items
     BackgroundColor = 'backgroundColor', // background color of a document view
+    FillColor = 'fillColor', // fill color of an ink stroke or shape
     WidgetColor = 'widgetColor', // color to display UI widgets on a document view -- used for the sidebar divider dragger on a text note
     HideLinkButton = 'hideLinkButton', // hides the blue-dot link button.  used when a document acts like a button
     LinkSource = 'linkSource', // source document of a link -- used by LinkAnchorBox
@@ -158,6 +159,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
                     )) ||
                 ''
             );
+        case StyleProp.FillColor:
+            return Cast(doc?._fillColor, 'string', Cast(doc?.fillColor, 'string', 'transparent'));
         case StyleProp.Color:
             if (MainView.Instance.LastButton === doc) return Colors.DARK_GRAY;
             const docColor: Opt<string> = StrCast(doc?.[fieldKey + 'color'], StrCast(doc?._color));
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 28f08b6ce..eecab9d86 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -190,7 +190,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                         CollectionStackedTimeline.SelectingRegion = this;
                     } else {
                         this._markerEnd = this.currentTime;
-                        CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this._markerStart, this._markerEnd, undefined, true);
+                        CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this._markerStart, this._markerEnd, undefined, true);
                         this._markerEnd = undefined;
                         CollectionStackedTimeline.SelectingRegion = undefined;
                     }
@@ -257,7 +257,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                         this._markerEnd = tmp;
                     }
                     if (!isClick && Math.abs(movement[0]) > 15 && !this.IsTrimming) {
-                        const anchor = CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this._markerStart, this._markerEnd, undefined, true);
+                        const anchor = CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this._markerStart, this._markerEnd, undefined, true);
                         setTimeout(() => DocumentManager.Instance.getDocumentView(anchor)?.select(false));
                     }
                     (!isClick || !wasSelecting) && (this._markerEnd = undefined);
@@ -273,7 +273,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                 undefined,
                 () => {
                     if (shiftKey) {
-                        CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this.currentTime, undefined, undefined, true);
+                        CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.currentTime, undefined, undefined, true);
                     } else {
                         !wasPlaying && this.props.setTime(this.toTimeline(clientX - rect.x, rect.width));
                     }
@@ -388,8 +388,10 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
     // creates marker on timeline
     @undoBatch
     @action
-    static createAnchor(rootDoc: Doc, dataDoc: Doc, fieldKey: string, startTag: string, endTag: string, anchorStartTime: Opt<number>, anchorEndTime: Opt<number>, docAnchor: Opt<Doc>, addAsAnnotation: boolean) {
+    static createAnchor(rootDoc: Doc, dataDoc: Doc, fieldKey: string, anchorStartTime: Opt<number>, anchorEndTime: Opt<number>, docAnchor: Opt<Doc>, addAsAnnotation: boolean) {
         if (anchorStartTime === undefined) return rootDoc;
+        const startTag = '_timecodeToShow';
+        const endTag = '_timecodeToHide';
         const anchor =
             docAnchor ??
             Docs.Create.LabelDocument({
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index e6f29ec37..437b22040 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -21,7 +21,7 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV
 import { CollectionSubView } from './CollectionSubView';
 import './CollectionTimeView.scss';
 import React = require('react');
-import { DocFocusOptions } from '../nodes/DocumentView';
+import { DocFocusOptions, DocumentView } from '../nodes/DocumentView';
 import { PresBox } from '../nodes/trails';
 
 @observer
@@ -52,8 +52,7 @@ export class CollectionTimeView extends CollectionSubView() {
             title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as any,
             annotationOn: this.rootDoc,
         });
-        anchor.presPinPivotField = this.pivotField; // should be captured in pinDocView below
-        PresBox.pinDocView(anchor, { pinData: { viewType: true, filters: true } }, this.rootDoc);
+        PresBox.pinDocView(anchor, { pinData: { viewType: true, pivot: true, filters: true } }, this.rootDoc);
 
         if (addAsAnnotation) {
             // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
@@ -67,32 +66,12 @@ export class CollectionTimeView extends CollectionSubView() {
     };
 
     @action
-    scrollFocus = (anchor: Doc, options: DocFocusOptions) => {
-        if (options.preview) {
-            // if in preview, then override document's fields with view spec
-            this._focusFilters = StrListCast(anchor.presPinDocFilters);
-            this._focusRangeFilters = StrListCast(anchor.presPinDocRangeFilters);
-            this._focusPivotField = StrCast(anchor.presPinPivotField);
-            return undefined;
-        }
-        const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
-
-        // should be part of restoreTargetDocView
-        this.layoutDoc._prevFilterIndex = 1;
-        this.layoutDoc._pivotField = anchor.presPinPivotField;
-
-        return PresBox.restoreTargetDocView(
-            this.props.DocumentView?.(), //
-            { pinDocLayout: BoolCast(anchor.presPinLayout) },
-            anchor,
-            focusSpeed,
-            {
-                viewType: anchor.presPinViewType ? true : false,
-                filters: anchor.presPinDocFilters || anchor.presPinDocRangeFilters ? true : false,
-            }
-        )
-            ? focusSpeed
-            : undefined;
+    scrollPreview = (docView: DocumentView, anchor: Doc, options: DocFocusOptions) => {
+        // if in preview, then override document's fields with view spec
+        this._focusFilters = StrListCast(anchor.presPinDocFilters);
+        this._focusRangeFilters = StrListCast(anchor.presPinDocRangeFilters);
+        this._focusPivotField = StrCast(anchor.presPinPivotField);
+        return undefined;
     };
 
     layoutEngine = () => this._layoutEngine;
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index f93810deb..b75f315ca 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -247,13 +247,13 @@ export class TabDocView extends React.Component<TabDocViewProps> {
                 alert('Cannot pin presentation document to itself');
                 return;
             }
-            const anchorDoc = DocumentManager.Instance.getDocumentView(doc)?.ComponentView?.getAnchor?.(false);
-            const pinDoc = anchorDoc && anchorDoc !== doc ? Doc.MakeDelegate(anchorDoc) : Doc.MakeDelegate(doc);
+            const anchorDoc = DocumentManager.Instance.getDocumentView(doc)?.ComponentView?.getAnchor?.(false, pinProps);
+            const pinDoc = Doc.MakeDelegate(anchorDoc && anchorDoc !== doc ? anchorDoc : doc);
             pinDoc.presentationTargetDoc = anchorDoc ?? doc;
             pinDoc.title = doc.title + ' - Slide';
             pinDoc.data = new List<Doc>(); // the children of the alias' layout are the presentation slide children.  the alias' data field might be children of a collection, PDF data, etc -- in any case we don't want the tree view to "see" this data
             pinDoc.presMovement = doc.type === DocumentType.SCRIPTING || pinProps?.pinDocLayout ? PresMovement.None : PresMovement.Zoom;
-            pinDoc.presDuration = 2000;
+            pinDoc.presDuration = pinDoc.presDuration ?? 1000;
             pinDoc.groupWithUp = false;
             pinDoc.context = curPres;
             // these should potentially all be props passed down by the CollectionTreeView to the TreeView elements.  That way the PresBox could configure all of its children at render time
@@ -274,7 +274,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
                 pinDoc.presStartTime = NumCast(doc.clipStart);
                 pinDoc.presEndTime = NumCast(doc.clipEnd, duration);
             }
-            PresBox.pinDocView(pinDoc, pinProps.pinDocContent ? { ...pinProps, pinData: PresBox.pinDataTypes(doc) } : pinProps, doc);
+            //PresBox.pinDocView(pinDoc, pinProps.pinDocContent ? { ...pinProps, pinData: PresBox.pinDataTypes(doc) } : pinProps, anchorDoc && anchorDoc !== doc && !anchorDoc.unrendered ? anchorDoc : doc);
             pinDoc.onClick = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)');
             Doc.AddDocToList(curPres, 'data', pinDoc, presSelected);
             //save position
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 7dd9cdb8b..fa0695fb2 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -14,6 +14,7 @@ export interface ViewDefBounds {
     x: number;
     y: number;
     z?: number;
+    rotation?: number;
     text?: string;
     zIndex?: number;
     width?: number;
@@ -31,6 +32,7 @@ export interface PoolData {
     x: number;
     y: number;
     z?: number;
+    rotation?: number;
     zIndex?: number;
     width?: number;
     height?: number;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 7d176f426..0477c6a16 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -8,7 +8,6 @@ import { DataSym, Doc, DocListCast, HeightSym, Opt, StrListCast, WidthSym } from
 import { Id } from '../../../../fields/FieldSymbols';
 import { InkData, InkField, InkTool, PointData, Segment } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
-import { ObjectField } from '../../../../fields/ObjectField';
 import { RichTextField } from '../../../../fields/RichTextField';
 import { listSpec } from '../../../../fields/Schema';
 import { ScriptField } from '../../../../fields/ScriptField';
@@ -41,7 +40,7 @@ import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDo
 import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, ViewAdjustment } from '../../nodes/DocumentView';
 import { FieldViewProps } from '../../nodes/FieldView';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
-import { PresBox } from '../../nodes/trails/PresBox';
+import { PinProps, PresBox } from '../../nodes/trails/PresBox';
 import { VideoBox } from '../../nodes/VideoBox';
 import { CreateImage } from '../../nodes/WebBoxRenderer';
 import { StyleProp } from '../../StyleProvider';
@@ -523,6 +522,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     }
                 }
                 break;
+            case StyleProp.FillColor:
+                if (doc && this.Document._currentFrame !== undefined) {
+                    return CollectionFreeFormDocumentView.getStringValues(doc, NumCast(this.Document._currentFrame))?.fillColor;
+                }
         }
         return styleProp;
     };
@@ -1216,7 +1219,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         // if (SelectionManager.Views().length !== 1 || SelectionManager.Views()[0].Document !== doc) {
         //     SelectionManager.DeselectAll();
         // }
-        if (this.props.getScrollHeight || this.props.Document.scrollTop !== undefined || this.props.Document.currentTimecode !== undefined || doc.type === DocumentType.MARKER) {
+        if (this.props.getScrollHeight || this.props.Document.scrollTop !== undefined || this.props.Document.currentTimecode !== undefined) {
             this.props.focus(doc, options);
         } else {
             const xfToCollection = options?.docTransform ?? Transform.Identity();
@@ -1386,7 +1389,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 dontScaleFilter={this.props.dontScaleFilter}
                 dontRegisterView={this.props.dontRenderDocuments || this.props.dontRegisterView}
                 pointerEvents={this.pointerEvents}
-                rotation={this.props.styleProvider?.(childLayout, this.props, StyleProp.JitterRotation) || 0}
+                //rotation={this.props.styleProvider?.(childLayout, this.props, StyleProp.JitterRotation) || 0}
                 //fitContentsToBox={this.props.fitContentsToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this
             />
         );
@@ -1425,17 +1428,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const contentFrameNumber = Cast(childDocLayout._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
         const { z, zIndex } = childDoc;
         const { backgroundColor, color } = contentFrameNumber === undefined ? { backgroundColor: undefined, color: undefined } : CollectionFreeFormDocumentView.getStringValues(childDoc, contentFrameNumber);
-        const { x, y, opacity } = layoutFrameNumber === undefined ? { x: childDoc.x, y: childDoc.y, opacity: this.props.childOpacity?.() } : CollectionFreeFormDocumentView.getValues(childDoc, layoutFrameNumber);
+        const { x, y, _width, _height, opacity, _rotation } =
+            layoutFrameNumber === undefined
+                ? { _width: Cast(childDocLayout._width, 'number'), _height: Cast(childDocLayout._height, 'number'), _rotation: Cast(childDocLayout._rotation, 'number'), x: childDoc.x, y: childDoc.y, opacity: this.props.childOpacity?.() }
+                : CollectionFreeFormDocumentView.getValues(childDoc, layoutFrameNumber);
         return {
             x: Number.isNaN(NumCast(x)) ? 0 : NumCast(x),
             y: Number.isNaN(NumCast(y)) ? 0 : NumCast(y),
             z: Cast(z, 'number'),
+            rotation: Cast(_rotation, 'number'),
             color: Cast(color, 'string') ? StrCast(color) : this.props.styleProvider?.(childDoc, this.props, StyleProp.Color),
             backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this.getClusterColor(childDoc, this.props, StyleProp.BackgroundColor),
             opacity: this._keyframeEditing ? 1 : Cast(opacity, 'number') ?? this.props.styleProvider?.(childDoc, this.props, StyleProp.Opacity),
             zIndex: Cast(zIndex, 'number'),
-            width: Cast(childDocLayout._width, 'number'),
-            height: Cast(childDocLayout._height, 'number'),
+            width: _width,
+            height: _height,
             transition: StrCast(childDocLayout.dataTransition),
             pair: params.pair,
             replica: '',
@@ -1556,6 +1563,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     newPos.x !== lastPos.x ||
                     newPos.y !== lastPos.y ||
                     newPos.z !== lastPos.z ||
+                    newPos.rotation !== lastPos.rotation ||
                     newPos.zIndex !== lastPos.zIndex ||
                     newPos.transition !== lastPos.transition
                 ) {
@@ -1592,37 +1600,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     }
 
     @action
-    scrollFocus = (anchor: Doc, options: DocFocusOptions) => {
+    scrollFocus = (docView: DocumentView, anchor: Doc, options: DocFocusOptions) => {
         const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
         if (options.preview) {
             this._focusFilters = StrListCast(anchor.presPinDocFilters);
             this._focusRangeFilters = StrListCast(anchor.presPinDocRangeFilters);
             return undefined;
         }
-        return PresBox.restoreTargetDocView(
-            this.props.DocumentView?.(), //
-            { pinDocLayout: BoolCast(anchor.presPinLayout) },
-            anchor,
-            focusSpeed,
-            {
-                dataview: anchor.presData ? true : false,
-                pannable: anchor.presPinData ? true : false,
-                viewType: anchor.presPinViewType ? true : false,
-                filters: anchor.presPinDocFilters || anchor.presPinDocRangeFilters ? true : false,
-            }
-        )
-            ? focusSpeed
-            : undefined;
+        return PresBox.restoreTargetDocView(docView, anchor, focusSpeed) ? focusSpeed : undefined;
     }; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
 
-    getAnchor = (addAsAnnotation: boolean) => {
-        if (this.props.Document.annotationOn) {
-            return this.rootDoc;
-        }
-
+    getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         // create an anchor that saves information about the current state of the freeform view (pan, zoom, view type)
-        const anchor = Docs.Create.TextanchorDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._viewType), presTransition: 500, annotationOn: this.rootDoc });
-        PresBox.pinDocView(anchor, { pinData: { pannable: true, viewType: true, filters: true } }, this.rootDoc);
+        const anchor = Docs.Create.CollectionAnchorDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._viewType), unrendered: true, presTransition: 500, annotationOn: this.rootDoc });
+        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: true, viewType: true, filters: true } }, this.rootDoc);
 
         if (addAsAnnotation) {
             if (Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), null) !== undefined) {
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 1d59d3356..81dc3aafd 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -18,6 +18,7 @@ import { ContextMenuProps } from '../ContextMenuItem';
 import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
 import './AudioBox.scss';
 import { FieldView, FieldViewProps } from './FieldView';
+import { PinProps, PresBox } from './trails';
 
 /**
  * AudioBox
@@ -136,20 +137,19 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         return { la1, la2, linkTime };
     }
 
-    getAnchor = (addAsAnnotation: boolean) => {
-        return (
+    getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+        const anchor =
             CollectionStackedTimeline.createAnchor(
                 this.rootDoc,
                 this.dataDoc,
                 this.annotationKey,
-                '_timecodeToShow' /* audioStart */,
-                '_timecodeToHide' /* audioEnd */,
                 this._ele?.currentTime || Cast(this.props.Document._currentTimecode, 'number', null) || (this.mediaState === media_state.Recording ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined),
                 undefined,
                 undefined,
                 addAsAnnotation
-            ) || this.rootDoc
-        );
+            ) || this.rootDoc;
+        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.rootDoc);
+        return anchor;
     };
 
     // updates timecode and shows it in timeline, follows links at time
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index ba510e1dc..9a32556f8 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,7 +1,6 @@
 import { action, computed, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc, Opt } from '../../../fields/Doc';
-import { InkField } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { ComputedField } from '../../../fields/ScriptField';
@@ -20,11 +19,10 @@ import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView';
 import React = require('react');
 
 export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
-    dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; color?: string; backgroundColor?: string; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined;
+    dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; rotation?: number; color?: string; backgroundColor?: string; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined;
     sizeProvider?: (doc: Doc, replica: string) => { width: number; height: number } | undefined;
     renderCutoffProvider: (doc: Doc) => boolean;
     zIndex?: number;
-    rotation: number;
     dataTransition?: string;
     replica: string;
     CollectionFreeFormView: CollectionFreeFormView;
@@ -46,7 +44,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         { key: 'panX' },
         { key: 'panY' },
     ]; // fields that are configured to be animatable using animation frames
-    public static animStringFields = ['backgroundColor', 'color']; // fields that are configured to be animatable using animation frames
+    public static animStringFields = ['backgroundColor', 'color', 'fillColor']; // fields that are configured to be animatable using animation frames
     public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames
     @observable _animPos: number[] | undefined = undefined;
     @observable _contentView: DocumentView | undefined | null;
@@ -56,7 +54,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     }
 
     get transform() {
-        return `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Document.rotation, this.props.rotation)}deg)`;
+        return `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Rot, this.Rot)}deg)`;
     }
     get X() {
         return this.dataProvider?.x ?? NumCast(this.Document.x);
@@ -67,6 +65,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     get ZInd() {
         return this.dataProvider?.zIndex ?? NumCast(this.Document.zIndex);
     }
+    get Rot() {
+        return this.dataProvider?.rotation ?? NumCast(this.Document._rotation);
+    }
     get Opacity() {
         return this.dataProvider?.opacity;
     }
@@ -84,13 +85,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     }
 
     styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => {
-        if (doc === this.layoutDoc)
+        if (doc === this.layoutDoc) {
             // prettier-ignore
             switch (property) {
                 case StyleProp.Opacity:         return this.Opacity; // only change the opacity for this specific document, not its children
                 case StyleProp.BackgroundColor: return this.BackgroundColor;
                 case StyleProp.Color:           return this.Color;
             }
+        }
         return this.props.styleProvider?.(doc, props, property);
     };
 
@@ -108,6 +110,15 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         }, {} as { [val: string]: Opt<string> });
     }
 
+    public static setStringValues(time: number, d: Doc, vals: { [val: string]: Opt<string> }) {
+        const timecode = Math.round(time);
+        Object.keys(vals).forEach(val => {
+            const findexed = Cast(d[`${val}-indexed`], listSpec('string'), []).slice();
+            findexed[timecode] = vals[val] as any as string;
+            d[`${val}-indexed`] = new List<string>(findexed);
+        });
+    }
+
     public static setValues(time: number, d: Doc, vals: { [val: string]: Opt<number> }) {
         const timecode = Math.round(time);
         Object.keys(vals).forEach(val => {
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 2432d4c37..5783d0e57 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -16,6 +16,7 @@ import { LinkDescriptionPopup } from './LinkDescriptionPopup';
 import { TaskCompletionBox } from './TaskCompletedBox';
 import React = require('react');
 import _ = require('lodash');
+import { PinProps } from './trails';
 
 const higflyout = require('@hig/flyout');
 export const { anchorPoints } = higflyout;
@@ -175,7 +176,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
     };
 
     public static finishLinkClick = undoBatch(
-        action((screenX: number, screenY: number, startLink: Doc, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView) => {
+        action((screenX: number, screenY: number, startLink: Doc, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView, pinProps?: PinProps) => {
             if (startLink === endLink) {
                 DocumentLinksButton.StartLink = undefined;
                 DocumentLinksButton.StartLinkView = undefined;
@@ -183,7 +184,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
                 DocumentLinksButton.AnnotationUri = undefined;
                 //!this.props.StartLink
             } else if (startLink !== endLink) {
-                endLink = endLinkView?.docView?._componentView?.getAnchor?.(true) || endLink;
+                endLink = endLinkView?.docView?._componentView?.getAnchor?.(true, pinProps) || endLink;
                 startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.(true) || startLink;
                 const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined, undefined, undefined, true);
 
@@ -283,7 +284,18 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
                         className={'documentLinksButton-endLink'}
                         ref={this._linkButton}
                         onPointerDown={DocumentLinksButton.StartLink && this.completeLink}
-                        onClick={e => DocumentLinksButton.StartLink && DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View)}>
+                        onClick={e =>
+                            DocumentLinksButton.StartLink &&
+                            DocumentLinksButton.finishLinkClick(
+                                e.clientX,
+                                e.clientY,
+                                DocumentLinksButton.StartLink,
+                                this.props.View.props.Document,
+                                true,
+                                this.props.View,
+                                (e.shiftKey ? { pinDocLayout: true, pinDocContent: true, pinData: { poslayoutview: true, dataannos: true, dataview: true } } : {}) as PinProps
+                            )
+                        }>
                         <FontAwesomeIcon className="documentdecorations-icon" icon="link" />
                     </div>
                 ) : null}
@@ -292,7 +304,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
     }
 
     render() {
-        const menuTitle = this.props.StartLink ? 'Drag or tap to start link' : 'Tap to complete link';
+        const menuTitle = this.props.StartLink ? 'Drag or tap to start link' : 'Tap to complete link (shift key captures viewing state)';
         const buttonTitle = 'Tap to view links; double tap to open link collection';
         const title = this.props.ShowCount ? buttonTitle : menuTitle;
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 9d966cdb2..ff6fe3712 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -54,7 +54,7 @@ import { LinkDocPreview } from './LinkDocPreview';
 import { RadialMenu } from './RadialMenu';
 import { ScriptingBox } from './ScriptingBox';
 import { PresEffect, PresEffectDirection } from './trails';
-import { PinProps } from './trails/PresBox';
+import { PinProps, PresBox } from './trails/PresBox';
 import React = require('react');
 import { GestureOverlay } from '../GestureOverlay';
 const { Howl } = require('howler');
@@ -121,8 +121,9 @@ export type DocFocusFunc = (doc: Doc, options: DocFocusOptions) => void;
 export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => any;
 export interface DocComponentView {
     updateIcon?: () => void; // updates the icon representation of the document
-    getAnchor?: (addAsAnnotation: boolean) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
-    scrollFocus?: (doc: Doc, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus
+    getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
+    scrollPreview?: (docView: DocumentView, doc: Doc, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus
+    scrollFocus?: (docView: DocumentView, doc: Doc, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus
     brushView?: (view: { width: number; height: number; panX: number; panY: number }) => void;
     reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height.  However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling.
     shrinkWrap?: () => void; // requests a document to display all of its contents with no white space.  currently only implemented (needed?) for freeform views
@@ -585,7 +586,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         LightboxView.SetCookie(StrCast(anchor['cookies-set']));
 
         // Restore viewing specification of target by reading them out of the anchor and applying to the target doc.
-        const focusSpeed = this._componentView?.scrollFocus?.(anchor, { ...options, preview: LinkDocPreview.LinkInfo ? true : false });
+        const presItem = DocCast(options.originatingDoc); // if originating doc was a presItem, then anchor will be its proto.  use presItem instead
+        const targetMatch = Doc.AreProtosEqual(anchor, this.rootDoc) || (DocCast(anchor)?.unrendered && Doc.AreProtosEqual(DocCast(anchor.annotationOn), this.rootDoc)) ? true : false;
+        const scrollFocus =
+            (LinkDocPreview.LinkInfo ? this._componentView?.scrollPreview : undefined) ??
+            this._componentView?.scrollFocus ??
+            ((docView: DocumentView, anchor: Doc, options: DocFocusOptions) => (focusSpeed => (PresBox.restoreTargetDocView(docView, anchor, focusSpeed) ? focusSpeed : undefined))(options.instant ? 0 : options.zoomTime ?? 500));
+        const focusSpeed = targetMatch && scrollFocus?.(this.props.DocumentView(), presItem?.proto === anchor ? presItem : anchor, options);
 
         // FOCUS: navigate through the display hierarchy making sure the target is in view
         const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus?.(true) ?? ViewAdjustment.doNothing;
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 5c0005dae..8fa01c97a 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -13,7 +13,7 @@ import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxAnnotatableComponent } from '../DocComponent';
-import { DocFocusOptions } from './DocumentView';
+import { DocFocusOptions, DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 
 const EquationSchema = createSchema({});
@@ -42,14 +42,14 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
         );
     }
     getAnchor = (addAsAnnotation: boolean) => {
-        const anchor = Docs.Create.TextanchorDocument({ annotationOn: this.rootDoc });
+        const anchor = Docs.Create.TextanchorDocument({ annotationOn: this.rootDoc, unrendered: true });
         anchor.xRange = new List<number>(Array.from(this._plot.options.xAxis.domain));
         anchor.yRange = new List<number>(Array.from(this._plot.options.yAxis.domain));
         if (addAsAnnotation) this.addDocument(anchor);
         return anchor;
     };
     @action
-    scrollFocus = (doc: Doc, smooth: DocFocusOptions) => {
+    scrollFocus = (docView: DocumentView, doc: Doc, options: DocFocusOptions) => {
         this.dataDoc.xRange = new List<number>(Array.from(Cast(doc.xRange, listSpec('number'), Cast(this.dataDoc.xRange, listSpec('number'), [-10, 10]))));
         this.dataDoc.yRange = new List<number>(Array.from(Cast(doc.yRange, listSpec('number'), Cast(this.dataDoc.xRange, listSpec('number'), [-1, 9]))));
         return 0;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 0ba576e55..363cd1d94 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -8,7 +8,7 @@ import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
 import { createSchema } from '../../../fields/Schema';
 import { ComputedField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { DashColor, emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
@@ -26,11 +26,11 @@ import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComp
 import { MarqueeAnnotator } from '../MarqueeAnnotator';
 import { AnchorMenu } from '../pdf/AnchorMenu';
 import { StyleProp } from '../StyleProvider';
-import { DocFocusOptions, OpenWhere } from './DocumentView';
+import { DocFocusOptions, DocumentView, OpenWhere } from './DocumentView';
 import { FaceRectangles } from './FaceRectangles';
 import { FieldView, FieldViewProps } from './FieldView';
 import './ImageBox.scss';
-import { PresBox } from './trails';
+import { PinProps, PresBox } from './trails';
 import React = require('react');
 import Color = require('color');
 import { LinkDocPreview } from './LinkDocPreview';
@@ -71,34 +71,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document));
     };
 
-    @action
-    scrollFocus = (anchor: Doc, options: DocFocusOptions) => {
-        const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
-        return PresBox.restoreTargetDocView(
-            this.props.DocumentView?.(), //
-            { pinDocLayout: BoolCast(anchor.presPinLayout) },
-            anchor,
-            focusSpeed,
-            !anchor.presPinData
-                ? {}
-                : {
-                      pannable: true,
-                      dataannos: anchor.presAnnotations !== undefined,
-                      dataview: true,
-                  }
-        )
-            ? focusSpeed
-            : undefined;
-    }; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
-
-    getAnchor = (addAsAnnotation: boolean) => {
+    getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         const anchor =
             this._getAnchor?.(this._savedAnnotations, false) ?? // use marquee anchor, otherwise, save zoom/pan as anchor
             Docs.Create.ImageanchorDocument({ title: 'ImgAnchor:' + this.rootDoc.title, presTransition: 1000, unrendered: true, annotationOn: this.rootDoc });
         if (anchor) {
             if (!addAsAnnotation) anchor.backgroundColor = 'transparent';
             /* addAsAnnotation &&*/ this.addDocument(anchor);
-            PresBox.pinDocView(anchor, { pinData: { pannable: true } }, this.rootDoc);
+            PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: true } }, this.rootDoc);
             return anchor;
         }
         return this.rootDoc;
@@ -177,9 +157,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         const nh = nscale / NumCast(this.dataDoc[this.fieldKey + '-nativeHeight']);
         const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '-nativeWidth']);
         this.dataDoc[this.fieldKey + '-nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '-nativeHeight']) * nh;
-        this.dataDoc[this.fieldKey + '-nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '-nativeWidth']) * nw;
+        this.dataDoc[this.fieldKey + '-nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '-nativeWidth']) * nh;
         this.rootDoc._panX = nh * NumCast(this.rootDoc._panX);
-        this.rootDoc._panY = nw * NumCast(this.rootDoc._panY);
+        this.rootDoc._panY = nh * NumCast(this.rootDoc._panY);
         this.dataDoc._panXMax = this.dataDoc._panXMax ? nh * NumCast(this.dataDoc._panXMax) : undefined;
         this.dataDoc._panXMin = this.dataDoc._panXMin ? nh * NumCast(this.dataDoc._panXMin) : undefined;
         this.dataDoc._panYMax = this.dataDoc._panYMax ? nw * NumCast(this.dataDoc._panYMax) : undefined;
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index a2143f629..33fab6873 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -36,8 +36,6 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp
         this._timeout && clearTimeout(this._timeout);
     }
 
-    getAnchor = (addAsAnnotation: boolean) => this.rootDoc;
-
     getTitle() {
         return this.rootDoc['title-custom'] ? StrCast(this.rootDoc.title) : this.props.label ? this.props.label : typeof this.rootDoc[this.fieldKey] === 'string' ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title);
     }
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 88d134bba..8d7ab2f0d 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -5,7 +5,7 @@ import * as Pdfjs from 'pdfjs-dist';
 import 'pdfjs-dist/web/pdf_viewer.css';
 import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
-import { BoolCast, Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
+import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField, PdfField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils';
@@ -22,13 +22,13 @@ import { LightboxView } from '../LightboxView';
 import { CreateImage } from '../nodes/WebBoxRenderer';
 import { PDFViewer } from '../pdf/PDFViewer';
 import { SidebarAnnos } from '../SidebarAnnos';
+import { DocFocusOptions, DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { ImageBox } from './ImageBox';
 import './PDFBox.scss';
+import { PinProps, PresBox } from './trails';
 import { VideoBox } from './VideoBox';
 import React = require('react');
-import { PresBox } from './trails';
-import { DocFocusOptions } from './DocumentView';
 
 @observer
 export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
@@ -200,18 +200,18 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
 
     brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._pdfViewer?.brushView(view);
 
-    scrollFocus = (doc: Doc, options: DocFocusOptions) => {
+    scrollFocus = (docView: DocumentView, anchor: Doc, options: DocFocusOptions) => {
         let didToggle = false;
-        if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
+        if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(anchor) && !this.SidebarShown) {
             this.toggleSidebar(options.preview);
             didToggle = true;
         }
-        if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
-        this._initialScrollTarget = doc;
-        PresBox.restoreTargetDocView(this.props.DocumentView?.(), {}, doc, options.zoomTime ?? 500, { pannable: doc.presPinData ? true : false });
-        return this._pdfViewer?.scrollFocus(doc, NumCast(doc.presPinViewScroll, NumCast(doc.y)), options) ?? (didToggle ? 1 : undefined);
+        if (this._sidebarRef?.current?.makeDocUnfiltered(anchor)) return 1;
+        this._initialScrollTarget = anchor;
+        PresBox.restoreTargetDocView(docView, anchor, options.zoomTime ?? 500);
+        return this._pdfViewer?.scrollFocus(anchor, NumCast(anchor.presPinViewScroll, NumCast(anchor.y)), options) ?? (didToggle ? 1 : undefined);
     };
-    getAnchor = (addAsAnnotation: boolean) => {
+    getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         let ele: Opt<HTMLDivElement> = undefined;
         if (this._pdfViewer?.selectionContent()) {
             ele = document.createElement('div');
@@ -223,11 +223,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                 unrendered: true,
                 annotationOn: this.rootDoc,
             });
-            PresBox.pinDocView(anchor, { pinData: { scrollable: true, pannable: true } }, this.rootDoc);
             return anchor;
         };
         const annoAnchor = this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations(), true);
         const anchor = annoAnchor ?? docAnchor();
+        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true, pannable: true } }, this.rootDoc);
         anchor.text = ele?.textContent ?? '';
         anchor.textHtml = ele?.innerHTML;
         if (addAsAnnotation || annoAnchor) {
@@ -287,8 +287,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
 
     setPdfViewer = (pdfViewer: PDFViewer) => {
         this._pdfViewer = pdfViewer;
-        if (this._initialScrollTarget) {
-            this.scrollFocus(this._initialScrollTarget, { instant: true });
+        const docView = this.props.DocumentView?.();
+        if (this._initialScrollTarget && docView) {
+            this.scrollFocus(docView, this._initialScrollTarget, { instant: true });
             this._initialScrollTarget = undefined;
         }
     };
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index f94996c66..61e4894f0 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -130,7 +130,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
     }
     getAnchor = (addAsAnnotation: boolean) => {
         const startTime = Cast(this.layoutDoc._currentTimecode, 'number', null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined);
-        return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, '_timecodeToShow', '_timecodeToHide', startTime, startTime === undefined ? undefined : startTime + 3, undefined, addAsAnnotation) || this.rootDoc;
+        return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, startTime, startTime === undefined ? undefined : startTime + 3, undefined, addAsAnnotation) || this.rootDoc;
     };
 
     videoLoad = () => {
diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx
index 281967a21..fa2021642 100644
--- a/src/client/views/nodes/ScriptingBox.tsx
+++ b/src/client/views/nodes/ScriptingBox.tsx
@@ -20,6 +20,7 @@ import { EditableView } from '../EditableView';
 import { FieldView, FieldViewProps } from '../nodes/FieldView';
 import { OverlayView } from '../OverlayView';
 import { DocumentIconContainer } from './DocumentIcon';
+import { DocFocusOptions, DocumentView } from './DocumentView';
 import './ScriptingBox.scss';
 const _global = (window /* browser */ || global) /* node */ as any;
 
@@ -481,10 +482,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
         return value;
     }
 
-    scrollFocus = () => {
-        return undefined;
-    };
-
     getSuggestedParams(pos: number) {
         const firstScript = this.rawText.slice(0, pos);
         const indexP = firstScript.lastIndexOf('.');
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 1dfa55c64..c26562e7c 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -7,6 +7,7 @@ import * as rp from 'request-promise';
 import { Doc, DocListCast, HeightSym, WidthSym } from '../../../fields/Doc';
 import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
+import { ObjectField } from '../../../fields/ObjectField';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { AudioField, ImageField, VideoField } from '../../../fields/URLField';
 import { emptyFunction, formatTime, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
@@ -27,11 +28,11 @@ import { DocumentDecorations } from '../DocumentDecorations';
 import { MarqueeAnnotator } from '../MarqueeAnnotator';
 import { AnchorMenu } from '../pdf/AnchorMenu';
 import { StyleProp } from '../StyleProvider';
+import { OpenWhere } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { RecordingBox } from './RecordingBox';
+import { PinProps, PresBox } from './trails';
 import './VideoBox.scss';
-import { ObjectField } from '../../../fields/ObjectField';
-import { DocFocusOptions, OpenWhere } from './DocumentView';
 const path = require('path');
 
 /**
@@ -390,13 +391,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, 'move', true));
     };
 
-    getAnchor = (addAsAnnotation: boolean) => {
+    getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         const timecode = Cast(this.layoutDoc._currentTimecode, 'number', null);
         const marquee = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation);
-        return (
-            CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, '_timecodeToShow' /* videoStart */, '_timecodeToHide' /* videoEnd */, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) ||
-            this.rootDoc
-        );
+        if (!addAsAnnotation && marquee) marquee.backgroundColor = 'transparent';
+        const anchor = CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) || this.rootDoc;
+        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.rootDoc);
+        return anchor;
     };
 
     // sets video info on load
@@ -953,14 +954,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         );
     };
 
-    scrollFocus = (doc: Doc, options: DocFocusOptions) => {
-        if (doc !== this.rootDoc) {
-            const showTime = Cast(doc._timecodeToShow, 'number', null);
-            showTime !== undefined && setTimeout(() => this.Seek(showTime), 100);
-            return 0.1;
-        }
-    };
-
     // renders CollectionStackedTimeline
     @computed get renderTimeline() {
         return (
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 4c9bead9d..caab18e0d 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, trace } from 'mobx';
+import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as WebRequest from 'web-request';
 import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
@@ -11,8 +11,9 @@ import { listSpec } from '../../../fields/Schema';
 import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField, WebField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, getWordAtPoint, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils';
+import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
+import { DragManager } from '../../util/DragManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SnappingManager } from '../../util/SnappingManager';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
@@ -21,7 +22,6 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm/Collec
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
-import { DocumentDecorations } from '../DocumentDecorations';
 import { Colors } from '../global/globalEnums';
 import { LightboxView } from '../LightboxView';
 import { MarqueeAnnotator } from '../MarqueeAnnotator';
@@ -29,13 +29,13 @@ import { AnchorMenu } from '../pdf/AnchorMenu';
 import { Annotation } from '../pdf/Annotation';
 import { SidebarAnnos } from '../SidebarAnnos';
 import { StyleProp } from '../StyleProvider';
-import { DocFocusOptions, DocumentView, DocumentViewInternal, DocumentViewProps } from './DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewProps } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { LinkDocPreview } from './LinkDocPreview';
 import { VideoBox } from './VideoBox';
 import './WebBox.scss';
 import React = require('react');
-import { DragManager } from '../../util/DragManager';
+import { PinProps, PresBox } from './trails';
 const { CreateImage } = require('./WebBoxRenderer');
 const _global = (window /* browser */ || global) /* node */ as any;
 const htmlToText = require('html-to-text');
@@ -189,6 +189,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             const reqdFuncs: { [key: string]: string } = {};
             // bcz: need to make sure that doc.data-annotations points to the currently active web page's annotations (this could/should be when the doc is created)
             reqdFuncs[this.fieldKey + '-annotations'] = `copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-annotations"`;
+            reqdFuncs[this.fieldKey + '-annotations-setter'] = `this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-annotations"] = value`;
             reqdFuncs[this.fieldKey + '-sidebar'] = `copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-sidebar"`;
             DocUtils.AssignScripts(this.dataDoc, {}, reqdFuncs);
         });
@@ -293,27 +294,35 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
 
     setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => (this._setBrushViewer = func);
     brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._setBrushViewer?.(view);
-    scrollFocus = (doc: Doc, options: DocFocusOptions) => {
-        if (this._url && StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl), options.preview);
-        if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
+    focus = (doc: Doc, options: DocFocusOptions) => {
+        !doc.unrendered && this.props.DocumentView?.() && this.scrollFocus(this.props.DocumentView?.(), doc, {});
+        this.props.focus(doc, options);
+    };
+    scrollFocus = (docView: DocumentView, anchor: Doc, options: DocFocusOptions) => {
+        if (this._url && StrCast(anchor.webUrl) !== this._url) this.submitURL(StrCast(anchor.webUrl), options.preview);
+        if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(anchor) && !this.SidebarShown) {
             this.toggleSidebar(options.preview);
         }
-        if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
-        if (doc !== this.rootDoc && this._outerRef.current) {
+        if (this._sidebarRef?.current?.makeDocUnfiltered(anchor)) return 1;
+        if (anchor !== this.rootDoc && this._outerRef.current) {
             const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
-            const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(doc.y) + doc[HeightSym](), this._scrollHeight));
+            const scrollTo = !anchor[HeightSym]()
+                ? NumCast(anchor.y)
+                : Utils.scrollIntoView(NumCast(anchor.y), anchor[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(anchor.y) + anchor[HeightSym](), this._scrollHeight));
+            const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
             if (scrollTo !== undefined && this._initialScroll === undefined) {
-                const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
                 this.goTo(scrollTo, focusSpeed, options.easeFunc);
+                PresBox.restoreTargetDocView(docView, anchor, focusSpeed);
                 return focusSpeed;
             } else if (!this._webPageHasBeenRendered || !this._scrollHeight || this._initialScroll !== undefined) {
                 this._initialScroll = scrollTo;
+                return PresBox.restoreTargetDocView(docView, anchor, focusSpeed) ? focusSpeed : undefined;
             }
         }
         return undefined;
     };
 
-    getAnchor = (addAsAnnotation: boolean) => {
+    getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         let ele: Opt<HTMLDivElement> = undefined;
         try {
             const contents = this._iframe?.contentWindow?.getSelection()?.getRangeAt(0).cloneContents();
@@ -330,9 +339,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                 unrendered: true,
                 annotationOn: this.rootDoc,
             });
+        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true, pannable: true } }, this.rootDoc);
         anchor.text = ele?.textContent ?? '';
         anchor.textHtml = ele?.innerHTML;
-        addAsAnnotation && this.addDocumentWrapper(anchor);
+        //addAsAnnotation &&
+        this.addDocumentWrapper(anchor);
         return anchor;
     };
 
@@ -901,6 +912,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                 PanelHeight={this.panelHeight}
                 ScreenToLocalTransform={this.scrollXf}
                 NativeDimScaling={returnOne}
+                focus={this.focus}
                 dropAction={'alias'}
                 docFilters={docFilters}
                 select={emptyFunction}
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 93761633c..0468d1c47 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -27,6 +27,7 @@ import { ActiveFillColor, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, SetAc
 import { InkTranscription } from '../../InkTranscription';
 import { StyleProp } from '../../StyleProvider';
 import { FieldView, FieldViewProps } from '.././FieldView';
+import { CollectionFreeFormDocumentView } from '../CollectionFreeFormDocumentView';
 import { OpenWhere } from '../DocumentView';
 import { RichTextMenu } from '../formattedText/RichTextMenu';
 import { WebBox } from '../WebBox';
@@ -551,11 +552,30 @@ ScriptingGlobals.add(function setView(view: string) {
 
 // toggle: Set overlay status of selected document
 ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: boolean) {
-    const selected = SelectionManager.Views().lastElement()?.props.Document ?? LinkManager.currentLink;
-    if (checkResult) {
-        return selected?._backgroundColor ?? 'transparent';
+    const selectedViews = SelectionManager.Views();
+    if (selectedViews.length) {
+        if (checkResult) {
+            const selView = selectedViews.lastElement();
+            const layoutFrameNumber = Cast(selView.props.ContainingCollectionDoc?._currentFrame, 'number'); // frame number that container is at which determines layout frame values
+            const contentFrameNumber = Cast(selView.rootDoc?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
+            return CollectionFreeFormDocumentView.getStringValues(selView?.rootDoc, contentFrameNumber).backgroundColor ?? 'transparent';
+        }
+        selectedViews.forEach(dv => {
+            const layoutFrameNumber = Cast(dv.props.ContainingCollectionDoc?._currentFrame, 'number'); // frame number that container is at which determines layout frame values
+            const contentFrameNumber = Cast(dv.rootDoc?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
+            if (contentFrameNumber !== undefined) {
+                CollectionFreeFormDocumentView.setStringValues(contentFrameNumber, dv.rootDoc, { backgroundColor: color });
+            } else {
+                dv.rootDoc._backgroundColor = color;
+            }
+        });
+    } else {
+        const selected = SelectionManager.Docs().length ? SelectionManager.Docs() : LinkManager.currentLink ? [LinkManager.currentLink] : [];
+        if (checkResult) {
+            return selected.lastElement()?._backgroundColor ?? 'transparent';
+        }
+        selected.forEach(doc => (doc._backgroundColor = color));
     }
-    if (selected) selected._backgroundColor = color;
 });
 
 // toggle: Set overlay status of selected document
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 788de7af9..269438a7c 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -45,7 +45,7 @@ import { LightboxView } from '../../LightboxView';
 import { AnchorMenu } from '../../pdf/AnchorMenu';
 import { SidebarAnnos } from '../../SidebarAnnos';
 import { StyleProp } from '../../StyleProvider';
-import { DocFocusOptions, DocumentViewInternal, OpenWhere } from '../DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewInternal, OpenWhere } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
 import { LinkDocPreview } from '../LinkDocPreview';
 import { DashDocCommentView } from './DashDocCommentView';
@@ -932,7 +932,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         return anchorDoc ?? this.rootDoc;
     }
 
-    scrollFocus = (textAnchor: Doc, options: DocFocusOptions) => {
+    scrollFocus = (docView: DocumentView, textAnchor: Doc, options: DocFocusOptions) => {
         let didToggle = false;
         if (DocListCast(this.Document[this.fieldKey + '-sidebar']).includes(textAnchor) && !this.SidebarShown) {
             this.toggleSidebar(options.preview);
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 13cbd87eb..45d386436 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -4,9 +4,9 @@ import { Tooltip } from '@material-ui/core';
 import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { ColorState, SketchPicker } from 'react-color';
-import { AnimationSym, Doc, DocListCast, FieldResult, Opt, StrListCast } from '../../../../fields/Doc';
+import { AnimationSym, Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../../fields/Doc';
 import { Copy, Id } from '../../../../fields/FieldSymbols';
-import { InkTool } from '../../../../fields/InkField';
+import { InkField, InkTool } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
 import { ObjectField } from '../../../../fields/ObjectField';
 import { listSpec } from '../../../../fields/Schema';
@@ -34,8 +34,23 @@ import { FieldView, FieldViewProps } from '../FieldView';
 import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
+import { SerializationHelper } from '../../../util/SerializationHelper';
 const { Howl } = require('howler');
 
+export interface pinDataTypes {
+    scrollable?: boolean;
+    pannable?: boolean;
+    viewType?: boolean;
+    inkable?: boolean;
+    filters?: boolean;
+    pivot?: boolean;
+    temporal?: boolean;
+    clippable?: boolean;
+    dataview?: boolean;
+    textview?: boolean;
+    poslayoutview?: boolean;
+    dataannos?: boolean;
+}
 export interface PinProps {
     audioRange?: boolean;
     activeFrame?: number;
@@ -45,18 +60,7 @@ export interface PinProps {
     pinDocLayout?: boolean; // pin layout info (width/height/x/y)
     pinDocContent?: boolean; // pin data info (scroll/pan/zoom/text)
     pinAudioPlay?: boolean; // pin audio annotation
-    pinData?: {
-        scrollable?: boolean | undefined;
-        pannable?: boolean | undefined;
-        viewType?: boolean | undefined;
-        filters?: boolean | undefined;
-        temporal?: boolean | undefined;
-        clippable?: boolean | undefined;
-        dataview?: boolean | undefined;
-        textview?: boolean | undefined;
-        poslayoutview?: boolean | undefined;
-        dataannos?: boolean | undefined;
-    };
+    pinData?: pinDataTypes;
 }
 
 @observer
@@ -318,19 +322,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             this.onHideDocument(); //Handles hide after/before
         }
     });
-    static pinDataTypes(target?: Doc): {
-        scrollable?: boolean;
-        pannable?: boolean;
-        viewType?: boolean;
-        filters?: boolean;
-        temporal?: boolean;
-        clippable?: boolean;
-        dataview?: boolean;
-        textview?: boolean;
-        poslayoutview?: boolean;
-        dataannos?: boolean;
-    } {
+    static pinDataTypes(target?: Doc): pinDataTypes {
         const targetType = target?.type as any;
+        const inkable = [DocumentType.INK].includes(targetType);
         const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._viewType === CollectionViewType.Stacking;
         const pannable = [DocumentType.IMG, DocumentType.PDF].includes(targetType) || (targetType === DocumentType.COL && target?._viewType === CollectionViewType.Freeform);
         const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(targetType);
@@ -340,18 +334,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const textview = [DocumentType.RTF].includes(targetType) && target?.activeFrame === undefined;
         const viewType = targetType === DocumentType.COL;
         const filters = true;
+        const pivot = true;
         const dataannos = false;
-        return { scrollable, pannable, viewType, filters, temporal, clippable, dataview, textview, poslayoutview, dataannos };
+        return { scrollable, pannable, inkable, viewType, pivot, filters, temporal, clippable, dataview, textview, poslayoutview, dataannos };
     }
 
     @action
     playAnnotation = (anno: AudioField) => {};
     @action
-    static restoreTargetDocView(bestTargetView: Opt<DocumentView>, pinProps: PinProps | undefined, activeItem: Doc, transTime: number, pinDataTypes = this.pinDataTypes(bestTargetView?.rootDoc)) {
-        if (!bestTargetView) return;
-        const bestTarget = bestTargetView.rootDoc;
+    static restoreTargetDocView(bestTargetView: Opt<DocumentView>, activeItem: Doc, transTime: number, pinDocLayout: boolean = BoolCast(activeItem.presPinLayout), pinDataTypes?: pinDataTypes, targetDoc?: Doc) {
+        const bestTarget = bestTargetView?.rootDoc ?? (targetDoc?.unrendered ? DocCast(targetDoc?.annotationOn) : targetDoc);
+        if (!bestTarget) return;
         let changed = false;
-        if (pinProps?.pinDocLayout) {
+        if (pinDocLayout) {
             if (
                 bestTarget.x !== NumCast(activeItem.presX, NumCast(bestTarget.x)) ||
                 bestTarget.y !== NumCast(activeItem.presY, NumCast(bestTarget.y)) ||
@@ -369,33 +364,51 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 changed = true;
             }
         }
-        if (pinDataTypes.clippable) {
+        if (pinDataTypes?.clippable || (!pinDataTypes && activeItem.presPinClipWidth !== undefined)) {
             if (bestTarget._clipWidth !== activeItem.presPinClipWidth) {
                 bestTarget._clipWidth = activeItem.presPinClipWidth;
                 changed = true;
             }
         }
-        if (pinDataTypes.temporal) {
+        if (pinDataTypes?.temporal || (!pinDataTypes && activeItem.presStartTime !== undefined)) {
             if (bestTarget._currentTimecode !== activeItem.presStartTime) {
                 bestTarget._currentTimecode = activeItem.presStartTime;
                 changed = true;
             }
         }
-        if (pinDataTypes.viewType && activeItem.presPinViewType !== undefined) {
+        if (pinDataTypes?.inkable || (!pinDataTypes && (activeItem.presFillColor !== undefined || activeItem.color !== undefined))) {
+            if (bestTarget.fillColor !== activeItem.presFillColor) {
+                Doc.GetProto(bestTarget).fillColor = activeItem.presFillColor;
+                changed = true;
+            }
+            if (bestTarget.color !== activeItem.color) {
+                Doc.GetProto(bestTarget).color = activeItem.color;
+                changed = true;
+            }
+        }
+        if ((pinDataTypes?.viewType && activeItem.presPinViewType !== undefined) || (!pinDataTypes && activeItem.presPinViewType !== undefined)) {
             if (bestTarget._viewType !== activeItem.presPinViewType) {
                 bestTarget._viewType = activeItem.presPinViewType;
                 changed = true;
             }
         }
 
-        if (pinDataTypes.filters && activeItem.presPinDocFilters !== undefined) {
+        if ((pinDataTypes?.filters && activeItem.presPinDocFilters !== undefined) || (!pinDataTypes && activeItem.presPinDocFilters !== undefined)) {
             if (bestTarget.docFilters !== activeItem.presPinDocFilters) {
                 bestTarget.docFilters = ObjectField.MakeCopy(activeItem.presPinDocFilters as ObjectField) || new List<string>([]);
                 changed = true;
             }
         }
 
-        if (pinDataTypes.scrollable) {
+        if ((pinDataTypes?.pivot && activeItem.presPinPvitoField !== undefined) || (!pinDataTypes && activeItem.presPinPivotField !== undefined)) {
+            if (bestTarget.pivotField !== activeItem.presPinPivotField) {
+                bestTarget.pivotField = activeItem.presPinPivotField;
+                bestTarget._prevFilterIndex = 1; // need to revisit this...see CollectionTimeView
+                changed = true;
+            }
+        }
+
+        if (pinDataTypes?.scrollable || (!pinDataTypes && activeItem.presPinViewScroll !== undefined)) {
             if (bestTarget._scrollTop !== activeItem.presPinViewScroll) {
                 bestTarget._scrollTop = activeItem.presPinViewScroll;
                 changed = true;
@@ -406,39 +419,59 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 }
             }
         }
-        if (pinDataTypes.dataannos) {
+        if (pinDataTypes?.dataannos || (!pinDataTypes && activeItem.presAnnotations !== undefined)) {
             const fkey = Doc.LayoutFieldKey(bestTarget);
-            Doc.GetProto(bestTarget)[fkey + '-annotations'] = new List<Doc>([...DocListCast(bestTarget[fkey + '-annotations']).filter(doc => doc.unrendered), ...DocListCast(activeItem.presAnnotations)]);
+            const oldItems = DocListCast(bestTarget[fkey + '-annotations']).filter(doc => doc.unrendered);
+            const newItems = DocListCast(activeItem.presAnnotations).map(doc => {
+                doc.hidden = false;
+                return doc;
+            });
+            const hiddenItems = DocListCast(bestTarget[fkey + '-annotations'])
+                .filter(doc => !doc.unrendered && !newItems.includes(doc))
+                .map(doc => {
+                    doc.hidden = true;
+                    return doc;
+                });
+            const newList = new List<Doc>([...oldItems, ...hiddenItems, ...newItems]);
+            Doc.GetProto(bestTarget)[fkey + '-annotations'] = newList;
         }
-        if (pinDataTypes.dataview && activeItem.presData !== undefined) {
+        if ((pinDataTypes?.dataview && activeItem.presData !== undefined) || (!pinDataTypes && activeItem.presData !== undefined)) {
             bestTarget._dataTransition = `all ${transTime}ms`;
             const fkey = Doc.LayoutFieldKey(bestTarget);
             Doc.GetProto(bestTarget)[fkey] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
             bestTarget[fkey + '-useAlt'] = activeItem.presUseAlt;
             setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10);
         }
-        if (pinDataTypes.textview && activeItem.presData !== undefined) Doc.GetProto(bestTarget)[Doc.LayoutFieldKey(bestTarget)] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
-        if (pinDataTypes.poslayoutview) {
+        if ((pinDataTypes?.textview && activeItem.presData !== undefined) || (!pinDataTypes && activeItem.presData !== undefined)) {
+            Doc.GetProto(bestTarget)[Doc.LayoutFieldKey(bestTarget)] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
+        }
+        if (pinDataTypes?.poslayoutview || (!pinDataTypes && activeItem.presPinLayoutData !== undefined)) {
             changed = true;
+            const layoutField = Doc.LayoutFieldKey(bestTarget);
+            const transitioned = new Set<Doc>();
             StrListCast(activeItem.presPinLayoutData)
-                .map(str => JSON.parse(str) as { id: string; x: number; y: number; w: number; h: number })
-                .forEach(data => {
-                    const doc = DocServer.GetCachedRefField(data.id) as Doc;
-                    doc._dataTransition = `all ${transTime}ms`;
-                    doc.x = data.x;
-                    doc.y = data.y;
-                    doc._width = data.w;
-                    doc._height = data.h;
+                .map(str => JSON.parse(str) as { id: string; x: number; y: number; back: string; fill: string; w: number; h: number; data: string; text: string })
+                .forEach(async data => {
+                    const doc = DocCast(DocServer.GetCachedRefField(data.id));
+                    if (doc) {
+                        transitioned.add(doc);
+                        const field = !data.data ? undefined : await SerializationHelper.Deserialize(data.data);
+                        const tfield = !data.text ? undefined : await SerializationHelper.Deserialize(data.text);
+                        doc._dataTransition = `all ${transTime}ms`;
+                        doc.x = data.x;
+                        doc.y = data.y;
+                        data.back && (doc._backgroundColor = data.back);
+                        data.fill && (doc._fillColor = data.fill);
+                        doc._width = data.w;
+                        doc._height = data.h;
+                        data.data && (Doc.GetProto(doc).data = field);
+                        data.text && (Doc.GetProto(doc).text = tfield);
+                        Doc.AddDocToList(Doc.GetProto(bestTarget), layoutField, doc);
+                    }
                 });
-            setTimeout(
-                () =>
-                    StrListCast(activeItem.presPinLayoutData)
-                        .map(str => JSON.parse(str) as { id: string; x: number; y: number; w: number; h: number })
-                        .forEach(action(data => ((DocServer.GetCachedRefField(data.id) as Doc)._dataTransition = undefined))),
-                transTime + 10
-            );
+            setTimeout(() => Array.from(transitioned).forEach(action(doc => (doc._dataTransition = undefined))), transTime + 10);
         }
-        if (pinDataTypes.pannable) {
+        if (pinDataTypes?.pannable || (!pinDataTypes && (activeItem.presPinViewBounds !== undefined || activeItem.presPinViewX !== undefined || activeItem.presPinViewScale !== undefined))) {
             const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number'));
             if (contentBounds) {
                 const viewport = { panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] };
@@ -460,7 +493,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             }
         }
         if (changed) {
-            return bestTargetView.setViewTransition('all', transTime);
+            return bestTargetView?.setViewTransition('all', transTime);
         }
     }
 
@@ -499,12 +532,31 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 pinDoc.presAnnotations = new List<Doc>(DocListCast(Doc.GetProto(targetDoc)[fkey + '-annotations']).filter(doc => !doc.unrendered));
             }
             if (pinProps.pinData.textview) pinDoc.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof ObjectField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as ObjectField)[Copy]() : targetDoc.text;
+            if (pinProps.pinData.inkable) {
+                pinDoc.presFillColor = targetDoc.fillColor;
+                pinDoc.presColor = targetDoc.color;
+            }
             if (pinProps.pinData.scrollable) pinDoc.presPinViewScroll = targetDoc._scrollTop;
             if (pinProps.pinData.clippable) pinDoc.presPinClipWidth = targetDoc._clipWidth;
             if (pinProps.pinData.poslayoutview)
-                pinDoc.presPinLayoutData = new List<string>(DocListCast(targetDoc[fkey] as ObjectField).map(d => JSON.stringify({ id: d[Id], x: NumCast(d.x), y: NumCast(d.y), w: NumCast(d._width), h: NumCast(d._height) })));
+                pinDoc.presPinLayoutData = new List<string>(
+                    DocListCast(targetDoc[fkey] as ObjectField).map(d =>
+                        JSON.stringify({
+                            id: d[Id],
+                            x: NumCast(d.x),
+                            y: NumCast(d.y),
+                            w: NumCast(d._width),
+                            h: NumCast(d._height),
+                            fill: StrCast(d._fillColor),
+                            back: StrCast(d._backgroundColor),
+                            data: SerializationHelper.Serialize(d.data instanceof ObjectField ? d.data[Copy]() : ''),
+                            text: SerializationHelper.Serialize(d.text instanceof ObjectField ? d.text[Copy]() : ''),
+                        })
+                    )
+                );
             if (pinProps.pinData.viewType) pinDoc.presPinViewType = targetDoc._viewType;
             if (pinProps.pinData.filters) pinDoc.presPinDocFilters = ObjectField.MakeCopy(targetDoc.docFilters as ObjectField);
+            if (pinProps.pinData.pivot) pinDoc.presPinPivotField = targetDoc._pivotField;
             if (pinProps.pinData.pannable) {
                 pinDoc.presPinViewX = NumCast(targetDoc._panX);
                 pinDoc.presPinViewY = NumCast(targetDoc._panY);
@@ -586,7 +638,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const pinDocLayout = (BoolCast(activeItem.presPinLayout) || BoolCast(activeItem.presPinView)) && DocCast(targetDoc.context)?._currentFrame === undefined;
             if (activeItem.presPinData || activeItem.presPinView || pinDocLayout) {
                 // targetDoc may or may not be displayed.  so get the first available document (or alias) view that matches targetDoc and use it
-                PresBox.restoreTargetDocView(DocumentManager.Instance.getFirstDocumentView(targetDoc), { pinDocLayout }, activeItem, NumCast(activeItem.presTransition, 500));
+                // PresBox.restoreTargetDocView(DocumentManager.Instance.getFirstDocumentView(targetDoc), { pinDocLayout }, activeItem, NumCast(activeItem.presTransition, 500), undefined, targetDoc);
             }
         };
         const finishAndRestoreLayout = () => {
@@ -660,16 +712,43 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     _exitTrail: Opt<() => void>;
     PlayTrail = (docs: Doc[]) => {
-        const savedStates = docs.map(doc => (doc._viewType !== CollectionViewType.Freeform ? undefined : { c: doc, x: NumCast(doc.panX), y: NumCast(doc.panY), s: NumCast(doc.viewScale) }));
+        const savedStates = docs.map(doc => {
+            switch (doc.type) {
+                case DocumentType.COL:
+                    if (doc._viewType === CollectionViewType.Freeform) return { type: CollectionViewType.Freeform, doc, x: NumCast(doc.panX), y: NumCast(doc.panY), s: NumCast(doc.viewScale) };
+                    break;
+                case DocumentType.INK:
+                    if (doc.data instanceof InkField) {
+                        return { type: doc.type, doc, data: doc.data?.[Copy](), fillColor: doc.fillColor, color: doc.color, x: NumCast(doc.x), y: NumCast(doc.y) };
+                    }
+            }
+            return undefined;
+        });
         this.startPresentation(0);
         this._exitTrail = () => {
             savedStates
                 .filter(savedState => savedState)
                 .map(savedState => {
-                    const { x, y, s, c } = savedState!;
-                    c._panX = x;
-                    c._panY = y;
-                    c._viewScale = s;
+                    switch (savedState?.type) {
+                        case CollectionViewType.Freeform:
+                            {
+                                const { x, y, s, doc } = savedState!;
+                                doc._panX = x;
+                                doc._panY = y;
+                                doc._viewScale = s;
+                            }
+                            break;
+                        case DocumentType.INK:
+                            {
+                                const { data, fillColor, color, x, y, doc } = savedState!;
+                                doc.x = x;
+                                doc.y = y;
+                                doc.data = data;
+                                doc.fillColor = fillColor;
+                                doc.color = color;
+                            }
+                            break;
+                    }
                 });
             LightboxView.SetLightboxDoc(undefined);
             Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, this.rootDoc);
@@ -1869,7 +1948,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         );
     }
 
-    scrollFocus = () => {
+    scrollFocus = (docView: DocumentView, doc: Doc, options: DocFocusOptions) => {
         // this.gotoDocument(0);
         // this.startOrPause(false);
         return undefined;
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 4a7f5d038..fc8f1da49 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -41,7 +41,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
     @observable private highlightColor: string = 'rgba(245, 230, 95, 0.616)';
     @observable private _showLinkPopup: boolean = false;
 
-    @observable public Highlighting: boolean = false;
     @observable public Status: 'marquee' | 'annotation' | '' = '';
 
     public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search
@@ -121,9 +120,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
 
     @action
     highlightClicked = (e: React.MouseEvent) => {
-        if (!this.Highlight(this.highlightColor, false, undefined, true) && this.Pinned) {
-            this.Highlighting = !this.Highlighting;
-        }
+        this.Highlight(this.highlightColor, false, undefined, true);
         AnchorMenu.Instance.fadeOut(true);
     };
 
@@ -138,7 +135,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
         const button = (
             <button className="antimodeMenu-button anchor-color-preview-button" title="" key="highlighter-button" onClick={this.highlightClicked}>
                 <div className="anchor-color-preview">
-                    <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: 'transform 0.1s', transform: this.Highlighting ? '' : 'rotate(-45deg)' }} />
+                    <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: 'transform 0.1s', transform: 'rotate(-45deg)' }} />
                     <div className="color-preview" style={{ backgroundColor: this.highlightColor }}></div>
                 </div>
             </button>
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 2db44788f..9877690f8 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -30,7 +30,7 @@ const _global = (window /* browser */ || global) /* node */ as any;
 
 //pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`;
 // The workerSrc property shall be specified.
-pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@2.14.305/build/pdf.worker.js';
+pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@2.16.105/build/pdf.worker.js';
 
 interface IViewerProps extends FieldViewProps {
     Document: Doc;
@@ -179,7 +179,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
         let focusSpeed: Opt<number>;
         if (doc !== this.props.rootDoc && mainCont) {
             const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
-            const scrollTo = doc.unrendered ? scrollTop : Utils.scrollIntoView(scrollTop, doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, 0.1 * windowHeight, this._scrollHeight);
+            const scrollTo = doc.unrendered ? scrollTop : Utils.scrollIntoView(scrollTop, doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, this._scrollHeight);
             if (scrollTo !== undefined && scrollTo !== this.props.layoutDoc._scrollTop) {
                 if (!this._pdfViewer) this._initialScroll = { loc: scrollTo, easeFunc: options.easeFunc };
                 else if (!options.instant && !options.preview) this._scrollStopper = smoothScroll((focusSpeed = options.zoomTime ?? 500), mainCont, scrollTo, options.easeFunc, this._scrollStopper);
@@ -518,6 +518,10 @@ export class PDFViewer extends React.Component<IViewerProps> {
         return this.props.styleProvider?.(doc, props, property);
     };
 
+    focus = (doc: Doc, options: DocFocusOptions) => {
+        !doc.unrendered && this.props.DocumentView?.() && this.scrollToAnnotation(doc);
+        this.props.focus(doc, options);
+    };
     renderAnnotations = (docFilters?: () => string[], mixBlendMode?: any, display?: string) => (
         <div
             className="pdfViewerDash-overlay"
@@ -540,6 +544,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
                 PanelWidth={this.panelWidth}
                 ScreenToLocalTransform={this.overlayTransform}
                 dropAction={'alias'}
+                focus={this.focus}
                 docFilters={docFilters}
                 select={emptyFunction}
                 bringToFront={emptyFunction}
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index df49c32f0..ed5eaa756 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -722,13 +722,22 @@ export namespace Doc {
         return bestAlias ?? Doc.MakeAlias(doc);
     }
 
-    export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<Doc, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], dontCreate: boolean, asBranch: boolean): Promise<Doc> {
+    export async function makeClone(
+        doc: Doc,
+        cloneMap: Map<string, Doc>,
+        linkMap: Map<Doc, Doc>,
+        rtfs: { copy: Doc; key: string; field: RichTextField }[],
+        exclusions: string[],
+        topLevelExclusions: string[],
+        dontCreate: boolean,
+        asBranch: boolean
+    ): Promise<Doc> {
         if (Doc.IsBaseProto(doc)) return doc;
         if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
         const copy = dontCreate ? (asBranch ? Cast(doc.branchMaster, Doc, null) || doc : doc) : new Doc(undefined, true);
         cloneMap.set(doc[Id], copy);
         const fieldExclusions = doc.type === DocumentType.MARKER ? exclusions.filter(ex => ex !== 'annotationOn') : exclusions;
-        const filter = [...fieldExclusions, ...Cast(doc.cloneFieldFilter, listSpec('string'), [])];
+        const filter = [...fieldExclusions, ...topLevelExclusions, ...Cast(doc.cloneFieldFilter, listSpec('string'), [])];
         await Promise.all(
             Object.keys(doc).map(async key => {
                 if (filter.includes(key)) return;
@@ -739,10 +748,10 @@ export namespace Doc {
                     const list = await Cast(doc[key], listSpec(Doc));
                     const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc);
                     if (docs !== undefined && docs.length) {
-                        const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)));
+                        const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, [], dontCreate, asBranch)));
                         !dontCreate && assignKey(new List<Doc>(clones));
                     } else if (doc[key] instanceof Doc) {
-                        assignKey(key.includes('layout[') ? undefined : key.startsWith('layout') ? (doc[key] as Doc) : await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)); // reference documents except copy documents that are expanded template fields
+                        assignKey(key.includes('layout[') ? undefined : key.startsWith('layout') ? (doc[key] as Doc) : await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, [], dontCreate, asBranch)); // reference documents except copy documents that are expanded template fields
                     } else {
                         !dontCreate && assignKey(ObjectField.MakeCopy(field));
                         if (field instanceof RichTextField) {
@@ -754,17 +763,18 @@ export namespace Doc {
                 };
                 if (key === 'proto') {
                     if (doc[key] instanceof Doc) {
-                        assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch));
+                        assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, [], dontCreate, asBranch));
                     }
                 } else if (key === 'anchor1' || key === 'anchor2') {
                     if (doc[key] instanceof Doc) {
-                        assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, true, asBranch));
+                        assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, [], true, asBranch));
                     }
                 } else {
                     if (field instanceof RefField) {
                         assignKey(field);
                     } else if (cfield instanceof ComputedField) {
-                        !dontCreate && assignKey(ComputedField.MakeFunction(cfield.script.originalScript));
+                        !dontCreate && assignKey(cfield[Copy]());
+                        //   ComputedField.MakeFunction(cfield.script.originalScript));
                     } else if (field instanceof ObjectField) {
                         await copyObjectField(field);
                     } else if (field instanceof Promise) {
@@ -776,7 +786,7 @@ export namespace Doc {
             })
         );
         for (const link of Array.from(doc[DirectLinksSym])) {
-            const linkClone = await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch);
+            const linkClone = await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, [], dontCreate, asBranch);
             linkMap.set(link, linkClone);
         }
         if (!dontCreate) {
@@ -793,7 +803,7 @@ export namespace Doc {
     export async function MakeClone(doc: Doc, dontCreate: boolean = false, asBranch = false, cloneMap: Map<string, Doc> = new Map()) {
         const linkMap = new Map<Doc, Doc>();
         const rtfMap: { copy: Doc; key: string; field: RichTextField }[] = [];
-        const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf', 'branches', 'branchOf', 'context'], dontCreate, asBranch);
+        const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf', 'branches', 'branchOf'], ['context'], dontCreate, asBranch);
         Array.from(linkMap.entries()).map((links: Doc[]) => LinkManager.Instance.addLink(links[1], true));
         rtfMap.map(({ copy, key, field }) => {
             const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index b23732b45..5f02bd73d 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -185,9 +185,11 @@ export class ComputedField extends ScriptField {
         const compiled = ScriptField.CompileScript(script, params, false);
         return compiled.compiled ? new ComputedField(compiled) : undefined;
     }
-    public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) {
+    public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }, setterscript?: string) {
         const compiled = ScriptField.CompileScript(script, params, true, capturedVariables);
-        return compiled.compiled ? new ComputedField(compiled) : undefined;
+        const compiledsetter = setterscript ? ScriptField.CompileScript(setterscript, { ...params, value: 'any' }, false, capturedVariables) : undefined;
+        const compiledsetscript = compiledsetter?.compiled ? compiledsetter : undefined;
+        return compiled.compiled ? new ComputedField(compiled, compiledsetscript) : undefined;
     }
     public static MakeInterpolatedNumber(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number, defaultVal: Opt<number>) {
         if (!doc[`${fieldKey}-indexed`]) {
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 33809824f..f461cf3fa 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -501,7 +501,7 @@ export namespace DashUploadUtils {
 
     const parseExifData = async (source: string) => {
         const image = await request.get(source, { encoding: null });
-        const { data, error } = await new Promise(resolve => {
+        const { data, error } = await new Promise<{ data: any; error: any }>(resolve => {
             new ExifImage({ image }, (error, data) => {
                 let reason: Opt<string> = undefined;
                 if (error) {
diff --git a/src/server/websocket.ts b/src/server/websocket.ts
index 9b91a35a6..68b003496 100644
--- a/src/server/websocket.ts
+++ b/src/server/websocket.ts
@@ -199,7 +199,7 @@ export namespace WebSocket {
         return Database.Instance.getDocument(id, callback);
     }
     function GetRefField([id, callback]: [string, (result?: Transferable) => void]) {
-        process.stdout.write(`.`);
+        process.stdout.write(`+`);
         GetRefFieldLocal([id, callback]);
     }
 
-- 
cgit v1.2.3-70-g09d2


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

---
 src/client/util/SelectionManager.ts                |  45 +--
 .../views/collections/CollectionCarousel3DView.tsx | 149 +++++-----
 src/client/views/collections/CollectionView.tsx    |   2 +-
 .../CollectionFreeFormLayoutEngines.tsx            |   7 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   9 +-
 .../collectionFreeForm/MarqueeOptionsMenu.tsx      |   2 -
 .../CollectionMulticolumnView.tsx                  |   3 +-
 .../CollectionMultirowView.tsx                     |   3 +-
 .../collectionSchema/CollectionSchemaView.tsx      | 318 +++++++++------------
 .../collections/collectionSchema/SchemaRowBox.tsx  |  31 +-
 .../nodes/CollectionFreeFormDocumentView.scss      |   4 +-
 src/client/views/nodes/DocumentView.tsx            |  18 +-
 12 files changed, 282 insertions(+), 309 deletions(-)

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

diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 0f4f77588..313c255a0 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,3 +1,4 @@
+import { ModalManager } from '@material-ui/core';
 import { action, observable, ObservableMap } from 'mobx';
 import { computedFn } from 'mobx-utils';
 import { Doc, Opt } from '../../fields/Doc';
@@ -10,7 +11,8 @@ import { ScriptingGlobals } from './ScriptingGlobals';
 export namespace SelectionManager {
     class Manager {
         @observable IsDragging: boolean = false;
-        SelectedViews: ObservableMap<DocumentView, Doc> = new ObservableMap();
+        SelectedViewsMap: ObservableMap<DocumentView, Doc> = new ObservableMap();
+        @observable SelectedViews: DocumentView[] = [];
         @observable SelectedSchemaDocument: Doc | undefined;
 
         @action
@@ -20,7 +22,7 @@ export namespace SelectionManager {
         @action
         SelectView(docView: DocumentView, ctrlPressed: boolean): void {
             // if doc is not in SelectedDocuments, add it
-            if (!manager.SelectedViews.get(docView) && docView.props.Document.type !== DocumentType.MARKER) {
+            if (!manager.SelectedViewsMap.get(docView) && docView.props.Document.type !== DocumentType.MARKER) {
                 if (!ctrlPressed) {
                     if (LinkManager.currentLink && !LinkManager.Links(docView.rootDoc).includes(LinkManager.currentLink) && docView.rootDoc !== LinkManager.currentLink) {
                         LinkManager.currentLink = undefined;
@@ -28,33 +30,38 @@ export namespace SelectionManager {
                     this.DeselectAll();
                 }
 
-                manager.SelectedViews.set(docView, docView.rootDoc);
+                manager.SelectedViews.push(docView);
+                manager.SelectedViewsMap.set(docView, docView.rootDoc);
                 docView.props.whenChildContentsActiveChanged(true);
-            } else if (!ctrlPressed && (Array.from(manager.SelectedViews.entries()).length > 1 || manager.SelectedSchemaDocument)) {
-                Array.from(manager.SelectedViews.keys()).map(dv => dv !== docView && dv.props.whenChildContentsActiveChanged(false));
+            } else if (!ctrlPressed && (Array.from(manager.SelectedViewsMap.entries()).length > 1 || manager.SelectedSchemaDocument)) {
+                Array.from(manager.SelectedViewsMap.keys()).map(dv => dv !== docView && dv.props.whenChildContentsActiveChanged(false));
                 manager.SelectedSchemaDocument = undefined;
-                manager.SelectedViews.clear();
-                manager.SelectedViews.set(docView, docView.rootDoc);
+                manager.SelectedViews.length = 0;
+                manager.SelectedViewsMap.clear();
+                manager.SelectedViews.push(docView);
+                manager.SelectedViewsMap.set(docView, docView.rootDoc);
             }
         }
         @action
-        DeselectView(docView: DocumentView): void {
-            if (manager.SelectedViews.get(docView)) {
-                manager.SelectedViews.delete(docView);
+        DeselectView(docView?: DocumentView): void {
+            if (docView && manager.SelectedViewsMap.get(docView)) {
+                manager.SelectedViewsMap.delete(docView);
+                manager.SelectedViews.splice(manager.SelectedViews.indexOf(docView), 1);
                 docView.props.whenChildContentsActiveChanged(false);
             }
         }
         @action
         DeselectAll(): void {
             manager.SelectedSchemaDocument = undefined;
-            Array.from(manager.SelectedViews.keys()).forEach(dv => dv.props.whenChildContentsActiveChanged(false));
-            manager.SelectedViews.clear();
+            Array.from(manager.SelectedViewsMap.keys()).forEach(dv => dv.props.whenChildContentsActiveChanged(false));
+            manager.SelectedViewsMap.clear();
+            manager.SelectedViews.length = 0;
         }
     }
 
     const manager = new Manager();
 
-    export function DeselectView(docView: DocumentView): void {
+    export function DeselectView(docView?: DocumentView): void {
         manager.DeselectView(docView);
     }
     export function SelectView(docView: DocumentView, ctrlPressed: boolean): void {
@@ -67,7 +74,7 @@ export namespace SelectionManager {
 
     const IsSelectedCache = computedFn(function isSelected(doc: DocumentView) {
         // wrapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed
-        return manager.SelectedViews.get(doc) ? true : false;
+        return manager.SelectedViewsMap.get(doc) ? true : false;
     });
     // computed functions, such as used in IsSelected generate errors if they're called outside of a
     // reaction context.  Specifying the context with 'outsideReaction' allows an efficiency feature
@@ -76,7 +83,7 @@ export namespace SelectionManager {
         return !doc
             ? false
             : outsideReaction
-            ? manager.SelectedViews.get(doc)
+            ? manager.SelectedViewsMap.get(doc)
                 ? true
                 : false // get() accesses a hashtable -- setting anything in the hashtable generates a mobx invalidation for every get()
             : IsSelectedCache(doc);
@@ -85,7 +92,7 @@ export namespace SelectionManager {
     export function DeselectAll(except?: Doc): void {
         let found: DocumentView | undefined = undefined;
         if (except) {
-            for (const view of Array.from(manager.SelectedViews.keys())) {
+            for (const view of Array.from(manager.SelectedViewsMap.keys())) {
                 if (view.props.Document === except) found = view;
             }
         }
@@ -95,13 +102,15 @@ export namespace SelectionManager {
     }
 
     export function Views(): Array<DocumentView> {
-        return Array.from(manager.SelectedViews.keys()); //.filter(dv => manager.SelectedViews.get(dv)?._viewType !== CollectionViewType.Docking);
+        return manager.SelectedViews;
+        // Array.from(manager.SelectedViewsMap.keys()); //.filter(dv => manager.SelectedViews.get(dv)?._viewType !== CollectionViewType.Docking);
     }
     export function SelectedSchemaDoc(): Doc | undefined {
         return manager.SelectedSchemaDocument;
     }
     export function Docs(): Doc[] {
-        return Array.from(manager.SelectedViews.values()).filter(doc => doc?._viewType !== CollectionViewType.Docking);
+        return manager.SelectedViews.map(dv => dv.rootDoc).filter(doc => doc?._viewType !== CollectionViewType.Docking);
+        // Array.from(manager.SelectedViewsMap.values()).filter(doc => doc?._viewType !== CollectionViewType.Docking);
     }
 }
 ScriptingGlobals.add(function SelectionManager_selectedDocType(type: string, expertMode: boolean, checkContext?: boolean) {
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index 01f41869e..57ff1b292 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -9,7 +9,7 @@ import { OmitKeys, returnFalse, Utils } from '../../../Utils';
 import { DragManager } from '../../util/DragManager';
 import { DocumentView } from '../nodes/DocumentView';
 import { StyleProp } from '../StyleProvider';
-import "./CollectionCarousel3DView.scss";
+import './CollectionCarousel3DView.scss';
 import { CollectionSubView } from './CollectionSubView';
 
 @observer
@@ -20,134 +20,143 @@ export class CollectionCarousel3DView extends CollectionSubView() {
 
     private _dropDisposer?: DragManager.DragDropDisposer;
 
-    componentWillUnmount() { this._dropDisposer?.(); }
+    componentWillUnmount() {
+        this._dropDisposer?.();
+    }
 
-    protected createDashEventsTarget = (ele: HTMLDivElement | null) => { //used for stacking and masonry view
+    protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
+        //used for stacking and masonry view
         this._dropDisposer?.();
         if (ele) {
             this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
         }
-    }
+    };
 
     panelWidth = () => this.props.PanelWidth() / 3;
     panelHeight = () => this.props.PanelHeight() * 0.6;
     onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
+    isContentActive = () => this.props.isSelected() || this.props.isContentActive() || this.props.isAnyChildContentActive();
+    isChildContentActive = () => (this.isContentActive() ? true : false);
+
     @computed get content() {
         const currentIndex = NumCast(this.layoutDoc._itemIndex);
-        const displayDoc = (childPair: { layout: Doc, data: Doc }) => {
-            return <DocumentView  {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "childLayoutTemplate", "childLayoutString"]).omit}
-                onDoubleClick={this.onChildDoubleClick}
-                renderDepth={this.props.renderDepth + 1}
-                LayoutTemplate={this.props.childLayoutTemplate}
-                LayoutTemplateString={this.props.childLayoutString}
-                Document={childPair.layout}
-                DataDoc={childPair.data}
-                PanelWidth={this.panelWidth}
-                PanelHeight={this.panelHeight}
-                bringToFront={returnFalse}
-            />;
+        const displayDoc = (childPair: { layout: Doc; data: Doc }) => {
+            return (
+                <DocumentView
+                    {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'childLayoutTemplate', 'childLayoutString']).omit}
+                    onDoubleClick={this.onChildDoubleClick}
+                    renderDepth={this.props.renderDepth + 1}
+                    LayoutTemplate={this.props.childLayoutTemplate}
+                    LayoutTemplateString={this.props.childLayoutString}
+                    Document={childPair.layout}
+                    DataDoc={childPair.data}
+                    isContentActive={this.isChildContentActive}
+                    isDocumentActive={this.props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this.props.isDocumentActive : this.isContentActive}
+                    PanelWidth={this.panelWidth}
+                    PanelHeight={this.panelHeight}
+                    bringToFront={returnFalse}
+                />
+            );
         };
 
-        return (this.childLayoutPairs.map((childPair, index) => {
+        return this.childLayoutPairs.map((childPair, index) => {
             return (
-                <div key={childPair.layout[Id]}
-                    className={`collectionCarousel3DView-item${index === currentIndex ? "-active" : ""} ${index}`}
-                    style={index === currentIndex ?
-                        { opacity: '1', transform: 'scale(1.3)', width: this.panelWidth() } :
-                        { opacity: '0.5', transform: 'scale(0.6)', userSelect: 'none', width: this.panelWidth() }}>
+                <div
+                    key={childPair.layout[Id]}
+                    className={`collectionCarousel3DView-item${index === currentIndex ? '-active' : ''} ${index}`}
+                    style={index === currentIndex ? { opacity: '1', transform: 'scale(1.3)', width: this.panelWidth() } : { opacity: '0.5', transform: 'scale(0.6)', userSelect: 'none', width: this.panelWidth() }}>
                     {displayDoc(childPair)}
-                </div>);
-        }));
+                </div>
+            );
+        });
     }
 
     changeSlide = (direction: number) => {
         this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) + direction + this.childLayoutPairs.length) % this.childLayoutPairs.length;
-    }
+    };
 
     onArrowClick = (e: React.MouseEvent, direction: number) => {
         e.stopPropagation();
         this.changeSlide(direction);
-        !this.layoutDoc.autoScrollOn && (this.layoutDoc.showScrollButton = (direction === 1) ? "fwd" : "back"); // while autoscroll is on, keep the other autoscroll button hidden
+        !this.layoutDoc.autoScrollOn && (this.layoutDoc.showScrollButton = direction === 1 ? 'fwd' : 'back'); // while autoscroll is on, keep the other autoscroll button hidden
         !this.layoutDoc.autoScrollOn && this.fadeScrollButton(); // keep pause button visible while autoscroll is on
-    }
+    };
 
     interval?: number;
     startAutoScroll = (direction: number) => {
         this.interval = window.setInterval(() => {
             this.changeSlide(direction);
         }, this.scrollSpeed);
-    }
+    };
 
     stopAutoScroll = () => {
         window.clearInterval(this.interval);
         this.interval = undefined;
         this.fadeScrollButton();
-    }
+    };
 
     toggleAutoScroll = (direction: number) => {
         this.layoutDoc.autoScrollOn = this.layoutDoc.autoScrollOn ? false : true;
         this.layoutDoc.autoScrollOn ? this.startAutoScroll(direction) : this.stopAutoScroll();
-    }
+    };
 
     fadeScrollButton = () => {
         window.setTimeout(() => {
-            !this.layoutDoc.autoScrollOn && (this.layoutDoc.showScrollButton = "none"); //fade away after 1.5s if it's not clicked.
+            !this.layoutDoc.autoScrollOn && (this.layoutDoc.showScrollButton = 'none'); //fade away after 1.5s if it's not clicked.
         }, 1500);
-    }
+    };
 
     @computed get buttons() {
         if (!this.props.isContentActive()) return null;
-        return <div className="arrow-buttons" >
-            <div key="back" className="carousel3DView-back" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }}
-                onClick={(e) => this.onArrowClick(e, -1)}
-            >
-                <FontAwesomeIcon icon={"angle-left"} size={"2x"} />
-            </div>
-            <div key="fwd" className="carousel3DView-fwd" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }}
-                onClick={(e) => this.onArrowClick(e, 1)}
-            >
-                <FontAwesomeIcon icon={"angle-right"} size={"2x"} />
+        return (
+            <div className="arrow-buttons">
+                <div key="back" className="carousel3DView-back" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={e => this.onArrowClick(e, -1)}>
+                    <FontAwesomeIcon icon={'angle-left'} size={'2x'} />
+                </div>
+                <div key="fwd" className="carousel3DView-fwd" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={e => this.onArrowClick(e, 1)}>
+                    <FontAwesomeIcon icon={'angle-right'} size={'2x'} />
+                </div>
+                {this.autoScrollButton}
             </div>
-            {this.autoScrollButton}
-        </div>;
+        );
     }
 
     @computed get autoScrollButton() {
         const whichButton = this.layoutDoc.showScrollButton;
-        return <>
-            <div className={`carousel3DView-back-scroll${whichButton === "back" ? "" : "-hidden"}`} style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }}
-                onClick={() => this.toggleAutoScroll(-1)}>
-                {this.layoutDoc.autoScrollOn ? <FontAwesomeIcon icon={"pause"} size={"1x"} /> : <FontAwesomeIcon icon={"angle-double-left"} size={"1x"} />}
-            </div>
-            <div className={`carousel3DView-fwd-scroll${whichButton === "fwd" ? "" : "-hidden"}`} style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }}
-                onClick={() => this.toggleAutoScroll(1)}>
-                {this.layoutDoc.autoScrollOn ? <FontAwesomeIcon icon={"pause"} size={"1x"} /> : <FontAwesomeIcon icon={"angle-double-right"} size={"1x"} />}
-            </div>
-        </>;
+        return (
+            <>
+                <div className={`carousel3DView-back-scroll${whichButton === 'back' ? '' : '-hidden'}`} style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={() => this.toggleAutoScroll(-1)}>
+                    {this.layoutDoc.autoScrollOn ? <FontAwesomeIcon icon={'pause'} size={'1x'} /> : <FontAwesomeIcon icon={'angle-double-left'} size={'1x'} />}
+                </div>
+                <div className={`carousel3DView-fwd-scroll${whichButton === 'fwd' ? '' : '-hidden'}`} style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={() => this.toggleAutoScroll(1)}>
+                    {this.layoutDoc.autoScrollOn ? <FontAwesomeIcon icon={'pause'} size={'1x'} /> : <FontAwesomeIcon icon={'angle-double-right'} size={'1x'} />}
+                </div>
+            </>
+        );
     }
 
     @computed get dots() {
-        return (this.childLayoutPairs.map((_child, index) =>
-            <div key={Utils.GenerateGuid()} className={`dot${index === NumCast(this.layoutDoc._itemIndex) ? "-active" : ""}`}
-                onClick={() => this.layoutDoc._itemIndex = index} />));
+        return this.childLayoutPairs.map((_child, index) => <div key={Utils.GenerateGuid()} className={`dot${index === NumCast(this.layoutDoc._itemIndex) ? '-active' : ''}`} onClick={() => (this.layoutDoc._itemIndex = index)} />);
     }
 
     render() {
         const index = NumCast(this.layoutDoc._itemIndex);
         const translateX = this.panelWidth() * (1 - index);
 
-        return <div className="collectionCarousel3DView-outer" ref={this.createDashEventsTarget}
-            style={{
-                background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
-                color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color),
-            }}  >
-            <div className="carousel-wrapper" style={{ transform: `translateX(${translateX}px)` }}>
-                {this.content}
-            </div>
-            {this.props.Document._chromeHidden ? (null) : this.buttons}
-            <div className="dot-bar">
-                {this.dots}
+        return (
+            <div
+                className="collectionCarousel3DView-outer"
+                ref={this.createDashEventsTarget}
+                style={{
+                    background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
+                    color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color),
+                }}>
+                <div className="carousel-wrapper" style={{ transform: `translateX(${translateX}px)` }}>
+                    {this.content}
+                </div>
+                {this.props.Document._chromeHidden ? null : this.buttons}
+                <div className="dot-bar">{this.dots}</div>
             </div>
-        </div>;
+        );
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index eafa50d27..51624689e 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -257,7 +257,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
         return StrCast(this.rootDoc.childLayoutString, this.props.childLayoutString);
     }
 
-    isContentActive = (outsideReaction?: boolean) => this.props.isContentActive();
+    isContentActive = (outsideReaction?: boolean) => this.props.isContentActive() || this.isAnyChildContentActive();
 
     render() {
         TraceMobx();
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index fa0695fb2..81b0c4d8a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -116,6 +116,8 @@ export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc
             zIndex: NumCast(layout.zIndex),
             pair: { layout, data },
             replica: '',
+            color: 'white',
+            backgroundColor: 'white',
         });
     });
     const divider = { type: 'div', color: 'transparent', x: -burstRadius[0], y: 0, width: 15, height: 15, payload: undefined };
@@ -408,7 +410,7 @@ function normalizeResults(
         .map(ele => {
             const newPosRaw = ele[1];
             if (newPosRaw) {
-                const newPos = {
+                const newPos: PoolData = {
                     x: newPosRaw.x * scale,
                     y: newPosRaw.y * scale,
                     z: newPosRaw.z,
@@ -417,6 +419,9 @@ function normalizeResults(
                     zIndex: newPosRaw.zIndex,
                     width: (newPosRaw.width || 0) * scale,
                     height: newPosRaw.height! * scale,
+                    backgroundColor: newPosRaw.backgroundColor,
+                    opacity: newPosRaw.opacity,
+                    color: newPosRaw.color,
                     pair: ele[1].pair,
                 };
                 poolData.set(newPos.pair.layout[Id] + (newPos.replica || ''), { transition: 'all 1s', ...newPos });
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 3a8edb1a5..4f81af95d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1291,10 +1291,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
     pointerEvents = () => {
         const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine);
-        const pointerEvents =
-            this.props.isContentActive() === false || DocumentDecorations.Instance.Interacting
-                ? 'none'
-                : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.());
+        const pointerEvents = DocumentDecorations.Instance.Interacting
+            ? 'none'
+            : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.());
         return pointerEvents;
     };
     getChildDocView(entry: PoolData) {
@@ -1995,7 +1994,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         TraceMobx();
         return (
             <div
-                className={'collectionfreeformview-container'}
+                className="collectionfreeformview-container"
                 ref={this.createDashEventsTarget}
                 onWheel={this.onPointerWheel}
                 onClick={this.onClick}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
index 9581563ce..c9168d40a 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -3,8 +3,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
 import { observer } from 'mobx-react';
 import { unimplementedFunction } from '../../../../Utils';
-import { DocumentType } from '../../../documents/DocumentTypes';
-import { SelectionManager } from '../../../util/SelectionManager';
 import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
 
 @observer
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index 88d045fa7..b73b1d779 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -2,14 +2,13 @@ import { action, computed } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, DocListCast } from '../../../../fields/Doc';
-import { List } from '../../../../fields/List';
 import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
 import { returnFalse } from '../../../../Utils';
 import { DragManager, dropActionType } from '../../../util/DragManager';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { Transform } from '../../../util/Transform';
 import { undoBatch } from '../../../util/UndoManager';
-import { DocFocusOptions, DocumentView } from '../../nodes/DocumentView';
+import { DocumentView } from '../../nodes/DocumentView';
 import { CollectionSubView } from '../CollectionSubView';
 import './CollectionMulticolumnView.scss';
 import ResizeBar from './MulticolumnResizer';
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index f18917bef..0cca83803 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -2,14 +2,13 @@ import { action, computed } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, DocListCast } from '../../../../fields/Doc';
-import { List } from '../../../../fields/List';
 import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
 import { returnFalse } from '../../../../Utils';
 import { DragManager, dropActionType } from '../../../util/DragManager';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { Transform } from '../../../util/Transform';
 import { undoBatch } from '../../../util/UndoManager';
-import { DocFocusOptions, DocumentView } from '../../nodes/DocumentView';
+import { DocumentView } from '../../nodes/DocumentView';
 import { CollectionSubView } from '../CollectionSubView';
 import './CollectionMultirowView.scss';
 import HeightLabel from './MultirowHeightLabel';
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index 151e6cbef..ad31113a2 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -1,15 +1,17 @@
 import React = require('react');
-import { action, computed, observable, ObservableMap, ObservableSet, untracked } from 'mobx';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, observable, ObservableMap, trace, untracked } from 'mobx';
 import { observer } from 'mobx-react';
-import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../../fields/Doc';
+import { Doc, DocListCast, Field, StrListCast } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
 import { List } from '../../../../fields/List';
 import { RichTextField } from '../../../../fields/RichTextField';
 import { listSpec } from '../../../../fields/Schema';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
 import { ImageField } from '../../../../fields/URLField';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTransparent, returnTrue, setupMoveUpEvents, smoothScroll, Utils } from '../../../../Utils';
+import { emptyFunction, returnEmptyDoclist, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../Utils';
 import { Docs, DocUtils } from '../../../documents/Documents';
+import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
 import { SelectionManager } from '../../../util/SelectionManager';
 import { Transform } from '../../../util/Transform';
@@ -17,18 +19,14 @@ import { undoBatch } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
 import { ContextMenuProps } from '../../ContextMenuItem';
 import { EditableView } from '../../EditableView';
-import { DocComponentView, DocFocusOptions, DocumentView } from '../../nodes/DocumentView';
+import { DocFocusOptions, DocumentView } from '../../nodes/DocumentView';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
+import { KeyValueBox } from '../../nodes/KeyValueBox';
+import { DefaultStyleProvider } from '../../StyleProvider';
 import { CollectionSubView } from '../CollectionSubView';
 import './CollectionSchemaView.scss';
 import { SchemaColumnHeader } from './SchemaColumnHeader';
 import { SchemaRowBox } from './SchemaRowBox';
-import { DefaultStyleProvider } from '../../StyleProvider';
-import { DocumentManager } from '../../../util/DocumentManager';
-import { ScriptField } from '../../../../fields/ScriptField';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { IconButton } from 'browndash-components';
-import { KeyValueBox } from '../../nodes/KeyValueBox';
 
 export enum ColumnType {
     Number,
@@ -43,7 +41,6 @@ const defaultColumnKeys: string[] = ['title', 'type', 'author', 'creationDate',
 @observer
 export class CollectionSchemaView extends CollectionSubView() {
     private _ref: HTMLDivElement | null = null;
-    private _selectedDocSortedArray: Doc[] = [];
     private _closestDropIndex: number = 0;
     private _previewRef: HTMLDivElement | null = null;
     private _makeNewColumn: boolean = false;
@@ -53,13 +50,11 @@ export class CollectionSchemaView extends CollectionSubView() {
     public static _rowMenuWidth: number = 100;
     public static _previewDividerWidth: number = 4;
 
-    @observable _lastSelectedRow: number | undefined;
     @computed get _selectedDocs() {
-        return new Set(SelectionManager.Docs().filter(doc => Doc.AreProtosEqual(DocCast(doc.context), this.props.Document)));
+        return SelectionManager.Docs().filter(doc => Doc.AreProtosEqual(DocCast(doc.context), this.props.Document));
     }
     @observable _rowEles: ObservableMap = new ObservableMap<Doc, HTMLDivElement>();
     @observable _colEles: HTMLDivElement[] = [];
-    @observable _isDragging: boolean = false;
     @observable _displayColumnWidths: number[] | undefined;
     @observable _columnMenuIndex: number | undefined;
     @observable _menuOptions: string[] = [];
@@ -133,7 +128,7 @@ export class CollectionSchemaView extends CollectionSubView() {
     }
 
     componentDidMount() {
-        this.props.setContentView?.(this as DocComponentView);
+        this.props.setContentView?.(this);
         document.addEventListener('keydown', this.onKeyDown);
     }
 
@@ -143,24 +138,36 @@ export class CollectionSchemaView extends CollectionSubView() {
 
     @action
     onKeyDown = (e: KeyboardEvent) => {
-        if (this._selectedDocs.size > 0) {
-            if (e.key == 'ArrowDown') {
-                const lastDoc = Array.from(this._selectedDocs.values()).lastElement();
-                const lastIndex = this.rowIndex(lastDoc);
-                if (lastIndex >= 0 && lastIndex < this.childDocs.length - 1) {
-                    !e.shiftKey && this.clearSelection();
-                    const newDoc = this.childDocs[lastIndex + 1];
-                    this.addDocToSelection(newDoc, e.shiftKey, lastIndex + 1);
-                }
-            }
-            if (e.key == 'ArrowUp') {
-                const firstDoc = Array.from(this._selectedDocs.values())[0];
-                const firstIndex = this.rowIndex(firstDoc);
-                if (firstIndex > 0 && firstIndex < this.childDocs.length) {
-                    !e.shiftKey && this.clearSelection();
-                    const newDoc = this.childDocs[firstIndex - 1];
-                    this.addDocToSelection(newDoc, e.shiftKey, firstIndex - 1);
-                }
+        if (this._selectedDocs.length > 0) {
+            switch (e.key) {
+                case 'ArrowDown':
+                    {
+                        const lastDoc = this._selectedDocs.lastElement();
+                        const lastIndex = this.rowIndex(lastDoc);
+                        const curDoc = this.childDocs[lastIndex];
+                        if (lastIndex >= 0 && lastIndex < this.childDocs.length - 1) {
+                            !e.shiftKey && this.clearSelection();
+                            const newDoc = this.childDocs[lastIndex + 1];
+                            if (this._selectedDocs.includes(newDoc)) SelectionManager.DeselectView(DocumentManager.Instance.getFirstDocumentView(curDoc));
+                            else this.addDocToSelection(newDoc, e.shiftKey, lastIndex + 1);
+                        }
+                        e.stopPropagation();
+                    }
+                    break;
+                case 'ArrowUp':
+                    {
+                        const firstDoc = this._selectedDocs.lastElement();
+                        const firstIndex = this.rowIndex(firstDoc);
+                        const curDoc = this.childDocs[firstIndex];
+                        if (firstIndex > 0 && firstIndex < this.childDocs.length) {
+                            !e.shiftKey && this.clearSelection();
+                            const newDoc = this.childDocs[firstIndex - 1];
+                            if (this._selectedDocs.includes(newDoc)) SelectionManager.DeselectView(DocumentManager.Instance.getFirstDocumentView(curDoc));
+                            else this.addDocToSelection(newDoc, e.shiftKey, firstIndex - 1);
+                        }
+                        e.stopPropagation();
+                    }
+                    break;
             }
         }
     };
@@ -326,60 +333,25 @@ export class CollectionSchemaView extends CollectionSubView() {
     addDocToSelection = (doc: Doc, extendSelection: boolean, index: number) => {
         const rowDocView = DocumentManager.Instance.getDocumentView(doc);
         if (rowDocView) SelectionManager.SelectView(rowDocView, extendSelection);
-        this._lastSelectedRow = index;
-    };
-
-    @action
-    removeDocFromSelection = (doc: Doc) => {
-        const rowDocView = DocumentManager.Instance.getDocumentView(doc);
-        if (rowDocView) SelectionManager.DeselectView(rowDocView);
-        if (this._selectedDocs.size === 0) {
-            this._lastSelectedRow = undefined;
-        }
     };
 
     @action
-    clearSelection = () => {
-        SelectionManager.DeselectAll();
-        this._lastSelectedRow = undefined;
-    };
-
-    rowOnClickScript = ScriptField.MakeFunction('scriptContext.selectRow(self, shiftKey, ctrlKey || metaKey)', { scriptContext: 'any', shiftKey: 'boolean', ctrlKey: 'boolean', metaKey: 'boolean' })!;
-
-    @action
-    selectRow = (doc: Doc, shift: boolean, ctrl: boolean) => {
-        const index = this.childDocs.indexOf(doc);
-        if (index < 0) return;
-        if (shift && this._lastSelectedRow !== undefined) {
-            const startRow = Math.min(this._lastSelectedRow, index);
-            const endRow = Math.max(this._lastSelectedRow, index);
-            for (let i = startRow; i <= endRow; i++) {
-                const currDoc = this.childDocs[i];
-                if (!this._selectedDocs.has(currDoc)) this.addDocToSelection(currDoc, true, i);
-            }
-            this._lastSelectedRow = index;
-        } else if (ctrl) {
-            if (!this._selectedDocs.has(doc)) {
-                this.addDocToSelection(doc, true, index);
-            } else {
-                this.removeDocFromSelection(doc);
-            }
-        } else {
-            if (!this._selectedDocs.has(doc)) {
-                this.clearSelection();
-                this.addDocToSelection(doc, false, index);
-            }
+    clearSelection = () => SelectionManager.DeselectAll();
+
+    selectRows = (rootDoc: Doc, lastSelected: Doc) => {
+        const index = this.childDocs.indexOf(rootDoc);
+        const lastSelectedRow = this.childDocs.indexOf(lastSelected);
+        const startRow = Math.min(lastSelectedRow, index);
+        const endRow = Math.max(lastSelectedRow, index);
+        for (let i = startRow; i <= endRow; i++) {
+            const currDoc = this.childDocs[i];
+            if (!this._selectedDocs.includes(currDoc)) this.addDocToSelection(currDoc, true, i);
         }
     };
 
-    @action
-    sortedSelectedDocs = (): Doc[] => {
-        return this.childDocs.filter(doc => this._selectedDocs.has(doc));
-    };
+    sortedSelectedDocs = () => this.childDocs.filter(doc => this._selectedDocs.includes(doc));
 
-    setDropIndex = (index: number) => {
-        this._closestDropIndex = index;
-    };
+    setDropIndex = (index: number) => (this._closestDropIndex = index);
 
     @action
     onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
@@ -394,17 +366,20 @@ export class CollectionSchemaView extends CollectionSubView() {
                 return total + curr;
             }, CollectionSchemaView._rowMenuWidth);
             this.swapColumns(de.complete.columnDragData.colIndex, i);
+            e.stopPropagation();
             return true;
         }
-        if (super.onInternalDrop(e, de)) {
-            this._isDragging = false;
-            const pushedDocs: Doc[] = this.childDocs.filter((doc: Doc, index: number) => index >= this._closestDropIndex && !this._selectedDocs.has(doc));
+        const draggedDocs = de.complete.docDragData?.draggedDocuments;
+        if (draggedDocs && super.onInternalDrop(e, de)) {
+            const pushedDocs = this.childDocs.filter((doc, index) => index >= this._closestDropIndex && !draggedDocs.includes(doc));
             this.props.removeDocument?.(pushedDocs);
-            this.props.removeDocument?.(this._selectedDocSortedArray);
-            this.addDocument(this._selectedDocSortedArray);
+            this.props.removeDocument?.(draggedDocs);
+            this.addDocument(draggedDocs);
             this.addDocument(pushedDocs);
             this.setSort(undefined);
-            this.clearSelection();
+            SelectionManager.DeselectAll();
+            setTimeout(() => draggedDocs.forEach(doc => DocumentManager.Instance.AddViewRenderedCb(doc, dv => dv.select(true))), 100);
+            e.stopPropagation();
             return true;
         }
         return false;
@@ -412,46 +387,11 @@ export class CollectionSchemaView extends CollectionSubView() {
 
     @action
     onExternalDrop = async (e: React.DragEvent): Promise<void> => {
-        super.onExternalDrop(
-            e,
-            {},
-            undoBatch(
-                action(docus => {
-                    this._isDragging = false;
-                    docus.map((doc: Doc) => {
-                        this.addDocument(doc);
-                    });
-                })
-            )
-        );
+        super.onExternalDrop(e, {}, undoBatch(action(docus => docus.map((doc: Doc) => this.addDocument(doc)))));
         this.setSort(undefined);
     };
 
-    @action
-    startDrag = (e: PointerEvent, doc: Doc, index: number) => {
-        if (!this._selectedDocs.has(doc)) {
-            this.clearSelection();
-            this.addDocToSelection(doc, false, index);
-        }
-        this._isDragging = true;
-        this._selectedDocSortedArray = this.sortedSelectedDocs();
-        const dragData = new DragManager.DocumentDragData(this._selectedDocSortedArray, 'move');
-        dragData.moveDocument = this.props.moveDocument;
-        const dragItem: HTMLElement[] = Array.from(this._selectedDocs.values()).map((doc: Doc) => this._rowEles.get(doc));
-
-        DragManager.StartDocumentDrag(
-            dragItem.map(ele => ele),
-            dragData,
-            e.clientX,
-            e.clientY,
-            undefined
-        );
-        return true;
-    };
-
-    onDividerDown = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, emptyFunction);
-    };
+    onDividerDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, emptyFunction);
 
     @action
     onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => {
@@ -557,9 +497,6 @@ export class CollectionSchemaView extends CollectionSubView() {
         return undefined;
     };
 
-    isChildContentActive = () =>
-        this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive)) ? true : this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false ? false : undefined;
-
     @computed get fieldDefaultInput() {
         switch (this._newFieldType) {
             case ColumnType.Number:
@@ -644,23 +581,17 @@ export class CollectionSchemaView extends CollectionSubView() {
         ContextMenu.Instance.clearItems();
         ContextMenu.Instance.addItem({
             description: 'Change field',
-            event: () => {
-                this.openColumnMenu(index, false);
-            },
+            event: () => this.openColumnMenu(index, false),
             icon: 'pencil-alt',
         });
         ContextMenu.Instance.addItem({
             description: 'Filter field',
-            event: () => {
-                this.openFilterMenu(index);
-            },
+            event: () => this.openFilterMenu(index),
             icon: 'filter',
         });
         ContextMenu.Instance.addItem({
             description: 'Delete column',
-            event: () => {
-                this.removeColumn(index);
-            },
+            event: () => this.removeColumn(index),
             icon: 'trash',
         });
         ContextMenu.Instance.displayMenu(x, y, undefined, false);
@@ -672,9 +603,7 @@ export class CollectionSchemaView extends CollectionSubView() {
         this._menuOptions = this.documentKeys.filter(value => value.toLowerCase().includes(this._menuValue.toLowerCase()));
     };
 
-    getFieldFilters = (field: string) => {
-        return StrListCast(this.Document._docFilters).filter(filter => filter.split(':')[0] == field);
-    };
+    getFieldFilters = (field: string) => StrListCast(this.Document._docFilters).filter(filter => filter.split(':')[0] == field);
 
     removeFieldFilters = (field: string) => {
         this.getFieldFilters(field).forEach(filter => {
@@ -694,9 +623,7 @@ export class CollectionSchemaView extends CollectionSubView() {
     };
 
     @action
-    updateFilterSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
-        this._filterValue = e.target.value;
-    };
+    updateFilterSearch = (e: React.ChangeEvent<HTMLInputElement>) => (this._filterValue = e.target.value);
 
     @computed get newFieldMenu() {
         return (
@@ -705,7 +632,6 @@ export class CollectionSchemaView extends CollectionSubView() {
                     <input
                         type="radio"
                         name="newFieldType"
-                        id=""
                         checked={this._newFieldType == ColumnType.Number}
                         onChange={action(() => {
                             this._newFieldType = ColumnType.Number;
@@ -718,7 +644,6 @@ export class CollectionSchemaView extends CollectionSubView() {
                     <input
                         type="radio"
                         name="newFieldType"
-                        id=""
                         checked={this._newFieldType == ColumnType.Boolean}
                         onChange={action(() => {
                             this._newFieldType = ColumnType.Boolean;
@@ -731,7 +656,6 @@ export class CollectionSchemaView extends CollectionSubView() {
                     <input
                         type="radio"
                         name="newFieldType"
-                        id=""
                         checked={this._newFieldType == ColumnType.String}
                         onChange={action(() => {
                             this._newFieldType = ColumnType.String;
@@ -880,13 +804,11 @@ export class CollectionSchemaView extends CollectionSubView() {
         );
     }
 
-    tableWidthFunc = () => this.tableWidth;
-    rowHeightFunc = () => CollectionSchemaView._rowHeight;
-    rowClickScriptFunc = () => this.rowOnClickScript;
     isContentActive = () => this.props.isSelected() || this.props.isContentActive();
     screenToLocal = () => this.props.ScreenToLocalTransform().translate(-this.tableWidth, 0);
     previewWidthFunc = () => this.previewWidth;
     render() {
+        trace();
         return (
             <div
                 className="collectionSchemaView"
@@ -894,6 +816,12 @@ export class CollectionSchemaView extends CollectionSubView() {
                     this._ref = ele;
                     this.createDashEventsTarget(ele);
                 }}
+                onPointerDown={e => {
+                    // this is analogous to the panning code for a freeform view.
+                    // however, schema views don't pan so it does nothing.  but it does eat the pointerDown event
+                    // if the content is active to prevent the schema from being dragged
+                    this.isContentActive() && setupMoveUpEvents(this, e, returnFalse, emptyFunction, emptyFunction, false);
+                }}
                 onDrop={this.onExternalDrop.bind(this)}>
                 <div className="schema-table">
                     <div className="schema-header-row" style={{ height: CollectionSchemaView._rowHeight }}>
@@ -927,53 +855,16 @@ export class CollectionSchemaView extends CollectionSubView() {
                     </div>
                     {this._columnMenuIndex !== undefined && this.renderColumnMenu}
                     {this._filterColumnIndex !== undefined && this.renderFilterMenu}
-                    <div className="schema-table-content">
-                        {this.childDocs.map((doc: Doc, index: number) => {
-                            const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS ? undefined : this.props.DataDoc;
-                            let dref: Opt<DocumentView>;
-                            return (
-                                <div className="schema-row-wrapper" style={{ maxHeight: CollectionSchemaView._rowHeight }}>
-                                    <DocumentView
-                                        {...this.props}
-                                        ref={r => (dref = r || undefined)}
-                                        LayoutTemplate={this.props.childLayoutTemplate}
-                                        LayoutTemplateString={SchemaRowBox.LayoutString(this.props.fieldKey)}
-                                        Document={doc}
-                                        DataDoc={dataDoc}
-                                        ContainingCollectionView={this.props.CollectionView}
-                                        ContainingCollectionDoc={this.Document}
-                                        PanelWidth={this.tableWidthFunc}
-                                        PanelHeight={this.rowHeightFunc}
-                                        styleProvider={DefaultStyleProvider}
-                                        focus={this.focusDocument}
-                                        docFilters={this.childDocFilters}
-                                        docRangeFilters={this.childDocRangeFilters}
-                                        searchFilterDocs={this.searchFilterDocs}
-                                        rootSelected={this.rootSelected}
-                                        ScreenToLocalTransform={Transform.Identity}
-                                        bringToFront={emptyFunction}
-                                        isDocumentActive={this.isContentActive}
-                                        isContentActive={emptyFunction}
-                                        hideDecorations={true}
-                                        hideTitle={true}
-                                        hideDocumentButtonBar={true}
-                                        hideLinkAnchors={true}
-                                        fitWidth={returnTrue}
-                                        onClick={this.rowClickScriptFunc}
-                                        scriptContext={this}
-                                    />
-                                </div>
-                            );
-                        })}
-                    </div>
+                    <CollectionSchemaViewDocs schema={this} />
+
                     <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} />
                 </div>
                 {this.previewWidth > 0 && <div className="schema-preview-divider" style={{ width: CollectionSchemaView._previewDividerWidth }} onPointerDown={this.onDividerDown}></div>}
                 {this.previewWidth > 0 && (
                     <div style={{ width: `${this.previewWidth}px` }} ref={ref => (this._previewRef = ref)}>
-                        {this._lastSelectedRow !== undefined && (
+                        {Array.from(this._selectedDocs).lastElement() && (
                             <DocumentView
-                                Document={this.childDocs[this._lastSelectedRow]}
+                                Document={Array.from(this._selectedDocs).lastElement()}
                                 DataDoc={undefined}
                                 fitContentsToBox={returnTrue}
                                 dontCenter={'y'}
@@ -1007,3 +898,54 @@ export class CollectionSchemaView extends CollectionSubView() {
         );
     }
 }
+
+interface CollectionSchemaViewDocsProps {
+    schema: CollectionSchemaView;
+}
+
+@observer
+class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsProps> {
+    tableWidthFunc = () => this.props.schema.tableWidth;
+    rowHeightFunc = () => CollectionSchemaView._rowHeight;
+    render() {
+        return (
+            <div className="schema-table-content">
+                {this.props.schema.childDocs.map((doc: Doc, index: number) => {
+                    const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS ? undefined : this.props.schema.props.DataDoc;
+                    return (
+                        <div className="schema-row-wrapper" style={{ maxHeight: CollectionSchemaView._rowHeight }}>
+                            <DocumentView
+                                {...this.props.schema.props}
+                                LayoutTemplate={this.props.schema.props.childLayoutTemplate}
+                                LayoutTemplateString={SchemaRowBox.LayoutString(this.props.schema.props.fieldKey)}
+                                Document={doc}
+                                DataDoc={dataDoc}
+                                ContainingCollectionView={this.props.schema.props.CollectionView}
+                                ContainingCollectionDoc={this.props.schema.Document}
+                                PanelWidth={this.tableWidthFunc}
+                                PanelHeight={this.rowHeightFunc}
+                                styleProvider={DefaultStyleProvider}
+                                focus={this.props.schema.focusDocument}
+                                docFilters={this.props.schema.childDocFilters}
+                                docRangeFilters={this.props.schema.childDocRangeFilters}
+                                searchFilterDocs={this.props.schema.searchFilterDocs}
+                                rootSelected={this.props.schema.rootSelected}
+                                ScreenToLocalTransform={Transform.Identity}
+                                bringToFront={emptyFunction}
+                                isDocumentActive={this.props.schema.props.childDocumentsActive?.() ? this.props.schema.props.isDocumentActive : this.props.schema.isContentActive}
+                                isContentActive={emptyFunction}
+                                whenChildContentsActiveChanged={active => this.props.schema.props.whenChildContentsActiveChanged(active)}
+                                hideDecorations={true}
+                                hideTitle={true}
+                                hideDocumentButtonBar={true}
+                                hideLinkAnchors={true}
+                                fitWidth={returnTrue}
+                                scriptContext={this}
+                            />
+                        </div>
+                    );
+                })}
+            </div>
+        );
+    }
+}
diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
index 87284be70..0c8c0ee59 100644
--- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
@@ -1,9 +1,9 @@
 import React = require('react');
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed } from 'mobx';
+import { computed } from 'mobx';
 import { observer } from 'mobx-react';
-import { emptyFunction, setupMoveUpEvents } from '../../../../Utils';
 import { DragManager } from '../../../util/DragManager';
+import { SnappingManager } from '../../../util/SnappingManager';
 import { undoBatch } from '../../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../../DocComponent';
 import { Colors } from '../../global/globalEnums';
@@ -36,24 +36,30 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return this.schemaView?.rowIndex(this.rootDoc) ?? -1;
     }
 
-    @action
-    onRowPointerDown = (e: React.PointerEvent) => {
-        if (!this.isContentActive()) return;
-        setupMoveUpEvents(this, e, e => this.schemaView?.startDrag(e, this.rootDoc, this.rowIndex) ?? true, emptyFunction, emptyFunction);
+    componentDidMount(): void {
+        this.props.setContentView?.(this);
+    }
+
+    select = (ctrlKey: boolean, shiftKey: boolean) => {
+        if (!this.schemaView) return;
+        const lastSelected = Array.from(this.schemaView._selectedDocs).lastElement();
+        if (shiftKey && lastSelected) this.schemaView.selectRows(this.rootDoc, lastSelected);
+        else {
+            this.props.select?.(ctrlKey);
+        }
     };
 
     onPointerEnter = (e: any) => {
-        if (!this.schemaView?._isDragging) return;
+        //if (!this.schemaView?._isDragging) return;
+        if (!SnappingManager.GetIsDragging()) return;
         document.removeEventListener('pointermove', this.onPointerMove);
         document.addEventListener('pointermove', this.onPointerMove);
     };
 
     onPointerMove = (e: any) => {
-        if (!this.schemaView?._isDragging) return;
-        let dragIsRow: boolean = true;
-        DragManager.docsBeingDragged.forEach(doc => {
-            dragIsRow = this.schemaView?._selectedDocs.has(doc) ?? false;
-        });
+        if (!SnappingManager.GetIsDragging()) return;
+        const dragIsRow = DragManager.docsBeingDragged.some(doc => doc.context === this.schemaDoc); //  this.schemaView?._selectedDocs.has(doc) ?? false;
+
         if (this._ref && dragIsRow) {
             const rect = this._ref.getBoundingClientRect();
             const y = e.clientY - rect.top; //y position within the element.
@@ -88,7 +94,6 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         ? { height: CollectionSchemaView._rowHeight, backgroundColor: Colors.LIGHT_BLUE, pointerEvents: this.schemaView?.props.isContentActive() ? 'all' : undefined /*, opacity: this.props.dragging ? 0.5 : 1 */ }
                         : { height: CollectionSchemaView._rowHeight, pointerEvents: this.schemaView?.props.isContentActive() ? 'all' : undefined }
                 }
-                onPointerDown={this.onRowPointerDown}
                 onPointerEnter={this.onPointerEnter}
                 onPointerLeave={this.onPointerLeave}
                 ref={(row: HTMLDivElement | null) => {
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
index 724394025..f99011b8f 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.scss
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
@@ -5,5 +5,5 @@
     touch-action: manipulation;
     top: 0;
     left: 0;
-    pointer-events: none;
-}
\ No newline at end of file
+    //pointer-events: none;
+}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c13934945..1f717932e 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -121,6 +121,7 @@ export interface DocComponentView {
     addDocTab?: (doc: Doc, where: OpenWhere) => boolean; // determines how to add a document - used in following links to open the target ina local lightbox
     reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height.  However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling.
     shrinkWrap?: () => void; // requests a document to display all of its contents with no white space.  currently only implemented (needed?) for freeform views
+    select?: (ctrlKey: boolean, shiftKey: boolean) => void;
     menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected.
     isAnyChildContentActive?: () => boolean; // is any child content of the document active
     getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
@@ -236,7 +237,7 @@ export interface DocumentViewInternalProps extends DocumentViewProps {
     NativeHeight: () => number;
     isSelected: (outsideReaction?: boolean) => boolean;
     isHovering: () => boolean;
-    select: (ctrlPressed: boolean) => void;
+    select: (ctrlPressed: boolean, shiftPress?: boolean) => void;
     DocumentView: () => DocumentView;
     viewPath: () => DocumentView[];
 }
@@ -538,7 +539,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
 
     startDragging(x: number, y: number, dropAction: dropActionType, hideSource = false) {
         if (this._mainCont.current) {
-            const dragData = new DragManager.DocumentDragData([this.props.Document]);
+            const views = SelectionManager.Views().filter(dv => dv.docView?._mainCont.current);
+            const selected = views.some(dv => dv.rootDoc === this.Document) ? views : [this.props.DocumentView()];
+            const dragData = new DragManager.DocumentDragData(selected.map(dv => dv.rootDoc));
             const [left, top] = this.props.ScreenToLocalTransform().scale(this.NativeDimScaling).inverse().transformPoint(0, 0);
             dragData.offset = this.props
                 .ScreenToLocalTransform()
@@ -551,8 +554,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             dragData.canEmbed = this.props.canEmbedOnDrag;
             const ffview = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
             ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView()));
-            DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart && !this.props.dontHideOnDrag) }, () =>
-                setTimeout(action(() => ffview && (ffview.ChildDrag = undefined)))
+            DragManager.StartDocumentDrag(
+                selected.map(dv => dv.docView!._mainCont.current!),
+                dragData,
+                x,
+                y,
+                { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart && !this.props.dontHideOnDrag) },
+                () => setTimeout(action(() => ffview && (ffview.ChildDrag = undefined)))
             ); // this needs to happen after the drop event is processed.
             ffview?.setupDragLines(false);
         }
@@ -659,7 +667,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                         }),
                         350
                     );
-                    this.props.select(e.ctrlKey || e.shiftKey);
+                    (this._componentView?.select ?? this.props.select)(e.ctrlKey || e.metaKey, e.shiftKey);
                 }
                 preventDefault = false;
             }
-- 
cgit v1.2.3-70-g09d2

