Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
A
aidmi
Manage
Activity
Members
Labels
Plan
Wiki
Custom issue tracker
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Locked files
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Model registry
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Code review analytics
Insights
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Terms and privacy
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
project
aidmi
Commits
954405da
Commit
954405da
authored
8 months ago
by
Mike Feranda
Browse files
Options
Downloads
Patches
Plain Diff
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
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
aidmi.info.yml
+1
-1
1 addition, 1 deletion
aidmi.info.yml
aidmi.libraries.yml
+3
-0
3 additions, 0 deletions
aidmi.libraries.yml
css/aidmi.css
+21
-0
21 additions, 0 deletions
css/aidmi.css
js/aidmi_ckeditor.js
+167
-46
167 additions, 46 deletions
js/aidmi_ckeditor.js
with
192 additions
and
47 deletions
aidmi.info.yml
+
1
−
1
View file @
954405da
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
This diff is collapsed.
Click to expand it.
aidmi.libraries.yml
+
3
−
0
View file @
954405da
aidmi_ckeditor
:
css
:
theme
:
css/aidmi.css
:
{}
js
:
js/aidmi_ckeditor.js
:
{
preprocess
:
false
}
dependencies
:
...
...
This diff is collapsed.
Click to expand it.
css/aidmi.css
0 → 100644
+
21
−
0
View file @
954405da
.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
This diff is collapsed.
Click to expand it.
js/aidmi_ckeditor.js
+
167
−
46
View file @
954405da
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
);
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment