How to Delete an Annotation Using Wagtail Review and Annotator.js on Button Click?
I am working with both Wagtail Review and Annotator.js in my project. I want to add a delete button (an "X" inside a red circle) to annotations, and when clicked, it should delete the corresponding annotation. I have added the delete button inside the annotation's controls, but I am struggling to properly trigger the deletion.
What I have so far:
- I am using Annotator.js to create and manage annotations. The annotations appear on hover over the parent element (
annotator-outer annotator-viewer
). - The delete button is injected dynamically inside the
.annotator-controls
span when the annotation becomes visible. - I am trying to use the click event to trigger the annotation deletion, but I am not sure how to properly call the
Annotator.js
delete function or if I am handling the interaction correctly within Wagtail Review.
Here’s the current approach I have:
- When the annotation parent (
.annotator-outer.annotator-viewer
) is hovered, I dynamically add the "X" delete button inside.annotator-controls
. - I attach a click event listener to the delete button.
- I expect the click to trigger the annotation deletion using Annotator.js.
Code snippet:
document.addEventListener('DOMContentLoaded', (event) => {
function deleteAnnotation() {
console.log('Delete function triggered!');
// Assuming annotator.js method is accessible to delete the annotation
if (window.annotator && window.annotator.deleteSelected) {
window.annotator.deleteSelected();
console.log('Annotation deleted');
} else {
console.log('annotator.js method not found.');
}
}
function injectTextAndAddEvent(span) {
span.textContent = '×'; // Add delete button content
span.addEventListener('click', deleteAnnotation); // Attach click event
}
document.querySelectorAll('.annotator-outer.annotator-viewer').forEach(parentDiv => {
parentDiv.addEventListener('mouseenter', function() {
const span = parentDiv.querySelector('.annotator-controls');
if (span) {
injectTextAndAddEvent(span);
}
});
});
});
Problem:
- The button appears as expected, but clicking it does not delete the annotation.
- I'm not sure if I'm using the correct
Annotator.js
method or if there’s a better way to interact with the annotations inside the Wagtail Review framework.
What I need help with:
- How can I properly trigger the deletion of an annotation using Annotator.js when clicking the delete button?
- Is there a specific method in Annotator.js or Wagtail Review that I should use for deleting annotations?
I would appreciate any insights from someone familiar with both Annotator.js and Wagtail Review.
Modify deleteAnnotation
to take the annotation object and use removeAnnotation
to delete it.
document.addEventListener('DOMContentLoaded', () => {
function deleteAnnotation(annotation) {
window.annotator?.removeAnnotation(annotation);
}
function injectDeleteButton(span, annotation) {
span.textContent = '×';
span.onclick = () => deleteAnnotation(annotation);
}
document.querySelectorAll('.annotator-outer.annotator-viewer').forEach(parent => {
parent.onmouseenter = () => {
const span = parent.querySelector('.annotator-controls');
const annotation = /* fetch the annotation */;
span && injectDeleteButton(span, annotation);
};
});
});
To delete an annotation both in the frontend and backend, we've made updates to the JavaScript code and the Django view annotations_api.py
.
The goal here is to dynamically delete an annotation in both the front-end and back-end without requiring a page reload.
Steps to Delete the Annotation
1. JavaScript (Frontend)
- The JavaScript code listens for
mouseenter
andmouseleave
events on elements with the classannotator-outer annotator-viewer
. - When an annotation element is hovered, it identifies the annotation's ID and dynamically attaches a delete event to a "Delete" button within the annotation container.
- When the delete button is clicked, it triggers an API DELETE request to remove the annotation from the backend and also removes the annotation's highlight on the frontend.
JavaScript Code
document.addEventListener('DOMContentLoaded', function() {
const csrfToken = document.getElementById('wagtailreview-respond-form').csrfmiddlewaretoken.value;
// Function to remove the highlight from the text corresponding to the deleted annotation
function removeHighlight(annotationId) {
const highlights = document.querySelectorAll(`[data-annotation-id="${annotationId}"]`);
highlights.forEach(function(highlight) {
highlight.classList.remove('annotator-hl'); // Option 1: Reset background color
// Option 2: Remove the highlight element entirely
// highlight.parentNode.removeChild(highlight);
});
}
// Function to delete the annotation by sending an API DELETE request
function deleteAnnotation(annotationId) {
const prefix = app.annotations.store.options.prefix;
const deleteUrl = `${prefix}annotations/${annotationId}/`;
// Retrieve necessary headers
const mode = app.annotations.store.options.headers["X-WagtailReview-mode"];
const reviewerId = app.annotations.store.options.headers["X-WagtailReview-reviewer"];
const token = app.annotations.store.options.headers["X-WagtailReview-token"];
fetch(deleteUrl, {
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
'X-WagtailReview-Mode': mode,
'X-WagtailReview-Reviewer': reviewerId,
'X-WagtailReview-Token': token,
'Content-Type': 'application/json'
}
})
.then(response => {
if (response.ok) {
console.log(`Annotation ${annotationId} deleted successfully`);
removeHighlight(annotationId); // Call this function to remove the highlight
} else {
console.error(`Failed to delete annotation ${annotationId}: ${response.status}`);
}
})
.catch(error => {
console.error('Error during deletion:', error);
});
}
// Function to inject the delete event into the span
function injectTextAndAddEvent(span, annotationId) {
span.addEventListener('click', function() {
deleteAnnotation(annotationId); // Call delete function with the correct ID
});
}
document.querySelectorAll('.annotator-outer.annotator-viewer').forEach(parentDiv => {
parentDiv.addEventListener('mouseenter', function() {
const textDiv = parentDiv.querySelector('ul li div');
const annotationText = textDiv.childNodes[1].textContent.trim(); // Get only the second child node, which is the text
app.annotations.query().then(response => {
const annotations = response.results;
const span = parentDiv.querySelector('.annotator-controls');
annotations.forEach(annotation => {
if (annotation.text === annotationText) {
const annotationId = annotation.id;
if (span) {
injectTextAndAddEvent(span, annotationId);
}
}
});
});
});
parentDiv.addEventListener('mouseleave', function() {
const span = parentDiv.querySelector('.annotator-controls');
if (span) {
span.removeEventListener('click', deleteAnnotation); // Optional clean-up
}
});
});
});
Explanation of the JavaScript Code
- removeHighlight: Removes the highlight of the deleted annotation by referencing the
data-annotation-id
. - deleteAnnotation: Sends a DELETE request to the backend, and upon a successful response, removes the annotation’s highlight.
- injectTextAndAddEvent: Injects the delete functionality into the annotation's control button when the annotation is hovered.
2. Django Backend Update (annotations_api.py)
The view responsible for handling annotations in annotations_api.py
is updated to allow DELETE requests.
Updated item
View in annotations_api.py
@never_cache
def item(request, id):
reviewer, mode = _check_reviewer_credentials(request)
if request.method == 'GET':
annotation = get_object_or_404(Annotation, id=id)
# only allow retrieving annotations within the same review as the current user's credentials
if reviewer.review != annotation.reviewer.review:
raise PermissionDenied
return JsonResponse(annotation.as_json_data())
elif request.method == 'DELETE':
annotation = get_object_or_404(Annotation, id=id)
# Check if the reviewer is allowed to delete the annotation
if reviewer.review != annotation.reviewer.review:
raise PermissionDenied
# If the reviewer has permission, delete the annotation
annotation.delete()
return JsonResponse({'status': 'Annotation deleted successfully'}, status=204)
else:
return HttpResponseNotAllowed(['GET', 'DELETE'], "Method not allowed")
Explanation of the Django Code
- GET Request: Retrieves a specific annotation by ID, ensuring the reviewer has permission.
- DELETE Request: Deletes the annotation by ID if the reviewer has permission. Returns a 204 status indicating successful deletion.
Summary:
- Frontend: JavaScript dynamically injects a delete event handler, sends a DELETE request, and removes highlights.
- Backend:
annotations_api.py
processes DELETE requests to remove the annotation from the database securely.
This setup ensures that both the frontend and backend handle annotation deletions without refreshing the page. Let me know if this solution fits your requirements or if additional adjustments are needed.