349 lines
15 KiB
JavaScript
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();
|
|
})(); |