added gridstack
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import { GridStack, type GridStackNode } from 'gridstack';
|
||||
|
||||
const STORAGE_PREFIX = 'gridstack:';
|
||||
|
||||
/** Vanilla custom element (not Lit) — its children are Flow-rendered Vaadin
|
||||
* components, not something this element renders itself. A Lit render root
|
||||
* would fight gridstack for ownership of the light DOM, so this class never
|
||||
* touches its own childList except by reading/reacting to it. */
|
||||
class GridStackLayout extends HTMLElement {
|
||||
private grid?: GridStack;
|
||||
private observer?: MutationObserver;
|
||||
private storageKey: string | null = null;
|
||||
private persistTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
connectedCallback() {
|
||||
this.observer = new MutationObserver((mutations) => this.onMutation(mutations));
|
||||
this.observer.observe(this, { childList: true });
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.observer?.disconnect();
|
||||
this.observer = undefined;
|
||||
clearTimeout(this.persistTimer);
|
||||
this.grid?.destroy(false); // keep DOM — Flow owns the children
|
||||
this.grid = undefined;
|
||||
}
|
||||
|
||||
/** Called from the server once on attach (and again if the element is
|
||||
* re-attached). Idempotent: tears down any previous grid instance
|
||||
* first so it's safe to call more than once. */
|
||||
async initGrid(optionsJson: string, storageKey: string | null) {
|
||||
this.grid?.destroy(false);
|
||||
this.storageKey = storageKey;
|
||||
|
||||
this.classList.add('grid-stack');
|
||||
const options = JSON.parse(optionsJson);
|
||||
this.grid = GridStack.init(options, this);
|
||||
|
||||
this.registerChildren();
|
||||
this.restore();
|
||||
|
||||
this.grid.on('change added removed', () => this.schedulePersist());
|
||||
this.grid.on('resizestop', () => window.dispatchEvent(new Event('resize')));
|
||||
}
|
||||
|
||||
/** Drops any saved layout for this grid and re-applies the positions
|
||||
* currently declared server-side (the gs-* attributes on each child). */
|
||||
clearStorage() {
|
||||
if (this.storageKey) {
|
||||
localStorage.removeItem(STORAGE_PREFIX + this.storageKey);
|
||||
}
|
||||
if (!this.grid) return;
|
||||
for (const el of Array.from(this.children) as HTMLElement[]) {
|
||||
if (!el.classList.contains('grid-stack-item')) continue;
|
||||
this.grid.update(el, {
|
||||
x: this.intAttr(el, 'gs-x'),
|
||||
y: this.intAttr(el, 'gs-y'),
|
||||
w: this.intAttr(el, 'gs-w'),
|
||||
h: this.intAttr(el, 'gs-h'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onMutation(mutations: MutationRecord[]) {
|
||||
if (!this.grid) return;
|
||||
for (const mutation of mutations) {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (!(node instanceof HTMLElement)) return;
|
||||
if (!node.classList.contains('grid-stack-item')) return;
|
||||
if ((node as any).gridstackNode) return;
|
||||
this.grid!.makeWidget(node);
|
||||
});
|
||||
mutation.removedNodes.forEach((node) => {
|
||||
if (!(node instanceof HTMLElement)) return;
|
||||
if (!(node as any).gridstackNode) return;
|
||||
this.grid!.removeWidget(node, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private registerChildren() {
|
||||
for (const el of Array.from(this.children) as HTMLElement[]) {
|
||||
if (!el.classList.contains('grid-stack-item')) continue;
|
||||
if ((el as any).gridstackNode) continue;
|
||||
this.grid!.makeWidget(el);
|
||||
}
|
||||
}
|
||||
|
||||
private schedulePersist() {
|
||||
clearTimeout(this.persistTimer);
|
||||
this.persistTimer = setTimeout(() => this.persist(), 150);
|
||||
}
|
||||
|
||||
private persist() {
|
||||
if (!this.grid) return;
|
||||
const nodes = this.grid.save(false) as GridStackNode[];
|
||||
if (this.storageKey) {
|
||||
localStorage.setItem(STORAGE_PREFIX + this.storageKey, JSON.stringify(nodes));
|
||||
}
|
||||
(this as any).$server?.onLayoutChange(JSON.stringify(nodes));
|
||||
}
|
||||
|
||||
private restore() {
|
||||
if (!this.storageKey || !this.grid) return;
|
||||
const raw = localStorage.getItem(STORAGE_PREFIX + this.storageKey);
|
||||
if (!raw) return;
|
||||
|
||||
let nodes: GridStackNode[];
|
||||
try {
|
||||
nodes = JSON.parse(raw);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const node of nodes) {
|
||||
if (!node.id) continue;
|
||||
const el = this.querySelector(`:scope > [gs-id="${node.id}"]`) as HTMLElement | null;
|
||||
if (!el) continue; // item no longer present server-side — skip, degrade gracefully
|
||||
this.grid.update(el, { x: node.x, y: node.y, w: node.w, h: node.h });
|
||||
}
|
||||
}
|
||||
|
||||
private intAttr(el: HTMLElement, name: string): number | undefined {
|
||||
const value = el.getAttribute(name);
|
||||
return value === null ? undefined : parseInt(value, 10);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('grid-stack-layout', GridStackLayout);
|
||||
Reference in New Issue
Block a user