From 7abb98e924564d5942b0773acf88f00d9590de6b Mon Sep 17 00:00:00 2001 From: Meik Date: Sat, 21 Feb 2026 20:14:08 +0100 Subject: [PATCH] Add trade fair column settings modal with visibility controls --- web/index.html | 18 ++- web/style.css | 138 +++++++++++++++++++++++ web/trade-fairs.js | 274 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 420 insertions(+), 10 deletions(-) diff --git a/web/index.html b/web/index.html index a24c8ad..f762d79 100644 --- a/web/index.html +++ b/web/index.html @@ -1546,7 +1546,23 @@ Messesuche -
+
+
+ +
+ +
diff --git a/web/style.css b/web/style.css index 7550de4..e49b918 100644 --- a/web/style.css +++ b/web/style.css @@ -1856,11 +1856,143 @@ h1 { align-items: flex-end; } +.bookmark-subpage__toolbar-actions { + margin-left: auto; + display: inline-flex; + align-items: center; + gap: 8px; +} + .bookmark-subpage__meta { font-size: 12px; color: #4b5563; } +.bookmark-subpage__config-btn { + border: 1px solid #d1d5db; + background: #ffffff; + color: #1f2937; + border-radius: 8px; + width: 32px; + height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 16px; +} + +.bookmark-subpage__config-btn:hover { + background: #f3f4f6; +} + +.bookmark-columns-modal { + position: fixed; + inset: 0; + z-index: 1100; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.bookmark-columns-modal[hidden] { + display: none; +} + +.bookmark-columns-modal__backdrop { + position: absolute; + inset: 0; + background: rgba(15, 23, 42, 0.5); +} + +.bookmark-columns-modal__content { + position: relative; + background: #ffffff; + border-radius: 12px; + width: min(640px, 94vw); + max-height: 86vh; + overflow: auto; + padding: 16px; + box-shadow: 0 16px 44px rgba(15, 23, 42, 0.25); +} + +.bookmark-columns-modal__content h3 { + margin: 0 0 6px; + font-size: 18px; +} + +.bookmark-columns-modal__hint { + margin: 0 0 12px; + color: #4b5563; + font-size: 13px; +} + +.bookmark-columns-modal__close { + position: absolute; + right: 12px; + top: 8px; + border: none; + background: transparent; + font-size: 24px; + line-height: 1; + cursor: pointer; + color: #374151; +} + +.bookmark-columns-modal__list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.bookmark-columns-modal__item { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 8px; + border: 1px solid #e5e7eb; + border-radius: 8px; + padding: 8px 10px; + background: #f9fafb; +} + +.bookmark-columns-modal__toggle { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: #1f2937; +} + +.bookmark-columns-modal__item-actions { + display: inline-flex; + gap: 6px; +} + +.bookmark-columns-modal__move { + width: 28px; + height: 28px; + border: 1px solid #d1d5db; + border-radius: 6px; + background: #fff; + cursor: pointer; + font-size: 14px; + color: #374151; +} + +.bookmark-columns-modal__move:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.bookmark-columns-modal__actions { + margin-top: 14px; + display: flex; + justify-content: flex-end; + gap: 8px; +} + .bookmark-subpage__table-wrap { width: 100%; overflow-x: auto; @@ -1997,6 +2129,12 @@ h1 { .bookmark-subpage__toolbar { gap: 8px; } + + .bookmark-subpage__toolbar-actions { + margin-left: 0; + width: 100%; + justify-content: space-between; + } } .screenshot-modal { diff --git a/web/trade-fairs.js b/web/trade-fairs.js index 569a0b0..8f2c115 100644 --- a/web/trade-fairs.js +++ b/web/trade-fairs.js @@ -2,11 +2,19 @@ const tableBody = document.getElementById('tradeFairTableBody'); const searchInput = document.getElementById('tradeFairSearchInput'); const meta = document.getElementById('tradeFairMeta'); + const columnSettingsBtn = document.getElementById('tradeFairColumnSettingsBtn'); + const columnsModal = document.getElementById('tradeFairColumnsModal'); + const columnsModalBackdrop = document.getElementById('tradeFairColumnsModalBackdrop'); + const columnsModalClose = document.getElementById('tradeFairColumnsModalClose'); + const columnsModalDone = document.getElementById('tradeFairColumnsDoneBtn'); + const columnsModalReset = document.getElementById('tradeFairColumnsResetBtn'); + const columnsList = document.getElementById('tradeFairColumnsList'); 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 COLUMN_VISIBILITY_STATE_KEY = 'fb_trade_fairs_column_visibility_v1'; const LAST_OPEN_STATE_KEY = 'fb_trade_fairs_last_open_v1'; const DEFAULT_COLUMN_ORDER = [ 'tage_bis_start', @@ -33,12 +41,14 @@ } const headerCellsByKey = new Map(); + const columnLabelByKey = new Map(); sortButtons.forEach((button) => { const key = button.dataset.tradeSort; const th = button.closest('th'); if (!key || !th) { return; } + columnLabelByKey.set(key, button.textContent.trim()); th.dataset.columnKey = key; th.draggable = true; headerCellsByKey.set(key, th); @@ -586,8 +596,6 @@ rang: index + 1 })); - const HIDDEN_FREE_FAIRS_COUNT = TRADE_FAIRS.length - EFFECTIVE_TRADE_FAIRS.length; - const SORT_TYPES = { tage_bis_start: 'number', rang: 'number', @@ -613,6 +621,7 @@ let searchTerm = ''; let draggedColumnKey = null; let lastOpenedByTradeFair = {}; + let columnVisibility = Object.fromEntries(DEFAULT_COLUMN_ORDER.map((key) => [key, true])); function toNumber(value) { if (typeof value === 'number' && Number.isFinite(value)) { @@ -807,6 +816,251 @@ } } + function normalizeColumnVisibility(rawState) { + if (!rawState || typeof rawState !== 'object' || Array.isArray(rawState)) { + return null; + } + + const normalized = {}; + DEFAULT_COLUMN_ORDER.forEach((key) => { + const value = rawState[key]; + normalized[key] = value !== false; + }); + return normalized; + } + + function loadColumnVisibility() { + try { + const raw = localStorage.getItem(COLUMN_VISIBILITY_STATE_KEY); + if (!raw) { + return; + } + const parsed = JSON.parse(raw); + const normalized = normalizeColumnVisibility(parsed); + if (!normalized) { + return; + } + columnVisibility = normalized; + } catch (_error) { + // ignore + } + } + + function persistColumnVisibility() { + try { + localStorage.setItem(COLUMN_VISIBILITY_STATE_KEY, JSON.stringify(columnVisibility)); + } catch (_error) { + // ignore + } + } + + function isColumnVisible(columnKey) { + return columnVisibility[columnKey] !== false; + } + + function getVisibleColumnOrder() { + const order = getColumnOrderFromHeader().filter((columnKey) => isColumnVisible(columnKey)); + if (order.length) { + return order; + } + return ['messe']; + } + + function ensureValidSortColumn() { + if (isColumnVisible(sortKey)) { + return; + } + const visibleColumns = getVisibleColumnOrder(); + if (visibleColumns.includes('rang')) { + sortKey = 'rang'; + sortDirection = 'asc'; + persistSortState(); + return; + } + sortKey = visibleColumns[0] || 'messe'; + sortDirection = 'asc'; + persistSortState(); + } + + function applyColumnVisibilityToHeader() { + headerCellsByKey.forEach((th, key) => { + const visible = isColumnVisible(key); + th.hidden = !visible; + th.style.display = visible ? '' : 'none'; + }); + } + + function getColumnLabel(columnKey) { + const fromBase = sortButtons.find((button) => button.dataset.tradeSort === columnKey)?.dataset.baseLabel; + if (fromBase) { + return fromBase; + } + return columnLabelByKey.get(columnKey) || columnKey; + } + + function moveColumnInOrder(columnKey, direction) { + const order = getColumnOrderFromHeader(); + const index = order.indexOf(columnKey); + if (index === -1) { + return; + } + const targetIndex = direction < 0 ? index - 1 : index + 1; + if (targetIndex < 0 || targetIndex >= order.length) { + return; + } + + const nextOrder = [...order]; + const [item] = nextOrder.splice(index, 1); + nextOrder.splice(targetIndex, 0, item); + applyColumnOrder(nextOrder); + persistColumnOrder(); + applyColumnVisibilityToHeader(); + render(); + renderColumnSettingsList(); + } + + function setColumnVisibility(columnKey, visible) { + const nextState = { + ...columnVisibility, + [columnKey]: visible + }; + const visibleCount = DEFAULT_COLUMN_ORDER.filter((key) => nextState[key] !== false).length; + if (visibleCount === 0) { + return false; + } + + columnVisibility = nextState; + persistColumnVisibility(); + applyColumnVisibilityToHeader(); + ensureValidSortColumn(); + updateSortButtonState(); + render(); + return true; + } + + function renderColumnSettingsList() { + if (!columnsList) { + return; + } + + const order = getColumnOrderFromHeader(); + columnsList.innerHTML = ''; + order.forEach((columnKey, index) => { + const item = document.createElement('div'); + item.className = 'bookmark-columns-modal__item'; + + const label = document.createElement('label'); + label.className = 'bookmark-columns-modal__toggle'; + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.checked = isColumnVisible(columnKey); + checkbox.addEventListener('change', () => { + const ok = setColumnVisibility(columnKey, checkbox.checked); + if (!ok) { + checkbox.checked = true; + return; + } + renderColumnSettingsList(); + }); + const text = document.createElement('span'); + text.textContent = getColumnLabel(columnKey); + label.append(checkbox, text); + + const actions = document.createElement('div'); + actions.className = 'bookmark-columns-modal__item-actions'; + const upButton = document.createElement('button'); + upButton.type = 'button'; + upButton.className = 'bookmark-columns-modal__move'; + upButton.textContent = '↑'; + upButton.title = 'Nach oben'; + upButton.disabled = index === 0; + upButton.addEventListener('click', () => { + moveColumnInOrder(columnKey, -1); + }); + + const downButton = document.createElement('button'); + downButton.type = 'button'; + downButton.className = 'bookmark-columns-modal__move'; + downButton.textContent = '↓'; + downButton.title = 'Nach unten'; + downButton.disabled = index === order.length - 1; + downButton.addEventListener('click', () => { + moveColumnInOrder(columnKey, 1); + }); + + actions.append(upButton, downButton); + item.append(label, actions); + columnsList.appendChild(item); + }); + } + + function closeColumnSettingsModal() { + if (!columnsModal) { + return; + } + columnsModal.hidden = true; + } + + function openColumnSettingsModal() { + if (!columnsModal) { + return; + } + renderColumnSettingsList(); + columnsModal.hidden = false; + } + + function resetColumnSettings() { + applyColumnOrder(DEFAULT_COLUMN_ORDER); + columnVisibility = Object.fromEntries(DEFAULT_COLUMN_ORDER.map((key) => [key, true])); + persistColumnOrder(); + persistColumnVisibility(); + applyColumnVisibilityToHeader(); + ensureValidSortColumn(); + updateSortButtonState(); + render(); + renderColumnSettingsList(); + } + + function setupColumnSettingsModal() { + if (!columnSettingsBtn || !columnsModal) { + return; + } + + columnSettingsBtn.addEventListener('click', () => { + openColumnSettingsModal(); + }); + + if (columnsModalBackdrop) { + columnsModalBackdrop.addEventListener('click', () => { + closeColumnSettingsModal(); + }); + } + + if (columnsModalClose) { + columnsModalClose.addEventListener('click', () => { + closeColumnSettingsModal(); + }); + } + + if (columnsModalDone) { + columnsModalDone.addEventListener('click', () => { + closeColumnSettingsModal(); + }); + } + + if (columnsModalReset) { + columnsModalReset.addEventListener('click', () => { + resetColumnSettings(); + }); + } + + document.addEventListener('keydown', (event) => { + if (event.key === 'Escape' && columnsModal && !columnsModal.hidden) { + closeColumnSettingsModal(); + } + }); + } + function normalizeLastOpenedState(rawState) { if (!rawState || typeof rawState !== 'object' || Array.isArray(rawState)) { return {}; @@ -1007,6 +1261,7 @@ clearDropMarkers(); persistColumnOrder(); render(); + renderColumnSettingsList(); }); th.addEventListener('dragend', () => { @@ -1183,14 +1438,14 @@ const normalizedRows = EFFECTIVE_TRADE_FAIRS.map(normalizeRow); const filtered = normalizedRows.filter((row) => matchesSearch(row, searchTerm)); const sorted = sortRows(filtered); - const columnOrder = getColumnOrderFromHeader(); + const visibleColumnOrder = getVisibleColumnOrder(); tableBody.innerHTML = ''; if (!sorted.length) { const emptyRow = document.createElement('tr'); const emptyCell = document.createElement('td'); - emptyCell.colSpan = columnOrder.length; + emptyCell.colSpan = visibleColumnOrder.length; emptyCell.textContent = 'Keine Messe zum Suchbegriff gefunden.'; emptyRow.appendChild(emptyCell); tableBody.appendChild(emptyRow); @@ -1198,7 +1453,7 @@ sorted.forEach((row) => { const tr = document.createElement('tr'); const rowCells = createRowCells(row); - columnOrder.forEach((columnKey) => { + visibleColumnOrder.forEach((columnKey) => { const cell = rowCells[columnKey] || createCell('k.A.', columnKey); tr.appendChild(cell); }); @@ -1209,10 +1464,7 @@ const activeSortButton = sortButtons.find((button) => button.dataset.tradeSort === sortKey); const sortLabel = activeSortButton?.dataset.baseLabel || sortKey; - const hiddenInfo = HIDDEN_FREE_FAIRS_COUNT > 0 - ? ` | ${HIDDEN_FREE_FAIRS_COUNT} kostenlose Messe${HIDDEN_FREE_FAIRS_COUNT === 1 ? '' : 'n'} ausgeblendet` - : ''; - meta.textContent = `${sorted.length} von ${EFFECTIVE_TRADE_FAIRS.length} Messen${hiddenInfo} | Sortierung: ${sortLabel} (${sortDirection === 'asc' ? 'aufsteigend' : 'absteigend'})`; + meta.textContent = `${sorted.length} von ${EFFECTIVE_TRADE_FAIRS.length} Messen | Sortierung: ${sortLabel} (${sortDirection === 'asc' ? 'aufsteigend' : 'absteigend'})`; } sortButtons.forEach((button) => { @@ -1248,8 +1500,12 @@ lastOpenedByTradeFair = loadLastOpenedState(); loadColumnOrder(); + loadColumnVisibility(); + applyColumnVisibilityToHeader(); setupColumnDragAndDrop(); + setupColumnSettingsModal(); loadSortState(); + ensureValidSortColumn(); updateSortButtonState(); render(); })();