bug: fix dnd events interaction with third-party libs

This commit is contained in:
mathuo 2023-03-21 22:53:39 +01:00
parent 84e88f458f
commit 84088505a8
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
14 changed files with 3409 additions and 2484 deletions

View File

@ -8,11 +8,15 @@
"/packages/docs/sandboxes/customheader-dockview",
"/packages/docs/sandboxes/dnd-dockview",
"/packages/docs/sandboxes/events-dockview",
"/packages/docs/sandboxes/externaldnd-dockview",
"/packages/docs/sandboxes/fullwidthtab-dockview",
"/packages/docs/sandboxes/groupcontol-dockview",
"/packages/docs/sandboxes/layout-dockview",
"/packages/docs/sandboxes/nativeapp-dockview",
"/packages/docs/sandboxes/nested-dockview",
"/packages/docs/sandboxes/resize-dockview",
"/packages/docs/sandboxes/simple-dockview",
"/packages/docs/sandboxes/updatetitle-dockview",
"/packages/docs/sandboxes/watermark-dockview"
],
"node": "16"

View File

@ -41,6 +41,20 @@ export abstract class DragHandler extends CompositeDisposable {
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
/**
* Although this is not used by dockview many third party dnd libraries will check
* dataTransfer.types to determine valid drag events.
*
* For example: in react-dnd if dataTransfer.types is not set then the dragStart event will be cancelled
* through .preventDefault(). Since this is applied globally to all drag events this would break dockviews
* dnd logic. You can see the code at
* https://github.com/react-dnd/react-dnd/blob/main/packages/backend-html5/src/HTML5BackendImpl.ts#L542
*/
event.dataTransfer.setData(
'text/plain',
'__dockview_internal_drag_event__'
);
}
}),
addDisposableListener(this.el, 'dragend', () => {

View File

@ -55,8 +55,8 @@ export type CanDisplayOverlay =
| ((dragEvent: DragEvent, state: Position) => boolean);
export class Droptarget extends CompositeDisposable {
private target: HTMLElement | undefined;
private overlay: HTMLElement | undefined;
private targetElement: HTMLElement | undefined;
private overlayElement: HTMLElement | undefined;
private _state: Position | undefined;
private readonly _onDrop = new Emitter<DroptargetEvent>();
@ -127,23 +127,23 @@ export class Droptarget extends CompositeDisposable {
return;
}
if (!this.target) {
this.target = document.createElement('div');
this.target.className = 'drop-target-dropzone';
this.overlay = document.createElement('div');
this.overlay.className = 'drop-target-selection';
if (!this.targetElement) {
this.targetElement = document.createElement('div');
this.targetElement.className = 'drop-target-dropzone';
this.overlayElement = document.createElement('div');
this.overlayElement.className = 'drop-target-selection';
this._state = 'center';
this.target.appendChild(this.overlay);
this.targetElement.appendChild(this.overlayElement);
this.element.classList.add('drop-target');
this.element.append(this.target);
this.element.append(this.targetElement);
}
if (this.options.acceptedTargetZones.length === 0) {
return;
}
if (!this.target || !this.overlay) {
if (!this.targetElement || !this.overlayElement) {
return;
}
@ -159,13 +159,15 @@ export class Droptarget extends CompositeDisposable {
},
onDrop: (e) => {
e.preventDefault();
e.stopPropagation();
const state = this._state;
this.removeDropTarget();
if (state) {
// only stop the propagation of the event if we are dealing with it
// which is only when the target has state
e.stopPropagation();
this._onDrop.fire({ position: state, nativeEvent: e });
}
},
@ -182,7 +184,7 @@ export class Droptarget extends CompositeDisposable {
width: number,
height: number
): void {
if (!this.overlay) {
if (!this.overlayElement) {
return;
}
@ -235,12 +237,12 @@ export class Droptarget extends CompositeDisposable {
transform = '';
}
this.overlay.style.transform = transform;
this.overlayElement.style.transform = transform;
toggleClass(this.overlay, 'small-right', isSmallX && isRight);
toggleClass(this.overlay, 'small-left', isSmallX && isLeft);
toggleClass(this.overlay, 'small-top', isSmallY && isTop);
toggleClass(this.overlay, 'small-bottom', isSmallY && isBottom);
toggleClass(this.overlayElement, 'small-right', isSmallX && isRight);
toggleClass(this.overlayElement, 'small-left', isSmallX && isLeft);
toggleClass(this.overlayElement, 'small-top', isSmallY && isTop);
toggleClass(this.overlayElement, 'small-bottom', isSmallY && isBottom);
}
private setState(quadrant: Position): void {
@ -301,11 +303,11 @@ export class Droptarget extends CompositeDisposable {
}
private removeDropTarget(): void {
if (this.target) {
if (this.targetElement) {
this._state = undefined;
this.element.removeChild(this.target);
this.target = undefined;
this.overlay = undefined;
this.element.removeChild(this.targetElement);
this.targetElement = undefined;
this.overlayElement = undefined;
this.element.classList.remove('drop-target');
}
}

View File

@ -24,6 +24,7 @@ import CustomHeadersDockview from '@site/sandboxes/customheader-dockview/src/app
import DockviewNative from '@site/sandboxes/fullwidthtab-dockview/src/app';
import DockviewNative2 from '@site/sandboxes/nativeapp-dockview/src/app';
import DockviewSetTitle from '@site/sandboxes/updatetitle-dockview/src/app';
import DockviewExternalDnd from '@site/sandboxes/externaldnd-dockview/src/app';
// import { attach as attachDockviewVanilla } from '@site/sandboxes/vanilla-dockview/src/app';
# Dockview
@ -331,6 +332,14 @@ return (
<DndDockview />
</Container>
### Third Party Dnd Libraries
To be completed...
<Container>
<DockviewExternalDnd />
</Container>
## Panels
### Add Panel

View File

@ -19,11 +19,13 @@
"@docusaurus/core": "2.3.1",
"@docusaurus/preset-classic": "2.3.1",
"@mdx-js/react": "^1.6.22",
"@minoru/react-dnd-treeview": "^3.4.3",
"axios": "^1.3.3",
"clsx": "^1.2.1",
"dockview": "^1.6.0",
"prism-react-renderer": "^1.3.5",
"react": "^18.2.0",
"react-dnd": "^16.0.1",
"react-dom": "^18.2.0",
"recoil": "^0.7.6",
"uuid": "^9.0.0",

View File

@ -0,0 +1,33 @@
{
"name": "externaldnd-dockview",
"description": "",
"keywords": [
"dockview"
],
"version": "1.0.0",
"main": "src/index.tsx",
"dependencies": {
"@minoru/react-dnd-treeview": "^3.4.3",
"dockview": "*",
"react": "^18.2.0",
"react-dnd": "^16.0.1",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"typescript": "^4.9.5"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -0,0 +1,3 @@
.externaldnd-dockview {
color: white;
}

View File

@ -0,0 +1,106 @@
import {
DockviewReact,
DockviewReadyEvent,
IDockviewPanelProps,
} from 'dockview';
import * as React from 'react';
import TreeComponent from './treeview';
import { getBackendOptions, MultiBackend } from '@minoru/react-dnd-treeview';
import { DndProvider } from 'react-dnd';
import './app.scss';
const components = {
default: (props: IDockviewPanelProps<{ title: string }>) => {
return (
<div style={{ padding: '20px', color: 'white' }}>
{props.params.title}
</div>
);
},
treeview: () => {
return (
<div style={{ color: 'white' }}>
<TreeComponent />
</div>
);
},
};
export const App: React.FC = () => {
const onReady = (event: DockviewReadyEvent) => {
const panel = event.api.addPanel({
id: 'panel_1',
component: 'default',
params: {
title: 'Panel 1',
},
});
panel.group.locked = true;
panel.group.header.hidden = true;
event.api.addPanel({
id: 'panel_2',
component: 'default',
params: {
title: 'Panel 2',
},
});
event.api.addPanel({
id: 'panel_3',
component: 'treeview',
});
event.api.addPanel({
id: 'panel_4',
component: 'default',
params: {
title: 'Panel 4',
},
position: { referencePanel: 'panel_1', direction: 'right' },
});
const panel5 = event.api.addPanel({
id: 'panel_5',
component: 'default',
params: {
title: 'Panel 5',
},
position: { referencePanel: 'panel_3', direction: 'right' },
});
// panel5.group!.model.header.hidden = true;
// panel5.group!.model.locked = true;
event.api.addPanel({
id: 'panel_6',
component: 'default',
params: {
title: 'Panel 6',
},
position: { referencePanel: 'panel_5', direction: 'below' },
});
event.api.addPanel({
id: 'panel_7',
component: 'default',
params: {
title: 'Panel 7',
},
position: { referencePanel: 'panel_6', direction: 'right' },
});
};
return (
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
<DockviewReact
components={components}
onReady={onReady}
className="dockview-theme-abyss externaldnd-dockview"
/>
</DndProvider>
);
};
export default App;

View File

@ -0,0 +1,20 @@
import { StrictMode } from 'react';
import * as ReactDOMClient from 'react-dom/client';
import './styles.css';
import 'dockview/dist/styles/dockview.css';
import App from './app';
const rootElement = document.getElementById('root');
if (rootElement) {
const root = ReactDOMClient.createRoot(rootElement);
root.render(
<StrictMode>
<div className="app">
<App />
</div>
</StrictMode>
);
}

View File

@ -0,0 +1,16 @@
body {
margin: 0px;
color: white;
font-family: sans-serif;
text-align: center;
}
#root {
height: 100vh;
width: 100vw;
}
.app {
height: 100%;
}

View File

@ -0,0 +1,74 @@
import { useState } from 'react';
import { Tree } from '@minoru/react-dnd-treeview';
import * as React from 'react';
const SampleData = [
{
id: 1,
parent: 0,
droppable: true,
text: 'Folder 1',
},
{
id: 2,
parent: 1,
droppable: false,
text: 'File 1-1',
},
{
id: 3,
parent: 1,
droppable: false,
text: 'File 1-2',
},
{
id: 4,
parent: 0,
droppable: true,
text: 'Folder 2',
},
{
id: 5,
parent: 4,
droppable: true,
text: 'Folder 2-1',
},
{
id: 6,
parent: 5,
droppable: false,
text: 'File 2-1-1',
},
{
id: 7,
parent: 0,
droppable: false,
text: 'File 3',
},
];
const TreeComponent = () => {
const [treeData, setTreeData] = useState(SampleData);
const handleDrop = (newTreeData: any) => {
console.log('handleDrop');
setTreeData(newTreeData);
};
return (
<Tree
tree={treeData}
rootId={0}
onDrop={handleDrop}
onDragEnd={(event) => console.log('onDragEnd', event)}
render={(node, { depth, isOpen, onToggle }) => (
<div style={{ marginLeft: depth * 10 }}>
{node.droppable && (
<span onClick={onToggle}>{isOpen ? '[-]' : '[+]'}</span>
)}
{node.text}
</div>
)}
/>
);
};
export default TreeComponent;

View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"outDir": "build/dist",
"module": "esnext",
"target": "es5",
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
"jsx": "react-jsx",
"moduleResolution": "node",
"rootDir": "src",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": true
}
}

5505
yarn.lock

File diff suppressed because it is too large Load Diff