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

Issue #3475416 by mferanda: Split off files, rename, and code clean-up

parent 847bd849
Branches
Tags 1.0.0-beta11
No related merge requests found
## AIDmi - AI, describe my image!
AIDmi provides a button that will use an AI API to send an image to return a detailed description.
## Installation
Make sure you have gemini-api-php/client installed.
composer require gemini-api-php/client
Enable aidmi.
drush en aidmi
- Configure the settings under Configure > Web services > AIDmi Settings
- Select API: Only Gemini at the moment.
- API Key Input Method: API key in Settings or File Path
- File Path is recommended. Make sure you save the file outside of web/ (Example on page)
- API Instructions: Default text provided that is sent to AI for image description.
......@@ -3,7 +3,8 @@ aidmi_ckeditor:
theme:
css/aidmi.css: {}
js:
js/aidmi_ckeditor.js: { preprocess: false }
js/aidmi.ckeditor.js: { preprocess: false }
js/aidmi.dialog.js: { preprocess: false }
dependencies:
- core/ckeditor5
\ No newline at end of file
......@@ -2,4 +2,4 @@ selected_api: 'gemini' # Set the default API to 'gemini'
api_input_method: 'api_key'
api_key: ''
api_key_file_path: '/api_keys/aidmi.key'
api_instructions: 'Describe this image in detail, following the latest WCAG standards, without HTML, for insertion into an image alt tag.'
api_instructions: 'Describe the image in context of a web document. For graphs, provide the story being shown as well. This is to meet web accessibility requirements.'
.aidmi-dialog {
max-width: 400px;
height: auto;
}
......@@ -7,15 +6,19 @@
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-content-scrollable {
max-height: 300px;
overflow-y: auto;
padding: 10px;
}
.aidmi-dialog-subtitle {
font-size: 0.9em;
color: #666; /* Use a color that ensures sufficient contrast */
margin-top: -10px; /* Adjust spacing as needed */
color: #666;
margin-top: -10px;
margin-bottom: 10px;
}
\ No newline at end of file
}
.aidmi-dialog-textarea {
resize:vertical;
}
let aidmiActiveEditorInstance;
let aidmiLastSelection;
let aidmiEditedText;
(function ($, Drupal, once) {
Drupal.aidmi = Drupal.aidmi || {};
Drupal.behaviors.ckeditorImageUploadMonitor = {
attach: function (context, settings) {
// 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) {
aidmiLastSelection = selection.getFirstRange().clone();
}
}
// Restore that last selection.
function restoreSelection(editorInstance) {
if (aidmiLastSelection) {
editorInstance.model.change(writer => {
// Restore the selection.
writer.setSelection(aidmiLastSelection);
});
// 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.
editorElement.addEventListener('focus', function () {
aidmiActiveEditorInstance = editorElement.ckeditorInstance;
Drupal.aidmi.activeEditorInstance = editorElement.ckeditorInstance;
});
});
// 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).
const config = {
childList: true, // Monitor additions or removals of child elements.
......@@ -68,9 +40,9 @@ let aidmiEditedText;
// Check if the alternative text form exists.
altTextForm = document.querySelector('.ck-text-alternative-form');
// Get the selected image.
imgTag = aidmiActiveEditorInstance.data.stringify(aidmiActiveEditorInstance.model.getSelectedContent(aidmiActiveEditorInstance.model.document.selection));
imgTag = Drupal.aidmi.activeEditorInstance.data.stringify(Drupal.aidmi.activeEditorInstance.model.getSelectedContent(Drupal.aidmi.activeEditorInstance.model.document.selection));
if ((altTextForm) && (altTextField) && (imgTag)) {
aidmiButton(altTextField, aidmiActiveEditorInstance, imgTag);
Drupal.aidmi.aidmiButton(altTextField, Drupal.aidmi.activeEditorInstance, imgTag);
}
}
}
......@@ -78,19 +50,27 @@ let aidmiEditedText;
};
// Create an instance of MutationObserver and pass the callback function.
const observer = new MutationObserver(callback);
const observer = new MutationObserver(callback);
// Select the target node (element) you want to observe.
const targetNode = document.body; // You can change this to any specific element.
// Start observing the target node with the provided configuration.
observer.observe(targetNode, config);
}
}
Drupal.behaviors.aidmiCKEditorFunctions = {
attach: function (context, settings) {
Drupal.aidmi.lastSelection = '';
// Function to add the AIDmi Button.
function aidmiButton(altTextField, ckEditorInstance, imgTag) {
Drupal.aidmi.aidmiButton = function (altTextField, ckEditorInstance, imgTag) {
// 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.
aidmiEditedText = ''; // Make sure previous text is wiped out.
let aidmiEditedText = '';
if (!existingButton) {
const button = document.createElement('button');
......@@ -119,15 +99,19 @@ let aidmiEditedText;
button.disabled = true;
button.textContent = 'AIDmi Processing...';
// Call the aidmiAjax function and handle the result with .then().
imgDesc = aidmiAjax(imgUuid)
imgDesc = Drupal.aidmi.aidmiAjax(imgUuid)
.then((data) => {
// Capture the current selection before opening the dialog.
captureSelection(Drupal.aidmi.activeEditorInstance);
// Open the Off-Canvas Dialog and show the retrieved description.
openOffCanvasDialog(data, (isConfirmed) => {
Drupal.aidmi.aidmiDialog(data, (isConfirmed) => {
if (isConfirmed) {
// Check if the imgTag has an alt attribute.
updateImageAltInCKEditor(ckEditorInstance, imgUuid, aidmiEditedText);
}
});
});
// Restore the CKEditor selection after closing the dialog.
restoreSelection(Drupal.aidmi.activeEditorInstance);
})
.catch((error) => {
console.log('Error:', error); // Handle errors here.
......@@ -147,9 +131,7 @@ let aidmiEditedText;
}
}
function aidmiAjax(uuid) {
// Initially clear the field or show a placeholder/loading message.
altTextField.value = ''; // Keep the field empty.
Drupal.aidmi.aidmiAjax = function (uuid) {
// Return a Promise that resolves when the AJAX call succeeds.
return new Promise((resolve, reject) => {
$.ajax({
......@@ -167,89 +149,27 @@ let aidmiEditedText;
});
}
// Open dialog for AIDmi.
function openOffCanvasDialog(content, callback) {
// Capture the current selection before opening the dialog.
captureSelection(aidmiActiveEditorInstance);
// 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 = `
<div>
<h2 id="aidmi-dialog-title">${Drupal.t(tempETitle)}</h2>
<p id="aidmi-dialog-subtitle" class="aidmi-dialog-subtitle">${Drupal.t(tempESubTitle)}</p>
<textarea id="aidmi-dialog-textarea" class="aidmi-dialog-textarea" rows="6" style="width: 100%;">${Drupal.t(content)}</textarea>
</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 () {
// Get the value from the textarea.
aidmiEditedText = document.getElementById('aidmi-dialog-textarea').value;
// Pass the modified text back through the callback.
callback(aidmiEditedText);
// Close the dialog.
$(tempElement).dialog('close');
$(tempElement).dialog('destroy').remove(); // Properly remove the dialog from the DOM.
}
},
{
text: Drupal.t('Cancel'),
click: function () {
// Return false when Cancel is clicked.
callback(false);
// Close the dialog.
$(tempElement).dialog('close');
$(tempElement).dialog('destroy').remove(); // Properly remove the dialog from the DOM.
}
}
],
close: function () {
// Return focus to the original element when the dialog is closed.
previousFocus.focus();
// Restore the CKEditor selection after closing the dialog.
restoreSelection(aidmiActiveEditorInstance);
}
};
// Open the dialog using Drupal's dialog API.
const dialogInstance = Drupal.dialog(tempElement, options);
dialogInstance.showModal();
// Set focus on the textarea for screen readers.
const textareaElement = document.getElementById('aidmi-dialog-textarea');
textareaElement.setAttribute('tabindex', '-1');
textareaElement.focus();
// 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) {
Drupal.aidmi.lastSelection = selection.getFirstRange().clone();
}
}
// Ensure the Esc key closes the dialog.
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
$(tempElement).dialog('close');
}
});
// Restore that last selection.
function restoreSelection(editorInstance) {
if (Drupal.aidmi.lastSelection) {
editorInstance.model.change(writer => {
// Restore the selection.
writer.setSelection(Drupal.aidmi.lastSelection);
});
// Focus the editor view.
editorInstance.editing.view.focus();
}
}
// Update image alt tag in CKEditor
......
(function ($, Drupal) {
Drupal.behaviors.aidmiDialog = {
attach: function (context, settings) {
Drupal.aidmi = Drupal.aidmi || {};
Drupal.aidmi.aidmiDialog = function (content, 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 = `
<div>
<h2 id="aidmi-dialog-title">${Drupal.t(tempETitle)}</h2>
<p id="aidmi-dialog-subtitle" class="aidmi-dialog-subtitle">${Drupal.t(tempESubTitle)}</p>
<textarea id="aidmi-dialog-textarea" class="aidmi-dialog-textarea" rows="6" style="width: 100%;">${Drupal.t(content)}</textarea>
</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 () {
// Get the value from the textarea.
aidmiEditedText = document.getElementById('aidmi-dialog-textarea').value;
// Pass the modified text back through the callback.
callback(aidmiEditedText);
// Close the dialog.
$(tempElement).dialog('close');
$(tempElement).dialog('destroy').remove(); // Properly remove the dialog from the DOM.
}
},
{
text: Drupal.t('Cancel'),
click: function () {
// Return false when Cancel is clicked.
callback(false);
// Close the dialog.
$(tempElement).dialog('close');
$(tempElement).dialog('destroy').remove(); // Properly remove the dialog from the DOM.
}
}
],
close: function () {
// Return focus to the original element when the dialog is closed.
previousFocus.focus();
}
};
// Open the dialog using Drupal's dialog API.
const dialogInstance = Drupal.dialog(tempElement, options);
dialogInstance.showModal();
// Set focus on the textarea for screen readers.
const textareaElement = document.getElementById('aidmi-dialog-textarea');
textareaElement.setAttribute('tabindex', '-1');
textareaElement.focus();
// Ensure the Esc key closes the dialog.
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
// Close the dialog.
$(tempElement).dialog('close');
$(tempElement).dialog('destroy').remove(); // Properly remove the dialog from the DOM.
}
});
}
}
};
})(jQuery, Drupal);
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment