17396466a9
CI / build-and-test (pull_request) Successful in 1m13s
Adds a /gridstack route demonstrating GridStackLayout: four default widgets (line/bar/pie chart cards plus a usage hint), buttons to add and remove widgets at runtime, a reset-layout button and a layout-change status line. Layout is persisted per browser via the existing localStorage support. The view gets its own SideNavItem in MainLayout and full de/en/es translations. Fixes an infinite recursion in GridStackLayout.add(Component): a single-argument add(item) resolves to that overload rather than the varargs one, so its delegation to add(item) called itself. It now appends through the element API and passes a GridStackItem through unwrapped. Adds the first test sources (browserless UI tests) covering the view's widget count and the add/remove buttons. Closes #7 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PYAcKoXKPN3rJ7nBroKZkc
218 lines
7.7 KiB
Java
218 lines
7.7 KiB
Java
package com.example.components;
|
|
|
|
import com.vaadin.flow.component.AttachEvent;
|
|
import com.vaadin.flow.component.ClientCallable;
|
|
import com.vaadin.flow.component.Component;
|
|
import com.vaadin.flow.component.ComponentEvent;
|
|
import com.vaadin.flow.component.ComponentEventListener;
|
|
import com.vaadin.flow.component.HasSize;
|
|
import com.vaadin.flow.component.HasStyle;
|
|
import com.vaadin.flow.component.dependency.CssImport;
|
|
import com.vaadin.flow.component.dependency.JsModule;
|
|
import com.vaadin.flow.component.dependency.NpmPackage;
|
|
import com.vaadin.flow.component.Tag;
|
|
import com.vaadin.flow.shared.Registration;
|
|
import tools.jackson.databind.JsonNode;
|
|
import tools.jackson.databind.json.JsonMapper;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* Vaadin wrapper around <a href="https://gridstack.js.org">gridstack.js</a> —
|
|
* a draggable, resizable grid container. Children must be {@link GridStackItem}
|
|
* instances (or a plain {@link Component}, which gets auto-wrapped); gridstack
|
|
* manipulates the real DOM nodes directly, so this component keeps its children
|
|
* in light DOM (see {@code grid-stack.ts}) rather than routing them through a
|
|
* Lit render root the way {@link ApexChart} does.
|
|
* <p>
|
|
* Layout changes made by drag/resize are, by default, only persisted in the
|
|
* browser's {@code localStorage} (see {@link #setStorageKey(String)}); the
|
|
* server is notified via {@link #addLayoutChangeListener}, and item positions
|
|
* are kept in sync in {@link #onLayoutChange(String)} so {@link #getLayout()}
|
|
* stays truthful across a reload of this component (though not across a page
|
|
* reload, since server-side item state is not itself persisted).
|
|
*/
|
|
@Tag("grid-stack-layout")
|
|
@NpmPackage(value = "gridstack", version = "13.1.0")
|
|
@JsModule("./components/grid-stack.ts")
|
|
@CssImport("gridstack/dist/gridstack.min.css")
|
|
public class GridStackLayout extends Component implements HasSize, HasStyle {
|
|
|
|
private static final JsonMapper MAPPER = JsonMapper.builder().build();
|
|
|
|
private final Map<String, Object> options = new LinkedHashMap<>();
|
|
private String storageKey;
|
|
|
|
public GridStackLayout() {
|
|
options.put("column", 12);
|
|
options.put("cellHeight", "120px");
|
|
options.put("margin", "8px");
|
|
options.put("minRow", 1);
|
|
options.put("float", true);
|
|
options.put("animate", true);
|
|
}
|
|
|
|
public GridStackLayout setColumn(int column) {
|
|
options.put("column", column);
|
|
return this;
|
|
}
|
|
|
|
public GridStackLayout setCellHeight(String cellHeight) {
|
|
options.put("cellHeight", cellHeight);
|
|
return this;
|
|
}
|
|
|
|
public GridStackLayout setMargin(String margin) {
|
|
options.put("margin", margin);
|
|
return this;
|
|
}
|
|
|
|
public GridStackLayout setMinRow(int minRow) {
|
|
options.put("minRow", minRow);
|
|
return this;
|
|
}
|
|
|
|
public GridStackLayout setFloat(boolean floatItems) {
|
|
options.put("float", floatItems);
|
|
return this;
|
|
}
|
|
|
|
public GridStackLayout setAnimate(boolean animate) {
|
|
options.put("animate", animate);
|
|
return this;
|
|
}
|
|
|
|
public GridStackLayout setStaticGrid(boolean staticGrid) {
|
|
options.put("staticGrid", staticGrid);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Enables browser-localStorage persistence of the layout under the given
|
|
* key (shared across sessions/tabs on the same origin — pick something
|
|
* unique per grid instance/view). Pass {@code null} to disable.
|
|
*/
|
|
public GridStackLayout setStorageKey(String storageKey) {
|
|
this.storageKey = storageKey;
|
|
return this;
|
|
}
|
|
|
|
public void add(GridStackItem... items) {
|
|
for (GridStackItem item : items) {
|
|
getElement().appendChild(item.getElement());
|
|
}
|
|
}
|
|
|
|
/** Convenience overload: wraps a plain component in a default-sized
|
|
* {@link GridStackItem} (an item passed in directly is added as is).
|
|
* <p>
|
|
* Appends through the element API rather than calling {@code add(item)}:
|
|
* a single-argument call resolves to this overload, not the varargs one,
|
|
* so delegating would recurse into itself. */
|
|
public GridStackItem add(Component content) {
|
|
GridStackItem item = content instanceof GridStackItem existing
|
|
? existing
|
|
: new GridStackItem(content);
|
|
getElement().appendChild(item.getElement());
|
|
return item;
|
|
}
|
|
|
|
public void remove(GridStackItem... items) {
|
|
for (GridStackItem item : items) {
|
|
getElement().removeChild(item.getElement());
|
|
}
|
|
}
|
|
|
|
public void removeAll() {
|
|
getElement().removeAllChildren();
|
|
}
|
|
|
|
/** Restores every item to the position/size currently declared server-side
|
|
* (its {@code gs-*} attributes) and clears any saved localStorage layout. */
|
|
public void resetLayout() {
|
|
getElement().callJsFunction("clearStorage");
|
|
}
|
|
|
|
public List<GridStackItem.Position> getLayout() {
|
|
List<GridStackItem.Position> positions = new ArrayList<>();
|
|
getElement().getChildren().forEach(child -> {
|
|
if (!(child.getComponent().orElse(null) instanceof GridStackItem item)) {
|
|
return;
|
|
}
|
|
positions.add(new GridStackItem.Position(
|
|
item.getItemId(),
|
|
intAttr(item, "gs-x"),
|
|
intAttr(item, "gs-y"),
|
|
intAttr(item, "gs-w"),
|
|
intAttr(item, "gs-h")));
|
|
});
|
|
return positions;
|
|
}
|
|
|
|
@Override
|
|
protected void onAttach(AttachEvent attachEvent) {
|
|
super.onAttach(attachEvent);
|
|
getElement().callJsFunction("initGrid", MAPPER.writeValueAsString(options), storageKey);
|
|
}
|
|
|
|
@ClientCallable
|
|
private void onLayoutChange(String nodesJson) {
|
|
JsonNode nodes = MAPPER.readTree(nodesJson);
|
|
List<GridStackItem.Position> positions = new ArrayList<>();
|
|
|
|
for (JsonNode node : nodes) {
|
|
String id = node.path("id").asString(null);
|
|
if (id == null) continue;
|
|
|
|
findItem(id).ifPresent(item -> item.setPosition(
|
|
node.path("x").asInt(),
|
|
node.path("y").asInt(),
|
|
node.path("w").asInt(),
|
|
node.path("h").asInt()));
|
|
|
|
positions.add(new GridStackItem.Position(
|
|
id,
|
|
node.path("x").asInt(),
|
|
node.path("y").asInt(),
|
|
node.path("w").asInt(),
|
|
node.path("h").asInt()));
|
|
}
|
|
|
|
fireEvent(new LayoutChangeEvent(this, true, positions));
|
|
}
|
|
|
|
private java.util.Optional<GridStackItem> findItem(String id) {
|
|
return getElement().getChildren()
|
|
.map(child -> child.getComponent().orElse(null))
|
|
.filter(GridStackItem.class::isInstance)
|
|
.map(GridStackItem.class::cast)
|
|
.filter(item -> id.equals(item.getItemId()))
|
|
.findFirst();
|
|
}
|
|
|
|
private int intAttr(Component component, String name) {
|
|
String value = component.getElement().getAttribute(name);
|
|
return value == null ? 0 : Integer.parseInt(value);
|
|
}
|
|
|
|
public Registration addLayoutChangeListener(ComponentEventListener<LayoutChangeEvent> listener) {
|
|
return addListener(LayoutChangeEvent.class, listener);
|
|
}
|
|
|
|
public static class LayoutChangeEvent extends ComponentEvent<GridStackLayout> {
|
|
private final List<GridStackItem.Position> positions;
|
|
|
|
LayoutChangeEvent(GridStackLayout source, boolean fromClient, List<GridStackItem.Position> positions) {
|
|
super(source, fromClient);
|
|
this.positions = positions;
|
|
}
|
|
|
|
public List<GridStackItem.Position> getPositions() {
|
|
return positions;
|
|
}
|
|
}
|
|
}
|