From df7257d1b39f51a7e00a495f0d4a2366f0e21f7d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 14 May 2023 22:04:36 -0400
Subject: fixed webpage link following by adding a presData for the current URL
 to all embedded docs.  fixed getView() in showDocuments to not get called
 with the proper anchors. changed unrendered MARKERs to CONFIGs. changed
 anchors to Configs or Markers as appropriate.

---
 src/client/views/collections/CollectionCarousel3DView.tsx | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

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

diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index a266c9207..b10b453bf 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -39,7 +39,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
     isChildContentActive = () => (this.isContentActive() ? true : false);
 
     @computed get content() {
-        const currentIndex = NumCast(this.layoutDoc._itemIndex);
+        const currentIndex = NumCast(this.layoutDoc._carousel_index);
         const displayDoc = (childPair: { layout: Doc; data: Doc }) => {
             return (
                 <DocumentView
@@ -75,7 +75,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
     }
 
     changeSlide = (direction: number) => {
-        this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) + direction + this.childLayoutPairs.length) % this.childLayoutPairs.length;
+        this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + direction + this.childLayoutPairs.length) % this.childLayoutPairs.length;
     };
 
     onArrowClick = (e: React.MouseEvent, direction: number) => {
@@ -139,11 +139,11 @@ export class CollectionCarousel3DView extends CollectionSubView() {
     }
 
     @computed get dots() {
-        return this.childLayoutPairs.map((_child, index) => <div key={Utils.GenerateGuid()} className={`dot${index === NumCast(this.layoutDoc._itemIndex) ? '-active' : ''}`} onClick={() => (this.layoutDoc._itemIndex = index)} />);
+        return this.childLayoutPairs.map((_child, index) => <div key={Utils.GenerateGuid()} className={`dot${index === NumCast(this.layoutDoc._carousel_index) ? '-active' : ''}`} onClick={() => (this.layoutDoc._carousel_index = index)} />);
     }
 
     render() {
-        const index = NumCast(this.layoutDoc._itemIndex);
+        const index = NumCast(this.layoutDoc._carousel_index);
         const translateX = this.panelWidth() * (1 - index);
 
         return (
-- 
cgit v1.2.3-70-g09d2


From ad32fce170ffeef8e03c108569113dca9de15d5d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 21 May 2023 22:25:28 -0400
Subject: fixed comparisonBox to create embeddings of documents by fixing
 targetDropAction to work for non-collections.  fixed carouse3D view to have
 correct screentolocal XFs,

---
 src/Utils.ts                                       |  2 +-
 src/client/documents/Documents.ts                  |  4 +-
 src/client/util/DragManager.ts                     |  9 ++++-
 src/client/views/DashboardView.tsx                 |  1 -
 src/client/views/DocumentDecorations.tsx           | 12 +++---
 src/client/views/GlobalKeyHandler.ts               | 46 +++++-----------------
 src/client/views/PreviewCursor.tsx                 | 21 +++-------
 src/client/views/PropertiesButtons.tsx             |  2 +-
 .../collections/CollectionCarousel3DView.scss      | 14 +++++--
 .../views/collections/CollectionCarousel3DView.tsx | 32 +++++++++------
 src/client/views/global/globalCssVariables.scss    |  7 ++++
 .../views/global/globalCssVariables.scss.d.ts      |  3 ++
 src/client/views/nodes/ComparisonBox.tsx           | 12 ++----
 src/client/views/nodes/DocumentView.tsx            |  2 +-
 src/fields/Doc.ts                                  | 28 ++++++++++---
 15 files changed, 102 insertions(+), 93 deletions(-)

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

diff --git a/src/Utils.ts b/src/Utils.ts
index 73de6d754..f2ad814d4 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -540,7 +540,7 @@ export function returnNone() {
 }
 
 export function returnVal(val1?: number, val2?: number) {
-    return val1 !== undefined ? val1 : val2 !== undefined ? val2 : 0;
+    return val1 ? val1 : val2 !== undefined ? val2 : 0;
 }
 
 export function returnOne() {
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 966cd6a68..3be476e68 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1195,12 +1195,14 @@ export namespace Docs {
                     },
                 ],
             };
-            return DockDocument(
+            const doc = DockDocument(
                 configs.map(c => c.doc),
                 JSON.stringify(layoutConfig),
                 options,
                 id
             );
+            configs.map(c => (c.doc.embedContainer = doc));
+            return doc;
         }
 
         export function DelegateDocument(proto: Doc, options: DocumentOptions = {}) {
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index c8bc26b93..fb4a8985c 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -174,6 +174,13 @@ export namespace DragManager {
         userDropAction: dropActionType;
     }
 
+    let defaultPreDropFunc = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
+        if (de.complete.docDragData) {
+            targetAction && (de.complete.docDragData.dropAction = targetAction);
+            e.stopPropagation();
+        }
+    };
+
     export function MakeDropTarget(element: HTMLElement, dropFunc: (e: Event, de: DropEvent) => void, doc?: Doc, preDropFunc?: (e: Event, de: DropEvent, targetAction: dropActionType) => void): DragDropDisposer {
         if ('canDrop' in element.dataset) {
             throw new Error("Element is already droppable, can't make it droppable again");
@@ -182,7 +189,7 @@ export namespace DragManager {
         const handler = (e: Event) => dropFunc(e, (e as CustomEvent<DropEvent>).detail);
         const preDropHandler = (e: Event) => {
             const de = (e as CustomEvent<DropEvent>).detail;
-            preDropFunc?.(e, de, StrCast(doc?.targetDropAction) as dropActionType);
+            (preDropFunc ?? defaultPreDropFunc)(e, de, StrCast(doc?.targetDropAction) as dropActionType);
         };
         element.addEventListener('dashOnDrop', handler);
         doc && element.addEventListener('dashPreDrop', preDropHandler);
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index b51c77cb2..fb41ca8af 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -366,7 +366,6 @@ export class DashboardView extends React.Component {
         const title = name ? name : `Dashboard ${dashboardCount}`;
         const freeformDoc = Doc.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
         const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: title }, id, 'row');
-        freeformDoc.embedContainer = dashboardDoc;
 
         // switching the tabs from the datadoc to the regular doc
         const dashboardTabs = DocListCast(dashboardDoc[DataSym].data);
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index beddc0e8a..72c6d449a 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -189,7 +189,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
     @action
     onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => {
         const dragDocView = SelectionManager.Views()[0];
-        const containers = new Set<Doc|undefined>();
+        const containers = new Set<Doc | undefined>();
         SelectionManager.Views().forEach(v => containers.add(DocCast(v.rootDoc.embedContainer)));
         if (containers.size > 1) return false;
         const { left, top } = dragDocView.getBounds() || { left: 0, top: 0 };
@@ -274,19 +274,19 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         if (selectedDocs.length) {
             if (e.ctrlKey) {
                 // open an embedding in a new tab with Ctrl Key
-                CollectionDockingView.AddSplit(Doc.BestEmbedding(selectedDocs[0].props.Document), OpenWhereMod.right);
+                CollectionDockingView.AddSplit(Doc.BestEmbedding(selectedDocs[0].rootDoc), OpenWhereMod.right);
             } else if (e.shiftKey) {
                 // open centered in a new workspace with Shift Key
-                const embedding = Doc.MakeEmbedding(selectedDocs[0].props.Document);
+                const embedding = Doc.MakeEmbedding(selectedDocs[0].rootDoc);
                 embedding.embedContainer = undefined;
                 embedding.x = -embedding[WidthSym]() / 2;
                 embedding.y = -embedding[HeightSym]() / 2;
                 CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([embedding], { title: 'Tab for ' + embedding.title }), OpenWhereMod.right);
             } else if (e.altKey) {
                 // open same document in new tab
-                CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, OpenWhereMod.right);
+                CollectionDockingView.ToggleSplit(selectedDocs[0].rootDoc, OpenWhereMod.right);
             } else {
-                var openDoc = selectedDocs[0].props.Document;
+                var openDoc = selectedDocs[0].rootDoc;
                 if (openDoc.layout_fieldKey === 'layout_icon') {
                     openDoc = DocListCast(openDoc.proto_embeddings).find(embedding => !embedding.embedContainer) ?? Doc.MakeEmbedding(openDoc);
                     Doc.deiconifyView(openDoc);
@@ -294,7 +294,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                 LightboxView.SetLightboxDoc(
                     openDoc,
                     undefined,
-                    selectedDocs.slice(1).map(view => view.props.Document)
+                    selectedDocs.slice(1).map(view => view.rootDoc)
                 );
             }
         }
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 625bc760d..9a78eb9fc 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -1,22 +1,19 @@
 import { random } from 'lodash';
 import { action, runInAction } from 'mobx';
-import { DateField } from '../../fields/DateField';
 import { Doc, DocListCast } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { InkTool } from '../../fields/InkField';
-import { List } from '../../fields/List';
 import { ScriptField } from '../../fields/ScriptField';
-import { Cast, PromiseValue } from '../../fields/Types';
+import { Cast, DocCast, NumCast, PromiseValue } from '../../fields/Types';
 import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
 import { DocServer } from '../DocServer';
-import { DocumentType } from '../documents/DocumentTypes';
 import { DragManager } from '../util/DragManager';
 import { GroupManager } from '../util/GroupManager';
 import { SelectionManager } from '../util/SelectionManager';
 import { SettingsManager } from '../util/SettingsManager';
 import { SharingManager } from '../util/SharingManager';
 import { SnappingManager } from '../util/SnappingManager';
-import { undoBatch, UndoManager } from '../util/UndoManager';
+import { undoable, UndoManager } from '../util/UndoManager';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { CollectionFreeFormViewChrome } from './collections/CollectionMenu';
@@ -350,38 +347,13 @@ export class KeyManager {
     });
 
     public paste(e: ClipboardEvent) {
-        const plain = e.clipboardData?.getData('text/plain');
-        const clone = plain?.startsWith('__DashCloneId(');
-        if (plain && (plain.startsWith('__DashDocId(') || clone)) {
-            const first = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
-            if (first?.props.Document.type === DocumentType.COL) {
-                const docids = plain.split(':');
-                let count = 1;
-                const list: Doc[] = [];
-                const targetDataDoc = Doc.GetProto(first.props.Document);
-                const fieldKey = first.LayoutFieldKey;
-                const docList = DocListCast(targetDataDoc[fieldKey]);
-                docids.map(
-                    (did, i) =>
-                        i &&
-                        DocServer.GetRefField(did).then(async doc => {
-                            count++;
-                            if (doc instanceof Doc) {
-                                list.push(doc);
-                            }
-                            if (count === docids.length) {
-                                const added = await Promise.all(list.filter(d => !docList.includes(d)).map(async d => (clone ? (await Doc.MakeClone(d, true)).clone : d)));
-                                if (added.length) {
-                                    added.map(doc => (doc.embedContainer = targetDataDoc));
-                                    undoBatch(() => {
-                                        targetDataDoc[fieldKey] = new List<Doc>([...docList, ...added]);
-                                        targetDataDoc[fieldKey + '_modificationDate'] = new DateField(new Date(Date.now()));
-                                    })();
-                                }
-                            }
-                        })
-                );
-            }
+        const plain = e.clipboardData?.getData('text/plain'); // list of document ids, separated by ':'s
+        if (!plain) return;
+        const clone = plain.startsWith('__DashCloneId(');
+        const docids = plain.split(':'); // hack!  docids[0] is the top left of the selection rectangle
+        const addDocument = SelectionManager.Views().lastElement()?.ComponentView?.addDocument;
+        if (addDocument && (plain.startsWith('__DashDocId(') || clone)) {
+            Doc.Paste(docids.slice(1), clone, addDocument);
         }
     }
 
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index f7fb443c0..8dd13ff08 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -1,14 +1,15 @@
 import { action, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
+import { listenerCount } from 'process';
 import * as React from 'react';
 import { Doc } from '../../fields/Doc';
-import { Cast, NumCast, StrCast } from '../../fields/Types';
+import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { returnFalse } from '../../Utils';
 import { DocServer } from '../DocServer';
 import { Docs, DocumentOptions, DocUtils } from '../documents/Documents';
 import { ImageUtils } from '../util/Import & Export/ImageUtils';
 import { Transform } from '../util/Transform';
-import { undoBatch, UndoManager } from '../util/UndoManager';
+import { undoable, undoBatch, UndoManager } from '../util/UndoManager';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import './PreviewCursor.scss';
 
@@ -66,23 +67,11 @@ export class PreviewCursor extends React.Component<{}> {
                 } else if (plain.startsWith('__DashDocId(') || plain.startsWith('__DashCloneId(')) {
                     const clone = plain.startsWith('__DashCloneId(');
                     const docids = plain.split(':');
-                    const strs = docids[0].split(',');
+                    const strs = docids[0].split(','); // hack!  docids[0] is the top left of the selection rectangle
                     const ptx = Number(strs[0].substring((clone ? '__DashCloneId(' : '__DashDocId(').length));
                     const pty = Number(strs[1].substring(0, strs[1].length - 1));
+                    Doc.Paste(docids.slice(1), clone, PreviewCursor._addDocument, ptx, pty, newPoint);
 
-                    const batch = UndoManager.StartBatch('cloning');
-                    {
-                        const toCopy = await Promise.all(docids.slice(1).map(async did => Cast(await DocServer.GetRefField(did), Doc, null)));
-                        const docs = clone ? (await Promise.all(Doc.MakeClones(toCopy, false))).map(res => res.clone) : toCopy;
-                        const firstx = docs.length ? NumCast(docs[0].x) + ptx - newPoint[0] : 0;
-                        const firsty = docs.length ? NumCast(docs[0].y) + pty - newPoint[1] : 0;
-                        docs.map(doc => {
-                            doc.x = NumCast(doc.x) - firstx;
-                            doc.y = NumCast(doc.y) - firsty;
-                        });
-                        PreviewCursor._addDocument(docs);
-                    }
-                    batch.end();
                     e.stopPropagation();
                 } else {
                     FormattedTextBox.PasteOnLoad = e;
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index a5c58c9d2..49e8ed369 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -414,7 +414,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                 {toggle(this.onClickButton)}
                 {toggle(this.layout_fitWidthButton)}
                 {toggle(this.freezeThumb)}
-                {toggle(this.forceActiveButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
+                {toggle(this.forceActiveButton)}
                 {toggle(this.fitContentButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
                 {toggle(this.isLightboxButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
                 {toggle(this.layout_autoHeightButton, { display: !isText && !isStacking && !isTree ? 'none' : '' })}
diff --git a/src/client/views/collections/CollectionCarousel3DView.scss b/src/client/views/collections/CollectionCarousel3DView.scss
index 5c8b491eb..6bd1d9f5f 100644
--- a/src/client/views/collections/CollectionCarousel3DView.scss
+++ b/src/client/views/collections/CollectionCarousel3DView.scss
@@ -1,3 +1,4 @@
+@import '../global/globalCssVariables';
 .collectionCarousel3DView-outer {
     height: 100%;
     position: relative;
@@ -7,8 +8,8 @@
 .carousel-wrapper {
     display: flex;
     position: absolute;
-    top: 15%;
-    height: 60%;
+    top: $CAROUSEL3D_TOP * 1%;
+    height: ($CAROUSEL3D_SIDE_SCALE * 100) * 1%;
     align-items: center;
     transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955);
 
@@ -17,10 +18,17 @@
         flex: 1;
         transition: opacity 0.3s linear, transform 0.5s cubic-bezier(0.455, 0.03, 0.515, 0.955);
         pointer-events: none;
+        opacity: 0.5;
+        z-index: 1;
+        transform: scale($CAROUSEL3D_SIDE_SCALE);
+        user-select: none;
     }
 
     .collectionCarousel3DView-item-active {
         pointer-events: unset;
+        opacity: 1;
+        z-index: 2;
+        transform: scale($CAROUSEL3D_CENTER_SCALE);
     }
 }
 
@@ -105,4 +113,4 @@
 .carousel3DView-back-scroll:hover,
 .carousel3DView-fwd-scroll:hover {
     background: lightgray;
-}
\ No newline at end of file
+}
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index b10b453bf..d94e552b4 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -7,6 +7,8 @@ import { Id } from '../../../fields/FieldSymbols';
 import { NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { returnFalse, returnZero, Utils } from '../../../Utils';
 import { DragManager } from '../../util/DragManager';
+import { SelectionManager } from '../../util/SelectionManager';
+import { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } from '../global/globalCssVariables.scss';
 import { DocumentView } from '../nodes/DocumentView';
 import { StyleProp } from '../StyleProvider';
 import './CollectionCarousel3DView.scss';
@@ -32,11 +34,17 @@ export class CollectionCarousel3DView extends CollectionSubView() {
         }
     };
 
+    centerScale = Number(CAROUSEL3D_CENTER_SCALE);
     panelWidth = () => this.props.PanelWidth() / 3;
-    panelHeight = () => this.props.PanelHeight() * 0.6;
+    panelHeight = () => this.props.PanelHeight() * Number(CAROUSEL3D_SIDE_SCALE);
     onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
     isContentActive = () => this.props.isSelected() || this.props.isContentActive() || this.props.isAnyChildContentActive();
     isChildContentActive = () => (this.isContentActive() ? true : false);
+    childScreenToLocal = () =>
+        this.props // document's left is the panel shifted by the doc's index * panelWidth/#docs.  But it scales by centerScale around its center, so it's left moves left by the distance of the left from the center (panelwidth/2) * the scale delta (centerScale-1)
+            .ScreenToLocalTransform() // the top behaves the same way ecept it's shifted by the 'top' amount specified for the panel in css and then by the scale factor.
+            .translate(-this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, -((Number(CAROUSEL3D_TOP) / 100) * this.props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2)
+            .scale(1 / this.centerScale);
 
     @computed get content() {
         const currentIndex = NumCast(this.layoutDoc._carousel_index);
@@ -46,13 +54,14 @@ export class CollectionCarousel3DView extends CollectionSubView() {
                     {...this.props}
                     NativeWidth={returnZero}
                     NativeHeight={returnZero}
-                    suppressSetHeight={true}
+                    //suppressSetHeight={true}
                     onDoubleClick={this.onChildDoubleClick}
                     renderDepth={this.props.renderDepth + 1}
                     LayoutTemplate={this.props.childLayoutTemplate}
                     LayoutTemplateString={this.props.childLayoutString}
                     Document={childPair.layout}
                     DataDoc={childPair.data}
+                    ScreenToLocalTransform={this.childScreenToLocal}
                     isContentActive={this.isChildContentActive}
                     isDocumentActive={this.props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this.props.isDocumentActive : this.isContentActive}
                     PanelWidth={this.panelWidth}
@@ -64,10 +73,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
 
         return this.childLayoutPairs.map((childPair, index) => {
             return (
-                <div
-                    key={childPair.layout[Id]}
-                    className={`collectionCarousel3DView-item${index === currentIndex ? '-active' : ''} ${index}`}
-                    style={index === currentIndex ? { opacity: '1', transform: 'scale(1.3)', width: this.panelWidth() } : { opacity: '0.5', transform: 'scale(0.6)', userSelect: 'none', width: this.panelWidth() }}>
+                <div key={childPair.layout[Id]} className={`collectionCarousel3DView-item${index === currentIndex ? '-active' : ''} ${index}`} style={{ width: this.panelWidth() }}>
                     {displayDoc(childPair)}
                 </div>
             );
@@ -75,6 +81,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
     }
 
     changeSlide = (direction: number) => {
+        SelectionManager.DeselectAll();
         this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + direction + this.childLayoutPairs.length) % this.childLayoutPairs.length;
     };
 
@@ -114,10 +121,10 @@ export class CollectionCarousel3DView extends CollectionSubView() {
         return (
             <div className="arrow-buttons">
                 <div key="back" className="carousel3DView-back" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={e => this.onArrowClick(e, -1)}>
-                    <FontAwesomeIcon icon={'angle-left'} size={'2x'} />
+                    <FontAwesomeIcon icon="angle-left" size={'2x'} />
                 </div>
                 <div key="fwd" className="carousel3DView-fwd" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={e => this.onArrowClick(e, 1)}>
-                    <FontAwesomeIcon icon={'angle-right'} size={'2x'} />
+                    <FontAwesomeIcon icon="angle-right" size={'2x'} />
                 </div>
                 {this.autoScrollButton}
             </div>
@@ -141,11 +148,12 @@ export class CollectionCarousel3DView extends CollectionSubView() {
     @computed get dots() {
         return this.childLayoutPairs.map((_child, index) => <div key={Utils.GenerateGuid()} className={`dot${index === NumCast(this.layoutDoc._carousel_index) ? '-active' : ''}`} onClick={() => (this.layoutDoc._carousel_index = index)} />);
     }
-
-    render() {
+    @computed get translateX() {
         const index = NumCast(this.layoutDoc._carousel_index);
-        const translateX = this.panelWidth() * (1 - index);
+        return this.panelWidth() * (1 - index);
+    }
 
+    render() {
         return (
             <div
                 className="collectionCarousel3DView-outer"
@@ -154,7 +162,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
                     background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
                     color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color),
                 }}>
-                <div className="carousel-wrapper" style={{ transform: `translateX(${translateX}px)` }}>
+                <div className="carousel-wrapper" style={{ transform: `translateX(${this.translateX}px)` }}>
                     {this.content}
                 </div>
                 {this.props.Document._chromeHidden ? null : this.buttons}
diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss
index 3496bb835..422dae15b 100644
--- a/src/client/views/global/globalCssVariables.scss
+++ b/src/client/views/global/globalCssVariables.scss
@@ -70,6 +70,10 @@ $DFLT_IMAGE_NATIVE_DIM: 900px;
 $LEFT_MENU_WIDTH: 60px;
 $TREE_BULLET_WIDTH: 20px;
 
+$CAROUSEL3D_CENTER_SCALE: 1.3;
+$CAROUSEL3D_SIDE_SCALE: 0.3;
+$CAROUSEL3D_TOP: 15;
+
 :export {
     contextMenuZindex: $contextMenu-zindex;
     SCHEMA_DIVIDER_WIDTH: $SCHEMA_DIVIDER_WIDTH;
@@ -84,4 +88,7 @@ $TREE_BULLET_WIDTH: 20px;
     TREE_BULLET_WIDTH: $TREE_BULLET_WIDTH;
     INK_MASK_SIZE: $INK_MASK_SIZE;
     MEDIUM_GRAY: $medium-gray;
+    CAROUSEL3D_CENTER_SCALE: $CAROUSEL3D_CENTER_SCALE;
+    CAROUSEL3D_SIDE_SCALE: $CAROUSEL3D_SIDE_SCALE;
+    CAROUSEL3D_TOP: $CAROUSEL3D_TOP;
 }
diff --git a/src/client/views/global/globalCssVariables.scss.d.ts b/src/client/views/global/globalCssVariables.scss.d.ts
index 537ea1e5d..efb702564 100644
--- a/src/client/views/global/globalCssVariables.scss.d.ts
+++ b/src/client/views/global/globalCssVariables.scss.d.ts
@@ -12,6 +12,9 @@ interface IGlobalScss {
     TREE_BULLET_WIDTH: string;
     INK_MASK_SIZE: number;
     MEDIUM_GRAY: string;
+    CAROUSEL3D_CENTER_SCALE: string;
+    CAROUSEL3D_SIDE_SCALE: string;
+    CAROUSEL3D_TOP: string;
 }
 declare const globalCssVariables: IGlobalScss;
 
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 3abe7a331..1cc09a63c 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -41,18 +41,17 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
         this._disposers[disposerId]?.();
         if (ele) {
             // create disposers identified by disposerId to remove drag & drop listeners
-            this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.dropHandler(e, dropEvent, fieldKey), this.layoutDoc);
+            this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.internalDrop(e, dropEvent, fieldKey), this.layoutDoc);
         }
     };
 
     @undoBatch
-    private dropHandler = (event: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => {
+    private internalDrop = (event: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => {
         if (dropEvent.complete.docDragData) {
             event.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place
             const droppedDocs = dropEvent.complete.docDragData?.droppedDocuments;
-            if (droppedDocs?.length) {
-                this.dataDoc[fieldKey] = droppedDocs[0];
-            }
+            droppedDocs.lastElement().embedContainer = this.dataDoc;
+            this.dataDoc[fieldKey] = droppedDocs.lastElement();
         }
     };
 
@@ -130,9 +129,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
             return whichDoc ? (
                 <>
                     <DocumentView
-                        ref={r => {
-                            //whichDoc !== targetDoc && r?.focus(whichDoc, { instant: true });
-                        }}
                         {...this.props}
                         NativeWidth={returnZero}
                         NativeHeight={returnZero}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index ab285b230..cc105326f 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -38,7 +38,6 @@ import { EditableView } from '../EditableView';
 import { GestureOverlay } from '../GestureOverlay';
 import { InkingStroke } from '../InkingStroke';
 import { LightboxView } from '../LightboxView';
-import { OverlayView } from '../OverlayView';
 import { StyleProp } from '../StyleProvider';
 import { UndoStack } from '../UndoStack';
 import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView';
@@ -117,6 +116,7 @@ export interface DocComponentView {
     brushView?: (view: { width: number; height: number; panX: number; panY: number }) => void;
     getView?: (doc: Doc) => Promise<Opt<DocumentView>>; // returns a nested DocumentView for the specified doc or undefined
     addDocTab?: (doc: Doc, where: OpenWhere) => boolean; // determines how to add a document - used in following links to open the target ina local lightbox
+    addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; // add a document (used only by collections)
     reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height.  However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling.
     shrinkWrap?: () => void; // requests a document to display all of its contents with no white space.  currently only implemented (needed?) for freeform views
     select?: (ctrlKey: boolean, shiftKey: boolean) => void;
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 128cfffb2..8dd322e05 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -9,7 +9,7 @@ import { LinkManager } from '../client/util/LinkManager';
 import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals';
 import { SelectionManager } from '../client/util/SelectionManager';
 import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from '../client/util/SerializationHelper';
-import { UndoManager } from '../client/util/UndoManager';
+import { undoable, UndoManager } from '../client/util/UndoManager';
 import { decycle } from '../decycler/decycler';
 import { DashColor, incrementTitleCopy, intersectRect, Utils } from '../Utils';
 import { DateField } from './DateField';
@@ -211,14 +211,14 @@ export class Doc extends RefField {
     public static get MyTrails() {
         return DocCast(Doc.ActiveDashboard?.myTrails);
     }
-    public static IsInMyOverlay(doc:Doc) {
+    public static IsInMyOverlay(doc: Doc) {
         return DocListCast(Doc.MyOverlayDocs?.data).includes(doc);
     }
-    public static AddToMyOverlay(doc:Doc) {
+    public static AddToMyOverlay(doc: Doc) {
         Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc);
     }
-    public static RemFromMyOverlay(doc:Doc) {
-        Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc)
+    public static RemFromMyOverlay(doc: Doc) {
+        Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc);
     }
     public static get MyOverlayDocs() {
         return DocCast(Doc.UserDoc().myOverlayDocs);
@@ -1520,6 +1520,24 @@ export namespace Doc {
         return style;
     }
 
+    export function Paste(docids: string[], clone: boolean, addDocument: (doc: Doc | Doc[]) => boolean, ptx?: number, pty?: number, newPoint?: number[]) {
+        DocServer.GetRefFields(docids).then(async fieldlist => {
+            const list = Array.from(Object.values(fieldlist))
+                .map(d => DocCast(d))
+                .filter(d => d);
+            const docs = clone ? (await Promise.all(Doc.MakeClones(list, false))).map(res => res.clone) : list;
+            if (ptx !== undefined && pty !== undefined && newPoint !== undefined) {
+                const firstx = list.length ? NumCast(list[0].x) + ptx - newPoint[0] : 0;
+                const firsty = list.length ? NumCast(list[0].y) + pty - newPoint[1] : 0;
+                docs.map(doc => {
+                    doc.x = NumCast(doc.x) - firstx;
+                    doc.y = NumCast(doc.y) - firsty;
+                });
+            }
+            undoable(addDocument, 'Paste Doc')(docs); // embedContainer gets set in addDocument
+        });
+    }
+
     // prettier-ignore
     export function toIcon(doc?: Doc, isOpen?: boolean) {
         switch (StrCast(doc?.type)) {
-- 
cgit v1.2.3-70-g09d2

