Files
vendoo/public/template-workspace.js
T

349 lines
15 KiB
JavaScript

(() => {
'use strict';
const ROOT_ID = 'templates';
const FILTER_KEY = 'vendoo_template_workspace_filters';
const createNode = (tag, className = '', text = '') => {
const node = document.createElement(tag);
if (className) node.className = className;
if (text) node.textContent = text;
return node;
};
const normalize = value => String(value || '').trim().toLocaleLowerCase('de-DE');
function readStoredFilters() {
try {
const parsed = JSON.parse(localStorage.getItem(FILTER_KEY) || 'null');
return parsed && typeof parsed === 'object' ? parsed : {};
} catch {
return {};
}
}
function persistFilters(root) {
try {
localStorage.setItem(FILTER_KEY, JSON.stringify({
platform: root.querySelector('[data-ux-template-platform]')?.value || 'all',
language: root.querySelector('[data-ux-template-language]')?.value || 'all',
}));
} catch {}
}
function createHero(root) {
if (root.querySelector('.ux-template-hero')) return;
const header = root.querySelector('.templates-header');
const addButton = root.querySelector('#add-template-btn');
if (!header || !addButton) return;
const hero = createNode('section', 'ux-template-hero');
const copy = createNode('div', 'ux-template-hero-copy');
copy.append(
createNode('span', 'ux-template-kicker', 'Wiederverwendbare Listing-Basis'),
createNode('h2', '', 'Vorlagen schneller finden und sauber pflegen'),
createNode('p', '', 'Plattform, Sprache, Verkäuferhinweise und Standard-Tags zentral vorbereiten und bei neuen Artikeln wiederverwenden.')
);
const metrics = createNode('div', 'ux-template-metrics');
const total = createNode('article', 'ux-template-metric');
const totalValue = createNode('strong', '', '0');
totalValue.dataset.uxTemplateMetric = 'total';
total.append(totalValue, createNode('span', '', 'Vorlagen'));
const universal = createNode('article', 'ux-template-metric');
const universalValue = createNode('strong', '', '0');
universalValue.dataset.uxTemplateMetric = 'universal';
universal.append(universalValue, createNode('span', '', 'Für alle Plattformen'));
metrics.append(total, universal);
addButton.textContent = 'Neue Vorlage erstellen';
addButton.classList.add('ux-template-primary-action');
hero.append(copy, metrics, addButton);
header.replaceWith(hero);
}
function createToolbar(root) {
if (root.querySelector('.ux-template-toolbar')) return;
const list = root.querySelector('#template-list');
if (!list) return;
const stored = readStoredFilters();
const toolbar = createNode('section', 'ux-template-toolbar card');
const searchLabel = createNode('label', 'ux-template-search');
searchLabel.append(createNode('span', '', 'Vorlagen suchen'));
const search = createNode('input');
search.type = 'search';
search.placeholder = 'Name oder Plattform …';
search.dataset.uxTemplateSearch = 'true';
searchLabel.append(search);
const controls = createNode('div', 'ux-template-filter-controls');
const platformLabel = createNode('label');
platformLabel.append(createNode('span', '', 'Plattform'));
const platform = createNode('select');
platform.dataset.uxTemplatePlatform = 'true';
platform.dataset.uxTemplateInitial = stored.platform || 'all';
platform.append(new Option('Alle Plattformen', 'all'));
platformLabel.append(platform);
const languageLabel = createNode('label');
languageLabel.append(createNode('span', '', 'Sprache'));
const language = createNode('select');
language.dataset.uxTemplateLanguage = 'true';
language.append(
new Option('Alle Sprachen', 'all'),
new Option('Deutsch', 'DE'),
new Option('English', 'EN'),
new Option('Français', 'FR')
);
language.value = ['DE', 'EN', 'FR'].includes(stored.language) ? stored.language : 'all';
languageLabel.append(language);
const result = createNode('span', 'ux-template-result-count', '0 Vorlagen');
result.setAttribute('aria-live', 'polite');
controls.append(platformLabel, languageLabel, result);
toolbar.append(searchLabel, controls);
list.before(toolbar);
search.addEventListener('input', () => applyFilters(root));
platform.addEventListener('change', () => {
persistFilters(root);
applyFilters(root);
});
language.addEventListener('change', () => {
persistFilters(root);
applyFilters(root);
});
}
function parseTemplateMeta(item) {
const metaText = item.querySelector('.tpl-meta')?.textContent?.trim() || '';
const parts = metaText.split('·').map(part => part.trim()).filter(Boolean);
const platform = parts[0] || 'Alle Plattformen';
const language = (parts[1] || 'DE').toUpperCase();
return {
name: item.querySelector('.tpl-name')?.textContent?.trim() || 'Unbenannte Vorlage',
platform,
language,
universal: normalize(platform).includes('alle plattform'),
};
}
function syncPlatformOptions(root) {
const select = root.querySelector('[data-ux-template-platform]');
if (!select) return;
const initial = select.dataset.uxTemplateInitial;
const current = initial || select.value || readStoredFilters().platform || 'all';
const platforms = [...new Set([...root.querySelectorAll('#template-list .template-item')]
.map(item => parseTemplateMeta(item).platform)
.filter(Boolean))]
.sort((a, b) => a.localeCompare(b, 'de'));
const expected = ['all', ...platforms];
const present = [...select.options].map(option => option.value);
if (expected.join('\u0000') !== present.join('\u0000')) {
select.replaceChildren(new Option('Alle Plattformen', 'all'));
platforms.forEach(value => select.append(new Option(value, value)));
}
select.value = expected.includes(current) ? current : 'all';
delete select.dataset.uxTemplateInitial;
}
function enhanceTemplateCards(root) {
root.querySelectorAll('#template-list .template-item').forEach(item => {
const meta = parseTemplateMeta(item);
item.dataset.uxTemplateName = normalize(meta.name);
item.dataset.uxTemplatePlatform = meta.platform;
item.dataset.uxTemplateLanguage = meta.language;
item.dataset.uxTemplateUniversal = meta.universal ? 'true' : 'false';
if (item.dataset.uxTemplateEnhanced === 'true') return;
item.dataset.uxTemplateEnhanced = 'true';
item.classList.add('ux-template-card');
const info = item.querySelector('.tpl-info');
const name = item.querySelector('.tpl-name');
const metaNode = item.querySelector('.tpl-meta');
const actions = item.querySelector('.tpl-actions');
if (!info || !name || !metaNode || !actions) return;
const cardHead = createNode('div', 'ux-template-card-head');
const identity = createNode('div', 'ux-template-card-identity');
const mark = createNode('span', 'ux-template-card-mark', meta.name.slice(0, 2).toUpperCase());
identity.append(mark, info);
const badges = createNode('div', 'ux-template-badges');
badges.append(
createNode('span', meta.universal ? 'ux-template-badge is-universal' : 'ux-template-badge', meta.platform),
createNode('span', 'ux-template-badge is-language', meta.language)
);
cardHead.append(identity, badges);
const edit = actions.querySelector('.tpl-edit');
const remove = actions.querySelector('.tpl-delete');
const footer = createNode('div', 'ux-template-card-footer');
if (edit) {
edit.classList.add('vd-button', 'compact', 'ux-template-edit');
edit.textContent = 'Vorlage bearbeiten';
footer.append(edit);
}
if (remove) {
const more = createNode('details', 'ux-template-more-actions');
const summary = createNode('summary', '', 'Weitere Aktionen');
const body = createNode('div', 'ux-template-more-actions-body');
remove.classList.add('danger');
body.append(remove);
more.append(summary, body);
footer.append(more);
}
actions.remove();
item.replaceChildren(cardHead, footer);
});
}
function updateMetrics(root) {
const cards = [...root.querySelectorAll('#template-list .template-item')];
const total = root.querySelector('[data-ux-template-metric="total"]');
const universal = root.querySelector('[data-ux-template-metric="universal"]');
if (total) total.textContent = String(cards.length);
if (universal) universal.textContent = String(cards.filter(card => card.dataset.uxTemplateUniversal === 'true').length);
}
function applyFilters(root) {
syncPlatformOptions(root);
const search = normalize(root.querySelector('[data-ux-template-search]')?.value);
const platform = root.querySelector('[data-ux-template-platform]')?.value || 'all';
const language = root.querySelector('[data-ux-template-language]')?.value || 'all';
let visible = 0;
root.querySelectorAll('#template-list .template-item').forEach(item => {
const haystack = `${item.dataset.uxTemplateName || ''} ${normalize(item.dataset.uxTemplatePlatform)}`;
const matchesSearch = !search || haystack.includes(search);
const matchesPlatform = platform === 'all' || item.dataset.uxTemplatePlatform === platform;
const matchesLanguage = language === 'all' || item.dataset.uxTemplateLanguage === language;
const show = matchesSearch && matchesPlatform && matchesLanguage;
item.classList.toggle('ux-template-filtered-out', !show);
if (show) visible += 1;
});
const count = root.querySelector('.ux-template-result-count');
if (count) count.textContent = `${visible} ${visible === 1 ? 'Vorlage' : 'Vorlagen'}`;
root.classList.toggle('ux-template-no-results', visible === 0 && root.querySelectorAll('#template-list .template-item').length > 0);
updateMetrics(root);
}
function updateEditorStatus(root) {
const status = root.querySelector('.ux-template-editor-status');
if (!status) return;
const name = root.querySelector('#tpl-name')?.value.trim();
const platform = root.querySelector('#tpl-platform')?.selectedOptions?.[0]?.textContent || 'Alle Plattformen';
const language = root.querySelector('#tpl-language')?.selectedOptions?.[0]?.textContent || 'Deutsch';
status.textContent = name ? `${name} · ${platform} · ${language}` : 'Name fehlt noch';
status.classList.toggle('is-incomplete', !name);
}
function structureEditor(root) {
const form = root.querySelector('#template-form');
if (!form || form.dataset.uxTemplateStructured === 'true') return;
const title = form.querySelector('#template-form-title');
const groups = [...form.querySelectorAll(':scope > .setting-group')];
const actions = form.querySelector(':scope > .result-actions');
if (!title || groups.length < 5 || !actions) return;
const head = createNode('header', 'ux-template-editor-head');
const copy = createNode('div');
copy.append(createNode('span', 'ux-template-kicker', 'Vorlagen-Editor'), title, createNode('p', '', 'Lege die wiederverwendbare Grundlage fest. Änderungen werden erst über „Vorlage speichern“ übernommen.'));
const status = createNode('span', 'ux-template-editor-status', 'Entwurf');
status.setAttribute('aria-live', 'polite');
head.append(copy, status);
const layout = createNode('div', 'ux-template-editor-layout');
const basics = createNode('section', 'ux-template-editor-section card');
basics.append(createNode('div', 'ux-template-section-heading', 'Grundlage'));
groups.slice(0, 3).forEach(group => basics.append(group));
const content = createNode('section', 'ux-template-editor-section card ux-template-content-section');
content.append(createNode('div', 'ux-template-section-heading', 'Inhalte & Standardwerte'));
groups.slice(3).forEach(group => content.append(group));
layout.append(basics, content);
const save = actions.querySelector('#save-template-btn');
const cancel = actions.querySelector('#cancel-template-btn');
if (save) save.textContent = 'Vorlage speichern';
actions.classList.add('ux-template-editor-actions');
if (cancel) actions.prepend(cancel);
form.replaceChildren(head, layout, actions);
form.dataset.uxTemplateStructured = 'true';
['tpl-name', 'tpl-platform', 'tpl-language', 'tpl-tags'].forEach(id => {
root.querySelector(`#${id}`)?.addEventListener('input', () => updateEditorStatus(root));
root.querySelector(`#${id}`)?.addEventListener('change', () => updateEditorStatus(root));
});
updateEditorStatus(root);
}
function syncMode(root) {
const form = root.querySelector('#template-form');
const open = Boolean(form && !form.classList.contains('hidden'));
root.classList.toggle('ux-template-editor-open', open);
root.classList.toggle('ux-template-list-open', !open);
if (open) {
updateEditorStatus(root);
requestAnimationFrame(() => form.scrollIntoView({ behavior: 'smooth', block: 'start' }));
}
}
function enhance(root) {
createHero(root);
createToolbar(root);
structureEditor(root);
enhanceTemplateCards(root);
applyFilters(root);
syncMode(root);
root.classList.add('ux-template-workspace-ready');
}
function setupKeyboardNavigation(root) {
const toolbar = root.querySelector('.ux-template-toolbar');
toolbar?.addEventListener('keydown', event => {
if (!['ArrowLeft', 'ArrowRight'].includes(event.key)) return;
const controls = [...toolbar.querySelectorAll('input, select')].filter(control => !control.disabled);
const index = controls.indexOf(document.activeElement);
if (index < 0) return;
event.preventDefault();
const direction = event.key === 'ArrowRight' ? 1 : -1;
controls[(index + direction + controls.length) % controls.length]?.focus();
});
}
function init() {
const root = document.getElementById(ROOT_ID);
if (!root || root.dataset.uxTemplateWorkspace === 'true') return;
root.dataset.uxTemplateWorkspace = 'true';
enhance(root);
setupKeyboardNavigation(root);
let scheduled = false;
const schedule = () => {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
enhance(root);
});
};
const observer = new MutationObserver(mutations => {
if (mutations.some(mutation => mutation.type === 'childList' || (mutation.type === 'attributes' && mutation.attributeName === 'class'))) schedule();
});
const list = root.querySelector('#template-list');
const form = root.querySelector('#template-form');
if (list) observer.observe(list, { childList: true });
if (form) observer.observe(form, { attributes: true, attributeFilter: ['class'] });
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init, { once: true });
else init();
})();