mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
dbeaver/pro#5753 feat: react-dnd (#3454)
* dbeaver/pro#5753 feat: react-dnd * chore: fix after review --------- Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
This commit is contained in:
co-authored by
Daria Marutkina
parent
1f4266ec95
commit
84cc6fcf60
@@ -33,6 +33,7 @@
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dbeaver/react-dnd": "workspace:^",
|
||||
"@dbeaver/ui-kit": "workspace:^",
|
||||
"react": "^19",
|
||||
"react-data-grid": "file:./artifacts",
|
||||
|
||||
@@ -4,19 +4,24 @@ import { DataGridCellContext, type IDataGridCellRenderer } from './DataGridCellC
|
||||
import { createCellMouseEvent } from './eventUtils.js';
|
||||
import { DataGridCellInnerContext, type IDataGridCellInnerContext } from './DataGridCellInnerContext.js';
|
||||
import { useGridReactiveValue } from './useGridReactiveValue.js';
|
||||
import { HeaderDnDContext } from './useHeaderDnD.js';
|
||||
|
||||
export const BaseCell = memo(function BaseCell<TRow, TSummaryRow>(props: CellRendererProps<TRow, TSummaryRow>) {
|
||||
const cellContext = use(DataGridCellContext);
|
||||
const tooltip = useGridReactiveValue(cellContext?.cellTooltip, props.rowIdx, props.column.idx);
|
||||
const dndContext = use(HeaderDnDContext)!;
|
||||
const virtualColIdx = props.column.idx;
|
||||
const dataColIdx = dndContext.getDataColIdx(props.column.idx);
|
||||
const rowIdx = props.rowIdx;
|
||||
const tooltip = useGridReactiveValue(cellContext?.cellTooltip, rowIdx, dataColIdx);
|
||||
|
||||
function handleClick(event: React.MouseEvent<HTMLDivElement>) {
|
||||
props.onClick?.(
|
||||
{
|
||||
rowIdx: props.rowIdx,
|
||||
rowIdx,
|
||||
row: props.row,
|
||||
column: props.column,
|
||||
selectCell(enableEditor) {
|
||||
props.selectCell({ rowIdx: props.rowIdx, idx: props.column.idx }, enableEditor);
|
||||
props.selectCell({ rowIdx, idx: virtualColIdx }, enableEditor);
|
||||
},
|
||||
},
|
||||
createCellMouseEvent(event),
|
||||
@@ -26,11 +31,11 @@ export const BaseCell = memo(function BaseCell<TRow, TSummaryRow>(props: CellRen
|
||||
function handleDoubleClick(event: React.MouseEvent<HTMLDivElement>) {
|
||||
props.onDoubleClick?.(
|
||||
{
|
||||
rowIdx: props.rowIdx,
|
||||
rowIdx,
|
||||
row: props.row,
|
||||
column: props.column,
|
||||
selectCell(enableEditor) {
|
||||
props.selectCell({ rowIdx: props.rowIdx, idx: props.column.idx }, enableEditor);
|
||||
props.selectCell({ rowIdx, idx: virtualColIdx }, enableEditor);
|
||||
},
|
||||
},
|
||||
createCellMouseEvent(event),
|
||||
@@ -40,11 +45,11 @@ export const BaseCell = memo(function BaseCell<TRow, TSummaryRow>(props: CellRen
|
||||
function handleContextMenu(event: React.MouseEvent<HTMLDivElement>) {
|
||||
props.onContextMenu?.(
|
||||
{
|
||||
rowIdx: props.rowIdx,
|
||||
rowIdx,
|
||||
row: props.row,
|
||||
column: props.column,
|
||||
selectCell(enableEditor) {
|
||||
props.selectCell({ rowIdx: props.rowIdx, idx: props.column.idx }, enableEditor);
|
||||
props.selectCell({ rowIdx, idx: virtualColIdx }, enableEditor);
|
||||
},
|
||||
},
|
||||
createCellMouseEvent(event),
|
||||
@@ -74,7 +79,7 @@ export const BaseCell = memo(function BaseCell<TRow, TSummaryRow>(props: CellRen
|
||||
[...Object.values(props), tooltip],
|
||||
);
|
||||
|
||||
const cellElement = useGridReactiveValue(cellContext?.cellElement, props.rowIdx, props.column.idx, mappedProps, renderDefaultCell);
|
||||
const cellElement = useGridReactiveValue(cellContext?.cellElement, rowIdx, dataColIdx, mappedProps, renderDefaultCell);
|
||||
|
||||
const innerCellContext = useMemo<IDataGridCellInnerContext>(() => ({ isFocused: props.isCellSelected }), [props.isCellSelected]);
|
||||
return <DataGridCellInnerContext value={innerCellContext}>{cellElement ?? <Cell title={tooltip} {...props} />}</DataGridCellInnerContext>;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { mapRenderHeaderCell } from './mapRenderHeaderCell.js';
|
||||
import { mapEditCellRenderer } from './mapEditCellRenderer.js';
|
||||
import { DataGridRowContext, type IDataGridRowContext } from './DataGridRowContext.js';
|
||||
import './DataGrid.css';
|
||||
import { HeaderDnDContext, useHeaderDnD } from './useHeaderDnD.js';
|
||||
|
||||
export interface ICellPosition {
|
||||
rowIdx: number;
|
||||
@@ -46,9 +47,11 @@ export const DataGrid = forwardRef<DataGridRef, DataGridProps>(function DataGrid
|
||||
headerElement,
|
||||
getHeaderWidth,
|
||||
headerText,
|
||||
getHeaderOrder,
|
||||
getHeaderResizable,
|
||||
getHeaderHeight,
|
||||
getHeaderPinned,
|
||||
getHeaderDnD,
|
||||
cell,
|
||||
cellText,
|
||||
cellElement,
|
||||
@@ -59,6 +62,7 @@ export const DataGrid = forwardRef<DataGridRef, DataGridProps>(function DataGrid
|
||||
rowCount,
|
||||
getRowId,
|
||||
getRowHeight,
|
||||
onHeaderReorder,
|
||||
onScroll,
|
||||
onScrollToBottom,
|
||||
onFocus,
|
||||
@@ -87,21 +91,23 @@ export const DataGrid = forwardRef<DataGridRef, DataGridProps>(function DataGrid
|
||||
minWidth: 26,
|
||||
editable: row => getCellEditable?.(row.idx, i) ?? false,
|
||||
frozen: getHeaderPinned?.(i),
|
||||
renderHeaderCell: mapRenderHeaderCell,
|
||||
renderCell: mapCellContentRenderer,
|
||||
renderEditCell: mapEditCellRenderer,
|
||||
renderHeaderCell: mapRenderHeaderCell(i),
|
||||
renderCell: mapCellContentRenderer(i),
|
||||
renderEditCell: mapEditCellRenderer(i),
|
||||
};
|
||||
});
|
||||
|
||||
const dndHeaderContext = useHeaderDnD({ columns, onReorder: onHeaderReorder, getCanDrag: getHeaderDnD, getHeaderOrder });
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
selectCell: (position: ICellPosition) => {
|
||||
innerGridRef.current?.selectCell({ idx: position.colIdx, rowIdx: position.rowIdx });
|
||||
innerGridRef.current?.selectCell({ idx: dndHeaderContext.getDataColIdx(position.colIdx), rowIdx: position.rowIdx });
|
||||
},
|
||||
scrollToCell: (position: Partial<ICellPosition>) => {
|
||||
innerGridRef.current?.scrollToCell({ idx: position.colIdx, rowIdx: position.rowIdx });
|
||||
innerGridRef.current?.scrollToCell({ idx: position.colIdx && dndHeaderContext.getDataColIdx(position.colIdx), rowIdx: position.rowIdx });
|
||||
},
|
||||
openEditor: (position: ICellPosition) => {
|
||||
innerGridRef.current?.selectCell({ idx: position.colIdx, rowIdx: position.rowIdx }, true);
|
||||
innerGridRef.current?.selectCell({ idx: dndHeaderContext.getDataColIdx(position.colIdx), rowIdx: position.rowIdx }, true);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -124,34 +130,40 @@ export const DataGrid = forwardRef<DataGridRef, DataGridProps>(function DataGrid
|
||||
);
|
||||
|
||||
function handleCellFocus(args: CellSelectArgs<IInnerRow, unknown>) {
|
||||
onFocus?.({ colIdx: args.column.idx, rowIdx: args.rowIdx });
|
||||
onFocus?.({ colIdx: dndHeaderContext.getDataColIdx(args.column.idx), rowIdx: args.rowIdx });
|
||||
}
|
||||
|
||||
function handleCellKeyDown(args: CellSelectArgs<IInnerRow, unknown>, event: DataGridCellKeyboardEvent) {
|
||||
onCellKeyDown?.({ colIdx: dndHeaderContext.getDataColIdx(args.column.idx), rowIdx: args.rowIdx }, event);
|
||||
}
|
||||
|
||||
return (
|
||||
<DataGridRowContext value={{ rowCount, onScrollToBottom }}>
|
||||
<DataGridCellContext value={{ cell, cellText, cellElement, cellTooltip, onCellChange }}>
|
||||
<DataGridCellHeaderContext value={{ headerElement, headerText }}>
|
||||
<DataGridBase
|
||||
key={gridKey.current}
|
||||
ref={innerGridRef}
|
||||
columns={columns}
|
||||
rows={rows}
|
||||
className={className}
|
||||
headerRowHeight={getHeaderHeight?.()}
|
||||
onScroll={onScroll}
|
||||
rowHeight={getRowHeight ? row => getRowHeight(row.idx) : undefined}
|
||||
rowKeyGetter={getRowId ? row => getRowId(row.idx) : undefined}
|
||||
onSelectedCellChange={handleCellFocus}
|
||||
onCellKeyDown={onCellKeyDown ? (args, event) => onCellKeyDown({ rowIdx: args.rowIdx, colIdx: args.column.idx }, event) : undefined}
|
||||
renderers={{
|
||||
renderRow: rowRenderer,
|
||||
renderCell: cellRenderer,
|
||||
noRowsFallback: children,
|
||||
}}
|
||||
minimumRowsToRender={100}
|
||||
/>
|
||||
</DataGridCellHeaderContext>
|
||||
</DataGridCellContext>
|
||||
</DataGridRowContext>
|
||||
<HeaderDnDContext value={dndHeaderContext}>
|
||||
<DataGridRowContext value={{ rowCount, onScrollToBottom }}>
|
||||
<DataGridCellContext value={{ cell, cellText, cellElement, cellTooltip, onCellChange }}>
|
||||
<DataGridCellHeaderContext value={{ headerElement, headerText, getHeaderDnD }}>
|
||||
<DataGridBase
|
||||
key={gridKey.current}
|
||||
ref={innerGridRef}
|
||||
columns={dndHeaderContext.columns}
|
||||
rows={rows}
|
||||
className={className}
|
||||
headerRowHeight={getHeaderHeight?.()}
|
||||
onScroll={onScroll}
|
||||
rowHeight={getRowHeight ? row => getRowHeight(row.idx) : undefined}
|
||||
rowKeyGetter={getRowId ? row => getRowId(row.idx) : undefined}
|
||||
onSelectedCellChange={handleCellFocus}
|
||||
onCellKeyDown={handleCellKeyDown}
|
||||
renderers={{
|
||||
renderRow: rowRenderer,
|
||||
renderCell: cellRenderer,
|
||||
noRowsFallback: children,
|
||||
}}
|
||||
minimumRowsToRender={100}
|
||||
/>
|
||||
</DataGridCellHeaderContext>
|
||||
</DataGridCellContext>
|
||||
</DataGridRowContext>
|
||||
</HeaderDnDContext>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,10 +4,13 @@ import type { IGridReactiveValue } from './IGridReactiveValue.js';
|
||||
export interface IDataGridHeaderCellContext {
|
||||
headerElement?: IGridReactiveValue<React.ReactNode, [colIdx: number]>;
|
||||
headerText?: IGridReactiveValue<string, [colIdx: number]>;
|
||||
getHeaderOrder?: () => number[];
|
||||
getHeaderWidth?: (colIdx: number) => number | string | null;
|
||||
getHeaderResizable?: (colIdx: number) => boolean;
|
||||
getHeaderHeight?: () => number;
|
||||
getHeaderPinned?: (colIdx: number) => boolean;
|
||||
getHeaderDnD?: (colIdx: number) => boolean;
|
||||
onHeaderReorder?: (from: number, to: number) => void;
|
||||
}
|
||||
|
||||
export const DataGridCellHeaderContext = createContext<IDataGridHeaderCellContext | null>(null);
|
||||
|
||||
@@ -4,6 +4,7 @@ export * from './DataGridCellInnerContext.js';
|
||||
export * from './IGridReactiveValue.js';
|
||||
export * from './useCreateGridReactiveValue.js';
|
||||
export * from './useGridReactiveValue.js';
|
||||
export * from './reorderArray.js';
|
||||
|
||||
export * from './formatters/NullFormatter.js';
|
||||
export * from './formatters/BooleanFormatter.js';
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { RenderCellProps } from 'react-data-grid';
|
||||
import type { IInnerRow } from './IInnerRow.js';
|
||||
import { CellContentRenderer } from './renderers/CellContentRenderer.js';
|
||||
|
||||
export function mapCellContentRenderer({ rowIdx, column }: RenderCellProps<IInnerRow, unknown>) {
|
||||
return <CellContentRenderer rowIdx={rowIdx} colIdx={column.idx} />;
|
||||
export function mapCellContentRenderer(colIdx: number) {
|
||||
return function RenderCell({ rowIdx }: RenderCellProps<IInnerRow, unknown>) {
|
||||
return <CellContentRenderer rowIdx={rowIdx} colIdx={colIdx} />;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { RenderEditCellProps } from 'react-data-grid';
|
||||
import type { IInnerRow } from './IInnerRow.js';
|
||||
import { TextEditor } from './editors/TextEditor.js';
|
||||
|
||||
export function mapEditCellRenderer({ rowIdx, column, onClose }: RenderEditCellProps<IInnerRow, unknown>) {
|
||||
return <TextEditor rowIdx={rowIdx} colIdx={column.idx} onClose={() => onClose()} />;
|
||||
export function mapEditCellRenderer(colIdx: number) {
|
||||
return function RenderEditCell({ rowIdx, onClose }: RenderEditCellProps<IInnerRow, unknown>) {
|
||||
return <TextEditor rowIdx={rowIdx} colIdx={colIdx} onClose={() => onClose()} />;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { RenderHeaderCellProps } from 'react-data-grid';
|
||||
import type { IInnerRow } from './IInnerRow.js';
|
||||
import { HeaderCellContentRenderer } from './renderers/HeaderCellContentRenderer.js';
|
||||
|
||||
export function mapRenderHeaderCell({ column }: RenderHeaderCellProps<IInnerRow, unknown>) {
|
||||
return <HeaderCellContentRenderer colIdx={column.idx} />;
|
||||
export function mapRenderHeaderCell(colIdx: number) {
|
||||
return function RenderHeaderCell({}: RenderHeaderCellProps<IInnerRow, unknown>) {
|
||||
return <HeaderCellContentRenderer colIdx={colIdx} />;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,13 +1,55 @@
|
||||
import { use } from 'react';
|
||||
import { memo, use } from 'react';
|
||||
import { useDrag, useDrop, type DnDStoreProvider } from '@dbeaver/react-dnd';
|
||||
import { DataGridCellHeaderContext } from '../DataGridHeaderCellContext.js';
|
||||
import { useGridReactiveValue } from '../useGridReactiveValue.js';
|
||||
import { HeaderDnDContext } from '../useHeaderDnD.js';
|
||||
|
||||
interface Props {
|
||||
colIdx: number;
|
||||
}
|
||||
export function HeaderCellContentRenderer({ colIdx }: Props) {
|
||||
export const HeaderCellContentRenderer = memo(function HeaderCellContentRenderer({ colIdx }: Props) {
|
||||
const dndHeaderContext = use(HeaderDnDContext);
|
||||
const cellHeaderContext = use(DataGridCellHeaderContext);
|
||||
const headerElement = useGridReactiveValue(cellHeaderContext?.headerElement, colIdx);
|
||||
const getHeaderText = useGridReactiveValue(headerElement ? undefined : cellHeaderContext?.headerText, colIdx);
|
||||
return headerElement ?? getHeaderText ?? '';
|
||||
}
|
||||
|
||||
const draggable = dndHeaderContext?.getCanDrag?.(colIdx) ?? false;
|
||||
const drag = useDrag({
|
||||
draggable,
|
||||
onDragStart(event, store) {
|
||||
if (dndHeaderContext?.id) {
|
||||
store.setData('dbeaver-react-data-grid-id', dndHeaderContext?.id);
|
||||
}
|
||||
store.setData('dbeaver-react-data-grid-col-idx', colIdx);
|
||||
},
|
||||
onDragEnd() {
|
||||
dndHeaderContext?.onDragEnd(colIdx);
|
||||
},
|
||||
});
|
||||
|
||||
function canDrop(event: React.DragEvent<HTMLElement>, store: DnDStoreProvider | null) {
|
||||
const gridId = store?.getData('dbeaver-react-data-grid-id');
|
||||
return gridId === dndHeaderContext?.id;
|
||||
}
|
||||
const drop = useDrop({
|
||||
canDrop,
|
||||
onDragOver(event, store) {
|
||||
if (!canDrop(event, store)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dragColIdx = store?.getData('dbeaver-react-data-grid-col-idx');
|
||||
const position = event.clientX - event.currentTarget.getBoundingClientRect().left;
|
||||
const width = event.currentTarget.offsetWidth;
|
||||
const isAfter = position > width / 2;
|
||||
|
||||
dndHeaderContext?.onDragOver(dragColIdx, colIdx, isAfter);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="tw:h-full tw:content-center" {...drag.props} {...drop.props}>
|
||||
{headerElement ?? getHeaderText ?? ''}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export function reorderArray<T>(array: T[], from: number, to: number): T[] {
|
||||
const newArray = [...array];
|
||||
if (from < to) {
|
||||
newArray.splice(to, 0, array[from]!);
|
||||
newArray.splice(from, 1);
|
||||
} else {
|
||||
newArray.splice(from, 1);
|
||||
newArray.splice(to, 0, array[from]!);
|
||||
}
|
||||
return newArray;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { ColumnOrColumnGroup } from 'react-data-grid';
|
||||
import type { IInnerRow } from './IInnerRow.js';
|
||||
import { createContext, useId, useMemo, useState } from 'react';
|
||||
import { reorderArray } from './reorderArray.js';
|
||||
|
||||
interface IHeaderDnDOptions {
|
||||
columns: Array<ColumnOrColumnGroup<IInnerRow, unknown>>;
|
||||
getCanDrag?: (colIdx: number) => boolean;
|
||||
onReorder?: (from: number, to: number) => void;
|
||||
getHeaderOrder?: () => number[];
|
||||
}
|
||||
|
||||
interface IHeaderDnD {
|
||||
id: string;
|
||||
columns: Array<ColumnOrColumnGroup<IInnerRow, unknown>>;
|
||||
getDataColIdx: (virtualColIdx: number) => number;
|
||||
getVirtualColIdx: (dataColIdx: number) => number;
|
||||
getCanDrag?: (colIdx: number) => boolean;
|
||||
onDragOver: (dragColIdx: number, dropColIdx: number, isAfter: boolean) => void;
|
||||
onDragEnd: (colIdx: number) => void;
|
||||
}
|
||||
|
||||
export const HeaderDnDContext = createContext<IHeaderDnD | null>(null);
|
||||
|
||||
export function useHeaderDnD({ columns, getCanDrag, onReorder, getHeaderOrder }: IHeaderDnDOptions): IHeaderDnD {
|
||||
const id = useId();
|
||||
const [activeDnDElement, setActiveDnDElement] = useState<number | null>(null);
|
||||
const [dragOverElement, setDragOverElement] = useState<[number, boolean] | null>(null);
|
||||
|
||||
function getDataColIdx(virtualColIdx: number) {
|
||||
// Given a visible (ordered) column index, return the original data column index
|
||||
const order = getHeaderOrder?.();
|
||||
if (order) {
|
||||
return order[virtualColIdx] ?? virtualColIdx;
|
||||
}
|
||||
return virtualColIdx;
|
||||
}
|
||||
|
||||
function getVirtualColIdx(dataColIdx: number) {
|
||||
// Given the original data column index, return its visible (ordered) index
|
||||
const order = getHeaderOrder?.();
|
||||
if (order) {
|
||||
return order.indexOf(dataColIdx);
|
||||
}
|
||||
return dataColIdx;
|
||||
}
|
||||
|
||||
function onDragEnd(dragColIdx: number) {
|
||||
setActiveDnDElement(null);
|
||||
setDragOverElement(null);
|
||||
|
||||
if (dragOverElement) {
|
||||
const isAfter = dragOverElement?.[1];
|
||||
let to = dragOverElement?.[0];
|
||||
|
||||
if (isAfter) {
|
||||
to = getDataColIdx(getVirtualColIdx(to) + 1);
|
||||
}
|
||||
|
||||
onReorder?.(dragColIdx, to);
|
||||
}
|
||||
}
|
||||
|
||||
function onDragOver(dragColIdx: number, dropColIdx: number, isAfter: boolean) {
|
||||
setActiveDnDElement(dragColIdx);
|
||||
if (dragColIdx !== dropColIdx) {
|
||||
setDragOverElement([dropColIdx, isAfter]);
|
||||
}
|
||||
}
|
||||
|
||||
columns = useMemo(() => columns.map((col, idx) => columns[getDataColIdx(idx)]!), [columns, getHeaderOrder]);
|
||||
|
||||
if (activeDnDElement !== null && dragOverElement !== null) {
|
||||
const from = getVirtualColIdx(activeDnDElement);
|
||||
let to = getVirtualColIdx(dragOverElement[0]);
|
||||
const isAfter = dragOverElement[1];
|
||||
|
||||
if (isAfter) {
|
||||
to++;
|
||||
}
|
||||
|
||||
columns = reorderArray(columns, from, to);
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
columns,
|
||||
getDataColIdx,
|
||||
getVirtualColIdx,
|
||||
getCanDrag,
|
||||
onDragOver,
|
||||
onDragEnd,
|
||||
};
|
||||
}
|
||||
@@ -14,6 +14,9 @@
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../react-dnd"
|
||||
},
|
||||
{
|
||||
"path": "../ui-kit"
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# @dbeaver/react-dnd
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@dbeaver/react-dnd",
|
||||
"type": "module",
|
||||
"sideEffects": [],
|
||||
"exports": {
|
||||
".": "./lib/index.js",
|
||||
"./*": "./lib/*.js"
|
||||
},
|
||||
"scripts": {
|
||||
"clear": "rimraf lib"
|
||||
},
|
||||
"files": [
|
||||
"package.json",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"lib",
|
||||
"!lib/**/*.d.ts.map",
|
||||
"!lib/**/*.test.js",
|
||||
"!lib/**/*.test.d.ts",
|
||||
"!lib/**/*.test.d.ts.map",
|
||||
"!lib/**/*.test.js.map",
|
||||
"!lib/tests",
|
||||
"!.tsbuildinfo"
|
||||
],
|
||||
"packageManager": "yarn@4.6.0",
|
||||
"devDependencies": {
|
||||
"@dbeaver/tsconfig": "workspace:^",
|
||||
"@types/react": "^19",
|
||||
"rimraf": "^6",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './useDrag.js';
|
||||
export * from './useDrop.js';
|
||||
export { type DnDStoreProvider } from './store.js';
|
||||
@@ -0,0 +1,46 @@
|
||||
export const DND_STORE_PREFIX = 'application/dbeaver-react-dnd-';
|
||||
|
||||
export interface DnDStoreProvider {
|
||||
getData: (key: string) => any;
|
||||
setData: (key: string, value: any) => void;
|
||||
removeData: () => void;
|
||||
}
|
||||
|
||||
class DnDStore {
|
||||
private data: Map<string, Record<string, any>> = new Map();
|
||||
setData(id: string, key: string, value: any) {
|
||||
if (!this.data.has(id)) {
|
||||
this.data.set(id, {});
|
||||
}
|
||||
this.data.get(id)![key] = value;
|
||||
}
|
||||
getData(id: string, key: string) {
|
||||
return this.data.get(id)?.[key];
|
||||
}
|
||||
removeData(id: string) {
|
||||
this.data.delete(id);
|
||||
}
|
||||
|
||||
getProvider(id: string) {
|
||||
return {
|
||||
getData: (key: string) => this.getData(id, key),
|
||||
setData: (key: string, value: any) => {
|
||||
this.setData(id, key, value);
|
||||
},
|
||||
removeData: () => {
|
||||
this.removeData(id);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function getStoreProvider(event: React.DragEvent<HTMLElement>): DnDStoreProvider | null {
|
||||
const id = event.dataTransfer.types.find(type => type.startsWith(DND_STORE_PREFIX))?.slice(DND_STORE_PREFIX.length);
|
||||
|
||||
return id ? globalDnDStore.getProvider(id) : null;
|
||||
}
|
||||
|
||||
// TODO: do we want to use react context here?
|
||||
// the main idea was to use native DataTransfer, but it's available only for drag start and drop events
|
||||
// so we need to use a custom store
|
||||
export const globalDnDStore = new DnDStore();
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useId, useMemo, useRef, useState } from 'react';
|
||||
import { DND_STORE_PREFIX, globalDnDStore, type DnDStoreProvider } from './store.js';
|
||||
|
||||
interface IDragOptions {
|
||||
draggable?: boolean;
|
||||
onDrag?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider) => void;
|
||||
onDragStart?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider) => void;
|
||||
onDragEnd?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider) => void;
|
||||
}
|
||||
interface IDrag {
|
||||
isDragging: boolean;
|
||||
props: {
|
||||
draggable?: boolean;
|
||||
onDrag: (event: React.DragEvent<HTMLElement>) => void;
|
||||
onDragStart: (event: React.DragEvent<HTMLElement>) => void;
|
||||
onDragEnd: (event: React.DragEvent<HTMLElement>) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export function useDrag({ draggable, onDragStart, onDrag, onDragEnd }: IDragOptions = {}): IDrag {
|
||||
const id = useId();
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const optionsRef = useRef<IDragOptions>({ draggable, onDragStart, onDrag, onDragEnd });
|
||||
optionsRef.current = { draggable, onDragStart, onDrag, onDragEnd };
|
||||
|
||||
function handleDragStart(event: React.DragEvent<HTMLElement>) {
|
||||
if (!optionsRef.current.draggable) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData(`${DND_STORE_PREFIX}${id}`, id);
|
||||
setDragging(true);
|
||||
optionsRef.current.onDragStart?.(event, globalDnDStore.getProvider(id));
|
||||
}
|
||||
|
||||
function handleDrag(event: React.DragEvent<HTMLElement>) {
|
||||
if (!optionsRef.current.draggable) {
|
||||
return;
|
||||
}
|
||||
optionsRef.current.onDrag?.(event, globalDnDStore.getProvider(id));
|
||||
}
|
||||
|
||||
function handleDragEnd(event: React.DragEvent<HTMLElement>) {
|
||||
if (!optionsRef.current.draggable) {
|
||||
return;
|
||||
}
|
||||
setDragging(false);
|
||||
optionsRef.current.onDragEnd?.(event, globalDnDStore.getProvider(id));
|
||||
globalDnDStore.removeData(id);
|
||||
}
|
||||
|
||||
return {
|
||||
isDragging: dragging,
|
||||
props: {
|
||||
draggable,
|
||||
...useMemo(
|
||||
() => ({
|
||||
onDrag: handleDrag,
|
||||
onDragStart: handleDragStart,
|
||||
onDragEnd: handleDragEnd,
|
||||
}),
|
||||
[],
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { getStoreProvider, type DnDStoreProvider } from './store.js';
|
||||
|
||||
interface IDropOptions {
|
||||
canDrop?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider | null) => boolean;
|
||||
onDrop?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider | null) => void;
|
||||
onDragOver?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider | null) => void;
|
||||
onDragEnter?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider | null) => void;
|
||||
onDragLeave?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider | null) => void;
|
||||
}
|
||||
|
||||
interface IDrop {
|
||||
props: {
|
||||
onDrop: (event: React.DragEvent<HTMLElement>) => void;
|
||||
onDragOver: (event: React.DragEvent<HTMLElement>) => void;
|
||||
onDragEnter: (event: React.DragEvent<HTMLElement>) => void;
|
||||
onDragLeave: (event: React.DragEvent<HTMLElement>) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export function useDrop({ canDrop, onDrop, onDragOver, onDragEnter, onDragLeave }: IDropOptions = {}): IDrop {
|
||||
const optionsRef = useRef<IDropOptions>({ canDrop, onDrop, onDragOver, onDragEnter, onDragLeave });
|
||||
optionsRef.current = { canDrop, onDrop, onDragOver, onDragEnter, onDragLeave };
|
||||
|
||||
function handleOver(event: React.DragEvent<HTMLElement>) {
|
||||
const provider = getStoreProvider(event);
|
||||
const isDroppable = optionsRef.current.canDrop?.(event, provider);
|
||||
if (isDroppable) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
} else {
|
||||
event.dataTransfer.dropEffect = 'none';
|
||||
}
|
||||
optionsRef.current.onDragOver?.(event, provider);
|
||||
}
|
||||
|
||||
function handleEnter(event: React.DragEvent<HTMLElement>) {
|
||||
const provider = getStoreProvider(event);
|
||||
optionsRef.current.onDragEnter?.(event, provider);
|
||||
}
|
||||
function handleLeave(event: React.DragEvent<HTMLElement>) {
|
||||
const provider = getStoreProvider(event);
|
||||
optionsRef.current.onDragLeave?.(event, provider);
|
||||
}
|
||||
function handleDrop(event: React.DragEvent<HTMLElement>) {
|
||||
const provider = getStoreProvider(event);
|
||||
optionsRef.current.onDrop?.(event, provider);
|
||||
}
|
||||
|
||||
return {
|
||||
props: useMemo(
|
||||
() => ({
|
||||
onDrop: handleDrop,
|
||||
onDragOver: handleOver,
|
||||
onDragEnter: handleEnter,
|
||||
onDragLeave: handleLeave,
|
||||
}),
|
||||
[],
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "@dbeaver/tsconfig/tsconfig.webview.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./lib/.tsbuildinfo",
|
||||
"rootDir": "src",
|
||||
"rootDirs": [
|
||||
"src"
|
||||
],
|
||||
"outDir": "lib",
|
||||
"incremental": true,
|
||||
"composite": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/* this file required for tailwindcss vscode extension to work */
|
||||
@import './@dbeaver/ui-kit/.ladle/global.css';
|
||||
@@ -348,6 +348,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@dbeaver/react-data-grid@workspace:@dbeaver/react-data-grid"
|
||||
dependencies:
|
||||
"@dbeaver/react-dnd": "workspace:^"
|
||||
"@dbeaver/tsconfig": "workspace:^"
|
||||
"@dbeaver/ui-kit": "workspace:^"
|
||||
"@types/react": "npm:^19"
|
||||
@@ -360,6 +361,18 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@dbeaver/react-dnd@workspace:@dbeaver/react-dnd, @dbeaver/react-dnd@workspace:^":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@dbeaver/react-dnd@workspace:@dbeaver/react-dnd"
|
||||
dependencies:
|
||||
"@dbeaver/tsconfig": "workspace:^"
|
||||
"@types/react": "npm:^19"
|
||||
react: "npm:^19"
|
||||
rimraf: "npm:^6"
|
||||
typescript: "npm:^5"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@dbeaver/result-set-api@workspace:../common-typescript/@dbeaver/result-set-api":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@dbeaver/result-set-api@workspace:../common-typescript/@dbeaver/result-set-api"
|
||||
|
||||
@@ -4491,6 +4491,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@dbeaver/react-data-grid@workspace:../common-react/@dbeaver/react-data-grid"
|
||||
dependencies:
|
||||
"@dbeaver/react-dnd": "workspace:^"
|
||||
"@dbeaver/tsconfig": "workspace:^"
|
||||
"@dbeaver/ui-kit": "workspace:^"
|
||||
"@types/react": "npm:^19"
|
||||
@@ -4503,6 +4504,18 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@dbeaver/react-dnd@workspace:../common-react/@dbeaver/react-dnd, @dbeaver/react-dnd@workspace:^":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@dbeaver/react-dnd@workspace:../common-react/@dbeaver/react-dnd"
|
||||
dependencies:
|
||||
"@dbeaver/tsconfig": "workspace:^"
|
||||
"@types/react": "npm:^19"
|
||||
react: "npm:^19"
|
||||
rimraf: "npm:^6"
|
||||
typescript: "npm:^5"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@dbeaver/result-set-api@workspace:../common-typescript/@dbeaver/result-set-api, @dbeaver/result-set-api@workspace:^":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@dbeaver/result-set-api@workspace:../common-typescript/@dbeaver/result-set-api"
|
||||
|
||||
Reference in New Issue
Block a user