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); | ||||
|         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(); | ||||
|  | ||||
|   | ||||
| @@ -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,14 +367,39 @@ 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.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(); | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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 }; | ||||
|       } | ||||
|     } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user