Add trade fair column settings modal with visibility controls

This commit is contained in:
2026-02-21 20:14:08 +01:00
parent 4b1e935000
commit 7abb98e924
3 changed files with 420 additions and 10 deletions

View File

@@ -1546,7 +1546,23 @@
<span>Messesuche</span>
<input type="search" id="tradeFairSearchInput" placeholder="Direkt nach Messe suchen (z.B. IFA, CMT, offerta)">
</label>
<div class="bookmark-subpage__toolbar-actions">
<div class="bookmark-subpage__meta" id="tradeFairMeta"></div>
<button type="button" class="bookmark-subpage__config-btn" id="tradeFairColumnSettingsBtn" title="Spalten konfigurieren" aria-label="Spalten konfigurieren">⚙️</button>
</div>
</div>
<div class="bookmark-columns-modal" id="tradeFairColumnsModal" hidden>
<div class="bookmark-columns-modal__backdrop" id="tradeFairColumnsModalBackdrop"></div>
<div class="bookmark-columns-modal__content" role="dialog" aria-modal="true" aria-labelledby="tradeFairColumnsModalTitle">
<button type="button" class="bookmark-columns-modal__close" id="tradeFairColumnsModalClose" aria-label="Schließen">×</button>
<h3 id="tradeFairColumnsModalTitle">Spalten konfigurieren</h3>
<p class="bookmark-columns-modal__hint">Reihenfolge mit Pfeilen anpassen und Spalten ein-/ausblenden.</p>
<div class="bookmark-columns-modal__list" id="tradeFairColumnsList"></div>
<div class="bookmark-columns-modal__actions">
<button type="button" class="btn btn-secondary" id="tradeFairColumnsResetBtn">Standard wiederherstellen</button>
<button type="button" class="btn btn-primary" id="tradeFairColumnsDoneBtn">Fertig</button>
</div>
</div>
</div>
<div class="bookmark-subpage__table-wrap">
<table class="bookmark-subpage__table">

View File

@@ -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 {

View File

@@ -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();
})();