Add draggable trade fair columns with persisted order and last-searched column
This commit is contained in:
@@ -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="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="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="stadt">Stadt</button></th>
|
||||
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="bundesland">Bundesland</button></th>
|
||||
|
||||
@@ -1883,6 +1883,7 @@ h1 {
|
||||
.bookmark-subpage__table th {
|
||||
background: #f3f4f6;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bookmark-subpage__sort {
|
||||
@@ -1905,6 +1906,32 @@ h1 {
|
||||
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 {
|
||||
border: none;
|
||||
background: transparent;
|
||||
@@ -1921,11 +1948,18 @@ h1 {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.bookmark-subpage__table td:nth-child(4),
|
||||
.bookmark-subpage__table td:nth-child(15) {
|
||||
.bookmark-subpage__table th[data-column-key="thema"],
|
||||
.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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,48 @@
|
||||
const searchInput = document.getElementById('tradeFairSearchInput');
|
||||
const meta = document.getElementById('tradeFairMeta');
|
||||
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 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;
|
||||
}
|
||||
|
||||
@@ -526,6 +565,7 @@
|
||||
tage_bis_start: 'number',
|
||||
rang: 'number',
|
||||
messe: 'text',
|
||||
zuletzt_gesucht_am: 'date',
|
||||
thema: 'text',
|
||||
stadt: 'text',
|
||||
bundesland: 'text',
|
||||
@@ -544,6 +584,8 @@
|
||||
let sortKey = 'rang';
|
||||
let sortDirection = 'asc';
|
||||
let searchTerm = '';
|
||||
let draggedColumnKey = null;
|
||||
let lastOpenedByTradeFair = {};
|
||||
|
||||
function toNumber(value) {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
@@ -582,7 +624,8 @@
|
||||
function normalizeRow(row) {
|
||||
return {
|
||||
...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() {
|
||||
try {
|
||||
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');
|
||||
if (columnKey) {
|
||||
td.dataset.column = columnKey;
|
||||
}
|
||||
td.textContent = content;
|
||||
return td;
|
||||
}
|
||||
@@ -757,81 +1033,106 @@
|
||||
function openTradeFairSearch(row) {
|
||||
const queries = buildTradeFairQueries(row);
|
||||
if (!queries.length) {
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
|
||||
let opened = 0;
|
||||
if (typeof window.openBookmarkQueries === 'function') {
|
||||
const opened = window.openBookmarkQueries(queries);
|
||||
if (opened > 0) {
|
||||
return;
|
||||
}
|
||||
opened = window.openBookmarkQueries(queries);
|
||||
}
|
||||
|
||||
queries.forEach((query) => {
|
||||
openQueryFallback(query);
|
||||
});
|
||||
if (opened === 0) {
|
||||
queries.forEach((query) => {
|
||||
opened += openQueryFallback(query);
|
||||
});
|
||||
}
|
||||
|
||||
if (opened > 0) {
|
||||
setTradeFairLastOpened(row);
|
||||
}
|
||||
return opened;
|
||||
}
|
||||
|
||||
function createMesseCell(row) {
|
||||
const td = document.createElement('td');
|
||||
td.dataset.column = 'messe';
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'bookmark-subpage__messe-link';
|
||||
button.textContent = row.messe || 'k.A.';
|
||||
button.title = 'Als Bookmark-Suche oeffnen (3 Tabs)';
|
||||
button.title = getTradeFairHoverTitle(row);
|
||||
button.addEventListener('click', () => {
|
||||
openTradeFairSearch(row);
|
||||
const opened = openTradeFairSearch(row);
|
||||
if (opened > 0) {
|
||||
button.title = getTradeFairHoverTitle(row);
|
||||
render();
|
||||
}
|
||||
});
|
||||
td.appendChild(button);
|
||||
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() {
|
||||
const normalizedRows = TRADE_FAIRS.map(normalizeRow);
|
||||
const filtered = normalizedRows.filter((row) => matchesSearch(row, searchTerm));
|
||||
const sorted = sortRows(filtered);
|
||||
const columnOrder = getColumnOrderFromHeader();
|
||||
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
if (!sorted.length) {
|
||||
const emptyRow = document.createElement('tr');
|
||||
const emptyCell = document.createElement('td');
|
||||
emptyCell.colSpan = 16;
|
||||
emptyCell.colSpan = columnOrder.length;
|
||||
emptyCell.textContent = 'Keine Messe zum Suchbegriff gefunden.';
|
||||
emptyRow.appendChild(emptyCell);
|
||||
tableBody.appendChild(emptyRow);
|
||||
} else {
|
||||
sorted.forEach((row) => {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
tr.appendChild(createCell(Number.isFinite(row.tage_bis_start) ? String(row.tage_bis_start) : 'k.A.'));
|
||||
tr.appendChild(createCell(String(row.rang)));
|
||||
tr.appendChild(createMesseCell(row));
|
||||
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);
|
||||
const rowCells = createRowCells(row);
|
||||
columnOrder.forEach((columnKey) => {
|
||||
const cell = rowCells[columnKey] || createCell('k.A.', columnKey);
|
||||
tr.appendChild(cell);
|
||||
});
|
||||
|
||||
tableBody.appendChild(tr);
|
||||
});
|
||||
@@ -873,6 +1174,9 @@
|
||||
render();
|
||||
});
|
||||
|
||||
lastOpenedByTradeFair = loadLastOpenedState();
|
||||
loadColumnOrder();
|
||||
setupColumnDragAndDrop();
|
||||
loadSortState();
|
||||
updateSortButtonState();
|
||||
render();
|
||||
|
||||
Reference in New Issue
Block a user