fix: 🐛 add title in options of wysiwyg image extension

This commit is contained in:
Alexis Gousseau 2026-02-17 14:58:53 +01:00
parent f36d4861da
commit ce3de272bc
9 changed files with 70 additions and 12 deletions

View file

@ -257,7 +257,8 @@ return [
"position" => "Image position",
"format" => "Image format",
"image" => "Image",
"alt" => "ALT attribute of the image",
"attribute_alt" => "Alt attribute of the image",
"attribute_title" => "Title associated to the image",
"caption" => "Caption of the image",
"has_caption" => "Presence of a legend",
"landscape" => "Landscape",

View file

@ -258,7 +258,8 @@ return [
"position" => "Position de l'image",
"format" => "Format de l'image",
"image" => "Image",
"alt" => "Texte alternatif de l'image",
"attribute_alt" => "Texte alternatif de l'image",
"attribute_title" => "Titre associé à l'image",
"caption" => "Légende de l'image",
"has_caption" => "Présence d'une légende",
"landscape" => "Paysage",

View file

@ -1667,7 +1667,7 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
{/* Pagination boutons */}
{options.length > 0 && lastPage !== 1 && (
<div className="border-t flex flex-col xl:flex-row justify-between items-center gap-1.5 p-1">
<div className="border-t flex flex-col lg:flex-row justify-between items-center gap-1.5 p-1">
<BtnOptionnalLinkWithTooltip
content={
<>

View file

@ -23,6 +23,7 @@ const Image = TiptapImage.extend({
width: null,
height: null,
alt: null,
title: null,
caption: null,
style: null,
};
@ -44,6 +45,7 @@ const Image = TiptapImage.extend({
return {
src: img.src,
alt: img.getAttribute("alt"),
title: img.getAttribute("title"),
width: parseInt(
img.getAttribute("width") || imageDetails.width,
10,

View file

@ -57,10 +57,12 @@ export default function ImageButton({ editor, disabled }: ImageProps) {
// Values of inputs text.
const [inputText, setInputText] = useState<{
alt: string;
title: string;
caption: string | null;
hasCaption: boolean;
}>({
alt: "",
title: "",
caption: null,
hasCaption: false,
});
@ -78,7 +80,8 @@ export default function ImageButton({ editor, disabled }: ImageProps) {
.ucFirst()
.value();
const transImage = useTrans("back.wysiwyg.image.image");
const transAlt = useTrans("back.wysiwyg.image.alt");
const transAlt = useTrans("back.wysiwyg.image.attribute_alt");
const transTitle = useTrans("back.wysiwyg.image.attribute_title");
const transCaption = useTrans("back.wysiwyg.image.caption");
const transValidation = (attribute: string) => {
return {
@ -94,6 +97,7 @@ export default function ImageButton({ editor, disabled }: ImageProps) {
};
};
const transValidationAlt = transValidation(transAlt);
const transValidationTitle = transValidation(transTitle);
const transValidationCaption = transValidation(transCaption);
// List of image types.
@ -169,6 +173,7 @@ export default function ImageButton({ editor, disabled }: ImageProps) {
upload: "image",
inputFile: "image",
alt: "alt",
title: "title",
caption: "caption",
};
// Manage zod errors.
@ -216,6 +221,7 @@ export default function ImageButton({ editor, disabled }: ImageProps) {
setFile(null);
setInputText({
alt: "",
title: "",
caption: null,
hasCaption: false,
});
@ -275,6 +281,7 @@ export default function ImageButton({ editor, disabled }: ImageProps) {
const inputTextSchema = imageTextSchema({
hasCaption: inputText.hasCaption,
transValidationAlt,
transValidationTitle,
transValidationCaption,
});
@ -285,6 +292,7 @@ export default function ImageButton({ editor, disabled }: ImageProps) {
.parse({
inputFile,
alt: inputText.alt,
title: inputText.title,
caption: inputText.caption,
});
@ -295,6 +303,7 @@ export default function ImageButton({ editor, disabled }: ImageProps) {
editor.commands.setImage({
src: imageSrc,
alt: parsedZod.alt,
title: parsedZod.title,
caption: parsedZod.caption,
width: String(inputType.width),
height: String(inputType.height),
@ -336,6 +345,7 @@ export default function ImageButton({ editor, disabled }: ImageProps) {
useEffect(() => {
setInputText({
alt: inputText.alt,
title: inputText.title,
caption: inputText.hasCaption ? inputText.caption : null,
hasCaption: inputText.hasCaption,
});

View file

@ -7,6 +7,7 @@ import str from "@/hooks/use-string";
type ImageInputsProps = {
value: {
alt: string;
title: string;
caption: string | null;
hasCaption: boolean;
};
@ -31,13 +32,30 @@ export default function ImageInputs({
{/* Input alt attribute of the image. */}
<SimpleString
name="alt"
label={str(useTrans("back.wysiwyg.image.alt"))
label={str(useTrans("back.wysiwyg.image.attribute_alt"))
.ucFirst()
.value()}
placeholder="ex: John Doe"
required
value={value.alt}
setData={(_, v) => onChange({ ...value, alt: v })}
setData={(_, currentValue) =>
onChange({ ...value, alt: currentValue })
}
disabled={disabled || loading}
parentErrors={errors}
/>
{/* Input title attribute of the image. */}
<SimpleString
name="title"
label={str(useTrans("back.wysiwyg.image.attribute_title"))
.ucFirst()
.value()}
placeholder="ex: John Doe"
required
value={value.title}
setData={(_, currentValue) =>
onChange({ ...value, title: currentValue })
}
disabled={disabled || loading}
parentErrors={errors}
/>
@ -52,11 +70,11 @@ export default function ImageInputs({
name="hasCaption"
checked={value.hasCaption}
className="justify-center"
setData={(_, v) =>
setData={(_, currentValue) =>
onChange({
...value,
hasCaption: v,
caption: v ? value.caption : null,
hasCaption: currentValue,
caption: currentValue ? value.caption : null,
})
}
/>
@ -68,7 +86,9 @@ export default function ImageInputs({
placeholder="ex: John Doe"
required={false}
value={value.caption ?? ""}
setData={(_, v) => onChange({ ...value, caption: v })}
setData={(_, currentValue) =>
onChange({ ...value, caption: currentValue })
}
disabled={disabled || loading}
parentErrors={errors}
/>

View file

@ -84,6 +84,7 @@ const PLACEMENT_STYLES: Record<PlacementValue, string> = {
type ImageAttrs = {
src: string;
alt: string;
title: string;
width: number;
height: number;
caption: string | null;
@ -112,10 +113,12 @@ export default function ImageNodeView(props: NodeViewProps) {
// Values of inputs text.
const [inputText, setInputText] = useState<{
alt: string;
title: string;
caption: string | null;
hasCaption: boolean;
}>({
alt: attrs.alt,
title: attrs.title,
caption: attrs.caption,
hasCaption: !!attrs.caption,
});
@ -123,7 +126,8 @@ export default function ImageNodeView(props: NodeViewProps) {
const [topSticky, setTopSticky] = useState<number>(0);
// Translations.
const transAlt = useTrans("back.wysiwyg.image.alt");
const transAlt = useTrans("back.wysiwyg.image.attribute_alt");
const transTitle = useTrans("back.wysiwyg.image.attribute_title");
const transCaption = useTrans("back.wysiwyg.image.caption");
const transValidation = (attribute: string) => {
return {
@ -139,6 +143,7 @@ export default function ImageNodeView(props: NodeViewProps) {
};
};
const transValidationAlt = transValidation(transAlt);
const transValidationTitle = transValidation(transTitle);
const transValidationCaption = transValidation(transCaption);
/**
@ -260,6 +265,7 @@ export default function ImageNodeView(props: NodeViewProps) {
const fieldErrors: typeof errors = {};
const fieldMapping: Record<string, string> = {
alt: "alt",
title: "title",
caption: "caption",
};
if (err instanceof z.ZodError) {
@ -284,6 +290,7 @@ export default function ImageNodeView(props: NodeViewProps) {
const inputTextSchema = imageTextSchema({
hasCaption: inputText.hasCaption,
transValidationAlt,
transValidationTitle,
transValidationCaption,
});
@ -291,12 +298,14 @@ export default function ImageNodeView(props: NodeViewProps) {
// Validate data.
const parsedZod = inputTextSchema.parse({
alt: inputText.alt,
title: inputText.title,
caption: inputText.caption,
});
// Update data of the image in @tiptap editor.
updateAttributes({
alt: parsedZod.alt,
title: parsedZod.title,
caption: parsedZod.caption,
});
setOpen(false);
@ -374,6 +383,7 @@ export default function ImageNodeView(props: NodeViewProps) {
useEffect(() => {
setInputText({
alt: inputText.alt,
title: inputText.title,
caption: inputText.hasCaption ? inputText.caption : null,
hasCaption: inputText.hasCaption,
});
@ -423,6 +433,7 @@ export default function ImageNodeView(props: NodeViewProps) {
ref={imgRef}
src={attrs.src.replace(url, "")}
alt={attrs.alt}
title={attrs.title}
width={attrs.width}
height={attrs.height}
draggable={false}

View file

@ -17,6 +17,7 @@ export const imageDetails = {
export const imageTextSchema = ({
hasCaption,
transValidationAlt,
transValidationTitle,
transValidationCaption,
}: {
hasCaption: boolean;
@ -25,6 +26,11 @@ export const imageTextSchema = ({
max: string;
string: string;
};
transValidationTitle: {
min: string;
max: string;
string: string;
};
transValidationCaption: {
min: string;
max: string;
@ -36,15 +42,21 @@ export const imageTextSchema = ({
.string({ message: transValidationAlt.string })
.min(3, transValidationAlt.min)
.max(255, transValidationAlt.max);
// Attribute title zod rules.
const titleRules = z
.string({ message: transValidationTitle.string })
.min(3, transValidationTitle.min)
.max(255, transValidationTitle.max);
// Caption zod rules.
const captionRules = z
.string({ message: transValidationAlt.string })
.string({ message: transValidationCaption.string })
.min(3, transValidationCaption.min)
.max(255, transValidationCaption.max);
// Return zod object.
return z.object({
alt: altRules,
title: titleRules,
caption: hasCaption ? captionRules : z.null(),
});
};

View file

@ -9,6 +9,7 @@ declare module "@tiptap/core" {
setImage: (attrs: {
src: string;
alt: string;
title: string;
caption: string | null;
width: string;
height: string;