Can you make sortable.js work with boostrap 5 modal?
I'm building a checklist app for fun and I'm trying to use sortable.js with python django.
I can make a sortable list work in this example with the html as follows
{% extends 'BJJApp/base.html' %}
{% load static %}
{%load crispy_forms_tags %}
{% block content %}
<br><br>
<div id="standalone-items-container">
{% for item, formset, links in standalone_items_formsets_links %}
<div class="modal fade" id="exampleModalToggle-{{ item.id }}" aria-hidden="true" aria-labelledby="exampleModalToggleLabel-{{ item.id }}" data-item-id="{{ item.id }}" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="exampleModalToggleLabel-{{ item.id }}" style="color: {% if item.important %}red{% else %}inherit{% endif %};">{{ item.title }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="POST" id="main-form-{{ item.id }}" action="{% url 'viewitem' item.id %}">
{% csrf_token %}
<div class="form-group">
<label for="title">Title</label>
<input type="text" name="title" class="form-control" id="title-{{ item.id }}" value="{{ item.title }}" required disabled>
</div>
<div class="form-group">
<label for="memo">Memo</label>
<textarea name="memo" rows="5" class="form-control" id="memo-{{ item.id }}" disabled>{{ item.memo }}</textarea>
</div>
<div class="form-group form-check">
<input type="checkbox" name="important" class="form-check-input" id="important-{{ item.id }}" {% if item.important %}checked{% endif %} disabled>
<label class="form-check-label" for="important">Important</label>
</div>
</form>
</div>
<div id="links-{{ item.id }}">
{% if links %}
<ul>
{% for link in links %}
<li><a href="{{ link.url }}" target="_blank">{{ link.url|urlizetrunc:50 }}</a></li>
{% endfor %}
</ul>
{% else %}
<p> No links available for this item.</p>
{% endif %}
</div>
<div class="d-flex justify-content-end">
<a href="{% url 'updatelinks' item.id %}" style="display: none" id="updatelinks-{{ item.id }}">
<button type="button" class="btn btn-warning me-5">
Add or Remove Links
</button>
</a>
</div>
<br>
<div class="modal-footer" >
<button type="button" id="edit-button-{{ item.id }}" class="btn btn-primary me-2" onclick="toggleEdit({{ item.id }})">Edit</button>
<!-- Complete Button Form (if item is not completed) -->
{% if item.datecompleted is None %}
<form method="POST" action="{% url 'completeitem' item.id %}" style="display:inline-block;">
{% csrf_token %}
<button type="submit" class="btn btn-success me-2">Complete</button>
</form>
{% endif %}
<!-- UnComplete Button Form (if item is completed) -->
{% if item.datecompleted %}
<form method="POST" action="{% url 'uncompleteitem' item.id %}" style="display:inline-block;">
{% csrf_token %}
<button type="submit" class="btn btn-success me-2">UnComplete</button>
</form>
{% endif %}
<!-- Delete Button Form -->
<form method="POST" action="{% url 'deleteitem' item.id %}" style="display:inline-block;">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Delete</button>
</form>
</div>
</div>
</div>
</div>
<!--
<button class="btn btn-primary" id="exampleModalToggleButton-{{item.id}}" data-bs-target="#exampleModalToggle-{{ item.id }}" data-bs-toggle="modal" style="color: {% if item.important %}red{% else %}inherit{% endif %};" onclick="storeModalId('{{ item.id }}')">
{{ item.title }}
</button> -->
<div class="card mb-3" style="max-width: 800px;" draggable="true" data-item-id="{{ item.id }}">
<div class="card-body d-flex justify-content-between align-items-center" style="cursor: pointer;">
<!-- <div class="card-body d-flex justify-content-between align-items-center" data-bs-target="#exampleModalToggle-{{ item.id }}" data-bs-toggle="modal" onclick="storeReferrerAndModal('{{ item.id }}', false)" style="cursor: pointer;"> -->
<!-- Card Content -->
<div>
<h5 class="card-title" id="card-title-{{ item.id }}" style="color: {% if item.important %}red{% else %}inherit{% endif %};" >{{ forloop.counter }}. {{ item.title }}</h5>
<p class="card-text">{{ item.memo }}</p>
</div>
<!-- Buttons -->
<div>
<button class="btn btn-primary" id="exampleModalToggleButton-{{item.id}}" data-bs-target="#exampleModalToggle-{{ item.id }}" data-bs-toggle="modal" onclick="storeReferrerAndModal('{{ item.id }}', false)">
Details
</button>
<!-- Complete Button Form (if item is not completed) -->
{% if item.datecompleted is None %}
<form method="POST" action="{% url 'completeitem' item.id %}" style="display:inline-block;">
{% csrf_token %}
<button type="submit" class="btn btn-success">Complete</button>
</form>
{% endif %}
<!-- UnComplete Button Form (if item is completed) -->
{% if item.datecompleted %}
<form method="POST" action="{% url 'uncompleteitem' item.id %}" style="display:inline-block;">
{% csrf_token %}
<button type="submit" class="btn btn-success">UnComplete</button>
</form>
{% endif %}
<!-- Delete Button Form -->
<form method="POST" action="{% url 'deleteitem' item.id %}" style="display:inline-block;">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Delete</button>
</form>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
{% block scripts %}
<script src="{% static 'Checklist/Checklist.js' %}" ></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const container = document.getElementById('standalone-items-container');
const csrfToken = '{{ csrf_token }}'; // CSRF token for secure POST requests
Sortable.create(container, {
animation: 150, // Smooth animation while dragging
onEnd: function (event) {
// Get the updated order of item IDs
const updatedOrder = Array.from(container.children).map((card, index) => {
// Update the displayed order on the card
const titleElement = card.querySelector('.card-title');
titleElement.textContent = `${index + 1}. ${titleElement.textContent.split('. ').slice(1).join('. ')}`;
return card.dataset.itemId;
});
// Send the updated order to the backend
fetch("{% url 'update_item_order' %}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": csrfToken, // CSRF token for Django
},
body: JSON.stringify({ order: updatedOrder }),
})
.then(response => {
if (!response.ok) {
throw new Error("Failed to update order.");
}
return response.json();
})
.then(data => {
console.log("Order updated:", data);
})
.catch(error => {
console.error("Error updating order:", error);
});
},
});
});
</script>
{% endblock %}
in my views I have
def test5(request):
items = Item.objects.filter(user=request.user, datecompleted__isnull=True)
if request.user.profile.role == "instructor":
courses = request.user.checklist_courses.filter(related_course__isnull=False)
else:
courses = request.user.checklist_courses.exclude(
creator__profile__role="instructor"
)
courses_percentages = []
standalone_items_formsets_links = []
course_items_formsets_links = []
standalone_items = items.filter(courses__isnull=True).order_by("order")
course_items = items.filter(courses__isnull=False)
for item in standalone_items:
LanguageFormSet = inlineformset_factory(Item, Link, fields=("url",), extra=1)
formset = LanguageFormSet(instance=item)
links = Link.objects.filter(item=item)
standalone_items_formsets_links.append((item, formset, links))
for item in course_items:
LanguageFormSet = inlineformset_factory(Item, Link, fields=("url",), extra=1)
formset = LanguageFormSet(instance=item)
links = Link.objects.filter(item=item)
course_items_formsets_links.append((item, formset, links))
for course in courses:
total_items = course.items.count()
completed_items = course.items.filter(datecompleted__isnull=False).count()
# Avoid division by zero
if total_items > 0:
progress_percentage = (completed_items / total_items) * 100
else:
progress_percentage = 0
courses_percentages.append((course, progress_percentage))
return render(
request,
"BJJApp/test5.html",
{
"standalone_items": standalone_items,
"courses_percentages": courses_percentages,
"standalone_items_formsets_links": standalone_items_formsets_links,
"course_items_formsets_links": course_items_formsets_links,
},
)
def update_item_order(request):
if request.method == "POST":
try:
data = json.loads(request.body)
item_ids = data.get("order", [])
# Update the order field for each item
for idx, item_id in enumerate(item_ids, start=1):
Item.objects.filter(id=item_id).update(order=idx)
return JsonResponse({"success": True})
except Exception as e:
return JsonResponse({"success": False, "error": str(e)}, status=400)
return JsonResponse(
{"success": False, "error": "Invalid request method."}, status=405
)
this works fine and I can drag and drop and update the order number and display the updated number of the items in the card.
but when I change it to modal, it doesn't work and doesn't update. Can anyone help?
{% extends 'BJJApp/base.html' %}
{% load static %}
{%load crispy_forms_tags %}
{% block content %}
<br><br>
<div id="standalone-items-container">
{% for item, formset, links in standalone_items_formsets_links %}
<div class="modal fade" id="exampleModalToggle-{{ item.id }}" aria-hidden="true" aria-labelledby="exampleModalToggleLabel-{{ item.id }}" data-item-id="{{ item.id }}" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="exampleModalToggleLabel-{{ item.id }}" style="color: {% if item.important %}red{% else %}inherit{% endif %};">{{ item.title }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="POST" id="main-form-{{ item.id }}" action="{% url 'viewitem' item.id %}">
{% csrf_token %}
<div class="form-group">
<label for="title">Title</label>
<input type="text" name="title" class="form-control" id="title-{{ item.id }}" value="{{ item.title }}" required disabled>
</div>
<div class="form-group">
<label for="memo">Memo</label>
<textarea name="memo" rows="5" class="form-control" id="memo-{{ item.id }}" disabled>{{ item.memo }}</textarea>
</div>
<div class="form-group form-check">
<input type="checkbox" name="important" class="form-check-input" id="important-{{ item.id }}" {% if item.important %}checked{% endif %} disabled>
<label class="form-check-label" for="important">Important</label>
</div>
</form>
</div>
<div id="links-{{ item.id }}">
{% if links %}
<ul>
{% for link in links %}
<li><a href="{{ link.url }}" target="_blank">{{ link.url|urlizetrunc:50 }}</a></li>
{% endfor %}
</ul>
{% else %}
<p> No links available for this item.</p>
{% endif %}
</div>
<div class="d-flex justify-content-end">
<a href="{% url 'updatelinks' item.id %}" style="display: none" id="updatelinks-{{ item.id }}">
<button type="button" class="btn btn-warning me-5">
Add or Remove Links
</button>
</a>
</div>
<br>
<div class="modal-footer" >
<button type="button" id="edit-button-{{ item.id }}" class="btn btn-primary me-2" onclick="toggleEdit({{ item.id }})">Edit</button>
<!-- Complete Button Form (if item is not completed) -->
{% if item.datecompleted is None %}
<form method="POST" action="{% url 'completeitem' item.id %}" style="display:inline-block;">
{% csrf_token %}
<button type="submit" class="btn btn-success me-2">Complete</button>
</form>
{% endif %}
<!-- UnComplete Button Form (if item is completed) -->
{% if item.datecompleted %}
<form method="POST" action="{% url 'uncompleteitem' item.id %}" style="display:inline-block;">
{% csrf_token %}
<button type="submit" class="btn btn-success me-2">UnComplete</button>
</form>
{% endif %}
<!-- Delete Button Form -->
<form method="POST" action="{% url 'deleteitem' item.id %}" style="display:inline-block;">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Delete</button>
</form>
</div>
</div>
</div>
</div>
<!--
<button class="btn btn-primary" id="exampleModalToggleButton-{{item.id}}" data-bs-target="#exampleModalToggle-{{ item.id }}" data-bs-toggle="modal" style="color: {% if item.important %}red{% else %}inherit{% endif %};" onclick="storeModalId('{{ item.id }}')">
{{ item.title }}
</button> -->
<div class="card" style="max-width: 800px;" data-item-id="{{ item.id }}">
<div class="card-body d-flex justify-content-between align-items-center" data-bs-target="#exampleModalToggle-{{ item.id }}" data-bs-toggle="modal" onclick="storeReferrerAndModal('{{ item.id }}', false)" style="cursor: pointer;">
<!-- Card Content -->
<div>
<h5 class="card-title" id="card-title-{{ item.id }}" style="color: {% if item.important %}red{% else %}inherit{% endif %};" >{{ forloop.counter }}. {{ item.title }}</h5>
<p class="card-text">{{ item.memo }}</p>
</div>
<!-- Buttons -->
<div>
<button class="btn btn-primary" id="exampleModalToggleButton-{{item.id}}" data-bs-target="#exampleModalToggle-{{ item.id }}" data-bs-toggle="modal" onclick="storeReferrerAndModal('{{ item.id }}', false)">
Details
</button>
<!-- Complete Button Form (if item is not completed) -->
{% if item.datecompleted is None %}
<form method="POST" action="{% url 'completeitem' item.id %}" style="display:inline-block;">
{% csrf_token %}
<button type="submit" class="btn btn-success">Complete</button>
</form>
{% endif %}
<!-- UnComplete Button Form (if item is completed) -->
{% if item.datecompleted %}
<form method="POST" action="{% url 'uncompleteitem' item.id %}" style="display:inline-block;">
{% csrf_token %}
<button type="submit" class="btn btn-success">UnComplete</button>
</form>
{% endif %}
<!-- Delete Button Form -->
<form method="POST" action="{% url 'deleteitem' item.id %}" style="display:inline-block;">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Delete</button>
</form>
</div>
</div>
</div>
<br>
{% endfor %}
</div>
{% endblock %}
{% block scripts %}
<script src="{% static 'Checklist/Checklist.js' %}" ></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const container = document.getElementById('standalone-items-container');
const csrfToken = '{{ csrf_token }}'; // CSRF token for secure POST requests
Sortable.create(container, {
animation: 150, // Smooth animation while dragging
onEnd: function (event) {
// Get the updated order of item IDs
const updatedOrder = Array.from(container.children).map((card, index) => {
// Update the displayed order on the card
const titleElement = card.querySelector('.card-title');
titleElement.textContent = `${index + 1}. ${titleElement.textContent.split('. ').slice(1).join('. ')}`;
return card.dataset.itemId;
});
// Send the updated order to the backend
fetch("{% url 'update_item_order' %}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": csrfToken, // CSRF token for Django
},
body: JSON.stringify({ order: updatedOrder }),
})
.then(response => {
if (!response.ok) {
throw new Error("Failed to update order.");
}
return response.json();
})
.then(data => {
console.log("Order updated:", data);
})
.catch(error => {
console.error("Error updating order:", error);
});
},
});
});
</script>
{% endblock %}
Thanks