292 lines
10 KiB
JavaScript
Executable File
292 lines
10 KiB
JavaScript
Executable File
(() => {
|
|
const perf = window.AssetPerf;
|
|
perf?.start('browser-state:gesamtes Modul');
|
|
|
|
const persistent = document.body?.dataset.persistBrowserState === 'true';
|
|
const fallbackStorage = persistent ? window.localStorage : window.sessionStorage;
|
|
const alternateStorage = persistent ? window.sessionStorage : window.localStorage;
|
|
const prefix = 'assetmanager:';
|
|
|
|
let serverBacked = false;
|
|
let serverState = {};
|
|
let pendingUpdates = {};
|
|
let pendingRemovals = new Set();
|
|
let saveTimer = null;
|
|
let saveChain = Promise.resolve();
|
|
|
|
function browserGet(storage, key) {
|
|
try { return storage.getItem(prefix + key); } catch (_) { return null; }
|
|
}
|
|
|
|
function browserSet(storage, key, value) {
|
|
try { storage.setItem(prefix + key, value); } catch (_) {}
|
|
}
|
|
|
|
function browserRemove(storage, key) {
|
|
try { storage.removeItem(prefix + key); } catch (_) {}
|
|
}
|
|
|
|
function collectBrowserState() {
|
|
const result = {};
|
|
const collect = storage => {
|
|
try {
|
|
for (let index = 0; index < storage.length; index += 1) {
|
|
const fullKey = storage.key(index);
|
|
if (!fullKey || !fullKey.startsWith(prefix)) continue;
|
|
const key = fullKey.slice(prefix.length);
|
|
if (!key || Object.prototype.hasOwnProperty.call(result, key)) continue;
|
|
const value = storage.getItem(fullKey);
|
|
if (value !== null) result[key] = value;
|
|
}
|
|
} catch (_) {}
|
|
};
|
|
// Prefer the storage mode that was active for this user before the update.
|
|
collect(fallbackStorage);
|
|
collect(alternateStorage);
|
|
return result;
|
|
}
|
|
|
|
async function postState(updates = {}, removals = []) {
|
|
const response = await fetch('/api/user-ui-state', {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({updates, remove: removals}),
|
|
keepalive: true,
|
|
});
|
|
if (!response.ok) throw new Error(`UI state save failed (${response.status})`);
|
|
}
|
|
|
|
function flushPending() {
|
|
if (!serverBacked) return Promise.resolve();
|
|
if (saveTimer !== null) {
|
|
clearTimeout(saveTimer);
|
|
saveTimer = null;
|
|
}
|
|
const updates = pendingUpdates;
|
|
const removals = [...pendingRemovals];
|
|
pendingUpdates = {};
|
|
pendingRemovals = new Set();
|
|
if (!Object.keys(updates).length && !removals.length) return saveChain;
|
|
|
|
saveChain = saveChain
|
|
.catch(() => {})
|
|
.then(() => postState(updates, removals))
|
|
.catch(error => console.warn('[AssetManager] Benutzerbezogener Tabellenzustand konnte nicht gespeichert werden.', error));
|
|
return saveChain;
|
|
}
|
|
|
|
function scheduleSave() {
|
|
if (!serverBacked) return;
|
|
if (saveTimer !== null) clearTimeout(saveTimer);
|
|
saveTimer = window.setTimeout(flushPending, 350);
|
|
}
|
|
|
|
const api = {
|
|
persistent,
|
|
serverBacked: false,
|
|
ready: Promise.resolve(),
|
|
get(key) {
|
|
const normalized = String(key || '');
|
|
if (!normalized) return null;
|
|
if (serverBacked) {
|
|
const value = serverState[normalized];
|
|
return value === undefined || value === null ? null : String(value);
|
|
}
|
|
return browserGet(fallbackStorage, normalized);
|
|
},
|
|
set(key, value) {
|
|
const normalized = String(key || '');
|
|
if (!normalized) return;
|
|
const serialized = String(value);
|
|
if (serverBacked) {
|
|
serverState[normalized] = serialized;
|
|
pendingUpdates[normalized] = serialized;
|
|
pendingRemovals.delete(normalized);
|
|
scheduleSave();
|
|
return;
|
|
}
|
|
browserSet(fallbackStorage, normalized, serialized);
|
|
},
|
|
remove(key) {
|
|
const normalized = String(key || '');
|
|
if (!normalized) return;
|
|
if (serverBacked) {
|
|
delete serverState[normalized];
|
|
delete pendingUpdates[normalized];
|
|
pendingRemovals.add(normalized);
|
|
scheduleSave();
|
|
return;
|
|
}
|
|
browserRemove(fallbackStorage, normalized);
|
|
},
|
|
flush: flushPending,
|
|
};
|
|
window.AssetBrowserState = api;
|
|
|
|
async function loadUserState() {
|
|
try {
|
|
const response = await fetch('/api/user-ui-state', {credentials: 'same-origin', cache: 'no-store'});
|
|
if (!response.ok) return;
|
|
const payload = await response.json();
|
|
if (!payload?.authenticated) return;
|
|
|
|
serverBacked = true;
|
|
api.serverBacked = true;
|
|
serverState = payload.state && typeof payload.state === 'object' && !Array.isArray(payload.state)
|
|
? {...payload.state}
|
|
: {};
|
|
|
|
// One-time/non-destructive migration: existing values from this browser are
|
|
// copied only when the account does not already have a value for the key.
|
|
const browserState = collectBrowserState();
|
|
const migration = {};
|
|
Object.entries(browserState).forEach(([key, value]) => {
|
|
if (!Object.prototype.hasOwnProperty.call(serverState, key)) {
|
|
serverState[key] = value;
|
|
migration[key] = value;
|
|
}
|
|
});
|
|
if (Object.keys(migration).length) {
|
|
try { await postState(migration, []); }
|
|
catch (error) { console.warn('[AssetManager] Vorhandener Browserzustand konnte nicht in das Benutzerkonto übernommen werden.', error); }
|
|
}
|
|
} catch (error) {
|
|
// Browser storage remains the safe fallback if the account-state endpoint
|
|
// is temporarily unavailable.
|
|
console.warn('[AssetManager] Benutzerbezogener Tabellenzustand nicht verfügbar; Browser-Fallback wird verwendet.', error);
|
|
}
|
|
}
|
|
|
|
api.ready = loadUserState();
|
|
|
|
function tableKey(table, index) {
|
|
return table.dataset.storageKey || table.id || `table-${location.pathname}-${index}`;
|
|
}
|
|
|
|
function filterControls(table) {
|
|
return [...table.querySelectorAll('thead input, thead select, thead textarea')].filter(control => {
|
|
const type = (control.type || '').toLowerCase();
|
|
return !['checkbox', 'radio', 'button', 'submit'].includes(type);
|
|
});
|
|
}
|
|
|
|
function controlKey(control, index) {
|
|
const cell = control.closest('th');
|
|
return control.name || control.dataset.field || cell?.dataset.field || `filter-${index}`;
|
|
}
|
|
|
|
function restoreSort(table, index, force = false) {
|
|
if (!force && table.dataset.browserSortRestored === '1') return;
|
|
if (typeof table.assetApplySort !== 'function') return;
|
|
|
|
const key = `${tableKey(table, index)}:sort`;
|
|
let state = null;
|
|
try { state = JSON.parse(api.get(key) || 'null'); } catch (_) {}
|
|
if (state?.field && (state.direction === 'asc' || state.direction === 'desc')) {
|
|
table.assetApplySort(state.field, state.direction, {silent: true});
|
|
} else if (table.dataset.defaultSortField) {
|
|
table.assetApplySort(
|
|
table.dataset.defaultSortField,
|
|
table.dataset.defaultSortDirection === 'desc' ? 'desc' : 'asc',
|
|
{silent: true}
|
|
);
|
|
} else {
|
|
table.assetUpdateRowNumbers?.();
|
|
}
|
|
table.dataset.browserSortRestored = '1';
|
|
}
|
|
|
|
function restoreTable(table, index) {
|
|
if (table.dataset.browserStateReady === '1') return;
|
|
const restoreStarted = performance.now();
|
|
const controls = filterControls(table);
|
|
if (!controls.length) {
|
|
table.dataset.browserStateReady = '1';
|
|
restoreSort(table, index);
|
|
return;
|
|
}
|
|
|
|
table.dataset.browserStateReady = '1';
|
|
const key = `${tableKey(table, index)}:filters`;
|
|
let values = {};
|
|
try { values = JSON.parse(api.get(key) || '{}') || {}; } catch (_) {}
|
|
|
|
let serverValuesChanged = false;
|
|
controls.forEach((control, controlIndex) => {
|
|
const field = controlKey(control, controlIndex);
|
|
if (Object.prototype.hasOwnProperty.call(values, field)) {
|
|
if (control.value !== values[field]) serverValuesChanged = true;
|
|
control.value = values[field];
|
|
}
|
|
const save = () => {
|
|
const current = {};
|
|
filterControls(table).forEach((item, itemIndex) => {
|
|
current[controlKey(item, itemIndex)] = item.value;
|
|
});
|
|
api.set(key, JSON.stringify(current));
|
|
};
|
|
control.addEventListener('input', save);
|
|
control.addEventListener('change', save);
|
|
});
|
|
|
|
if (typeof table.assetApplyFilters === 'function') table.assetApplyFilters();
|
|
if (table.dataset.serverFilters === '1' && serverValuesChanged && typeof table.assetSubmitServerFilters === 'function') {
|
|
table.assetSubmitServerFilters(true);
|
|
}
|
|
restoreSort(table, index);
|
|
table.dataset.browserStateRestored = '1';
|
|
window.dispatchEvent(new CustomEvent('asset-table-state-ready', { detail: { table } }));
|
|
const duration = performance.now() - restoreStarted;
|
|
const logger = duration >= 50 ? console.warn : console.debug;
|
|
logger('[AssetManager][Performance]', 'browser-state:restoreTable', {
|
|
dauer_ms: Math.round(duration * 10) / 10,
|
|
filter: controls.length,
|
|
gespeicherte_werte: Object.keys(values).length
|
|
});
|
|
}
|
|
|
|
async function initializeTables() {
|
|
await api.ready;
|
|
document.querySelectorAll('table').forEach(restoreTable);
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initializeTables, { once: true });
|
|
} else {
|
|
initializeTables();
|
|
}
|
|
|
|
window.addEventListener('asset-table-structure-ready', initializeTables);
|
|
|
|
window.addEventListener('asset-table-filters-cleared', event => {
|
|
const table = event.detail?.table;
|
|
if (!(table instanceof HTMLTableElement)) return;
|
|
const tables = [...document.querySelectorAll('table')];
|
|
const index = tables.indexOf(table);
|
|
api.remove(`${tableKey(table, index >= 0 ? index : 0)}:filters`);
|
|
});
|
|
|
|
window.addEventListener('asset-table-sort-changed', event => {
|
|
const table = event.detail?.table;
|
|
if (!(table instanceof HTMLTableElement)) return;
|
|
const tables = [...document.querySelectorAll('table')];
|
|
const index = tables.indexOf(table);
|
|
api.set(`${tableKey(table, index >= 0 ? index : 0)}:sort`, JSON.stringify({
|
|
field: event.detail?.field || '',
|
|
direction: event.detail?.direction || 'asc'
|
|
}));
|
|
});
|
|
|
|
window.addEventListener('asset-table-content-reloaded', event => {
|
|
const table = event.detail?.table;
|
|
if (!(table instanceof HTMLTableElement)) return;
|
|
const tables = [...document.querySelectorAll('table')];
|
|
const index = tables.indexOf(table);
|
|
restoreSort(table, index >= 0 ? index : 0, true);
|
|
});
|
|
|
|
window.addEventListener('pagehide', () => { flushPending(); });
|
|
perf?.end('browser-state:gesamtes Modul');
|
|
})();
|