added gridstack

This commit is contained in:
Pit Friedrich
2026-07-25 21:56:47 +02:00
parent c2d29f0635
commit bcf604b7c5
7 changed files with 73 additions and 7 deletions
+2
View File
@@ -32,6 +32,8 @@ Vaadin Flow (server-side Java UI, no hand-written HTML/JS for views) on Spring B
Since `apex-chart.ts` renders into light DOM (`createRenderRoot()` returns `this`), global CSS can reach into the chart markup — but series/legend/grid colors are driven entirely by the options JSON, not CSS, because ApexCharts renders its own SVG/canvas. Since `apex-chart.ts` renders into light DOM (`createRenderRoot()` returns `this`), global CSS can reach into the chart markup — but series/legend/grid colors are driven entirely by the options JSON, not CSS, because ApexCharts renders its own SVG/canvas.
**`GridStackLayout`/`GridStackItem`** (`components/`) wrap [gridstack.js](https://gridstack.js.org) 13.1.0 for draggable/resizable grids. Unlike `ApexChart`, the bridge (`frontend/components/grid-stack.ts`) is a plain `HTMLElement`, not a Lit component — its children are server-rendered `GridStackItem`s living in light DOM, and a Lit render root would fight gridstack for ownership of them. A `MutationObserver` calls `makeWidget`/`removeWidget` as Flow adds/removes children, so there's no explicit add/remove protocol to the client. Every `GridStackItem` needs a stable `gs-id` (auto-generated if not given) — `GridStackLayout.setStorageKey(...)` persists drag/resize state to browser `localStorage` keyed on it and restores by matching ids, so items lose their saved position if their id changes between reloads. Item styling (`.grid-stack-item-content`) extends the `--dialect-*` alias layer in `styles.css`, same as `.dialect-card`.
**Views** (`views/`): `MainLayout` (`@Layout`, applies to all routes) is the `AppLayout` shell — navbar + `SideNav` drawer, with one `SideNavItem` per route. Routes: `DashboardView` (`@Route("")`) wraps each chart in a `Card` (`components/Card.java`); `FormView` (`@Route("formular")`) demonstrates form controls bound via `Binder`; `TableView` (`@Route("tabelle")`) demonstrates a `Grid` over dummy data with a live text filter (`GridListDataView.addFilter`, `TextField` in `ValueChangeMode.EAGER`). New views should reuse `Card` to wrap their content rather than adding components directly, and get a matching `SideNavItem` in `MainLayout`. **Views** (`views/`): `MainLayout` (`@Layout`, applies to all routes) is the `AppLayout` shell — navbar + `SideNav` drawer, with one `SideNavItem` per route. Routes: `DashboardView` (`@Route("")`) wraps each chart in a `Card` (`components/Card.java`); `FormView` (`@Route("formular")`) demonstrates form controls bound via `Binder`; `TableView` (`@Route("tabelle")`) demonstrates a `Grid` over dummy data with a live text filter (`GridListDataView.addFilter`, `TextField` in `ValueChangeMode.EAGER`). New views should reuse `Card` to wrap their content rather than adding components directly, and get a matching `SideNavItem` in `MainLayout`.
**Styling**: `src/main/resources/META-INF/resources/styles.css` is the one project-level stylesheet (loaded via `@StyleSheet("styles.css")` in `Application.java`). It defines `--dialect-*` design tokens (Dialect design system: primary orange `#E86C00`, cool-gray background, card radius/shadow) and aliases them onto Aura's own CSS custom properties (`--aura-accent-color-*`, `--aura-background-color-*`, `--aura-orange`, `--aura-yellow`) rather than fighting the theme. Aura tokens use OKLCH + relative-color syntax and accept plain hex overrides. When restyling, prefer extending this alias layer over hardcoding new colors in components. **Styling**: `src/main/resources/META-INF/resources/styles.css` is the one project-level stylesheet (loaded via `@StyleSheet("styles.css")` in `Application.java`). It defines `--dialect-*` design tokens (Dialect design system: primary orange `#E86C00`, cool-gray background, card radius/shadow) and aliases them onto Aura's own CSS custom properties (`--aura-accent-color-*`, `--aura-background-color-*`, `--aura-orange`, `--aura-yellow`) rather than fighting the theme. Aura tokens use OKLCH + relative-color syntax and accept plain hex overrides. When restyling, prefer extending this alias layer over hardcoding new colors in components.
+17
View File
@@ -18,6 +18,7 @@
"@vaadin/vaadin-usage-statistics": "2.1.3", "@vaadin/vaadin-usage-statistics": "2.1.3",
"apexcharts": "5.15.2", "apexcharts": "5.15.2",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"gridstack": "13.1.0",
"lit": "3.3.3", "lit": "3.3.3",
"ol": "10.6.1", "ol": "10.6.1",
"proj4": "2.17.0", "proj4": "2.17.0",
@@ -5304,6 +5305,22 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/gridstack": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/gridstack/-/gridstack-13.1.0.tgz",
"integrity": "sha512-cOGK9Ksy0mzTOTbEI32vpZdCby2WbzbnZcYxgASopkJWg8beBpp7gRhiTNROUtTHbDansXNOxoCfkyRTlx5DNw==",
"funding": [
{
"type": "paypal",
"url": "https://www.paypal.me/alaind831"
},
{
"type": "venmo",
"url": "https://www.venmo.com/adumesny"
}
],
"license": "MIT"
},
"node_modules/has-ansi": { "node_modules/has-ansi": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+6 -2
View File
@@ -14,6 +14,7 @@
"@vaadin/vaadin-usage-statistics": "2.1.3", "@vaadin/vaadin-usage-statistics": "2.1.3",
"apexcharts": "5.15.2", "apexcharts": "5.15.2",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"gridstack": "13.1.0",
"lit": "3.3.3", "lit": "3.3.3",
"ol": "10.6.1", "ol": "10.6.1",
"proj4": "2.17.0", "proj4": "2.17.0",
@@ -61,6 +62,7 @@
"@vaadin/vaadin-usage-statistics": "2.1.3", "@vaadin/vaadin-usage-statistics": "2.1.3",
"apexcharts": "5.15.2", "apexcharts": "5.15.2",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"gridstack": "13.1.0",
"lit": "3.3.3", "lit": "3.3.3",
"ol": "10.6.1", "ol": "10.6.1",
"proj4": "2.17.0", "proj4": "2.17.0",
@@ -95,7 +97,7 @@
"workbox-routing": "7.4.1", "workbox-routing": "7.4.1",
"workbox-strategies": "7.4.1" "workbox-strategies": "7.4.1"
}, },
"hash": "54350847a380a8c1b6cc526f6f7eff8dd9253d848e38919e7e1b633cec9f3aca", "hash": "a8466446482b2201dfcaacaf427dabc79f870e7716e2d8f65cf31eca3870ef6a",
"overrides": { "overrides": {
"@fortawesome/fontawesome-free": "$@fortawesome/fontawesome-free", "@fortawesome/fontawesome-free": "$@fortawesome/fontawesome-free",
"@vaadin/a11y-base": "25.2.0", "@vaadin/a11y-base": "25.2.0",
@@ -177,6 +179,7 @@
"@vaadin/virtual-list": "25.2.0", "@vaadin/virtual-list": "25.2.0",
"apexcharts": "$apexcharts", "apexcharts": "$apexcharts",
"date-fns": "$date-fns", "date-fns": "$date-fns",
"gridstack": "$gridstack",
"lit": "$lit", "lit": "$lit",
"ol": "$ol", "ol": "$ol",
"proj4": "$proj4", "proj4": "$proj4",
@@ -277,6 +280,7 @@
"glob": "13.0.6" "glob": "13.0.6"
}, },
"apexcharts": "$apexcharts", "apexcharts": "$apexcharts",
"@fortawesome/fontawesome-free": "$@fortawesome/fontawesome-free" "@fortawesome/fontawesome-free": "$@fortawesome/fontawesome-free",
"gridstack": "$gridstack"
} }
} }
Binary file not shown.
+14 -3
View File
@@ -34,13 +34,15 @@ class GridStackLayout extends HTMLElement {
this.classList.add('grid-stack'); this.classList.add('grid-stack');
const options = JSON.parse(optionsJson); const options = JSON.parse(optionsJson);
this.grid = GridStack.init(options, this); const grid = GridStack.init(options, this);
if (!grid) return;
this.grid = grid;
this.registerChildren(); this.registerChildren();
this.restore(); this.restore();
this.grid.on('change added removed', () => this.schedulePersist()); grid.on('change added removed', () => this.schedulePersist());
this.grid.on('resizestop', () => window.dispatchEvent(new Event('resize'))); grid.on('resizestop', () => window.dispatchEvent(new Event('resize')));
} }
/** Drops any saved layout for this grid and re-applies the positions /** Drops any saved layout for this grid and re-applies the positions
@@ -73,6 +75,15 @@ class GridStackLayout extends HTMLElement {
mutation.removedNodes.forEach((node) => { mutation.removedNodes.forEach((node) => {
if (!(node instanceof HTMLElement)) return; if (!(node instanceof HTMLElement)) return;
if (!(node as any).gridstackNode) return; if (!(node as any).gridstackNode) return;
// gridstack's own _sortDom() re-appends children (within this
// same element) to match layout order after every
// move/resize, which fires childList mutations too. A
// reordered node is back in the (still connected) DOM by the
// time this callback runs; only a genuinely removed node is
// disconnected. Without this check, removeWidget() here
// would fire on gridstack's own reorder, re-triggering
// _sortDom() and looping forever.
if (node.isConnected) return;
this.grid!.removeWidget(node, false); this.grid!.removeWidget(node, false);
}); });
} }
@@ -27,7 +27,11 @@ public class GridStackItem extends Div {
getElement().setAttribute("gs-h", String.valueOf(h)); getElement().setAttribute("gs-h", String.valueOf(h));
this.content.addClassName("grid-stack-item-content"); this.content.addClassName("grid-stack-item-content");
add(this.content); // Attach the wrapper div directly through the element API — calling
// the overridden add(Component...) below would route it through
// `content.add(...)`, making the wrapper try to add itself as its
// own child.
getElement().appendChild(this.content.getElement());
this.content.add(content); this.content.add(content);
} }
@@ -80,7 +84,7 @@ public class GridStackItem extends Div {
} }
/** Snapshot of a widget's grid placement, as reported by the client after a /** Snapshot of a widget's grid placement, as reported by the client after a
* drag/resize (see {@link GridStackLayout#onLayoutChange(String)}). */ * drag/resize. */
public record Position(String id, int x, int y, int w, int h) { public record Position(String id, int x, int y, int w, int h) {
} }
} }
@@ -122,3 +122,31 @@ vaadin-app-layout::part(content) {
color: var(--dialect-ink); color: var(--dialect-ink);
margin: 0 0 12px 0; margin: 0 0 12px 0;
} }
/* GridStackLayout (components/GridStackLayout.java): extend the same alias
layer used by .dialect-card rather than hardcoding new colors. */
.grid-stack-item-content {
background: var(--dialect-surface);
border-radius: var(--dialect-radius);
box-shadow: var(--dialect-shadow);
overflow: auto;
}
/* A Card placed inside a grid item should fill the item instead of
stacking its own surface/shadow on top of grid-stack-item-content's. */
.grid-stack-item-content > .dialect-card {
height: 100%;
box-shadow: none;
}
.grid-stack-item-content .ui-resizable-handle {
background: var(--dialect-primary);
opacity: 0.6;
border-radius: 2px;
}
.grid-stack-placeholder > .placeholder-content {
background: var(--dialect-bg);
border: 2px dashed var(--dialect-border);
border-radius: var(--dialect-radius);
}