Add draggable trade fair columns with persisted order and last-searched column

This commit is contained in:
2026-02-21 13:03:58 +01:00
parent 3ba24fd969
commit bf25e0f70e
3 changed files with 384 additions and 45 deletions

View File

@@ -1555,6 +1555,7 @@
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="tage_bis_start">Tage bis Start</button></th> <th><button type="button" class="bookmark-subpage__sort" data-trade-sort="tage_bis_start">Tage bis Start</button></th>
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="rang">Rang</button></th> <th><button type="button" class="bookmark-subpage__sort" data-trade-sort="rang">Rang</button></th>
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="messe">Messe</button></th> <th><button type="button" class="bookmark-subpage__sort" data-trade-sort="messe">Messe</button></th>
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="zuletzt_gesucht_am">Zuletzt gesucht am</button></th>
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="thema">Thema</button></th> <th><button type="button" class="bookmark-subpage__sort" data-trade-sort="thema">Thema</button></th>
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="stadt">Stadt</button></th> <th><button type="button" class="bookmark-subpage__sort" data-trade-sort="stadt">Stadt</button></th>
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="bundesland">Bundesland</button></th> <th><button type="button" class="bookmark-subpage__sort" data-trade-sort="bundesland">Bundesland</button></th>

View File

@@ -1883,6 +1883,7 @@ h1 {
.bookmark-subpage__table th { .bookmark-subpage__table th {
background: #f3f4f6; background: #f3f4f6;
white-space: nowrap; white-space: nowrap;
position: relative;
} }
.bookmark-subpage__sort { .bookmark-subpage__sort {
@@ -1905,6 +1906,32 @@ h1 {
color: #0f4bb8; color: #0f4bb8;
} }
.bookmark-subpage__table th[draggable="true"] {
cursor: move;
}
.bookmark-subpage__table th.is-dragging {
opacity: 0.55;
}
.bookmark-subpage__table th.is-drop-before::before,
.bookmark-subpage__table th.is-drop-after::after {
content: '';
position: absolute;
top: 3px;
bottom: 3px;
width: 2px;
background: #0f4bb8;
}
.bookmark-subpage__table th.is-drop-before::before {
left: -1px;
}
.bookmark-subpage__table th.is-drop-after::after {
right: -1px;
}
.bookmark-subpage__messe-link { .bookmark-subpage__messe-link {
border: none; border: none;
background: transparent; background: transparent;
@@ -1921,11 +1948,18 @@ h1 {
color: #1d4ed8; color: #1d4ed8;
} }
.bookmark-subpage__table td:nth-child(4), .bookmark-subpage__table th[data-column-key="thema"],
.bookmark-subpage__table td:nth-child(15) { .bookmark-subpage__table td[data-column="thema"],
.bookmark-subpage__table th[data-column-key="notiz"],
.bookmark-subpage__table td[data-column="notiz"] {
min-width: 320px; min-width: 320px;
} }
.bookmark-subpage__table th[data-column-key="zuletzt_gesucht_am"],
.bookmark-subpage__table td[data-column="zuletzt_gesucht_am"] {
min-width: 170px;
}
.bookmark-subpage__table a { .bookmark-subpage__table a {
color: #1d4ed8; color: #1d4ed8;
} }

View File

@@ -3,9 +3,48 @@
const searchInput = document.getElementById('tradeFairSearchInput'); const searchInput = document.getElementById('tradeFairSearchInput');
const meta = document.getElementById('tradeFairMeta'); const meta = document.getElementById('tradeFairMeta');
const sortButtons = Array.from(document.querySelectorAll('.bookmark-subpage__sort[data-trade-sort]')); const sortButtons = Array.from(document.querySelectorAll('.bookmark-subpage__sort[data-trade-sort]'));
const table = tableBody ? tableBody.closest('table') : null;
const headerRow = table ? table.querySelector('thead tr') : null;
const SORT_STATE_KEY = 'fb_trade_fairs_sort_v1'; const SORT_STATE_KEY = 'fb_trade_fairs_sort_v1';
const COLUMN_ORDER_STATE_KEY = 'fb_trade_fairs_columns_v1';
const LAST_OPEN_STATE_KEY = 'fb_trade_fairs_last_open_v1';
const DEFAULT_COLUMN_ORDER = [
'tage_bis_start',
'rang',
'messe',
'zuletzt_gesucht_am',
'thema',
'stadt',
'bundesland',
'termin_start',
'termin_ende',
'besucher',
'besucher_jahr',
'besucher_status',
'ausstellungsflaeche_m2',
'ticketpreis_we_eur',
'ticketpreis_unterderwoche_eur',
'notiz',
'quelle_homepage'
];
if (!tableBody || !searchInput || !meta || !sortButtons.length) { if (!tableBody || !searchInput || !meta || !sortButtons.length || !table || !headerRow) {
return;
}
const headerCellsByKey = new Map();
sortButtons.forEach((button) => {
const key = button.dataset.tradeSort;
const th = button.closest('th');
if (!key || !th) {
return;
}
th.dataset.columnKey = key;
th.draggable = true;
headerCellsByKey.set(key, th);
});
if (headerCellsByKey.size !== DEFAULT_COLUMN_ORDER.length) {
return; return;
} }
@@ -526,6 +565,7 @@
tage_bis_start: 'number', tage_bis_start: 'number',
rang: 'number', rang: 'number',
messe: 'text', messe: 'text',
zuletzt_gesucht_am: 'date',
thema: 'text', thema: 'text',
stadt: 'text', stadt: 'text',
bundesland: 'text', bundesland: 'text',
@@ -544,6 +584,8 @@
let sortKey = 'rang'; let sortKey = 'rang';
let sortDirection = 'asc'; let sortDirection = 'asc';
let searchTerm = ''; let searchTerm = '';
let draggedColumnKey = null;
let lastOpenedByTradeFair = {};
function toNumber(value) { function toNumber(value) {
if (typeof value === 'number' && Number.isFinite(value)) { if (typeof value === 'number' && Number.isFinite(value)) {
@@ -582,7 +624,8 @@
function normalizeRow(row) { function normalizeRow(row) {
return { return {
...row, ...row,
tage_bis_start: getDaysUntil(row.termin_start) tage_bis_start: getDaysUntil(row.termin_start),
zuletzt_gesucht_am: lastOpenedByTradeFair[getTradeFairOpenKey(row)] || null
}; };
} }
@@ -676,6 +719,145 @@
}; };
} }
function normalizeColumnOrder(rawOrder) {
if (!Array.isArray(rawOrder) || rawOrder.length !== DEFAULT_COLUMN_ORDER.length) {
return null;
}
const filtered = rawOrder.filter((key) => DEFAULT_COLUMN_ORDER.includes(key));
if (filtered.length !== DEFAULT_COLUMN_ORDER.length) {
return null;
}
if (new Set(filtered).size !== DEFAULT_COLUMN_ORDER.length) {
return null;
}
return filtered;
}
function getColumnOrderFromHeader() {
const keys = Array.from(headerRow.children)
.map((th) => th.dataset.columnKey)
.filter((key) => DEFAULT_COLUMN_ORDER.includes(key));
if (keys.length !== DEFAULT_COLUMN_ORDER.length) {
return [...DEFAULT_COLUMN_ORDER];
}
return keys;
}
function applyColumnOrder(order) {
order.forEach((key) => {
const th = headerCellsByKey.get(key);
if (th) {
headerRow.appendChild(th);
}
});
}
function loadColumnOrder() {
try {
const raw = localStorage.getItem(COLUMN_ORDER_STATE_KEY);
if (!raw) {
return;
}
const parsed = JSON.parse(raw);
const normalized = normalizeColumnOrder(parsed);
if (!normalized) {
return;
}
applyColumnOrder(normalized);
} catch (_error) {
// ignore
}
}
function persistColumnOrder() {
try {
localStorage.setItem(COLUMN_ORDER_STATE_KEY, JSON.stringify(getColumnOrderFromHeader()));
} catch (_error) {
// ignore
}
}
function normalizeLastOpenedState(rawState) {
if (!rawState || typeof rawState !== 'object' || Array.isArray(rawState)) {
return {};
}
const normalized = {};
Object.entries(rawState).forEach(([key, value]) => {
if (typeof key !== 'string' || typeof value !== 'string') {
return;
}
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
return;
}
normalized[key] = parsed.toISOString();
});
return normalized;
}
function loadLastOpenedState() {
try {
const raw = localStorage.getItem(LAST_OPEN_STATE_KEY);
if (!raw) {
return {};
}
return normalizeLastOpenedState(JSON.parse(raw));
} catch (_error) {
return {};
}
}
function persistLastOpenedState() {
try {
localStorage.setItem(LAST_OPEN_STATE_KEY, JSON.stringify(lastOpenedByTradeFair));
} catch (_error) {
// ignore
}
}
function getTradeFairOpenKey(row) {
const messe = String(row.messe || '').trim().toLocaleLowerCase('de-DE');
const city = String(row.stadt || '').trim().toLocaleLowerCase('de-DE');
return `${messe}|${city}`;
}
function setTradeFairLastOpened(row) {
const key = getTradeFairOpenKey(row);
if (!key) {
return null;
}
const nowIso = new Date().toISOString();
lastOpenedByTradeFair[key] = nowIso;
persistLastOpenedState();
return nowIso;
}
function formatDateTime(iso) {
const parsed = new Date(iso);
if (Number.isNaN(parsed.getTime())) {
return 'k.A.';
}
return parsed.toLocaleString('de-DE');
}
function formatLastSearched(iso) {
if (!iso) {
return 'noch nie';
}
return formatDateTime(iso);
}
function getTradeFairHoverTitle(row) {
const key = getTradeFairOpenKey(row);
const lastOpened = key ? lastOpenedByTradeFair[key] : '';
const suffix = lastOpened ? formatDateTime(lastOpened) : 'noch nie';
return `Als Bookmark-Suche oeffnen (3 Tabs)\\nZuletzt geoeffnet: ${suffix}`;
}
function loadSortState() { function loadSortState() {
try { try {
const raw = localStorage.getItem(SORT_STATE_KEY); const raw = localStorage.getItem(SORT_STATE_KEY);
@@ -716,8 +898,102 @@
}); });
} }
function createCell(content) { function clearDropMarkers() {
Array.from(headerRow.children).forEach((th) => {
if (th.dataset.columnKey !== draggedColumnKey) {
th.classList.remove('is-dragging');
}
th.classList.remove('is-drop-before', 'is-drop-after');
});
}
function setupColumnDragAndDrop() {
Array.from(headerCellsByKey.values()).forEach((th) => {
th.addEventListener('dragstart', (event) => {
const key = th.dataset.columnKey;
if (!key) {
event.preventDefault();
return;
}
draggedColumnKey = key;
clearDropMarkers();
th.classList.add('is-dragging');
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
try {
event.dataTransfer.setData('text/plain', key);
} catch (_error) {
// ignore
}
}
});
th.addEventListener('dragover', (event) => {
if (!draggedColumnKey || th.dataset.columnKey === draggedColumnKey) {
return;
}
event.preventDefault();
clearDropMarkers();
const rect = th.getBoundingClientRect();
const insertAfter = event.clientX >= rect.left + (rect.width / 2);
th.classList.toggle('is-drop-before', !insertAfter);
th.classList.toggle('is-drop-after', insertAfter);
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'move';
}
});
th.addEventListener('dragleave', () => {
th.classList.remove('is-drop-before', 'is-drop-after');
});
th.addEventListener('drop', (event) => {
event.preventDefault();
const targetKey = th.dataset.columnKey;
const sourceKey = draggedColumnKey || (event.dataTransfer ? event.dataTransfer.getData('text/plain') : '');
if (!sourceKey || !targetKey || sourceKey === targetKey) {
draggedColumnKey = null;
clearDropMarkers();
return;
}
const sourceTh = headerCellsByKey.get(sourceKey);
const targetTh = headerCellsByKey.get(targetKey);
if (!sourceTh || !targetTh) {
draggedColumnKey = null;
clearDropMarkers();
return;
}
const rect = targetTh.getBoundingClientRect();
const insertAfter = event.clientX >= rect.left + (rect.width / 2);
if (insertAfter) {
headerRow.insertBefore(sourceTh, targetTh.nextSibling);
} else {
headerRow.insertBefore(sourceTh, targetTh);
}
draggedColumnKey = null;
clearDropMarkers();
persistColumnOrder();
render();
});
th.addEventListener('dragend', () => {
draggedColumnKey = null;
clearDropMarkers();
});
});
}
function createCell(content, columnKey) {
const td = document.createElement('td'); const td = document.createElement('td');
if (columnKey) {
td.dataset.column = columnKey;
}
td.textContent = content; td.textContent = content;
return td; return td;
} }
@@ -757,81 +1033,106 @@
function openTradeFairSearch(row) { function openTradeFairSearch(row) {
const queries = buildTradeFairQueries(row); const queries = buildTradeFairQueries(row);
if (!queries.length) { if (!queries.length) {
return; return 0;
} }
let opened = 0;
if (typeof window.openBookmarkQueries === 'function') { if (typeof window.openBookmarkQueries === 'function') {
const opened = window.openBookmarkQueries(queries); opened = window.openBookmarkQueries(queries);
if (opened > 0) {
return;
}
} }
queries.forEach((query) => { if (opened === 0) {
openQueryFallback(query); queries.forEach((query) => {
}); opened += openQueryFallback(query);
});
}
if (opened > 0) {
setTradeFairLastOpened(row);
}
return opened;
} }
function createMesseCell(row) { function createMesseCell(row) {
const td = document.createElement('td'); const td = document.createElement('td');
td.dataset.column = 'messe';
const button = document.createElement('button'); const button = document.createElement('button');
button.type = 'button'; button.type = 'button';
button.className = 'bookmark-subpage__messe-link'; button.className = 'bookmark-subpage__messe-link';
button.textContent = row.messe || 'k.A.'; button.textContent = row.messe || 'k.A.';
button.title = 'Als Bookmark-Suche oeffnen (3 Tabs)'; button.title = getTradeFairHoverTitle(row);
button.addEventListener('click', () => { button.addEventListener('click', () => {
openTradeFairSearch(row); const opened = openTradeFairSearch(row);
if (opened > 0) {
button.title = getTradeFairHoverTitle(row);
render();
}
}); });
td.appendChild(button); td.appendChild(button);
return td; return td;
} }
function createSourceCell(row) {
const sourceCell = document.createElement('td');
sourceCell.dataset.column = 'quelle_homepage';
if (row.quelle_homepage) {
const link = document.createElement('a');
link.href = row.quelle_homepage;
link.target = '_blank';
link.rel = 'noopener';
link.textContent = row.quelle_homepage;
sourceCell.appendChild(link);
} else {
sourceCell.textContent = 'k.A.';
}
return sourceCell;
}
function createRowCells(row) {
return {
tage_bis_start: createCell(Number.isFinite(row.tage_bis_start) ? String(row.tage_bis_start) : 'k.A.', 'tage_bis_start'),
rang: createCell(String(row.rang), 'rang'),
messe: createMesseCell(row),
zuletzt_gesucht_am: createCell(formatLastSearched(row.zuletzt_gesucht_am), 'zuletzt_gesucht_am'),
thema: createCell(row.thema, 'thema'),
stadt: createCell(row.stadt, 'stadt'),
bundesland: createCell(row.bundesland, 'bundesland'),
termin_start: createCell(formatDate(row.termin_start), 'termin_start'),
termin_ende: createCell(formatDate(row.termin_ende), 'termin_ende'),
besucher: createCell(formatNumber(row.besucher), 'besucher'),
besucher_jahr: createCell(formatNumber(row.besucher_jahr), 'besucher_jahr'),
besucher_status: createCell(row.besucher_status, 'besucher_status'),
ausstellungsflaeche_m2: createCell(formatNumber(row.ausstellungsflaeche_m2), 'ausstellungsflaeche_m2'),
ticketpreis_we_eur: createCell(formatPrice(row.ticketpreis_we_eur), 'ticketpreis_we_eur'),
ticketpreis_unterderwoche_eur: createCell(formatPrice(row.ticketpreis_unterderwoche_eur), 'ticketpreis_unterderwoche_eur'),
notiz: createCell(row.notiz, 'notiz'),
quelle_homepage: createSourceCell(row)
};
}
function render() { function render() {
const normalizedRows = TRADE_FAIRS.map(normalizeRow); const normalizedRows = TRADE_FAIRS.map(normalizeRow);
const filtered = normalizedRows.filter((row) => matchesSearch(row, searchTerm)); const filtered = normalizedRows.filter((row) => matchesSearch(row, searchTerm));
const sorted = sortRows(filtered); const sorted = sortRows(filtered);
const columnOrder = getColumnOrderFromHeader();
tableBody.innerHTML = ''; tableBody.innerHTML = '';
if (!sorted.length) { if (!sorted.length) {
const emptyRow = document.createElement('tr'); const emptyRow = document.createElement('tr');
const emptyCell = document.createElement('td'); const emptyCell = document.createElement('td');
emptyCell.colSpan = 16; emptyCell.colSpan = columnOrder.length;
emptyCell.textContent = 'Keine Messe zum Suchbegriff gefunden.'; emptyCell.textContent = 'Keine Messe zum Suchbegriff gefunden.';
emptyRow.appendChild(emptyCell); emptyRow.appendChild(emptyCell);
tableBody.appendChild(emptyRow); tableBody.appendChild(emptyRow);
} else { } else {
sorted.forEach((row) => { sorted.forEach((row) => {
const tr = document.createElement('tr'); const tr = document.createElement('tr');
const rowCells = createRowCells(row);
tr.appendChild(createCell(Number.isFinite(row.tage_bis_start) ? String(row.tage_bis_start) : 'k.A.')); columnOrder.forEach((columnKey) => {
tr.appendChild(createCell(String(row.rang))); const cell = rowCells[columnKey] || createCell('k.A.', columnKey);
tr.appendChild(createMesseCell(row)); tr.appendChild(cell);
tr.appendChild(createCell(row.thema)); });
tr.appendChild(createCell(row.stadt));
tr.appendChild(createCell(row.bundesland));
tr.appendChild(createCell(formatDate(row.termin_start)));
tr.appendChild(createCell(formatDate(row.termin_ende)));
tr.appendChild(createCell(formatNumber(row.besucher)));
tr.appendChild(createCell(formatNumber(row.besucher_jahr)));
tr.appendChild(createCell(row.besucher_status));
tr.appendChild(createCell(formatNumber(row.ausstellungsflaeche_m2)));
tr.appendChild(createCell(formatPrice(row.ticketpreis_we_eur)));
tr.appendChild(createCell(formatPrice(row.ticketpreis_unterderwoche_eur)));
tr.appendChild(createCell(row.notiz));
const sourceCell = document.createElement('td');
if (row.quelle_homepage) {
const link = document.createElement('a');
link.href = row.quelle_homepage;
link.target = '_blank';
link.rel = 'noopener';
link.textContent = row.quelle_homepage;
sourceCell.appendChild(link);
} else {
sourceCell.textContent = 'k.A.';
}
tr.appendChild(sourceCell);
tableBody.appendChild(tr); tableBody.appendChild(tr);
}); });
@@ -873,6 +1174,9 @@
render(); render();
}); });
lastOpenedByTradeFair = loadLastOpenedState();
loadColumnOrder();
setupColumnDragAndDrop();
loadSortState(); loadSortState();
updateSortButtonState(); updateSortButtonState();
render(); render();