feat(dees-dashboardgrid): enhance drag-and-drop functionality with preview state and previous position tracking

This commit is contained in:
2025-09-18 08:05:41 +00:00
parent 6f9c92a866
commit 0de4283fae
4 changed files with 110 additions and 20 deletions

Binary file not shown.

View File

@@ -89,10 +89,12 @@ export const demoFunc = () => {
grid.applyBreakpointLayout(target);
updateStatus();
};
if ('addEventListener' in mediaQuery) {
if (typeof mediaQuery.addEventListener === 'function') {
mediaQuery.addEventListener('change', handleBreakpoint);
} else {
mediaQuery.addListener(handleBreakpoint);
(mediaQuery as MediaQueryList & {
addListener?: (listener: (this: MediaQueryList, ev: MediaQueryListEvent) => void) => void;
}).addListener?.(handleBreakpoint);
}
handleBreakpoint();

View File

@@ -49,6 +49,7 @@ type DragState = {
offsetX: number;
offsetY: number;
start: DashboardLayoutItem;
previousPosition: DashboardLayoutItem;
currentPointer: PointerPosition;
lastPlacement: PlacementResult | null;
};
@@ -111,20 +112,23 @@ export class DeesDashboardgrid extends DeesElement {
@state()
private resolvedMargins: DashboardResolvedMargins | null = null;
@state()
private previewWidgets: DashboardWidget[] | null = null;
private containerBounds: DOMRect | null = null;
private dragState: DragState | null = null;
private resizeState: ResizeState | null = null;
private resizeObserver?: ResizeObserver;
private interactionActive = false;
connectedCallback(): void {
super.connectedCallback();
public override async connectedCallback(): Promise<void> {
await super.connectedCallback();
this.computeMetrics();
this.observeResize();
}
disconnectedCallback(): void {
super.disconnectedCallback();
public override async disconnectedCallback(): Promise<void> {
await super.disconnectedCallback();
this.disconnectResizeObserver();
this.releasePointerEvents();
}
@@ -145,7 +149,8 @@ export class DeesDashboardgrid extends DeesElement {
}
public render(): TemplateResult {
if (this.widgets.length === 0) {
const baseWidgets = this.widgets;
if (baseWidgets.length === 0) {
return html`
<div class="empty-state">
<dees-icon .icon=${'lucide:layoutGrid'}></dees-icon>
@@ -158,12 +163,14 @@ export class DeesDashboardgrid extends DeesElement {
const metrics = this.ensureMetrics();
const margins = this.resolvedMargins ?? resolveMargins(this.margin);
const cellHeight = metrics.cellHeightPx;
const gridHeight = calculateGridHeight(this.widgets, margins, cellHeight);
const layoutForHeight = this.previewWidgets ?? this.widgets;
const gridHeight = calculateGridHeight(layoutForHeight, margins, cellHeight);
const previewMap = this.previewWidgets ? new Map(this.previewWidgets.map(widget => [widget.id, widget])) : null;
return html`
<div class="grid-container" style="height: ${gridHeight}px;">
${this.showGridLines ? this.renderGridLines(metrics, gridHeight) : null}
${this.widgets.map(widget => this.renderWidget(widget, metrics, margins))}
${baseWidgets.map(widget => this.renderWidget(widget, metrics, margins, previewMap))}
${this.placeholderPosition ? this.renderPlaceholder(metrics, margins) : null}
</div>
`;
@@ -199,11 +206,14 @@ export class DeesDashboardgrid extends DeesElement {
widget: DashboardWidget,
metrics: GridCellMetrics,
margins: DashboardResolvedMargins,
previewMap: Map<string, DashboardWidget> | null,
): TemplateResult {
const isDragging = this.dragState?.widgetId === widget.id;
const isResizing = this.resizeState?.widgetId === widget.id;
const isLocked = widget.locked || !this.editable;
const rect = this.computeWidgetRect(widget, metrics, margins);
const previewWidget = previewMap?.get(widget.id) ?? null;
const layoutForRender = isDragging ? widget : previewWidget ?? widget;
const rect = this.computeWidgetRect(layoutForRender, metrics, margins);
const sideProperty = this.rtl ? 'right' : 'left';
const sideValue = this.pxToPercent(rect.left, metrics.containerWidth);
@@ -322,6 +332,7 @@ export class DeesDashboardgrid extends DeesElement {
offsetX: event.clientX - widgetRect.left,
offsetY: event.clientY - widgetRect.top,
start: { id: widget.id, x: widget.x, y: widget.y, w: widget.w, h: widget.h },
previousPosition: { id: widget.id, x: widget.x, y: widget.y, w: widget.w, h: widget.h },
currentPointer: { clientX: event.clientX, clientY: event.clientY },
lastPlacement: null,
};
@@ -337,11 +348,14 @@ export class DeesDashboardgrid extends DeesElement {
private handleDragMove = (event: PointerEvent): void => {
if (!this.dragState) return;
const metrics = this.ensureMetrics();
const widget = this.widgets.find(item => item.id === this.dragState!.widgetId);
const activeWidgets = this.widgets;
const widget = activeWidgets.find(item => item.id === this.dragState!.widgetId);
if (!widget) return;
event.preventDefault();
const previousPosition = this.dragState.previousPosition;
const coords = computeGridCoordinates({
pointer: { clientX: event.clientX, clientY: event.clientY },
containerRect: this.containerBounds ?? this.getBoundingClientRect(),
@@ -353,15 +367,40 @@ export class DeesDashboardgrid extends DeesElement {
dragOffsetY: this.dragState.offsetY,
});
const placement = resolveWidgetPlacement(this.widgets, widget.id, { x: coords.x, y: coords.y }, this.columns);
const placement = resolveWidgetPlacement(
activeWidgets,
widget.id,
{ x: coords.x, y: coords.y },
this.columns,
previousPosition,
);
if (placement) {
const updatedWidget = placement.widgets.find(item => item.id === widget.id);
this.dragState = {
...this.dragState,
currentPointer: { clientX: event.clientX, clientY: event.clientY },
lastPlacement: placement,
previousPosition: updatedWidget
? { id: updatedWidget.id, x: updatedWidget.x, y: updatedWidget.y, w: updatedWidget.w, h: updatedWidget.h }
: { id: widget.id, x: coords.x, y: coords.y, w: widget.w, h: widget.h },
};
this.previewWidgets = placement.widgets;
const previewWidget = placement.widgets.find(item => item.id === widget.id);
if (previewWidget) {
this.placeholderPosition = {
id: previewWidget.id,
x: previewWidget.x,
y: previewWidget.y,
w: previewWidget.w,
h: previewWidget.h,
};
} else {
this.placeholderPosition = { id: widget.id, x: coords.x, y: coords.y, w: widget.w, h: widget.h };
}
} else {
this.previewWidgets = null;
this.placeholderPosition = null;
}
this.requestUpdate();
};
@@ -372,10 +411,18 @@ export class DeesDashboardgrid extends DeesElement {
return;
}
const layoutSource = this.widgets;
this.previewWidgets = null;
const target = this.placeholderPosition ?? dragState.start;
const placement =
dragState.lastPlacement ??
resolveWidgetPlacement(this.widgets, dragState.widgetId, { x: target.x, y: target.y }, this.columns);
resolveWidgetPlacement(
layoutSource,
dragState.widgetId,
{ x: target.x, y: target.y },
this.columns,
dragState.previousPosition,
);
if (placement) {
this.commitPlacement(placement, dragState.widgetId, 'widget-move');
@@ -423,7 +470,8 @@ export class DeesDashboardgrid extends DeesElement {
private handleResizeMove = (event: PointerEvent): void => {
if (!this.resizeState) return;
const metrics = this.ensureMetrics();
const widget = this.widgets.find(item => item.id === this.resizeState!.widgetId);
const activeWidgets = this.widgets;
const widget = activeWidgets.find(item => item.id === this.resizeState!.widgetId);
if (!widget) return;
event.preventDefault();
@@ -441,15 +489,37 @@ export class DeesDashboardgrid extends DeesElement {
});
const placement = resolveWidgetPlacement(
this.widgets,
activeWidgets,
widget.id,
{ x: widget.x, y: widget.y, w: nextSize.width, h: nextSize.height },
this.columns,
this.resizeState.start,
);
if (placement) {
this.resizeState = { ...this.resizeState, lastPlacement: placement };
this.placeholderPosition = { id: widget.id, x: widget.x, y: widget.y, w: nextSize.width, h: nextSize.height };
this.previewWidgets = placement.widgets;
const previewWidget = placement.widgets.find(item => item.id === widget.id);
if (previewWidget) {
this.placeholderPosition = {
id: previewWidget.id,
x: previewWidget.x,
y: previewWidget.y,
w: previewWidget.w,
h: previewWidget.h,
};
} else {
this.placeholderPosition = {
id: widget.id,
x: widget.x,
y: widget.y,
w: nextSize.width,
h: nextSize.height,
};
}
} else {
this.previewWidgets = null;
this.placeholderPosition = null;
}
this.requestUpdate();
@@ -461,7 +531,22 @@ export class DeesDashboardgrid extends DeesElement {
return;
}
const placement = resizeState.lastPlacement;
const layoutSource = this.widgets;
this.previewWidgets = null;
const placement =
resizeState.lastPlacement ??
resolveWidgetPlacement(
layoutSource,
resizeState.widgetId,
{
x: this.placeholderPosition?.x ?? resizeState.start.x,
y: this.placeholderPosition?.y ?? resizeState.start.y,
w: this.placeholderPosition?.w ?? resizeState.start.w,
h: this.placeholderPosition?.h ?? resizeState.start.h,
},
this.columns,
resizeState.start,
);
if (placement) {
this.commitPlacement(placement, resizeState.widgetId, 'widget-resize');
@@ -545,6 +630,7 @@ export class DeesDashboardgrid extends DeesElement {
}
private commitPlacement(result: PlacementResult, widgetId: string, type: 'widget-move' | 'widget-resize'): void {
this.previewWidgets = null;
this.widgets = result.widgets;
const subject = this.widgets.find(item => item.id === widgetId);
if (subject) {

View File

@@ -129,6 +129,7 @@ export const resolveWidgetPlacement = (
widgetId: string,
next: { x: number; y: number; w?: number; h?: number },
columns: number,
previousPosition?: DashboardLayoutItem,
): PlacementResult | null => {
const sourceWidgets = cloneWidgets(widgets);
const moving = sourceWidgets.find(widget => widget.id === widgetId);
@@ -160,8 +161,9 @@ export const resolveWidgetPlacement = (
if (!other.locked && !other.noMove && other.w === moving.w && other.h === moving.h) {
const otherClone = sourceWidgets.find(widget => widget.id === other.id);
if (otherClone) {
otherClone.x = original.x;
otherClone.y = original.y;
const swapTarget = previousPosition ?? original;
otherClone.x = swapTarget.x;
otherClone.y = swapTarget.y;
return { widgets: sourceWidgets, movedWidgets: [moving.id, otherClone.id], swappedWith: otherClone.id };
}
}