553 lines
21 KiB
JavaScript
553 lines
21 KiB
JavaScript
(() => {
|
||
'use strict';
|
||
|
||
const PROFILE_KEY = 'ai-disclosure.bulk.profiles.v1';
|
||
const TEMPLATE_KEY = 'ai-disclosure.bulk.templates.v1';
|
||
const EXPORT_FORMAT = 'ai-disclosure-bulk-browser-data';
|
||
const $ = id => document.getElementById(id);
|
||
|
||
const profileFields = [
|
||
['profile-lang', 'lang'],
|
||
['profile-role', 'legalRole'],
|
||
['profile-use-context', 'useContext'],
|
||
['profile-assurance', 'assurance'],
|
||
['profile-author', 'author'],
|
||
['profile-author-url', 'authorUrl'],
|
||
['profile-responsible-role', 'responsibleRole'],
|
||
['profile-responsible', 'responsible'],
|
||
['profile-responsible-url', 'responsibleUrl'],
|
||
['profile-complaint-name', 'complaintName'],
|
||
['profile-complaint-email', 'complaintEmail'],
|
||
['profile-complaint-url', 'complaintUrl']
|
||
];
|
||
|
||
let cfg = null;
|
||
let activeTemplate = null;
|
||
let currentResults = [];
|
||
let exportsCache = {};
|
||
|
||
function readStore(key) {
|
||
try {
|
||
const parsed = JSON.parse(localStorage.getItem(key) || '{}');
|
||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
||
} catch (_) {
|
||
return {};
|
||
}
|
||
}
|
||
|
||
function writeStore(key, value) {
|
||
localStorage.setItem(key, JSON.stringify(value));
|
||
}
|
||
|
||
function setStatus(text, level = '') {
|
||
const target = $('template-status');
|
||
target.textContent = text;
|
||
target.className = `status ${level}`.trim();
|
||
}
|
||
|
||
function addMessage(text, level = '') {
|
||
const p = document.createElement('p');
|
||
p.className = `message ${level}`.trim();
|
||
p.textContent = text;
|
||
$('url-messages').appendChild(p);
|
||
}
|
||
|
||
function safeHTTPURL(raw) {
|
||
try {
|
||
const url = new URL(raw);
|
||
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) return null;
|
||
return url;
|
||
} catch (_) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function parseTemplate(raw) {
|
||
const value = raw.trim();
|
||
if (!value) throw new Error('Bitte zuerst eine Declaration-URL oder einen Query-String einfügen.');
|
||
let params;
|
||
const absolute = safeHTTPURL(value);
|
||
if (absolute) {
|
||
params = new URLSearchParams(absolute.search);
|
||
} else {
|
||
const query = value.startsWith('?') ? value.slice(1) : value;
|
||
params = new URLSearchParams(query);
|
||
}
|
||
params.delete('subject');
|
||
params.delete('link');
|
||
if ([...params.keys()].length === 0) throw new Error('Die Vorlage enthält keine Parameter.');
|
||
return params;
|
||
}
|
||
|
||
function activateTemplate(params, label = 'Vorlage geladen') {
|
||
activeTemplate = new URLSearchParams(params);
|
||
activeTemplate.delete('subject');
|
||
activeTemplate.delete('link');
|
||
$('template-input').value = activeTemplate.toString();
|
||
renderTemplateParams();
|
||
setStatus(`${label}: ${[...activeTemplate.keys()].length} Parameter aktiv.`, 'good');
|
||
}
|
||
|
||
function renderTemplateParams() {
|
||
const list = $('template-params');
|
||
list.replaceChildren();
|
||
if (!activeTemplate) return;
|
||
const entries = [...activeTemplate.entries()].sort(([a], [b]) => a.localeCompare(b));
|
||
for (const [key, value] of entries) {
|
||
const row = document.createElement('div');
|
||
const dt = document.createElement('dt');
|
||
const dd = document.createElement('dd');
|
||
dt.textContent = key;
|
||
dd.textContent = value;
|
||
row.append(dt, dd);
|
||
list.appendChild(row);
|
||
}
|
||
}
|
||
|
||
function refreshTemplateSelect(selected = '') {
|
||
const templates = readStore(TEMPLATE_KEY);
|
||
const select = $('saved-template-select');
|
||
select.replaceChildren(new Option('Gespeicherte Vorlage wählen …', ''));
|
||
Object.keys(templates).sort((a, b) => a.localeCompare(b)).forEach(name => select.add(new Option(name, name)));
|
||
if (selected && templates[selected]) select.value = selected;
|
||
}
|
||
|
||
function saveTemplate() {
|
||
if (!activeTemplate) {
|
||
setStatus('Bitte zuerst eine gültige Vorlage laden.', 'error');
|
||
return;
|
||
}
|
||
const name = $('template-name').value.trim();
|
||
if (!name) {
|
||
setStatus('Bitte einen Namen für die Vorlage angeben.', 'error');
|
||
$('template-name').focus();
|
||
return;
|
||
}
|
||
const templates = readStore(TEMPLATE_KEY);
|
||
templates[name] = {query: activeTemplate.toString(), updatedAt: new Date().toISOString()};
|
||
writeStore(TEMPLATE_KEY, templates);
|
||
refreshTemplateSelect(name);
|
||
setStatus(`Vorlage „${name}“ wurde nur in diesem Browser gespeichert.`, 'good');
|
||
}
|
||
|
||
function loadSavedTemplate(name) {
|
||
if (!name) return;
|
||
const item = readStore(TEMPLATE_KEY)[name];
|
||
if (!item || typeof item.query !== 'string') return;
|
||
$('template-name').value = name;
|
||
activateTemplate(new URLSearchParams(item.query), `Vorlage „${name}“ geladen`);
|
||
}
|
||
|
||
function deleteTemplate() {
|
||
const name = $('saved-template-select').value;
|
||
if (!name) return;
|
||
const templates = readStore(TEMPLATE_KEY);
|
||
delete templates[name];
|
||
writeStore(TEMPLATE_KEY, templates);
|
||
refreshTemplateSelect();
|
||
if ($('template-name').value === name) $('template-name').value = '';
|
||
setStatus(`Vorlage „${name}“ wurde aus diesem Browser gelöscht.`);
|
||
}
|
||
|
||
function emptyProfileForm() {
|
||
$('profile-name').value = '';
|
||
for (const [id] of profileFields) $(id).value = '';
|
||
$('profile-select').value = '';
|
||
}
|
||
|
||
function profileFromForm() {
|
||
const profile = {};
|
||
for (const [id, key] of profileFields) {
|
||
const value = $(id).value.trim();
|
||
if (value) profile[key] = value;
|
||
}
|
||
return profile;
|
||
}
|
||
|
||
function validateProfile(profile) {
|
||
const urlKeys = ['authorUrl', 'responsibleUrl', 'complaintUrl'];
|
||
for (const key of urlKeys) {
|
||
if (profile[key] && !safeHTTPURL(profile[key])) return `${key} muss eine absolute http(s)-URL ohne Zugangsdaten sein.`;
|
||
}
|
||
if (profile.complaintEmail && !$('profile-complaint-email').checkValidity()) return 'Die E-Mail-Adresse der Rückmeldestelle ist nicht gültig.';
|
||
return '';
|
||
}
|
||
|
||
function validateMergedParams(params) {
|
||
if (params.get('authorUrl') && !params.get('author')) return 'Zu einer Autoren-URL muss auch ein Autor bzw. eine Autorin angegeben werden.';
|
||
const responsibilityStarted = params.get('responsibleRole') || params.get('responsible') || params.get('responsibleUrl');
|
||
if (responsibilityStarted && (!params.get('responsibleRole') || !params.get('responsible'))) return 'Bei redaktioneller Verantwortung sind Rolle und Name/Organisation gemeinsam erforderlich.';
|
||
const complaintStarted = params.get('complaintName') || params.get('complaintEmail') || params.get('complaintUrl');
|
||
if (complaintStarted && (!params.get('complaintName') || (!params.get('complaintEmail') && !params.get('complaintUrl')))) return 'Die Rückmeldestelle benötigt einen Namen und mindestens E-Mail oder Kontakt-URL.';
|
||
return '';
|
||
}
|
||
|
||
function refreshProfileSelect(selected = '') {
|
||
const profiles = readStore(PROFILE_KEY);
|
||
const select = $('profile-select');
|
||
select.replaceChildren(new Option('Website-Profil wählen …', ''));
|
||
Object.keys(profiles).sort((a, b) => a.localeCompare(b)).forEach(name => select.add(new Option(name, name)));
|
||
if (selected && profiles[selected]) select.value = selected;
|
||
}
|
||
|
||
function saveProfile() {
|
||
const name = $('profile-name').value.trim();
|
||
if (!name) {
|
||
alert('Bitte einen Profilnamen angeben.');
|
||
$('profile-name').focus();
|
||
return;
|
||
}
|
||
const profile = profileFromForm();
|
||
const error = validateProfile(profile);
|
||
if (error) {
|
||
alert(error);
|
||
return;
|
||
}
|
||
const profiles = readStore(PROFILE_KEY);
|
||
profiles[name] = {values: profile, updatedAt: new Date().toISOString()};
|
||
writeStore(PROFILE_KEY, profiles);
|
||
refreshProfileSelect(name);
|
||
}
|
||
|
||
function loadProfile(name) {
|
||
if (!name) {
|
||
emptyProfileForm();
|
||
return;
|
||
}
|
||
const item = readStore(PROFILE_KEY)[name];
|
||
if (!item || !item.values) return;
|
||
$('profile-name').value = name;
|
||
for (const [id, key] of profileFields) $(id).value = item.values[key] || '';
|
||
}
|
||
|
||
function deleteProfile() {
|
||
const name = $('profile-select').value;
|
||
if (!name) return;
|
||
const profiles = readStore(PROFILE_KEY);
|
||
delete profiles[name];
|
||
writeStore(PROFILE_KEY, profiles);
|
||
refreshProfileSelect();
|
||
emptyProfileForm();
|
||
}
|
||
|
||
function activeQuery() {
|
||
if (!activeTemplate) throw new Error('Bitte zuerst eine Kennzeichnungsvorlage laden.');
|
||
const params = new URLSearchParams(activeTemplate);
|
||
params.delete('subject');
|
||
params.delete('link');
|
||
const profile = profileFromForm();
|
||
const error = validateProfile(profile);
|
||
if (error) throw new Error(error);
|
||
for (const [key, value] of Object.entries(profile)) params.set(key, value);
|
||
const mergedError = validateMergedParams(params);
|
||
if (mergedError) throw new Error(mergedError);
|
||
return params;
|
||
}
|
||
|
||
function csvCellLine(line) {
|
||
let value = line.trim();
|
||
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
|
||
value = value.slice(1, -1).replaceAll('""', '"');
|
||
}
|
||
return value;
|
||
}
|
||
|
||
function parseSubjects() {
|
||
$('url-messages').replaceChildren();
|
||
let lines = $('url-input').value.split(/\r?\n/).map(csvCellLine).filter(Boolean);
|
||
if (lines[0]?.toLowerCase() === 'url') lines = lines.slice(1);
|
||
const baseRaw = $('base-url').value.trim();
|
||
let base = null;
|
||
if (baseRaw) {
|
||
base = safeHTTPURL(baseRaw);
|
||
if (!base) addMessage('Die Basis-URL ist ungültig und wird nicht verwendet.', 'error');
|
||
}
|
||
|
||
const unique = new Map();
|
||
const invalid = [];
|
||
let duplicates = 0;
|
||
for (const raw of lines) {
|
||
let parsed = safeHTTPURL(raw);
|
||
if (!parsed && base) {
|
||
try {
|
||
const candidate = new URL(raw, base);
|
||
if (['http:', 'https:'].includes(candidate.protocol) && !candidate.username && !candidate.password) parsed = candidate;
|
||
} catch (_) {}
|
||
}
|
||
if (!parsed) {
|
||
invalid.push(raw);
|
||
continue;
|
||
}
|
||
const normalized = parsed.href;
|
||
if (unique.has(normalized)) {
|
||
duplicates++;
|
||
continue;
|
||
}
|
||
unique.set(normalized, normalized);
|
||
}
|
||
const subjects = [...unique.values()];
|
||
$('url-counter').querySelector('strong').textContent = String(subjects.length);
|
||
if (duplicates) addMessage(`${duplicates} Duplikat${duplicates === 1 ? '' : 'e'} entfernt.`, 'info');
|
||
if (invalid.length) addMessage(`${invalid.length} ungültige Zeile${invalid.length === 1 ? '' : 'n'} ignoriert: ${invalid.slice(0, 3).join(', ')}${invalid.length > 3 ? ' …' : ''}`, 'error');
|
||
if (cfg && subjects.length > cfg.maxURLs) addMessage(`Maximal ${cfg.maxURLs} URLs pro Lauf. Bitte die Liste aufteilen.`, 'error');
|
||
return subjects;
|
||
}
|
||
|
||
function csvEscape(value) {
|
||
const text = String(value ?? '');
|
||
return /[",\n\r]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
|
||
}
|
||
|
||
function buildExports(results) {
|
||
const successful = results.filter(item => item.ok && item.data);
|
||
const html = successful.map(item => `<!-- ${item.subject} -->\n${item.data.embedHtml || item.data.html}`).join('\n\n');
|
||
const markdown = successful.map(item => `### ${item.subject}\n\n${item.data.markdown}`).join('\n\n');
|
||
const jsonArray = successful.map(item => item.data.jsonLd);
|
||
const json = JSON.stringify(jsonArray, null, 2);
|
||
const jsonl = successful.map(item => JSON.stringify(item.data.jsonLd)).join('\n');
|
||
const csvRows = [['subject', 'declaration', 'manifest', 'badge']];
|
||
successful.forEach(item => csvRows.push([item.subject, item.data.declarationUrl, item.data.manifestUrl, item.data.badgeUrl]));
|
||
const csv = csvRows.map(row => row.map(csvEscape).join(',')).join('\n');
|
||
return {html, markdown, json, jsonl, csv};
|
||
}
|
||
|
||
function buildDisclosurePreview(data) {
|
||
const wrap = document.createElement('p');
|
||
wrap.className = 'bulk-disclosure-preview';
|
||
const link = document.createElement('a');
|
||
link.className = 'bulk-disclosure-preview__link';
|
||
link.href = data.declarationUrl;
|
||
link.target = '_blank';
|
||
link.rel = 'noopener';
|
||
|
||
const badge = document.createElement('img');
|
||
badge.className = 'bulk-disclosure-preview__badge';
|
||
badge.src = data.badgeUrl;
|
||
badge.alt = '';
|
||
badge.setAttribute('aria-hidden', 'true');
|
||
|
||
const text = document.createElement('span');
|
||
text.className = 'bulk-disclosure-preview__text';
|
||
text.textContent = data.accessibleText || 'KI-Nutzung – Details zur Kennzeichnung';
|
||
|
||
link.append(badge, text);
|
||
wrap.appendChild(link);
|
||
return wrap;
|
||
}
|
||
|
||
function renderResults(results) {
|
||
currentResults = results;
|
||
exportsCache = buildExports(results);
|
||
const successful = results.filter(item => item.ok).length;
|
||
const failed = results.length - successful;
|
||
$('results-section').hidden = false;
|
||
$('result-summary').textContent = `${successful} Kennzeichnung${successful === 1 ? '' : 'en'} erzeugt${failed ? `, ${failed} Fehler` : ''}.`;
|
||
const tbody = $('result-rows');
|
||
tbody.replaceChildren();
|
||
results.forEach(item => {
|
||
const tr = document.createElement('tr');
|
||
const subjectCell = document.createElement('td');
|
||
const code = document.createElement('code');
|
||
code.textContent = item.subject;
|
||
subjectCell.appendChild(code);
|
||
const previewCell = document.createElement('td');
|
||
const statusCell = document.createElement('td');
|
||
statusCell.className = item.ok ? 'ok' : 'failed';
|
||
statusCell.textContent = item.ok ? '✓ erzeugt' : `Fehler: ${item.error || 'unbekannt'}`;
|
||
const linksCell = document.createElement('td');
|
||
if (item.ok && item.data) {
|
||
previewCell.appendChild(buildDisclosurePreview(item.data));
|
||
const links = document.createElement('div');
|
||
links.className = 'result-links';
|
||
[['Declaration', item.data.declarationUrl], ['JSON-LD', item.data.manifestUrl], ['SVG (nur Grafik)', item.data.badgeUrl]].forEach(([label, href]) => {
|
||
const a = document.createElement('a');
|
||
a.textContent = label;
|
||
a.href = href;
|
||
a.target = '_blank';
|
||
a.rel = 'noopener';
|
||
links.appendChild(a);
|
||
});
|
||
linksCell.appendChild(links);
|
||
}
|
||
tr.append(subjectCell, previewCell, statusCell, linksCell);
|
||
tbody.appendChild(tr);
|
||
});
|
||
$('output-html').value = exportsCache.html;
|
||
$('output-markdown').value = exportsCache.markdown;
|
||
$('output-json').value = exportsCache.json;
|
||
$('output-csv').value = exportsCache.csv;
|
||
$('results-section').scrollIntoView({behavior: 'smooth', block: 'start'});
|
||
}
|
||
|
||
async function generate() {
|
||
let query;
|
||
try {
|
||
query = activeQuery();
|
||
} catch (error) {
|
||
setStatus(error.message, 'error');
|
||
return;
|
||
}
|
||
const subjects = parseSubjects();
|
||
if (!subjects.length) {
|
||
addMessage('Bitte mindestens eine gültige URL einfügen.', 'error');
|
||
return;
|
||
}
|
||
if (subjects.length > cfg.maxURLs) return;
|
||
|
||
const button = $('generate');
|
||
const original = button.textContent;
|
||
button.disabled = true;
|
||
button.textContent = `${subjects.length} Kennzeichnungen werden erzeugt …`;
|
||
try {
|
||
const response = await fetch('/api/render-batch', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json', 'Accept': 'application/json'},
|
||
body: JSON.stringify({template: query.toString(), subjects})
|
||
});
|
||
const payload = await response.json().catch(() => ({}));
|
||
if (!response.ok) throw new Error(payload.detail || `HTTP ${response.status}`);
|
||
renderResults(payload.results || []);
|
||
} catch (error) {
|
||
addMessage(`Bulk-Lauf fehlgeschlagen: ${error.message}`, 'error');
|
||
} finally {
|
||
button.disabled = false;
|
||
button.textContent = original;
|
||
}
|
||
}
|
||
|
||
async function copyExport(kind, button) {
|
||
const value = exportsCache[kind] || '';
|
||
if (!value) return;
|
||
await navigator.clipboard.writeText(value);
|
||
const old = button.textContent;
|
||
button.textContent = 'Kopiert';
|
||
setTimeout(() => { button.textContent = old; }, 1300);
|
||
}
|
||
|
||
function downloadExport(kind) {
|
||
const spec = {
|
||
html: ['ai-disclosure-bulk.html', 'text/html;charset=utf-8'],
|
||
markdown: ['ai-disclosure-bulk.md', 'text/markdown;charset=utf-8'],
|
||
json: ['ai-disclosure-bulk.json', 'application/ld+json;charset=utf-8'],
|
||
jsonl: ['ai-disclosure-bulk.jsonl', 'application/x-ndjson;charset=utf-8'],
|
||
csv: ['ai-disclosure-bulk.csv', 'text/csv;charset=utf-8']
|
||
}[kind];
|
||
const value = exportsCache[kind] || '';
|
||
if (!spec || !value) return;
|
||
downloadBlob(spec[0], value, spec[1]);
|
||
}
|
||
|
||
function downloadBlob(filename, content, type) {
|
||
const blob = new Blob([content], {type});
|
||
const href = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = href;
|
||
a.download = filename;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
a.remove();
|
||
setTimeout(() => URL.revokeObjectURL(href), 500);
|
||
}
|
||
|
||
function exportBrowserData() {
|
||
const payload = {
|
||
format: EXPORT_FORMAT,
|
||
version: 1,
|
||
exportedAt: new Date().toISOString(),
|
||
profiles: readStore(PROFILE_KEY),
|
||
templates: readStore(TEMPLATE_KEY)
|
||
};
|
||
downloadBlob('ai-disclosure-bulk-browser-data.json', JSON.stringify(payload, null, 2), 'application/json;charset=utf-8');
|
||
}
|
||
|
||
async function importBrowserData(file) {
|
||
if (!file) return;
|
||
try {
|
||
const payload = JSON.parse(await file.text());
|
||
if (payload.format !== EXPORT_FORMAT || payload.version !== 1) throw new Error('Unbekanntes Exportformat.');
|
||
if (!payload.profiles || typeof payload.profiles !== 'object' || !payload.templates || typeof payload.templates !== 'object') throw new Error('Export ist unvollständig.');
|
||
writeStore(PROFILE_KEY, payload.profiles);
|
||
writeStore(TEMPLATE_KEY, payload.templates);
|
||
refreshProfileSelect();
|
||
refreshTemplateSelect();
|
||
alert('Profile und Vorlagen wurden importiert.');
|
||
} catch (error) {
|
||
alert(`Import fehlgeschlagen: ${error.message}`);
|
||
} finally {
|
||
$('import-browser-data').value = '';
|
||
}
|
||
}
|
||
|
||
function importFragmentTemplate() {
|
||
if (!location.hash.startsWith('#')) return;
|
||
const hash = new URLSearchParams(location.hash.slice(1));
|
||
const raw = hash.get('template');
|
||
if (!raw) return;
|
||
try {
|
||
const payload = JSON.parse(raw);
|
||
if (payload.version !== 1 || typeof payload.query !== 'string') throw new Error('Unbekanntes Vorlagenformat.');
|
||
const params = new URLSearchParams(payload.query);
|
||
if (payload.theme) params.set('theme', payload.theme);
|
||
activateTemplate(params, 'Aus dem Generator übernommen');
|
||
$('template-name').value = payload.name || '';
|
||
} catch (error) {
|
||
setStatus(`Vorlage aus dem Generator konnte nicht übernommen werden: ${error.message}`, 'error');
|
||
} finally {
|
||
history.replaceState(null, '', location.pathname + location.search);
|
||
}
|
||
}
|
||
|
||
function clearBrowserData() {
|
||
if (!confirm('Alle lokal gespeicherten Website-Profile und Kennzeichnungsvorlagen in diesem Browser löschen?')) return;
|
||
localStorage.removeItem(PROFILE_KEY);
|
||
localStorage.removeItem(TEMPLATE_KEY);
|
||
refreshProfileSelect();
|
||
refreshTemplateSelect();
|
||
emptyProfileForm();
|
||
setStatus('Lokale Profile und Vorlagen wurden gelöscht.');
|
||
}
|
||
|
||
async function init() {
|
||
try {
|
||
const response = await fetch('/api/config', {headers: {'Accept': 'application/json'}});
|
||
cfg = await response.json();
|
||
} catch (_) {
|
||
cfg = {maxURLs: 500};
|
||
}
|
||
refreshTemplateSelect();
|
||
refreshProfileSelect();
|
||
importFragmentTemplate();
|
||
parseSubjects();
|
||
|
||
$('load-template').addEventListener('click', () => {
|
||
try {
|
||
activateTemplate(parseTemplate($('template-input').value));
|
||
} catch (error) {
|
||
setStatus(error.message, 'error');
|
||
}
|
||
});
|
||
$('save-template').addEventListener('click', saveTemplate);
|
||
$('saved-template-select').addEventListener('change', event => loadSavedTemplate(event.target.value));
|
||
$('delete-template').addEventListener('click', deleteTemplate);
|
||
$('profile-select').addEventListener('change', event => loadProfile(event.target.value));
|
||
$('new-profile').addEventListener('click', emptyProfileForm);
|
||
$('save-profile').addEventListener('click', saveProfile);
|
||
$('delete-profile').addEventListener('click', deleteProfile);
|
||
$('export-browser-data').addEventListener('click', exportBrowserData);
|
||
$('import-browser-data').addEventListener('change', event => importBrowserData(event.target.files?.[0]));
|
||
$('clear-browser-data').addEventListener('click', clearBrowserData);
|
||
$('url-input').addEventListener('input', parseSubjects);
|
||
$('base-url').addEventListener('input', parseSubjects);
|
||
$('clear-urls').addEventListener('click', () => {
|
||
$('url-input').value = '';
|
||
$('url-messages').replaceChildren();
|
||
parseSubjects();
|
||
});
|
||
$('generate').addEventListener('click', generate);
|
||
document.querySelectorAll('[data-copy]').forEach(button => button.addEventListener('click', () => copyExport(button.dataset.copy, button)));
|
||
document.querySelectorAll('[data-download]').forEach(button => button.addEventListener('click', () => downloadExport(button.dataset.download)));
|
||
}
|
||
|
||
init();
|
||
})();
|