From 533f6aaf00f382b59bcdbee8dbaa386b4b98ed5b Mon Sep 17 00:00:00 2001 From: Mike Feranda <26969-mferanda@users.noreply.drupalcode.org> Date: Thu, 19 Sep 2024 02:45:43 +0000 Subject: [PATCH] Issue #3475416 by mferanda: Split off files, rename, and code clean-up --- README.md | 19 +++ aidmi.libraries.yml | 3 +- config/install/aidmi.settings.yml | 2 +- css/aidmi.css | 19 ++- js/{aidmi_ckeditor.js => aidmi.ckeditor.js} | 170 ++++++-------------- js/aidmi.dialog.js | 88 ++++++++++ 6 files changed, 166 insertions(+), 135 deletions(-) create mode 100644 README.md rename js/{aidmi_ckeditor.js => aidmi.ckeditor.js} (63%) create mode 100644 js/aidmi.dialog.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..4d22d1e --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +## 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. diff --git a/aidmi.libraries.yml b/aidmi.libraries.yml index fd1b6e9..3c30b13 100644 --- a/aidmi.libraries.yml +++ b/aidmi.libraries.yml @@ -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 diff --git a/config/install/aidmi.settings.yml b/config/install/aidmi.settings.yml index 1bdf5cf..00fe043 100644 --- a/config/install/aidmi.settings.yml +++ b/config/install/aidmi.settings.yml @@ -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.' diff --git a/css/aidmi.css b/css/aidmi.css index bc36f3c..d1fbeb1 100644 --- a/css/aidmi.css +++ b/css/aidmi.css @@ -1,5 +1,4 @@ .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; +} diff --git a/js/aidmi_ckeditor.js b/js/aidmi.ckeditor.js similarity index 63% rename from js/aidmi_ckeditor.js rename to js/aidmi.ckeditor.js index 2c8ddf6..9f97930 100644 --- a/js/aidmi_ckeditor.js +++ b/js/aidmi.ckeditor.js @@ -1,46 +1,18 @@ -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 diff --git a/js/aidmi.dialog.js b/js/aidmi.dialog.js new file mode 100644 index 0000000..af8b9ce --- /dev/null +++ b/js/aidmi.dialog.js @@ -0,0 +1,88 @@ +(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 -- GitLab