feat(BlockEditor): add image alignment, link, and replace/delete controls

- add align (left/center/right/full), href, newTab fields to image block
- render floating toolbar on image hover with alignment buttons and link popover
- add replace and delete actions to image toolbar
- wrap image in <a> in disabled mode and HTML export when href is set
- update htmlToBlocks/blocksToHtml to serialize/parse align, href, newTab
- guard handleContainerMouseDown to prevent multi-block selection on input/textarea focus
- add alignment and link icons to shared icons index
- update README with image block spec and toolbar behaviour
This commit is contained in:
2026-04-26 16:26:41 -04:00
parent 83490de15d
commit d66b107636
5 changed files with 425 additions and 63 deletions
@@ -89,10 +89,31 @@ function blockToElement(block) {
}
if (block.type === 'image') {
const fig = document.createElement('figure');
const align = block.align || 'center';
fig.setAttribute('data-align', align);
if (align === 'left' || align === 'right' || align === 'center') {
// CSS inline minimal pour les destinations qui ignorent data-align.
fig.setAttribute('style',
align === 'center' ? 'text-align:center'
: align === 'left' ? 'text-align:left'
: 'text-align:right');
}
const img = document.createElement('img');
img.setAttribute('src', block.src || '');
if (block.alt) img.setAttribute('alt', block.alt);
fig.appendChild(img);
if (align === 'full') img.setAttribute('width', '100%');
let imgHost = img;
if (block.href) {
const a = document.createElement('a');
a.setAttribute('href', block.href);
if (block.newTab) {
a.setAttribute('target', '_blank');
a.setAttribute('rel', 'noopener noreferrer');
}
a.appendChild(img);
imgHost = a;
}
fig.appendChild(imgHost);
if (block.caption) {
const cap = document.createElement('figcaption');
cap.textContent = block.caption;
@@ -270,12 +291,20 @@ function parseChildren(node, out) {
const img = child.querySelector('img');
if (img) {
const cap = child.querySelector('figcaption');
const linkEl = img.parentElement?.tagName === 'A' ? img.parentElement : null;
const dataAlign = child.getAttribute('data-align');
const align = ['left', 'center', 'right', 'full'].includes(dataAlign) ? dataAlign : 'center';
const href = linkEl?.getAttribute('href') || '';
const newTab = !!href && linkEl?.getAttribute('target') === '_blank';
out.push({
id: newBlockId(),
type: 'image',
src: img.getAttribute('src') || '',
alt: img.getAttribute('alt') || '',
caption: cap?.textContent?.trim() || '',
align,
href,
newTab,
});
}
continue;
@@ -289,6 +318,9 @@ function parseChildren(node, out) {
src: child.getAttribute('src') || '',
alt: child.getAttribute('alt') || '',
caption: '',
align: 'center',
href: '',
newTab: false,
});
continue;
}