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', }; };