feat(ux): add media library workflow and preview navigation

This commit is contained in:
Masterluke77
2026-07-11 09:10:50 +02:00
parent e869ddfe1e
commit 9aff28f4ad
+406
View File
@@ -0,0 +1,406 @@
(() => {
'use strict';
const DENSITY_KEY = 'vendoo_media_density_v1';
const DENSITIES = new Set(['compact', 'comfortable', 'large']);
let activePath = '';
let refreshFrame = 0;
let gridObserver = null;
let countObserver = null;
function root() {
return document.getElementById('media-library');
}
function createElement(tag, className, text) {
const element = document.createElement(tag);
if (className) element.className = className;
if (text !== undefined) element.textContent = text;
return element;
}
function queueRefresh() {
if (refreshFrame) return;
refreshFrame = requestAnimationFrame(() => {
refreshFrame = 0;
refreshPresentation();
});
}
function savedDensity() {
const value = localStorage.getItem(DENSITY_KEY) || 'comfortable';
return DENSITIES.has(value) ? value : 'comfortable';
}
function applyDensity(value) {
const density = DENSITIES.has(value) ? value : 'comfortable';
const section = root();
if (!section) return;
section.dataset.mediaDensity = density;
localStorage.setItem(DENSITY_KEY, density);
section.querySelectorAll('[data-media-density-option]').forEach(button => {
const active = button.dataset.mediaDensityOption === density;
button.classList.toggle('is-active', active);
button.setAttribute('aria-pressed', String(active));
});
}
function createCommandCenter() {
const section = root();
const summary = section?.querySelector('.media-summary');
if (!section || !summary || section.querySelector('.ux-media-command')) return;
const command = createElement('section', 'ux-media-command');
command.setAttribute('aria-label', 'Galerieansicht');
const copy = createElement('div', 'ux-media-command-copy');
const kicker = createElement('span', '', 'Arbeitsansicht');
const title = createElement('strong', '', 'Bilder schnell finden, prüfen und weiterverwenden');
const result = createElement('small', '', 'Die Galerie wird geladen …');
result.id = 'ux-media-result-summary';
copy.append(kicker, title, result);
const density = createElement('div', 'ux-media-density');
density.setAttribute('role', 'group');
density.setAttribute('aria-label', 'Kartengröße');
const options = [
['compact', 'Klein', 'Kompakte Karten'],
['comfortable', 'Mittel', 'Normale Karten'],
['large', 'Groß', 'Große Karten'],
];
for (const [value, label, ariaLabel] of options) {
const button = createElement('button', '', label);
button.type = 'button';
button.dataset.mediaDensityOption = value;
button.setAttribute('aria-label', ariaLabel);
button.addEventListener('click', () => applyDensity(value));
density.append(button);
}
command.append(copy, density);
summary.before(command);
applyDensity(savedDensity());
}
function wrapSelect(select, labelText) {
if (!select || select.closest('.ux-media-filter-field')) return null;
const label = createElement('label', 'ux-media-filter-field');
label.append(createElement('span', '', labelText), select);
return label;
}
function reorganizeToolbar() {
const toolbar = root()?.querySelector('.media-toolbar');
if (!toolbar || toolbar.querySelector('.ux-media-filter-details')) return;
const source = document.getElementById('media-source');
const sort = document.getElementById('media-sort');
const favorites = document.getElementById('media-favorites-only')?.closest('.media-check');
const pageSize = document.getElementById('media-page-size');
const details = createElement('details', 'ux-media-filter-details');
const summary = createElement('summary', '', 'Filter & Anzeige');
const panel = createElement('div', 'ux-media-filter-panel');
const sourceField = wrapSelect(source, 'Quelle');
const sortField = wrapSelect(sort, 'Sortierung');
const pageField = wrapSelect(pageSize, 'Bilder pro Seite');
[sourceField, sortField, favorites, pageField].filter(Boolean).forEach(node => panel.append(node));
details.append(summary, panel);
const viewSwitch = toolbar.querySelector('.media-view-switch');
if (viewSwitch) toolbar.insertBefore(details, viewSwitch);
else toolbar.append(details);
document.addEventListener('click', event => {
if (!details.open || details.contains(event.target)) return;
details.open = false;
});
}
function createActiveFilters() {
const toolbar = root()?.querySelector('.media-toolbar');
if (!toolbar || document.getElementById('ux-media-active-filters')) return;
const filters = createElement('div', 'ux-media-active-filters is-empty');
filters.id = 'ux-media-active-filters';
filters.setAttribute('aria-live', 'polite');
toolbar.after(filters);
}
function controlEvent(control) {
return control?.tagName === 'INPUT' && control.type === 'search' ? 'input' : 'change';
}
function resetControl(control, value) {
if (!control) return;
if (control.type === 'checkbox') control.checked = Boolean(value);
else control.value = value;
control.dispatchEvent(new Event(controlEvent(control), { bubbles: true }));
queueRefresh();
}
function filterChip(label, control, resetValue) {
const chip = createElement('span', 'ux-media-filter-chip');
chip.append(createElement('span', '', label));
const clear = createElement('button', '', '×');
clear.type = 'button';
clear.setAttribute('aria-label', `${label} entfernen`);
clear.addEventListener('click', () => resetControl(control, resetValue));
chip.append(clear);
return chip;
}
function selectedText(select) {
if (!select?.value) return '';
return select.options[select.selectedIndex]?.textContent?.trim() || select.value;
}
function updateActiveFilters() {
const container = document.getElementById('ux-media-active-filters');
if (!container) return;
const search = document.getElementById('media-search');
const source = document.getElementById('media-source');
const favorites = document.getElementById('media-favorites-only');
const chips = [];
if (search?.value.trim()) chips.push(filterChip(`Suche: ${search.value.trim()}`, search, ''));
if (source?.value && source.value !== 'all') chips.push(filterChip(`Quelle: ${selectedText(source)}`, source, 'all'));
if (favorites?.checked) chips.push(filterChip('Nur Favoriten', favorites, false));
container.replaceChildren();
container.classList.toggle('is-empty', chips.length === 0);
if (!chips.length) return;
container.append(createElement('strong', '', 'Aktive Filter'), ...chips);
const reset = createElement('button', 'ux-media-filter-reset', 'Alle entfernen');
reset.type = 'button';
reset.addEventListener('click', () => {
resetControl(search, '');
resetControl(source, 'all');
resetControl(favorites, false);
});
container.append(reset);
}
function enhanceBatchBar() {
const bar = root()?.querySelector('.media-batchbar');
if (!bar || bar.querySelector('.ux-media-batch-primary')) return;
const actions = bar.querySelector(':scope > div');
if (!actions) return;
const primary = createElement('div', 'ux-media-batch-primary');
const more = createElement('details', 'ux-media-batch-more');
const summary = createElement('summary', '', 'Weitere Aktionen');
const secondary = createElement('div', 'ux-media-batch-secondary');
for (const id of ['media-send-to-factory', 'media-download-selected']) {
const button = document.getElementById(id);
if (button) primary.append(button);
}
const remove = document.getElementById('media-delete-selected');
if (remove) secondary.append(remove);
more.append(summary, secondary);
actions.replaceChildren(primary, more);
}
function selectedCount() {
const value = document.getElementById('media-selected-count')?.textContent || '0';
return Number.parseInt(value, 10) || 0;
}
function updateSelectionState() {
const section = root();
if (!section) return;
const count = selectedCount();
section.toggleAttribute('data-has-media-selection', count > 0);
const more = section.querySelector('.ux-media-batch-more');
if (!count && more) more.open = false;
}
function annotateCards() {
const cards = root()?.querySelectorAll('#media-grid .media-card') || [];
cards.forEach(card => {
const preview = card.querySelector('[data-media-preview]');
const title = card.querySelector('.media-card-content > strong')?.textContent?.trim() || 'Bild';
if (preview) {
preview.setAttribute('title', `${title} groß öffnen`);
preview.setAttribute('aria-label', `${title} in großer Vorschau öffnen`);
}
card.querySelectorAll('.media-action').forEach(action => {
const label = action.querySelector('.media-action-label')?.textContent?.trim();
if (label && !action.getAttribute('aria-label')) action.setAttribute('aria-label', label);
});
});
}
function updateResultSummary() {
const target = document.getElementById('ux-media-result-summary');
if (!target) return;
const summary = root()?.querySelector('.media-pagination-summary')?.textContent?.trim();
const view = document.getElementById('media-view-list')?.classList.contains('active') ? 'Listenansicht' : 'Rasteransicht';
target.textContent = `${summary || '0 Bilder'} · ${view}`;
}
function updatePaginationState() {
const section = root();
if (!section) return;
const pageInfo = section.querySelector('[data-media-page-info]')?.textContent?.trim() || '';
const onePage = /von\s+1\b/i.test(pageInfo);
section.querySelector('.media-pagination-top')?.classList.toggle('is-redundant', onePage);
}
function previewButtons() {
return [...(root()?.querySelectorAll('[data-media-preview]') || [])];
}
function currentPreviewIndex() {
const buttons = previewButtons();
return buttons.findIndex(button => button.dataset.mediaPreview === activePath);
}
function navigatePreview(direction) {
const buttons = previewButtons();
if (!buttons.length) return;
const current = currentPreviewIndex();
const next = current < 0 ? 0 : (current + direction + buttons.length) % buttons.length;
activePath = buttons[next].dataset.mediaPreview || '';
buttons[next].click();
queueRefresh();
}
function updateLightboxNavigation() {
const modal = document.getElementById('media-lightbox');
const buttons = previewButtons();
const index = currentPreviewIndex();
const position = document.getElementById('ux-media-lightbox-position');
if (position) position.textContent = index >= 0 ? `${index + 1} von ${buttons.length}` : `${buttons.length} Bilder`;
modal?.querySelectorAll('[data-ux-media-preview-nav]').forEach(button => {
button.disabled = buttons.length < 2;
});
}
function activeCard() {
if (!activePath) return null;
return previewButtons().find(button => button.dataset.mediaPreview === activePath)?.closest('.media-card') || null;
}
function enhanceLightbox() {
const modal = document.getElementById('media-lightbox');
const dialog = modal?.querySelector('.media-lightbox-dialog');
const header = dialog?.querySelector(':scope > header');
const footer = dialog?.querySelector(':scope > footer');
const body = dialog?.querySelector('.media-lightbox-body');
const meta = document.getElementById('media-lightbox-meta');
if (!modal || !dialog || !header || !footer || !body) return;
if (!header.querySelector('.ux-media-lightbox-nav')) {
const nav = createElement('div', 'ux-media-lightbox-nav');
const previous = createElement('button', 'icon-button', '');
previous.type = 'button';
previous.dataset.uxMediaPreviewNav = 'previous';
previous.setAttribute('aria-label', 'Vorheriges Bild');
previous.addEventListener('click', () => navigatePreview(-1));
const position = createElement('span', 'ux-media-lightbox-position', '0 Bilder');
position.id = 'ux-media-lightbox-position';
const next = createElement('button', 'icon-button', '');
next.type = 'button';
next.dataset.uxMediaPreviewNav = 'next';
next.setAttribute('aria-label', 'Nächstes Bild');
next.addEventListener('click', () => navigatePreview(1));
nav.append(previous, position, next);
header.insertBefore(nav, header.querySelector('.media-lightbox-close'));
}
if (meta && !meta.closest('.media-lightbox-meta-details')) {
const details = createElement('details', 'media-lightbox-meta-details');
details.open = true;
details.append(createElement('summary', '', 'Bildinformationen anzeigen'), meta);
body.append(details);
}
if (!document.getElementById('ux-media-lightbox-use')) {
const use = createElement('button', 'vd-button primary ux-media-use', 'Im Artikel verwenden');
use.type = 'button';
use.id = 'ux-media-lightbox-use';
use.addEventListener('click', () => activeCard()?.querySelector('[data-media-use]')?.click());
footer.prepend(use);
}
}
function bindControls() {
const section = root();
if (!section || section.dataset.uxMediaBound === 'true') return;
section.dataset.uxMediaBound = 'true';
for (const id of ['media-search', 'media-source', 'media-sort', 'media-favorites-only', 'media-page-size']) {
const control = document.getElementById(id);
control?.addEventListener(controlEvent(control), queueRefresh);
}
document.getElementById('media-view-grid')?.addEventListener('click', queueRefresh);
document.getElementById('media-view-list')?.addEventListener('click', queueRefresh);
section.addEventListener('click', event => {
const preview = event.target.closest?.('[data-media-preview]');
if (preview) {
activePath = preview.dataset.mediaPreview || '';
queueRefresh();
}
if (event.target.closest?.('[data-media-source], [data-media-select], [data-media-favorite]')) queueRefresh();
}, true);
document.addEventListener('keydown', event => {
const modal = document.getElementById('media-lightbox');
if (!modal || modal.classList.contains('hidden')) return;
if (event.target.closest('input, textarea, select')) return;
if (event.key === 'ArrowLeft') {
event.preventDefault();
navigatePreview(-1);
}
if (event.key === 'ArrowRight') {
event.preventDefault();
navigatePreview(1);
}
});
}
function observeDynamicContent() {
const grid = document.getElementById('media-grid');
const count = document.getElementById('media-selected-count');
if (grid && !gridObserver) {
gridObserver = new MutationObserver(queueRefresh);
gridObserver.observe(grid, { childList: true, subtree: true });
}
if (count && !countObserver) {
countObserver = new MutationObserver(queueRefresh);
countObserver.observe(count, { childList: true, characterData: true, subtree: true });
}
window.addEventListener('beforeunload', () => {
gridObserver?.disconnect();
countObserver?.disconnect();
if (refreshFrame) cancelAnimationFrame(refreshFrame);
}, { once: true });
}
function refreshPresentation() {
updateActiveFilters();
updateSelectionState();
annotateCards();
updateResultSummary();
updatePaginationState();
updateLightboxNavigation();
}
function initializeMediaUx() {
if (!root() || root().dataset.uxMediaReady === 'true') return;
root().dataset.uxMediaReady = 'true';
createCommandCenter();
reorganizeToolbar();
createActiveFilters();
enhanceBatchBar();
enhanceLightbox();
bindControls();
observeDynamicContent();
refreshPresentation();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeMediaUx, { once: true });
} else {
initializeMediaUx();
}
})();