Audit complet de mise en forme — Bouton (3e élément du tableau)

Implémente toutes les options manquantes identifiées pour l'élément
Bouton : typographie (gras/italique/transformation/police/taille/
espacement des lettres/couleur), icône SVG optionnelle avec position
(avant/après le texte) et taille réglables — au-delà de l'Étiquette,
qui n'a qu'une icône fixe —, tous les attributs de boîte partagés déjà
créés pour Titre/Paragraphe/Image, et un effet visuel universel au
survol/clic.

Deux ajouts transversaux demandés pendant le test : un réglage "les 4
côtés de la bordure à la fois" dans le module de boîte partagé
(bénéficie automatiquement à tous les éléments qui l'utilisent), et le
glisser-déposer d'un élément du canevas vers une autre page via le
panneau Pages (le mécanisme serveur existait déjà pour la pagination
automatique, seule l'interaction manuelle manquait).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-26 12:47:46 +02:00
co-authored by Claude Sonnet 5
parent 9ad50c58b8
commit 8f879f6321
8 changed files with 529 additions and 16 deletions
+164 -3
View File
@@ -347,6 +347,37 @@ function forgeDocDeleteElement(id) {
});
}
function forgeDocMoveElementToPage(elementId, targetPageId) {
// Retour utilisateur du 26/09/2026 : "j'aimerais pouvoir glisser
// déposer un élément d'une page dans une autre page" — déposé depuis
// le canevas sur une rangée du panneau Pages (voir
// forgeDocRenderPageManagerList). move_document_element_to_page
// (document_engine/elements/) redevient TOUJOURS top-niveau sur la
// page cible (une rangée d'origine n'a aucun sens comme enfant d'une
// rangée de la page cible) — Annuler restaure donc explicitement
// l'ancien parent/position via forgeDocApiMove, une fois l'élément
// revenu sur son ancienne page.
const el = window.forgeDocState.elementsById[elementId];
if (!el || targetPageId === window.forgeDocState.activePageId) return Promise.resolve();
const state = {
id: elementId,
fromPageId: window.forgeDocState.activePageId,
toPageId: targetPageId,
parentId: el.parent_id,
index: el.order_index,
};
function performForward() { return forgeDocApiMoveToPage(state.id, state.toPageId); }
function performUndo() {
return forgeDocApiMoveToPage(state.id, state.fromPageId)
.then(() => forgeDocApiMove(state.id, state.parentId, state.index));
}
return performForward().then(() => {
forgeDocPushCommand({ undo: performUndo, redo: performForward });
window.forgeDocState.selectedId = null;
return forgeDocRefreshCanvas();
});
}
function forgeDocUpdateAttributes(id, newAttributes) {
const el = window.forgeDocState.elementsById[id];
if (!el) return Promise.resolve();
@@ -851,6 +882,30 @@ function forgeDocRenderPageManagerList() {
e.preventDefault();
forgeDocSwitchPage(Number(row.dataset.pageId));
});
// Glisser un élément du canevas jusqu'ici pour le déplacer vers CETTE
// page (retour utilisateur du 26/09/2026 : "j'aimerais pouvoir
// glisser déposer un élément d'une page dans une autre page") — même
// charge utile ("text/forge-doc-move") que le réordonnancement d'un
// élément AU SEIN d'une page (voir forgeDocBindCanvasDropZone), donc
// aucune modification de la source de glisser côté canevas. Sans
// rapport avec le glisser-déposer des PAGES elles-mêmes (délégué au
// conteneur, voir forgeDocBindPageManagerDrag juste plus bas) : les
// deux coexistent sur la même rangée sans conflit, chacun ignorant
// le type de charge utile qui ne le concerne pas.
row.addEventListener('dragover', (e) => {
if (!e.dataTransfer.types.includes('text/forge-doc-move')) return;
e.preventDefault();
row.classList.add('docPageRow--dropTarget');
});
row.addEventListener('dragleave', () => row.classList.remove('docPageRow--dropTarget'));
row.addEventListener('drop', (e) => {
const moveId = e.dataTransfer.getData('text/forge-doc-move');
row.classList.remove('docPageRow--dropTarget');
if (!moveId) return;
e.preventDefault();
e.stopPropagation();
forgeDocMoveElementToPage(Number.parseInt(moveId, 10), Number(row.dataset.pageId));
});
});
container.querySelectorAll('.docPageRowRename').forEach((btn) => {
btn.addEventListener('click', (e) => {
@@ -2018,6 +2073,34 @@ function forgeDocRenderBoxFieldsHtml(a, idPrefix) {
${FORGE_DOC_ALIGN_SELF_OPTIONS.map(([v, l]) => `<button type="button" class="docSegBtn ${alignSelf === v ? 'is-active' : ''}" data-align-self="${v}">${l}</button>`).join('')}
</div>
</div>
<div class="docField">
<span class="docFieldLabel">Bordure — les 4 côtés à la fois</span>
<div class="docBorderSideRow" id="${idPrefix}BorderAllRow">
${(() => {
// Retour utilisateur du 26/09/2026 : "une option pour modifier les
// 4 bordures en même temps" — un raccourci qui applique la MÊME
// valeur aux 4 côtés d'un coup, en plus (jamais à la place) du
// réglage par côté juste en dessous. Si les 4 côtés portent déjà
// exactement la même valeur, cette rangée en repart (plutôt que
// d'afficher un état neutre déconnecté de la réalité) ; sinon elle
// repart d'un état neutre ("Aucune"/1px/couleur par défaut), un
// signe que les côtés diffèrent actuellement.
const sides = FORGE_DOC_BORDER_SIDES.map(([side]) => border[side] || {});
const allSame = sides.every((s) => (
s.style === sides[0].style && s.width === sides[0].width && s.color === sides[0].color
));
const common = allSame ? sides[0] : {};
return `
<span class="docBorderSideLabel">Tous</span>
<select class="docBorderStyleSelect" id="${idPrefix}BorderAllStyle">
${FORGE_DOC_BORDER_STYLES.map(([v, l]) => `<option value="${v}" ${(common.style || 'none') === v ? 'selected' : ''}>${l}</option>`).join('')}
</select>
<input type="range" class="docBorderWidthRange" id="${idPrefix}BorderAllWidth" min="1" max="10" step="1" value="${Number.parseFloat(common.width) || 1}" title="Épaisseur">
<input type="color" class="docBorderColorInput" id="${idPrefix}BorderAllColor" value="${forgeDocToHexColor(common.color, '#c9c4b8')}" title="Couleur">
`;
})()}
</div>
</div>
<div class="docField">
<span class="docFieldLabel">Bordure (par côté)</span>
<div class="docBorderEditor" id="${idPrefix}BorderEditor">
@@ -2064,6 +2147,21 @@ function forgeDocBindBoxFields(panel, el, idPrefix, patch) {
btn.addEventListener('click', () => patch({ align_self: btn.dataset.alignSelf }));
});
function applyBorderToAllSides() {
const style = document.getElementById(`${idPrefix}BorderAllStyle`).value;
const width = `${document.getElementById(`${idPrefix}BorderAllWidth`).value}px`;
const color = document.getElementById(`${idPrefix}BorderAllColor`).value;
const sideValue = { style, width, color };
patch({
border: {
top: sideValue, right: sideValue, bottom: sideValue, left: sideValue,
},
});
}
document.getElementById(`${idPrefix}BorderAllStyle`).addEventListener('change', applyBorderToAllSides);
document.getElementById(`${idPrefix}BorderAllWidth`).addEventListener('change', applyBorderToAllSides);
document.getElementById(`${idPrefix}BorderAllColor`).addEventListener('change', applyBorderToAllSides);
function patchBorderSide(side, partial) {
// Toujours relu depuis el.attributes AU MOMENT de l'appel (jamais un
// instantané capturé au moment du binding) : el.attributes est
@@ -2085,6 +2183,10 @@ function forgeDocBindBoxFields(panel, el, idPrefix, patch) {
});
}
const FORGE_DOC_TEXT_TRANSFORM_OPTIONS = [
['none', 'Normal'], ['uppercase', 'MAJUSCULES'], ['lowercase', 'minuscules'], ['capitalize', 'Capitales'],
];
function forgeDocRenderTextProps(panel, el) {
const a = el.attributes;
panel.innerHTML = `
@@ -2116,7 +2218,7 @@ function forgeDocRenderTextProps(panel, el) {
<div class="docField">
<span class="docFieldLabel">Majuscules / minuscules</span>
<div class="docSegmented" id="docTextTransformSeg">
${[['none', 'Normal'], ['uppercase', 'MAJUSCULES'], ['lowercase', 'minuscules'], ['capitalize', 'Capitales']].map(([v, l]) => `<button type="button" class="docSegBtn ${(a.text_transform || 'none') === v ? 'is-active' : ''}" data-transform="${v}">${l}</button>`).join('')}
${FORGE_DOC_TEXT_TRANSFORM_OPTIONS.map(([v, l]) => `<button type="button" class="docSegBtn ${(a.text_transform || 'none') === v ? 'is-active' : ''}" data-transform="${v}">${l}</button>`).join('')}
</div>
</div>
<div class="docField">
@@ -2282,6 +2384,10 @@ function forgeDocRenderImageProps(panel, el) {
forgeDocBindBoxFields(panel, el, 'docImage', patch);
}
const FORGE_DOC_ICON_POSITION_OPTIONS = [
['before', 'Avant le texte'], ['after', 'Après le texte'],
];
function forgeDocRenderButtonProps(panel, el) {
const a = el.attributes;
const attachmentStatus = a.attachment_filename
@@ -2289,13 +2395,53 @@ function forgeDocRenderButtonProps(panel, el) {
: '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"><label for="docButtonLabel">Texte du bouton</label><input type="text" id="docButtonLabel" value="${forgeDocEscapeHtml(a.label || '')}"></div>
<div class="docField"><label for="docButtonTarget">Cible (URL ou ancre)</label><input type="text" id="docButtonTarget" value="${forgeDocEscapeHtml(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>
<div class="docField"><label for="docButtonSvg">Icône SVG (optionnel)</label><textarea id="docButtonSvg" rows="3">${forgeDocEscapeHtml(a.svg_markup || '')}</textarea></div>
<div class="docField">
<span class="docFieldLabel">Position de l'icône</span>
<div class="docSegmented" id="docButtonIconPosSeg">
${FORGE_DOC_ICON_POSITION_OPTIONS.map(([v, l]) => `<button type="button" class="docSegBtn ${(a.icon_position || 'before') === v ? 'is-active' : ''}" data-pos="${v}">${l}</button>`).join('')}
</div>
</div>
${forgeDocRenderToggleSliderHtml('docButtonIconSize', "Taille de l'icône", a.icon_size, {
min: 10, max: 48, step: 1, unit: 'px', fallback: 16,
})}
<div class="docField">
<span class="docFieldLabel">Mise en forme</span>
<div class="docToolbarRow">
<button type="button" class="docFmtBtn ${a.bold ? 'is-active' : ''}" id="docButtonBoldBtn" title="Gras">B</button>
<button type="button" class="docFmtBtn docFmtBtn--italic ${a.italic ? 'is-active' : ''}" id="docButtonItalicBtn" title="Italique">I</button>
</div>
</div>
<div class="docField">
<span class="docFieldLabel">Majuscules / minuscules</span>
<div class="docSegmented" id="docButtonTextTransformSeg">
${FORGE_DOC_TEXT_TRANSFORM_OPTIONS.map(([v, l]) => `<button type="button" class="docSegBtn ${(a.text_transform || 'none') === v ? 'is-active' : ''}" data-transform="${v}">${l}</button>`).join('')}
</div>
</div>
<div class="docField">
<label for="docButtonColorInput">Couleur du texte</label>
<input type="color" id="docButtonColorInput" value="${forgeDocToHexColor(a.text_color, '#14100c')}">
</div>
<div class="docField">
<label for="docButtonFontFamilyInput">Police de caractère</label>
<select id="docButtonFontFamilyInput">
${FORGE_DOC_FONT_FAMILY_OPTIONS.map(([v, l]) => `<option value="${forgeDocEscapeHtml(v)}" ${(a.font_family || '') === v ? 'selected' : ''}>${l}</option>`).join('')}
</select>
</div>
${forgeDocRenderToggleSliderHtml('docButtonFontSize', 'Taille de police', a.font_size, {
min: 10, max: 32, step: 1, unit: 'px', fallback: 14,
})}
${forgeDocRenderToggleSliderHtml('docButtonLetterSpacing', 'Espacement des lettres', a.letter_spacing, {
min: -2, max: 10, step: 0.5, unit: 'px', fallback: 0,
})}
${forgeDocRenderBoxFieldsHtml(a, 'docButton')}
${forgeDocDeleteButtonHtml()}
`;
function patch(partial) { forgeDocUpdateAttributes(el.id, { ...el.attributes, ...partial }); }
@@ -2318,6 +2464,21 @@ function forgeDocRenderButtonProps(panel, el) {
}
});
});
document.getElementById('docButtonSvg').addEventListener('change', (e) => patch({ svg_markup: e.target.value.trim() }));
panel.querySelectorAll('#docButtonIconPosSeg .docSegBtn').forEach((btn) => {
btn.addEventListener('click', () => patch({ icon_position: btn.dataset.pos }));
});
forgeDocBindToggleSlider('docButtonIconSize', { unit: 'px' }, (v) => patch({ icon_size: v }));
document.getElementById('docButtonBoldBtn').addEventListener('click', () => patch({ bold: !a.bold }));
document.getElementById('docButtonItalicBtn').addEventListener('click', () => patch({ italic: !a.italic }));
panel.querySelectorAll('#docButtonTextTransformSeg .docSegBtn').forEach((btn) => {
btn.addEventListener('click', () => patch({ text_transform: btn.dataset.transform }));
});
document.getElementById('docButtonColorInput').addEventListener('change', (e) => patch({ text_color: e.target.value }));
document.getElementById('docButtonFontFamilyInput').addEventListener('change', (e) => patch({ font_family: e.target.value }));
forgeDocBindToggleSlider('docButtonFontSize', { unit: 'px' }, (v) => patch({ font_size: v }));
forgeDocBindToggleSlider('docButtonLetterSpacing', { unit: 'px' }, (v) => patch({ letter_spacing: v }));
forgeDocBindBoxFields(panel, el, 'docButton', patch);
}
function forgeDocRenderBadgeProps(panel, el) {