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