Files
emailtemplate.it/public/assets/js/ui-list.js
2026-02-03 03:38:30 +01:00

649 lines
25 KiB
JavaScript
Raw 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.
import { apiAction, apiUpdate, toast } from './api.js';
function esc(s = '') {
return String(s)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
const showConfirmDialog = (() => {
let dialog;
let titleEl;
let textEl;
let btnOk;
let btnCancel;
let pendingResolve = null;
const ensure = () => {
if (dialog) return;
dialog = document.createElement('dialog');
dialog.className = 'rounded-2xl p-0 w-[520px]';
dialog.innerHTML = `
<div class="p-4 bg-white rounded-2xl space-y-4">
<h3 class="text-lg font-semibold" data-confirm-title></h3>
<p class="text-sm text-slate-600" data-confirm-text></p>
<div class="flex justify-end gap-2">
<button type="button" class="btn" data-confirm-cancel>Abbrechen</button>
<button type="button" class="btn btn-danger" data-confirm-ok>Bestätigen</button>
</div>
</div>
`;
document.body.appendChild(dialog);
titleEl = dialog.querySelector('[data-confirm-title]');
textEl = dialog.querySelector('[data-confirm-text]');
btnOk = dialog.querySelector('[data-confirm-ok]');
btnCancel = dialog.querySelector('[data-confirm-cancel]');
btnOk?.addEventListener('click', () => {
if (pendingResolve) pendingResolve(true);
pendingResolve = null;
dialog.close();
});
btnCancel?.addEventListener('click', () => {
if (pendingResolve) pendingResolve(false);
pendingResolve = null;
dialog.close();
});
dialog.addEventListener('close', () => {
if (pendingResolve) pendingResolve(false);
pendingResolve = null;
});
};
return async ({ title, text, confirmLabel = 'Bestätigen', cancelLabel = 'Abbrechen' }) => {
ensure();
if (titleEl) titleEl.textContent = title || 'Bestätigung';
if (textEl) textEl.textContent = text || '';
if (btnOk) btnOk.textContent = confirmLabel;
if (btnCancel) btnCancel.textContent = cancelLabel;
if (!dialog.open) dialog.showModal();
return await new Promise(resolve => {
pendingResolve = resolve;
});
};
})();
function formatVersionDate(value) {
if (!value) return '';
try {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleString('de-DE');
} catch {
return value;
}
}
function normalizeApiName(v = '') {
return String(v)
.trim()
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9_-]+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}
async function fetchContentList(sectionId) {
const res = await apiAction('content.list', { method: 'GET', data: { section_id: sectionId } });
return Array.isArray(res?.items) ? res.items : [];
}
async function fetchContentItem(id, sectionId) {
return await apiAction('content.get', { method: 'GET', data: { id, section_id: sectionId } });
}
async function openContentEditor(item, section) {
const versionId = Number(item?.version_id || 0);
const id = Number(item?.id || 0);
const name = item?.name || '';
if (!id) return;
const detail = await fetchContentItem(id, section.id).catch(() => ({}));
const html = detail?.html ?? detail?.item?.html ?? detail?.content ?? '';
window.__currentItemId = id;
window.__currentEditorCtx = { id, mode: section.slug, section };
if (window.EditorUI && typeof window.EditorUI.open === 'function') {
window.EditorUI.open({ id, name, html, section, version_id: versionId }, 'content');
} else if (window.__openEditor) {
window.__openEditor({ resource: 'content', id, name, html, section, version_id: versionId });
} else {
toast('Editor ist nicht initialisiert.', false);
}
}
async function openTemplateManager(item, section) {
const dlg = document.getElementById('manageTemplateDialog');
const inpName = document.getElementById('manage_tpl_name');
const inpApiName = document.getElementById('manage_tpl_api_name');
const apiWrap = document.getElementById('manage_tpl_api_wrap');
const apiWarn = document.getElementById('manage_tpl_api_warn');
const badge = document.getElementById('manage_tpl_badge');
const versionsWrap = document.getElementById('manage_tpl_versions');
const btnClose = document.getElementById('manageTemplateClose');
const btnDelete = document.getElementById('manageTemplateDelete');
const deleteHint = document.getElementById('manage_tpl_delete_hint');
const delDlg = document.getElementById('deleteDialog');
const delText = document.getElementById('deleteText');
const delForm = document.getElementById('deleteForm');
const delCancel = document.getElementById('deleteCancel');
const detail = await fetchContentItem(item.id, section.id).catch(() => ({}));
const row = detail?.item || detail?.data || detail || {};
const initialApi = row.api_name || '';
if (badge) badge.textContent = `ID ${item.id}`;
if (inpName) inpName.value = row.name || '';
if (inpApiName) inpApiName.value = initialApi;
if (apiWarn) apiWarn.classList.add('hidden');
if (apiWrap) apiWrap.classList.toggle('hidden', !section?.is_template);
let versions = [];
let activeId = 0;
const isTemplateSection = () => {
if (section?.is_template) return true;
const slug = (section?.slug || '').toString().toLowerCase();
return slug === 'emailtemplate' || slug.includes('template');
};
const fetchTemplateReferences = async () => {
if (!isTemplateSection()) return null;
const res = await apiAction('templates.references', { method: 'GET', data: { template_id: item.id } }).catch(() => null);
if (!res || res.ok === false) return null;
return Array.isArray(res?.references) ? res.references : [];
};
const confirmTemplateReferences = async (actionLabel) => {
if (!isTemplateSection()) return true;
const refs = await fetchTemplateReferences();
if (refs === null) {
return await showConfirmDialog({
title: 'Referenzen nicht geprüft',
text: `Referenzen konnten nicht geprüft werden. ${actionLabel} trotzdem?`,
confirmLabel: actionLabel,
});
}
if (!refs.length) return true;
const preview = refs.slice(0, 6).map(r => `${r.name || 'Template'} #${r.id}`).join(', ');
const more = refs.length > 6 ? ` und ${refs.length - 6} weitere` : '';
return await showConfirmDialog({
title: 'Template wird verwendet',
text: `Dieses Template wird in ${refs.length} anderen Template(s) verwendet (${preview}${more}). ${actionLabel} trotzdem?`,
confirmLabel: actionLabel,
});
};
const updateDeleteState = () => {
const hasActive = !!activeId;
if (btnDelete) btnDelete.disabled = hasActive;
if (deleteHint) {
deleteHint.textContent = hasActive
? 'Aktive Version vorhanden Löschen deaktiviert.'
: 'Nur möglich, wenn keine aktive Version existiert.';
}
};
const loadVersions = async () => {
const res = await apiAction('content_versions.list', { method: 'GET', data: { content_id: item.id } }).catch(() => ({}));
versions = Array.isArray(res?.items) ? res.items : [];
activeId = 0;
versions.forEach(v => {
if (Number(v.is_active) === 1) activeId = Number(v.id || 0);
});
renderVersions();
updateDeleteState();
};
const renderVersions = () => {
if (!versionsWrap) return;
if (!versions.length) {
versionsWrap.innerHTML = '<div class=\"text-xs text-slate-500\">Keine Versionen vorhanden</div>';
return;
}
versionsWrap.innerHTML = versions.map(v => {
const isActive = Number(v.is_active) === 1;
const label = `${isActive ? '✓ ' : ''}#${v.version_no} ${formatVersionDate(v.created_at)}` + (isActive ? ' (aktiv)' : '');
const deleteBtn = isActive ? '' : `<button class=\"btn btn-danger\" data-version-delete=\"${v.id}\">Löschen</button>`;
return `<div class=\"flex items-center gap-2 border rounded-lg px-3 py-2\" data-version-row=\"${v.id}\">
<div class=\"text-xs text-slate-600\">${label}</div>
<div class=\"ms-auto flex gap-2\">
<button class=\"btn\" data-version-edit=\"${v.id}\">Bearbeiten</button>
${isActive ? `<button class=\"btn\" data-version-deactivate=\"${v.id}\">Deaktivieren</button>` : `<button class=\"btn\" data-version-activate=\"${v.id}\">Aktivieren</button>`}
${deleteBtn}
</div>
</div>`;
}).join('');
};
const onApiInput = () => {
if (!inpApiName) return;
const next = normalizeApiName(inpApiName.value);
if (next !== inpApiName.value) inpApiName.value = next;
if (apiWarn) {
apiWarn.classList.toggle('hidden', inpApiName.value.trim() === initialApi);
}
};
let autoSaveTimer = null;
const autoSave = async () => {
try {
const payload = {
name: inpName ? inpName.value : '',
section_id: section.id,
};
if (section?.is_template) {
payload.api_name = inpApiName ? inpApiName.value : '';
}
const res = await apiUpdate('content', item.id, payload);
if (res?.ok && typeof window.loadList === 'function') window.loadList(section);
if (!res?.ok) toast(res?.error || 'Speichern fehlgeschlagen', false);
} catch {
toast('Speichern fehlgeschlagen', false);
}
};
const scheduleAutoSave = () => {
if (autoSaveTimer) clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(autoSave, 450);
};
const cleanup = () => {
inpApiName && inpApiName.removeEventListener('input', onApiInput);
inpName && inpName.removeEventListener('input', scheduleAutoSave);
inpApiName && inpApiName.removeEventListener('input', scheduleAutoSave);
versionsWrap && versionsWrap.removeEventListener('click', onVersionsClick);
if (btnClose) btnClose.onclick = null;
if (btnDelete) btnDelete.onclick = null;
if (autoSaveTimer) {
clearTimeout(autoSaveTimer);
autoSaveTimer = null;
}
};
const onDeleteItem = async () => {
if (activeId) return;
if (section?.is_template) {
const ok = await confirmTemplateReferences('Löschen');
if (!ok) return;
}
if (!delDlg || !delForm) {
const res = await apiAction('content.delete', { method: 'POST', data: { id: item.id, section_id: section.id } });
toast(res && res.ok ? 'Gelöscht' : 'Löschen fehlgeschlagen', !!(res && res.ok), { duration: 3000 });
dlg && dlg.close();
if (typeof window.loadList === 'function') window.loadList(section);
return;
}
if (delText) {
delText.innerHTML = `Soll <strong>${item.name || '(ohne Name)'} #${item.id}</strong> wirklich gelöscht werden?`;
}
const cleanupDelete = () => {
delForm.onsubmit = null;
if (delCancel) delCancel.onclick = null;
};
if (delCancel) delCancel.onclick = () => { delDlg.close(); cleanupDelete(); };
delForm.onsubmit = async (ev) => {
ev.preventDefault();
if (section?.is_template) {
const ok = await confirmTemplateReferences('Löschen');
if (!ok) return;
}
const res = await apiAction('content.delete', { method: 'POST', data: { id: item.id, section_id: section.id } });
delDlg.close();
cleanupDelete();
toast(res && res.ok ? 'Gelöscht' : 'Löschen fehlgeschlagen', !!(res && res.ok), { duration: 3000 });
dlg && dlg.close();
if (typeof window.loadList === 'function') window.loadList(section);
};
delDlg.showModal();
};
const onVersionsClick = async (ev) => {
const target = ev.target;
if (!target || !target.dataset) return;
const vid = Number(target.dataset.versionEdit || target.dataset.versionActivate || target.dataset.versionDeactivate || target.dataset.versionDelete || 0);
if (!vid) return;
if (target.dataset.versionEdit !== undefined) {
dlg && dlg.close();
const versionItem = { ...item, version_id: vid };
openContentEditor(versionItem, section);
return;
}
if (target.dataset.versionActivate !== undefined) {
const res = await apiAction('content_versions.activate', { method: 'POST', data: { id: vid } });
toast(res && res.ok ? 'Version aktiviert' : 'Aktivieren fehlgeschlagen', !!(res && res.ok));
await loadVersions();
if (typeof window.loadList === 'function') window.loadList(section);
return;
}
if (target.dataset.versionDeactivate !== undefined) {
if (section?.is_template) {
const ok = await confirmTemplateReferences('Deaktivieren');
if (!ok) return;
}
let res = await apiAction('content_versions.deactivate', { method: 'POST', data: { content_id: item.id } });
if (res && res.ok === false && Array.isArray(res.references) && res.references.length) {
const ok = await showConfirmDialog({
title: 'Template wird verwendet',
text: 'Dieses Template wird in anderen Templates verwendet. Trotzdem deaktivieren?',
confirmLabel: 'Deaktivieren',
});
if (!ok) return;
res = await apiAction('content_versions.deactivate', { method: 'POST', data: { content_id: item.id, force: 1 } });
}
toast(res && res.ok ? 'Aktive Version deaktiviert' : 'Deaktivieren fehlgeschlagen', !!(res && res.ok));
await loadVersions();
if (typeof window.loadList === 'function') window.loadList(section);
return;
}
if (target.dataset.versionDelete !== undefined) {
const versionRow = versions.find(v => Number(v.id) === vid);
if (versionRow && Number(versionRow.is_active) === 1) return;
if (!confirm('Version wirklich löschen?')) return;
let res = await apiAction('content_versions.delete', { method: 'POST', data: { id: vid, content_id: item.id } });
if (res && res.ok === false && Array.isArray(res.references) && res.references.length) {
const ok = await showConfirmDialog({
title: 'Template wird verwendet',
text: 'Dieses Template wird in anderen Templates verwendet. Trotzdem löschen?',
confirmLabel: 'Löschen',
});
if (!ok) return;
res = await apiAction('content_versions.delete', { method: 'POST', data: { id: vid, content_id: item.id, force: 1 } });
}
toast(res && res.ok ? 'Version gelöscht' : 'Löschen fehlgeschlagen', !!(res && res.ok));
await loadVersions();
if (typeof window.loadList === 'function') window.loadList(section);
return;
}
};
inpApiName && inpApiName.addEventListener('input', onApiInput);
inpName && inpName.addEventListener('input', scheduleAutoSave);
inpApiName && inpApiName.addEventListener('input', scheduleAutoSave);
btnClose && (btnClose.onclick = () => { dlg && dlg.close(); cleanup(); });
btnDelete && (btnDelete.onclick = onDeleteItem);
versionsWrap && versionsWrap.addEventListener('click', onVersionsClick);
await loadVersions();
dlg && dlg.addEventListener('close', cleanup, { once: true });
dlg && dlg.showModal();
}
export async function loadList(section) {
const el = document.getElementById('view-content');
if (typeof section === 'string') {
const sections = window.__sectionsConfig || [];
section = sections.find(s => String(s.slug || '').toLowerCase() === section.toLowerCase())
|| sections.find(s => String(s.name || '').toLowerCase() === section.toLowerCase())
|| null;
} else if (typeof section === 'number') {
const sections = window.__sectionsConfig || [];
section = sections.find(s => Number(s.id) === section) || null;
}
section = section || window.__activeSection || null;
if (!el || !section) return;
const label = section.name || 'Section';
const isTemplate = Number(section.is_template) === 1;
el.innerHTML = `<div class='rounded-2xl border bg-white overflow-hidden'>
<div class='px-4 py-2 border-b bg-gray-50 text-sm font-medium flex items-center gap-3'>
<span>${esc(label)}</span>
<div class='ms-auto flex items-center gap-2'>
<div class='flex items-center gap-1'>
<input id='filter-section' class='input text-sm' placeholder='Suche Name${isTemplate ? '/API' : ''}' />
<button id='filter-section-reset' class='btn' type='button' title='Suche zurücksetzen'>×</button>
</div>
<select id='sort-section' class='input text-sm'>
<option value='created_asc'>Erstelldatum (aufsteigend)</option>
<option value='name_asc'>Name AZ</option>
<option value='name_desc'>Name ZA</option>
<option value='updated_desc'>Zuletzt bearbeitet</option>
</select>
</div>
</div>
<div id='list-section' class='divide-y'>Lade …</div></div>`;
let data = [];
try {
data = await fetchContentList(section.id);
} catch (err) {
list.innerHTML = `<div class='p-4 text-sm text-gray-500'>${esc(err.message || 'Laden fehlgeschlagen')}</div>`;
return;
}
const list = el.querySelector('#list-section');
const filterInput = el.querySelector('#filter-section');
const filterReset = el.querySelector('#filter-section-reset');
const sortSelect = el.querySelector('#sort-section');
if (!Array.isArray(data) || data.length === 0) {
list.innerHTML = `<div class='p-4 text-sm text-gray-500'>Keine Einträge</div>`;
return;
}
const sortDefault = (window.__listSortDefault || 'created_asc');
if (sortSelect) sortSelect.value = sortDefault;
function compareByName(a, b) {
const av = String(a?.name || '');
const bv = String(b?.name || '');
return av.localeCompare(bv, undefined, { numeric: true, sensitivity: 'base' });
}
function parseDate(value) {
const t = Date.parse(value || '');
return Number.isFinite(t) ? t : 0;
}
function sortItems(items, key) {
const listCopy = items.slice();
if (key === 'name_asc') {
return listCopy.sort(compareByName);
}
if (key === 'name_desc') {
return listCopy.sort((a, b) => compareByName(b, a));
}
if (key === 'updated_desc') {
return listCopy.sort((a, b) => parseDate(b.updated_at) - parseDate(a.updated_at));
}
return listCopy.sort((a, b) => parseDate(a.created_at) - parseDate(b.created_at));
}
function matchesQuery(item, query) {
if (!query) return true;
const q = query.toLowerCase();
const name = String(item?.name || '').toLowerCase();
const api = isTemplate ? String(item?.api_name || '').toLowerCase() : '';
return name.includes(q) || (api && api.includes(q));
}
const versionCache = new Map();
async function loadVersionOptions(selectEl, itemId) {
if (!selectEl || !itemId) return;
if (versionCache.has(itemId)) {
const cached = versionCache.get(itemId);
renderVersionSelect(selectEl, cached.items, cached.activeId);
return;
}
try {
const res = await apiAction('content_versions.list', { method: 'GET', data: { content_id: itemId } });
const items = Array.isArray(res?.items) ? res.items : [];
const active = items.find(v => Number(v.is_active) === 1);
const activeId = active ? String(active.id) : (items[0] ? String(items[0].id) : '');
versionCache.set(itemId, { items, activeId });
renderVersionSelect(selectEl, items, activeId);
} catch {
renderVersionSelect(selectEl, [], '');
}
}
function renderVersionSelect(selectEl, items, activeId) {
selectEl.innerHTML = '';
if (!items.length) {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = 'Keine Versionen';
opt.disabled = true;
selectEl.appendChild(opt);
selectEl.disabled = true;
return;
}
selectEl.disabled = false;
const sorted = items.slice().sort((a, b) => {
const aActive = Number(a.is_active) === 1 ? 1 : 0;
const bActive = Number(b.is_active) === 1 ? 1 : 0;
if (aActive !== bActive) return bActive - aActive;
return Number(b.id || 0) - Number(a.id || 0);
});
sorted.forEach(item => {
const opt = document.createElement('option');
const label = `${Number(item.is_active) === 1 ? '✓ ' : ''}#${item.version_no} ${formatVersionDate(item.created_at)}` + (Number(item.is_active) === 1 ? ' (aktiv)' : '');
opt.value = String(item.id);
opt.textContent = label;
selectEl.appendChild(opt);
});
if (activeId) selectEl.value = activeId;
}
function render(items) {
list.innerHTML = items.map(item => {
const name = esc(item.name || '');
const apiName = isTemplate ? esc(item.api_name || '') : '';
const apiLine = (isTemplate && apiName) ? `<div class='text-xs text-slate-500'>API: ${apiName}</div>` : '';
const versionSelect = `<select class="input h-8 py-0 text-xs min-w-[160px]" data-version-select="${item.id}" disabled>
<option value="">Versionen laden…</option>
</select>`;
const nameCell = `<div class='min-w-48'>
<div class='font-medium truncate' title="${name}">${name || '(ohne Name)'}</div>
${apiLine}
</div>`;
const openBtn = `<button class='btn' data-open='${item.id}'>Bearbeiten</button>`;
const editTplBtn = `<button class='btn' data-edit='${item.id}'>Verwaltung</button>`;
const testBtn = isTemplate ? `<button class='btn' data-test='${item.id}' data-name='${name}'>Testversand</button>` : '';
const prevBtn = `<button class='btn' data-preview='${item.id}'>Vorschau</button>`;
return `<div class='p-3 flex items-center gap-3'>
${nameCell}
<div class='text-xs text-gray-500'>#${item.id}</div>
<div class='ms-auto flex gap-2 items-center'>
${openBtn}
${versionSelect}
${[testBtn, prevBtn, editTplBtn].filter(Boolean).join('')}
</div>
</div>`;
}).join('');
bindListHandlers(list);
}
function applyFilter() {
const query = (filterInput?.value || '').trim();
const sortKey = sortSelect?.value || sortDefault;
const filtered = data.filter(item => matchesQuery(item, query));
const sorted = sortItems(filtered, sortKey);
render(sorted);
}
async function persistSort(nextValue) {
window.__listSortDefault = nextValue;
try {
await apiAction('account.settings.update', { method: 'POST', data: { list_sort: nextValue } });
} catch {}
}
if (filterInput) filterInput.addEventListener('input', applyFilter);
if (filterReset) {
filterReset.addEventListener('click', () => {
if (filterInput) {
filterInput.value = '';
filterInput.focus();
}
applyFilter();
});
}
if (sortSelect) {
sortSelect.addEventListener('change', () => {
const next = sortSelect.value || sortDefault;
persistSort(next);
applyFilter();
});
}
applyFilter();
function bindListHandlers(scope) {
scope.querySelectorAll('[data-version-select]').forEach(sel => {
const id = Number(sel.getAttribute('data-version-select') || 0);
loadVersionOptions(sel, id);
sel.addEventListener('focus', () => loadVersionOptions(sel, id));
sel.addEventListener('click', () => loadVersionOptions(sel, id));
sel.addEventListener('change', () => {
const versionId = Number(sel.value || 0);
if (!versionId) return;
const item = data.find(it => Number(it.id) === id);
if (!item) return;
openContentEditor({ ...item, version_id: versionId }, section);
});
});
scope.querySelectorAll('[data-open]').forEach(btn => btn.addEventListener('click', () => {
const id = Number(btn.dataset.open || 0);
const item = data.find(it => Number(it.id) === id);
if (!item) return;
openContentEditor(item, section);
}));
scope.querySelectorAll('[data-edit]').forEach(btn => btn.addEventListener('click', () => {
const id = Number(btn.dataset.edit || 0);
const item = data.find(it => Number(it.id) === id);
if (item) openTemplateManager(item, section);
}));
const prevDlg = document.getElementById('previewDialog');
const prevFrame = document.getElementById('previewFrame');
scope.querySelectorAll('[data-preview]').forEach(btn => btn.addEventListener('click', async () => {
const id = Number(btn.dataset.preview || 0);
const obj = await apiAction('content.get', { method: 'GET', data: { id, section_id: section.id, active_only: 1 } });
const html = (obj?.html || obj?.content || '<em>(leer)</em>');
prevFrame.srcdoc = '<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head><body>' + html + '</body></html>';
prevDlg.showModal();
}));
scope.querySelectorAll('[data-test]').forEach(btn => btn.addEventListener('click', async () => {
const id = Number(btn.dataset.test || 0);
const nm = btn.dataset.name || '';
if (!id) {
toast('Testversand: Ungültige ID', false);
return;
}
const activeCheck = await apiAction('content.get', { method: 'GET', data: { id, section_id: section.id, active_only: 1 } }).catch(() => ({}));
if (!activeCheck?.active_version_id && !activeCheck?.item?.active_version_id) {
toast('Testversand nur mit aktiver Version möglich.', false);
return;
}
if (window.AdminTestSend && typeof window.AdminTestSend.open === 'function') {
window.AdminTestSend.open({ id, name: nm });
} else {
toast('Testversand ist aktuell nicht verfügbar.', false);
}
}));
// delete handling removed from overview
}
}
export function initLists() {
if (window.__sectionsReady && typeof window.__sectionsReady.then === 'function') {
window.__sectionsReady.then(() => {
if (window.__activeSection) loadList(window.__activeSection);
});
} else if (window.__activeSection) {
loadList(window.__activeSection);
}
window.__reloadList = loadList;
window.loadList = loadList;
}