feat(dees-dashboardgrid): enhance drag-and-drop functionality with preview state and previous position tracking
This commit is contained in:
BIN
readme.plan.md
BIN
readme.plan.md
Binary file not shown.
@@ -89,10 +89,12 @@ export const demoFunc = () => {
|
|||||||
grid.applyBreakpointLayout(target);
|
grid.applyBreakpointLayout(target);
|
||||||
updateStatus();
|
updateStatus();
|
||||||
};
|
};
|
||||||
if ('addEventListener' in mediaQuery) {
|
if (typeof mediaQuery.addEventListener === 'function') {
|
||||||
mediaQuery.addEventListener('change', handleBreakpoint);
|
mediaQuery.addEventListener('change', handleBreakpoint);
|
||||||
} else {
|
} else {
|
||||||
mediaQuery.addListener(handleBreakpoint);
|
(mediaQuery as MediaQueryList & {
|
||||||
|
addListener?: (listener: (this: MediaQueryList, ev: MediaQueryListEvent) => void) => void;
|
||||||
|
}).addListener?.(handleBreakpoint);
|
||||||
}
|
}
|
||||||
handleBreakpoint();
|
handleBreakpoint();
|
||||||
|
|
||||||
|
@@ -49,6 +49,7 @@ type DragState = {
|
|||||||
offsetX: number;
|
offsetX: number;
|
||||||
offsetY: number;
|
offsetY: number;
|
||||||
start: DashboardLayoutItem;
|
start: DashboardLayoutItem;
|
||||||
|
previousPosition: DashboardLayoutItem;
|
||||||
currentPointer: PointerPosition;
|
currentPointer: PointerPosition;
|
||||||
lastPlacement: PlacementResult | null;
|
lastPlacement: PlacementResult | null;
|
||||||
};
|
};
|
||||||
@@ -111,20 +112,23 @@ export class DeesDashboardgrid extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
private resolvedMargins: DashboardResolvedMargins | null = null;
|
private resolvedMargins: DashboardResolvedMargins | null = null;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private previewWidgets: DashboardWidget[] | null = null;
|
||||||
|
|
||||||
private containerBounds: DOMRect | null = null;
|
private containerBounds: DOMRect | null = null;
|
||||||
private dragState: DragState | null = null;
|
private dragState: DragState | null = null;
|
||||||
private resizeState: ResizeState | null = null;
|
private resizeState: ResizeState | null = null;
|
||||||
private resizeObserver?: ResizeObserver;
|
private resizeObserver?: ResizeObserver;
|
||||||
private interactionActive = false;
|
private interactionActive = false;
|
||||||
|
|
||||||
connectedCallback(): void {
|
public override async connectedCallback(): Promise<void> {
|
||||||
super.connectedCallback();
|
await super.connectedCallback();
|
||||||
this.computeMetrics();
|
this.computeMetrics();
|
||||||
this.observeResize();
|
this.observeResize();
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback(): void {
|
public override async disconnectedCallback(): Promise<void> {
|
||||||
super.disconnectedCallback();
|
await super.disconnectedCallback();
|
||||||
this.disconnectResizeObserver();
|
this.disconnectResizeObserver();
|
||||||
this.releasePointerEvents();
|
this.releasePointerEvents();
|
||||||
}
|
}
|
||||||
@@ -145,7 +149,8 @@ export class DeesDashboardgrid extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
if (this.widgets.length === 0) {
|
const baseWidgets = this.widgets;
|
||||||
|
if (baseWidgets.length === 0) {
|
||||||
return html`
|
return html`
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<dees-icon .icon=${'lucide:layoutGrid'}></dees-icon>
|
<dees-icon .icon=${'lucide:layoutGrid'}></dees-icon>
|
||||||
@@ -158,12 +163,14 @@ export class DeesDashboardgrid extends DeesElement {
|
|||||||
const metrics = this.ensureMetrics();
|
const metrics = this.ensureMetrics();
|
||||||
const margins = this.resolvedMargins ?? resolveMargins(this.margin);
|
const margins = this.resolvedMargins ?? resolveMargins(this.margin);
|
||||||
const cellHeight = metrics.cellHeightPx;
|
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`
|
return html`
|
||||||
<div class="grid-container" style="height: ${gridHeight}px;">
|
<div class="grid-container" style="height: ${gridHeight}px;">
|
||||||
${this.showGridLines ? this.renderGridLines(metrics, gridHeight) : null}
|
${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}
|
${this.placeholderPosition ? this.renderPlaceholder(metrics, margins) : null}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -199,11 +206,14 @@ export class DeesDashboardgrid extends DeesElement {
|
|||||||
widget: DashboardWidget,
|
widget: DashboardWidget,
|
||||||
metrics: GridCellMetrics,
|
metrics: GridCellMetrics,
|
||||||
margins: DashboardResolvedMargins,
|
margins: DashboardResolvedMargins,
|
||||||
|
previewMap: Map<string, DashboardWidget> | null,
|
||||||
): TemplateResult {
|
): TemplateResult {
|
||||||
const isDragging = this.dragState?.widgetId === widget.id;
|
const isDragging = this.dragState?.widgetId === widget.id;
|
||||||
const isResizing = this.resizeState?.widgetId === widget.id;
|
const isResizing = this.resizeState?.widgetId === widget.id;
|
||||||
const isLocked = widget.locked || !this.editable;
|
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 sideProperty = this.rtl ? 'right' : 'left';
|
||||||
const sideValue = this.pxToPercent(rect.left, metrics.containerWidth);
|
const sideValue = this.pxToPercent(rect.left, metrics.containerWidth);
|
||||||
@@ -322,6 +332,7 @@ export class DeesDashboardgrid extends DeesElement {
|
|||||||
offsetX: event.clientX - widgetRect.left,
|
offsetX: event.clientX - widgetRect.left,
|
||||||
offsetY: event.clientY - widgetRect.top,
|
offsetY: event.clientY - widgetRect.top,
|
||||||
start: { id: widget.id, x: widget.x, y: widget.y, w: widget.w, h: widget.h },
|
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 },
|
currentPointer: { clientX: event.clientX, clientY: event.clientY },
|
||||||
lastPlacement: null,
|
lastPlacement: null,
|
||||||
};
|
};
|
||||||
@@ -337,11 +348,14 @@ export class DeesDashboardgrid extends DeesElement {
|
|||||||
private handleDragMove = (event: PointerEvent): void => {
|
private handleDragMove = (event: PointerEvent): void => {
|
||||||
if (!this.dragState) return;
|
if (!this.dragState) return;
|
||||||
const metrics = this.ensureMetrics();
|
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;
|
if (!widget) return;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
const previousPosition = this.dragState.previousPosition;
|
||||||
|
|
||||||
const coords = computeGridCoordinates({
|
const coords = computeGridCoordinates({
|
||||||
pointer: { clientX: event.clientX, clientY: event.clientY },
|
pointer: { clientX: event.clientX, clientY: event.clientY },
|
||||||
containerRect: this.containerBounds ?? this.getBoundingClientRect(),
|
containerRect: this.containerBounds ?? this.getBoundingClientRect(),
|
||||||
@@ -353,14 +367,39 @@ export class DeesDashboardgrid extends DeesElement {
|
|||||||
dragOffsetY: this.dragState.offsetY,
|
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) {
|
if (placement) {
|
||||||
|
const updatedWidget = placement.widgets.find(item => item.id === widget.id);
|
||||||
this.dragState = {
|
this.dragState = {
|
||||||
...this.dragState,
|
...this.dragState,
|
||||||
currentPointer: { clientX: event.clientX, clientY: event.clientY },
|
currentPointer: { clientX: event.clientX, clientY: event.clientY },
|
||||||
lastPlacement: placement,
|
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.placeholderPosition = { 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();
|
this.requestUpdate();
|
||||||
@@ -372,10 +411,18 @@ export class DeesDashboardgrid extends DeesElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const layoutSource = this.widgets;
|
||||||
|
this.previewWidgets = null;
|
||||||
const target = this.placeholderPosition ?? dragState.start;
|
const target = this.placeholderPosition ?? dragState.start;
|
||||||
const placement =
|
const placement =
|
||||||
dragState.lastPlacement ??
|
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) {
|
if (placement) {
|
||||||
this.commitPlacement(placement, dragState.widgetId, 'widget-move');
|
this.commitPlacement(placement, dragState.widgetId, 'widget-move');
|
||||||
@@ -423,7 +470,8 @@ export class DeesDashboardgrid extends DeesElement {
|
|||||||
private handleResizeMove = (event: PointerEvent): void => {
|
private handleResizeMove = (event: PointerEvent): void => {
|
||||||
if (!this.resizeState) return;
|
if (!this.resizeState) return;
|
||||||
const metrics = this.ensureMetrics();
|
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;
|
if (!widget) return;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -441,15 +489,37 @@ export class DeesDashboardgrid extends DeesElement {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const placement = resolveWidgetPlacement(
|
const placement = resolveWidgetPlacement(
|
||||||
this.widgets,
|
activeWidgets,
|
||||||
widget.id,
|
widget.id,
|
||||||
{ x: widget.x, y: widget.y, w: nextSize.width, h: nextSize.height },
|
{ x: widget.x, y: widget.y, w: nextSize.width, h: nextSize.height },
|
||||||
this.columns,
|
this.columns,
|
||||||
|
this.resizeState.start,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (placement) {
|
if (placement) {
|
||||||
this.resizeState = { ...this.resizeState, lastPlacement: 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();
|
this.requestUpdate();
|
||||||
@@ -461,7 +531,22 @@ export class DeesDashboardgrid extends DeesElement {
|
|||||||
return;
|
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) {
|
if (placement) {
|
||||||
this.commitPlacement(placement, resizeState.widgetId, 'widget-resize');
|
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 {
|
private commitPlacement(result: PlacementResult, widgetId: string, type: 'widget-move' | 'widget-resize'): void {
|
||||||
|
this.previewWidgets = null;
|
||||||
this.widgets = result.widgets;
|
this.widgets = result.widgets;
|
||||||
const subject = this.widgets.find(item => item.id === widgetId);
|
const subject = this.widgets.find(item => item.id === widgetId);
|
||||||
if (subject) {
|
if (subject) {
|
||||||
|
@@ -129,6 +129,7 @@ export const resolveWidgetPlacement = (
|
|||||||
widgetId: string,
|
widgetId: string,
|
||||||
next: { x: number; y: number; w?: number; h?: number },
|
next: { x: number; y: number; w?: number; h?: number },
|
||||||
columns: number,
|
columns: number,
|
||||||
|
previousPosition?: DashboardLayoutItem,
|
||||||
): PlacementResult | null => {
|
): PlacementResult | null => {
|
||||||
const sourceWidgets = cloneWidgets(widgets);
|
const sourceWidgets = cloneWidgets(widgets);
|
||||||
const moving = sourceWidgets.find(widget => widget.id === widgetId);
|
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) {
|
if (!other.locked && !other.noMove && other.w === moving.w && other.h === moving.h) {
|
||||||
const otherClone = sourceWidgets.find(widget => widget.id === other.id);
|
const otherClone = sourceWidgets.find(widget => widget.id === other.id);
|
||||||
if (otherClone) {
|
if (otherClone) {
|
||||||
otherClone.x = original.x;
|
const swapTarget = previousPosition ?? original;
|
||||||
otherClone.y = original.y;
|
otherClone.x = swapTarget.x;
|
||||||
|
otherClone.y = swapTarget.y;
|
||||||
return { widgets: sourceWidgets, movedWidgets: [moving.id, otherClone.id], swappedWith: otherClone.id };
|
return { widgets: sourceWidgets, movedWidgets: [moving.id, otherClone.id], swappedWith: otherClone.id };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user