Raiju/admin/dashboard/scripts/icons.php
2026-10-25 06:05:42 +03:00

604 lines
29 KiB
PHP

<?php if (!defined('RAIJU_DASHBOARD')) { http_response_code(404); exit; } ?>
<?php if ($isAdminUser): ?>
// ============================================================
// ICON MANAGER
// ============================================================
let iconManagerData = [];
let iconManagerPath = '';
let iconManagerSortMode = 'alpha';
let iconManagerSearchTerm = '';
let iconManagerHighlightedKeys = new Set();
let iconManagerHighlightTimer = null;
const ICONIFY_IMPORT_SETS = {
lucide: { label: 'Lucide', license: 'ISC', licenseUrl: 'https://github.com/lucide-icons/lucide/blob/main/LICENSE' },
tabler: { label: 'Tabler Icons', license: 'MIT', licenseUrl: 'https://github.com/tabler/tabler-icons/blob/master/LICENSE' },
heroicons:{ label: 'Heroicons', license: 'MIT', licenseUrl: 'https://github.com/tailwindlabs/heroicons/blob/master/LICENSE' },
ph: { label: 'Phosphor', license: 'MIT', licenseUrl: 'https://github.com/phosphor-icons/core/blob/main/LICENSE' },
bi: { label: 'Bootstrap Icons',license: 'MIT', licenseUrl: 'https://github.com/twbs/icons/blob/main/LICENSE.md', style: 'fill' },
iconoir: { label: 'Iconoir', license: 'MIT', licenseUrl: 'https://github.com/iconoir-icons/iconoir/blob/main/LICENSE' },
ion: { label: 'Ionicons', license: 'MIT', licenseUrl: 'https://github.com/ionic-team/ionicons/blob/main/LICENSE' },
mynaui: { label: 'Myna UI', license: 'MIT', licenseUrl: 'https://github.com/MynaUI/icons/blob/main/LICENSE' },
mingcute: { label: 'MingCute', license: 'Apache-2.0', licenseUrl: 'https://github.com/mingcute-design/mingcute-icons/blob/main/LICENSE' },
tdesign: { label: 'TDesign Icons', license: 'MIT', licenseUrl: 'https://github.com/Tencent/tdesign-icons/blob/main/LICENSE' }
};
async function fetchJsonWithTimeout(url, options = {}, timeoutMs = 12000) {
const controller = new AbortController();
const timer = setTimeout(function() { controller.abort(); }, timeoutMs);
try {
const response = await fetch(url, Object.assign({}, options, {
signal: controller.signal,
cache: 'no-store',
}));
return await response.json();
} finally {
clearTimeout(timer);
}
}
function cleanSvgAttributes(attrs) {
if (!attrs) return '';
const allowedAttrs = new Set([
'd', 'points', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx', 'ry',
'width', 'height', 'fill', 'stroke', 'stroke-width', 'stroke-linecap',
'stroke-linejoin', 'stroke-miterlimit', 'stroke-dasharray', 'stroke-dashoffset',
'fill-rule', 'clip-rule', 'opacity', 'fill-opacity', 'stroke-opacity',
'transform', 'id', 'clip-path', 'mask', 'offset', 'stop-color', 'stop-opacity',
'gradientUnits', 'gradientTransform'
]);
let cleaned = '';
attrs.replace(/\s+([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*("[^"]*"|'[^']*'|[^\s>\/]+))?/g, function(match, rawName, rawValue) {
const name = rawName.replace(/^xlink:/i, '').trim();
const lower = name.toLowerCase();
if (lower.startsWith('on') || lower === 'style' || lower === 'class') return '';
if (!allowedAttrs.has(name) && !allowedAttrs.has(lower)) return '';
const attrName = allowedAttrs.has(name) ? name : lower;
let value = rawValue || '';
if (value) {
const unquoted = value.replace(/^["']|["']$/g, '').trim();
if ((lower === 'clip-path' || lower === 'mask') && unquoted && !/^url\(#[-_a-zA-Z0-9]+\)$/.test(unquoted)) return '';
if ((lower === 'fill' || lower === 'stroke') && /^url\(/i.test(unquoted) && !/^url\(#[-_a-zA-Z0-9]+\)$/i.test(unquoted)) return '';
value = '="' + escapeHtml(unquoted) + '"';
}
cleaned += ' ' + attrName + value;
return '';
});
return cleaned;
}
function sanitizeIconSvgClient(svg) {
svg = (svg || '').trim()
.replace(/<\?xml[\s\S]*?\?>/gi, '')
.replace(/<!DOCTYPE[\s\S]*?>/gi, '')
.replace(/<\s*\/?\s*svg\b[^>]*>/gi, '')
.replace(/<!--[\s\S]*?-->/g, '')
.replace(/<\s*script\b[^>]*>[\s\S]*?<\s*\/\s*script\s*>/gi, '')
.replace(/<\s*(metadata|desc|style|sodipodi:namedview)\b[^>]*>[\s\S]*?<\s*\/\s*\1\s*>/gi, '')
.replace(/<\s*defs\b[^>]*>\s*(?:<\s*style\b[^>]*>[\s\S]*?<\s*\/\s*style\s*>\s*)*<\s*\/\s*defs\s*>/gi, '')
.replace(/\s+on[a-z]+\s*=\s*(["']).*?\1/gi, '')
.replace(/\s+(href|xlink:href)\s*=\s*(["'])\s*(?!#)[^"']*\2/gi, '');
const allowed = new Set(['path', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'rect', 'g', 'defs', 'clipPath', 'mask', 'linearGradient', 'radialGradient', 'stop', 'title']);
svg = svg.replace(/<\s*(\/?)([a-zA-Z][a-zA-Z0-9:-]*)([^>]*)>/g, function(match, closing, tag, attrs) {
if (!allowed.has(tag)) return '';
if (closing) return `</${tag}>`;
const selfClosing = /\/\s*$/.test(attrs || '');
const cleanedAttrs = cleanSvgAttributes(attrs || '');
return `<${tag}${cleanedAttrs}${selfClosing ? '/' : ''}>`;
});
svg = svg.replace(/^\s*(?:\.[\w-]+\s*\{[^}]*\}\s*)+/gm, '');
svg = removeEmptySvgContainers(svg);
return svg.trim();
}
function normalizeIconViewBoxClient(viewBox) {
const raw = (viewBox || '').trim();
if (!raw) return '0 0 24 24';
const parts = raw.split(/[\s,]+/);
if (parts.length !== 4) return '0 0 24 24';
const numbers = parts.map(Number);
if (numbers.some(function(value) { return !Number.isFinite(value); }) || numbers[2] <= 0 || numbers[3] <= 0) {
return '0 0 24 24';
}
return numbers.map(function(value) {
return Number.isInteger(value) ? String(value) : String(parseFloat(value.toFixed(6)));
}).join(' ');
}
function extractIconViewBoxClient(svg) {
const markup = String(svg || '');
const viewBoxMatch = markup.match(/<\s*svg\b[^>]*\sviewBox\s*=\s*(["'])(.*?)\1/i);
if (viewBoxMatch) return normalizeIconViewBoxClient(viewBoxMatch[2]);
const widthMatch = markup.match(/<\s*svg\b[^>]*\swidth\s*=\s*(["'])([0-9.]+)(?:px)?\1/i);
const heightMatch = markup.match(/<\s*svg\b[^>]*\sheight\s*=\s*(["'])([0-9.]+)(?:px)?\1/i);
if (widthMatch && heightMatch) return normalizeIconViewBoxClient('0 0 ' + widthMatch[2] + ' ' + heightMatch[2]);
return '';
}
function removeEmptySvgContainers(svg) {
let previous = '';
let cleaned = svg || '';
while (cleaned !== previous) {
previous = cleaned;
cleaned = cleaned
.replace(/<defs\b[^>]*>\s*<\/defs>/gi, '')
.replace(/<g\b[^>]*>\s*<\/g>/gi, '')
.replace(/<clipPath\b[^>]*>\s*<\/clipPath>/gi, '')
.replace(/<mask\b[^>]*>\s*<\/mask>/gi, '');
}
return cleaned;
}
function forceIconCurrentColor(svg) {
let normalized = sanitizeIconSvgClient(svg)
.replace(/\s(fill|stroke)\s*=\s*(["'])(?!none\2|currentColor\2|url\(#)[^"']*\2/gi, ' $1="currentColor"')
.replace(/\sstop-color\s*=\s*(["'])(?!currentColor\1|none\1)[^"']*\1/gi, ' stop-color="currentColor"');
normalized = normalized.replace(/<\s*(polygon|circle|ellipse|rect)\b([^>]*)\/?>/gi, function(match, tag, attrs) {
if (/\s(?:fill|stroke)\s*=/i.test(attrs)) return match;
const selfClosing = /\/\s*>$/.test(match);
const cleanedAttrs = (attrs || '').replace(/\/\s*$/, '');
return `<${tag}${cleanedAttrs} fill="currentColor"${selfClosing ? '/>' : '>'}`;
});
return normalized;
}
async function loadIconManager() {
const button = document.getElementById('iconManagerRefreshBtn');
if (button) button.disabled = true;
try {
const result = await fetchJsonWithTimeout('api.php?action=list-icons&csrf_token=' + encodeURIComponent(CSRF_TOKEN) + '&_=' + Date.now());
if (result.success) {
iconManagerData = result.data.icons || [];
iconManagerPath = result.data.path || 'content/settings/iconset.json';
renderIconManager(result.data);
} else {
showToast(result.message, 'error');
}
} catch (error) {
showToast(t('icons.load_error', {message: error.message}), 'error');
} finally {
if (button) button.disabled = false;
}
}
function getFilteredIconManagerIcons() {
const term = iconManagerSearchTerm.toLowerCase();
let icons = iconManagerData.slice();
if (term) {
icons = icons.filter(function(iconItem) {
const haystack = [
iconItem.key || '',
iconItem.label || '',
(iconItem.tags || []).join(' '),
iconItem.source || ''
].join(' ').toLowerCase();
return haystack.includes(term);
});
}
icons.sort(function(a, b) {
if (iconManagerSortMode === 'newest' || iconManagerSortMode === 'oldest') {
const aTime = Date.parse(a.createdAt || a.updatedAt || '') || 0;
const bTime = Date.parse(b.createdAt || b.updatedAt || '') || 0;
if (aTime !== bTime) {
return iconManagerSortMode === 'newest' ? bTime - aTime : aTime - bTime;
}
}
return String(a.key || '').localeCompare(String(b.key || ''), undefined, { sensitivity: 'base' });
});
return icons;
}
function iconSvgAttributes(iconItem) {
return (iconItem.style || 'stroke') === 'fill'
? 'fill="currentColor"'
: 'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
}
function iconSvgMarkup(iconItem) {
const svg = iconItem.svg || '';
return (iconItem.style || 'stroke') === 'fill'
? svg.replace(/\s+stroke\s*=\s*(["'])currentColor\1/gi, '')
: svg;
}
function highlightIconManagerCard(key) {
if (key) iconManagerHighlightedKeys.add(key);
if (iconManagerHighlightTimer) clearTimeout(iconManagerHighlightTimer);
if (iconManagerHighlightedKeys.size) {
iconManagerHighlightTimer = setTimeout(function() {
iconManagerHighlightedKeys.clear();
renderIconManager();
}, 60000);
}
}
function renderIconManager(data = null) {
const grid = document.getElementById('iconManagerGrid');
const empty = document.getElementById('iconManagerEmpty');
const path = document.getElementById('iconManagerPath');
if (!grid) return;
if (data) {
iconManagerData = data.icons || [];
iconManagerPath = data.path || iconManagerPath;
}
grid.innerHTML = '';
// The site-relative path only: a server path says nothing an editor needs and reveals the install location
if (path) path.textContent = t('icons.path', {path: (iconManagerPath || 'content/settings/iconset.json').replace(/^.*?(?=content\/)/, '')});
const icons = getFilteredIconManagerIcons();
if (!icons.length) {
if (empty) empty.style.display = '';
renderAdminListFooter('iconManagerFooter', 'icons', 0, getDashboardPageSize('icons'), renderIconManager, 'iconManagerFooterTop');
return;
}
if (empty) empty.style.display = 'none';
const pageSize = getDashboardPageSize('icons');
const paged = pageSlice(icons, 'icons', pageSize);
renderAdminListFooter('iconManagerFooter', 'icons', icons.length, pageSize, renderIconManager, 'iconManagerFooterTop');
paged.items.forEach(function(iconItem) {
const card = document.createElement('article');
card.className = 'icon-manager-card';
if (iconManagerHighlightedKeys.has(iconItem.key)) {
card.classList.add('icon-manager-card--highlight');
}
const sourceLabel = iconItem.source === 'custom' ? t('icons.source_custom') : t('icons.source_core');
card.title = sourceLabel;
card.dataset.iconKey = iconItem.key || '';
card.dataset.iconTags = (iconItem.tags || []).join(' ');
card.innerHTML = `
<div class="icon-manager-card__preview">
<svg viewBox="${escapeHtml(iconItem.viewBox || '0 0 24 24')}" ${iconSvgAttributes(iconItem)}>${iconSvgMarkup(iconItem)}</svg>
</div>
<div class="icon-manager-card__body">
<strong>${escapeHtml(iconItem.key)}</strong>
<span>${escapeHtml(iconItem.label || '')}</span>
</div>
<div class="icon-manager-card__actions">
<button type="button" class="icon-manager-action-btn" data-icon-edit="${escapeHtml(iconItem.key)}" title="${t('pages.edit')}" aria-label="${t('pages.edit')}">${icon('edit', 14, '2')}</button>
<button type="button" class="icon-manager-action-btn icon-manager-action-btn--danger" data-icon-delete="${escapeHtml(iconItem.key)}" title="${t('btn.delete')}" aria-label="${t('btn.delete')}"${iconItem.canDelete ? '' : ' disabled'}>${icon('trash', 14, '2')}</button>
</div>
`;
grid.appendChild(card);
});
grid.querySelectorAll('[data-icon-edit]').forEach(function(btn) {
btn.addEventListener('click', function() {
const iconItem = iconManagerData.find(function(item) { return item.key === btn.dataset.iconEdit; });
if (iconItem) openIconManagerModal(iconItem);
});
});
grid.querySelectorAll('[data-icon-delete]').forEach(function(btn) {
btn.addEventListener('click', function() {
const iconItem = iconManagerData.find(function(item) { return item.key === btn.dataset.iconDelete; });
if (iconItem) deleteIconManagerIcon(iconItem);
});
});
}
function openIconManagerModal(iconItem) {
const isEdit = !!iconItem;
document.getElementById('iconManagerModalTitle').textContent = isEdit ? t('icons.edit_icon') : t('icons.add_icon');
document.getElementById('iconManagerOldKey').value = isEdit ? iconItem.key : '';
document.getElementById('iconManagerKey').value = isEdit ? iconItem.key : '';
document.getElementById('iconManagerLabel').value = isEdit ? (iconItem.label || '') : '';
document.getElementById('iconManagerTags').value = isEdit ? (iconItem.tags || []).join(', ') : '';
document.getElementById('iconManagerViewBox').value = isEdit ? (iconItem.viewBox || '0 0 24 24') : '0 0 24 24';
document.getElementById('iconManagerSvg').value = isEdit ? (iconItem.svg || '') : '';
document.getElementById('iconRenameWarning').hidden = true;
updateIconManagerPreview();
closeAllComboboxes();
document.getElementById('iconManagerModalOverlay').style.display = 'flex';
}
function closeIconManagerModal() {
document.getElementById('iconManagerModalOverlay').style.display = 'none';
}
// Custom icon-set picker (replaces native <select> to avoid modal overlay clipping)
(function() {
var picker = document.getElementById('iconifyImportSetPicker');
if (!picker) return;
var btn = document.getElementById('iconifyImportSetBtn');
var label = document.getElementById('iconifyImportSetLabel2');
var list = document.getElementById('iconifyImportSetList');
var hidden = document.getElementById('iconifyImportSet');
function openPicker() {
var rect = btn.getBoundingClientRect();
list.style.top = (rect.bottom + 4) + 'px';
list.style.left = rect.left + 'px';
list.style.width = rect.width + 'px';
list.hidden = false;
btn.setAttribute('aria-expanded', 'true');
}
function closePicker() {
list.hidden = true;
btn.setAttribute('aria-expanded', 'false');
}
btn.addEventListener('click', function(e) {
e.stopPropagation();
list.hidden ? openPicker() : closePicker();
});
list.addEventListener('click', function(e) {
var opt = e.target.closest('[role="option"]');
if (!opt) return;
list.querySelectorAll('[role="option"]').forEach(function(o) { o.setAttribute('aria-selected', 'false'); });
opt.setAttribute('aria-selected', 'true');
hidden.value = opt.dataset.value;
label.textContent = opt.textContent;
closePicker();
updateIconifyImportLicense();
});
document.addEventListener('click', function(e) {
if (!picker.contains(e.target)) closePicker();
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && !list.hidden) closePicker();
});
})();
function openIconifyImportModal() {
const overlay = document.getElementById('iconifyImportModalOverlay');
if (!overlay) return;
closeAllComboboxes();
// Also close icon-set picker if open
var setList = document.getElementById('iconifyImportSetList');
if (setList) setList.hidden = true;
overlay.style.display = 'flex';
updateIconifyImportLicense();
setTimeout(function() {
document.getElementById('iconifyImportQuery')?.focus();
}, 50);
}
function closeIconifyImportModal() {
document.getElementById('iconifyImportModalOverlay').style.display = 'none';
var setList = document.getElementById('iconifyImportSetList');
if (setList) setList.hidden = true;
}
function updateIconifyImportLicense() {
const prefix = document.getElementById('iconifyImportSet')?.value || 'lucide';
const setInfo = ICONIFY_IMPORT_SETS[prefix] || ICONIFY_IMPORT_SETS.lucide;
const license = document.getElementById('iconifyImportLicense');
if (license) {
license.innerHTML = escapeHtml(t('icons.import_license_hint', {
set: setInfo.label,
license: setInfo.license
}));
}
}
async function searchIconifyImport() {
const prefix = document.getElementById('iconifyImportSet')?.value || 'lucide';
const query = document.getElementById('iconifyImportQuery')?.value.trim() || '';
const results = document.getElementById('iconifyImportResults');
const empty = document.getElementById('iconifyImportEmpty');
const button = document.getElementById('iconifyImportSearchBtn');
if (!results || query.length < 2) {
showToast(t('icons.import_query_short'), 'error');
return;
}
results.innerHTML = '<p class="form-hint">' + escapeHtml(t('icons.import_loading')) + '</p>';
if (empty) empty.style.display = 'none';
if (button) button.disabled = true;
try {
const url = 'api.php?action=iconify-search&csrf_token=' + encodeURIComponent(CSRF_TOKEN)
+ '&prefix=' + encodeURIComponent(prefix)
+ '&query=' + encodeURIComponent(query);
const result = await fetchJsonWithTimeout(url, {}, 15000);
if (!result.success) {
showToast(result.message, 'error');
results.innerHTML = '';
return;
}
renderIconifyImportResults(result.data.icons || []);
} catch (error) {
showToast(t('icons.import_error', {message: error.message}), 'error');
results.innerHTML = '';
} finally {
if (button) button.disabled = false;
}
}
function renderIconifyImportResults(icons) {
const results = document.getElementById('iconifyImportResults');
const empty = document.getElementById('iconifyImportEmpty');
if (!results) return;
results.innerHTML = '';
if (!icons.length) {
if (empty) empty.style.display = '';
return;
}
if (empty) empty.style.display = 'none';
icons.forEach(function(iconItem) {
const card = document.createElement('article');
card.className = 'iconify-import-card';
card.innerHTML = `
<div class="iconify-import-card__preview">
<svg viewBox="${escapeHtml(iconItem.viewBox || '0 0 24 24')}" ${iconSvgAttributes(iconItem)}>${iconSvgMarkup(iconItem)}</svg>
</div>
<div class="iconify-import-card__body">
<strong>${escapeHtml(iconItem.name || '')}</strong>
<span>${escapeHtml(iconItem.setLabel || iconItem.prefix || '')} · ${escapeHtml(iconItem.license || '')}</span>
<code>${escapeHtml(iconItem.key || '')}</code>
</div>
<button type="button" class="btn btn-secondary btn-sm" data-iconify-import="${escapeHtml(iconItem.full || '')}" data-iconify-key="${escapeHtml(iconItem.key || '')}">${iconManagerData.some(item => item.key === iconItem.key) ? t('icons.imported_short') : t('icons.import')}</button>
`;
if (iconManagerData.some(item => item.key === iconItem.key)) {
card.querySelector('[data-iconify-import]').disabled = true;
card.classList.add('iconify-import-card--imported');
}
results.appendChild(card);
});
results.querySelectorAll('[data-iconify-import]').forEach(function(button) {
button.addEventListener('click', function() {
importIconifyIcon(button.dataset.iconifyImport, button);
});
});
}
async function importIconifyIcon(fullName, button) {
const formData = new FormData();
formData.append('action', 'iconify-import');
formData.append('csrf_token', CSRF_TOKEN);
formData.append('icon', fullName);
if (button) button.disabled = true;
try {
const result = await fetchJsonWithTimeout('api.php', { method: 'POST', body: formData }, 15000);
if (!result.success) {
showToast(result.message, 'error');
return;
}
iconManagerSortMode = 'newest';
const sortInput = document.getElementById('iconManagerSort');
if (sortInput) sortInput.value = 'newest';
const importedKey = String(fullName || '').replace(':', '-');
highlightIconManagerCard(importedKey);
renderIconManager(result.data);
showToast(t('icons.imported'), 'success');
if (button) {
button.textContent = t('icons.imported_short');
button.disabled = true;
button.closest('.iconify-import-card')?.classList.add('iconify-import-card--imported');
}
} catch (error) {
showToast(t('icons.import_error', {message: error.message}), 'error');
} finally {
if (button && button.textContent !== t('icons.imported_short')) button.disabled = false;
}
}
function updateIconManagerPreview() {
const preview = document.getElementById('iconManagerPreview');
const svgInput = document.getElementById('iconManagerSvg');
const viewBoxInput = document.getElementById('iconManagerViewBox');
if (preview) {
preview.setAttribute('viewBox', normalizeIconViewBoxClient(viewBoxInput?.value || ''));
preview.innerHTML = sanitizeIconSvgClient(svgInput.value);
}
const oldKey = document.getElementById('iconManagerOldKey').value;
const newKey = document.getElementById('iconManagerKey').value;
const warning = document.getElementById('iconRenameWarning');
if (warning) warning.hidden = !(oldKey && newKey && oldKey !== newKey);
}
document.getElementById('iconManagerSvg')?.addEventListener('input', function() {
const viewBoxInput = document.getElementById('iconManagerViewBox');
const extractedViewBox = extractIconViewBoxClient(this.value);
if (extractedViewBox && viewBoxInput && (!viewBoxInput.value.trim() || normalizeIconViewBoxClient(viewBoxInput.value) === '0 0 24 24')) {
viewBoxInput.value = extractedViewBox;
}
updateIconManagerPreview();
});
document.getElementById('iconManagerViewBox')?.addEventListener('input', updateIconManagerPreview);
document.getElementById('iconManagerKey')?.addEventListener('input', updateIconManagerPreview);
document.getElementById('iconManagerCleanupBtn')?.addEventListener('click', function() {
const input = document.getElementById('iconManagerSvg');
const extractedViewBox = extractIconViewBoxClient(input.value);
if (extractedViewBox) document.getElementById('iconManagerViewBox').value = extractedViewBox;
input.value = sanitizeIconSvgClient(input.value);
updateIconManagerPreview();
showToast(t('icons.cleanup_done'), 'success');
});
document.getElementById('iconManagerCurrentColorBtn')?.addEventListener('click', function() {
const input = document.getElementById('iconManagerSvg');
input.value = forceIconCurrentColor(input.value);
updateIconManagerPreview();
showToast(t('icons.current_color_done'), 'success');
});
document.getElementById('iconManagerForm')?.addEventListener('submit', async function(e) {
e.preventDefault();
const oldKey = document.getElementById('iconManagerOldKey').value;
const newKey = document.getElementById('iconManagerKey').value.trim();
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(newKey)) {
showToast('Invalid icon key. Use lowercase letters, numbers, hyphens, and underscores.', 'error');
return;
}
const formData = new FormData();
formData.append('action', 'save-icon');
formData.append('csrf_token', CSRF_TOKEN);
formData.append('old_key', oldKey);
formData.append('key', newKey);
formData.append('label', document.getElementById('iconManagerLabel').value);
formData.append('tags', document.getElementById('iconManagerTags').value);
formData.append('viewBox', document.getElementById('iconManagerViewBox').value);
formData.append('svg', document.getElementById('iconManagerSvg').value);
try {
const result = await fetchJsonWithTimeout('api.php', { method: 'POST', body: formData }, 15000);
if (result.success) {
closeIconManagerModal();
iconManagerSortMode = 'newest';
const sortInput = document.getElementById('iconManagerSort');
if (sortInput) sortInput.value = 'newest';
highlightIconManagerCard(newKey);
renderIconManager(result.data);
showToast(t('icons.saved'), 'success');
} else {
showToast(result.message, 'error');
}
} catch (error) {
showToast(t('icons.save_error', {message: error.message}), 'error');
}
});
document.getElementById('iconifyImportSet')?.addEventListener('change', function() {
updateIconifyImportLicense();
const query = document.getElementById('iconifyImportQuery')?.value.trim() || '';
if (query.length >= 2) searchIconifyImport();
});
document.getElementById('iconManagerSearch')?.addEventListener('input', function() {
iconManagerSearchTerm = this.value.trim();
dashboardListPages.icons = 1;
renderIconManager();
});
document.getElementById('iconManagerSort')?.addEventListener('change', function() {
iconManagerSortMode = this.value || 'alpha';
dashboardListPages.icons = 1;
renderIconManager();
});
document.getElementById('iconifyImportQuery')?.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
searchIconifyImport();
}
});
document.getElementById('iconifyImportSearchBtn')?.addEventListener('click', searchIconifyImport);
function deleteIconManagerIcon(iconItem) {
showModal(t('icons.delete_icon'), t('icons.delete_confirm', {key: iconItem.key}), async function() {
closeModal();
const formData = new FormData();
formData.append('action', 'delete-icon');
formData.append('csrf_token', CSRF_TOKEN);
formData.append('key', iconItem.key);
try {
const result = await fetchJsonWithTimeout('api.php', { method: 'POST', body: formData }, 15000);
if (result.success) {
renderIconManager(result.data);
showToast(t('icons.deleted'), 'success');
} else {
showToast(result.message, 'error');
}
} catch (error) {
showToast(t('icons.delete_error', {message: error.message}), 'error');
}
});
}
<?php endif; ?>