diff --git a/readme.plan.md b/readme.plan.md index 146d4d2..d0d0bc5 100644 Binary files a/readme.plan.md and b/readme.plan.md differ diff --git a/ts_web/elements/dees-dashboardgrid/dees-dashboardgrid.demo.ts b/ts_web/elements/dees-dashboardgrid/dees-dashboardgrid.demo.ts index a9f413d..8254083 100644 --- a/ts_web/elements/dees-dashboardgrid/dees-dashboardgrid.demo.ts +++ b/ts_web/elements/dees-dashboardgrid/dees-dashboardgrid.demo.ts @@ -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(); diff --git a/ts_web/elements/dees-dashboardgrid/dees-dashboardgrid.ts b/ts_web/elements/dees-dashboardgrid/dees-dashboardgrid.ts index 02617c5..7a60801 100644 --- a/ts_web/elements/dees-dashboardgrid/dees-dashboardgrid.ts +++ b/ts_web/elements/dees-dashboardgrid/dees-dashboardgrid.ts @@ -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 { + await super.connectedCallback(); this.computeMetrics(); this.observeResize(); } - disconnectedCallback(): void { - super.disconnectedCallback(); + public override async disconnectedCallback(): Promise { + 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`
@@ -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`
${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}
`; @@ -199,11 +206,14 @@ export class DeesDashboardgrid extends DeesElement { widget: DashboardWidget, metrics: GridCellMetrics, margins: DashboardResolvedMargins, + previewMap: Map | 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) { diff --git a/ts_web/elements/dees-dashboardgrid/layout.ts b/ts_web/elements/dees-dashboardgrid/layout.ts index 018cee4..4de43cc 100644 --- a/ts_web/elements/dees-dashboardgrid/layout.ts +++ b/ts_web/elements/dees-dashboardgrid/layout.ts @@ -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 }; } }