Files
vendoo/public/media-library-ux.js

407 lines
16 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
'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();
}
})();