// varlog — app.js document.addEventListener('DOMContentLoaded', function () { // ─── Auto-resize textareas ─────────────────────────────────────────────── document.querySelectorAll('textarea.form-control').forEach(function (ta) { function resize() { ta.style.height = 'auto'; ta.style.height = ta.scrollHeight + 'px'; } ta.addEventListener('input', resize); resize(); }); // ─── Ctrl+Enter : soumettre le formulaire ──────────────────────────────── var form = document.querySelector('form[method="POST"]'); if (form) { form.addEventListener('keydown', function (e) { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { form.submit(); } }); } // ─── Slug auto-génération ──────────────────────────────────────────────── const titleInput = document.getElementById('title'); const slugField = document.getElementById('slug'); const slugPreview = document.getElementById('slug-preview'); if (titleInput && slugField) { if (slugField.value !== '') slugField._auto = false; titleInput.addEventListener('input', function () { if (slugField._auto !== false) { const generated = slugify(this.value); slugField.value = generated; if (slugPreview) slugPreview.textContent = generated; } }); slugField.addEventListener('input', function () { this._auto = (this.value === ''); if (slugPreview) slugPreview.textContent = this.value; }); } function slugify(s) { const map = {'à':'a','â':'a','ä':'a','é':'e','è':'e','ê':'e','ë':'e','î':'i','ï':'i','ô':'o','ö':'o','ù':'u','û':'u','ü':'u','ç':'c','æ':'ae','œ':'oe'}; return s.toLowerCase().replace(/[àâäéèêëîïôöùûüçæœ]/g, c => map[c] || c) .replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); } // ─── Rôle : nom technique auto depuis le label ─────────────────────────── var roleLabelInput = document.getElementById('role-label'); var roleNameInput = document.getElementById('role-name'); if (roleLabelInput && roleNameInput) { roleLabelInput.addEventListener('input', function () { if (roleNameInput._manual) return; roleNameInput.value = slugify(this.value); }); roleNameInput.addEventListener('input', function () { this._manual = (this.value !== ''); }); roleNameInput.addEventListener('blur', function () { if (this.value === '') { this._manual = false; this.value = slugify(roleLabelInput.value); } }); } // ─── Aperçu couleur catégorie ──────────────────────────────────────────── const KNOWN_CATS = { 'actualité': 10, 'travaux': 35, 'scolaire': 55, 'linux': 120, 'domotique': 160, 'télécom': 190, 'blog': 220, 'informatique': 255, 'réflexion': 285, 'loisirs': 320, 'perso': 345, }; const FREE_HUES = [87, 140, 205, 237, 302]; function gradient(hue) { return `linear-gradient(135deg,hsl(${hue},70%,88%) 0%,hsl(${hue},60%,28%) 100%)`; } function hashHue(str) { let h = 5381; for (let i = 0; i < str.length; i++) h = (((h << 5) + h) + str.charCodeAt(i)) | 0; return ((Math.abs(h) * 0.6180339887) * 360 | 0) % 360; } function nearestKnown(hue) { let best = null, bestDist = Infinity; for (const [name, h] of Object.entries(KNOWN_CATS)) { const d = Math.min(Math.abs(hue - h), 360 - Math.abs(hue - h)); if (d < bestDist) { bestDist = d; best = name; } } return { name: best, dist: bestDist }; } function updateCatPreview(val) { const key = val.trim().toLowerCase(); const swatch = document.getElementById('cat-swatch'); const hint = document.getElementById('cat-hint'); const freeEl = document.getElementById('cat-free-swatches'); if (!swatch) return; freeEl.innerHTML = ''; if (!key) { swatch.style.background = '#e5e7eb'; swatch.title = ''; hint.textContent = ''; return; } if (KNOWN_CATS[key] !== undefined) { const hue = KNOWN_CATS[key]; swatch.style.background = gradient(hue); swatch.title = `${hue}°`; hint.textContent = `Catégorie existante · teinte fixe (${hue}°)`; hint.className = 'text-muted d-block mt-1'; return; } const hue = hashHue(key); const { name, dist } = nearestKnown(hue); swatch.style.background = gradient(hue); swatch.title = `${hue}°`; if (dist < 20) { hint.innerHTML = `⚠ Teinte proche de ${name} (${dist}° d'écart) · couleurs disponibles :`; hint.className = 'text-warning d-block mt-1'; FREE_HUES.forEach(h => { const el = document.createElement('span'); el.title = `${h}°`; el.style.cssText = `display:inline-block;width:28px;height:20px;border-radius:4px;cursor:help;background:${gradient(h)}`; freeEl.appendChild(el); }); } else { hint.textContent = `Nouvelle catégorie · teinte libre (${hue}°)`; hint.className = 'text-muted d-block mt-1'; } } const catInput = document.getElementById('category'); if (catInput) { catInput.addEventListener('input', function () { updateCatPreview(this.value); }); updateCatPreview(catInput.value); } // ─── Copier la référence Markdown ──────────────────────────────────────── document.querySelectorAll('[data-copy-md-name]').forEach(function (btn) { btn.addEventListener('click', function () { const name = this.dataset.copyMdName; const isImage = this.dataset.copyMdIsImage === '1'; const ref = isImage ? `![](${name})` : `[${name}](${name})`; navigator.clipboard.writeText(ref).then(() => { const orig = this.textContent; this.textContent = 'Copié !'; setTimeout(() => { this.textContent = orig; }, 1500); }); }); }); // ─── Boîtes de confirmation (suppression) ─────────────────────────────── document.querySelectorAll('button[data-confirm], a[data-confirm]').forEach(function (el) { el.addEventListener('click', function (e) { if (!confirm(this.dataset.confirm)) e.preventDefault(); }); }); document.querySelectorAll('form[data-confirm]').forEach(function (form) { form.addEventListener('submit', function (e) { if (!confirm(this.dataset.confirm)) e.preventDefault(); }); }); // ─── Insérer une référence Markdown au curseur ─────────────────────────── const ta = document.getElementById('content'); if (ta) { ta._savedStart = null; ta._savedEnd = null; function saveCursor() { if (document.activeElement === ta) { ta._savedStart = ta.selectionStart; ta._savedEnd = ta.selectionEnd; } } document.addEventListener('mousedown', saveCursor); ta.addEventListener('keyup', saveCursor); ta.addEventListener('mouseup', saveCursor); document.querySelectorAll('[data-insert-ref]').forEach(function (el) { el.addEventListener('click', function () { insertRef(this.dataset.insertRef); }); if (el.tagName === 'IMG') { el.addEventListener('mouseenter', function () { this.style.borderColor = '#0d6efd'; }); el.addEventListener('mouseleave', function () { this.style.borderColor = 'transparent'; }); } }); } function insertRef(url) { if (!ta) return; const isImage = /\.(jpe?g|png|gif|webp|svg|avif)(\?.*)?$/i.test(url); const label = url.startsWith('http') ? (decodeURIComponent(url.split('/').pop().split('?')[0]) || url) : url; const ref = isImage ? `![](${url})` : `[${label}](${url})`; const len = ta.value.length; const start = ta._savedStart !== null ? ta._savedStart : len; const end = ta._savedEnd !== null ? ta._savedEnd : len; ta.focus(); ta.setRangeText(ref, start, end, 'end'); ta._savedStart = ta._savedEnd = start + ref.length; ta.dispatchEvent(new Event('input')); } // ─── Compteurs SEO ─────────────────────────────────────────────────────── function initCounter(inputId, counterId, max) { const input = document.getElementById(inputId); const counter = document.getElementById(counterId); if (!input || !counter) return; function update() { const len = input.value.length; counter.textContent = `${len} / ${max}`; counter.className = len > max ? 'text-danger' : 'text-muted'; } input.addEventListener('input', update); update(); } initCounter('seo_title', 'seo_title_counter', 60); initCounter('seo_description', 'seo_desc_counter', 155); // ─── Page catégories ───────────────────────────────────────────────────── function catComputeGradient(val) { const key = val.trim().toLowerCase(); if (!key) return null; if (KNOWN_CATS[key] !== undefined) return { hue: KNOWN_CATS[key], known: true }; const hue = hashHue(key); const { name, dist } = nearestKnown(hue); return { hue, known: false, conflict: dist < 20 ? name : null }; } document.querySelectorAll('form[action="/?action=rename_category"] input[name="new"]').forEach(function (input) { input.addEventListener('input', function () { const swatch = input.closest('form').querySelector('.rename-swatch'); const result = catComputeGradient(input.value); if (swatch) swatch.style.background = result ? gradient(result.hue) : '#e5e7eb'; }); }); const newCatInput = document.getElementById('new-cat-input'); if (newCatInput) { newCatInput.addEventListener('input', function () { const swatch = document.getElementById('new-cat-swatch'); const hint = document.getElementById('new-cat-hint'); const result = catComputeGradient(this.value); if (!result) { swatch.style.background = '#e5e7eb'; hint.textContent = ''; return; } swatch.style.background = gradient(result.hue); if (result.known) { hint.textContent = `Catégorie existante · teinte fixe (${result.hue}°)`; hint.className = 'text-muted d-block mb-3'; } else if (result.conflict) { hint.textContent = `⚠ Teinte proche de « ${result.conflict} » — choisissez un autre nom ou une couleur disponible ci-dessous`; hint.className = 'text-warning d-block mb-3'; } else { hint.textContent = `Couleur libre · teinte ${result.hue}°`; hint.className = 'text-success d-block mb-3'; } }); } // ─── Import image : récupérer les métadonnées ──────────────────────────── const fetchMetaBtn = document.getElementById('fetch-meta-btn'); if (fetchMetaBtn) { fetchMetaBtn.addEventListener('click', async function () { const urlInput = document.getElementById('import-url'); const resultDiv = document.getElementById('meta-result'); const url = urlInput ? urlInput.value.trim() : ''; if (!url) { resultDiv.innerHTML = 'Saisissez une URL d\'abord.'; return; } fetchMetaBtn.disabled = true; fetchMetaBtn.textContent = 'Chargement…'; resultDiv.innerHTML = ''; try { const res = await fetch(`/?action=fetch_file_meta&url=${encodeURIComponent(url)}`); const data = await res.json(); if (!data.ok) { resultDiv.innerHTML = `${data.error || 'Erreur lors de la récupération.'}`; return; } // Auto-remplissage dynamique des champs (si vides) const AUTOFILL = { img_author: { keys: ['author', 'credit'], label: 'Auteur / crédit' }, img_source: { keys: ['canonical', 'source'], label: 'URL source' }, }; const autofillKeys = new Set(); const autofillNotice = []; for (const [fieldName, cfg] of Object.entries(AUTOFILL)) { const f = document.querySelector(`input[name="${fieldName}"]`); if (!f || f.value) continue; for (const key of cfg.keys) { if (data[key]) { f.value = data[key]; autofillKeys.add(key); autofillNotice.push(`${cfg.label} : ${data[key]}`); break; } } } // Affichage dynamique de tous les champs retournés const isPdf = (data.mime === 'application/pdf'); const isHtml = (data.mime || '').startsWith('text/html'); const META_ORDER = ['mime','size','pages','page_size','pdf_version', 'width','site_name','og_type','language', 'title','description','author','subject','keywords', 'credit','source','creator','producer','date','camera','copyright', 'canonical','og_image']; const META_LABELS = { mime: 'Type', size: 'Taille', width: 'Dimensions', pages: 'Pages', page_size: 'Format', pdf_version: 'Version PDF', site_name: 'Site', og_type: 'Type OG', language: 'Langue', title: isPdf || isHtml ? 'Titre' : 'Titre EXIF/IPTC', author: isPdf || isHtml ? 'Auteur' : 'Auteur EXIF/IPTC', date: isPdf ? 'Créé le' : isHtml ? 'Publié le' : 'Prise de vue', description: 'Description', subject: 'Sujet', keywords: 'Mots-clés', credit: 'Crédit', source: 'Source IPTC', creator: 'Créé avec', producer: 'Produit par', camera: 'Appareil', copyright: 'Copyright', canonical: 'URL canonique', og_image: 'Image OG', }; function fmtVal(key, val) { if (key === 'size') return (val/1024).toFixed(0) + ' Ko' + (val >= 1048576 ? ` (${(val/1048576).toFixed(1)} Mo)` : ''); if (key === 'width') return `${data.width} × ${data.height} px`; if (key === 'og_image') return ``; if (key === 'canonical') return `${val}`; return String(val); } const SKIP = new Set(['ok', 'height']); const seen = new Set(); const rows = []; for (const key of META_ORDER) { const val = data[key]; if (val == null || val === '' || key === 'height') continue; seen.add(key); const badge = autofillKeys.has(key) ? ' ↓ pré-rempli' : ''; rows.push([META_LABELS[key] ?? key, fmtVal(key, val) + badge]); } for (const [key, val] of Object.entries(data)) { if (seen.has(key) || SKIP.has(key) || val == null || val === '') continue; rows.push([key, fmtVal(key, val)]); } let html = ''; if (rows.length > 0) { const trs = rows.map(([k, v]) => `${k}${v}` ).join(''); html = `${trs}
`; } else { html = 'Aucune métadonnée disponible pour ce fichier.'; } if (autofillNotice.length > 0) { html += `
✓ Pré-rempli — ${autofillNotice.join(' · ')}
`; } resultDiv.innerHTML = html; } catch { resultDiv.innerHTML = 'Erreur de connexion.'; } finally { fetchMetaBtn.disabled = false; fetchMetaBtn.textContent = 'Métadonnées'; } }); } // ─── Import image : toggle mode download ──────────────────────────────── document.querySelectorAll('input[name="mode"]').forEach(function (r) { r.addEventListener('change', function () { const dl = this.value === 'download'; const ss = this.value === 'screenshot'; const warn = document.getElementById('copyright-warning'); const fields = document.getElementById('download-fields'); if (warn) warn.style.display = dl ? 'block' : 'none'; if (fields) fields.style.display = (dl || ss) ? 'block' : 'none'; }); }); // ─── Données page (mode édition uniquement) ────────────────────────────── const pageEl = document.getElementById('vl-page'); if (!pageEl) return; const uuid = pageEl.dataset.uuid; const insertUrl = pageEl.dataset.insertUrl; // Auto-insertion après import d'image if (insertUrl && ta) { const isImage = /\.(jpe?g|png|gif|webp|svg|avif)(\?.*)?$/i.test(insertUrl); const name = decodeURIComponent(insertUrl.split('/').pop().split('?')[0]) || 'fichier'; const ref = isImage ? `![](${insertUrl})` : `[${name}](${insertUrl})`; const sep = ta.value.length > 0 && !ta.value.endsWith('\n') ? '\n' : ''; ta.value += sep + ref; ta.focus(); ta.selectionStart = ta.selectionEnd = ta.value.length; ta.dispatchEvent(new Event('input')); } // ─── Autosave ──────────────────────────────────────────────────────────── const indicator = document.getElementById('autosave-indicator'); if (!indicator || !uuid) return; let timer = null; function scheduleAutosave() { clearTimeout(timer); timer = setTimeout(doAutosave, 3000); } async function doAutosave() { const title = document.getElementById('title').value; const slug = document.getElementById('slug').value; const content = document.getElementById('content').value; indicator.textContent = 'Sauvegarde…'; try { const res = await fetch(`/?action=autosave&uuid=${encodeURIComponent(uuid)}`, { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: new URLSearchParams({title, slug, content}), }); const data = await res.json(); indicator.textContent = data.ok ? `Brouillon sauvegardé à ${data.time}` : 'Erreur de sauvegarde'; } catch { indicator.textContent = 'Erreur de sauvegarde'; } } ['title', 'slug', 'content'].forEach(id => { document.getElementById(id)?.addEventListener('input', scheduleAutosave); }); });