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.
@@ -59,6 +59,8 @@ function applyThemeOverlay(options: any): any {
export class ApexChart extends LitElement { export class ApexChart extends LitElement {
private chart?: ApexCharts; private chart?: ApexCharts;
private lastOptions?: any; private lastOptions?: any;
private lastOptionsJson?: string;
private destroyTimer?: ReturnType<typeof setTimeout>;
private readonly onThemeChange = () => { private readonly onThemeChange = () => {
if (!this.chart || !this.lastOptions) return; if (!this.chart || !this.lastOptions) return;
@@ -71,7 +73,14 @@ export class ApexChart extends LitElement {
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
clearTimeout(this.destroyTimer);
this.destroyTimer = undefined;
window.addEventListener('dialect-theme-change', this.onThemeChange); 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() { render() {
@@ -80,6 +89,7 @@ export class ApexChart extends LitElement {
async renderChart(optionsJson: string) { async renderChart(optionsJson: string) {
await this.updateComplete; await this.updateComplete;
this.lastOptionsJson = optionsJson;
const options = JSON.parse(optionsJson); const options = JSON.parse(optionsJson);
options.chart = { options.chart = {
@@ -106,6 +116,15 @@ export class ApexChart extends LitElement {
disconnectedCallback() { disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
window.removeEventListener('dialect-theme-change', this.onThemeChange); window.removeEventListener('dialect-theme-change', this.onThemeChange);
// 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?.destroy();
this.chart = undefined;
}, 0);
} }
} }
+2 -1
View File
@@ -24,7 +24,8 @@ public enum Fa {
GRID("fa-solid", "fa-table-cells-large"), GRID("fa-solid", "fa-table-cells-large"),
ADD("fa-solid", "fa-plus"), ADD("fa-solid", "fa-plus"),
REMOVE("fa-solid", "fa-trash"), 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; private final String[] classes;
@@ -9,10 +9,19 @@ import java.util.UUID;
* A single cell inside a {@link GridStackLayout}. Position/size are written as * 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 * {@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). * 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 { 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 content = new Div();
private final Div dragHandle = new Div();
public GridStackItem(Component... content) { public GridStackItem(Component... content) {
this(UUID.randomUUID().toString(), 0, 0, 4, 3, content); this(UUID.randomUUID().toString(), 0, 0, 4, 3, content);
@@ -33,6 +42,15 @@ public class GridStackItem extends Div {
// own child. // own child.
getElement().appendChild(this.content.getElement()); getElement().appendChild(this.content.getElement());
this.content.add(content); 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) { public void add(Component... components) {
@@ -52,11 +70,20 @@ public class GridStackItem extends Div {
return this; 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) { public GridStackItem setMovable(boolean movable) {
setBooleanAttribute("gs-no-move", !movable); setBooleanAttribute("gs-no-move", !movable);
return this; 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 /** Writes an explicit "true" (gridstack reads this via its own
* {@code Utils.toBool}) or removes the attribute — avoids relying on * {@code Utils.toBool}) or removes the attribute — avoids relying on
* Vaadin's HTML5 empty-string boolean-attribute convention, which * 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("minRow", 1);
options.put("float", true); options.put("float", true);
options.put("animate", 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) { public GridStackLayout setColumn(int column) {
@@ -85,6 +89,14 @@ public class GridStackLayout extends Component implements HasSize, HasStyle {
return this; 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) { public GridStackLayout setStaticGrid(boolean staticGrid) {
options.put("staticGrid", staticGrid); options.put("staticGrid", staticGrid);
return this; return this;
@@ -146,6 +146,47 @@ vaadin-app-layout::part(content) {
box-shadow: none; 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 { .grid-stack-item-content .ui-resizable-handle {
background: var(--dialect-primary); background: var(--dialect-primary);
opacity: 0.6; opacity: 0.6;
@@ -24,7 +24,7 @@ card.registration=Registrierung
card.employees=Mitarbeiter card.employees=Mitarbeiter
card.gridstackHint=Bedienung 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.addWidget=Widget hinzufügen
gridstack.removeWidget=Letztes entfernen gridstack.removeWidget=Letztes entfernen
gridstack.reset=Layout zurücksetzen gridstack.reset=Layout zurücksetzen
@@ -32,6 +32,7 @@ gridstack.status=Layout geändert {0} Widgets
gridstack.statusInitial=Layout unverändert gridstack.statusInitial=Layout unverändert
gridstack.widget=Widget {0} gridstack.widget=Widget {0}
gridstack.widgetText=Frei platzierbare Karte. gridstack.widgetText=Frei platzierbare Karte.
gridstack.dragHandle=Verschieben
chart.revenueSeries=Umsatz 2026 chart.revenueSeries=Umsatz 2026
chart.pointClick=Serie {0}, Punkt {1} chart.pointClick=Serie {0}, Punkt {1}
@@ -24,7 +24,7 @@ card.registration=Registration
card.employees=Employees card.employees=Employees
card.gridstackHint=How it works 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.addWidget=Add widget
gridstack.removeWidget=Remove last gridstack.removeWidget=Remove last
gridstack.reset=Reset layout gridstack.reset=Reset layout
@@ -32,6 +32,7 @@ gridstack.status=Layout changed {0} widgets
gridstack.statusInitial=Layout unchanged gridstack.statusInitial=Layout unchanged
gridstack.widget=Widget {0} gridstack.widget=Widget {0}
gridstack.widgetText=Freely placeable card. gridstack.widgetText=Freely placeable card.
gridstack.dragHandle=Move
chart.revenueSeries=Revenue 2026 chart.revenueSeries=Revenue 2026
chart.pointClick=Series {0}, point {1} chart.pointClick=Series {0}, point {1}
@@ -24,7 +24,7 @@ card.registration=Registro
card.employees=Empleados card.employees=Empleados
card.gridstackHint=Cómo funciona 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.addWidget=Añadir widget
gridstack.removeWidget=Quitar el último gridstack.removeWidget=Quitar el último
gridstack.reset=Restablecer diseño gridstack.reset=Restablecer diseño
@@ -32,6 +32,7 @@ gridstack.status=Diseño modificado {0} widgets
gridstack.statusInitial=Diseño sin cambios gridstack.statusInitial=Diseño sin cambios
gridstack.widget=Widget {0} gridstack.widget=Widget {0}
gridstack.widgetText=Tarjeta de colocación libre. gridstack.widgetText=Tarjeta de colocación libre.
gridstack.dragHandle=Mover
chart.revenueSeries=Ingresos 2026 chart.revenueSeries=Ingresos 2026
chart.pointClick=Serie {0}, punto {1} chart.pointClick=Serie {0}, punto {1}