Audit complet de mise en forme — Image (2e élément du tableau)
Implémente toutes les options manquantes identifiées pour l'élément Image : dimensionnement/ratio/object-fit, filtres CSS, upload de fichier (en plus de l'URL), lien/plein écran au clic, chargement différé, légende, et tous les attributs de boîte partagés déjà créés pour Titre/Paragraphe (padding/margin/fond/bordure/ombre/opacité/ position du bloc). Système de pages : un support peut désormais avoir 0 page (un nouveau support démarre vide), suppression de toutes les pages en un clic, et la pagination automatique insère intelligemment la nouvelle page juste après celle qui déborde plutôt qu'en toute fin de liste. Bugs réels trouvés et corrigés en cours de route : le style de bloc (dont align-self) ciblait l'élément interne au lieu de son enveloppe (légende/lien) ; une image à sa taille native pouvait déclencher une pagination infinie ; upload/mise à jour d'attribut ne déclenchaient jamais le contrôle de débordement. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
6c7675fad0
commit
9ad50c58b8
@@ -47,6 +47,14 @@ function forgeDocApiAdd(kind, parentId) {
|
||||
// définition à "la page actuellement affichée", inutile de le faire
|
||||
// remonter dans chacun des ~10 sites d'appel (glisser-déposer, moteur
|
||||
// de layout, Annuler/Rétablir...).
|
||||
// Point d'entrée UNIQUE de tout ajout : un support peut avoir 0 page
|
||||
// (retour utilisateur du 26/09/2026) — bloquer ICI, une fois, couvre
|
||||
// tous les appelants (bibliothèque, glisser-déposer, rangée créée à la
|
||||
// volée) sans dupliquer ce contrôle dans chacun.
|
||||
if (window.forgeDocState.activePageId === null) {
|
||||
alert('Ajoutez d\'abord une page avant d\'ajouter du contenu.');
|
||||
return Promise.reject(new Error('aucune page active'));
|
||||
}
|
||||
const body = new URLSearchParams();
|
||||
body.set('kind', kind);
|
||||
body.set('page_id', String(window.forgeDocState.activePageId));
|
||||
@@ -108,6 +116,16 @@ function forgeDocApiUploadAttachment(id, file) {
|
||||
.then((r) => r.json());
|
||||
}
|
||||
|
||||
function forgeDocApiUploadImage(id, file) {
|
||||
// Mirroir de forgeDocApiUploadAttachment ci-dessus, mais route dédiée
|
||||
// au kind "image" (voir routes/document/document_element_upload_image.py) :
|
||||
// stocke sous uploads/ (jamais attachments/) et vide svg_markup côté serveur.
|
||||
const body = new FormData();
|
||||
body.set('file', file);
|
||||
return fetch(`/document/${window.forgeDocState.slug}/elements/${id}/upload-image`, { 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());
|
||||
@@ -156,18 +174,87 @@ function forgeDocCheckPageOverflow() {
|
||||
});
|
||||
if (splitIndex === -1) return Promise.resolve();
|
||||
|
||||
const overflowingEl = topLevel[splitIndex];
|
||||
const FORGE_DOC_MIN_IMAGE_HEIGHT = 60;
|
||||
if (overflowingEl.kind === 'image') {
|
||||
// Retour utilisateur du 26/09/2026 : "si l'image uploadée est trop
|
||||
// grande je préfère qu'elle soit redimensionnée plutôt que bouger sur
|
||||
// une autre page" — une image qui déborde est rétrécie SUR PLACE à
|
||||
// l'espace réellement disponible sous elle, plutôt que paginée.
|
||||
// Ajustement purement VISUEL (jamais persisté en attribut), recalculé
|
||||
// à chaque rafraîchissement du canevas (voir forgeDocRefreshCanvas).
|
||||
//
|
||||
// MAIS uniquement s'il reste assez de place pour un résultat encore
|
||||
// utilisable (retour utilisateur suivant : "le contenu continue de se
|
||||
// mettre même quand il n'y a plus de place [...] aucune page ne se
|
||||
// crée automatiquement" — une page déjà pleine d'images empilait des
|
||||
// vignettes rétrécies à l'infini plutôt que de basculer sur une
|
||||
// nouvelle page) : sous FORGE_DOC_MIN_IMAGE_HEIGHT, il ne reste plus
|
||||
// de place RÉELLE sur cette page, pas seulement pour cette image —
|
||||
// on tombe alors dans la pagination normale ci-dessous, comme
|
||||
// n'importe quel autre kind.
|
||||
const dom = document.querySelector(`#docPage [data-element-id="${overflowingEl.id}"]`);
|
||||
if (dom) {
|
||||
const figure = dom.closest('.docImageFigure');
|
||||
const caption = figure ? figure.querySelector('.docImageCaption') : null;
|
||||
const captionSpace = caption ? caption.getBoundingClientRect().height + 8 : 0;
|
||||
const available = Math.floor(pageBottom - dom.getBoundingClientRect().top - captionSpace - 2);
|
||||
if (available >= FORGE_DOC_MIN_IMAGE_HEIGHT) {
|
||||
dom.style.maxHeight = `${available}px`;
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const overflowing = topLevel.slice(splitIndex);
|
||||
// Garde-fou contre une boucle infinie (bug réel constaté le 26/09/2026 :
|
||||
// "quand j'ajoute une image ça crée des pages à l'infini") — si TOUT le
|
||||
// top-niveau de la page déborde déjà, le déplacer sur une page neuve ne
|
||||
// fait que reproduire EXACTEMENT le même débordement là-bas, à l'infini.
|
||||
// Rien à répartir dans ce cas : on laisse le contenu tel quel (clipsé
|
||||
// par .docPage { overflow: hidden }, voir sa règle CSS) — au créateur de
|
||||
// redimensionner l'élément lui-même.
|
||||
if (overflowing.length === topLevel.length) return Promise.resolve();
|
||||
|
||||
// La page neuve se glisse JUSTE APRÈS la page qui déborde (retour
|
||||
// utilisateur du 26/09/2026 : "la nouvelle page dois etre sous la page
|
||||
// qui deborde meme si ya d'autre page") — jamais ajoutée en toute fin
|
||||
// de la bande d'onglets s'il existe déjà d'autres pages après celle-ci
|
||||
// (ex. déborder sur la page 2 d'un support qui en compte 5 insère la
|
||||
// nouvelle page en position 3, décale les anciennes 3/4/5 en 4/5/6).
|
||||
const insertIndex = forgeDocOrderedPages().findIndex((p) => p.id === window.forgeDocState.activePageId) + 1;
|
||||
return forgeDocApiPageAdd().then((result) => {
|
||||
window.forgeDocState.pages.push({
|
||||
id: result.id, title: result.title, order_index: window.forgeDocState.pages.length,
|
||||
});
|
||||
return overflowing
|
||||
.reduce((chain, el) => chain.then(() => forgeDocApiMoveToPage(el.id, result.id)), Promise.resolve())
|
||||
return forgeDocApiPageMove(result.id, insertIndex)
|
||||
.then(() => forgeDocReorderLocalPage(result.id, insertIndex))
|
||||
.then(() => overflowing.reduce(
|
||||
(chain, el) => chain.then(() => forgeDocApiMoveToPage(el.id, result.id)),
|
||||
Promise.resolve(),
|
||||
))
|
||||
.then(() => forgeDocSwitchPage(result.id));
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocRenderEmptyPageState() {
|
||||
// Support à 0 page (retour utilisateur du 26/09/2026) — jamais appeler
|
||||
// /render sans page_id (404 côté serveur, voir document_render.py) :
|
||||
// le canevas affiche directement une invite à créer une page.
|
||||
const contentEl = document.getElementById('docPageContent');
|
||||
contentEl.innerHTML = '<div class="docEmptyState docEmptyPageState">Aucune page — cliquez sur « + Ajouter une page » pour commencer.</div>';
|
||||
contentEl.dataset.verticalAlign = 'top';
|
||||
window.forgeDocState.elements = [];
|
||||
forgeDocRebuildIndex();
|
||||
window.forgeDocState.selectedId = null;
|
||||
forgeDocRefreshRightPanelDefault();
|
||||
}
|
||||
|
||||
function forgeDocRefreshCanvas() {
|
||||
if (window.forgeDocState.activePageId === null) {
|
||||
forgeDocRenderEmptyPageState();
|
||||
return Promise.resolve();
|
||||
}
|
||||
return forgeDocApiRender().then((data) => {
|
||||
const contentEl = document.getElementById('docPageContent');
|
||||
contentEl.innerHTML = data.html;
|
||||
@@ -293,6 +380,17 @@ function forgeDocUpdateAttributes(id, newAttributes) {
|
||||
if (window.forgeDocState.selectedId === id) {
|
||||
forgeDocRenderProps(el);
|
||||
}
|
||||
// Bug réel constaté le 26/09/2026 : uploader une image trop grande
|
||||
// (ou tout autre changement d'attribut qui agrandit un élément) ne
|
||||
// déclenchait JAMAIS le rétrécissement/la pagination — seules les
|
||||
// mutations passant par forgeDocRefreshCanvas (ajout/déplacement/
|
||||
// suppression/Annuler-Rétablir) le faisaient. Cette fonction est le
|
||||
// point d'entrée UNIQUE de toute mise à jour d'attributs (voir
|
||||
// commentaire ci-dessus) : vérifier ICI, une fois, couvre aussi
|
||||
// l'upload d'image (voir forgeDocApiUploadImage plus bas, qui route
|
||||
// son propre patch DOM à travers ce même point) sans dupliquer
|
||||
// l'appel dans chaque site qui modifie un attribut.
|
||||
return forgeDocCheckPageOverflow();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -523,6 +621,11 @@ function forgeDocApiPageDelete(pageId) {
|
||||
.then((r) => r.json());
|
||||
}
|
||||
|
||||
function forgeDocApiPageDeleteAll() {
|
||||
return fetch(`/document/${window.forgeDocState.slug}/pages/delete-all`, { method: 'POST' })
|
||||
.then((r) => r.json());
|
||||
}
|
||||
|
||||
function forgeDocApiPageMove(pageId, newIndex) {
|
||||
return fetch(`/document/${window.forgeDocState.slug}/pages/${pageId}/move`, {
|
||||
method: 'POST',
|
||||
@@ -543,6 +646,22 @@ function forgeDocOrderedPages() {
|
||||
return [...window.forgeDocState.pages].sort((a, b) => a.order_index - b.order_index);
|
||||
}
|
||||
|
||||
function forgeDocReorderLocalPage(movedId, newIndex) {
|
||||
// Reflète côté client un déplacement déjà appliqué côté serveur (voir
|
||||
// move_document_page.py : renumérotation intégrale de order_index sur
|
||||
// TOUTES les pages) — mêmes calculs, jamais dupliqués autrement que
|
||||
// dans cette seule fonction (glisser-déposer du panneau Pages ET
|
||||
// pagination automatique, voir forgeDocCheckPageOverflow).
|
||||
const moved = window.forgeDocState.pages.find((p) => p.id === movedId);
|
||||
const rest = window.forgeDocState.pages
|
||||
.filter((p) => p.id !== movedId)
|
||||
.sort((a, b) => a.order_index - b.order_index);
|
||||
rest.splice(newIndex, 0, moved);
|
||||
rest.forEach((p, i) => { p.order_index = i; });
|
||||
window.forgeDocState.pages = rest;
|
||||
forgeDocRenderPageManagerList();
|
||||
}
|
||||
|
||||
function forgeDocUpdatePageNavPill() {
|
||||
const pill = document.getElementById('docPageNavPill');
|
||||
if (!pill) return;
|
||||
@@ -599,10 +718,9 @@ function forgeDocSetPageVerticalAlign(pageId, verticalAlign) {
|
||||
}
|
||||
|
||||
function forgeDocDeletePage(pageId) {
|
||||
// Filet de sécurité client (en plus du refus serveur, voir
|
||||
// routes/document/document_page_delete.py) : jamais même tenter de
|
||||
// supprimer la dernière page restante.
|
||||
if (window.forgeDocState.pages.length <= 1) return Promise.resolve();
|
||||
// Supprimer la dernière page restante est désormais autorisé (retour
|
||||
// utilisateur du 26/09/2026 : un support à 0 page est un état valide,
|
||||
// voir document_engine/pages/pages.md) — plus de garde-fou ici.
|
||||
return forgeDocApiPageDelete(pageId).then((result) => {
|
||||
if (result.error) {
|
||||
alert(result.error);
|
||||
@@ -610,13 +728,30 @@ function forgeDocDeletePage(pageId) {
|
||||
}
|
||||
window.forgeDocState.pages = window.forgeDocState.pages.filter((p) => p.id !== pageId);
|
||||
if (window.forgeDocState.activePageId === pageId) {
|
||||
return forgeDocSwitchPage(forgeDocOrderedPages()[0].id);
|
||||
const remaining = forgeDocOrderedPages();
|
||||
return forgeDocSwitchPage(remaining.length ? remaining[0].id : null);
|
||||
}
|
||||
forgeDocRenderPageManagerList();
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocDeleteAllPages() {
|
||||
// Action destructive et irréversible (retour utilisateur : "une option
|
||||
// dans page pour supprimer toute les page d'un coup") — confirmation
|
||||
// native obligatoire avant tout appel serveur.
|
||||
if (!window.confirm('Supprimer TOUTES les pages et tout leur contenu ? Cette action est irréversible.')) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return forgeDocApiPageDeleteAll().then(() => {
|
||||
window.forgeDocState.pages = [];
|
||||
window.forgeDocState.undoStack = [];
|
||||
window.forgeDocState.redoStack = [];
|
||||
forgeDocUpdateHistoryButtons();
|
||||
return forgeDocSwitchPage(null);
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocSelectAllText(el) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(el);
|
||||
@@ -701,7 +836,7 @@ function forgeDocRenderPageManagerList() {
|
||||
<span class="docPageRowTitle"><span class="docPageRowTitleText">${forgeDocEscapeHtml(p.title)}</span></span>
|
||||
<span class="docPageRowActions">
|
||||
<button type="button" class="docPageRowRename" data-page-id="${p.id}" aria-label="Renommer cette page" title="Renommer cette page">✎</button>
|
||||
<button type="button" class="docPageRowDelete" data-page-id="${p.id}" aria-label="Supprimer cette page" title="Supprimer cette page"${pages.length <= 1 ? ' disabled' : ''}>✕</button>
|
||||
<button type="button" class="docPageRowDelete" data-page-id="${p.id}" aria-label="Supprimer cette page" title="Supprimer cette page">✕</button>
|
||||
</span>
|
||||
</div>
|
||||
`).join('');
|
||||
@@ -733,6 +868,12 @@ function forgeDocRenderPageManagerList() {
|
||||
|
||||
const addBtn = document.getElementById('docPageManagerAdd');
|
||||
if (addBtn) addBtn.onclick = () => forgeDocAddPage();
|
||||
|
||||
const deleteAllBtn = document.getElementById('docPageManagerDeleteAll');
|
||||
if (deleteAllBtn) {
|
||||
deleteAllBtn.disabled = pages.length === 0;
|
||||
deleteAllBtn.onclick = () => forgeDocDeleteAllPages();
|
||||
}
|
||||
}
|
||||
|
||||
function forgeDocBindPageManagerDrag() {
|
||||
@@ -769,16 +910,7 @@ function forgeDocBindPageManagerDrag() {
|
||||
const movedId = draggedId;
|
||||
draggedId = null;
|
||||
if (newIndex < 0) return;
|
||||
forgeDocApiPageMove(movedId, newIndex).then(() => {
|
||||
const moved = window.forgeDocState.pages.find((p) => p.id === movedId);
|
||||
const rest = window.forgeDocState.pages
|
||||
.filter((p) => p.id !== movedId)
|
||||
.sort((a, b) => a.order_index - b.order_index);
|
||||
rest.splice(newIndex, 0, moved);
|
||||
rest.forEach((p, i) => { p.order_index = i; });
|
||||
window.forgeDocState.pages = rest;
|
||||
forgeDocRenderPageManagerList();
|
||||
});
|
||||
forgeDocApiPageMove(movedId, newIndex).then(() => forgeDocReorderLocalPage(movedId, newIndex));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -826,6 +958,35 @@ function forgeDocBindPageNavPill() {
|
||||
* Sélection & panneau Propriétés.
|
||||
* ------------------------------------------------------------------- */
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Aperçu plein écran d'une image (comportement "click_behavior":
|
||||
* "lightbox", voir document_engine/rendering/render_document_element.py
|
||||
* ::_render_image) — un seul overlay réutilisé, créé à la demande.
|
||||
* ------------------------------------------------------------------- */
|
||||
|
||||
function forgeDocOpenImageLightbox(src, alt) {
|
||||
let overlay = document.getElementById('docImageLightboxOverlay');
|
||||
if (!overlay) {
|
||||
overlay = document.createElement('div');
|
||||
overlay.id = 'docImageLightboxOverlay';
|
||||
overlay.className = 'docImageLightboxOverlay';
|
||||
overlay.innerHTML = '<img class="docImageLightboxImg" id="docImageLightboxImg" alt="">';
|
||||
overlay.addEventListener('click', forgeDocCloseImageLightbox);
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') forgeDocCloseImageLightbox();
|
||||
});
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
document.getElementById('docImageLightboxImg').src = src;
|
||||
document.getElementById('docImageLightboxImg').alt = alt || '';
|
||||
overlay.classList.add('is-open');
|
||||
}
|
||||
|
||||
function forgeDocCloseImageLightbox() {
|
||||
const overlay = document.getElementById('docImageLightboxOverlay');
|
||||
if (overlay) overlay.classList.remove('is-open');
|
||||
}
|
||||
|
||||
function forgeDocBindCanvasInteractions() {
|
||||
document.querySelectorAll('#docPage [data-element-id]').forEach((dom) => {
|
||||
const elementId = Number.parseInt(dom.dataset.elementId, 10);
|
||||
@@ -839,6 +1000,22 @@ function forgeDocBindCanvasInteractions() {
|
||||
window.location.href = `/document/${window.forgeDocState.slug}/elements/${elementId}/download-attachment`;
|
||||
return;
|
||||
}
|
||||
// Image "plein écran au clic" — seulement en Aperçu (même
|
||||
// principe que ci-dessus) ; en édition, on bloque juste la
|
||||
// navigation native éventuelle (lien/plein écran) pour rester sur
|
||||
// la sélection de l'élément.
|
||||
const lightboxWrapper = dom.closest('.docImageLightboxTrigger');
|
||||
if (lightboxWrapper) {
|
||||
e.preventDefault();
|
||||
if (window.forgeDocState.previewMode) {
|
||||
const img = lightboxWrapper.querySelector('img.docImage');
|
||||
if (img) forgeDocOpenImageLightbox(img.src, img.alt);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (dom.closest('.docImageLink') && !window.forgeDocState.previewMode) {
|
||||
e.preventDefault();
|
||||
}
|
||||
forgeDocSelectElement(elementId);
|
||||
});
|
||||
dom.setAttribute('draggable', 'true');
|
||||
@@ -1807,6 +1984,12 @@ function forgeDocRenderBoxFieldsHtml(a, idPrefix) {
|
||||
</div>
|
||||
${forgeDocRenderToggleSliderHtml(`${idPrefix}Radius`, 'Arrondi des bords', a.border_radius, {
|
||||
min: 0, max: 100, step: 2, unit: 'px', fallback: 8,
|
||||
})}
|
||||
${forgeDocRenderToggleSliderHtml(`${idPrefix}Width`, 'Largeur fixe', a.width, {
|
||||
min: 50, max: 1200, step: 10, unit: 'px', fallback: 300,
|
||||
})}
|
||||
${forgeDocRenderToggleSliderHtml(`${idPrefix}MaxWidth`, 'Largeur maximale', a.max_width, {
|
||||
min: 200, max: 1200, step: 20, unit: 'px', fallback: 600,
|
||||
})}
|
||||
${forgeDocRenderToggleSliderHtml(`${idPrefix}Height`, 'Hauteur fixe', a.height, {
|
||||
min: 0, max: 800, step: 10, unit: 'px', fallback: 200,
|
||||
@@ -1859,6 +2042,8 @@ function forgeDocBindBoxFields(panel, el, idPrefix, patch) {
|
||||
forgeDocBindToggleSlider(`${idPrefix}Padding`, { unit: 'px' }, (v) => patch({ padding: v }));
|
||||
forgeDocBindToggleSlider(`${idPrefix}Margin`, { unit: 'px' }, (v) => patch({ margin: v }));
|
||||
forgeDocBindToggleSlider(`${idPrefix}Radius`, { unit: 'px' }, (v) => patch({ border_radius: v }));
|
||||
forgeDocBindToggleSlider(`${idPrefix}Width`, { unit: 'px' }, (v) => patch({ width: v }));
|
||||
forgeDocBindToggleSlider(`${idPrefix}MaxWidth`, { unit: 'px' }, (v) => patch({ max_width: v }));
|
||||
forgeDocBindToggleSlider(`${idPrefix}Height`, { unit: 'px' }, (v) => patch({ height: v }));
|
||||
forgeDocBindToggleSlider(`${idPrefix}MinHeight`, { unit: 'px' }, (v) => patch({ min_height: v }));
|
||||
forgeDocBindToggleSlider(`${idPrefix}MaxHeight`, { unit: 'px' }, (v) => patch({ max_height: v }));
|
||||
@@ -1959,9 +2144,6 @@ function forgeDocRenderTextProps(panel, el) {
|
||||
${FORGE_DOC_TEXT_SHADOW_PRESETS.map(([v, l]) => `<button type="button" class="docSegBtn ${(a.text_shadow || '') === v ? 'is-active' : ''}" data-shadow="${forgeDocEscapeHtml(v)}">${l}</button>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
${forgeDocRenderToggleSliderHtml('docMaxWidth', 'Largeur maximale', a.max_width, {
|
||||
min: 200, max: 1200, step: 20, unit: 'px', fallback: 600,
|
||||
})}
|
||||
${forgeDocRenderBoxFieldsHtml(a, 'docText')}
|
||||
${forgeDocDeleteButtonHtml()}
|
||||
`;
|
||||
@@ -1969,7 +2151,6 @@ function forgeDocRenderTextProps(panel, el) {
|
||||
function patch(partial) { forgeDocUpdateAttributes(el.id, { ...el.attributes, ...partial }); }
|
||||
|
||||
document.getElementById('docContentInput').addEventListener('change', (e) => patch({ content: e.target.value }));
|
||||
forgeDocBindToggleSlider('docMaxWidth', { unit: 'px' }, (v) => patch({ max_width: v }));
|
||||
panel.querySelectorAll('#docStyleSeg .docSegBtn').forEach((btn) => {
|
||||
btn.addEventListener('click', () => patch({ style: btn.dataset.style }));
|
||||
});
|
||||
@@ -1994,19 +2175,111 @@ function forgeDocRenderTextProps(panel, el) {
|
||||
forgeDocBindBoxFields(panel, el, 'docText', patch);
|
||||
}
|
||||
|
||||
const FORGE_DOC_OBJECT_FIT_OPTIONS = [
|
||||
['', 'Taille réelle'], ['cover', 'Couvrir'], ['contain', 'Contenir'], ['fill', 'Étirer'],
|
||||
];
|
||||
const FORGE_DOC_ASPECT_RATIO_OPTIONS = [
|
||||
['', 'Libre'], ['1 / 1', 'Carré'], ['4 / 3', '4:3'], ['16 / 9', '16:9'],
|
||||
];
|
||||
const FORGE_DOC_FILTER_OPTIONS = [
|
||||
['', 'Aucun'], ['grayscale', 'Noir et blanc'], ['sepia', 'Sépia'], ['blur', 'Flouté'],
|
||||
];
|
||||
const FORGE_DOC_IMAGE_CLICK_OPTIONS = [
|
||||
['', 'Aucun'], ['link', 'Ouvrir un lien'], ['lightbox', 'Plein écran'],
|
||||
];
|
||||
|
||||
function forgeDocRenderImageProps(panel, el) {
|
||||
const a = el.attributes;
|
||||
const clickBehavior = a.click_behavior || '';
|
||||
panel.innerHTML = `
|
||||
${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">
|
||||
<span class="docFieldLabel">Fichier image</span>
|
||||
<div id="docImageUploadStatus">${a.src ? 'Image actuelle en place' : 'Aucun fichier choisi'}</div>
|
||||
<input type="file" id="docImageUploadInput" accept="image/png,image/jpeg,image/gif,image/webp,image/svg+xml">
|
||||
</div>
|
||||
<div class="docField"><label for="docImageSrc">Ou URL d'une image externe</label><input type="text" id="docImageSrc" value="${forgeDocEscapeHtml(a.src || '')}" placeholder="https://..."></div>
|
||||
<div class="docField"><label for="docImageAlt">Texte alternatif</label><input type="text" id="docImageAlt" value="${forgeDocEscapeHtml(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>
|
||||
<div class="docField"><label for="docImageCaption">Légende (optionnel)</label><input type="text" id="docImageCaption" value="${forgeDocEscapeHtml(a.caption || '')}"></div>
|
||||
<div class="docField">
|
||||
<span class="docFieldLabel">Ajustement dans son cadre</span>
|
||||
<div class="docSegmented" id="docImageFitSeg">
|
||||
${FORGE_DOC_OBJECT_FIT_OPTIONS.map(([v, l]) => `<button type="button" class="docSegBtn ${(a.object_fit || '') === v ? 'is-active' : ''}" data-fit="${v}">${l}</button>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="docField">
|
||||
<span class="docFieldLabel">Format (ratio)</span>
|
||||
<div class="docSegmented" id="docImageRatioSeg">
|
||||
${FORGE_DOC_ASPECT_RATIO_OPTIONS.map(([v, l]) => `<button type="button" class="docSegBtn ${(a.aspect_ratio || '') === v ? 'is-active' : ''}" data-ratio="${forgeDocEscapeHtml(v)}">${l}</button>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="docField">
|
||||
<span class="docFieldLabel">Filtre</span>
|
||||
<div class="docSegmented" id="docImageFilterSeg">
|
||||
${FORGE_DOC_FILTER_OPTIONS.map(([v, l]) => `<button type="button" class="docSegBtn ${(a.filter_preset || '') === v ? 'is-active' : ''}" data-filter="${v}">${l}</button>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="docField">
|
||||
<label class="docCheckboxLabel"><input type="checkbox" id="docImageLazyLoad" ${a.lazy_load ? 'checked' : ''}> Chargement différé (images longues à charger)</label>
|
||||
</div>
|
||||
<div class="docField">
|
||||
<span class="docFieldLabel">Au clic (en Aperçu)</span>
|
||||
<div class="docSegmented" id="docImageClickSeg">
|
||||
${FORGE_DOC_IMAGE_CLICK_OPTIONS.map(([v, l]) => `<button type="button" class="docSegBtn ${clickBehavior === v ? 'is-active' : ''}" data-click="${v}">${l}</button>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="docField" id="docImageLinkUrlField" ${clickBehavior === 'link' ? '' : 'hidden'}>
|
||||
<label for="docImageLinkUrl">URL du lien</label>
|
||||
<input type="text" id="docImageLinkUrl" value="${forgeDocEscapeHtml(a.link_url || '')}" placeholder="https://...">
|
||||
</div>
|
||||
${forgeDocRenderBoxFieldsHtml(a, 'docImage')}
|
||||
${forgeDocDeleteButtonHtml()}
|
||||
`;
|
||||
function patch(partial) { forgeDocUpdateAttributes(el.id, { ...el.attributes, ...partial }); }
|
||||
document.getElementById('docImageUploadInput').addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
forgeDocApiUploadImage(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();
|
||||
}
|
||||
// Bug réel constaté le 26/09/2026 : "quand j'ajoute une image ça
|
||||
// crée des pages à l'infini" / "ce n'est pas redimensionner" —
|
||||
// cette route d'upload patch le DOM elle-même (fichier multipart,
|
||||
// jamais via forgeDocApiUpdate/forgeDocUpdateAttributes qui fait
|
||||
// déjà cette vérification), donc jamais couverte par le point
|
||||
// d'entrée central. Vérifiée ICI explicitement pour cette raison.
|
||||
forgeDocCheckPageOverflow();
|
||||
});
|
||||
});
|
||||
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() }));
|
||||
document.getElementById('docImageCaption').addEventListener('change', (e) => patch({ caption: e.target.value.trim() }));
|
||||
panel.querySelectorAll('#docImageFitSeg .docSegBtn').forEach((btn) => {
|
||||
btn.addEventListener('click', () => patch({ object_fit: btn.dataset.fit }));
|
||||
});
|
||||
panel.querySelectorAll('#docImageRatioSeg .docSegBtn').forEach((btn) => {
|
||||
btn.addEventListener('click', () => patch({ aspect_ratio: btn.dataset.ratio }));
|
||||
});
|
||||
panel.querySelectorAll('#docImageFilterSeg .docSegBtn').forEach((btn) => {
|
||||
btn.addEventListener('click', () => patch({ filter_preset: btn.dataset.filter }));
|
||||
});
|
||||
document.getElementById('docImageLazyLoad').addEventListener('change', (e) => patch({ lazy_load: e.target.checked }));
|
||||
panel.querySelectorAll('#docImageClickSeg .docSegBtn').forEach((btn) => {
|
||||
btn.addEventListener('click', () => patch({ click_behavior: btn.dataset.click }));
|
||||
});
|
||||
document.getElementById('docImageLinkUrl').addEventListener('change', (e) => patch({ link_url: e.target.value.trim() }));
|
||||
forgeDocBindBoxFields(panel, el, 'docImage', patch);
|
||||
}
|
||||
|
||||
function forgeDocRenderButtonProps(panel, el) {
|
||||
|
||||
Reference in New Issue
Block a user