Initial commit: AegisSight-Monitor (OSINT-Monitoringsystem)
Dieser Commit ist enthalten in:
282
src/static/js/layout.js
Normale Datei
282
src/static/js/layout.js
Normale Datei
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* LayoutManager: Drag & Resize Dashboard-Layout mit gridstack.js
|
||||
* Persistenz über localStorage, Reset auf Standard-Layout möglich.
|
||||
*/
|
||||
const LayoutManager = {
|
||||
_grid: null,
|
||||
_storageKey: 'osint_layout',
|
||||
_initialized: false,
|
||||
_saveTimeout: null,
|
||||
_hiddenTiles: {},
|
||||
|
||||
DEFAULT_LAYOUT: [
|
||||
{ id: 'lagebild', x: 0, y: 0, w: 6, h: 4, minW: 4, minH: 4 },
|
||||
{ id: 'faktencheck', x: 6, y: 0, w: 6, h: 4, minW: 4, minH: 4 },
|
||||
{ id: 'quellen', x: 0, y: 4, w: 12, h: 2, minW: 6, minH: 2 },
|
||||
{ id: 'timeline', x: 0, y: 5, w: 12, h: 4, minW: 6, minH: 4 },
|
||||
],
|
||||
|
||||
TILE_MAP: {
|
||||
lagebild: '.incident-analysis-summary',
|
||||
faktencheck: '.incident-analysis-factcheck',
|
||||
quellen: '.source-overview-card',
|
||||
timeline: '.timeline-card',
|
||||
},
|
||||
|
||||
init() {
|
||||
if (this._initialized) return;
|
||||
|
||||
const container = document.querySelector('.grid-stack');
|
||||
if (!container) return;
|
||||
|
||||
this._grid = GridStack.init({
|
||||
column: 12,
|
||||
cellHeight: 80,
|
||||
margin: 12,
|
||||
animate: true,
|
||||
handle: '.card-header',
|
||||
float: false,
|
||||
disableOneColumnMode: true,
|
||||
}, container);
|
||||
|
||||
const saved = this._load();
|
||||
if (saved) {
|
||||
this._applyLayout(saved);
|
||||
}
|
||||
|
||||
this._grid.on('change', () => this._debouncedSave());
|
||||
|
||||
const toolbar = document.getElementById('layout-toolbar');
|
||||
if (toolbar) toolbar.style.display = 'flex';
|
||||
|
||||
this._syncToggles();
|
||||
this._initialized = true;
|
||||
},
|
||||
|
||||
_applyLayout(layout) {
|
||||
if (!this._grid) return;
|
||||
|
||||
this._hiddenTiles = {};
|
||||
|
||||
layout.forEach(item => {
|
||||
const el = this._grid.engine.nodes.find(n => n.el && n.el.getAttribute('gs-id') === item.id);
|
||||
if (!el) return;
|
||||
|
||||
if (item.visible === false) {
|
||||
this._hiddenTiles[item.id] = item;
|
||||
this._grid.removeWidget(el.el, true, false);
|
||||
} else {
|
||||
this._grid.update(el.el, { x: item.x, y: item.y, w: item.w, h: item.h });
|
||||
}
|
||||
});
|
||||
|
||||
this._syncToggles();
|
||||
},
|
||||
|
||||
save() {
|
||||
if (!this._grid) return;
|
||||
|
||||
const items = [];
|
||||
this._grid.engine.nodes.forEach(node => {
|
||||
const id = node.el ? node.el.getAttribute('gs-id') : null;
|
||||
if (!id) return;
|
||||
items.push({
|
||||
id, x: node.x, y: node.y, w: node.w, h: node.h, visible: true,
|
||||
});
|
||||
});
|
||||
|
||||
Object.keys(this._hiddenTiles).forEach(id => {
|
||||
items.push({ ...this._hiddenTiles[id], visible: false });
|
||||
});
|
||||
|
||||
try {
|
||||
localStorage.setItem(this._storageKey, JSON.stringify(items));
|
||||
} catch (e) { /* quota */ }
|
||||
},
|
||||
|
||||
_debouncedSave() {
|
||||
clearTimeout(this._saveTimeout);
|
||||
this._saveTimeout = setTimeout(() => this.save(), 300);
|
||||
},
|
||||
|
||||
_load() {
|
||||
try {
|
||||
const raw = localStorage.getItem(this._storageKey);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed) || parsed.length === 0) return null;
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
toggleTile(tileId) {
|
||||
if (!this._grid) return;
|
||||
|
||||
const selector = this.TILE_MAP[tileId];
|
||||
if (!selector) return;
|
||||
|
||||
if (this._hiddenTiles[tileId]) {
|
||||
// Kachel einblenden
|
||||
const cfg = this._hiddenTiles[tileId];
|
||||
delete this._hiddenTiles[tileId];
|
||||
|
||||
const cardEl = document.querySelector(selector);
|
||||
if (!cardEl) return;
|
||||
|
||||
// Wrapper erstellen
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'grid-stack-item';
|
||||
wrapper.setAttribute('gs-id', tileId);
|
||||
wrapper.setAttribute('gs-x', cfg.x);
|
||||
wrapper.setAttribute('gs-y', cfg.y);
|
||||
wrapper.setAttribute('gs-w', cfg.w);
|
||||
wrapper.setAttribute('gs-h', cfg.h);
|
||||
wrapper.setAttribute('gs-min-w', cfg.minW || '');
|
||||
wrapper.setAttribute('gs-min-h', cfg.minH || '');
|
||||
const content = document.createElement('div');
|
||||
content.className = 'grid-stack-item-content';
|
||||
content.appendChild(cardEl);
|
||||
wrapper.appendChild(content);
|
||||
|
||||
this._grid.addWidget(wrapper);
|
||||
} else {
|
||||
// Kachel ausblenden
|
||||
const node = this._grid.engine.nodes.find(
|
||||
n => n.el && n.el.getAttribute('gs-id') === tileId
|
||||
);
|
||||
if (!node) return;
|
||||
|
||||
const defaults = this.DEFAULT_LAYOUT.find(d => d.id === tileId);
|
||||
this._hiddenTiles[tileId] = {
|
||||
id: tileId,
|
||||
x: node.x, y: node.y, w: node.w, h: node.h,
|
||||
minW: defaults ? defaults.minW : 4,
|
||||
minH: defaults ? defaults.minH : 2,
|
||||
visible: false,
|
||||
};
|
||||
|
||||
// Card aus dem Widget retten bevor es entfernt wird
|
||||
const cardEl = node.el.querySelector(selector);
|
||||
if (cardEl) {
|
||||
// Temporär im incident-view parken (unsichtbar)
|
||||
const parking = document.getElementById('tile-parking');
|
||||
if (parking) parking.appendChild(cardEl);
|
||||
}
|
||||
|
||||
this._grid.removeWidget(node.el, true, false);
|
||||
}
|
||||
|
||||
this._syncToggles();
|
||||
this.save();
|
||||
},
|
||||
|
||||
_syncToggles() {
|
||||
document.querySelectorAll('.layout-toggle-btn').forEach(btn => {
|
||||
const tileId = btn.getAttribute('data-tile');
|
||||
const isHidden = !!this._hiddenTiles[tileId];
|
||||
btn.classList.toggle('active', !isHidden);
|
||||
btn.setAttribute('aria-pressed', String(!isHidden));
|
||||
});
|
||||
},
|
||||
|
||||
reset() {
|
||||
localStorage.removeItem(this._storageKey);
|
||||
|
||||
// Cards einsammeln BEVOR der Grid zerstört wird (aus Grid + Parking)
|
||||
const cards = {};
|
||||
Object.entries(this.TILE_MAP).forEach(([id, selector]) => {
|
||||
const card = document.querySelector(selector);
|
||||
if (card) cards[id] = card;
|
||||
});
|
||||
|
||||
this._hiddenTiles = {};
|
||||
|
||||
if (this._grid) {
|
||||
this._grid.destroy(false);
|
||||
this._grid = null;
|
||||
}
|
||||
this._initialized = false;
|
||||
|
||||
const gridEl = document.querySelector('.grid-stack');
|
||||
if (!gridEl) return;
|
||||
|
||||
// Grid leeren (Cards sind bereits in cards-Map gesichert)
|
||||
gridEl.innerHTML = '';
|
||||
|
||||
// Cards in Default-Layout neu aufbauen
|
||||
this.DEFAULT_LAYOUT.forEach(cfg => {
|
||||
const cardEl = cards[cfg.id];
|
||||
if (!cardEl) return;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'grid-stack-item';
|
||||
wrapper.setAttribute('gs-id', cfg.id);
|
||||
wrapper.setAttribute('gs-x', cfg.x);
|
||||
wrapper.setAttribute('gs-y', cfg.y);
|
||||
wrapper.setAttribute('gs-w', cfg.w);
|
||||
wrapper.setAttribute('gs-h', cfg.h);
|
||||
wrapper.setAttribute('gs-min-w', cfg.minW);
|
||||
wrapper.setAttribute('gs-min-h', cfg.minH);
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.className = 'grid-stack-item-content';
|
||||
content.appendChild(cardEl);
|
||||
wrapper.appendChild(content);
|
||||
gridEl.appendChild(wrapper);
|
||||
});
|
||||
|
||||
this.init();
|
||||
},
|
||||
|
||||
resizeTileToContent(tileId) {
|
||||
if (!this._grid) return;
|
||||
|
||||
const node = this._grid.engine.nodes.find(
|
||||
n => n.el && n.el.getAttribute('gs-id') === tileId
|
||||
);
|
||||
if (!node || !node.el) return;
|
||||
|
||||
const wrapper = node.el.querySelector('.grid-stack-item-content');
|
||||
if (!wrapper) return;
|
||||
|
||||
const card = wrapper.firstElementChild;
|
||||
if (!card) return;
|
||||
|
||||
const cellH = this._grid.opts.cellHeight || 80;
|
||||
const margin = this._grid.opts.margin || 12;
|
||||
|
||||
// Temporär alle height-Constraints aufheben
|
||||
node.el.classList.add('gs-measuring');
|
||||
const naturalHeight = card.scrollHeight;
|
||||
node.el.classList.remove('gs-measuring');
|
||||
|
||||
// In Grid-Units umrechnen (aufrunden + 1 Puffer)
|
||||
const neededH = Math.ceil(naturalHeight / (cellH + margin)) + 1;
|
||||
const minH = node.minH || 2;
|
||||
const finalH = Math.max(neededH, minH);
|
||||
|
||||
this._grid.update(node.el, { h: finalH });
|
||||
this._debouncedSave();
|
||||
},
|
||||
|
||||
resetAllTilesToDefault() {
|
||||
if (!this._grid) return;
|
||||
this.DEFAULT_LAYOUT.forEach(cfg => {
|
||||
const node = this._grid.engine.nodes.find(
|
||||
n => n.el && n.el.getAttribute('gs-id') === cfg.id
|
||||
);
|
||||
if (node) this._grid.update(node.el, { h: cfg.h });
|
||||
});
|
||||
},
|
||||
|
||||
destroy() {
|
||||
if (this._grid) {
|
||||
this._grid.destroy(false);
|
||||
this._grid = null;
|
||||
}
|
||||
this._initialized = false;
|
||||
this._hiddenTiles = {};
|
||||
},
|
||||
};
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren