diff --git a/dev/index.html b/dev/index.html
index 2383256..d093b7e 100644
--- a/dev/index.html
+++ b/dev/index.html
@@ -26,6 +26,7 @@
withBackground: false,
stretched: false,
caption: "kimitsu no yayiba",
+ alt: "Picture of anime characters"
},
},
],
@@ -44,6 +45,7 @@
border: false,
background: false,
stretch: true,
+ alt: "optional",
},
},
},
diff --git a/package.json b/package.json
index 954edf8..456558b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@editorjs/image",
- "version": "2.10.3",
+ "version": "2.10.4",
"keywords": [
"codex editor",
"image",
diff --git a/src/index.css b/src/index.css
index 55342ef..41b78a2 100644
--- a/src/index.css
+++ b/src/index.css
@@ -44,7 +44,8 @@
}
}
- &__caption {
+ &__caption,
+ &__alt {
visibility: hidden;
position: absolute;
bottom: 0;
@@ -85,6 +86,10 @@
^&__caption {
visibility: hidden !important;
}
+
+ ^&__alt {
+ visibility: hidden !important;
+ }
}
.cdx-button {
@@ -163,7 +168,27 @@
visibility: visible;
}
- padding-bottom: 50px
+ padding-bottom: 50px;
+ }
+
+ &--alt {
+ ^&__alt {
+ visibility: visible;
+ }
+
+ padding-bottom: 50px;
+ }
+
+ &--caption&--alt {
+ ^&__caption {
+ bottom: 40px;
+ }
+
+ ^&__alt {
+ bottom: 0px;
+ }
+
+ padding-bottom: 80px;
}
}
diff --git a/src/index.ts b/src/index.ts
index 7700606..657cbfd 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -82,6 +82,11 @@ export default class ImageTool implements BlockTool {
*/
private isCaptionEnabled: boolean | null = null;
+ /**
+ * Alt text enabled state
+ */
+ private isAltEnabled: boolean = false;
+
/**
* @param tool - tool properties got from editor.js
* @param tool.data - previously saved data
@@ -104,6 +109,7 @@ export default class ImageTool implements BlockTool {
field: config.field,
types: config.types,
captionPlaceholder: this.api.i18n.t(config.captionPlaceholder ?? 'Caption'),
+ altPlaceholder: this.api.i18n.t(config.altPlaceholder ?? 'Alt text'),
buttonContent: config.buttonContent,
uploader: config.uploader,
actions: config.actions,
@@ -140,6 +146,7 @@ export default class ImageTool implements BlockTool {
*/
this._data = {
caption: '',
+ alt: '',
withBorder: false,
withBackground: false,
stretched: false,
@@ -204,6 +211,11 @@ export default class ImageTool implements BlockTool {
this.ui.applyTune('caption', true);
}
+ if (this.config.features?.alt === true || (this.config.features?.alt === 'optional' && this.data.alt)) {
+ this.isAltEnabled = true;
+ this.ui.applyTune('alt', true);
+ }
+
return this.ui.render() as HTMLDivElement;
}
@@ -221,8 +233,10 @@ export default class ImageTool implements BlockTool {
*/
public save(): ImageToolData {
const caption = this.ui.nodes.caption;
+ const alt = this.ui.nodes.alt;
this._data.caption = caption.innerHTML;
+ this._data.alt = alt.innerHTML;
return this.data;
}
@@ -240,13 +254,23 @@ export default class ImageTool implements BlockTool {
background: 'withBackground',
stretch: 'stretched',
caption: 'caption',
+ alt: 'alt',
};
if (this.config.features?.caption === 'optional') {
tunes.push({
name: 'caption',
icon: IconText,
- title: 'With caption',
+ title: this.api.i18n.t('With caption'),
+ toggle: true,
+ });
+ }
+
+ if (this.config.features?.alt === 'optional') {
+ tunes.push({
+ name: 'alt',
+ icon: IconText,
+ title: this.api.i18n.t('With alt text'),
toggle: true,
});
}
@@ -258,6 +282,10 @@ export default class ImageTool implements BlockTool {
return this.config.features?.caption !== false;
}
+ if (featureKey === 'alt') {
+ return this.config.features?.alt !== false;
+ }
+
return featureKey == null || this.config.features?.[featureKey as keyof FeaturesConfig] !== false;
});
@@ -272,6 +300,10 @@ export default class ImageTool implements BlockTool {
currentState = this.isCaptionEnabled ?? currentState;
}
+ if (tune.name === 'alt') {
+ currentState = this.isAltEnabled;
+ }
+
return currentState;
};
@@ -299,6 +331,15 @@ export default class ImageTool implements BlockTool {
newState = this.isCaptionEnabled;
}
+ /**
+ * For the alt tune, we can't rely on the this._data
+ * because it can be manualy toggled by user
+ */
+ if (tune.name === 'alt') {
+ this.isAltEnabled = !this.isAltEnabled;
+ newState = this.isAltEnabled;
+ }
+
this.tuneToggled(tune.name as keyof ImageToolData, newState);
},
}));
@@ -396,6 +437,9 @@ export default class ImageTool implements BlockTool {
this._data.caption = data.caption || '';
this.ui.fillCaption(this._data.caption);
+ this._data.alt = data.alt || '';
+ this.ui.fillAlt(this._data.alt);
+
ImageTool.tunes.forEach(({ name: tune }) => {
const value = typeof data[tune as keyof ImageToolData] !== 'undefined' ? data[tune as keyof ImageToolData] === true || data[tune as keyof ImageToolData] === 'true' : false;
@@ -407,6 +451,12 @@ export default class ImageTool implements BlockTool {
} else if (this.config.features?.caption === true) {
this.setTune('caption', true);
}
+
+ if (data.alt) {
+ this.setTune('alt', true);
+ } else if (this.config.features?.alt === true) {
+ this.setTune('alt', true);
+ }
}
/**
@@ -467,6 +517,13 @@ export default class ImageTool implements BlockTool {
this._data.caption = '';
this.ui.fillCaption('');
}
+ } else if (tuneName === 'alt') {
+ this.ui.applyTune(tuneName, state);
+
+ if (state == false) {
+ this._data.alt = '';
+ this.ui.fillAlt('');
+ }
} else {
/**
* Inverse tune state
diff --git a/src/types/types.ts b/src/types/types.ts
index 3de5505..c886c3d 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -73,6 +73,11 @@ export type ImageToolData = {
*/
caption: string;
+ /**
+ * Alt text for the image.
+ */
+ alt: string;
+
/**
* Flag indicating whether the image has a border.
*/
@@ -117,6 +122,11 @@ export type FeaturesConfig = {
* Can be set to 'optional' to allow users to toggle via block tunes.
*/
caption?: boolean | 'optional';
+ /**
+ * Flag to enable/disable alt text.
+ * Can be set to 'optional' to allow users to toggle via block tunes.
+ */
+ alt?: boolean | 'optional';
/**
* Flag to enable/disable tune - stretched
*/
@@ -159,6 +169,11 @@ export interface ImageConfig {
*/
captionPlaceholder?: string;
+ /**
+ * Placeholder text for the alt text field.
+ */
+ altPlaceholder?: string;
+
/**
* Additional data to send with requests.
*/
diff --git a/src/ui.ts b/src/ui.ts
index f66afa0..fbb0acb 100644
--- a/src/ui.ts
+++ b/src/ui.ts
@@ -56,6 +56,11 @@ interface Nodes {
* Caption element for the image.
*/
caption: HTMLElement;
+
+ /**
+ * Alt text element for the image.
+ */
+ alt: HTMLElement;
}
/**
@@ -133,6 +138,9 @@ export default class Ui {
caption: make('div', [this.CSS.input, this.CSS.caption], {
contentEditable: !this.readOnly,
}),
+ alt: make('div', [this.CSS.input, this.CSS.alt], {
+ contentEditable: !this.readOnly,
+ }),
};
/**
@@ -142,13 +150,22 @@ export default class Ui {
*
*
*
+ *
*
*
*/
this.nodes.caption.dataset.placeholder = this.config.captionPlaceholder;
+ this.nodes.alt.dataset.placeholder = this.config.altPlaceholder || this.api.i18n.t('Alt text');
+
+ // Add event listener to update alt attribute when alt text changes
+ this.nodes.alt.addEventListener('input', () => {
+ this.updateImageAlt(this.nodes.alt.innerHTML);
+ });
+
this.nodes.imageContainer.appendChild(this.nodes.imagePreloader);
this.nodes.wrapper.appendChild(this.nodes.imageContainer);
this.nodes.wrapper.appendChild(this.nodes.caption);
+ this.nodes.wrapper.appendChild(this.nodes.alt);
this.nodes.wrapper.appendChild(this.nodes.fileButton);
}
@@ -202,6 +219,11 @@ export default class Ui {
src: url,
};
+ // Add alt attribute for IMG tags
+ if (tag === 'IMG') {
+ attributes.alt = this.nodes.alt.textContent || '';
+ }
+
/**
* We use eventName variable because IMG and VIDEO tags have different event to be called on source load
* - IMG: load
@@ -259,6 +281,28 @@ export default class Ui {
}
}
+ /**
+ * Shows alt text input
+ * @param text - alt text content
+ */
+ public fillAlt(text: string): void {
+ if (this.nodes.alt !== undefined) {
+ this.nodes.alt.innerHTML = text;
+ // Update the alt attribute on the image element if it exists
+ this.updateImageAlt(text);
+ }
+ }
+
+ /**
+ * Updates the alt attribute on the image element
+ * @param altText - alt text to set
+ */
+ public updateImageAlt(altText: string): void {
+ if (this.nodes.imageEl && this.nodes.imageEl.tagName === 'IMG') {
+ this.nodes.imageEl.setAttribute('alt', altText);
+ }
+ }
+
/**
* Changes UI status
* @param status - see {@link Ui.status} constants
@@ -291,6 +335,7 @@ export default class Ui {
imagePreloader: 'image-tool__image-preloader',
imageEl: 'image-tool__image-picture',
caption: 'image-tool__caption',
+ alt: 'image-tool__alt',
};
};