From 071aa191d37f2805bd717c1d1caf8b2a57c394bc Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 8 Mar 2023 12:33:16 -0500
Subject: added progressive disclosure for collections.

---
 .../collections/collectionFreeForm/CollectionFreeFormView.tsx     | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 63b566f28..7322f98b5 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -111,7 +111,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @observable _panZoomTransition: number = 0; // sets the pan/zoom transform ease time- used by nudge(), focus() etc to smoothly zoom/pan.  set to 0 to use document's transition time or default of 0
     @observable _hLines: number[] | undefined;
     @observable _vLines: number[] | undefined;
-    @observable _firstRender = true; // this turns off rendering of the collection's content so that there's instant feedback when a tab is switched of what content will be shown.
+    @observable _firstRender = false; // this turns off rendering of the collection's content so that there's instant feedback when a tab is switched of what content will be shown.  could be used for performance improvement
     @observable _showAnimTimeline = false;
     @observable _clusterSets: Doc[][] = [];
     @observable _deleteList: DocumentView[] = [];
@@ -139,7 +139,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         };
     }
     @computed get fitContentsToBox() {
-        return (this.props.fitContentsToBox?.() || this.Document._fitContentsToBox || this.Document.isGroup) && !this.isAnnotationOverlay;
+        return (this.props.fitContentsToBox?.() || this.Document._fitContentsToBox) && !this.isAnnotationOverlay;
     }
     @computed get contentBounds() {
         const cb = Cast(this.rootDoc.contentBounds, listSpec('number'));
@@ -1553,7 +1553,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     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.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);
+        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !this.Document._isGroup, viewType: true, filters: true } }, this.rootDoc);
 
         if (addAsAnnotation) {
             if (Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), null) !== undefined) {
@@ -1574,7 +1574,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 this._firstRender = false;
                 this._disposers.groupBounds = reaction(
                     () => {
-                        if (this.props.Document._isGroup && this.childDocs.length === this.childDocList?.length) {
+                        if (this.Document._isGroup && this.childDocs.length === this.childDocList?.length) {
                             const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: cd[WidthSym](), height: cd[HeightSym]() }));
                             return aggregateBounds(clist, NumCast(this.layoutDoc._xPadding), NumCast(this.layoutDoc._yPadding));
                         }
-- 
cgit v1.2.3-70-g09d2


From 98fd2ecbbf5168986a5eba889bff6aeea0eebb49 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 8 Mar 2023 21:54:03 -0500
Subject: some cleanup of presBox properties panels.  fixes for undoing some
 slider actions.

---
 src/client/views/MainView.tsx                      |   2 -
 src/client/views/PropertiesView.tsx                |  31 +-
 src/client/views/collections/TabDocView.tsx        |   8 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   3 +-
 src/client/views/nodes/trails/PresBox.tsx          | 722 ++++++++++-----------
 5 files changed, 369 insertions(+), 397 deletions(-)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index e1ba5943d..945cd61db 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -553,8 +553,6 @@ export class MainView extends React.Component {
             Doc.MyTrails && (Doc.ActivePresentation = pres);
             Doc.AddDocToList(Doc.MyTrails, 'data', pres);
             this.closeFlyout();
-        } else {
-            PresBox.NavigateToDoc(DocCast(pres.presentationTargetDoc), pres);
         }
     };
 
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 4a63930bd..7a985628f 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -95,6 +95,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
 
     //Pres Trails booleans:
     @observable openPresTransitions: boolean = true;
+    @observable openPresProgressivize: boolean = false;
+    @observable openPresVisibilityAndDuration: boolean = false;
     @observable openAddSlide: boolean = false;
     @observable openSlideOptions: boolean = false;
 
@@ -1740,7 +1742,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
             }
             if (this.isPres) {
                 const selectedItem: boolean = PresBox.Instance?.selectedArray.size > 0;
-                const type = PresBox.Instance.activeItem?.type;
+                const type = [DocumentType.AUDIO, DocumentType.VID].includes(DocCast(PresBox.Instance.activeItem?.annotationOn)?.type as any as DocumentType)
+                    ? (DocCast(PresBox.Instance.activeItem?.annotationOn)?.type as any as DocumentType)
+                    : PresBox.targetRenderedDoc(PresBox.Instance.activeItem)?.type;
                 return (
                     <div className="propertiesView" style={{ width: this.props.width }}>
                         <div className="propertiesView-title" style={{ width: this.props.width }}>
@@ -1764,6 +1768,31 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                 {this.openPresTransitions ? <div className="propertiesView-presTrails-content">{PresBox.Instance.transitionDropdown}</div> : null}
                             </div>
                         )}
+                        {!selectedItem ? null : (
+                            <div className="propertiesView-presTrails">
+                                <div
+                                    className="propertiesView-presTrails-title"
+                                    onPointerDown={action(() => (this.openPresVisibilityAndDuration = !this.openPresVisibilityAndDuration))}
+                                    style={{ backgroundColor: this.openPresTransitions ? 'black' : '' }}>
+                                    &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> &nbsp; Visibilty
+                                    <div className="propertiesView-presTrails-title-icon">
+                                        <FontAwesomeIcon icon={this.openPresVisibilityAndDuration ? 'caret-down' : 'caret-right'} size="lg" color="white" />
+                                    </div>
+                                </div>
+                                {this.openPresVisibilityAndDuration ? <div className="propertiesView-presTrails-content">{PresBox.Instance.visibiltyDurationDropdown}</div> : null}
+                            </div>
+                        )}
+                        {!selectedItem ? null : (
+                            <div className="propertiesView-presTrails">
+                                <div className="propertiesView-presTrails-title" onPointerDown={action(() => (this.openPresProgressivize = !this.openPresProgressivize))} style={{ backgroundColor: this.openPresTransitions ? 'black' : '' }}>
+                                    &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> &nbsp; Progressivize
+                                    <div className="propertiesView-presTrails-title-icon">
+                                        <FontAwesomeIcon icon={this.openPresProgressivize ? 'caret-down' : 'caret-right'} size="lg" color="white" />
+                                    </div>
+                                </div>
+                                {this.openPresProgressivize ? <div className="propertiesView-presTrails-content">{PresBox.Instance.progressivizeDropdown}</div> : null}
+                            </div>
+                        )}
                         {!selectedItem || (type !== DocumentType.VID && type !== DocumentType.AUDIO) ? null : (
                             <div className="propertiesView-presTrails">
                                 <div className="propertiesView-presTrails-title" onPointerDown={action(() => (this.openSlideOptions = !this.openSlideOptions))} style={{ backgroundColor: this.openSlideOptions ? 'black' : '' }}>
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 99283996e..9459aaf1e 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -263,9 +263,6 @@ export class TabDocView extends React.Component<TabDocViewProps> {
             pinDoc.treeViewFieldKey = 'data'; // tree view will treat the 'data' field as the field where the hierarchical children are located instead of using the document's layout string field
             pinDoc.treeViewExpandedView = 'data'; // in case the data doc has an expandedView set, this will mask that field and use the 'data' field when expanding the tree view
             pinDoc.treeViewHideHeaderIfTemplate = true; // this will force the document to render itself as the tree view header
-            const presArray: Doc[] = PresBox.Instance?.sortArray();
-            const size: number = PresBox.Instance?.selectedArray.size;
-            const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined;
             const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], null);
 
             if (!pinProps?.audioRange && duration !== undefined) {
@@ -274,10 +271,6 @@ 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, anchorDoc && anchorDoc !== doc && !anchorDoc.unrendered ? anchorDoc : doc);
-            pinDoc.onClick = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)');
-            Doc.AddDocToList(curPres, 'data', pinDoc, presSelected);
-            //save position
             if (pinProps?.activeFrame !== undefined) {
                 pinDoc.presActiveFrame = pinProps?.activeFrame;
                 pinDoc.title = doc.title + ' (move)';
@@ -294,6 +287,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
                 pinDoc.presMovement = PresMovement.None;
             }
             if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true;
+            Doc.AddDocToList(curPres, 'data', pinDoc, PresBox.Instance?.sortArray()?.lastElement());
             PresBox.Instance?.clearSelectedArray();
             pinDoc && PresBox.Instance?.addToSelectedArray(pinDoc); //Update selected array
         });
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 7322f98b5..b92a8ec4d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -181,8 +181,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return this.getContainerTransform().translate(-this.cachedCenteringShiftX, -this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform);
     }
 
-    _keyTimer: NodeJS.Timeout | undefined;
-
     public static gotoKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], duration: number) {
         if (timer) clearTimeout(timer);
         return DocumentView.SetViewTransition(docs, 'all', duration, undefined, true);
@@ -208,6 +206,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return newTimer;
     }
 
+    _keyTimer: NodeJS.Timeout | undefined; // timer for turning off transition flag when key frame change has completed.  Need to clear this if you do a second navigation before first finishes, or else first timer can go off during second naviation.
     changeKeyFrame = (back = false) => {
         const currentFrame = Cast(this.Document._currentFrame, 'number', null);
         if (currentFrame === undefined) {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 8738e47af..b5a24a380 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -3,13 +3,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 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 { Copy, Id } from '../../../../fields/FieldSymbols';
 import { InkField, InkTool } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
 import { ObjectField } from '../../../../fields/ObjectField';
 import { listSpec } from '../../../../fields/Schema';
+import { ScriptField } from '../../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
 import { AudioField } from '../../../../fields/URLField';
 import { emptyFunction, emptyPath, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils';
@@ -69,9 +69,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(PresBox, fieldKey);
     }
+    static navigateToDocScript: ScriptField;
+
+    constructor(props: any) {
+        super(props);
+        if (!PresBox.navigateToDocScript) {
+            PresBox.navigateToDocScript = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)')!;
+        }
+    }
 
     private _disposers: { [name: string]: IReactionDisposer } = {};
     public selectedArray = new ObservableSet<Doc>();
+    _batch: UndoManager.Batch | undefined = undefined; // undo batch for dragging sliders which generate multiple scene edit events as the cursor moves
+    _keyTimer: NodeJS.Timeout | undefined; // timer for turning off transition flag when key frame change has completed.  Need to clear this if you do a second navigation before first finishes, or else first timer can go off during second naviation.
+    _unmounting = false; // flag that view is unmounting used to block RemFromMap from deleting things
 
     @observable public static Instance: PresBox;
 
@@ -143,14 +154,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     addToSelectedArray = action((doc: Doc) => this.selectedArray.add(doc));
     removeFromSelectedArray = action((doc: Doc) => this.selectedArray.delete(doc));
 
-    _unmounting = false;
     @action
     componentWillUnmount() {
         this._unmounting = true;
         if (this._presTimer) clearTimeout(this._presTimer);
         document.removeEventListener('keydown', PresBox.keyEventsWrapper, true);
         this.resetPresentation();
-        // Turn of progressivize editors
         this.turnOffEdit(true);
         Object.values(this._disposers).forEach(disposer => disposer?.());
     }
@@ -196,8 +205,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 if (editing) {
                     this.childDocs.forEach(doc => {
                         if (doc.presIndexed !== undefined) {
-                            this.presIndexedItems(doc)?.forEach(indexedDoc => (indexedDoc.opacity = undefined));
-                            doc.presIndexed = Math.min(this.presIndexedItems(doc)?.length ?? 0, 1);
+                            this.progressivizedItems(doc)?.forEach(indexedDoc => (indexedDoc.opacity = undefined));
+                            doc.presIndexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, 1);
                         }
                     });
                 }
@@ -222,7 +231,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     };
 
     stopTempMedia = (targetDocField: FieldResult) => {
-        const targetDoc = Cast(targetDocField, Doc, null);
+        const targetDoc = DocCast(DocCast(targetDocField).annotationOn) ?? DocCast(targetDocField);
         if ([DocumentType.VID, DocumentType.AUDIO].includes(targetDoc.type as any)) {
             const targMedia = DocumentManager.Instance.getDocumentView(targetDoc);
             targMedia?.ComponentView?.Pause?.();
@@ -235,7 +244,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     nextSlide = (slideNum?: number) => {
         const nextSlideInd = slideNum ?? this.itemIndex + 1;
         let curSlideInd = nextSlideInd;
-        CollectionStackedTimeline.CurrentlyPlaying?.map(clipView => clipView?.ComponentView?.Pause?.());
+        //CollectionStackedTimeline.CurrentlyPlaying?.map(clipView => clipView?.ComponentView?.Pause?.());
         this.clearSelectedArray();
         const doGroupWithUp =
             (nextSelected: number, force = false) =>
@@ -261,7 +270,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         doGroupWithUp(curSlideInd, true)();
     };
 
-    presIndexedItems = (doc: Doc) => {
+    // docs within a slide target that will be progressively revealed
+    progressivizedItems = (doc: Doc) => {
         const targetList = PresBox.targetRenderedDoc(doc);
         if (doc.presIndexed !== undefined && targetList) {
             const listItems = (Cast(targetList[Doc.LayoutFieldKey(targetList)], listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[]) ?? DocListCast(targetList[Doc.LayoutFieldKey(targetList) + '-annotations']);
@@ -278,7 +288,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 targetRenderedDoc._dataTransition = 'all 1s';
                 targetRenderedDoc.opacity = 1;
                 setTimeout(() => (targetRenderedDoc._dataTransition = 'inherit'), 1000);
-                const listItems = this.presIndexedItems(this.activeItem);
+                const listItems = this.progressivizedItems(this.activeItem);
                 if (listItems && presIndexed < listItems.length) {
                     if (!first) {
                         const listItemDoc = listItems[presIndexed];
@@ -365,7 +375,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); //Update selected array
             this.turnOffEdit();
             this.navigateToActiveItem(finished); //Handles movement to element only when presTrail is list
-            this.onHideDocument(); //Handles hide after/before
+            this.doHideBeforeAfter(); //Handles hide after/before
         }
     });
     static pinDataTypes(target?: Doc): pinDataTypes {
@@ -740,7 +750,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
      * they are hidden each time the presentation is updated.
      */
     @action
-    onHideDocument = () => {
+    doHideBeforeAfter = () => {
         this.childDocs.forEach((doc, index) => {
             const curDoc = Cast(doc, Doc, null);
             const tagDoc = PresBox.targetRenderedDoc(curDoc);
@@ -872,10 +882,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             if (doc.presHideBefore && index > startIndex) tagDoc.opacity = 0;
             if (doc.presHideAfter && index < startIndex) tagDoc.opacity = 0;
             if (doc.presIndexed !== undefined && index >= startIndex) {
-                this.presIndexedItems(doc)
+                this.progressivizedItems(doc)
                     ?.slice(doc.type === DocumentType.COL ? 1 : 0)
                     .forEach(indexedDoc => (indexedDoc.opacity = 0));
-                doc.presIndexed = Math.min(this.presIndexedItems(doc)?.length ?? 0, doc.type === DocumentType.COL ? 1 : 0);
+                doc.presIndexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, doc.type === DocumentType.COL ? 1 : 0);
             }
             // if (doc.presHide && this.childDocs.indexOf(doc) === startIndex)  tagDoc.opacity = 0;
         });
@@ -1338,12 +1348,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         if (timeInMS > 100000) timeInMS = 100000;
         setter(timeInMS);
     };
-    setTransitionTime = (number: String, change?: number) => {
+
+    @undoBatch
+    updateTransitionTime = (number: String, change?: number) => {
         PresBox.SetTransitionTime(number, (timeInMS: number) => this.selectedArray.forEach(doc => (doc.presTransition = timeInMS)), change);
     };
 
     // Converts seconds to ms and updates presTransition
-    setZoom = (number: String, change?: number) => {
+    @undoBatch
+    updateZoom = (number: String, change?: number) => {
         let scale = Number(number) / 100;
         if (change) scale += change;
         if (scale < 0.01) scale = 0.01;
@@ -1351,8 +1364,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this.selectedArray.forEach(doc => (doc.presZoom = scale));
     };
 
-    // Converts seconds to ms and updates presDuration
-    setDurationTime = (number: String, change?: number) => {
+    /*
+     * Converts seconds to ms and updates presDuration
+     */
+    @undoBatch
+    updateDurationTime = (number: String, change?: number) => {
         let timeInMS = Number(number) * 1000;
         if (change) timeInMS += change;
         if (timeInMS < 100) timeInMS = 100;
@@ -1360,9 +1376,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this.selectedArray.forEach(doc => (doc.presDuration = timeInMS));
     };
 
-    /**
-     * When the movement dropdown is changes
-     */
     @undoBatch
     updateMovement = action((movement: PresMovement, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presMovement = movement)));
 
@@ -1393,6 +1406,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         activeItem.presOpenInLightbox = !activeItem.presOpenInLightbox;
         this.selectedArray.forEach(doc => (doc.presOpenInLightbox = activeItem.presOpenInLightbox));
     };
+
     @undoBatch
     @action
     updateEaseFunc = (activeItem: Doc) => {
@@ -1408,10 +1422,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @action
     updateEffect = (effect: PresEffect, bullet: boolean, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (bullet ? (doc.presBulletEffect = effect) : (doc.presEffect = effect)));
 
-    _batch: UndoManager.Batch | undefined = undefined;
-
+    static _sliderBatch: any;
     public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void, hmargin?: number) => {
-        let batch: any;
         return (
             <input
                 type="range"
@@ -1422,10 +1434,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)` }}
                 className={`toolbar-slider ${active ? '' : 'none'}`}
                 onPointerDown={e => {
-                    batch = UndoManager.StartBatch('pres slider');
+                    PresBox._sliderBatch = UndoManager.StartBatch('pres slider');
                     e.stopPropagation();
                 }}
-                onPointerUp={() => batch?.end()}
+                onPointerUp={() => PresBox._sliderBatch.end()}
                 onChange={e => {
                     e.stopPropagation();
                     change(e.target.value);
@@ -1433,11 +1445,146 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             />
         );
     };
+
+    @undoBatch
+    @action
+    applyTo = (array: Doc[]) => {
+        this.updateMovement(this.activeItem.presMovement as PresMovement, true);
+        this.updateEffect(this.activeItem.presEffect as PresEffect, false, true);
+        this.updateEffect(this.activeItem.presBulletEffect as PresEffect, true, true);
+        this.updateEffectDirection(this.activeItem.presEffectDirection as PresEffectDirection, true);
+        const { presTransition, presDuration, presHideBefore, presHideAfter } = this.activeItem;
+        array.forEach(curDoc => {
+            curDoc.presTransition = presTransition;
+            curDoc.presDuration = presDuration;
+            curDoc.presHideBefore = presHideBefore;
+            curDoc.presHideAfter = presHideAfter;
+        });
+    };
+
+    @computed get visibiltyDurationDropdown() {
+        const activeItem = this.activeItem;
+        if (activeItem && this.targetDoc) {
+            const targetType = this.targetDoc.type;
+            let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 0;
+            if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration);
+            return (
+                <div className="presBox-ribbon">
+                    <div className="ribbon-doubleButton">
+                        <Tooltip title={<div className="dash-tooltip">{'Hide before presented'}</div>}>
+                            <div className={`ribbon-toggle ${activeItem.presHideBefore ? 'active' : ''}`} onClick={() => this.updateHideBefore(activeItem)}>
+                                Hide before
+                            </div>
+                        </Tooltip>
+                        <Tooltip title={<div className="dash-tooltip">{'Hide while presented'}</div>}>
+                            <div className={`ribbon-toggle ${activeItem.presHide ? 'active' : ''}`} onClick={() => this.updateHide(activeItem)}>
+                                Hide
+                            </div>
+                        </Tooltip>
+
+                        <Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}>
+                            <div className={`ribbon-toggle ${activeItem.presHideAfter ? 'active' : ''}`} onClick={() => this.updateHideAfter(activeItem)}>
+                                Hide after
+                            </div>
+                        </Tooltip>
+
+                        <Tooltip title={<div className="dash-tooltip">{'Open in lightbox view'}</div>}>
+                            <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presOpenInLightbox ? Colors.LIGHT_BLUE : '' }} onClick={() => this.updateOpenDoc(activeItem)}>
+                                Lightbox
+                            </div>
+                        </Tooltip>
+                        <Tooltip title={<div className="dash-tooltip">{'Transition movement style'}</div>}>
+                            <div className="ribbon-toggle" onClick={() => this.updateEaseFunc(activeItem)}>
+                                {`${StrCast(activeItem.presEaseFunc, 'ease')}`}
+                            </div>
+                        </Tooltip>
+                    </div>
+                    {[DocumentType.AUDIO, DocumentType.VID].includes(targetType as any as DocumentType) ? null : (
+                        <>
+                            <div className="ribbon-doubleButton">
+                                <div className="presBox-subheading">Slide Duration</div>
+                                <div className="ribbon-property">
+                                    <input className="presBox-input" type="number" value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s
+                                </div>
+                                <div className="ribbon-propertyUpDown">
+                                    <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), 1000)}>
+                                        <FontAwesomeIcon icon={'caret-up'} />
+                                    </div>
+                                    <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), -1000)}>
+                                        <FontAwesomeIcon icon={'caret-down'} />
+                                    </div>
+                                </div>
+                            </div>
+                            {PresBox.inputter('0.1', '0.1', '20', duration, targetType !== DocumentType.AUDIO, this.updateDurationTime)}
+                            <div className={'slider-headers'} style={{ display: targetType === DocumentType.AUDIO ? 'none' : 'grid' }}>
+                                <div className="slider-text">Short</div>
+                                <div className="slider-text">Medium</div>
+                                <div className="slider-text">Long</div>
+                            </div>
+                        </>
+                    )}
+                </div>
+            );
+        }
+    }
+    @computed get progressivizeDropdown() {
+        const activeItem = this.activeItem;
+        if (activeItem && this.targetDoc && activeItem.presIndexed !== undefined) {
+            const effect = activeItem.presBulletEffect ? activeItem.presBulletEffect : PresMovement.None;
+            const bulletEffect = (effect: PresEffect) => (
+                <div className={`presBox-dropdownOption ${activeItem.presEffect === effect || (effect === PresEffect.None && !activeItem.presEffect) ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateEffect(effect, true)}>
+                    {effect}
+                </div>
+            );
+            return (
+                <div className="presBox-ribbon">
+                    <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
+                        <div className="presBox-subheading">Progressivize Collection</div>
+                        <input
+                            className="presBox-checkbox"
+                            style={{ margin: 10 }}
+                            type="checkbox"
+                            onChange={() => {
+                                activeItem.presIndexed = activeItem.presIndexed === undefined ? 0 : undefined;
+                                activeItem.presHideBefore = activeItem.presIndexed !== undefined;
+                            }}
+                            checked={Cast(activeItem.presIndexed, 'number', null) !== undefined ? true : false}
+                        />
+                    </div>
+                    {activeItem.presIndexed === undefined ? null : (
+                        <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
+                            <div className="presBox-subheading">Expand Current Bullet</div>
+                            <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.presBulletExpand = !activeItem.presBulletExpand)} checked={BoolCast(activeItem.presBulletExpand)} />
+                        </div>
+                    )}
+                    <div className="presBox-subheading">Progressive effect</div>
+                    <div
+                        className="presBox-dropdown"
+                        onClick={action(e => {
+                            e.stopPropagation();
+                            this._openBulletEffectDropdown = !this._openBulletEffectDropdown;
+                        })}
+                        style={{ borderBottomLeftRadius: this._openBulletEffectDropdown ? 0 : 5, border: this._openBulletEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}>
+                        {effect?.toString()}
+                        <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openBulletEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
+                        <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} style={{ display: this._openBulletEffectDropdown ? 'grid' : 'none' }} onPointerDown={e => e.stopPropagation()}>
+                            {bulletEffect(PresEffect.None)}
+                            {bulletEffect(PresEffect.Fade)}
+                            {bulletEffect(PresEffect.Flip)}
+                            {bulletEffect(PresEffect.Rotate)}
+                            {bulletEffect(PresEffect.Bounce)}
+                            {bulletEffect(PresEffect.Roll)}
+                        </div>
+                    </div>
+                </div>
+            );
+        }
+        return null;
+    }
     @computed get transitionDropdown() {
-        const activeItem: Doc = this.activeItem;
-        const targetDoc: Doc = this.targetDoc;
-        const presEffect = (effect: PresEffect, bullet: boolean) => (
-            <div className={`presBox-dropdownOption ${activeItem.presEffect === effect || (effect === PresEffect.None && !activeItem.presEffect) ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateEffect(effect, bullet)}>
+        const activeItem = this.activeItem;
+        const presEffect = (effect: PresEffect) => (
+            <div className={`presBox-dropdownOption ${activeItem.presEffect === effect || (effect === PresEffect.None && !activeItem.presEffect) ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateEffect(effect, false)}>
                 {effect}
             </div>
         );
@@ -1458,15 +1605,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 </Tooltip>
             );
         };
-        if (activeItem && targetDoc) {
-            const type = targetDoc.type;
+        if (activeItem && this.targetDoc) {
             const transitionSpeed = activeItem.presTransition ? NumCast(activeItem.presTransition) / 1000 : 0.5;
             const zoom = NumCast(activeItem.presZoom, 1) * 100;
-            let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 0;
-            if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration);
             const effect = activeItem.presEffect ? activeItem.presEffect : PresMovement.None;
-            const bulletEffect = activeItem.presBulletEffect ? activeItem.presBulletEffect : PresMovement.None;
-            // activeItem.presMovement = activeItem.presMovement ? activeItem.presMovement : PresMovement.Zoom;
             return (
                 <div
                     className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presStatus === PresStatus.Edit ? 'active' : ''}`}
@@ -1500,95 +1642,39 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Zoom ? 'inline-flex' : 'none' }}>
                             <div className="presBox-subheading">Zoom (% screen filled)</div>
                             <div className="ribbon-property">
-                                <input className="presBox-input" type="number" value={zoom} onChange={action(e => this.setZoom(e.target.value))} />%
+                                <input className="presBox-input" type="number" value={zoom} onChange={e => this.updateZoom(e.target.value)} />%
                             </div>
                             <div className="ribbon-propertyUpDown">
-                                <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}>
+                                <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), 0.1)}>
                                     <FontAwesomeIcon icon={'caret-up'} />
                                 </div>
-                                <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), -0.1))}>
+                                <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), -0.1)}>
                                     <FontAwesomeIcon icon={'caret-down'} />
                                 </div>
                             </div>
                         </div>
-                        {PresBox.inputter('0', '1', '100', zoom, activeItem.presMovement === PresMovement.Zoom, this.setZoom)}
+                        {PresBox.inputter('0', '1', '100', zoom, activeItem.presMovement === PresMovement.Zoom, this.updateZoom)}
                         <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
-                            <div className="presBox-subheading">Transition Speed</div>
+                            <div className="presBox-subheading">Transition Time</div>
                             <div className="ribbon-property">
-                                <input className="presBox-input" type="number" value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.setTransitionTime(e.target.value))} /> s
+                                <input className="presBox-input" type="number" value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s
                             </div>
                             <div className="ribbon-propertyUpDown">
-                                <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setTransitionTime(String(transitionSpeed), 1000))}>
+                                <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), 1000)}>
                                     <FontAwesomeIcon icon={'caret-up'} />
                                 </div>
-                                <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setTransitionTime(String(transitionSpeed), -1000))}>
+                                <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), -1000)}>
                                     <FontAwesomeIcon icon={'caret-down'} />
                                 </div>
                             </div>
                         </div>
-                        {PresBox.inputter('0.1', '0.1', '100', transitionSpeed, true, this.setTransitionTime)}
+                        {PresBox.inputter('0.1', '0.1', '100', transitionSpeed, true, this.updateTransitionTime)}
                         <div className={'slider-headers'}>
                             <div className="slider-text">Fast</div>
                             <div className="slider-text">Medium</div>
                             <div className="slider-text">Slow</div>
                         </div>
                     </div>
-                    <div className="ribbon-box">
-                        Visibility {'&'} Duration
-                        <div className="ribbon-doubleButton">
-                            <Tooltip title={<div className="dash-tooltip">{'Hide before presented'}</div>}>
-                                <div className={`ribbon-toggle ${activeItem.presHideBefore ? 'active' : ''}`} onClick={() => this.updateHideBefore(activeItem)}>
-                                    Hide before
-                                </div>
-                            </Tooltip>
-                            <Tooltip title={<div className="dash-tooltip">{'Hide while presented'}</div>}>
-                                <div className={`ribbon-toggle ${activeItem.presHide ? 'active' : ''}`} onClick={() => this.updateHide(activeItem)}>
-                                    Hide
-                                </div>
-                            </Tooltip>
-
-                            <Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}>
-                                <div className={`ribbon-toggle ${activeItem.presHideAfter ? 'active' : ''}`} onClick={() => this.updateHideAfter(activeItem)}>
-                                    Hide after
-                                </div>
-                            </Tooltip>
-
-                            <Tooltip title={<div className="dash-tooltip">{'Open in lightbox view'}</div>}>
-                                <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presOpenInLightbox ? Colors.LIGHT_BLUE : '' }} onClick={() => this.updateOpenDoc(activeItem)}>
-                                    Lightbox
-                                </div>
-                            </Tooltip>
-                            <Tooltip title={<div className="dash-tooltip">{'Transition movement style'}</div>}>
-                                <div className="ribbon-toggle" onClick={() => this.updateEaseFunc(activeItem)}>
-                                    {`${StrCast(activeItem.presEaseFunc, 'ease')}`}
-                                </div>
-                            </Tooltip>
-                        </div>
-                        {type === DocumentType.AUDIO || type === DocumentType.VID ? null : (
-                            <>
-                                <div className="ribbon-doubleButton">
-                                    <div className="presBox-subheading">Slide Duration</div>
-                                    <div className="ribbon-property">
-                                        <input className="presBox-input" type="number" value={duration} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.setDurationTime(e.target.value))} /> s
-                                    </div>
-                                    <div className="ribbon-propertyUpDown">
-                                        <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setDurationTime(String(duration), 1000))}>
-                                            <FontAwesomeIcon icon={'caret-up'} />
-                                        </div>
-                                        <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setDurationTime(String(duration), -1000))}>
-                                            <FontAwesomeIcon icon={'caret-down'} />
-                                        </div>
-                                    </div>
-                                </div>
-                                {PresBox.inputter('0.1', '0.1', '20', duration, targetDoc.type !== DocumentType.AUDIO, this.setDurationTime)}
-                                <div className={'slider-headers'} style={{ display: targetDoc.type === DocumentType.AUDIO ? 'none' : 'grid' }}>
-                                    <div className="slider-text">Short</div>
-                                    <div className="slider-text">Medium</div>
-                                    <div className="slider-text">Long</div>
-                                </div>
-                            </>
-                        )}
-                    </div>
                     <div className="ribbon-box">
                         Effects
                         <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
@@ -1609,12 +1695,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             {effect?.toString()}
                             <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
                             <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} style={{ display: this._openEffectDropdown ? 'grid' : 'none' }} onPointerDown={e => e.stopPropagation()}>
-                                {presEffect(PresEffect.None, false)}
-                                {presEffect(PresEffect.Fade, false)}
-                                {presEffect(PresEffect.Flip, false)}
-                                {presEffect(PresEffect.Rotate, false)}
-                                {presEffect(PresEffect.Bounce, false)}
-                                {presEffect(PresEffect.Roll, false)}
+                                {presEffect(PresEffect.None)}
+                                {presEffect(PresEffect.Fade)}
+                                {presEffect(PresEffect.Flip)}
+                                {presEffect(PresEffect.Rotate)}
+                                {presEffect(PresEffect.Bounce)}
+                                {presEffect(PresEffect.Roll)}
                             </div>
                         </div>
                         <div className="ribbon-doubleButton" style={{ display: effect === PresEffectDirection.None ? 'none' : 'inline-flex' }}>
@@ -1628,49 +1714,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             {presDirection(PresEffectDirection.Bottom, 'angle-up', 2, 3, {})}
                             {presDirection(PresEffectDirection.Center, '', 2, 2, { width: 10, height: 10, alignSelf: 'center' })}
                         </div>
-                        <div className="ribbon-Box">Progressive Disclosure</div>
-                        <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
-                            <div className="presBox-subheading">Progressivize Collection</div>
-                            <input
-                                className="presBox-checkbox"
-                                style={{ margin: 10 }}
-                                type="checkbox"
-                                onChange={() => {
-                                    activeItem.presIndexed = activeItem.presIndexed === undefined ? 0 : undefined;
-                                    activeItem.presHideBefore = activeItem.presIndexed !== undefined;
-                                }}
-                                checked={Cast(activeItem.presIndexed, 'number', null) !== undefined ? true : false}
-                            />
-                        </div>
-                        {activeItem.presIndexed === undefined ? null : (
-                            <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
-                                <div className="presBox-subheading">Expand Current Bullet</div>
-                                <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.presBulletExpand = !activeItem.presBulletExpand)} checked={BoolCast(activeItem.presBulletExpand)} />
-                            </div>
-                        )}
-                        {activeItem.presIndexed === undefined ? null : (
-                            <div className="presBox-subheading">
-                                Progressive effect
-                                <div
-                                    className="presBox-dropdown"
-                                    onClick={action(e => {
-                                        e.stopPropagation();
-                                        this._openBulletEffectDropdown = !this._openBulletEffectDropdown;
-                                    })}
-                                    style={{ borderBottomLeftRadius: this._openBulletEffectDropdown ? 0 : 5, border: this._openBulletEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}>
-                                    {bulletEffect?.toString()}
-                                    <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openBulletEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
-                                    <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} style={{ display: this._openBulletEffectDropdown ? 'grid' : 'none' }} onPointerDown={e => e.stopPropagation()}>
-                                        {presEffect(PresEffect.None, true)}
-                                        {presEffect(PresEffect.Fade, true)}
-                                        {presEffect(PresEffect.Flip, true)}
-                                        {presEffect(PresEffect.Rotate, true)}
-                                        {presEffect(PresEffect.Bounce, true)}
-                                        {presEffect(PresEffect.Roll, true)}
-                                    </div>
-                                </div>
-                            </div>
-                        )}
                     </div>
                     <div className="ribbon-final-box">
                         <div className="ribbon-final-button-hidden" onClick={() => this.applyTo(this.childDocs)}>
@@ -1681,174 +1724,154 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             );
         }
     }
-
-    @undoBatch
-    @action
-    applyTo = (array: Doc[]) => {
-        this.updateMovement(this.activeItem.presMovement as PresMovement, true);
-        this.updateEffect(this.activeItem.presEffect as PresEffect, false, true);
-        this.updateEffect(this.activeItem.presBulletEffect as PresEffect, true, true);
-        this.updateEffectDirection(this.activeItem.presEffectDirection as PresEffectDirection, true);
-        const { presTransition, presDuration, presHideBefore, presHideAfter } = this.activeItem;
-        array.forEach(curDoc => {
-            curDoc.presTransition = presTransition;
-            curDoc.presDuration = presDuration;
-            curDoc.presHideBefore = presHideBefore;
-            curDoc.presHideAfter = presHideAfter;
-        });
-    };
-
     @computed get mediaOptionsDropdown() {
-        const activeItem: Doc = this.activeItem;
-        const targetDoc: Doc = this.targetDoc;
-        const clipStart: number = NumCast(activeItem.clipStart);
-        const clipEnd: number = NumCast(activeItem.clipEnd, NumCast(activeItem[Doc.LayoutFieldKey(activeItem) + '-duration']));
-        const mediaStopDocInd: number = NumCast(activeItem.mediaStopDoc);
-        if (activeItem && targetDoc) {
+        const activeItem = this.activeItem;
+        if (activeItem && this.targetDoc) {
+            const clipStart = NumCast(activeItem.clipStart);
+            const clipEnd = NumCast(activeItem.clipEnd, NumCast(activeItem[Doc.LayoutFieldKey(activeItem) + '-duration']));
             return (
-                <div>
-                    <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
-                        <div>
-                            <div className="ribbon-box">
-                                Start {'&'} End Time
-                                <div className={'slider-headers'}>
-                                    <div className="slider-block">
-                                        <div className="slider-text" style={{ fontWeight: 500 }}>
-                                            Start time (s)
-                                        </div>
-                                        <div id={'startTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}>
-                                            <input
-                                                className="presBox-input"
-                                                style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
-                                                type="number"
-                                                value={NumCast(activeItem.presStartTime).toFixed(2)}
-                                                onKeyDown={e => e.stopPropagation()}
-                                                onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
-                                                    activeItem.presStartTime = Number(e.target.value);
-                                                })}
-                                            />
-                                        </div>
-                                    </div>
-                                    <div className="slider-block">
-                                        <div className="slider-text" style={{ fontWeight: 500 }}>
-                                            Duration (s)
-                                        </div>
-                                        <div className="slider-number" style={{ backgroundColor: Colors.LIGHT_BLUE }}>
-                                            {Math.round((NumCast(activeItem.presEndTime) - NumCast(activeItem.presStartTime)) * 10) / 10}
-                                        </div>
+                <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
+                    <div>
+                        <div className="ribbon-box">
+                            Start {'&'} End Time
+                            <div className={'slider-headers'}>
+                                <div className="slider-block">
+                                    <div className="slider-text" style={{ fontWeight: 500 }}>
+                                        Start time (s)
                                     </div>
-                                    <div className="slider-block">
-                                        <div className="slider-text" style={{ fontWeight: 500 }}>
-                                            End time (s)
-                                        </div>
-                                        <div id={'endTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}>
-                                            <input
-                                                className="presBox-input"
-                                                onKeyDown={e => e.stopPropagation()}
-                                                style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
-                                                type="number"
-                                                value={NumCast(activeItem.presEndTime).toFixed(2)}
-                                                onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
-                                                    activeItem.presEndTime = Number(e.target.value);
-                                                })}
-                                            />
-                                        </div>
+                                    <div id={'startTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}>
+                                        <input
+                                            className="presBox-input"
+                                            style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
+                                            type="number"
+                                            value={NumCast(activeItem.presStartTime).toFixed(2)}
+                                            onKeyDown={e => e.stopPropagation()}
+                                            onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
+                                                activeItem.presStartTime = Number(e.target.value);
+                                            })}
+                                        />
                                     </div>
                                 </div>
-                                <div className="multiThumb-slider">
-                                    <input
-                                        type="range"
-                                        step="0.1"
-                                        min={clipStart}
-                                        max={clipEnd}
-                                        value={NumCast(activeItem.presEndTime)}
-                                        style={{ gridColumn: 1, gridRow: 1 }}
-                                        className={`toolbar-slider ${'end'}`}
-                                        id="toolbar-slider"
-                                        onPointerDown={e => {
-                                            this._batch = UndoManager.StartBatch('presEndTime');
-                                            const endBlock = document.getElementById('endTime');
-                                            if (endBlock) {
-                                                endBlock.style.color = Colors.LIGHT_GRAY;
-                                                endBlock.style.backgroundColor = Colors.MEDIUM_BLUE;
-                                            }
-                                            e.stopPropagation();
-                                        }}
-                                        onPointerUp={() => {
-                                            this._batch?.end();
-                                            const endBlock = document.getElementById('endTime');
-                                            if (endBlock) {
-                                                endBlock.style.color = Colors.BLACK;
-                                                endBlock.style.backgroundColor = Colors.LIGHT_GRAY;
-                                            }
-                                        }}
-                                        onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
-                                            e.stopPropagation();
-                                            activeItem.presEndTime = Number(e.target.value);
-                                        }}
-                                    />
-                                    <input
-                                        type="range"
-                                        step="0.1"
-                                        min={clipStart}
-                                        max={clipEnd}
-                                        value={NumCast(activeItem.presStartTime)}
-                                        style={{ gridColumn: 1, gridRow: 1 }}
-                                        className={`toolbar-slider ${'start'}`}
-                                        id="toolbar-slider"
-                                        onPointerDown={e => {
-                                            this._batch = UndoManager.StartBatch('presStartTime');
-                                            const startBlock = document.getElementById('startTime');
-                                            if (startBlock) {
-                                                startBlock.style.color = Colors.LIGHT_GRAY;
-                                                startBlock.style.backgroundColor = Colors.MEDIUM_BLUE;
-                                            }
-                                            e.stopPropagation();
-                                        }}
-                                        onPointerUp={() => {
-                                            this._batch?.end();
-                                            const startBlock = document.getElementById('startTime');
-                                            if (startBlock) {
-                                                startBlock.style.color = Colors.BLACK;
-                                                startBlock.style.backgroundColor = Colors.LIGHT_GRAY;
-                                            }
-                                        }}
-                                        onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
-                                            e.stopPropagation();
-                                            activeItem.presStartTime = Number(e.target.value);
-                                        }}
-                                    />
-                                </div>
-                                <div className="slider-headers">
-                                    <div className="slider-text">{clipStart.toFixed(2)} s</div>
-                                    <div className="slider-text"></div>
-                                    <div className="slider-text">{clipEnd.toFixed(2)} s</div>
-                                </div>
-                            </div>
-                            <div className="ribbon-final-box">
-                                Playback
-                                <div className="presBox-subheading">Start playing:</div>
-                                <div className="presBox-radioButtons">
-                                    <div className="checkbox-container">
-                                        <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStart = 'manual')} checked={activeItem.mediaStart === 'manual'} />
-                                        <div>On click</div>
+                                <div className="slider-block">
+                                    <div className="slider-text" style={{ fontWeight: 500 }}>
+                                        Duration (s)
                                     </div>
-                                    <div className="checkbox-container">
-                                        <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStart = 'auto')} checked={activeItem.mediaStart === 'auto'} />
-                                        <div>Automatically</div>
+                                    <div className="slider-number" style={{ backgroundColor: Colors.LIGHT_BLUE }}>
+                                        {Math.round((NumCast(activeItem.presEndTime) - NumCast(activeItem.presStartTime)) * 10) / 10}
                                     </div>
                                 </div>
-                                <div className="presBox-subheading">Stop playing:</div>
-                                <div className="presBox-radioButtons">
-                                    <div className="checkbox-container">
-                                        <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'manual')} checked={activeItem.mediaStop === 'manual'} />
-                                        <div>At audio end time</div>
+                                <div className="slider-block">
+                                    <div className="slider-text" style={{ fontWeight: 500 }}>
+                                        End time (s)
                                     </div>
-                                    <div className="checkbox-container">
-                                        <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'auto')} checked={activeItem.mediaStop === 'auto'} />
-                                        <div>On slide change</div>
+                                    <div id={'endTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}>
+                                        <input
+                                            className="presBox-input"
+                                            onKeyDown={e => e.stopPropagation()}
+                                            style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
+                                            type="number"
+                                            value={NumCast(activeItem.presEndTime).toFixed(2)}
+                                            onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
+                                                activeItem.presEndTime = Number(e.target.value);
+                                            })}
+                                        />
                                     </div>
-                                    {/* <div className="checkbox-container">
+                                </div>
+                            </div>
+                            <div className="multiThumb-slider">
+                                <input
+                                    type="range"
+                                    step="0.1"
+                                    min={clipStart}
+                                    max={clipEnd}
+                                    value={NumCast(activeItem.presEndTime)}
+                                    style={{ gridColumn: 1, gridRow: 1 }}
+                                    className={`toolbar-slider ${'end'}`}
+                                    id="toolbar-slider"
+                                    onPointerDown={e => {
+                                        this._batch = UndoManager.StartBatch('presEndTime');
+                                        const endBlock = document.getElementById('endTime');
+                                        if (endBlock) {
+                                            endBlock.style.color = Colors.LIGHT_GRAY;
+                                            endBlock.style.backgroundColor = Colors.MEDIUM_BLUE;
+                                        }
+                                        e.stopPropagation();
+                                    }}
+                                    onPointerUp={() => {
+                                        this._batch?.end();
+                                        const endBlock = document.getElementById('endTime');
+                                        if (endBlock) {
+                                            endBlock.style.color = Colors.BLACK;
+                                            endBlock.style.backgroundColor = Colors.LIGHT_GRAY;
+                                        }
+                                    }}
+                                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+                                        e.stopPropagation();
+                                        activeItem.presEndTime = Number(e.target.value);
+                                    }}
+                                />
+                                <input
+                                    type="range"
+                                    step="0.1"
+                                    min={clipStart}
+                                    max={clipEnd}
+                                    value={NumCast(activeItem.presStartTime)}
+                                    style={{ gridColumn: 1, gridRow: 1 }}
+                                    className={`toolbar-slider ${'start'}`}
+                                    id="toolbar-slider"
+                                    onPointerDown={e => {
+                                        this._batch = UndoManager.StartBatch('presStartTime');
+                                        const startBlock = document.getElementById('startTime');
+                                        if (startBlock) {
+                                            startBlock.style.color = Colors.LIGHT_GRAY;
+                                            startBlock.style.backgroundColor = Colors.MEDIUM_BLUE;
+                                        }
+                                        e.stopPropagation();
+                                    }}
+                                    onPointerUp={() => {
+                                        this._batch?.end();
+                                        const startBlock = document.getElementById('startTime');
+                                        if (startBlock) {
+                                            startBlock.style.color = Colors.BLACK;
+                                            startBlock.style.backgroundColor = Colors.LIGHT_GRAY;
+                                        }
+                                    }}
+                                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+                                        e.stopPropagation();
+                                        activeItem.presStartTime = Number(e.target.value);
+                                    }}
+                                />
+                            </div>
+                            <div className="slider-headers">
+                                <div className="slider-text">{clipStart.toFixed(2)} s</div>
+                                <div className="slider-text"></div>
+                                <div className="slider-text">{clipEnd.toFixed(2)} s</div>
+                            </div>
+                        </div>
+                        <div className="ribbon-final-box">
+                            Playback
+                            <div className="presBox-subheading">Start playing:</div>
+                            <div className="presBox-radioButtons">
+                                <div className="checkbox-container">
+                                    <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStart = 'manual')} checked={activeItem.mediaStart === 'manual'} />
+                                    <div>On click</div>
+                                </div>
+                                <div className="checkbox-container">
+                                    <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStart = 'auto')} checked={activeItem.mediaStart === 'auto'} />
+                                    <div>Automatically</div>
+                                </div>
+                            </div>
+                            <div className="presBox-subheading">Stop playing:</div>
+                            <div className="presBox-radioButtons">
+                                <div className="checkbox-container">
+                                    <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'manual')} checked={activeItem.mediaStop === 'manual'} />
+                                    <div>At audio end time</div>
+                                </div>
+                                <div className="checkbox-container">
+                                    <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'auto')} checked={activeItem.mediaStop === 'auto'} />
+                                    <div>On slide change</div>
+                                </div>
+                                {/* <div className="checkbox-container">
                                     <input className="presBox-checkbox"
                                         type="checkbox"
                                         onChange={() => activeItem.mediaStop = "afterSlide"}
@@ -1865,7 +1888,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                         </select>
                                     </div>
                                 </div> */}
-                                </div>
                             </div>
                         </div>
                     </div>
@@ -1873,7 +1895,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             );
         }
     }
-
     @computed get newDocumentToolbarDropdown() {
         return (
             <div
@@ -2079,65 +2100,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         );
     }
 
-    _keyTimer: NodeJS.Timeout | undefined;
-
-    /**
-     * Returns the collection type as a string for headers
-     */
-    @computed get stringType() {
-        if (this.activeItem) {
-            // prettier-ignore
-            switch (this.targetDoc.type) {
-                case DocumentType.PDF: return 'PDF';
-                case DocumentType.RTF: return 'Text node';
-                case DocumentType.COL: return 'Collection';
-                case DocumentType.AUDIO: return 'Audio';
-                case DocumentType.VID: return 'Video';
-                case DocumentType.IMG: return 'Image';
-                case DocumentType.WEB: return 'Web page';
-                case DocumentType.MAP: return 'Map';
-                default: return 'Other node';
-            }
-        }
-        return '';
-    }
-
-    @observable private openActiveColorPicker: boolean = false;
-    @observable private openViewedColorPicker: boolean = false;
-
-    @undoBatch
-    @action
-    switchActive = (color: ColorState) => {
-        this.targetDoc['pres-text-color'] = String(color.hex);
-        return true;
-    };
-    @undoBatch
-    @action
-    switchPresented = (color: ColorState) => {
-        this.targetDoc['pres-text-viewed-color'] = String(color.hex);
-        return true;
-    };
-
-    @computed get activeColorPicker() {
-        return !this.openActiveColorPicker ? null : (
-            <SketchPicker
-                onChange={this.switchActive}
-                presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
-                color={StrCast(this.targetDoc['pres-text-color'])}
-            />
-        );
-    }
-
-    @computed get viewedColorPicker() {
-        return !this.openViewedColorPicker ? null : (
-            <SketchPicker
-                onChange={this.switchPresented}
-                presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
-                color={StrCast(this.targetDoc['pres-text-viewed-color'])}
-            />
-        );
-    }
-
     @action
     turnOffEdit = (paths?: boolean) => paths && this.togglePath(true); // Turn off paths
 
@@ -2174,14 +2136,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 {isMini ? null : (
                     <>
                         <div className="toolbar-divider" />
-                        {/* <Tooltip title={<><div className="dash-tooltip">{this._expandBoolean ? "Minimize all" : "Expand all"}</div></>}>
-                            <div className={"toolbar-button"}
-                                style={{ color: this._expandBoolean ? Colors.MEDIUM_BLUE : 'white' }}
-                                onClick={this.toggleExpandMode}>
-                                <FontAwesomeIcon icon={"eye"} />
-                            </div>
-                        </Tooltip>
-                        <div className="toolbar-divider" /> */}
                         <Tooltip title={<div className="dash-tooltip">{this._presKeyEvents ? 'Keys are active' : 'Keys are not active - click anywhere on the presentation trail to activate keys'}</div>}>
                             <div className="toolbar-button" style={{ cursor: this._presKeyEvents ? 'default' : 'pointer', position: 'absolute', right: 30, fontSize: 16 }}>
                                 <FontAwesomeIcon className={'toolbar-thumbtack'} icon={'keyboard'} style={{ color: this._presKeyEvents ? activeColor : inactiveColor }} />
@@ -2256,7 +2210,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
 
     @computed get playButtons() {
-        const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && (this.activeItem.presIndexed === undefined || NumCast(this.activeItem.presIndexed) === (this.presIndexedItems(this.activeItem)?.length ?? 0));
+        const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && (this.activeItem.presIndexed === undefined || NumCast(this.activeItem.presIndexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0));
         const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0;
         const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc);
         // Case 1: There are still other frames and should go through all frames before going to next slide
@@ -2345,7 +2299,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 </Tooltip>
                 <div className="presPanel-button-text" onClick={() => this.gotoDocument(0, this.activeItem)} style={{ display: inOverlay || this.props.PanelWidth() > 250 ? 'inline-flex' : 'none' }}>
                     {inOverlay ? '' : 'Slide'} {this.itemIndex + 1}
-                    {this.activeItem?.presIndexed !== undefined ? `.${this.activeItem.presIndexed}/${this.presIndexedItems(this.activeItem)?.length}` : ''} / {this.childDocs.length}
+                    {this.activeItem?.presIndexed !== undefined ? `.${this.activeItem.presIndexed}/${this.progressivizedItems(this.activeItem)?.length}` : ''} / {this.childDocs.length}
                 </div>
                 <div className="presPanel-divider"></div>
                 {this.props.PanelWidth() > 250 ? (
@@ -2392,6 +2346,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             this.layoutDoc.presStatus = PresStatus.Manual;
         }
     };
+
     @undoBatch
     @action
     exitClicked = () => {
@@ -2423,14 +2378,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return this.childDocs;
     };
 
-    // TODO: [AL] implement sort function for an array of numbers (e.g. arr[1,2,4] v arr[1,2,1])
     sort = (treeViewMap: Map<Doc, number>) => [...treeViewMap.entries()].sort((a: [Doc, number], b: [Doc, number]) => (a[1] > b[1] ? 1 : a[1] < b[1] ? -1 : 0)).map(kv => kv[0]);
 
     render() {
         // needed to ensure that the childDocs are loaded for looking up fields
         this.childDocs.slice();
         const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
-        const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && (this.activeItem.presIndexed === undefined || NumCast(this.activeItem.presIndexed) === (this.presIndexedItems(this.activeItem)?.length ?? 0));
+        const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && (this.activeItem.presIndexed === undefined || NumCast(this.activeItem.presIndexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0));
         const presStart = !this.layoutDoc.presLoop && this.itemIndex === 0;
         const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc);
         return this.props.addDocTab === returnFalse ? ( // bcz: hack!! - addDocTab === returnFalse only when this is being rendered by the OverlayView which means the doc is a mini player
@@ -2466,7 +2420,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     </Tooltip>
                     <div className="presPanel-button-text">
                         Slide {this.itemIndex + 1}
-                        {this.activeItem.presIndexed !== undefined ? `.${this.activeItem.presIndexed}/${this.presIndexedItems(this.activeItem)?.length}` : ''} / {this.childDocs.length}
+                        {this.activeItem.presIndexed !== undefined ? `.${this.activeItem.presIndexed}/${this.progressivizedItems(this.activeItem)?.length}` : ''} / {this.childDocs.length}
                     </div>
                     <div className="presPanel-divider" />
                     <div className="presPanel-button-text" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.exitClicked, false, false)}>
@@ -2493,6 +2447,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 //childFitWidth={returnTrue}
                                 childOpacity={returnOne}
                                 //childLayoutString={PresElementBox.LayoutString('data')}
+                                childClickScript={PresBox.navigateToDocScript}
                                 childLayoutTemplate={this.childLayoutTemplate}
                                 childXPadding={Doc.IsComicStyle(this.rootDoc) ? 20 : undefined}
                                 filterAddDocument={this.addDocumentFilter}
@@ -2520,11 +2475,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             </div>
         );
     }
-    static NavigateToDoc(bestTarget: Doc, activeItem: Doc) {
-        PresBox.NavigateToTarget(bestTarget, activeItem);
-    }
 }
 
 ScriptingGlobals.add(function navigateToDoc(bestTarget: Doc, activeItem: Doc) {
-    PresBox.NavigateToDoc(bestTarget, activeItem);
+    PresBox.NavigateToTarget(bestTarget, activeItem);
 });
-- 
cgit v1.2.3-70-g09d2


From 096371e683b3f91edfadb2aaf9f8da3309b86014 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 9 Mar 2023 21:39:21 -0500
Subject: added reordering of progressivized docs

---
 src/client/documents/Documents.ts                  |  1 +
 src/client/util/CurrentUserUtils.ts                |  2 +-
 .../views/collections/CollectionTreeView.tsx       |  4 +--
 src/client/views/collections/CollectionView.tsx    |  4 +--
 src/client/views/collections/TreeView.tsx          | 12 ++++---
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 src/client/views/nodes/trails/PresBox.tsx          | 41 ++++++++++++----------
 7 files changed, 37 insertions(+), 29 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index a7378c431..6296b0df4 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -335,6 +335,7 @@ export class DocumentOptions {
     strokeWidth?: number;
     freezeChildren?: string; // whether children are now allowed to be added and or removed from a collection
     treeViewHideTitle?: boolean; // whether to hide the top document title of a tree view
+    treeViewHideUnrendered?: boolean; // tells tree view not to display documents that have an 'unrendered' tag unless they also have a treeViewFieldKey tag (presBox)
     treeViewHideHeaderIfTemplate?: boolean; // whether to hide the header for a document in a tree view only if a childLayoutTemplate is provided (presBox)
     treeViewHideHeader?: boolean; // whether to hide the header for a document in a tree view
     treeViewHideHeaderFields?: boolean; // whether to hide the drop down options for tree view items.
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index a7b2c3d04..2b0a2cdac 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -269,7 +269,7 @@ export class CurrentUserUtils {
             {key: "Button",     creator: Docs.Create.ButtonDocument,                        opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, _isLinkButton: true }},
             {key: "Script",     creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }},
             // {key: "DataViz",    creator: opts => Docs.Create.DataVizDocument(opts),         opts: { _width: 300, _height: 300 }},
-            {key: "Header",     creator: headerTemplate,                                    opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true,}},
+            {key: "Header",     creator: headerTemplate,                                    opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true, treeViewHideUnrendered: true}},
             {key: "Trail",      creator: Docs.Create.PresDocument,                          opts: { _width: 400, _height: 30, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, treeViewHideTitle: true, _fitWidth:true, _chromeHidden: true, boxShadow: "0 0" }},
             {key: "Tab",        creator: opts => Docs.Create.FreeformDocument([], opts),    opts: { _width: 500, _height: 800, _fitWidth: true, _backgroundGridShow: true, }},
             {key: "Slide",      creator: opts => Docs.Create.TreeDocument([], opts),        opts: { _width: 300, _height: 200, _viewType: CollectionViewType.Tree, 
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 330e116d1..553967b95 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -38,8 +38,8 @@ export type collectionTreeViewProps = {
     onCheckedClick?: () => ScriptField;
     onChildClick?: () => ScriptField;
     // TODO: [AL] add these fields
-    AddToMap?: (treeViewDoc: Doc, index: number[]) => Doc[];
-    RemFromMap?: (treeViewDoc: Doc, index: number[]) => Doc[];
+    AddToMap?: (treeViewDoc: Doc, index: number[]) => void;
+    RemFromMap?: (treeViewDoc: Doc, index: number[]) => void;
     hierarchyIndex?: number[];
 };
 
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 316f1e4e9..48e5748a0 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -64,8 +64,8 @@ interface CollectionViewProps_ extends FieldViewProps {
     childClickScript?: ScriptField;
     childDoubleClickScript?: ScriptField;
     //TODO: [AL] add these fields
-    AddToMap?: (treeViewDoc: Doc, index: number[]) => Doc[];
-    RemFromMap?: (treeViewDoc: Doc, index: number[]) => Doc[];
+    AddToMap?: (treeViewDoc: Doc, index: number[]) => void;
+    RemFromMap?: (treeViewDoc: Doc, index: number[]) => void;
     hierarchyIndex?: number[]; // hierarchical index of a document up to the rendering root (primarily used for tree views)
 }
 export interface CollectionViewProps extends React.PropsWithChildren<CollectionViewProps_> {}
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 0d2968ba1..c8dc68eae 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -66,8 +66,8 @@ export interface TreeViewProps {
     skipFields?: string[];
     firstLevel: boolean;
     // TODO: [AL] add these
-    AddToMap?: (treeViewDoc: Doc, index: number[]) => Doc[];
-    RemFromMap?: (treeViewDoc: Doc, index: number[]) => Doc[];
+    AddToMap?: (treeViewDoc: Doc, index: number[]) => void;
+    RemFromMap?: (treeViewDoc: Doc, index: number[]) => void;
     hierarchyIndex?: number[];
 }
 
@@ -1028,7 +1028,9 @@ export class TreeView extends React.Component<TreeViewProps> {
 
     // renders the text version of a document as the header.  This is used in the file system mode and in other vanilla tree views.
     @computed get renderTitleAsHeader() {
-        return (
+        return this.props.treeView.Document.treeViewHideUnrendered && this.doc.unrendered && !this.doc.treeViewFieldKey ? (
+            <div></div>
+        ) : (
             <>
                 {this.renderBullet}
                 {this.renderTitle}
@@ -1146,8 +1148,8 @@ export class TreeView extends React.Component<TreeViewProps> {
         unobserveHeight: (ref: any) => void,
         contextMenuItems: { script: ScriptField; filter: ScriptField; label: string; icon: string }[],
         // TODO: [AL] add these
-        AddToMap?: (treeViewDoc: Doc, index: number[]) => Doc[],
-        RemFromMap?: (treeViewDoc: Doc, index: number[]) => Doc[],
+        AddToMap?: (treeViewDoc: Doc, index: number[]) => void,
+        RemFromMap?: (treeViewDoc: Doc, index: number[]) => void,
         hierarchyIndex?: number[],
         renderCount?: number
     ) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index b92a8ec4d..87177cd74 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -519,7 +519,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 this._clusterSets.push([doc]);
             } else if (this._clusterSets.length) {
                 for (let i = this._clusterSets.length; i <= doc.cluster; i++) !this._clusterSets[i] && this._clusterSets.push([]);
-                this._clusterSets[doc.cluster].push(doc);
+                this._clusterSets[doc.cluster ?? 0].push(doc);
             }
         }
     }
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 2975e49ab..2d9aaeca4 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -9,7 +9,7 @@ import { InkField, InkTool } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
 import { ObjectField } from '../../../../fields/ObjectField';
 import { listSpec } from '../../../../fields/Schema';
-import { ScriptField } from '../../../../fields/ScriptField';
+import { ComputedField, ScriptField } from '../../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
 import { AudioField } from '../../../../fields/URLField';
 import { emptyFunction, emptyPath, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils';
@@ -35,6 +35,7 @@ import { FieldView, FieldViewProps } from '../FieldView';
 import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
+import { tag } from 'xregexp/types';
 const { Howl } = require('howler');
 
 export interface pinDataTypes {
@@ -306,8 +307,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         this.activeItem.presIndexed = presIndexed + 1;
                     }
                     return true;
-                } else {
-                    console.log(this.activeItem.presIndexed);
                 }
             }
         };
@@ -359,8 +358,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         Doc.UnBrushAllDocs();
         if (index >= 0 && index < this.childDocs.length) {
             this.rootDoc._itemIndex = index;
-            const activeItem: Doc = this.activeItem;
-            const targetDoc: Doc = this.targetDoc;
             if (from?.mediaStopTriggerList && this.layoutDoc.presStatus !== PresStatus.Edit) {
                 DocListCast(from.mediaStopTriggerList).forEach(this.stopTempMedia);
             }
@@ -368,8 +365,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 this.stopTempMedia(from.presentationTargetDoc);
             }
             // If next slide is audio / video 'Play automatically' then the next slide should be played
-            if (this.layoutDoc.presStatus !== PresStatus.Edit && (targetDoc.type === DocumentType.AUDIO || targetDoc.type === DocumentType.VID) && activeItem.mediaStart === 'auto') {
-                this.startTempMedia(targetDoc, activeItem);
+            if (this.layoutDoc.presStatus !== PresStatus.Edit && (this.targetDoc.type === DocumentType.AUDIO || this.targetDoc.type === DocumentType.VID) && this.activeItem.mediaStart === 'auto') {
+                this.startTempMedia(this.targetDoc, this.activeItem);
             }
             if (!group) this.clearSelectedArray();
             this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); //Update selected array
@@ -755,17 +752,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const curDoc = Cast(doc, Doc, null);
             const tagDoc = PresBox.targetRenderedDoc(curDoc);
             const itemIndexes: number[] = this.getAllIndexes(this.tagDocs, tagDoc);
+            let opacity: Opt<number> = index === this.itemIndex ? 1 : undefined;
             if (curDoc.presHide) {
                 if (index !== this.itemIndex) {
-                    tagDoc.opacity = 1;
+                    opacity = 1;
                 }
             }
             const hidingIndBef = itemIndexes.find(item => item >= this.itemIndex) ?? itemIndexes.slice().reverse().lastElement();
             if (curDoc.presHideBefore && index === hidingIndBef) {
                 if (index > this.itemIndex) {
-                    tagDoc.opacity = 0;
+                    opacity = 0;
                 } else if (index === this.itemIndex || !curDoc.presHideAfter) {
-                    tagDoc.opacity = 1;
+                    opacity = 1;
                     setTimeout(() => (tagDoc._dataTransition = undefined), 1000);
                 }
             }
@@ -776,17 +774,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     .find(item => item <= this.itemIndex) ?? itemIndexes.lastElement();
             if (curDoc.presHideAfter && index === hidingIndAft) {
                 if (index < this.itemIndex) {
-                    tagDoc.opacity = 0;
+                    opacity = 0;
                 } else if (index === this.itemIndex || !curDoc.presHideBefore) {
-                    tagDoc.opacity = 1;
+                    opacity = 1;
                 }
             }
             const hidingInd = itemIndexes.find(item => item === this.itemIndex);
             if (curDoc.presHide && index === hidingInd) {
                 if (index === this.itemIndex) {
-                    tagDoc.opacity = 0;
+                    opacity = 0;
                 }
             }
+            opacity !== undefined && (tagDoc.opacity = opacity);
         });
     };
 
@@ -882,7 +881,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             if (doc.presHideBefore && index > startIndex) tagDoc.opacity = 0;
             if (doc.presHideAfter && index < startIndex) tagDoc.opacity = 0;
             if (doc.presIndexed !== undefined && index >= startIndex) {
-                const type = DocCast(tagDoc.annotationOn)?.type ?? tagDoc.type;
                 const startInd = NumCast(doc.presIndexedStart);
                 this.progressivizedItems(doc)
                     ?.slice(startInd)
@@ -1552,6 +1550,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 const tagDoc = PresBox.targetRenderedDoc(this.activeItem);
                                 const type = DocCast(tagDoc?.annotationOn)?.type ?? tagDoc.type;
                                 activeItem.presIndexedStart = type === DocumentType.COL ? 1 : 0;
+                                // a progressivized slide doesn't have sub-slides, but rather iterates over the data list of the target being progressivized.
+                                // to avoid creating a new slide to correspond to each of the target's data list, we simply reference the target's data list.
+                                let dataField = Doc.LayoutFieldKey(tagDoc);
+                                if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField = dataField + '-annotations';
+
+                                if (DocCast(activeItem.presentationTargetDoc).annotationOn) activeItem.data = ComputedField.MakeFunction(`self.presentationTargetDoc.annotationOn["${dataField}"]`);
+                                else activeItem.data = ComputedField.MakeFunction(`self.presentationTargetDoc["${dataField}"]`);
                             }}
                             checked={Cast(activeItem.presIndexed, 'number', null) !== undefined ? true : false}
                         />
@@ -2364,7 +2369,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         clearTimeout(this._presTimer);
     };
 
-    AddToMap = (treeViewDoc: Doc, index: number[]): Doc[] => {
+    AddToMap = (treeViewDoc: Doc, index: number[]) => {
+        if (!treeViewDoc.presentationTargetDoc) return this.childDocs; // if treeViewDoc is not a pres elements, then it's a sub-bullet of a progressivized slide which isn't added to the linearized list of pres elements since it's not really a pres element.
         var indexNum = 0;
         for (let i = 0; i < index.length; i++) {
             indexNum += index[i] * 10 ** -i;
@@ -2377,15 +2383,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 this.dataDoc[this.presFieldKey] = new List<Doc>(sorted); // this is a flat array of Docs
             }
         }
-        return this.childDocs;
     };
 
-    RemFromMap = (treeViewDoc: Doc, index: number[]): Doc[] => {
+    RemFromMap = (treeViewDoc: Doc, index: number[]) => {
+        if (!treeViewDoc.presentationTargetDoc) return this.childDocs; // if treeViewDoc is not a pres elements, then it's a sub-bullet of a progressivized slide which isn't added to the linearized list of pres elements since it's not really a pres element.
         if (!this._unmounting && this.isTree) {
             this._treeViewMap.delete(treeViewDoc);
             this.dataDoc[this.presFieldKey] = new List<Doc>(this.sort(this._treeViewMap));
         }
-        return this.childDocs;
     };
 
     sort = (treeViewMap: Map<Doc, number>) => [...treeViewMap.entries()].sort((a: [Doc, number], b: [Doc, number]) => (a[1] > b[1] ? 1 : a[1] < b[1] ? -1 : 0)).map(kv => kv[0]);
-- 
cgit v1.2.3-70-g09d2


From f6b0ff82da9d0a64161a33fcb6e747caa13e62ef Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 10 Mar 2023 22:14:57 -0500
Subject: fixed resetting panZoom transition after focus

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 39 ++++++++++++----------
 1 file changed, 22 insertions(+), 17 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 87177cd74..f98b0839a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -74,7 +74,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @observable
     public static ShowPresPaths = false;
 
-    private _lastNudge: any;
+    private _panZoomTransitionTimer: any;
     private _lastX: number = 0;
     private _lastY: number = 0;
     private _downX: number = 0;
@@ -582,14 +582,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 !InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) &&
                 !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)
             ) {
+                // prettier-ignore
                 switch (Doc.ActiveTool) {
-                    case InkTool.Highlighter:
-                        break;
-                    // TODO: nda - this where we want to create the new "writingDoc" collection that we add strokes to
-                    case InkTool.Write:
-                        break;
-                    case InkTool.Pen:
-                        break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views
+                    case InkTool.Highlighter:  break;
+                    case InkTool.Write: break;
+                    case InkTool.Pen:  break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views
                     case InkTool.Eraser:
                         this._batch = UndoManager.StartBatch('collectionErase');
                         setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction);
@@ -1141,7 +1138,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             }
         }
         if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc || DocListCast(Doc.MyOverlayDocs?.data).includes(this.Document)) {
-            this._panZoomTransition = panTime;
+            this.setPanZoomTransition(panTime);
             const scale = this.getLocalTransform().inverse().Scale;
             const minScale = NumCast(this.rootDoc._viewScaleMin, 1);
             const minPanX = NumCast(this.rootDoc._panXMin, 0);
@@ -1175,12 +1172,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @action
     nudge = (x: number, y: number, nudgeTime: number = 500) => {
         if (this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform || this.props.ContainingCollectionDoc._panX !== undefined) {
-            // bcz: this isn't ideal, but want to try it out...
-            this.setPan(NumCast(this.layoutDoc._panX) + ((this.props.PanelWidth() / 2) * x) / this.zoomScaling(), NumCast(this.layoutDoc._panY) + ((this.props.PanelHeight() / 2) * -y) / this.zoomScaling(), nudgeTime, true);
-            this._lastNudge && clearTimeout(this._lastNudge);
-            this._lastNudge = setTimeout(
-                action(() => (this._panZoomTransition = 0)),
-                nudgeTime
+            this.setPan(
+                NumCast(this.layoutDoc._panX) + ((this.props.PanelWidth() / 2) * x) / this.zoomScaling(), // nudge x,y as a function of panel dimension and scale
+                NumCast(this.layoutDoc._panY) + ((this.props.PanelHeight() / 2) * -y) / this.zoomScaling(),
+                nudgeTime,
+                true
             );
             return true;
         }
@@ -1205,10 +1201,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
     };
 
+    @action
+    setPanZoomTransition = (transitionTime: number) => {
+        this._panZoomTransition = transitionTime;
+        this._panZoomTransitionTimer && clearTimeout(this._panZoomTransitionTimer);
+        this._panZoomTransitionTimer = setTimeout(
+            action(() => (this._panZoomTransition = 0)),
+            transitionTime
+        );
+    };
+
     @action
     zoomSmoothlyAboutPt(docpt: number[], scale: number, transitionTime = 500) {
         if (this.Document._isGroup) return;
-        this._panZoomTransition = transitionTime;
+        this.setPanZoomTransition(transitionTime);
         const screenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]);
         this.layoutDoc[this.scaleFieldKey] = scale;
         const newScreenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]);
@@ -1216,7 +1222,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const newpan = this.getTransform().transformDirection(scrDelta.x, scrDelta.y);
         this.layoutDoc._panX = NumCast(this.layoutDoc._panX) - newpan[0];
         this.layoutDoc._panY = NumCast(this.layoutDoc._panY) - newpan[1];
-        return new Promise<number>(res => setTimeout(() => res(runInAction(() => (this._panZoomTransition = 0))), this._panZoomTransition)); // set transition to be smooth, then reset
     }
 
     calculatePanIntoView = (doc: Doc, xf: Transform, scale?: number) => {
-- 
cgit v1.2.3-70-g09d2


From b0a21edf5ffddda7caecc7774419b4f5034d815f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 10 Mar 2023 22:25:39 -0500
Subject: fited notetaking view to create headers if they don't exist when
 switching from another view.  Also fixed fitWidth typo for collections

---
 src/client/views/collections/CollectionNoteTakingView.tsx              | 2 +-
 src/client/views/collections/TreeView.tsx                              | 3 +--
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx    | 3 ---
 src/client/views/collections/collectionFreeForm/MarqueeView.tsx        | 2 +-
 src/client/views/nodes/DocumentView.tsx                                | 1 -
 src/client/views/nodes/formattedText/FormattedTextBox.tsx              | 2 +-
 6 files changed, 4 insertions(+), 9 deletions(-)

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

diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index 6035871cd..fbf7db892 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -54,7 +54,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders?.find(sh => sh.heading === 'unset'));
         if (needsUnsetCategory || columnHeaders === undefined || columnHeaders.length === 0) {
             setTimeout(() => {
-                const columnHeaders = Array.from(Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null));
+                const columnHeaders = Array.from(Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null) ?? []);
                 const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders?.find(sh => sh.heading === 'unset'));
                 if (needsUnsetCategory || columnHeaders.length === 0) {
                     columnHeaders.push(new SchemaHeaderField('unset', undefined, undefined, 1));
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 054d61d0c..08ffa1466 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -1,6 +1,6 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, IReactionDisposer, observable, ObservableMap, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { DataSym, Doc, DocListCast, DocListCastOrNull, Field, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
@@ -32,7 +32,6 @@ import { CollectionTreeView, TreeViewType } from './CollectionTreeView';
 import { CollectionView } from './CollectionView';
 import './TreeView.scss';
 import React = require('react');
-import { render } from 'react-dom';
 
 export interface TreeViewProps {
     treeView: CollectionTreeView;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index f98b0839a..15d8144fc 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -94,9 +94,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     private _brushtimer: any;
     private _brushtimer1: any;
 
-    // private isWritingMode: boolean = true;
-    // private writingModeDocs: Doc[] = [];
-
     private get isAnnotationOverlay() {
         return this.props.isAnnotationOverlay;
     }
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index a73627165..0714bffbc 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -396,7 +396,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         newCollection.forceActive = makeGroup;
         newCollection.x = this.Bounds.left;
         newCollection.y = this.Bounds.top;
-        newCollection.fitwidth = true;
+        newCollection.fitWidth = true;
         selected.forEach(d => (d.context = newCollection));
         this.hideMarquee();
         return newCollection;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index d15e9bcdc..2b6629913 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -49,7 +49,6 @@ import './DocumentView.scss';
 import { FieldViewProps } from './FieldView';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { LinkAnchorBox } from './LinkAnchorBox';
-import { LinkDocPreview } from './LinkDocPreview';
 import { RadialMenu } from './RadialMenu';
 import { ScriptingBox } from './ScriptingBox';
 import { PresEffect, PresEffectDirection } from './trails';
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index d857b150c..d9ce9a0b0 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1435,7 +1435,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         } else (e.nativeEvent as any).handledByInnerReactInstance = true;
 
         if (this.Document.forceActive) e.stopPropagation();
-        this.tryUpdateScrollHeight(); // if a doc a fitwidth doc is being viewed in different context (eg freeform & lightbox), then it will have conflicting heights.  so when the doc is clicked on, we want to make sure it has the appropriate height for the selected view.
+        this.tryUpdateScrollHeight(); // if a doc a fitWidth doc is being viewed in different context (eg freeform & lightbox), then it will have conflicting heights.  so when the doc is clicked on, we want to make sure it has the appropriate height for the selected view.
         if ((e.target as any).tagName === 'AUDIOTAG') {
             e.preventDefault();
             e.stopPropagation();
-- 
cgit v1.2.3-70-g09d2


From 0e55893d0f7f2a0aa5098df73d0ece5a7f1a4ddf Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 15 Mar 2023 22:33:22 -0400
Subject: fixed up Clone() and export/import collection to work with links,
 presentations, and contexts better.

---
 .eslintrc.json                                     |  4 ++
 package-lock.json                                  | 78 +++++++++++++---------
 package.json                                       |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  6 +-
 src/client/views/nodes/DocumentView.tsx            |  1 +
 src/client/views/nodes/trails/PresBox.tsx          |  7 +-
 src/decycler/decycler.d.ts                         |  2 +
 src/decycler/decycler.js                           | 51 ++++++++++++++
 src/fields/Doc.ts                                  | 75 ++++++++++++---------
 src/server/ApiManagers/UploadManager.ts            | 24 +++----
 tsconfig.json                                      | 24 ++-----
 11 files changed, 170 insertions(+), 104 deletions(-)
 create mode 100644 src/decycler/decycler.d.ts
 create mode 100644 src/decycler/decycler.js

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

diff --git a/.eslintrc.json b/.eslintrc.json
index b9f8e1b7a..43bb53566 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -10,5 +10,9 @@
         "object-shorthand": "off",
         "class-methods-use-this": "off",
         "single-quote": "off"
+    },
+    "parserOptions": {
+        "ecmaVersion": 11,
+        "sourceType": "module"
     }
 }
diff --git a/package-lock.json b/package-lock.json
index 4695adf40..cc51ad9e0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -590,15 +590,30 @@
             "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz",
             "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA=="
         },
+        "@eslint-community/eslint-utils": {
+            "version": "4.2.0",
+            "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz",
+            "integrity": "sha512-gB8T4H4DEfX2IV9zGDJPOBgP1e/DbfCPDTtEqUMckpvzS1OYtva8JdFYBqMwYk7xAQ429WGF/UPqn8uQ//h2vQ==",
+            "dev": true,
+            "requires": {
+                "eslint-visitor-keys": "^3.3.0"
+            }
+        },
+        "@eslint-community/regexpp": {
+            "version": "4.4.0",
+            "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.4.0.tgz",
+            "integrity": "sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ==",
+            "dev": true
+        },
         "@eslint/eslintrc": {
-            "version": "1.4.1",
-            "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz",
-            "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==",
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.1.tgz",
+            "integrity": "sha512-eFRmABvW2E5Ho6f5fHLqgena46rOj7r7OKHYfLElqcBfGFHHpjBhivyi5+jOEQuSpdc/1phIZJlbC2te+tZNIw==",
             "dev": true,
             "requires": {
                 "ajv": "^6.12.4",
                 "debug": "^4.3.2",
-                "espree": "^9.4.0",
+                "espree": "^9.5.0",
                 "globals": "^13.19.0",
                 "ignore": "^5.2.0",
                 "import-fresh": "^3.2.1",
@@ -660,6 +675,12 @@
                 }
             }
         },
+        "@eslint/js": {
+            "version": "8.36.0",
+            "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.36.0.tgz",
+            "integrity": "sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg==",
+            "dev": true
+        },
         "@ffmpeg/core": {
             "version": "0.10.0",
             "resolved": "https://registry.npmjs.org/@ffmpeg/core/-/core-0.10.0.tgz",
@@ -6683,12 +6704,15 @@
             }
         },
         "eslint": {
-            "version": "8.34.0",
-            "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.34.0.tgz",
-            "integrity": "sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg==",
+            "version": "8.36.0",
+            "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.36.0.tgz",
+            "integrity": "sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw==",
             "dev": true,
             "requires": {
-                "@eslint/eslintrc": "^1.4.1",
+                "@eslint-community/eslint-utils": "^4.2.0",
+                "@eslint-community/regexpp": "^4.4.0",
+                "@eslint/eslintrc": "^2.0.1",
+                "@eslint/js": "8.36.0",
                 "@humanwhocodes/config-array": "^0.11.8",
                 "@humanwhocodes/module-importer": "^1.0.1",
                 "@nodelib/fs.walk": "^1.2.8",
@@ -6699,10 +6723,9 @@
                 "doctrine": "^3.0.0",
                 "escape-string-regexp": "^4.0.0",
                 "eslint-scope": "^7.1.1",
-                "eslint-utils": "^3.0.0",
                 "eslint-visitor-keys": "^3.3.0",
-                "espree": "^9.4.0",
-                "esquery": "^1.4.0",
+                "espree": "^9.5.0",
+                "esquery": "^1.4.2",
                 "esutils": "^2.0.2",
                 "fast-deep-equal": "^3.1.3",
                 "file-entry-cache": "^6.0.1",
@@ -6723,7 +6746,6 @@
                 "minimatch": "^3.1.2",
                 "natural-compare": "^1.4.0",
                 "optionator": "^0.9.1",
-                "regexpp": "^3.2.0",
                 "strip-ansi": "^6.0.1",
                 "strip-json-comments": "^3.1.0",
                 "text-table": "^0.2.0"
@@ -6811,6 +6833,15 @@
                         "estraverse": "^5.2.0"
                     }
                 },
+                "esquery": {
+                    "version": "1.5.0",
+                    "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+                    "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+                    "dev": true,
+                    "requires": {
+                        "estraverse": "^5.1.0"
+                    }
+                },
                 "estraverse": {
                     "version": "5.3.0",
                     "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
@@ -7852,23 +7883,6 @@
                 "estraverse": "^4.1.1"
             }
         },
-        "eslint-utils": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
-            "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
-            "dev": true,
-            "requires": {
-                "eslint-visitor-keys": "^2.0.0"
-            },
-            "dependencies": {
-                "eslint-visitor-keys": {
-                    "version": "2.1.0",
-                    "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
-                    "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
-                    "dev": true
-                }
-            }
-        },
         "eslint-visitor-keys": {
             "version": "3.3.0",
             "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
@@ -7876,9 +7890,9 @@
             "dev": true
         },
         "espree": {
-            "version": "9.4.1",
-            "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz",
-            "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==",
+            "version": "9.5.0",
+            "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.0.tgz",
+            "integrity": "sha512-JPbJGhKc47++oo4JkEoTe2wjy4fmMwvFpgJT9cQzmfXKp22Dr6Hf1tdCteLz1h0P3t+mGvWZ+4Uankvh8+c6zw==",
             "dev": true,
             "requires": {
                 "acorn": "^8.8.0",
diff --git a/package.json b/package.json
index 00ce356f9..2c4c41917 100644
--- a/package.json
+++ b/package.json
@@ -98,7 +98,7 @@
         "cross-env": "^5.2.1",
         "css-loader": "^2.1.1",
         "dotenv": "^8.6.0",
-        "eslint": "^8.18.0",
+        "eslint": "^8.36.0",
         "eslint-config-airbnb": "^19.0.4",
         "eslint-config-node": "^4.1.0",
         "eslint-config-prettier": "^8.5.0",
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 15d8144fc..f0c140ef1 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1796,11 +1796,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         !options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
         const mores = ContextMenu.Instance.findByDescription('More...');
         const moreItems = mores && 'subitems' in mores ? mores.subitems : [];
-        if (!Doc.noviceMode) {
-            e.persist();
-            moreItems.push({ description: 'Export collection', icon: 'download', event: async () => Doc.Zip(this.props.Document) });
-            moreItems.push({ description: 'Import exported collection', icon: 'upload', event: ({ x, y }) => this.importDocument(e.clientX, e.clientY) });
-        }
+        moreItems.push({ description: 'Import exported collection', icon: 'upload', event: ({ x, y }) => this.importDocument(e.clientX, e.clientY) });
         !mores && ContextMenu.Instance.addItem({ description: 'More...', subitems: moreItems, icon: 'eye' });
     };
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 02af30d0c..b7a760c1e 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1002,6 +1002,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     }
                     moreItems.push({ description: 'Copy ID', event: () => Utils.CopyText(Doc.globalServerPath(this.props.Document)), icon: 'fingerprint' });
                 }
+                moreItems.push({ description: 'Export collection', icon: 'download', event: async () => Doc.Zip(this.props.Document) });
             }
 
             if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && Doc.ActiveDashboard !== this.props.Document) {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index be40b3592..e79e7472a 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -570,9 +570,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 }
             } else {
                 if (bestTarget._panX !== activeItem.presPanX || bestTarget._panY !== activeItem.presPanY || bestTarget._viewScale !== activeItem.presViewScale) {
-                    bestTarget._panX = activeItem.presPanX;
-                    bestTarget._panY = activeItem.presPanY;
-                    bestTarget._viewScale = activeItem.presViewScale;
+                    bestTarget._panX = activeItem.presPanX ?? bestTarget._panX;
+                    bestTarget._panY = activeItem.presPanY ?? bestTarget._panY;
+                    bestTarget._viewScale = activeItem.presViewScale ?? bestTarget._viewScale;
                     changed = true;
                 }
             }
@@ -717,6 +717,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             zoomTime: activeItem.presMovement === PresMovement.Jump ? 0 : Math.min(Math.max(effect ? 750 : 500, (effect ? 0.2 : 1) * presTime), presTime),
             effect: activeItem,
             noSelect: true,
+            openLocation: OpenWhere.addLeft,
             anchorDoc: activeItem,
             easeFunc: StrCast(activeItem.presEaseFunc, 'ease') as any,
             zoomTextSelections: BoolCast(activeItem.presZoomText),
diff --git a/src/decycler/decycler.d.ts b/src/decycler/decycler.d.ts
new file mode 100644
index 000000000..84620f79c
--- /dev/null
+++ b/src/decycler/decycler.d.ts
@@ -0,0 +1,2 @@
+export declare const decycle: Function;
+export declare const retrocycle: Function;
diff --git a/src/decycler/decycler.js b/src/decycler/decycler.js
new file mode 100644
index 000000000..7fb8a45c7
--- /dev/null
+++ b/src/decycler/decycler.js
@@ -0,0 +1,51 @@
+///
+///  this is a modified copy of the npm project: https://www.npmjs.com/package/json-decycle
+///  the original code is used as replacer when stringifying JSON objects that have cycles.
+///  However, we want an additional replacer that stringifies Dash Fields and Docs in a custom way.
+///  So this modified code allows for a custom replacer to be added to this object that will run before this replacer
+///
+
+const g = e => typeof e === 'object' && e != null && !(e instanceof Boolean) && !(e instanceof Date) && !(e instanceof Number) && !(e instanceof RegExp) && !(e instanceof String);
+const b = e => String('#') + e.map(t => String(t).replace(/~/g, '~0').replace(/\//g, '~1')).join('/');
+// eslint-disable-next-line node/no-unsupported-features/es-syntax
+export function decycle(replacer) {
+    const e = new WeakMap();
+    return function (n, rr) {
+        const r = replacer(n, rr);
+        if (n !== '$ref' && g(r)) {
+            if (e.has(r)) return { $ref: b(e.get(r)) };
+            e.set(r, [...(e.get(this) === undefined ? [] : e.get(this)), n]);
+        }
+        return r;
+    };
+}
+// eslint-disable-next-line node/no-unsupported-features/es-syntax
+export function retrocycle() {
+    const e = new WeakMap();
+    const t = new WeakMap();
+    const n = new Set();
+    function r(o) {
+        const c = o.$ref.slice(1).split('/');
+        let s;
+        let a = this;
+        // eslint-disable-next-line no-plusplus
+        for (let p = 0; p < c.length; p++) {
+            s = c[p].replace(/~1/g, '/').replace(/~0/g, '~');
+            a = a[s];
+        }
+        const f = e.get(o);
+        f[t.get(o)] = a;
+    }
+    return function (c, s) {
+        if (c === '$ref') n.add(this);
+        else if (g(s)) {
+            const f = c === '' && Object.keys(this).length === 1;
+            if (f) n.forEach(r, this);
+            else {
+                e.set(s, this);
+                t.set(s, c);
+            }
+        }
+        return s;
+    };
+}
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index de94ed5db..168e29dd5 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -10,6 +10,7 @@ import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGloba
 import { SelectionManager } from '../client/util/SelectionManager';
 import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from '../client/util/SerializationHelper';
 import { UndoManager } from '../client/util/UndoManager';
+import { decycle } from '../decycler/decycler';
 import { DashColor, incrementTitleCopy, intersectRect, Utils } from '../Utils';
 import { DateField } from './DateField';
 import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from './FieldSymbols';
@@ -25,7 +26,6 @@ import { Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor } from './Ty
 import { AudioField, ImageField, MapField, PdfField, VideoField, WebField } from './URLField';
 import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from './util';
 import JSZip = require('jszip');
-
 export namespace Field {
     export function toKeyValueString(doc: Doc, key: string): string {
         const onDelegate = Object.keys(doc).includes(key);
@@ -701,22 +701,12 @@ 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[],
-        topLevelExclusions: string[],
-        dontCreate: boolean,
-        asBranch: boolean
-    ): Promise<Doc> {
+    export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<string, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: 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);
+        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, ...topLevelExclusions, ...Cast(doc.cloneFieldFilter, listSpec('string'), [])];
+        const filter = [...exclusions, ...Cast(doc.cloneFieldFilter, listSpec('string'), [])];
         await Promise.all(
             Object.keys(doc).map(async key => {
                 if (filter.includes(key)) return;
@@ -727,10 +717,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) {
@@ -740,13 +730,12 @@ 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));
-                    }
-                } 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));
+                const docAtKey = doc[key];
+                if (docAtKey instanceof Doc) {
+                    if (!Doc.IsSystem(docAtKey) && (key === 'annotationOn' || (key === 'proto' && cloneMap.has(doc[Id])) || ((key === 'anchor1' || key === 'anchor2') && doc.author === Doc.CurrentUserEmail))) {
+                        assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch));
+                    } else {
+                        assignKey(docAtKey);
                     }
                 } else {
                     if (field instanceof RefField) {
@@ -765,8 +754,8 @@ export namespace Doc {
             })
         );
         for (const link of Array.from(doc[DirectLinksSym])) {
-            const linkClone = await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, [], dontCreate, asBranch);
-            linkMap.set(link, linkClone);
+            const linkClone = await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch);
+            linkMap.set(link[Id], linkClone);
         }
         if (!dontCreate) {
             Doc.SetInPlace(copy, 'title', (asBranch ? 'BRANCH: ' : 'CLONE: ') + doc.title, true);
@@ -779,11 +768,29 @@ export namespace Doc {
         Doc.AddFileOrphan(copy);
         return copy;
     }
+    export function repairClone(doc: Doc, cloned: Doc[], visited: Set<Doc>) {
+        if (visited.has(doc)) return;
+        visited.add(doc);
+        Object.keys(doc).map(key => {
+            const docAtKey = DocCast(doc[key]);
+            if (docAtKey && !Doc.IsSystem(docAtKey)) {
+                if (!cloned.includes(docAtKey)) {
+                    doc[key] = undefined;
+                } else {
+                    repairClone(docAtKey, cloned, visited);
+                }
+            }
+        });
+    }
     export async function MakeClone(doc: Doc, dontCreate: boolean = false, asBranch = false, cloneMap: Map<string, Doc> = new Map()) {
-        const linkMap = new Map<Doc, Doc>();
+        const linkMap = new Map<string, Doc>();
         const rtfMap: { copy: Doc; key: string; field: RichTextField }[] = [];
-        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));
+        const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf', 'branches', 'branchOf'], dontCreate, asBranch);
+        const repaired = new Set<Doc>();
+        const linkedDocs = Array.from(linkMap.values());
+        const clonedDocs = [...Array.from(cloneMap.values()), ...linkedDocs];
+        clonedDocs.map(clone => Doc.repairClone(clone, Array.from(cloneMap.values()), repaired));
+        linkedDocs.map((link: Doc) => LinkManager.Instance.addLink(link, true));
         rtfMap.map(({ copy, key, field }) => {
             const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
                 const mapped = cloneMap.get(id);
@@ -797,7 +804,7 @@ export namespace Doc {
             const re = new RegExp(regex, 'g');
             copy[key] = new RichTextField(field.Data.replace(/("textId":|"audioId":|"anchorId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
         });
-        return { clone: copy, map: cloneMap };
+        return { clone: copy, map: cloneMap, linkMap };
     }
 
     export async function Zip(doc: Doc) {
@@ -806,9 +813,10 @@ export namespace Doc {
         // a.href = url;
         // a.download = `DocExport-${this.props.Document[Id]}.zip`;
         // a.click();
-        const { clone, map } = await Doc.MakeClone(doc, true);
+        const { clone, map, linkMap } = await Doc.MakeClone(doc, true);
+        clone.LINKS = new List<Doc>(Array.from(linkMap.values()));
         function replacer(key: any, value: any) {
-            if (['branchOf', 'cloneOf', 'context', 'cursors'].includes(key)) return undefined;
+            if (['branchOf', 'cloneOf', 'cursors'].includes(key)) return undefined;
             else if (value instanceof Doc) {
                 if (key !== 'field' && Number.isNaN(Number(key))) {
                     const __fields = value[FieldsSym]();
@@ -833,7 +841,7 @@ export namespace Doc {
 
         const docs: { [id: string]: any } = {};
         Array.from(map.entries()).forEach(f => (docs[f[0]] = f[1]));
-        const docString = JSON.stringify({ id: doc[Id], docs }, replacer);
+        const docString = JSON.stringify({ id: doc[Id], docs }, decycle(replacer));
 
         const zip = new JSZip();
 
@@ -1520,7 +1528,8 @@ export namespace Doc {
             const response = await fetch(upload, { method: 'POST', body: formData });
             const json = await response.json();
             if (json !== 'error') {
-                const doc = await DocServer.GetRefField(json);
+                const doc = DocCast(await DocServer.GetRefField(json));
+                (await DocListCastAsync(doc?.LINKS))?.forEach(link => LinkManager.Instance.addLink(link));
                 return doc;
             }
         }
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index fe4c475c9..9bacbd5c8 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -1,19 +1,19 @@
-import ApiManager, { Registration } from './ApiManager';
-import { Method, _success } from '../RouteManager';
 import * as formidable from 'formidable';
-import v4 = require('uuid/v4');
-const AdmZip = require('adm-zip');
-import { extname, basename, dirname } from 'path';
 import { createReadStream, createWriteStream, unlink, writeFile } from 'fs';
-import { publicDirectory, filesDirectory } from '..';
-import { Database } from '../database';
-import { DashUploadUtils, InjectSize, SizeSuffix } from '../DashUploadUtils';
+import { basename, dirname, extname, normalize } from 'path';
 import * as sharp from 'sharp';
-import { AcceptableMedia, Upload } from '../SharedMediaTypes';
-import { normalize } from 'path';
+import { filesDirectory, publicDirectory } from '..';
+import { retrocycle } from '../../decycler/decycler';
+import { DashUploadUtils, InjectSize, SizeSuffix } from '../DashUploadUtils';
+import { Database } from '../database';
+import { Method, _success } from '../RouteManager';
 import RouteSubscriber from '../RouteSubscriber';
-const imageDataUri = require('image-data-uri');
+import { AcceptableMedia, Upload } from '../SharedMediaTypes';
+import ApiManager, { Registration } from './ApiManager';
 import { SolrManager } from './SearchManager';
+import v4 = require('uuid/v4');
+const AdmZip = require('adm-zip');
+const imageDataUri = require('image-data-uri');
 const fs = require('fs');
 
 export enum Directory {
@@ -252,7 +252,7 @@ export default class UploadManager extends ApiManager {
                                 });
                                 const json = zip.getEntry('doc.json');
                                 try {
-                                    const data = JSON.parse(json.getData().toString('utf8'));
+                                    const data = JSON.parse(json.getData().toString('utf8'), retrocycle());
                                     const datadocs = data.docs;
                                     id = getId(data.id);
                                     const docs = Object.keys(datadocs).map(key => datadocs[key]);
diff --git a/tsconfig.json b/tsconfig.json
index 993ab13b9..bff9255db 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,34 +2,22 @@
     "compilerOptions": {
         "target": "es5",
         "downlevelIteration": true,
-        // "module": "system",
         "removeComments": true,
         "experimentalDecorators": true,
         "allowSyntheticDefaultImports": true,
+        "moduleDetection": "auto",
         "strict": true,
         "jsx": "react",
         "allowJs": true,
         "sourceMap": true,
         "outDir": "dist",
-        "lib": [
-            "dom",
-            "es2015"
-        ],
-        "typeRoots": [
-            "node_modules/@types",
-            "./src/typings"
-        ],
-        "types": [
-            "youtube",
-            "node"
-        ]
+        "lib": ["dom", "es2015"],
+        "typeRoots": ["node_modules/@types", "./src/typings"],
+        "types": ["youtube", "node"]
     },
     // "exclude": [
     //   "node_modules",
     //   "static"
     // ],
-    "typeRoots": [
-        "./node_modules/@types",
-        "./src/typings"
-    ]
-}
\ No newline at end of file
+    "typeRoots": ["./node_modules/@types", "./src/typings"]
+}
-- 
cgit v1.2.3-70-g09d2


From 081f328c117ffdf7ab284be86cdf0342041e7708 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 20 Mar 2023 15:32:06 -0400
Subject: cleaned up pointer events so that nested documents can be selected
 directly without selecting their container.  fixed following link to video
 timeline marker.  fixed focusing on groups.  added didMove to DocFocusOptions
 to restore ability to do toggle on/off of target.  fixed lockingPosition of
 ink strokes.  fixed clicking on inkstrokes in groups to use visiblePainted
 instead of all for pointer events.

---
 src/client/documents/Documents.ts                  |  3 ++-
 src/client/util/DocumentManager.ts                 |  3 ++-
 src/client/views/DocComponent.tsx                  |  5 +++++
 src/client/views/InkingStroke.tsx                  |  2 +-
 src/client/views/PropertiesView.tsx                | 10 ++++-----
 src/client/views/StyleProvider.tsx                 | 25 +++++++++++-----------
 .../collections/CollectionStackedTimeline.tsx      |  5 +----
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 15 ++++++++++---
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  1 -
 src/client/views/nodes/DocumentContentsView.tsx    |  4 ++--
 src/client/views/nodes/DocumentView.tsx            | 22 +++++++++----------
 src/client/views/nodes/button/FontIconBox.tsx      |  2 +-
 src/client/views/pdf/Annotation.tsx                |  6 +++---
 src/fields/Doc.ts                                  |  8 +++----
 14 files changed, 62 insertions(+), 49 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index ff6c8d440..3e89c8347 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -584,7 +584,7 @@ export namespace Docs {
             [
                 DocumentType.FONTICON,
                 {
-                    layout: { view: FontIconBox, dataField: defaultDataKey },
+                    layout: { view: FontIconBox, dataField: 'icon' },
                     options: { allowClickBeforeDoubleClick: true, hideLinkButton: true, _width: 40, _height: 40 },
                 },
             ],
@@ -1686,6 +1686,7 @@ export namespace DocUtils {
                 x: Cast(doc.x, 'number', null),
                 y: Cast(doc.y, 'number', null),
                 backgroundColor: '#ACCEF7',
+                hideAllLinks: true,
                 _width: 15,
                 _height: 15,
                 _xPadding: 0,
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index f2c554866..947613801 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -265,6 +265,7 @@ export class DocumentManager {
         let rootContextView = await new Promise<DocumentView>(res => {
             const viewIndex = docContextPath.findIndex(doc => this.getDocumentView(doc));
             if (viewIndex !== -1) return res(this.getDocumentView(docContextPath[viewIndex])!);
+            options.didMove = true;
             docContextPath.some(doc => TabDocView.Activate(doc)) || MainView.addDocTabFunc(docContextPath[0], options.openLocation as OpenWhere);
             this.AddViewRenderedCb(docContextPath[0], dv => res(dv));
         });
@@ -299,7 +300,7 @@ export class DocumentManager {
             PresBox.restoreTargetDocView(docView, viewSpec, options.zoomTime ?? 500);
             Doc.linkFollowHighlight(docView.rootDoc, undefined, options.effect);
             if (options.playAudio) DocumentManager.playAudioAnno(docView.rootDoc);
-            if (options.toggleTarget) docView.rootDoc.hidden = !docView.rootDoc.hidden;
+            if (options.toggleTarget && (!options.didMove || docView.rootDoc.hidden)) docView.rootDoc.hidden = !docView.rootDoc.hidden;
             if (options.effect) docView.rootDoc[AnimationSym] = options.effect;
 
             if (options.zoomTextSelections && Doc.UnhighlightTimer && contextView && viewSpec.textHtml) {
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 7c81d92d4..0b92fd864 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -16,6 +16,7 @@ import { Touchable } from './Touchable';
 ///  DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView)
 export interface DocComponentProps {
     Document: Doc;
+    fieldKey?: string;
     LayoutTemplate?: () => Opt<Doc>;
     LayoutTemplateString?: string;
 }
@@ -37,6 +38,10 @@ export function DocComponent<P extends DocComponentProps>() {
         @computed get dataDoc() {
             return this.props.Document[DataSym] as Doc;
         }
+        // key where data is stored
+        @computed get fieldKey() {
+            return this.props.fieldKey;
+        }
 
         protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
     }
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 4f08a8e22..3861331b5 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -418,7 +418,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
                 inkScaleX,
                 inkScaleY,
                 '',
-                this.props.pointerEvents?.() ?? (this.rootDoc._lockedPosition ? 'none' : 'visiblepainted'),
+                this.props.pointerEvents?.() ?? 'visiblepainted',
                 0.0,
                 false,
                 downHdlr,
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 03b4100a7..f3a5a5393 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1678,8 +1678,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                         <div className="propertiesView-input inline">
                                             <p>Center Target (no zoom)</p>
                                             <button
-                                                style={{ background: this.sourceAnchor?.followLinkZoomScale !== 0 ? '' : '#4476f7', borderRadius: 3 }}
-                                                onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoomScale', this.sourceAnchor, 0, 0.5, val => !val && this.sourceAnchor && (this.sourceAnchor.followLinkZoom = true))}
+                                                style={{ background: this.sourceAnchor?.followLinkZoom ? '' : '#4476f7', borderRadius: 3 }}
+                                                onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoom', this.sourceAnchor)}
                                                 onClick={e => e.stopPropagation()}
                                                 className="propertiesButton">
                                                 <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
@@ -1687,7 +1687,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                         </div>
                                         <div className="propertiesView-input inline" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 108px) 50px' }}>
                                             <p>Zoom %</p>
-                                            <div className="ribbon-property" style={{ display: !targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? 'none' : 'inline-flex' }}>
+                                            <div className="ribbon-property" style={{ display: !targZoom ? 'none' : 'inline-flex' }}>
                                                 <input className="presBox-input" style={{ width: '100%' }} type="number" value={zoom} />
                                                 <div className="ribbon-propertyUpDown" style={{ display: 'flex', flexDirection: 'column' }}>
                                                     <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}>
@@ -1706,11 +1706,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                                 <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
                                             </button>
                                         </div>
-                                        {!targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? null : PresBox.inputter('0', '1', '100', zoom, true, this.setZoom, 30)}
+                                        {!targZoom ? null : PresBox.inputter('0', '1', '100', zoom, true, this.setZoom, 30)}
                                         <div
                                             className={'slider-headers'}
                                             style={{
-                                                display: !targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? 'none' : 'grid',
+                                                display: !targZoom ? 'none' : 'grid',
                                                 justifyContent: 'space-between',
                                                 width: `calc(100% - ${indent * 2}px)`,
                                                 marginLeft: indent,
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index b11117b11..096396a41 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -89,7 +89,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
     const isAnnotated = property.includes(':annotated');
     const isOpen = property.includes(':open');
     const fieldKey = props?.fieldKey ? props.fieldKey + '-' : isCaption ? 'caption-' : '';
-    const isBackground = () => doc && BoolCast(doc._lockedPosition);
+    const lockedPosition = () => doc && BoolCast(doc._lockedPosition);
     const backgroundCol = () => props?.styleProvider?.(doc, props, StyleProp.BackgroundColor);
     const opacity = () => props?.styleProvider?.(doc, props, StyleProp.Opacity);
     const showTitle = () => props?.styleProvider?.(doc, props, StyleProp.ShowTitle);
@@ -268,7 +268,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
                         doc?.boxShadow,
                         doc?._viewType === CollectionViewType.Pile
                             ? '4px 4px 10px 2px'
-                            : isBackground() || doc?._isGroup || docProps?.LayoutTemplateString
+                            : lockedPosition() || doc?._isGroup || docProps?.LayoutTemplateString
                             ? undefined // groups have no drop shadow -- they're supposed to be "invisible".  LayoutString's imply collection is being rendered as something else (e.g., title of a Slide)
                             : `${darkScheme() ? Colors.DARK_GRAY : Colors.MEDIUM_GRAY} ${StrCast(doc.boxShadow, '0.2vw 0.2vw 0.8vw')}`
                     );
@@ -279,27 +279,28 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
                     return doc.z
                         ? `#9c9396  ${StrCast(doc?.boxShadow, '10px 10px 0.9vw')}` // if it's a floating doc, give it a big shadow
                         : props?.ContainingCollectionDoc?._useClusters
-                        ? `${backgroundCol()} ${StrCast(doc.boxShadow, `0vw 0vw ${(isBackground() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent
+                        ? `${backgroundCol()} ${StrCast(doc.boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent
                         : NumCast(doc.group, -1) !== -1
-                        ? `gray ${StrCast(doc.boxShadow, `0vw 0vw ${(isBackground() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent
-                        : isBackground()
+                        ? `gray ${StrCast(doc.boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent
+                        : lockedPosition()
                         ? undefined // if it's a background & has a cluster color, make the shadow spread really big
                         : StrCast(doc.boxShadow, '');
             }
         }
         case StyleProp.PointerEvents:
-            if (MainView.Instance._exploreMode) return 'all';
+            const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name);
+            if (MainView.Instance._exploreMode) return isInk ? 'visiblePainted' : 'all';
             if (doc?.pointerEvents) return StrCast(doc.pointerEvents);
+            if (props?.contentPointerEvents) return StrCast(props.contentPointerEvents);
             if (props?.pointerEvents?.() === 'none') return 'none';
-            const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name);
-            if (opacity() === 0 || (isInk && !docProps?.treeViewDoc) || doc?.isInkMask) return 'none';
-            if (!isInk) return props?.isDocumentActive?.() ? 'all' : undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active
-            return undefined;
+            if (opacity() === 0 || doc?.isInkMask) return 'none';
+            if (props?.isDocumentActive?.()) return isInk ? 'visiblePainted' : 'all';
+            return undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active
         case StyleProp.Decorations:
             if (props?.ContainingCollectionDoc?._viewType === CollectionViewType.Freeform || doc?.x !== undefined || doc?.y !== undefined) {
-                return doc && doc.pointerEvents === 'none' && isBackground() && !Doc.IsSystem(doc) && (props?.renderDepth || 0) > 0 ? (
+                return doc && doc.pointerEvents === 'none' && lockedPosition() && !Doc.IsSystem(doc) && (props?.renderDepth || 0) > 0 ? (
                     <div className="styleProvider-lock" onClick={() => toggleLockedPosition(doc)}>
-                        <FontAwesomeIcon icon={isBackground() ? 'lock' : 'unlock'} style={{ color: isBackground() ? 'red' : undefined }} size="lg" />
+                        <FontAwesomeIcon icon={lockedPosition() ? 'lock' : 'unlock'} style={{ color: lockedPosition() ? 'red' : undefined }} size="lg" />
                     </div>
                 ) : null;
             }
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index d4e83f609..4941bc722 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -817,10 +817,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
     // renders anchor LabelBox
     renderInner = computedFn(function (this: StackedTimelineAnchor, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), screenXf: () => Transform, width: () => number, height: () => number) {
         const anchor = observable({ view: undefined as any });
-        const focusFunc = (doc: Doc, options: DocFocusOptions) => {
-            this.props.playLink(mark);
-            this.props.focus(doc, options);
-        };
+        const focusFunc = (doc: Doc, options: DocFocusOptions) => this.props.playLink(mark);
         return {
             anchor,
             view: (
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index f0c140ef1..ac90c67a5 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -293,6 +293,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return dispTime === -1 || curTime === -1 || (curTime - dispTime >= -1e-4 && curTime <= endTime);
     }
 
+    groupFocus = (anchor: Doc, options: DocFocusOptions) => {
+        options.docTransform = new Transform(-NumCast(this.rootDoc.panX) + NumCast(anchor.x), -NumCast(this.rootDoc.panY) + NumCast(anchor.y), 1);
+        const res = this.props.focus(this.rootDoc, options);
+        options.docTransform = undefined;
+        return res;
+    };
+
     focus = (anchor: Doc, options: DocFocusOptions) => {
         const xfToCollection = options?.docTransform ?? Transform.Identity();
         const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined };
@@ -301,6 +308,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
         // focus on the document in the collection
         const didMove = !cantTransform && !anchor.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== savedState.scale);
+        if (didMove) options.didMove = true;
         // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active...
         if (didMove) {
             const focusTime = options?.instant ? 0 : options.zoomTime ?? 500;
@@ -1320,7 +1328,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 searchFilterDocs={this.searchFilterDocs}
                 isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive}
                 isContentActive={emptyFunction}
-                focus={this.isAnnotationOverlay ? this.props.focus : this.focus}
+                focus={this.Document._isGroup ? this.groupFocus : this.isAnnotationOverlay ? this.props.focus : this.focus}
                 addDocTab={this.addDocTab}
                 addDocument={this.props.addDocument}
                 removeDocument={this.props.removeDocument}
@@ -2246,8 +2254,9 @@ export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY
             zoomTime: browseTransitionTime,
         }) === undefined
     ) {
-        const selfFfview = dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined;
-        const parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
+        const selfFfview = !dv.rootDoc._isGroup && dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined;
+        let parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
+        while (parFfview?.rootDoc._isGroup) parFfview = parFfview.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
         const ffview = selfFfview && selfFfview.rootDoc[selfFfview.props.scaleField || '_viewScale'] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it.  Otherwise, zoom out on the parent ffview
         ffview?.zoomSmoothlyAboutPt(ffview.getTransform().transformPoint(clientX, clientY), 0.5, browseTransitionTime);
         Doc.linkFollowHighlight(dv?.props.Document, false);
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 901415d1e..9bdb2cee7 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -7,7 +7,6 @@ import { ComputedField } from '../../../fields/ScriptField';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
 import { numberRange } from '../../../Utils';
-import { DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
 import { SelectionManager } from '../../util/SelectionManager';
 import { Transform } from '../../util/Transform';
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 2d1839dd8..679c10a20 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,4 +1,4 @@
-import { computed, trace } from 'mobx';
+import { computed } from 'mobx';
 import { observer } from 'mobx-react';
 import { AclPrivate, Doc, Opt } from '../../../fields/Doc';
 import { ScriptField } from '../../../fields/ScriptField';
@@ -31,6 +31,7 @@ import { KeyValueBox } from './KeyValueBox';
 import { LabelBox } from './LabelBox';
 import { LinkAnchorBox } from './LinkAnchorBox';
 import { LinkBox } from './LinkBox';
+import { LoadingBox } from './LoadingBox';
 import { MapBox } from './MapBox/MapBox';
 import { PDFBox } from './PDFBox';
 import { RecordingBox } from './RecordingBox';
@@ -42,7 +43,6 @@ import { VideoBox } from './VideoBox';
 import { WebBox } from './WebBox';
 import React = require('react');
 import XRegExp = require('xregexp');
-import { LoadingBox } from './LoadingBox';
 
 const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 805e58cd0..36e8facf5 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -95,6 +95,7 @@ export interface DocFocusOptions {
     willZoomCentered?: boolean; // determines whether to zoom in on target document
     zoomScale?: number; // percent of containing frame to zoom into document
     zoomTime?: number;
+    didMove?: boolean; // whether a document was changed during the showDocument process
     docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom.  when this happens the docTransform must accumulate all the transforms of each level of the hierarchy
     instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom)
     preview?: boolean; // whether changes should be previewed by the componentView or written to the document
@@ -313,7 +314,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     @computed get titleHeight() {
         return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0;
     }
-    @computed get pointerEvents() {
+    @computed get pointerEvents(): 'none' | 'all' | 'visiblePainted' | undefined {
         return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents + (this.props.isSelected() ? ':selected' : ''));
     }
     @computed get finalLayoutKey() {
@@ -990,7 +991,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             const more = cm.findByDescription('More...');
             const moreItems = more && 'subitems' in more ? more.subitems : [];
             if (!Doc.IsSystem(this.rootDoc)) {
-                (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && moreItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' });
                 if (!Doc.noviceMode) {
                     moreItems.push({ description: 'Make View of Metadata Field', event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: 'concierge-bell' });
                     moreItems.push({ description: `${this.Document._chromeHidden ? 'Show' : 'Hide'} Chrome`, event: () => (this.Document._chromeHidden = !this.Document._chromeHidden), icon: 'project-diagram' });
@@ -1003,6 +1003,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     moreItems.push({ description: 'Copy ID', event: () => Utils.CopyText(Doc.globalServerPath(this.props.Document)), icon: 'fingerprint' });
                 }
                 moreItems.push({ description: 'Export collection', icon: 'download', event: async () => Doc.Zip(this.props.Document) });
+
+                (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && moreItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' });
             }
 
             if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && Doc.ActiveDashboard !== this.props.Document) {
@@ -1130,6 +1132,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             </Tooltip>
         );
     }
+    pointerEventsFunc = () => this.pointerEvents;
     @computed get contents() {
         TraceMobx();
         return (
@@ -1137,12 +1140,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 className="documentView-contentsView"
                 style={{
                     // prettier-ignore
-                    pointerEvents:
-                        this.opacity === 0 ? 'none' : 
-                        (this.props.pointerEvents?.() as any) ?? this.rootDoc.layoutKey === 'layout_icon'  ? 'none' :
-                        (this.props.contentPointerEvents as any)  ? (this.props.contentPointerEvents as any)  : 
-                        this.rootDoc.type !== DocumentType.INK && this.isContentActive() ? 'all'  : 
-                        'none',
+                    pointerEvents: 
+                        (this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents) ??
+                        (this.rootDoc.layoutKey === 'layout_icon' ? 'none' : 'all'),
                     height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
                 }}>
                 {!this._retryThumb || !this.thumbShown() ? null : (
@@ -1163,7 +1163,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 <DocumentContentsView
                     key={1}
                     {...this.props}
-                    pointerEvents={this.opacity === 0 ? returnNone : this.props.pointerEvents}
+                    pointerEvents={this.opacity === 0 ? returnNone : this.pointerEventsFunc}
                     docViewPath={this.props.viewPath}
                     thumbShown={this.thumbShown}
                     isHovering={this.props.isHovering}
@@ -1175,7 +1175,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     ScreenToLocalTransform={this.screenToLocal}
                     rootSelected={this.rootSelected}
                     onClick={this.onClickFunc}
-                    focus={emptyFunction}
+                    focus={this.props.focus}
                     layoutKey={this.finalLayoutKey}
                 />
                 {this.layoutDoc.hideAllLinks ? null : this.allLinkEndpoints}
@@ -1501,7 +1501,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 style={{
                     display: this.hidden ? 'inline' : undefined,
                     borderRadius: this.borderRounding,
-                    pointerEvents: this.pointerEvents,
+                    pointerEvents: this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents,
                 }}>
                 {!borderPath.path ? (
                     animRenderDoc
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index b3a3c3ae4..339887757 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -101,7 +101,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
         return StrCast(this.rootDoc.label, StrCast(this.rootDoc.title));
     }
     Icon = (color: string) => {
-        const icon = StrCast(this.dataDoc.icon, 'user') as any;
+        const icon = StrCast(this.dataDoc[this.fieldKey ?? 'icon'] ?? 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} />;
     };
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index d1f3397f5..db6b1f011 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -1,5 +1,5 @@
 import React = require('react');
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { action, computed } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
@@ -7,10 +7,10 @@ import { List } from '../../../fields/List';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
 import { LinkFollower } from '../../util/LinkFollower';
 import { undoBatch } from '../../util/UndoManager';
+import { OpenWhere } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { AnchorMenu } from './AnchorMenu';
 import './Annotation.scss';
-import { OpenWhere } from '../nodes/DocumentView';
 
 interface IAnnotationProps extends FieldViewProps {
     anno: Doc;
@@ -82,7 +82,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
             e.stopPropagation();
         } else if (e.button === 0) {
             e.stopPropagation();
-            LinkFollower.FollowLink(undefined, this.annoTextRegion,false);
+            LinkFollower.FollowLink(undefined, this.annoTextRegion, false);
         }
     };
 
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 44314dca2..40ef67f92 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -891,10 +891,10 @@ export namespace Doc {
         // img.file("smile.gif", imgData, {base64: true});
 
         // Generate the zip file asynchronously
-        // zip.generateAsync({ type: 'blob' }).then((content: any) => {
-        //     // Force down of the Zip file
-        //     saveAs(content, doc.title + '.zip'); // glr: Possibly change the name of the document to match the title?
-        // });
+        zip.generateAsync({ type: 'blob' }).then((content: any) => {
+            // Force down of the Zip file
+            saveAs(content, doc.title + '.zip'); // glr: Possibly change the name of the document to match the title?
+        });
     }
     //
     // Determines whether the layout needs to be expanded (as a template).
-- 
cgit v1.2.3-70-g09d2


From 2a4e86be98ad0f8d4aa4cb09b982d448b542d916 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 22 Mar 2023 14:20:09 -0400
Subject: removed gitlike and branch stuff.  updated fortawesome.  added Z
 ordering buttons.  moved ctrl-t/alt-t to edit text title into
 formattedTextBox

---
 package-lock.json                                  |  66 +++---
 package.json                                       |  10 +-
 src/client/documents/Gitlike.ts                    | 226 ++++++++++-----------
 src/client/util/CurrentUserUtils.ts                |   8 +
 src/client/util/DragManager.ts                     |   7 +
 src/client/views/MainView.tsx                      |   4 +
 .../views/collections/CollectionDockingView.tsx    |   3 +-
 src/client/views/collections/CollectionView.tsx    |  38 ++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   6 +
 src/client/views/nodes/DocumentView.tsx            |  32 +--
 src/client/views/nodes/button/ButtonScripts.ts     |  16 --
 src/client/views/nodes/button/FontIconBox.tsx      |   1 -
 .../views/nodes/formattedText/FormattedTextBox.tsx |   4 +-
 src/fields/Doc.ts                                  |  26 ++-
 14 files changed, 211 insertions(+), 236 deletions(-)
 delete mode 100644 src/client/views/nodes/button/ButtonScripts.ts

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

diff --git a/package-lock.json b/package-lock.json
index cc51ad9e0..18a435a41 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -698,67 +698,53 @@
             }
         },
         "@fortawesome/fontawesome-common-types": {
-            "version": "0.3.0",
-            "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.3.0.tgz",
-            "integrity": "sha512-CA3MAZBTxVsF6SkfkHXDerkhcQs0QPofy43eFdbWJJkZiq3SfiaH1msOkac59rQaqto5EqWnASboY1dBuKen5w=="
+            "version": "6.3.0",
+            "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.3.0.tgz",
+            "integrity": "sha512-4BC1NMoacEBzSXRwKjZ/X/gmnbp/HU5Qqat7E8xqorUtBFZS+bwfGH5/wqOC2K6GV0rgEobp3OjGRMa5fK9pFg=="
         },
         "@fortawesome/fontawesome-svg-core": {
-            "version": "1.3.0",
-            "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.3.0.tgz",
-            "integrity": "sha512-UIL6crBWhjTNQcONt96ExjUnKt1D68foe3xjEensLDclqQ6YagwCRYVQdrp/hW0ALRp/5Fv/VKw+MqTUWYYvPg==",
+            "version": "6.3.0",
+            "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.3.0.tgz",
+            "integrity": "sha512-uz9YifyKlixV6AcKlOX8WNdtF7l6nakGyLYxYaCa823bEBqyj/U2ssqtctO38itNEwXb8/lMzjdoJ+aaJuOdrw==",
             "requires": {
-                "@fortawesome/fontawesome-common-types": "^0.3.0"
+                "@fortawesome/fontawesome-common-types": "6.3.0"
             }
         },
         "@fortawesome/free-brands-svg-icons": {
-            "version": "5.15.4",
-            "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.15.4.tgz",
-            "integrity": "sha512-f1witbwycL9cTENJegcmcZRYyawAFbm8+c6IirLmwbbpqz46wyjbQYLuxOc7weXFXfB7QR8/Vd2u5R3q6JYD9g==",
+            "version": "6.3.0",
+            "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.3.0.tgz",
+            "integrity": "sha512-xI0c+a8xnKItAXCN8rZgCNCJQiVAd2Y7p9e2ND6zN3J3ekneu96qrePieJ7yA7073C1JxxoM3vH1RU7rYsaj8w==",
             "requires": {
-                "@fortawesome/fontawesome-common-types": "^0.2.36"
-            },
-            "dependencies": {
-                "@fortawesome/fontawesome-common-types": {
-                    "version": "0.2.36",
-                    "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
-                    "integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg=="
-                }
+                "@fortawesome/fontawesome-common-types": "6.3.0"
             }
         },
         "@fortawesome/free-regular-svg-icons": {
-            "version": "5.15.4",
-            "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.4.tgz",
-            "integrity": "sha512-9VNNnU3CXHy9XednJ3wzQp6SwNwT3XaM26oS4Rp391GsxVYA+0oDR2J194YCIWf7jNRCYKjUCOduxdceLrx+xw==",
+            "version": "6.3.0",
+            "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.3.0.tgz",
+            "integrity": "sha512-cZnwiVHZ51SVzWHOaNCIA+u9wevZjCuAGSvSYpNlm6A4H4Vhwh8481Bf/5rwheIC3fFKlgXxLKaw8Xeroz8Ntg==",
             "requires": {
-                "@fortawesome/fontawesome-common-types": "^0.2.36"
+                "@fortawesome/fontawesome-common-types": "6.3.0"
             },
             "dependencies": {
                 "@fortawesome/fontawesome-common-types": {
-                    "version": "0.2.36",
-                    "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
-                    "integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg=="
+                    "version": "6.3.0",
+                    "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.3.0.tgz",
+                    "integrity": "sha512-4BC1NMoacEBzSXRwKjZ/X/gmnbp/HU5Qqat7E8xqorUtBFZS+bwfGH5/wqOC2K6GV0rgEobp3OjGRMa5fK9pFg=="
                 }
             }
         },
         "@fortawesome/free-solid-svg-icons": {
-            "version": "5.15.4",
-            "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz",
-            "integrity": "sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==",
+            "version": "6.3.0",
+            "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.3.0.tgz",
+            "integrity": "sha512-x5tMwzF2lTH8pyv8yeZRodItP2IVlzzmBuD1M7BjawWgg9XAvktqJJ91Qjgoaf8qJpHQ8FEU9VxRfOkLhh86QA==",
             "requires": {
-                "@fortawesome/fontawesome-common-types": "^0.2.36"
-            },
-            "dependencies": {
-                "@fortawesome/fontawesome-common-types": {
-                    "version": "0.2.36",
-                    "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
-                    "integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg=="
-                }
+                "@fortawesome/fontawesome-common-types": "6.3.0"
             }
         },
         "@fortawesome/react-fontawesome": {
-            "version": "0.1.19",
-            "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.19.tgz",
-            "integrity": "sha512-Hyb+lB8T18cvLNX0S3llz7PcSOAJMLwiVKBuuzwM/nI5uoBw+gQjnf9il0fR1C3DKOI5Kc79pkJ4/xB0Uw9aFQ==",
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz",
+            "integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==",
             "requires": {
                 "prop-types": "^15.8.1"
             }
@@ -20903,7 +20889,7 @@
         "sparse-bitfield": {
             "version": "3.0.3",
             "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
-            "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=",
+            "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
             "optional": true,
             "requires": {
                 "memory-pager": "^1.0.2"
diff --git a/package.json b/package.json
index 2c4c41917..41bdd5f8f 100644
--- a/package.json
+++ b/package.json
@@ -129,11 +129,11 @@
     "dependencies": {
         "@ffmpeg/core": "0.10.0",
         "@ffmpeg/ffmpeg": "0.10.0",
-        "@fortawesome/fontawesome-svg-core": "^1.3.0",
-        "@fortawesome/free-brands-svg-icons": "^5.15.4",
-        "@fortawesome/free-regular-svg-icons": "^5.15.4",
-        "@fortawesome/free-solid-svg-icons": "^5.15.4",
-        "@fortawesome/react-fontawesome": "^0.1.19",
+        "@fortawesome/fontawesome-svg-core": "^6.3.0",
+        "@fortawesome/free-brands-svg-icons": "^6.3.0",
+        "@fortawesome/free-regular-svg-icons": "^6.3.0",
+        "@fortawesome/free-solid-svg-icons": "^6.3.0",
+        "@fortawesome/react-fontawesome": "^0.2.0",
         "@hig/flyout": "^1.3.1",
         "@hig/theme-context": "^2.1.3",
         "@hig/theme-data": "^2.23.1",
diff --git a/src/client/documents/Gitlike.ts b/src/client/documents/Gitlike.ts
index 575c984f5..5e2baf924 100644
--- a/src/client/documents/Gitlike.ts
+++ b/src/client/documents/Gitlike.ts
@@ -1,118 +1,118 @@
-import { Doc, DocListCast, DocListCastAsync, Field } from "../../fields/Doc";
-import { List } from "../../fields/List";
-import { Cast, DateCast } from "../../fields/Types";
-import { DateField } from "../../fields/DateField";
-import { Id } from "../../fields/FieldSymbols";
+// import { Doc, DocListCast, DocListCastAsync, Field } from "../../fields/Doc";
+// import { List } from "../../fields/List";
+// import { Cast, DateCast } from "../../fields/Types";
+// import { DateField } from "../../fields/DateField";
+// import { Id } from "../../fields/FieldSymbols";
 
-// synchs matching documents on the two branches that are being merged/pulled
-// currently this just synchs the main 'fieldKey' component of the data since 
-// we don't have individual timestamps for all fields -- this is a problematic design issue.
-function GitlikeSynchDocs(bd: Doc, md: Doc) {
-    const fieldKey = Doc.LayoutFieldKey(md);
-    const bdate = DateCast(bd[`${fieldKey}-lastModified`])?.date;
-    const mdate = DateCast(md[`${fieldKey}-lastModified`])?.date;
-    const bdproto = bd && Doc.GetProto(bd);
-    if (bdate !== mdate && bdate <= mdate) {
-        if (bdproto && md) {
-            bdproto[fieldKey] = Field.Copy(md[fieldKey]);
-            bdproto[`${fieldKey}-lastModified`] = new DateField();
-        }
-    }
-    const bldate = DateCast(bd._lastModified)?.date;
-    const mldate = DateCast(md._lastModified)?.date;
-    if (bldate === mldate || bldate > mldate) return;
-    if (bdproto && md) {
-        bd.x = Field.Copy(md.x);
-        bd.y = Field.Copy(md.y);
-        bd.width = Field.Copy(md.width);
-        bd.height = Field.Copy(md.height);
-        bdproto._lastModified = new DateField();
-    }
-}
+// // synchs matching documents on the two branches that are being merged/pulled
+// // currently this just synchs the main 'fieldKey' component of the data since
+// // we don't have individual timestamps for all fields -- this is a problematic design issue.
+// function GitlikeSynchDocs(bd: Doc, md: Doc) {
+//     const fieldKey = Doc.LayoutFieldKey(md);
+//     const bdate = DateCast(bd[`${fieldKey}-lastModified`])?.date;
+//     const mdate = DateCast(md[`${fieldKey}-lastModified`])?.date;
+//     const bdproto = bd && Doc.GetProto(bd);
+//     if (bdate !== mdate && bdate <= mdate) {
+//         if (bdproto && md) {
+//             bdproto[fieldKey] = Field.Copy(md[fieldKey]);
+//             bdproto[`${fieldKey}-lastModified`] = new DateField();
+//         }
+//     }
+//     const bldate = DateCast(bd._lastModified)?.date;
+//     const mldate = DateCast(md._lastModified)?.date;
+//     if (bldate === mldate || bldate > mldate) return;
+//     if (bdproto && md) {
+//         bd.x = Field.Copy(md.x);
+//         bd.y = Field.Copy(md.y);
+//         bd.width = Field.Copy(md.width);
+//         bd.height = Field.Copy(md.height);
+//         bdproto._lastModified = new DateField();
+//     }
+// }
 
-// pulls documents onto a branch from the branch's master
-// if a document exists on master but not on the branch, it is branched and added
-// NOTE: need to set a timestamp on the branch that is equal to the master's last merge timestamp.
-async function GitlikePullFromMaster(branch: Doc, suffix = "") {
-    const masterMain = Cast(branch.branchOf, Doc, null);
-    // get the set of documents on both the branch and master
-    const masterMainDocs = masterMain && await DocListCastAsync(masterMain[Doc.LayoutFieldKey(masterMain) + suffix]);
-    const branchMainDocs = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]);
-    // get the master documents that correspond to the branch documents
-    const branchMasterMainDocs = branchMainDocs?.map(bd => Cast(bd.branchOf, Doc, null) || bd);
-    const branchMasterMainDocProtos = branchMasterMainDocs?.map(doc => Doc.GetProto(doc));
-    // get documents on master that don't have a corresponding master doc (form a branch doc), and ...
-    const newDocsFromMaster = masterMainDocs?.filter(md => !branchMasterMainDocProtos?.includes(Doc.GetProto(md)));
-    const oldDocsFromMaster = masterMainDocs?.filter(md => branchMasterMainDocProtos?.includes(Doc.GetProto(md)));
-    oldDocsFromMaster?.forEach(md => {
-        const bd = branchMainDocs?.find(bd => (Cast(bd.branchOf, Doc, null) || bd) === md);
-        bd && GitlikeSynchDocs(bd, md);
-    });
-    const cloneMap = new Map<string, Doc>(); cloneMap.set(masterMain[Id], branch);
-    // make branch clones of them, then add them to the branch
-    const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true, cloneMap)).clone) || []);
-    newlyBranchedDocs.forEach(nd => {
-        Doc.AddDocToList(branch, Doc.LayoutFieldKey(branch) + suffix, nd);
-        nd.context = branch;
-    });
-    // if a branch doc's corresponding main branch doc doesn't have a context, then it was deleted.  
-    const remDocsFromMaster = branchMainDocs?.filter(bd => Cast(bd.branchOf, Doc, null) && !Cast(bd.branchOf, Doc, null)?.context);
-    // so then remove all the deleted main docs from this branch.
-    remDocsFromMaster?.forEach(rd => Doc.RemoveDocFromList(branch, Doc.LayoutFieldKey(branch) + suffix, rd));
-}
+// // pulls documents onto a branch from the branch's master
+// // if a document exists on master but not on the branch, it is branched and added
+// // NOTE: need to set a timestamp on the branch that is equal to the master's last merge timestamp.
+// async function GitlikePullFromMaster(branch: Doc, suffix = "") {
+//     const masterMain = Cast(branch.branchOf, Doc, null);
+//     // get the set of documents on both the branch and master
+//     const masterMainDocs = masterMain && await DocListCastAsync(masterMain[Doc.LayoutFieldKey(masterMain) + suffix]);
+//     const branchMainDocs = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]);
+//     // get the master documents that correspond to the branch documents
+//     const branchMasterMainDocs = branchMainDocs?.map(bd => Cast(bd.branchOf, Doc, null) || bd);
+//     const branchMasterMainDocProtos = branchMasterMainDocs?.map(doc => Doc.GetProto(doc));
+//     // get documents on master that don't have a corresponding master doc (form a branch doc), and ...
+//     const newDocsFromMaster = masterMainDocs?.filter(md => !branchMasterMainDocProtos?.includes(Doc.GetProto(md)));
+//     const oldDocsFromMaster = masterMainDocs?.filter(md => branchMasterMainDocProtos?.includes(Doc.GetProto(md)));
+//     oldDocsFromMaster?.forEach(md => {
+//         const bd = branchMainDocs?.find(bd => (Cast(bd.branchOf, Doc, null) || bd) === md);
+//         bd && GitlikeSynchDocs(bd, md);
+//     });
+//     const cloneMap = new Map<string, Doc>(); cloneMap.set(masterMain[Id], branch);
+//     // make branch clones of them, then add them to the branch
+//     const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true, cloneMap)).clone) || []);
+//     newlyBranchedDocs.forEach(nd => {
+//         Doc.AddDocToList(branch, Doc.LayoutFieldKey(branch) + suffix, nd);
+//         nd.context = branch;
+//     });
+//     // if a branch doc's corresponding main branch doc doesn't have a context, then it was deleted.
+//     const remDocsFromMaster = branchMainDocs?.filter(bd => Cast(bd.branchOf, Doc, null) && !Cast(bd.branchOf, Doc, null)?.context);
+//     // so then remove all the deleted main docs from this branch.
+//     remDocsFromMaster?.forEach(rd => Doc.RemoveDocFromList(branch, Doc.LayoutFieldKey(branch) + suffix, rd));
+// }
 
-// merges all branches from the master branch by first merging the top-level collection of documents, 
-// and then merging all the annotations on those documents.
-// TODO: need to add an incrementing timestamp whenever anything merges.  don't allow a branch to merge if it's last pull timestamp isn't equal to the last merge timestamp.
-async function GitlikeMergeWithMaster(master: Doc, suffix = "") {
-    const branches = await DocListCastAsync(master.branches);
-    branches?.map(async branch => {
-        const branchChildren = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]);
-        branchChildren && await Promise.all(branchChildren.map(async bd => {
-            const cloneMap = new Map<string, Doc>(); cloneMap.set(master[Id], branch);
-            // see if the branch's child exists on master.  
-            const masterChild = Cast(bd.branchOf, Doc, null) || (await Doc.MakeClone(bd, false, true, cloneMap)).clone;
-            // if the branch's child didn't exist on master, we make a branch clone of the child to add to master.  
-            // however, since master is supposed to have the "main" clone, and branches, the "branch" clones, we have to reverse the fields
-            // on the branch child and master clone.
-            if (masterChild.branchOf) {
-                const branchDocProto = Doc.GetProto(bd);
-                const masterChildProto = Doc.GetProto(masterChild);
-                const branchTitle = bd.title;
-                branchDocProto.title = masterChildProto.title;
-                masterChildProto.title = branchTitle;
-                masterChildProto.branchOf = masterChild.branchOf = undefined; // the master child should not be a branch of the branch child, so unset 'branchOf'
-                masterChildProto.branches = new List<Doc>([bd]); // the master child's branches needs to include the branch child
-                Doc.RemoveDocFromList(branchDocProto, "branches", masterChildProto);   // the branch child should not have the master child in its branch list.
-                branchDocProto.branchOf = masterChild;                                 // the branch child is now a branch of the master child
-            }
-            Doc.AddDocToList(master, Doc.LayoutFieldKey(master) + suffix, masterChild); // add the masterChild to master (if it's already there, this is a no-op)
-            masterChild.context = master;
-            GitlikeSynchDocs(masterChild, bd);//Doc.GetProto(masterChild), bd);
-        }));
-        const masterChildren = await DocListCastAsync(master[Doc.LayoutFieldKey(master) + suffix]);
-        masterChildren?.forEach(mc => {                      // see if any master children
-            if (!branchChildren?.find(bc => bc.branchOf === mc)) { //    are not in the list of children for this branch. 
-                Doc.RemoveDocFromList(master, Doc.LayoutFieldKey(master) + suffix, mc); // if so, delete the master child since the branch has deleted it.
-                mc.context = undefined;      // NOTE if we merge a branch that didn't do a pull, it will look like the branch deleted documents -- need edit timestamps that prevent merging if branch isn't up-to-date with last edit timestamp
-            }
-        });
-    });
-}
+// // merges all branches from the master branch by first merging the top-level collection of documents,
+// // and then merging all the annotations on those documents.
+// // TODO: need to add an incrementing timestamp whenever anything merges.  don't allow a branch to merge if it's last pull timestamp isn't equal to the last merge timestamp.
+// async function GitlikeMergeWithMaster(master: Doc, suffix = "") {
+//     const branches = await DocListCastAsync(master.branches);
+//     branches?.map(async branch => {
+//         const branchChildren = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]);
+//         branchChildren && await Promise.all(branchChildren.map(async bd => {
+//             const cloneMap = new Map<string, Doc>(); cloneMap.set(master[Id], branch);
+//             // see if the branch's child exists on master.
+//             const masterChild = Cast(bd.branchOf, Doc, null) || (await Doc.MakeClone(bd, false, true, cloneMap)).clone;
+//             // if the branch's child didn't exist on master, we make a branch clone of the child to add to master.
+//             // however, since master is supposed to have the "main" clone, and branches, the "branch" clones, we have to reverse the fields
+//             // on the branch child and master clone.
+//             if (masterChild.branchOf) {
+//                 const branchDocProto = Doc.GetProto(bd);
+//                 const masterChildProto = Doc.GetProto(masterChild);
+//                 const branchTitle = bd.title;
+//                 branchDocProto.title = masterChildProto.title;
+//                 masterChildProto.title = branchTitle;
+//                 masterChildProto.branchOf = masterChild.branchOf = undefined; // the master child should not be a branch of the branch child, so unset 'branchOf'
+//                 masterChildProto.branches = new List<Doc>([bd]); // the master child's branches needs to include the branch child
+//                 Doc.RemoveDocFromList(branchDocProto, "branches", masterChildProto);   // the branch child should not have the master child in its branch list.
+//                 branchDocProto.branchOf = masterChild;                                 // the branch child is now a branch of the master child
+//             }
+//             Doc.AddDocToList(master, Doc.LayoutFieldKey(master) + suffix, masterChild); // add the masterChild to master (if it's already there, this is a no-op)
+//             masterChild.context = master;
+//             GitlikeSynchDocs(masterChild, bd);//Doc.GetProto(masterChild), bd);
+//         }));
+//         const masterChildren = await DocListCastAsync(master[Doc.LayoutFieldKey(master) + suffix]);
+//         masterChildren?.forEach(mc => {                      // see if any master children
+//             if (!branchChildren?.find(bc => bc.branchOf === mc)) { //    are not in the list of children for this branch.
+//                 Doc.RemoveDocFromList(master, Doc.LayoutFieldKey(master) + suffix, mc); // if so, delete the master child since the branch has deleted it.
+//                 mc.context = undefined;      // NOTE if we merge a branch that didn't do a pull, it will look like the branch deleted documents -- need edit timestamps that prevent merging if branch isn't up-to-date with last edit timestamp
+//             }
+//         });
+//     });
+// }
 
-// performs a "git"-like task: pull or merge
-//    if pull, then target is a specific branch document that will be updated from its associated master
-//    if merge, then target is the master doc that will merge in all branches associated with it.
-// TODO: parameterize 'merge' to specify which branch(es) should be merged.   
-//       extend 'merge' to allow a specific branch to be merge target (not just master);
-//       make pull/merge be recursive (ie, this func currently just operates on the main doc and its children)
-export async function BranchTask(target: Doc, action: "pull" | "merge") {
-    const func = action === "pull" ? GitlikePullFromMaster : GitlikeMergeWithMaster;
-    await func(target, "");
-    await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-annotations"));
-    await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-sidebar"));
-}
+// // performs a "git"-like task: pull or merge
+// //    if pull, then target is a specific branch document that will be updated from its associated master
+// //    if merge, then target is the master doc that will merge in all branches associated with it.
+// // TODO: parameterize 'merge' to specify which branch(es) should be merged.
+// //       extend 'merge' to allow a specific branch to be merge target (not just master);
+// //       make pull/merge be recursive (ie, this func currently just operates on the main doc and its children)
+// export async function BranchTask(target: Doc, action: "pull" | "merge") {
+//     const func = action === "pull" ? GitlikePullFromMaster : GitlikeMergeWithMaster;
+//     await func(target, "");
+//     await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-annotations"));
+//     await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-sidebar"));
+// }
 
-export async function BranchCreate(target: Doc) {
-    return (await Doc.MakeClone(target, false, true)).clone;
-}
\ No newline at end of file
+// export async function BranchCreate(target: Doc) {
+//     return (await Doc.MakeClone(target, false, true)).clone;
+// }
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index c038fd6ab..f9766c3fb 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -608,6 +608,13 @@ export class CurrentUserUtils {
         return DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns);
     }
 
+    static zTools(): Button[] {
+        return [
+            { title: "Bottom",  icon: "arrows-down-to-line",  toolTip: "Make doc topmost",btnType: ButtonType.ClickButton,        expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'}, scripts: { onClick: 'sendToBack()'}}, // Only when floating document is selected in freeform
+            { title: "Top",     icon: "arrows-up-to-line",    toolTip: "Make doc bottommost",btnType: ButtonType.ClickButton,     expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'}, scripts: { onClick: 'bringToFront()'}}, // Only when floating document is selected in freeform
+            { title: "Z order", icon: "z",                    toolTip: "Bring Forward on Drag",btnType: ButtonType.ToggleButton,  expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: 'tab'},                                                                     scripts: { onClick: 'toggleRaiseOnDrag(_readOnly_)'}}, // Only when floating document is selected in freeform
+        ]
+    }
     static textTools():Button[] {
         return [
             { title: "Font",    toolTip: "Font", width: 100,     btnType: ButtonType.DropdownList,                      toolType:"font",     ignoreClick: true,   scripts: {script:  '{ return setFontAttr(self.toolType, value, _readOnly_);}'},
@@ -675,6 +682,7 @@ export class CurrentUserUtils {
             { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,         expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 20,  scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
             { title: "Text",    icon: "Text",          toolTip: "Text functions",       subMenu: CurrentUserUtils.textTools(),   expertMode: false, toolType:DocumentType.RTF,            funcs: {                                                                             linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available
             { title: "Ink",     icon: "Ink",           toolTip: "Ink functions",        subMenu: CurrentUserUtils.inkTools(),    expertMode: false, toolType:DocumentType.INK,            funcs: {                                                                             linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
+            { title: "Z",       icon: "z",             toolTip: "Z order functions",    subMenu: CurrentUserUtils.zTools(),      expertMode: false, toolType:"tab",                       funcs: {                                                                             linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available
             { title: "Web",     icon: "Web",           toolTip: "Web functions",        subMenu: CurrentUserUtils.webTools(),    expertMode: false, toolType:DocumentType.WEB,            funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Only when Web is selected
             { title: "Schema",  icon: "Schema",        toolTip: "Schema functions",     subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema,   funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} } // Only when Schema is selected
         ];
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index a56f87075..5a35fcd53 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -10,7 +10,9 @@ import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../fields/Types
 import { emptyFunction, Utils } from '../../Utils';
 import { Docs, DocUtils } from '../documents/Documents';
 import * as globalCssVariables from '../views/global/globalCssVariables.scss';
+import { Colors } from '../views/global/globalEnums';
 import { DocumentView } from '../views/nodes/DocumentView';
+import { ScriptingGlobals } from './ScriptingGlobals';
 import { SnappingManager } from './SnappingManager';
 import { UndoManager } from './UndoManager';
 
@@ -590,3 +592,8 @@ export namespace DragManager {
         endDrag?.();
     }
 }
+
+ScriptingGlobals.add(function toggleRaiseOnDrag(readOnly?: boolean) {
+    if (readOnly) return DragManager.GetRaiseWhenDragged() ? Colors.MEDIUM_BLUE : 'transparent';
+    DragManager.SetRaiseWhenDragged(!DragManager.GetRaiseWhenDragged());
+});
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 118635a38..e755f204b 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -477,6 +477,10 @@ export class MainView extends React.Component {
                 fa.faHighlighter,
                 fa.faRemoveFormat,
                 fa.faHandPointUp,
+                fa.faXRay,
+                fa.faZ,
+                fa.faArrowsUpToLine,
+                fa.faArrowsDownToLine,
             ]
         );
     }
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 9b6554d67..057c1e30f 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -410,11 +410,10 @@ export class CollectionDockingView extends CollectionSubView() {
             const _width = Number(getComputedStyle(content).width.replace('px', ''));
             const _height = Number(getComputedStyle(content).height.replace('px', ''));
             return CollectionFreeFormView.UpdateIcon(this.layoutDoc[Id] + '-icon' + new Date().getTime(), content, _width, _height, _width, _height, 0, 1, true, this.layoutDoc[Id] + '-icon', (iconFile, _nativeWidth, _nativeHeight) => {
-                const img = Docs.Create.ImageDocument(new ImageField(iconFile), { title: this.rootDoc.title + '-icon', _width, _height, _nativeWidth, _nativeHeight });
                 const proto = this.dataDoc; // Cast(img.proto, Doc, null)!;
                 proto['thumb-nativeWidth'] = _width;
                 proto['thumb-nativeHeight'] = _height;
-                this.dataDoc.thumb = new ImageField(iconFile);
+                proto.thumb = new ImageField(iconFile);
             });
         }
     }
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index aed88aa1a..eafa50d27 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -2,7 +2,6 @@ import { computed, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, DocListCast } from '../../../fields/Doc';
-import { Id } from '../../../fields/FieldSymbols';
 import { ObjectField } from '../../../fields/ObjectField';
 import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types';
@@ -10,13 +9,12 @@ import { TraceMobx } from '../../../fields/util';
 import { returnEmptyString } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
 import { CollectionViewType } from '../../documents/DocumentTypes';
-import { BranchCreate, BranchTask } from '../../documents/Gitlike';
 import { ImageUtils } from '../../util/Import & Export/ImageUtils';
 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 { OpenWhere } from '../nodes/DocumentView';
 import { FieldView, FieldViewProps } from '../nodes/FieldView';
 import { CollectionCarousel3DView } from './CollectionCarousel3DView';
 import { CollectionCarouselView } from './CollectionCarouselView';
@@ -193,23 +191,23 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
             }
             !Doc.noviceMode && optionItems.push({ description: `${this.rootDoc._isLightbox ? 'Unset' : 'Set'} is Lightbox`, event: () => (this.rootDoc._isLightbox = !this.rootDoc._isLightbox), icon: 'project-diagram' });
 
-            if (!Doc.noviceMode && false) {
-                optionItems.push({
-                    description: 'Create Branch',
-                    event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), OpenWhere.addRight),
-                    icon: 'project-diagram',
-                });
-                optionItems.push({
-                    description: 'Pull Master',
-                    event: () => BranchTask(this.rootDoc, 'pull'),
-                    icon: 'project-diagram',
-                });
-                optionItems.push({
-                    description: 'Merge Branches',
-                    event: () => BranchTask(this.rootDoc, 'merge'),
-                    icon: 'project-diagram',
-                });
-            }
+            // if (!Doc.noviceMode && false) {
+            //     optionItems.push({
+            //         description: 'Create Branch',
+            //         event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), OpenWhere.addRight),
+            //         icon: 'project-diagram',
+            //     });
+            //     optionItems.push({
+            //         description: 'Pull Master',
+            //         event: () => BranchTask(this.rootDoc, 'pull'),
+            //         icon: 'project-diagram',
+            //     });
+            //     optionItems.push({
+            //         description: 'Merge Branches',
+            //         event: () => BranchTask(this.rootDoc, 'merge'),
+            //         icon: 'project-diagram',
+            //     });
+            // }
 
             !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'hand-point-right' });
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index ac90c67a5..0ea614472 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -2280,3 +2280,9 @@ ScriptingGlobals.add(function pinWithView(readOnly: boolean, pinDocContent: bool
             TabDocView.PinDoc(view.rootDoc, { currentFrame: Cast(view.rootDoc.currentFrame, 'number', null), pinDocContent, pinViewport: MarqueeView.CurViewBounds(view.rootDoc, view.props.PanelWidth(), view.props.PanelHeight()) })
         );
 });
+ScriptingGlobals.add(function bringToFront() {
+    SelectionManager.Views().forEach(view => view.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.bringToFront(view.rootDoc));
+});
+ScriptingGlobals.add(function sendToBack(doc: Doc) {
+    SelectionManager.Views().forEach(view => view.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.bringToFront(view.rootDoc, true));
+});
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index edf508c95..c1665ea3d 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -162,6 +162,7 @@ export interface DocumentViewSharedProps {
     childHideResizeHandles?: () => boolean;
     dataTransition?: string; // specifies animation transition - used by collectionPile and potentially other layout engines when changing the size of documents so that the change won't be abrupt
     styleProvider: Opt<StyleProviderFunc>;
+    setTitleFocus?: () => void;
     focus: DocFocusFunc;
     fitWidth?: (doc: Doc) => boolean | undefined;
     docFilters: () => string[];
@@ -539,18 +540,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 .ScreenToLocalTransform()
                 .scale(this.NativeDimScaling)
                 .transformDirection(x - left, y - top);
-            // dragData.offset[0] = Math.min(this.rootDoc[WidthSym](), dragData.offset[0]);  // bcz: this was breaking dragging rotated objects since the offset may be out of bounds with regard to the unrotated document
-            // dragData.offset[1] = Math.min(this.rootDoc[HeightSym](), dragData.offset[1]);
             dragData.dropAction = dropAction;
             dragData.treeViewDoc = this.props.treeViewDoc;
             dragData.removeDocument = this.props.removeDocument;
             dragData.moveDocument = this.props.moveDocument;
             dragData.canEmbed = this.props.canEmbedOnDrag;
-            //dragData.dimSource :
-            // dragEffects field, set dim
-            // add kv pairs to a doc, swap properties with the node while dragging, and then swap when dropping
-            // add a dragEffects prop to DocumentView as a function that sets up. Each view has its own prop, when you start dragging:
-            // in Draganager, figure out which doc(s) you're dragging and change what opacity function returns
             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) }, () =>
@@ -560,22 +554,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         }
     }
 
-    onKeyDown = (e: React.KeyboardEvent) => {
-        if (e.altKey) {
-            e.stopPropagation();
-            e.preventDefault();
-            if (e.key === '†' || e.key === 't') {
-                if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = 'title';
-                if (!this._titleRef.current) setTimeout(() => this._titleRef.current?.setIsFocused(true));
-                else if (!this._titleRef.current.setIsFocused(true)) {
-                    // if focus didn't change, focus on interior text...
-                    this._titleRef.current?.setIsFocused(false);
-                    this._componentView?.setFocus?.();
-                }
-            }
-        }
-    };
-
     defaultRestoreTargetView = (docView: DocumentView, anchor: Doc, focusSpeed: number, options: DocFocusOptions) => {
         const targetMatch =
             Doc.AreProtosEqual(anchor, this.rootDoc) || // anchor is this document, so anchor's properties apply to this document
@@ -585,6 +563,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         return targetMatch && PresBox.restoreTargetDocView(docView, anchor, focusSpeed) ? focusSpeed : undefined;
     };
 
+    // switches text input focus to the title bar of the document (and displays the title bar if it hadn't been)
+    setTitleFocus = () => {
+        if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = 'title';
+        setTimeout(() => this._titleRef.current?.setIsFocused(true)); // use timeout in case title wasn't shown to allow re-render so that titleref will be defined
+    };
+
     onClick = action((e: React.MouseEvent | React.PointerEvent) => {
         if (!this.Document.ignoreClick && this.props.renderDepth >= 0 && Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
             let stopPropagate = true;
@@ -1176,6 +1160,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     rootSelected={this.rootSelected}
                     onClick={this.onClickFunc}
                     focus={this.props.focus}
+                    setTitleFocus={this.setTitleFocus}
                     layoutKey={this.finalLayoutKey}
                 />
                 {this.layoutDoc.hideAllLinks ? null : this.allLinkEndpoints}
@@ -1492,7 +1477,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 className={`${DocumentView.ROOT_DIV} docView-hack`}
                 ref={this._mainCont}
                 onContextMenu={this.onContextMenu}
-                onKeyDown={this.onKeyDown}
                 onPointerDown={this.onPointerDown}
                 onClick={this.onClick}
                 onPointerEnter={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.props.Document)}
diff --git a/src/client/views/nodes/button/ButtonScripts.ts b/src/client/views/nodes/button/ButtonScripts.ts
deleted file mode 100644
index b4a382faf..000000000
--- a/src/client/views/nodes/button/ButtonScripts.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { ScriptingGlobals } from "../../../util/ScriptingGlobals";
-import { SelectionManager } from "../../../util/SelectionManager";
-import { Colors } from "../../global/globalEnums";
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function changeView(view: string) {
-    const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
-    selected ? selected.Document._viewType = view : console.log("[FontIconBox.tsx] changeView failed");
-});
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function toggleOverlay(readOnly?: boolean) {
-    const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
-    if (readOnly) return selected?.Document.z ? Colors.MEDIUM_BLUE : "transparent";
-    selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log("failed");
-});
\ No newline at end of file
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 339887757..cb962cad3 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -1,5 +1,4 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { faAlignRight } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
 import { action, computed, observable, runInAction } from 'mobx';
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index da10bb0dc..a2c1195cb 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1669,8 +1669,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     };
 
     onKeyDown = (e: React.KeyboardEvent) => {
-        if (e.altKey) {
+        if ((e.altKey || e.ctrlKey) && e.key === 't') {
             e.preventDefault();
+            e.stopPropagation();
+            this.props.setTitleFocus?.();
             return;
         }
         const state = this._editorView!.state;
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 40da482d2..4d82551db 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -698,7 +698,7 @@ export namespace Doc {
     // this lists out all the tag ids that can be  in a RichTextField that might contain document ids.
     // if a document is cloned, we need to make sure to clone all of these referenced documents as well;
     export const DocsInTextFieldIds = ['audioId', 'textId', 'anchorId', 'docId'];
-    export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<string, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], cloneLinks: boolean, asBranch: boolean): Promise<Doc> {
+    export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<string, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], cloneLinks: boolean): Promise<Doc> {
         if (Doc.IsBaseProto(doc)) return doc;
         if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
         const copy = new Doc(undefined, true);
@@ -714,7 +714,7 @@ 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, cloneLinks, asBranch)));
+                        const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, cloneLinks)));
                         assignKey(new List<Doc>(clones));
                     } else {
                         assignKey(ObjectField.MakeCopy(field));
@@ -731,7 +731,7 @@ export namespace Doc {
                                 );
                                 const results = docids && (await DocServer.GetRefFields(docids));
                                 const docs = results && Array.from(Object.keys(results)).map(key => DocCast(results[key]));
-                                docs && docs.map(doc => Doc.makeClone(doc, cloneMap, linkMap, rtfs, exclusions, cloneLinks, asBranch));
+                                docs && docs.map(doc => Doc.makeClone(doc, cloneMap, linkMap, rtfs, exclusions, cloneLinks));
                                 rtfs.push({ copy, key, field });
                             }
                         }
@@ -740,7 +740,7 @@ export namespace Doc {
                 const docAtKey = doc[key];
                 if (docAtKey instanceof Doc) {
                     if (!Doc.IsSystem(docAtKey) && (key.startsWith('layout') || key === 'annotationOn' || key === 'proto' || ((key === 'anchor1' || key === 'anchor2') && doc.author === Doc.CurrentUserEmail))) {
-                        assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, cloneLinks, asBranch));
+                        assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, cloneLinks));
                     } else {
                         assignKey(docAtKey);
                     }
@@ -763,14 +763,11 @@ export namespace Doc {
                 cloneLinks ||
                 ((cloneMap.has(DocCast(link.anchor1)?.[Id]) || cloneMap.has(DocCast(DocCast(link.anchor1)?.annotationOn)?.[Id])) && (cloneMap.has(DocCast(link.anchor2)?.[Id]) || cloneMap.has(DocCast(DocCast(link.anchor2)?.annotationOn)?.[Id])))
             ) {
-                linkMap.set(link[Id], await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, cloneLinks, asBranch));
+                linkMap.set(link[Id], await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, cloneLinks));
             }
         });
-        Doc.SetInPlace(copy, 'title', (asBranch ? 'BRANCH: ' : 'CLONE: ') + doc.title, true);
-        asBranch ? (copy.branchOf = doc) : (copy.cloneOf = doc);
-        if (!Doc.IsPrototype(copy)) {
-            Doc.AddDocToList(doc, 'branches', Doc.GetProto(copy));
-        }
+        Doc.SetInPlace(copy, 'title', 'CLONE: ' + doc.title, true);
+        copy.cloneOf = doc;
         cloneMap.set(doc[Id], copy);
 
         Doc.AddFileOrphan(copy);
@@ -794,14 +791,15 @@ export namespace Doc {
                 }
             });
     }
-    export function MakeClones(docs: Doc[], cloneLinks: boolean, asBranch = false, cloneMap: Map<string, Doc> = new Map()) {
-        return docs.map(doc => Doc.MakeClone(doc, cloneLinks, asBranch, cloneMap));
+    export function MakeClones(docs: Doc[], cloneLinks: boolean) {
+        const cloneMap = new Map<string, Doc>();
+        return docs.map(doc => Doc.MakeClone(doc, cloneLinks, cloneMap));
     }
 
-    export async function MakeClone(doc: Doc, cloneLinks = true, asBranch = false, cloneMap: Map<string, Doc> = new Map()) {
+    export async function MakeClone(doc: Doc, cloneLinks = true, cloneMap: Map<string, Doc> = new Map()) {
         const linkMap = new Map<string, Doc>();
         const rtfMap: { copy: Doc; key: string; field: RichTextField }[] = [];
-        const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf', 'branches', 'branchOf'], cloneLinks, asBranch);
+        const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], cloneLinks);
         const repaired = new Set<Doc>();
         const linkedDocs = Array.from(linkMap.values());
         const clonedDocs = [...Array.from(cloneMap.values()), ...linkedDocs];
-- 
cgit v1.2.3-70-g09d2

