diff --git a/.changeset/polite-tigers-work.md b/.changeset/polite-tigers-work.md
new file mode 100644
index 0000000..d6847cb
--- /dev/null
+++ b/.changeset/polite-tigers-work.md
@@ -0,0 +1,7 @@
+---
+'@projectstorm/react-workspaces-dropzone-plugin-tray': patch
+---
+
+Disable vertical split drop targets for trays while retaining tray transform targets.
+
+Fix moving a collapsed-tray panel into another collection so the tray releases its floating-window ownership first.
diff --git a/.changeset/soft-bugs-relax.md b/.changeset/soft-bugs-relax.md
new file mode 100644
index 0000000..9347037
--- /dev/null
+++ b/.changeset/soft-bugs-relax.md
@@ -0,0 +1,7 @@
+---
+'@projectstorm/react-workspaces-core': patch
+'@projectstorm/react-workspaces-model-floating-window': patch
+'@projectstorm/react-workspaces-model-tray': patch
+---
+
+Move the content of floating tray windows, rather than the window shell, when dropping them into another collection. Release tray ownership first to prevent stale layout state during tray-to-tab drops.
diff --git a/demo/stories/RootTabSplit.stories.tsx b/demo/stories/RootTabSplit.stories.tsx
new file mode 100644
index 0000000..affb3d7
--- /dev/null
+++ b/demo/stories/RootTabSplit.stories.tsx
@@ -0,0 +1,33 @@
+import * as React from 'react';
+import { useState } from 'react';
+import 'typeface-open-sans';
+import { ExpandNodeModel } from '@projectstorm/react-workspaces-core';
+import { DefaultWorkspacePanelModel } from '@projectstorm/react-workspaces-defaults';
+import { WorkspaceTabModel } from '@projectstorm/react-workspaces-model-tabs';
+import { CompInternal, SharedArgs, StoryArgs, useEngine } from './helpers/tools';
+
+export const RootTabSplit = Object.assign(
+ function RootTabSplit(args: StoryArgs) {
+ const engine = useEngine(args);
+ const [model] = useState(() => {
+ const tabs = new WorkspaceTabModel();
+ tabs.addModel(new DefaultWorkspacePanelModel('Tab 1'));
+ tabs.addModel(new DefaultWorkspacePanelModel('Tab 2'));
+
+ return new ExpandNodeModel()
+ .setHorizontal(true)
+ .addModel(new DefaultWorkspacePanelModel('Left panel'))
+ .addModel(tabs);
+ });
+
+ return ;
+ },
+ { args: SharedArgs }
+);
+
+export default {
+ title: 'Workspace',
+ parameters: {
+ layout: 'fullscreen'
+ }
+};
diff --git a/demo/stories/helpers/tools.tsx b/demo/stories/helpers/tools.tsx
index 3bbe778..507bb84 100644
--- a/demo/stories/helpers/tools.tsx
+++ b/demo/stories/helpers/tools.tsx
@@ -25,7 +25,6 @@ import {
} from '@projectstorm/react-workspaces-behavior-panel-dropzone';
import { draggingItemDividerBehavior } from '@projectstorm/react-workspaces-behavior-divider-dropzone';
import { WorkspaceTabFactory } from '@projectstorm/react-workspaces-model-tabs';
-import { WorkspaceTrayModel } from '@projectstorm/react-workspaces-model-tray';
import { resizingBehavior } from '@projectstorm/react-workspaces-behavior-resize';
import { RootWorkspaceModel } from '@projectstorm/react-workspaces-model-floating-window';
import { ConvertToTabZone, getDirectiveForTabModel } from '@projectstorm/react-workspaces-dropzone-plugin-tabs';
@@ -112,12 +111,7 @@ export const useEngine = (args: StoryArgs = SharedArgs) => {
engine: e,
getDropZoneForModel: (model) => {
return (
- getDirectiveForTrayModel(
- model,
- [],
- () => new ExpandNodeModel(),
- !(e.rootModel.flatten().find((candidate) => candidate.id === e.draggingID) instanceof WorkspaceTrayModel)
- ) ||
+ getDirectiveForTrayModel(model) ||
getDirectiveForWorkspaceNode({
node: model,
transformZones: [ConvertToTabZone(tabFactory), ConvertToTrayZone(trayFactory)],
diff --git a/packages/core/src/core-models/WorkspaceCollectionModel.ts b/packages/core/src/core-models/WorkspaceCollectionModel.ts
index be7d68d..5ecfd71 100644
--- a/packages/core/src/core-models/WorkspaceCollectionModel.ts
+++ b/packages/core/src/core-models/WorkspaceCollectionModel.ts
@@ -130,6 +130,24 @@ export class WorkspaceCollectionModel<
this.children.splice(position - 1, 0, model);
}
} else {
+ // A model can be rendered by an intermediary (for example a tray's floating
+ // window) while still being owned by a collection elsewhere in the tree.
+ // Detach that owner before re-parenting so it cannot continue to render the
+ // same model with stale layout dimensions.
+ if (
+ model.parent &&
+ (!(model.parent instanceof WorkspaceCollectionModel) || !model.parent.children.includes(model))
+ ) {
+ const owner = model
+ .getRootModel()
+ .flatten()
+ .find(
+ (candidate) =>
+ candidate instanceof WorkspaceCollectionModel && candidate !== this && candidate.children.includes(model)
+ ) as WorkspaceCollectionModel | undefined;
+ owner?.removeModel(model);
+ }
+
if (model.parent) {
model.delete();
}
diff --git a/packages/core/src/core-models/WorkspaceModel.ts b/packages/core/src/core-models/WorkspaceModel.ts
index fa892f1..6539209 100644
--- a/packages/core/src/core-models/WorkspaceModel.ts
+++ b/packages/core/src/core-models/WorkspaceModel.ts
@@ -167,6 +167,15 @@ export class WorkspaceModel<
return [this.r_dimensions];
}
+ /**
+ * Returns the model that should be inserted when this model is dropped into
+ * a workspace collection. Models that merely present another model can
+ * override this to move their content instead of their presentation shell.
+ */
+ getDropModel(): WorkspaceModel {
+ return this;
+ }
+
invalidateDimensions() {
this.iterateListeners((cb) => cb.dimensionsInvalidated?.());
}
diff --git a/packages/core/src/widgets/hooks/dnd-model/useDraggableModel.ts b/packages/core/src/widgets/hooks/dnd-model/useDraggableModel.ts
index c227e48..8caf965 100644
--- a/packages/core/src/widgets/hooks/dnd-model/useDraggableModel.ts
+++ b/packages/core/src/widgets/hooks/dnd-model/useDraggableModel.ts
@@ -67,6 +67,8 @@ export const useDroppableModel = (props: UseDroppableModelOptions) => {
draggingNode = found || draggingNode;
}
+ draggingNode = draggingNode.getDropModel();
+
log(`workspace model dropped`, draggingNode);
props.onDrop(draggingNode);
},
diff --git a/packages/dropzone-plugin-tray/src/index.tsx b/packages/dropzone-plugin-tray/src/index.tsx
index ca1236b..8376c19 100644
--- a/packages/dropzone-plugin-tray/src/index.tsx
+++ b/packages/dropzone-plugin-tray/src/index.tsx
@@ -1,10 +1,5 @@
import * as React from 'react';
-import {
- Alignment,
- WorkspaceCollectionModel,
- WorkspaceModel,
- WorkspaceNodeModel
-} from '@projectstorm/react-workspaces-core';
+import { WorkspaceCollectionModel, WorkspaceModel } from '@projectstorm/react-workspaces-core';
import {
DropZoneLayerButtonWidget,
DropZonePanelDirective,
@@ -45,32 +40,12 @@ export const ConvertToTrayZone = (trayFactory: WorkspaceTrayFactory): TransformZ
export const getDirectiveForTrayModel = (
node: WorkspaceModel,
- transformZones: TransformZone[] = [],
- generateParentNode: () => WorkspaceNodeModel = () => new WorkspaceNodeModel(),
- allowSplit: boolean = true
+ transformZones: TransformZone[] = []
): DropZonePanelDirective | null => {
if (!(node instanceof WorkspaceCollectionModel) && node.parent instanceof WorkspaceTrayModel) {
- const tray = node.parent;
- const parent = tray.parent;
- const splitZones =
- allowSplit && parent instanceof WorkspaceNodeModel && parent === tray.getRootModel()
- ? [Alignment.TOP, Alignment.BOTTOM].map((alignment) => ({
- alignment,
- handleDrop: (model: WorkspaceModel, engine) => {
- const split = generateParentNode()
- .setVertical(true)
- .setExpand(tray.expandHorizontal, tray.expandVertical);
- parent.replaceModel(tray, split);
- split.addModel(tray);
- split.addModel(model, alignment === Alignment.TOP ? 0 : null);
- engine.normalize();
- }
- }))
- : [];
-
return {
transformZones: [AppendToTrayZone, ...transformZones],
- splitZones
+ splitZones: []
};
}
};
diff --git a/packages/model-floating-window/src/core/FloatingWindowModel.tsx b/packages/model-floating-window/src/core/FloatingWindowModel.tsx
index 7a5155c..ab8d4a3 100644
--- a/packages/model-floating-window/src/core/FloatingWindowModel.tsx
+++ b/packages/model-floating-window/src/core/FloatingWindowModel.tsx
@@ -56,6 +56,10 @@ export class FloatingWindowModel extends WorkspaceModel cb.draggableUpdated?.());
}
+ getDropModel(): WorkspaceModel {
+ return this.child || this;
+ }
+
fromArray(payload: SerializedFloatingWindowModel, engine: WorkspaceEngineInterface) {
super.fromArray(payload, engine);
this.dimension.update(payload.dimensions);
diff --git a/packages/model-tray/src/WorkspaceTrayModel.ts b/packages/model-tray/src/WorkspaceTrayModel.ts
index 5a2939f..d961e8e 100644
--- a/packages/model-tray/src/WorkspaceTrayModel.ts
+++ b/packages/model-tray/src/WorkspaceTrayModel.ts
@@ -183,6 +183,12 @@ export class WorkspaceTrayModel extends WorkspaceCollectionModel<
}
removeModel(model: WorkspaceModel): this {
+ // In collapsed mode the selected child is temporarily parented by the
+ // floating window. Remove that window before handing the child to another
+ // collection, otherwise the tray retains a second stale renderer for it.
+ if (this.floatingWindow.child === model) {
+ this.setFloatingModel(null);
+ }
super.removeModel(model);
if (this.selectedModel && this.selectedModel === model) {
this.selectedModel = null;