Add trade fair column settings modal with visibility controls
This commit is contained in:
@@ -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">
|
||||
|
||||
138
web/style.css
138
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 {
|
||||
|
||||
@@ -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();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user