(function () { 'use strict'; // Set to true to see debug output in the browser console var DEBUG = false; var usageCache = {}; var pendingRequests = {}; var debounceTimer = null; function log() { if (DEBUG) console.log.apply(console, ['[image-usage]'].concat(Array.prototype.slice.call(arguments))); } /** * Fetch usage data for a Kirby page ID (slug format, e.g. "community-led/inside"). * Returns a promise resolving to { filename: boolean }. */ function fetchUsage(pageId) { if (pageId in usageCache) return Promise.resolve(usageCache[pageId]); if (pageId in pendingRequests) return pendingRequests[pageId]; // Kirby panel encodes / as + in URLs var encoded = encodeURIComponent(pageId.replace(/\//g, '+')); log('Fetching usage for page:', pageId, '→', '/api/smart-forests/image-usage/' + encoded); var csrf = (window.panel && window.panel.system && window.panel.system.csrf) ? window.panel.system.csrf : false; var promise = fetch('/api/smart-forests/image-usage/' + encoded, { credentials: 'same-origin', headers: { 'x-csrf': csrf, 'x-fiber': 'true' } }) .then(function (r) { if (!r.ok) { log('API error', r.status, r.statusText); return {}; } return r.json(); }) .then(function (data) { log('Usage data for', pageId, data); usageCache[pageId] = data; delete pendingRequests[pageId]; return data; }) .catch(function (err) { log('Fetch failed:', err); usageCache[pageId] = {}; delete pendingRequests[pageId]; return {}; }); pendingRequests[pageId] = promise; return promise; } /** * Apply (or refresh) a usage badge on a file item element. * Skips DOM writes if the element is already in the correct state, * which prevents the MutationObserver from re-triggering indefinitely. */ function applyBadge(item, used) { var wantedState = used === true ? 'used' : used === false ? 'unused' : null; var currentState = item.dataset.sfUsage || null; // Already correct — skip to avoid triggering MutationObserver if (currentState === wantedState) return; // Remove existing badge item.querySelectorAll('.sf-usage-dot').forEach(function (el) { el.remove(); }); item.classList.remove('sf-is-used', 'sf-is-unused'); delete item.dataset.sfUsage; if (!wantedState) return; item.dataset.sfUsage = wantedState; item.classList.add('sf-is-' + wantedState); var dot = document.createElement('span'); dot.className = 'sf-usage-dot sf-usage-dot--' + wantedState; dot.setAttribute('title', used ? 'Utilisée dans le contenu' : 'Non utilisée dans le contenu'); var imageArea = item.querySelector('.k-item-image, .k-item-icon'); if (imageArea) { imageArea.appendChild(dot); } else { item.prepend(dot); } } var IMAGE_EXT = /\.(jpe?g|png|gif|webp|svg|avif)$/i; /** * Scan current DOM for file items, group by page, fetch usage, paint badges. * * File items in the panel have data-id = "{kirby-page-slug}/{filename}" * e.g. "community-led-forest-technologies/inside/image.jpg" */ function run() { // All k-item elements that have a data-id ending in an image extension var allItems = document.querySelectorAll('.k-item[data-id]'); log('Total .k-item[data-id] found:', allItems.length); var byPage = {}; allItems.forEach(function (item) { var dataId = item.getAttribute('data-id') || ''; if (!IMAGE_EXT.test(dataId)) return; // skip non-image items var parts = dataId.split('/'); if (parts.length < 2) return; var filename = parts[parts.length - 1]; var pageId = parts.slice(0, -1).join('/'); if (!byPage[pageId]) byPage[pageId] = []; byPage[pageId].push({ item: item, filename: filename }); }); var pageCount = Object.keys(byPage).length; log('Pages with image items:', pageCount, byPage); if (!pageCount) return; Object.keys(byPage).forEach(function (pageId) { fetchUsage(pageId).then(function (usage) { byPage[pageId].forEach(function (entry) { applyBadge(entry.item, usage[entry.filename]); }); }); }); } function schedule() { clearTimeout(debounceTimer); debounceTimer = setTimeout(run, 450); } new MutationObserver(schedule).observe(document.body, { childList: true, subtree: true }); schedule(); })();