Ajoute 4 mécanismes moteur manquants pour le thème sécurité incendie : étiquette, carte, image SVG inline, bouton avec pièce jointe

Contenu et mécanisme uniquement, aucun style ajouté (voir consigne du
24/09/2026) : deux nouveaux kinds de contenu (badge/carte, rendu en
div brutes sans CSS), un mode SVG inline pour l'image (svg_markup,
nettoyé par un nouveau sanitizer allow-list avant chaque rendu) et un
fichier téléchargeable joignable à un bouton (upload/download routes,
stockage sous db.support_dir). Le futur système de templates portera
l'habillage visuel de ces éléments.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-24 08:12:29 +02:00
co-authored by Claude Sonnet 5
parent e7c6ed7159
commit a34bcf4159
15 changed files with 668 additions and 18 deletions
+74
View File
@@ -98,6 +98,16 @@ function forgeDocApiMoveToPage(id, targetPageId) {
}).then((r) => r.json());
}
function forgeDocApiUploadAttachment(id, file) {
// Multipart, jamais du JSON comme forgeDocApiUpdate ci-dessus — un
// fichier ne se sérialise pas dans le corps JSON d'un PATCH classique
// (voir routes/document/document_element_upload_attachment.py).
const body = new FormData();
body.set('file', file);
return fetch(`/document/${window.forgeDocState.slug}/elements/${id}/upload-attachment`, { method: 'POST', body })
.then((r) => r.json());
}
function forgeDocApiRender() {
const url = `/document/${window.forgeDocState.slug}/render?page_id=${window.forgeDocState.activePageId}`;
return fetch(url).then((r) => r.json());
@@ -778,6 +788,14 @@ function forgeDocBindCanvasInteractions() {
const elementId = Number.parseInt(dom.dataset.elementId, 10);
dom.addEventListener('click', (e) => {
e.stopPropagation();
// En Aperçu, un bouton portant un fichier joint déclenche son
// téléchargement plutôt que la sélection (réservée à l'édition) —
// même principe que les mini-jeux, jouables uniquement en Aperçu
// (voir static/document/document-editor.css, .docEditor3--preview).
if (window.forgeDocState.previewMode && dom.dataset.attachmentFilename) {
window.location.href = `/document/${window.forgeDocState.slug}/elements/${elementId}/download-attachment`;
return;
}
forgeDocSelectElement(elementId);
});
dom.setAttribute('draggable', 'true');
@@ -1649,24 +1667,78 @@ function forgeDocRenderImageProps(panel, el) {
${forgeDocPropsHeader('image')}
<div class="docField"><label for="docImageSrc">URL de l'image</label><input type="text" id="docImageSrc" value="${a.src || ''}" placeholder="https://..."></div>
<div class="docField"><label for="docImageAlt">Texte alternatif</label><input type="text" id="docImageAlt" value="${a.alt || ''}"></div>
<div class="docField"><label for="docImageSvg">Code SVG (prioritaire sur l'URL si renseigné)</label><textarea id="docImageSvg" rows="4">${forgeDocEscapeHtml(a.svg_markup || '')}</textarea></div>
${forgeDocDeleteButtonHtml()}
`;
function patch(partial) { forgeDocUpdateAttributes(el.id, { ...el.attributes, ...partial }); }
document.getElementById('docImageSrc').addEventListener('change', (e) => patch({ src: e.target.value.trim() }));
document.getElementById('docImageAlt').addEventListener('change', (e) => patch({ alt: e.target.value }));
document.getElementById('docImageSvg').addEventListener('change', (e) => patch({ svg_markup: e.target.value.trim() }));
}
function forgeDocRenderButtonProps(panel, el) {
const a = el.attributes;
const attachmentStatus = a.attachment_filename
? `Fichier joint : ${forgeDocEscapeHtml(a.attachment_filename)}`
: 'Aucun fichier joint';
panel.innerHTML = `
${forgeDocPropsHeader('bouton')}
<div class="docField"><label for="docButtonLabel">Texte du bouton</label><input type="text" id="docButtonLabel" value="${a.label || ''}"></div>
<div class="docField"><label for="docButtonTarget">Cible (URL ou ancre)</label><input type="text" id="docButtonTarget" value="${a.target || ''}"></div>
<div class="docField">
<span class="docFieldLabel">Fichier téléchargeable</span>
<div id="docButtonAttachmentStatus">${attachmentStatus}</div>
<input type="file" id="docButtonAttachmentInput">
</div>
${forgeDocDeleteButtonHtml()}
`;
function patch(partial) { forgeDocUpdateAttributes(el.id, { ...el.attributes, ...partial }); }
document.getElementById('docButtonLabel').addEventListener('change', (e) => patch({ label: e.target.value }));
document.getElementById('docButtonTarget').addEventListener('change', (e) => patch({ target: e.target.value.trim() }));
document.getElementById('docButtonAttachmentInput').addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
forgeDocApiUploadAttachment(el.id, file).then((result) => {
if (result.error) { alert(result.error); return; }
el.attributes = result.attributes;
forgeDocRenderProps(window.forgeDocState.elementsById[el.id]);
const dom = document.querySelector(`[data-element-id="${el.id}"]`);
if (dom && result.rendered_html) {
const wrapper = document.createElement('div');
wrapper.innerHTML = result.rendered_html;
dom.replaceWith(wrapper.firstElementChild);
forgeDocBindCanvasInteractions();
forgeDocApplySelectionClass();
}
});
});
}
function forgeDocRenderBadgeProps(panel, el) {
const a = el.attributes;
panel.innerHTML = `
${forgeDocPropsHeader('étiquette')}
<div class="docField"><label for="docBadgeContent">Contenu</label><input type="text" id="docBadgeContent" value="${forgeDocEscapeHtml(a.content || '')}"></div>
${forgeDocDeleteButtonHtml()}
`;
document.getElementById('docBadgeContent').addEventListener('change', (e) => {
forgeDocUpdateAttributes(el.id, { ...el.attributes, content: e.target.value });
});
}
function forgeDocRenderCarteProps(panel, el) {
const a = el.attributes;
panel.innerHTML = `
${forgeDocPropsHeader('carte')}
<div class="docField"><label for="docCardLabel">Repère (lettre, chiffre...)</label><input type="text" id="docCardLabel" value="${forgeDocEscapeHtml(a.label || '')}"></div>
<div class="docField"><label for="docCardTitle">Titre</label><input type="text" id="docCardTitle" value="${forgeDocEscapeHtml(a.title || '')}"></div>
<div class="docField"><label for="docCardDescription">Description</label><textarea id="docCardDescription" rows="3">${forgeDocEscapeHtml(a.description || '')}</textarea></div>
${forgeDocDeleteButtonHtml()}
`;
function patch(partial) { forgeDocUpdateAttributes(el.id, { ...el.attributes, ...partial }); }
document.getElementById('docCardLabel').addEventListener('change', (e) => patch({ label: e.target.value }));
document.getElementById('docCardTitle').addEventListener('change', (e) => patch({ title: e.target.value }));
document.getElementById('docCardDescription').addEventListener('change', (e) => patch({ description: e.target.value }));
}
function forgeDocRenderListItemHtml(item, index) {
@@ -2777,6 +2849,8 @@ function forgeDocRenderProps(el) {
else if (el.kind === 'image') forgeDocRenderImageProps(panel, el);
else if (el.kind === 'bouton') forgeDocRenderButtonProps(panel, el);
else if (el.kind === 'liste_puces' || el.kind === 'liste_numerotee') forgeDocRenderListProps(panel, el);
else if (el.kind === 'badge') forgeDocRenderBadgeProps(panel, el);
else if (el.kind === 'carte') forgeDocRenderCarteProps(panel, el);
else if (el.kind === 'row') forgeDocRenderRowProps(panel, el);
else if (el.kind === 'quiz') forgeDocRenderQuizProps(panel, el);
else if (el.kind === 'association') forgeDocRenderAssociationProps(panel, el);