407 lines
16 KiB
JavaScript
407 lines
16 KiB
JavaScript
(() => {
|
||
'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();
|
||
}
|
||
})();
|