Merge pull request 'Fixes #11' (#12) from issue/11 into main

Reviewed-on: #12
This commit was merged in pull request #12.
This commit is contained in:
2026-07-26 12:07:17 +00:00
9 changed files with 108 additions and 5 deletions
Binary file not shown.
+20 -1
View File
@@ -59,6 +59,8 @@ function applyThemeOverlay(options: any): any {
export class ApexChart extends LitElement {
private chart?: ApexCharts;
private lastOptions?: any;
private lastOptionsJson?: string;
private destroyTimer?: ReturnType<typeof setTimeout>;
private readonly onThemeChange = () => {
if (!this.chart || !this.lastOptions) return;
@@ -71,7 +73,14 @@ export class ApexChart extends LitElement {
connectedCallback() {
super.connectedCallback();
clearTimeout(this.destroyTimer);
this.destroyTimer = undefined;
window.addEventListener('dialect-theme-change', this.onThemeChange);
// Back after a real teardown (Flow detach, cached view): rebuild, since
// the server only pushes options on setData(), not on re-attach.
if (!this.chart && this.lastOptionsJson) {
void this.renderChart(this.lastOptionsJson);
}
}
render() {
@@ -80,6 +89,7 @@ export class ApexChart extends LitElement {
async renderChart(optionsJson: string) {
await this.updateComplete;
this.lastOptionsJson = optionsJson;
const options = JSON.parse(optionsJson);
options.chart = {
@@ -106,6 +116,15 @@ export class ApexChart extends LitElement {
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener('dialect-theme-change', this.onThemeChange);
this.chart?.destroy();
// gridstack's _sortDom() re-appends item elements after every
// move/resize, which disconnects and immediately reconnects this
// element within the same task — destroying the chart synchronously
// would blank it with nothing to trigger a re-render. Defer past the
// current task and bail out if we came back.
this.destroyTimer = setTimeout(() => {
if (this.isConnected) return;
this.chart?.destroy();
this.chart = undefined;
}, 0);
}
}
+2 -1
View File
@@ -24,7 +24,8 @@ public enum Fa {
GRID("fa-solid", "fa-table-cells-large"),
ADD("fa-solid", "fa-plus"),
REMOVE("fa-solid", "fa-trash"),
RESET("fa-solid", "fa-arrow-rotate-left");
RESET("fa-solid", "fa-arrow-rotate-left"),
DRAG("fa-solid", "fa-grip-vertical");
private final String[] classes;
@@ -9,10 +9,19 @@ import java.util.UUID;
* A single cell inside a {@link GridStackLayout}. Position/size are written as
* {@code gs-*} attributes, which is how gridstack.js reads a widget's initial
* placement when {@code makeWidget} converts it client-side (see grid-stack.ts).
* <p>
* Dragging starts from the grip handle only, never from the item body, so the
* components inside an item stay interactive (see
* {@link GridStackLayout#setDragHandle(String)}).
*/
public class GridStackItem extends Div {
/** Selector gridstack is told to treat as the drag handle — see
* {@link GridStackLayout}'s {@code handle} option. */
public static final String DRAG_HANDLE_CLASS = "dialect-drag-handle";
private final Div content = new Div();
private final Div dragHandle = new Div();
public GridStackItem(Component... content) {
this(UUID.randomUUID().toString(), 0, 0, 4, 3, content);
@@ -33,6 +42,15 @@ public class GridStackItem extends Div {
// own child.
getElement().appendChild(this.content.getElement());
this.content.add(content);
// Sibling of the content wrapper, not a child of it: the wrapper is
// `overflow: auto`, so a handle inside it would scroll out of view.
dragHandle.addClassName(DRAG_HANDLE_CLASS);
dragHandle.add(Fa.DRAG.create());
String label = getTranslation("gridstack.dragHandle");
dragHandle.getElement().setAttribute("title", label);
dragHandle.getElement().setAttribute("aria-label", label);
getElement().appendChild(dragHandle.getElement());
}
public void add(Component... components) {
@@ -52,11 +70,20 @@ public class GridStackItem extends Div {
return this;
}
/** Also hides the grip handle when {@code false} — the {@code gs-no-move}
* attribute written here is what {@code styles.css} keys the handle's
* visibility off. */
public GridStackItem setMovable(boolean movable) {
setBooleanAttribute("gs-no-move", !movable);
return this;
}
/** The element gridstack drags the item by. Exposed so callers can restyle
* or reposition it; removing it from the DOM makes the item immovable. */
public Div getDragHandle() {
return dragHandle;
}
/** Writes an explicit "true" (gridstack reads this via its own
* {@code Utils.toBool}) or removes the attribute — avoids relying on
* Vaadin's HTML5 empty-string boolean-attribute convention, which
@@ -53,6 +53,10 @@ public class GridStackLayout extends Component implements HasSize, HasStyle {
options.put("minRow", 1);
options.put("float", true);
options.put("animate", true);
// gridstack's default handle is `.grid-stack-item-content`, i.e. the
// whole widget, which swallows clicks meant for the components inside
// it. Restrict dragging to the grip each GridStackItem renders.
options.put("handle", "." + GridStackItem.DRAG_HANDLE_CLASS);
}
public GridStackLayout setColumn(int column) {
@@ -85,6 +89,14 @@ public class GridStackLayout extends Component implements HasSize, HasStyle {
return this;
}
/** Overrides the selector items are dragged by (default: the grip rendered
* by {@link GridStackItem}). The selector must match an element inside the
* item. */
public GridStackLayout setDragHandle(String selector) {
options.put("handle", selector);
return this;
}
public GridStackLayout setStaticGrid(boolean staticGrid) {
options.put("staticGrid", staticGrid);
return this;
@@ -146,6 +146,47 @@ vaadin-app-layout::part(content) {
box-shadow: none;
}
/* Drag grip: gridstack drags items by this element only (`handle` option in
GridStackLayout), so the item body keeps its own interactivity. The item
itself is absolutely positioned by gridstack, so it is the offset parent. */
.dialect-drag-handle {
position: absolute;
top: 14px;
right: 16px;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 6px;
color: var(--dialect-ink);
opacity: 0;
cursor: grab;
transition: opacity 120ms ease;
}
.grid-stack-item:hover > .dialect-drag-handle,
.dialect-drag-handle:focus-visible {
opacity: 0.65;
}
.dialect-drag-handle:active {
cursor: grabbing;
opacity: 1;
}
/* Touch devices never hover — keep the grip permanently visible there. */
@media (pointer: coarse) {
.dialect-drag-handle {
opacity: 0.65;
}
}
.grid-stack-item[gs-no-move] > .dialect-drag-handle {
display: none;
}
.grid-stack-item-content .ui-resizable-handle {
background: var(--dialect-primary);
opacity: 0.6;
@@ -24,7 +24,7 @@ card.registration=Registrierung
card.employees=Mitarbeiter
card.gridstackHint=Bedienung
gridstack.hint=Karten am Titel greifen und verschieben, an der unteren rechten Ecke die Größe ändern. Das Layout wird im Browser gespeichert und beim nächsten Besuch wiederhergestellt.
gridstack.hint=Karten am Griff oben rechts verschieben, an der unteren rechten Ecke die Größe ändern. Das Layout wird im Browser gespeichert und beim nächsten Besuch wiederhergestellt.
gridstack.addWidget=Widget hinzufügen
gridstack.removeWidget=Letztes entfernen
gridstack.reset=Layout zurücksetzen
@@ -32,6 +32,7 @@ gridstack.status=Layout geändert {0} Widgets
gridstack.statusInitial=Layout unverändert
gridstack.widget=Widget {0}
gridstack.widgetText=Frei platzierbare Karte.
gridstack.dragHandle=Verschieben
chart.revenueSeries=Umsatz 2026
chart.pointClick=Serie {0}, Punkt {1}
@@ -24,7 +24,7 @@ card.registration=Registration
card.employees=Employees
card.gridstackHint=How it works
gridstack.hint=Drag cards by their header to move them, resize them from the bottom right corner. The layout is stored in your browser and restored on your next visit.
gridstack.hint=Move cards with the grip in their top right corner, resize them from the bottom right corner. The layout is stored in your browser and restored on your next visit.
gridstack.addWidget=Add widget
gridstack.removeWidget=Remove last
gridstack.reset=Reset layout
@@ -32,6 +32,7 @@ gridstack.status=Layout changed {0} widgets
gridstack.statusInitial=Layout unchanged
gridstack.widget=Widget {0}
gridstack.widgetText=Freely placeable card.
gridstack.dragHandle=Move
chart.revenueSeries=Revenue 2026
chart.pointClick=Series {0}, point {1}
@@ -24,7 +24,7 @@ card.registration=Registro
card.employees=Empleados
card.gridstackHint=Cómo funciona
gridstack.hint=Arrastra las tarjetas por su título para moverlas y cambia su tamaño desde la esquina inferior derecha. El diseño se guarda en el navegador y se restaura en la próxima visita.
gridstack.hint=Mueve las tarjetas con el asa de la esquina superior derecha y cambia su tamaño desde la esquina inferior derecha. El diseño se guarda en el navegador y se restaura en la próxima visita.
gridstack.addWidget=Añadir widget
gridstack.removeWidget=Quitar el último
gridstack.reset=Restablecer diseño
@@ -32,6 +32,7 @@ gridstack.status=Diseño modificado {0} widgets
gridstack.statusInitial=Diseño sin cambios
gridstack.widget=Widget {0}
gridstack.widgetText=Tarjeta de colocación libre.
gridstack.dragHandle=Mover
chart.revenueSeries=Ingresos 2026
chart.pointClick=Serie {0}, punto {1}