Skip to content
Snippets Groups Projects
Commit 954405da authored by Mike Feranda's avatar Mike Feranda
Browse files

Issue #3473643 by mferanda: Switch from confirm to a dialog box

parent f2135e83
No related branches found
No related tags found
No related merge requests found
name: 'AIDmi - AI Describe My Image'
type: module
description: 'A module to help generate 508-compliant descriptions for images using AI.'
core_version_requirement: ^10
core_version_requirement: ^10 || ^11
package: Custom
aidmi_ckeditor:
css:
theme:
css/aidmi.css: {}
js:
js/aidmi_ckeditor.js: { preprocess: false }
dependencies:
......
.aidmi-dialog {
max-width: 400px;
height: auto;
}
.aidmi-dialog button {
margin-right: 10px;
}
.aidmi-dialog-content-scrollable {
max-height: 300px; /* Adjust the max height based on how tall you want the scrollable area */
overflow-y: auto; /* Adds a vertical scrollbar if content exceeds the max height */
padding: 10px; /* Optional: Add padding inside the scrollable content */
}
.aidmi-dialog-subtitle {
font-size: 0.9em;
color: #666; /* Use a color that ensures sufficient contrast */
margin-top: -10px; /* Adjust spacing as needed */
margin-bottom: 10px;
}
\ No newline at end of file
let activeEditorInstance = null;
let activeEditorInstance;
let lastSelection;
(function ($, Drupal, once) {
Drupal.behaviors.ckeditorImageUploadMonitor = {
attach: function (context, settings) {
// Set the last active CKEditor
// Capture the selection before opening the dialog.
function captureSelection(editorInstance) {
const selection = editorInstance.model.document.selection;
// If there's a selection, clone it to restore later.
if (selection) {
lastSelection = selection.getFirstRange().clone();
}
}
// Restore that last selection.
function restoreSelection(editorInstance) {
if (lastSelection) {
editorInstance.model.change(writer => {
// Restore the selection.
writer.setSelection(lastSelection);
});
// Focus the editor view.
editorInstance.editing.view.focus();
}
}
// Set the last active CKEditor.
once('getActiveCKEditorInstance', '.ck-editor__editable', context).forEach(function (editorElement) {
// Attach a focus event listener to detect when an editor becomes active
// Attach a focus event listener to detect when an editor becomes active.
editorElement.addEventListener('focus', function () {
activeEditorInstance = editorElement.ckeditorInstance;
});
});
// Select the target node (element) you want to observe
const targetNode = document.body; // You can change this to any specific element
// Select the target node (element) you want to observe.
const targetNode = document.body; // You can change this to any specific element.
// Define the configuration for the observer (which types of changes to monitor)
// Define the configuration for the observer (which types of changes to monitor).
const config = {
childList: true, // Monitor additions or removals of child elements
attributes: true, // Monitor changes to attributes
characterData: false, // Don't monitor changes to text content
subtree: true, // Extend monitoring to all descendants of the observed node
attributeFilter: ['class', 'src'], // Only monitor changes to 'class' and 'src' attributes
attributeOldValue: true // Record the previous value of changed attributes
childList: true, // Monitor additions or removals of child elements.
attributes: true, // Monitor changes to attributes.
characterData: false, // Don't monitor changes to text content.
subtree: true, // Extend monitoring to all descendants of the observed node.
attributeFilter: ['class', 'src'], // Only monitor changes to 'class' and 'src' attributes.
attributeOldValue: true // Record the previous value of changed attributes.
};
// Define the callback function that will be triggered when mutations are observed
// Define the callback function that will be triggered when mutations are observed.
const callback = (mutationsList) => {
mutationsList.forEach((mutation) => {
if (mutation.type === 'attributes') {
// && $('.ck-text-alternative-form')
const regex = /^ck-labeled-field-view/;
if (typeof mutation.target !== 'undefined' && regex.test(mutation.target.id)) {
altTextField = mutation.target;
......@@ -42,82 +66,179 @@ let activeEditorInstance = null;
});
};
// Create an instance of MutationObserver and pass the callback function
// Create an instance of MutationObserver and pass the callback function.
const observer = new MutationObserver(callback);
// Start observing the target node with the provided configuration
// Start observing the target node with the provided configuration.
observer.observe(targetNode, config);
// Function to add the AIDmi Button
// Function to add the AIDmi Button.
function aidmiButton(altTextField, ckEditorInstance, imgTag) {
// Check if the image has a data-entity-uuid
// Check if the image has a data-entity-uuid.
uuidMatch = imgTag.match(/^<img[^>]*data-entity-uuid="([^"]*)"/);
if (uuidMatch) {
imgUuid = uuidMatch[1];
}
const existingButton = document.querySelector('.aidmi-button'); // Check if the button already exists
const existingButton = document.querySelector('.aidmi-button'); // Check if the button already exists.
if (!existingButton) {
const button = document.createElement('button');
button.type = 'button'; // Set the button type to 'button' to avoid form submission behavior
button.type = 'button'; // Set the button type to 'button' to avoid form submission behavior.
button.textContent = 'AI, describe my image!';
button.classList.add('aidmi-button');
// Attach the click event to disable the button after click
// Attach the click event to disable the button after click.
button.onclick = (e) => {
e.preventDefault();
// Disable the button immediately after click
// Disable the button immediately after click.
button.disabled = true;
button.textContent = 'AIDmi Processing...';
// Call the aidmiAjax function and handle the result with .then()
// Call the aidmiAjax function and handle the result with .then().
imgDesc = aidmiAjax(imgUuid)
.then((data) => {
if (confirm("Do you want to use this description?\n\n" + data)) {
// Once the promise resolves, set the actual text into the field
altTextField.value = data; // Set the resolved data to the field
altTextField.focus();
// Manually trigger the onchange event
const event = new Event('change', {
bubbles: true, // Ensure the event bubbles up the DOM
cancelable: true // Allow the event to be cancelable
});
altTextField.dispatchEvent(event); // Dispatch the event on the input field
}
// Open the Off-Canvas Dialog and show the retrieved description.
openOffCanvasDialog(data, (isConfirmed) => {
if (isConfirmed) {
// Check if the imgTag has an alt attribute.
updateImageAltInCKEditor(ckEditorInstance, imgUuid, data);
}
});
})
.catch((error) => {
console.error('Error:', error); // Handle errors here
altTextField.focus();
alert(t('AI connection error, please contact support.'))
console.log('Error:', error); // Handle errors here.
alert(Drupal.t('AI connection error, please contact support.'));
})
.finally(() => {
// Re-enable the button once the AJAX call is complete
button.disabled = false; // Re-enable the button
// Re-enable the button once the AJAX call is complete.
button.disabled = false; // Re-enable the button.
button.textContent = 'AI, describe my image!';
});
};
// Add the button to the DOM
// Add the button to the DOM.
altTextField.parentElement.appendChild(button);
}
}
function aidmiAjax(uuid) {
// Initially clear the field or show a placeholder/loading message
altTextField.value = ''; // Keep the field empty
// Return a Promise that resolves when the AJAX call succeeds
// Initially clear the field or show a placeholder/loading message.
altTextField.value = ''; // Keep the field empty.
// Return a Promise that resolves when the AJAX call succeeds.
return new Promise((resolve, reject) => {
$.ajax({
url: '/admin/aidmi/describe-image-ajax/' + uuid,
type: 'GET',
success: function (data) {
// Resolve the promise with the data from the AJAX call
// Resolve the promise with the data from the AJAX call.
resolve(data);
},
error: function (xhr, status, error) {
// Reject the promise if there's an error
// Reject the promise if there's an error.
reject(error);
}
});
});
}
// Open dialog for AIDmi.
function openOffCanvasDialog(content, callback) {
// Capture the current selection before opening the dialog
captureSelection(activeEditorInstance);
// 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');
// Set title text.
let tempETitle = 'AIDmi Description';
let tempESubTitle = 'Please evaluate for relevance and accuracy.';
// Create screen for dialog.
tempElement.innerHTML = `
<div>
<p id="aidmi-dialog-subtitle" class="aidmi-dialog-subtitle">${Drupal.t(tempESubTitle)}</p>
<div id="aidmi-dialog-description" class="aidmi-dialog-content-scrollable aidmi-dialog-description">${Drupal.t(content)}</div>
</div>`;
// Store the element that triggered the dialog, to return focus later.
const previousFocus = document.activeElement;
// Use the Drupal off-canvas dialog to show the content.
const options = {
dialogClass: 'aidmi-dialog',
title: tempETitle,
width: '400px',
buttons: [
{
text: Drupal.t('Insert Text'),
click: function() {
// Return true when Insert Text is clicked.
callback(true);
// Close the dialog.
$(tempElement).dialog('close');
}
},
{
text: Drupal.t('Cancel'),
click: function() {
// Return false when Cancel is clicked.
callback(false);
// Close the dialog.
$(tempElement).dialog('close');
}
}
],
close: function () {
// Return focus to the original element when the dialog is closed.
previousFocus.focus();
// Restore the CKEditor selection after closing the dialog
restoreSelection(activeEditorInstance);
}
};
// Open the dialog using Drupal's dialog API.
const dialogInstance = Drupal.dialog(tempElement, options);
dialogInstance.showModal();
// Set focus on the description to be read by screen readers.
const descriptionElement = document.getElementById('aidmi-dialog-description');
descriptionElement.setAttribute('tabindex', '-1');
// Ensure Esc key closes the dialog
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
$(tempElement).dialog('close');
}
});
}
// Update image alt tag in CKEditor
function updateImageAltInCKEditor(editorInstance, imgUuid, newAltText) {
// Get the model from the CKEditor instance.
const model = editorInstance.model;
// Use the model.change() to make sure CKEditor is aware of the modification.
model.change(writer => {
// Get the root of the document.
const root = model.document.getRoot();
// Traverse the root to find the image element by uuid.
for (const item of model.createRangeIn(root)) {
const element = item.item;
// Check if the element is an image and matches the imgUuid
if (element.is('element', 'imageBlock') || element.is('element', 'imageInline')) {
// Get the element in CKEditor by the UUID.
const uuid = element.getAttribute('dataEntityUuid');
if (uuid === imgUuid) {
// Update the alt attribute using CKEditor's writer
writer.setAttribute('alt', Drupal.t(newAltText), element);
// Set the selection on the image element.
const range = model.createRangeOn(element);
writer.setSelection(range);
}
}
}
});
}
}
};
})(jQuery, Drupal, once);
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment