-
Mike Feranda authoredMike Feranda authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
aidmiui.js 14.77 KiB
import { Plugin } from 'ckeditor5/src/core';
import {
ButtonView,
FocusCycler,
LabeledFieldView,
SwitchButtonView,
View,
ViewCollection,
createLabeledInputText,
injectCssTransitionDisabler,
submitHandler,
} from 'ckeditor5/src/ui';
import iconAidmi from '../../../../icons/aidmi.svg';
export default class AIDmiUI extends Plugin {
init() {
const editor = this.editor;
// This will register the AIDmi toolbar button.
editor.ui.componentFactory.add('aidmi', (locale) => {
const button = new ButtonView(locale);
// Create the toolbar button.
button.set({
label: editor.t('AI, describe my image!'),
icon: iconAidmi,
tooltip: true,
});
this.listenTo(button, 'execute', async () => {
try {
let output = null;
let parsedOutput = null;
let imagesJSON = this._aidmiJSONImages(editor.getData());
if (imagesJSON.length > 0) {
output = await this._aidmiContentAjax(editor.getData(), imagesJSON);
}
if (output) {
parsedOutput = JSON.parse(output);
this._openDialog(editor, parsedOutput);
}
} catch (error) {
console.log("Error:", error);
}
});
return button;
});
}
_aidmiJSONImages(content) {
// Create a temporary DOM element to parse the HTML string
const tempDiv = document.createElement('div');
tempDiv.innerHTML = content; // Convert the HTML string to DOM elements
// Find all images with the class 'aidmi-dialog-image'
let images = tempDiv.querySelectorAll('img');
// Initialize an empty array to store the JSON data
let imageJsonArray = [];
// Loop through each image and extract 'data-entity-uuid' and 'src'
images.forEach((image) => {
let imageObject = {
"data-entity-uuid": image.getAttribute('data-entity-uuid'),
"src": image.getAttribute('src')
};
// Push the image object to the array
imageJsonArray.push(imageObject);
});
// Convert the array to a JSON string for output or further use
return imageJsonArray;
}
_aidmiContentAjax(content, imagesJSON) {
const $ = jQuery;
return new Promise((resolve, reject) => {
$.ajax({
url: '/admin/aidmi/describe-content-ajax',
method: 'POST',
data: {
content: content,
imagesJSON: JSON.stringify(imagesJSON)
},
success: function(response) {
resolve(response);
},
error: function(xhr, status, error) {
reject(error);
}
});
});
}
_openDialog(editor, data, callback) {
let aidmiEditedText;
// Create a temporary container to hold the content.
const tempElement = document.createElement('div');
tempElement.setAttribute('role', 'dialog');
tempElement.setAttribute('aria-labelledby', 'aidmi-dialog-title');
tempElement.setAttribute('aria-describedby', 'aidmi-dialog-description');
tempElement.setAttribute('aria-modal', 'true'); // Ensures screen readers treat the dialog as a modal.
// Set title text.
let tempETitle = 'AIDmi Description';
let tempESubTitle = 'Please evaluate and edit the description as needed.';
// Create dialog content with a textarea for editing.
tempElement.innerHTML = `<p id="aidmi-dialog-subtitle" class="aidmi-dialog-subtitle">${Drupal.t(tempESubTitle)}</p>`;
// Variable to store the number of images
const imageCount = data.reduce((count, outerArray) => {
outerArray.forEach((item) => {
if (item.images && Array.isArray(item.images)) {
count += item.images.length;
}
});
return count;
}, 0);
// Loop through the images
data.forEach((outerArray) => {
outerArray.forEach((item) => {
// If item has an images array, loop through it
if (item.images && Array.isArray(item.images)) {
item.images.forEach((image) => {
let formImageInnerHTML = '';
// Start of image div alt option area.
formImageInnerHTML += `<div class="image-option-group" id="option-group-${image['data-entity-uuid']}">`;
// Add Image Display.
formImageInnerHTML += `
<!-- Image Display -->
<img class="aidmi-dialog-image" data-entity-uuid="${image['data-entity-uuid']}" src="${image.src}" alt="${Drupal.t(image.before_alt)}" />`;
// Add Current Alt Tag Section.
let imageOldSelectDisabled = false;
let imageLabel = 'Current Alt Tag - Read only';
let imageBeforeAlt = image.before_alt;
if (!imageBeforeAlt || imageBeforeAlt.length < 6) {
// Old alt shouldn't be an allowed option.
imageLabel += ' (Option disabled since alt is either blank or too short)';
imageOldSelectDisabled = true;
}
formImageInnerHTML += `
<!-- Current Alt Tag Section -->
<div>
<label for="aidmi-dialog-textarea-oldalt-${image['data-entity-uuid']}" class="aidmi-dialog-label">
<input type="radio" name="alt-selection-${image['data-entity-uuid']}" id="alt-old-${image['data-entity-uuid']}" value="current" aria-labelledby="label-old-${image['data-entity-uuid']}" ${imageOldSelectDisabled ? 'disabled' : ''} required/>
<span id="label-old-${image['data-entity-uuid']}">${Drupal.t(imageLabel)}</span>
</label>
<textarea id="aidmi-dialog-textarea-oldalt-${image['data-entity-uuid']}" class="aidmi-dialog-textarea" rows="3" aria-labelledby="label-old-${image['data-entity-uuid']}" readonly="readonly">${Drupal.t(image.before_alt)}</textarea>
</div>`;
// Add Suggested Alt Tag Section.
formImageInnerHTML += `
<!-- Suggested Alt Tag Section -->
<div>
<label for="aidmi-dialog-textarea-newalt-${image['data-entity-uuid']}" class="aidmi-dialog-label">
<input type="radio" name="alt-selection-${image['data-entity-uuid']}" id="alt-new-${image['data-entity-uuid']}" value="suggested" aria-labelledby="label-new-${image['data-entity-uuid']}" required>
<span id="label-new-${image['data-entity-uuid']}">${Drupal.t('Suggested Alt Tag - Review and modify as required')}</span>
</label>
<textarea id="aidmi-dialog-textarea-newalt-${image['data-entity-uuid']}" class="aidmi-dialog-textarea" rows="3" aria-labelledby="label-new-${image['data-entity-uuid']}">${Drupal.t(image.recommendation)}</textarea>
</div>`;
// Add Set as Decorative Image.
formImageInnerHTML += `
<!-- Set as Decorative Image -->
<div>
<label for="aidmi-dialog-textarea-dilt-${image['data-entity-uuid']}" class="aidmi-dialog-label">
<input type="radio" name="alt-selection-${image['data-entity-uuid']}" id="alt-decorative-${image['data-entity-uuid']}" value="decorative" aria-labelledby="label-decorative-${image['data-entity-uuid']}" required>
<span id="label-decorative-${image['data-entity-uuid']}">${Drupal.t('Set as Decorative Image')}</span>
</label>
</div>`;
// End of image div alt option area.
formImageInnerHTML += `</div><hr/>`;
// Insert it.
tempElement.innerHTML += formImageInnerHTML;
});
}
});
});
// Use the Drupal off-canvas dialog to show the content.
const options = {
dialogClass: 'aidmi-dialog',
title: tempETitle,
resizable: true,
width: '700px',
buttons: [
{
text: Drupal.t('Insert Text'),
class: 'aidmi-insert-button', // Add a class to the insert button for reference
click: () => {
// Validate all selections before inserting text
const selections = this._collectSelections(data);
if (selections) {
// Modify the images in the editor content based on the selections
this._modifyImagesInContent(editor, selections);
dialogInstance.close();
} else {
// Show a message if validation fails
alert(Drupal.t('Please select an option for each image before inserting.'));
}
}
},
{
text: Drupal.t('Cancel'),
click: function () {
// Close the dialog.
dialogInstance.close();
}
}
]
};
// Open the dialog using Drupal's dialog API.
const dialogInstance = Drupal.dialog(tempElement, options);
dialogInstance.showModal();
// Add event listeners to all radio buttons to validate the selection and remove highlights
document.querySelectorAll('input[type="radio"]').forEach(radio => {
radio.addEventListener('change', (event) => {
const groupId = event.target.name.replace('alt-selection-', 'option-group-');
// Remove the highlight class when a selection is made
document.getElementById(groupId).classList.remove('aidmi-highlight-missing');
});
});
// Set focus on the first input for screen readers
document.querySelector('input[type="radio"]').focus();
}
_collectSelections(data) {
let allSelected = true;
const selections = [];
// Iterate through each item in the data array to process images
data.forEach((outerArray) => {
outerArray.forEach((item) => {
if (item.images && Array.isArray(item.images)) {
item.images.forEach((image) => {
const uuid = image['data-entity-uuid'];
const selectedRadio = document.querySelector(`input[name="alt-selection-${uuid}"]:checked`);
// If no radio button is selected, set `allSelected` to false
if (!selectedRadio) {
allSelected = false;
// Highlight the missing selection group
document.getElementById(`option-group-${uuid}`).classList.add('aidmi-highlight-missing');
} else {
// Get the selected value and corresponding alt text or set as decorative
const altValue = selectedRadio.value;
let altText = '';
if (altValue === 'current') {
altText = document.getElementById(`aidmi-dialog-textarea-oldalt-${uuid}`).value;
} else if (altValue === 'suggested') {
altText = document.getElementById(`aidmi-dialog-textarea-newalt-${uuid}`).value;
}
selections.push({
uuid: uuid,
altText: altValue === 'decorative' ? '' : altText, // Leave alt text empty for decorative images
isDecorative: altValue === 'decorative'
});
}
});
}
});
});
return allSelected ? selections : null;
}
// Function to modify the images in the CKEditor content based on the selected options
_modifyImagesInContent(editor, selections) {
// Get the current content of the editor
let content = editor.getData();
// Create a temporary DOM element to manipulate the HTML content
const tempDiv = document.createElement('div');
tempDiv.innerHTML = content; // Convert the HTML string to DOM elements
// Loop through the selections and modify the corresponding images in the content
selections.forEach(selection => {
// Find the image element by its data-entity-uuid attribute
const imageElement = tempDiv.querySelector(`img[data-entity-uuid="${selection.uuid}"]`);
if (imageElement) {
// Update the alt attribute or set as decorative based on the selection
if (selection.isDecorative) {
imageElement.setAttribute('alt', ''); // Set alt attribute to empty for decorative images
imageElement.setAttribute('role', 'presentation'); // Add role="presentation" to decorative images
} else {
imageElement.setAttribute('alt', selection.altText); // Update the alt attribute
imageElement.removeAttribute('role'); // Remove role attribute if it's not a decorative image
}
} else {
console.warn(`Image with UUID ${selection.uuid} not found in content.`);
}
});
// Set the modified content back to the editor
editor.setData(tempDiv.innerHTML);
// Optional: Trigger the change event to update the editor state
editor.editing.view.document.fire('change:data');
}
}