Skip to content
Snippets Groups Projects
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');
    }   

}