From 51d7ce5f71465f2f578a08a998b2df353242ff4d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 5 Apr 2022 13:54:26 -0400
Subject: fixes to allow dragging golden layout windows to be aborted with
 'esc'.  some code cleanup as well to avoid rebuilding golden layout when tabs
 are closed.  Fixes for undo and goldenlayout

---
 .../views/collections/CollectionDockingView.tsx    | 109 +++++++++++++--------
 1 file changed, 70 insertions(+), 39 deletions(-)

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

diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 9e8374605..6931d9896 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -11,6 +11,7 @@ import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { Cast, NumCast, StrCast } from "../../../fields/Types";
 import { inheritParentAcls } from '../../../fields/util';
+import { emptyFunction } from '../../../Utils';
 import { DocServer } from "../../DocServer";
 import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
@@ -46,7 +47,7 @@ export class CollectionDockingView extends CollectionSubView() {
     private _reactionDisposer?: IReactionDisposer;
     private _lightboxReactionDisposer?: IReactionDisposer;
     private _containerRef = React.createRef<HTMLDivElement>();
-    private _flush: UndoManager.Batch | undefined;
+    public _flush: UndoManager.Batch | undefined;
     private _ignoreStateChange = "";
     public tabMap: Set<any> = new Set();
     public get initialized() { return this._goldenLayout !== null; }
@@ -62,15 +63,37 @@ export class CollectionDockingView extends CollectionSubView() {
         DragManager.StartWindowDrag = this.StartOtherDrag;
     }
 
-    public StartOtherDrag = (e: any, dragDocs: Doc[]) => {
-        !this._flush && (this._flush = UndoManager.StartBatch("golden layout drag"));
+    /**
+     * Switches from dragging a document around a freeform canvas to dragging it as a tab to be docked.
+     * 
+     * @param e fake mouse down event position data containing pageX and pageY coordinates
+     * @param dragDocs the documents to be dragged
+     * @param batch optionally an undo batch that has been started to use instead of starting a new batch
+    */
+    public StartOtherDrag = (e: { pageX: number, pageY: number }, dragDocs: Doc[], finishDrag?: (aborted: boolean) => void) => {
+        this._flush = this._flush ?? UndoManager.StartBatch("golden layout drag");
         const config = dragDocs.length === 1 ? CollectionDockingView.makeDocumentConfig(dragDocs[0]) :
-            { type: 'row', content: dragDocs.map((doc, i) => CollectionDockingView.makeDocumentConfig(doc)) };
+            { type: 'row', content: dragDocs.map(doc => CollectionDockingView.makeDocumentConfig(doc)) };
         const dragSource = this._goldenLayout.createDragSource(document.createElement("div"), config);
-        //dragSource._dragListener.on("dragStop", dragSource.destroy);
-        dragSource._dragListener.onMouseDown(e);
+        this.tabDragStart(dragSource, finishDrag);
+        dragSource._dragListener.onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 });
     }
 
+    tabItemDropped = () => DragManager.CompleteWindowDrag?.(false);
+    tabDragStart = (proxy: any, finishDrag?: (aborted: boolean) => void) => {
+        DragManager.CompleteWindowDrag = (aborted: boolean) => {
+            if (aborted) {
+                proxy._dragListener.AbortDrag();
+                if (this._flush) {
+                    this._flush.cancel();  // cancel the undo change being logged
+                    this._flush = undefined;
+                    this.setupGoldenLayout(); // restore golden layout to where it was before the drag (this is a no-op when using StartOtherDrag because the proxy dragged item was never in the golden layout)
+                }
+                DragManager.CompleteWindowDrag = undefined;
+            }
+            finishDrag?.(aborted);
+        }
+    }
     @undoBatch
     public CloseFullScreen = () => {
         this._goldenLayout._maximisedItem?.toggleMaximise();
@@ -106,7 +129,6 @@ export class CollectionDockingView extends CollectionSubView() {
         docconfig.callDownwards('_$init');
         instance._goldenLayout._$maximiseItem(docconfig);
         instance._goldenLayout.emit('stateChanged');
-        instance._ignoreStateChange = JSON.stringify(instance._goldenLayout.toConfig());
         instance.stateChanged();
         return true;
     }
@@ -255,7 +277,6 @@ export class CollectionDockingView extends CollectionSubView() {
     layoutChanged() {
         this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]);
         this._goldenLayout.emit('stateChanged');
-        this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
         this.stateChanged();
         return true;
     }
@@ -294,6 +315,9 @@ export class CollectionDockingView extends CollectionSubView() {
                 }
             }
             this._goldenLayout.init();
+            this._goldenLayout.root.layoutManager.on('itemDropped', this.tabItemDropped);
+            this._goldenLayout.root.layoutManager.on('dragStart', this.tabDragStart)
+            this._goldenLayout.root.layoutManager.on('activeContentItemChanged', this.stateChanged);
         }
     }
 
@@ -335,13 +359,12 @@ export class CollectionDockingView extends CollectionSubView() {
     @action
     onPointerUp = (e: MouseEvent): void => {
         window.removeEventListener("pointerup", this.onPointerUp);
-        if (this._flush) {
-            setTimeout(() => {
-                CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig());
-                this.stateChanged();
-                this._flush?.end();
-                this._flush = undefined;
-            }, 10);
+        const flush = this._flush;
+        this._flush = undefined;
+        if (flush) {
+            DragManager.CompleteWindowDrag = undefined;
+            if (!this.stateChanged()) flush.cancel();
+            else flush.end();
         }
     }
 
@@ -352,9 +375,12 @@ export class CollectionDockingView extends CollectionSubView() {
             hitFlyout = (par.className === "dockingViewButtonSelector");
         }
         if (!hitFlyout) {
+            const htmlTarget = e.target as HTMLElement;
             window.addEventListener("mouseup", this.onPointerUp);
-            if (!(e.target as HTMLElement).closest("*.lm_content") && ((e.target as HTMLElement).closest("*.lm_tab") || (e.target as HTMLElement).closest("*.lm_stack"))) {
-                this._flush = UndoManager.StartBatch("golden layout edit");
+            if (!htmlTarget.closest("*.lm_content") && (htmlTarget.closest("*.lm_tab") || htmlTarget.closest("*.lm_stack"))) {
+                if (htmlTarget.className !== "lm_close_tab") {
+                    this._flush = UndoManager.StartBatch("golden layout edit");
+                }
             }
         }
         if (!e.nativeEvent.cancelBubble && !InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) &&
@@ -388,38 +414,43 @@ export class CollectionDockingView extends CollectionSubView() {
 
     @action
     stateChanged = () => {
+        this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
         const json = JSON.stringify(this._goldenLayout.toConfig());
         const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g);
         const docids = matches?.map(m => m.replace("\"documentId\":\"", "").replace("\"", ""));
         const docs = !docids ? [] : docids.map(id => DocServer.GetCachedRefField(id)).filter(f => f).map(f => f as Doc);
-
-        this.props.Document.dockingConfig = json;
-        setTimeout(async () => {
-            const sublists = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
-            const tabs = sublists && Cast(sublists[0], Doc, null);
-            // const other = sublists && Cast(sublists[1], Doc, null);
-            const tabdocs = await DocListCastAsync(tabs?.data);
-            // const otherdocs = await DocListCastAsync(other?.data);
-            if (tabs) {
-                tabs.data = new List<Doc>(docs);
-                // DocListCast(tabs.aliases).forEach(tab => tab !== tabs && (tab.data = new List<Doc>(docs)));
-            }
-            // const otherSet = new Set<Doc>();
-            // otherdocs?.filter(doc => !docs.includes(doc)).forEach(doc => otherSet.add(doc));
-            // tabdocs?.filter(doc => !docs.includes(doc) && doc.type !== DocumentType.KVP).forEach(doc => otherSet.add(doc));
-            // const vals = Array.from(otherSet.values()).filter(val => val instanceof Doc).map(d => d).filter(d => d.type !== DocumentType.KVP);
-            // this.props.Document[DataSym][this.props.fieldKey + "-all"] = new List<Doc>([...docs, ...vals]);
-            // if (other) {
-            //     other.data = new List<Doc>(vals);
-            //     // DocListCast(other.aliases).forEach(tab => tab !== other && (tab.data = new List<Doc>(vals)));
-            // }
-        }, 0);
+        const changesMade = this.props.Document.dockingConfig !== json;
+        if (changesMade && !this._flush) {
+            this.props.Document.dockingConfig = json;
+            setTimeout(async () => {
+                const sublists = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
+                const tabs = sublists && Cast(sublists[0], Doc, null);
+                // const other = sublists && Cast(sublists[1], Doc, null);
+                const tabdocs = await DocListCastAsync(tabs?.data);
+                // const otherdocs = await DocListCastAsync(other?.data);
+                if (tabs) {
+                    tabs.data = new List<Doc>(docs);
+                    // DocListCast(tabs.aliases).forEach(tab => tab !== tabs && (tab.data = new List<Doc>(docs)));
+                }
+                // const otherSet = new Set<Doc>();
+                // otherdocs?.filter(doc => !docs.includes(doc)).forEach(doc => otherSet.add(doc));
+                // tabdocs?.filter(doc => !docs.includes(doc) && doc.type !== DocumentType.KVP).forEach(doc => otherSet.add(doc));
+                // const vals = Array.from(otherSet.values()).filter(val => val instanceof Doc).map(d => d).filter(d => d.type !== DocumentType.KVP);
+                // this.props.Document[DataSym][this.props.fieldKey + "-all"] = new List<Doc>([...docs, ...vals]);
+                // if (other) {
+                //     other.data = new List<Doc>(vals);
+                //     // DocListCast(other.aliases).forEach(tab => tab !== other && (tab.data = new List<Doc>(vals)));
+                // }
+            }, 0);
+        }
+        return changesMade;
     }
 
     tabDestroyed = (tab: any) => {
         this.tabMap.delete(tab);
         tab._disposers && Object.values(tab._disposers).forEach((disposer: any) => disposer?.());
         tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele));
+        this.stateChanged();
     }
     tabCreated = (tab: any) => {
         tab.contentItem.element[0]?.firstChild?.firstChild?.InitTab?.(tab);  // have to explicitly initialize tabs that reuse contents from previous tabs (ie, when dragging a tab around a new tab is created for the old content)
-- 
cgit v1.2.3-70-g09d2

