feat(dashboard): auf jedem Bildschirm dasselbe Bild – Leinwand je Reiter, massstaeblich skaliert
Je Reiter wird die Flaeche des ersten Desktop-Bildschirms als __canvas im Layout-JSON gespeichert (ohne API/DB-Aenderung, GRID_VERSION bleibt 3). Das Raster rendert in Leinwandbreite und wird per transform: scale(min(bw/cw, bh/ch)) eingepasst, Schrift eingeschlossen, ohne Scrollen; unter 768 px wie bisher. Ziehen/Groesse aendern unter Skalierung ueber eine eigene Positionsstrategie (createScaledStrategy aus react-grid-layout 2.2.3 rechnet den Rasterversatz falsch). Im Browser nachgewiesen: 1920x1080 -> 1366x768 (Faktor 0,66), Ziehen +200 px folgt der Maus, 700 px ohne Skalierung. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { Children, isValidElement } from 'react';
|
||||
import { createScaledStrategy, defaultPositionStrategy } from 'react-grid-layout/core';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { stubResizeObserver } from '@/test/fake-resize-observer';
|
||||
import { RESIZE_AXIS_FALLBACK } from './dashboard-grid';
|
||||
import {
|
||||
computeCanvasScale,
|
||||
DASHBOARD_BOTTOM_GAP_PX,
|
||||
RESIZE_AXIS_FALLBACK,
|
||||
scaledPositionStrategy,
|
||||
} from './dashboard-grid';
|
||||
|
||||
// Mock CSS imports that vitest cannot resolve
|
||||
vi.mock('react-grid-layout/css/styles.css', () => ({}));
|
||||
@@ -652,3 +658,139 @@ describe('RESIZE_AXIS_FALLBACK (Design „Mosaik“, Runde 3)', () => {
|
||||
expect(RESIZE_AXIS_FALLBACK.constrainSize?.(item, 3, 9, 'se', context)).toEqual({ w: 3, h: 9 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Leinwand: Dashboard wie ein Bild mitskalieren', () => {
|
||||
const ONE = {
|
||||
layouts: { lg: [{ i: 'inst-1', x: 0, y: 0, w: 8, h: 4 }], md: [], sm: [], xs: [], xxs: [] },
|
||||
widgets: [{ id: 'inst-1', widgetType: 'clock', config: {} }],
|
||||
};
|
||||
// jsdom: innerHeight 768, getBoundingClientRect -> 0 -> verfuegbare Hoehe 748.
|
||||
const JSDOM_AVAILABLE_H = 768 - DASHBOARD_BOTTOM_GAP_PX;
|
||||
|
||||
function setInnerHeight(value: number) {
|
||||
Object.defineProperty(window, 'innerHeight', { value, configurable: true, writable: true });
|
||||
}
|
||||
afterEach(() => setInnerHeight(768));
|
||||
|
||||
async function renderGrid(props: {
|
||||
canvas?: { w: number; h: number } | null;
|
||||
onCaptureCanvas?: (area: { w: number; h: number }) => void;
|
||||
}) {
|
||||
captured.props = null;
|
||||
const { DashboardGrid } = await import('./dashboard-grid');
|
||||
const view = render(
|
||||
<DashboardGrid
|
||||
{...ONE}
|
||||
isEditMode={false}
|
||||
onLayoutChange={vi.fn()}
|
||||
onRemoveWidget={vi.fn()}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
const outer = view.container.firstElementChild as HTMLElement;
|
||||
const inner = outer.firstElementChild as HTMLElement;
|
||||
return { outer, inner };
|
||||
}
|
||||
|
||||
describe('computeCanvasScale', () => {
|
||||
it('breitenbegrenzt: freier Platz unten', () => {
|
||||
expect(computeCanvasScale({ w: 1280, h: 900 }, { w: 1600, h: 800 })).toBe(0.8);
|
||||
});
|
||||
it('hoehenbegrenzt: freier Platz rechts', () => {
|
||||
expect(computeCanvasScale({ w: 1920, h: 600 }, { w: 1600, h: 800 })).toBe(0.75);
|
||||
});
|
||||
it('Vergroesserung bei groesserem Bildschirm (s > 1)', () => {
|
||||
expect(computeCanvasScale({ w: 2400, h: 1300 }, { w: 1600, h: 800 })).toBe(1.5);
|
||||
});
|
||||
it('gleiche Flaeche -> 1', () => {
|
||||
expect(computeCanvasScale({ w: 1600, h: 800 }, { w: 1600, h: 800 })).toBe(1);
|
||||
});
|
||||
it('unbrauchbare Hoehe begrenzt nicht, dann zaehlt nur die Breite', () => {
|
||||
expect(computeCanvasScale({ w: 800, h: 0 }, { w: 1600, h: 800 })).toBe(0.5);
|
||||
expect(computeCanvasScale({ w: 800, h: Number.NaN }, { w: 1600, h: 800 })).toBe(0.5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scaledPositionStrategy (Ziehen/Groesse aendern unter transform)', () => {
|
||||
it('s = 1 ist die Standardstrategie', () => {
|
||||
expect(scaledPositionStrategy(1)).toBe(defaultPositionStrategy);
|
||||
});
|
||||
|
||||
it('s != 1: transform-Strategie mit scale, OHNE calcDragPosition (RGL rechnet dann relativ zum Raster)', () => {
|
||||
const strategy = scaledPositionStrategy(0.5);
|
||||
expect(strategy.type).toBe('transform');
|
||||
expect(strategy.scale).toBe(0.5);
|
||||
expect(strategy.calcDragPosition).toBeUndefined();
|
||||
expect(strategy.calcStyle({ left: 10, top: 20, width: 30, height: 40 })).toEqual(
|
||||
defaultPositionStrategy.calcStyle({ left: 10, top: 20, width: 30, height: 40 }),
|
||||
);
|
||||
});
|
||||
|
||||
it('Grund: createScaledStrategy rechnet den Ziehstart in Fensterkoordinaten (ohne Raster-Versatz)', () => {
|
||||
// Kachel sichtbar bei clientX 300 (Raster beginnt bei 250 neben der
|
||||
// Seitenleiste, Kachel also 50 px im Raster), Maus 10 px in der Kachel.
|
||||
const pos = createScaledStrategy(0.5).calcDragPosition?.(310, 110, 10, 10);
|
||||
// Richtig waere (300 - 250) / 0.5 = 100; geliefert wird 300 / 0.5 = 600.
|
||||
expect(pos?.left).toBe(600);
|
||||
});
|
||||
});
|
||||
|
||||
it('skalierter Modus: virtuelle Breite canvas.w, transform scale(s) links oben, sichtbare Hoehe = Rasterhoehe x s', async () => {
|
||||
stubResizeObserver({ width: 1000, height: 800 });
|
||||
const { outer, inner } = await renderGrid({ canvas: { w: 2000, h: 800 } });
|
||||
|
||||
// s = min(1000 / 2000, 748 / 800) = 0.5
|
||||
expect(captured.props?.width).toBe(2000);
|
||||
expect(inner.style.width).toBe('2000px');
|
||||
expect(inner.style.transform).toBe('scale(0.5)');
|
||||
expect(inner.style.transformOrigin).toBe('top left');
|
||||
expect(outer.style.height).toBe('400px');
|
||||
expect(outer.style.overflow).toBe('hidden');
|
||||
const strategy = captured.props?.positionStrategy as { scale: number };
|
||||
expect(strategy.scale).toBe(0.5);
|
||||
});
|
||||
|
||||
it('skalierter Modus hoehenbegrenzt und vergroessernd', async () => {
|
||||
stubResizeObserver({ width: 1000, height: 800 });
|
||||
setInnerHeight(420); // verfuegbar 400
|
||||
const first = await renderGrid({ canvas: { w: 1000, h: 800 } });
|
||||
expect(first.inner.style.transform).toBe('scale(0.5)');
|
||||
cleanup();
|
||||
|
||||
setInnerHeight(768);
|
||||
const second = await renderGrid({ canvas: { w: 800, h: 374 } });
|
||||
// min(1000 / 800, 748 / 374) = 1.25
|
||||
expect(second.inner.style.transform).toBe('scale(1.25)');
|
||||
expect(captured.props?.width).toBe(800);
|
||||
});
|
||||
|
||||
it('schmal (< 768): wie bisher — Containerbreite, kein transform, keine Erfassung', async () => {
|
||||
stubResizeObserver({ width: 600, height: 800 });
|
||||
const onCaptureCanvas = vi.fn();
|
||||
const { outer, inner } = await renderGrid({ canvas: { w: 1600, h: 800 }, onCaptureCanvas });
|
||||
expect(captured.props?.width).toBe(600);
|
||||
expect(inner.style.transform).toBe('');
|
||||
expect(outer.style.height).toBe('');
|
||||
expect(captured.props?.positionStrategy).toBe(defaultPositionStrategy);
|
||||
|
||||
cleanup();
|
||||
await renderGrid({ canvas: null, onCaptureCanvas });
|
||||
expect(onCaptureCanvas).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ohne Leinwand am Desktop: unskaliert und die verfuegbare Flaeche wird zum Erfassen gemeldet', async () => {
|
||||
stubResizeObserver({ width: 1000, height: 800 });
|
||||
const onCaptureCanvas = vi.fn();
|
||||
const { inner } = await renderGrid({ canvas: null, onCaptureCanvas });
|
||||
expect(captured.props?.width).toBe(1000);
|
||||
expect(inner.style.transform).toBe('');
|
||||
expect(onCaptureCanvas).toHaveBeenCalledWith({ w: 1000, h: JSDOM_AVAILABLE_H });
|
||||
});
|
||||
|
||||
it('mit Leinwand wird nicht erneut erfasst', async () => {
|
||||
stubResizeObserver({ width: 1000, height: 800 });
|
||||
const onCaptureCanvas = vi.fn();
|
||||
await renderGrid({ canvas: { w: 1000, h: 748 }, onCaptureCanvas });
|
||||
expect(onCaptureCanvas).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,10 +3,17 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { Compactor, ResponsiveLayouts } from 'react-grid-layout';
|
||||
import { noCompactor, Responsive } from 'react-grid-layout';
|
||||
import { defaultConstraints, type LayoutConstraint } from 'react-grid-layout/core';
|
||||
import {
|
||||
defaultConstraints,
|
||||
defaultPositionStrategy,
|
||||
type LayoutConstraint,
|
||||
type PositionStrategy,
|
||||
transformStrategy,
|
||||
} from 'react-grid-layout/core';
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { CANVAS_MIN_HEIGHT, CANVAS_MIN_WIDTH, type GridCanvas } from '@/lib/grid-layout-migration';
|
||||
import {
|
||||
clampWidthToCols,
|
||||
GRID_COLS,
|
||||
@@ -101,6 +108,66 @@ const GRID_CONSTRAINTS: LayoutConstraint[] = [...defaultConstraints, RESIZE_AXIS
|
||||
|
||||
const GRID_MARGIN = 12;
|
||||
|
||||
/**
|
||||
* Dashboard wie ein Bild mitskalieren (Leinwand).
|
||||
*
|
||||
* Wunsch: an PC 1 eingerichtet, an PC 2 mit anderer Aufloesung GENAU gleich
|
||||
* aussehen — alles (auch die Schrift) gleichmaessig kleiner oder groesser,
|
||||
* nie scrollen; passt die Bildschirmform nicht, bleibt rechts oder unten
|
||||
* Platz frei.
|
||||
*
|
||||
* Umsetzung: Jeder Reiter hat eine Leinwand `{ w, h }` (gespeichert als
|
||||
* `__canvas`, siehe grid-layout-migration.ts) — die verfuegbare Flaeche, auf
|
||||
* der er eingerichtet wurde (erfasst beim ersten Oeffnen am Desktop, danach
|
||||
* fest). Wo die Leinwand erfasst wurde, sieht das Dashboard 1:1 aus. Auf
|
||||
* jedem Desktop-Bildschirm zeichnet react-grid-layout das Raster mit der
|
||||
* VIRTUELLEN Breite `canvas.w` — Breakpoint und Spaltenbreite sind damit
|
||||
* ueberall identisch — und ein umgebendes Element verkleinert/vergroessert
|
||||
* das Ergebnis per `transform: scale(s)` mit
|
||||
* `s = min(Breite / canvas.w, Hoehe / canvas.h)` (computeCanvasScale),
|
||||
* links oben ausgerichtet (nicht zentriert, bewusst entfernt in bc4c011).
|
||||
*
|
||||
* Unter CANVAS_MIN_WIDTH (Handy/schmal) bleibt alles wie zuvor: Breite =
|
||||
* gemessene Containerbreite, kein transform, keine Erfassung.
|
||||
*/
|
||||
/** Abstand unter dem Raster bis zur Fensterkante: main p-3 (12) + Wrapper p-2 (8). */
|
||||
export const DASHBOARD_BOTTOM_GAP_PX = 20;
|
||||
|
||||
/**
|
||||
* Skalierungsfaktor der Leinwand fuer die verfuegbare Flaeche: das kleinere
|
||||
* der beiden Verhaeltnisse, damit alles hineinpasst (freier Platz rechts
|
||||
* ODER unten). Eine unbrauchbare Hoehe (<= 0, nicht endlich) begrenzt nicht —
|
||||
* dann entscheidet nur die Breite.
|
||||
*/
|
||||
export function computeCanvasScale(available: GridCanvas, canvas: GridCanvas): number {
|
||||
const byWidth = available.w / canvas.w;
|
||||
const byHeight = available.h / canvas.h;
|
||||
const heightUsable = Number.isFinite(byHeight) && byHeight > 0;
|
||||
const s = heightUsable ? Math.min(byWidth, byHeight) : byWidth;
|
||||
return Number.isFinite(s) && s > 0 ? s : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Positionsstrategie fuer ein per `transform: scale(s)` skaliertes Raster.
|
||||
*
|
||||
* Bewusst NICHT `createScaledStrategy(s)` aus react-grid-layout 2.2.3: deren
|
||||
* `calcDragPosition` rechnet `(clientX - offsetX) / scale` =
|
||||
* `clientRect.left / scale` (chunk-KDANGDDL.mjs:315-320) — eine FENSTER-
|
||||
* Koordinate ohne Abzug der Raster-Oberkante/-Linkskante. Im Portal liegt das
|
||||
* Raster rechts neben der Seitenleiste; die Kachel spraenge beim Ziehstart um
|
||||
* (Seitenleiste + Abstand) / s Pixel nach rechts unten. Ohne
|
||||
* `calcDragPosition` nimmt react-grid-layout den eigenen Weg
|
||||
* (chunk-WGL5FSZH.mjs:146-163): `(clientRect.left - parentRect.left) /
|
||||
* transformScale` — relativ zum Raster und korrekt entskaliert. Die
|
||||
* Mausbewegungen teilen `DraggableCore` (Prop `scale`) bzw. `Resizable`
|
||||
* (`deltaX / transformScale`) selbst durch `scale`; beide lesen ihn aus
|
||||
* `positionStrategy.scale` (chunk-WGL5FSZH.mjs:675, 512, 527).
|
||||
*/
|
||||
export function scaledPositionStrategy(scale: number): PositionStrategy {
|
||||
if (scale === 1) return defaultPositionStrategy;
|
||||
return { type: 'transform', scale, calcStyle: transformStrategy.calcStyle };
|
||||
}
|
||||
|
||||
interface LayoutItemShape {
|
||||
i: string;
|
||||
x: number;
|
||||
@@ -121,6 +188,10 @@ interface DashboardGridProps {
|
||||
onOpenCatalog?: () => void;
|
||||
/** Leerer Zustand: fuegt eine vorgeschlagene Kachel direkt hinzu. */
|
||||
onQuickAdd?: (type: WidgetType) => void;
|
||||
/** Leinwand des aktiven Reiters; null = noch nicht erfasst (dann unskaliert). */
|
||||
canvas?: GridCanvas | null;
|
||||
/** Wird mit der verfuegbaren Flaeche gerufen, solange `canvas` fehlt (nur Desktop). */
|
||||
onCaptureCanvas?: (area: GridCanvas) => void;
|
||||
}
|
||||
|
||||
/** Vorschlaege im leeren Zustand — Plattform-Kacheln, immer verfuegbar. */
|
||||
@@ -224,9 +295,15 @@ export function DashboardGrid({
|
||||
onRemoveWidget,
|
||||
onOpenCatalog,
|
||||
onQuickAdd,
|
||||
canvas = null,
|
||||
onCaptureCanvas,
|
||||
}: DashboardGridProps) {
|
||||
const t = useTranslations('widgets');
|
||||
const [width, setWidth] = useState(1200);
|
||||
// Verfuegbare Hoehe bis zur unteren Fensterkante (Leinwand). 0 = unbekannt.
|
||||
const [availableHeight, setAvailableHeight] = useState(0);
|
||||
// Unskalierte Hoehe des Rasters (inneres Element) fuer die sichtbare Hoehe.
|
||||
const [innerHeight, setInnerHeight] = useState(0);
|
||||
|
||||
// quick-260922-vdk: Messung haengt am eingehaengten Knoten (Ref-Rueckruf),
|
||||
// nicht mehr an einem Effekt mit leerer Abhaengigkeitsliste.
|
||||
@@ -265,6 +342,16 @@ export function DashboardGrid({
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Leinwand: Hoehe = Fensterhoehe minus Oberkante des Rasters im DOKUMENT
|
||||
// (rect.top + scrollY, also unabhaengig vom Scrollstand) minus Abstand
|
||||
// unten. Das aeussere Element ist selbst nicht transformiert, seine
|
||||
// Oberkante ist also die echte.
|
||||
const applyHeight = useCallback((node: HTMLElement) => {
|
||||
const top = node.getBoundingClientRect().top + window.scrollY;
|
||||
const next = window.innerHeight - top - DASHBOARD_BOTTOM_GAP_PX;
|
||||
setAvailableHeight(Number.isFinite(next) && next > 0 ? Math.floor(next) : 0);
|
||||
}, []);
|
||||
|
||||
const measureRef = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
// Ein eventuell laufender Beobachter zuerst trennen — auch der
|
||||
@@ -277,27 +364,64 @@ export function DashboardGrid({
|
||||
|
||||
// Synchrone Erstmessung in der Commit-Phase, vor dem ersten Zeichnen.
|
||||
applyWidth(node.getBoundingClientRect().width);
|
||||
applyHeight(node);
|
||||
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
applyWidth(entries[0].contentRect.width);
|
||||
applyHeight(node);
|
||||
});
|
||||
observer.observe(node);
|
||||
observerRef.current = observer;
|
||||
// Bewusst keine Aufraeumfunktion zurueckgeben: React 19 ruft den
|
||||
// Ref-Rueckruf sonst beim Aushaengen nicht mehr mit null auf.
|
||||
},
|
||||
[applyWidth],
|
||||
[applyWidth, applyHeight],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const onResize = () => {
|
||||
if (nodeRef.current) {
|
||||
applyWidth(nodeRef.current.getBoundingClientRect().width);
|
||||
applyHeight(nodeRef.current);
|
||||
}
|
||||
};
|
||||
window.addEventListener('resize', onResize);
|
||||
return () => window.removeEventListener('resize', onResize);
|
||||
}, [applyWidth]);
|
||||
}, [applyWidth, applyHeight]);
|
||||
|
||||
// Unskalierte Hoehe des inneren Elements (react-grid-layout setzt die
|
||||
// Rasterhoehe selbst). `contentRect`/`offsetHeight` sind Layout-Masse und
|
||||
// vom transform unberuehrt — genau das Gewuenschte.
|
||||
const innerObserverRef = useRef<ResizeObserver | null>(null);
|
||||
const innerRef = useCallback((node: HTMLDivElement | null) => {
|
||||
innerObserverRef.current?.disconnect();
|
||||
innerObserverRef.current = null;
|
||||
if (!node) return;
|
||||
if (node.offsetHeight > 0) setInnerHeight(node.offsetHeight);
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const h = entries[0].contentRect.height;
|
||||
if (Number.isFinite(h) && h > 0) setInnerHeight(h);
|
||||
});
|
||||
observer.observe(node);
|
||||
innerObserverRef.current = observer;
|
||||
}, []);
|
||||
|
||||
// Desktop = verfuegbare Breite ab CANVAS_MIN_WIDTH; nur dort Leinwand.
|
||||
const isDesktop = width >= CANVAS_MIN_WIDTH;
|
||||
const scaled = isDesktop && canvas !== null;
|
||||
const scale = scaled ? computeCanvasScale({ w: width, h: availableHeight }, canvas) : 1;
|
||||
const positionStrategy = useMemo(() => scaledPositionStrategy(scale), [scale]);
|
||||
|
||||
// Leinwand erfassen: nur mit eingehaengtem Raster (also mit Kacheln), am
|
||||
// Desktop und mit brauchbarer Hoehe. Der Store uebernimmt sie einmal und
|
||||
// ignoriert weitere Aufrufe; danach ist `canvas` gesetzt und dieser
|
||||
// Effekt tut nichts mehr.
|
||||
const hasWidgets = widgets.length > 0;
|
||||
useEffect(() => {
|
||||
if (!onCaptureCanvas || canvas !== null || !hasWidgets) return;
|
||||
if (!isDesktop || availableHeight < CANVAS_MIN_HEIGHT) return;
|
||||
onCaptureCanvas({ w: width, h: availableHeight });
|
||||
}, [onCaptureCanvas, canvas, hasWidgets, isDesktop, width, availableHeight]);
|
||||
|
||||
// quick-260916-dyv: minW/minH (und zu kleine w/h) aus WIDGET_CONSTRAINTS —
|
||||
// siehe applyConstraintMinima. Vor dem Leerzustand, damit die Hook-Reihenfolge
|
||||
@@ -374,56 +498,72 @@ export function DashboardGrid({
|
||||
);
|
||||
}
|
||||
|
||||
// Gleiche DOM-Struktur in beiden Modi, damit react-grid-layout beim
|
||||
// Wechsel (etwa direkt nach dem Erfassen, s = 1) nicht neu einhaengt.
|
||||
// Aeusseres Element: volle Breite (Messung), im skalierten Modus die
|
||||
// sichtbare Hoehe = unskalierte Rasterhoehe x s und `overflow: hidden` —
|
||||
// der transform aendert die Layout-Box nicht, ohne beides bliebe toter
|
||||
// Scrollraum (s < 1) bzw. ragte die 1:1-Breite seitlich hinaus.
|
||||
const outerStyle: React.CSSProperties | undefined = scaled
|
||||
? { overflow: 'hidden', ...(innerHeight > 0 ? { height: innerHeight * scale } : {}) }
|
||||
: undefined;
|
||||
const innerStyle: React.CSSProperties | undefined = scaled
|
||||
? { width: canvas.w, transform: `scale(${scale})`, transformOrigin: 'top left' }
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div ref={measureRef}>
|
||||
<Responsive
|
||||
width={width}
|
||||
breakpoints={BREAKPOINTS}
|
||||
cols={COLS}
|
||||
layouts={effectiveLayouts as ResponsiveLayouts}
|
||||
rowHeight={20}
|
||||
margin={[GRID_MARGIN, GRID_MARGIN] as [number, number]}
|
||||
constraints={GRID_CONSTRAINTS}
|
||||
// containerPadding bewusst nicht gesetzt: react-grid-layout 2.2.3 nimmt
|
||||
// dann das margin (gemessen: effectiveContainerPadding = containerPadding ?? margin).
|
||||
dragConfig={{
|
||||
enabled: isEditMode,
|
||||
handle: WIDGET_DRAG_HANDLE_SELECTOR,
|
||||
cancel: WIDGET_DRAG_CANCEL_SELECTOR,
|
||||
threshold: 3,
|
||||
}}
|
||||
resizeConfig={{
|
||||
enabled: isEditMode,
|
||||
}}
|
||||
compactor={FREE_PLACEMENT_COMPACTOR}
|
||||
onLayoutChange={(_currentLayout, allLayouts) => onLayoutChange(allLayouts)}
|
||||
>
|
||||
{widgets.map((widget, index) => {
|
||||
const constraints = WIDGET_CONSTRAINTS[widget.widgetType as WidgetType];
|
||||
return (
|
||||
<div
|
||||
key={widget.id}
|
||||
data-grid={{
|
||||
...(effectiveLayouts.lg?.find((l) => l.i === widget.id) ?? {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: constraints?.defaultW ?? 8,
|
||||
h: constraints?.defaultH ?? 4,
|
||||
}),
|
||||
minW: constraints?.minW ?? 8,
|
||||
minH: constraints?.minH ?? 4,
|
||||
}}
|
||||
>
|
||||
<WidgetWrapper
|
||||
widget={widget}
|
||||
isEditMode={isEditMode}
|
||||
onRemove={onRemoveWidget}
|
||||
enterIndex={index}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Responsive>
|
||||
<div ref={measureRef} style={outerStyle} data-canvas-scale={scaled ? scale : undefined}>
|
||||
<div ref={innerRef} style={innerStyle}>
|
||||
<Responsive
|
||||
width={scaled ? canvas.w : width}
|
||||
positionStrategy={positionStrategy}
|
||||
breakpoints={BREAKPOINTS}
|
||||
cols={COLS}
|
||||
layouts={effectiveLayouts as ResponsiveLayouts}
|
||||
rowHeight={20}
|
||||
margin={[GRID_MARGIN, GRID_MARGIN] as [number, number]}
|
||||
constraints={GRID_CONSTRAINTS}
|
||||
// containerPadding bewusst nicht gesetzt: react-grid-layout 2.2.3 nimmt
|
||||
// dann das margin (gemessen: effectiveContainerPadding = containerPadding ?? margin).
|
||||
dragConfig={{
|
||||
enabled: isEditMode,
|
||||
handle: WIDGET_DRAG_HANDLE_SELECTOR,
|
||||
cancel: WIDGET_DRAG_CANCEL_SELECTOR,
|
||||
threshold: 3,
|
||||
}}
|
||||
resizeConfig={{
|
||||
enabled: isEditMode,
|
||||
}}
|
||||
compactor={FREE_PLACEMENT_COMPACTOR}
|
||||
onLayoutChange={(_currentLayout, allLayouts) => onLayoutChange(allLayouts)}
|
||||
>
|
||||
{widgets.map((widget, index) => {
|
||||
const constraints = WIDGET_CONSTRAINTS[widget.widgetType as WidgetType];
|
||||
return (
|
||||
<div
|
||||
key={widget.id}
|
||||
data-grid={{
|
||||
...(effectiveLayouts.lg?.find((l) => l.i === widget.id) ?? {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: constraints?.defaultW ?? 8,
|
||||
h: constraints?.defaultH ?? 4,
|
||||
}),
|
||||
minW: constraints?.minW ?? 8,
|
||||
minH: constraints?.minH ?? 4,
|
||||
}}
|
||||
>
|
||||
<WidgetWrapper
|
||||
widget={widget}
|
||||
isEditMode={isEditMode}
|
||||
onRemove={onRemoveWidget}
|
||||
enterIndex={index}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Responsive>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user