diff --git a/web/index.html b/web/index.html
index 4607937..fb544e7 100644
--- a/web/index.html
+++ b/web/index.html
@@ -1555,6 +1555,7 @@
|
|
|
+ |
|
|
|
diff --git a/web/style.css b/web/style.css
index 62992c9..f119db0 100644
--- a/web/style.css
+++ b/web/style.css
@@ -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;
}
diff --git a/web/trade-fairs.js b/web/trade-fairs.js
index 833e0c2..a920242 100644
--- a/web/trade-fairs.js
+++ b/web/trade-fairs.js
@@ -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();