commit e41db65b1d7f39bc7f0dde4e7eccf3378d318581
parent 11e296483e80674fb33d130f25d4ff01bded9311
Author: William Lindholm <william_lindholm@outlook.com>
Date: Fri, 10 Nov 2023 19:19:25 +0100
More refactoring.
Diffstat:
4 files changed, 209 insertions(+), 114 deletions(-)
diff --git a/database/database.db b/database/database.db
Binary files differ.
diff --git a/static/js/ajaxtable.js b/static/js/ajaxtable.js
@@ -1,101 +1,200 @@
-// Function to fetch data and update the table
-function fetchData(endpoint, queryParams) {
- const url = new URL(endpoint, window.location.origin);
- Object.keys(queryParams).forEach(key => url.searchParams.append(key, queryParams[key]));
-
- const xhr = new XMLHttpRequest();
- xhr.open('GET', url, true);
- xhr.setRequestHeader('Accept', 'text/html');
- xhr.onreadystatechange = function() {
- if (xhr.readyState === 4 && xhr.status === 200) {
- const tableBody = document.getElementById('table-body');
- tableBody.innerHTML = xhr.responseText;
- htmx.process(tableBody); // Reinitialize htmx for new content
-
- const totalPages = xhr.getResponseHeader('X-total-pages');
- const currentPage = xhr.getResponseHeader('X-current-page');
- updatePagination(parseInt(currentPage, 10), parseInt(totalPages, 10), queryParams.page_size, endpoint);
- }
- };
- xhr.send();
-}
-
-function searchData() {
- const searchInput = document.getElementById('search-bar').value;
- const pageSize = document.getElementById('page-size').value;
- const queryParams = {
- query: searchInput,
- page_size: pageSize
- };
- fetchData(API_ENDPOINT, queryParams);
-}
-
-function updatePagination(currentPage, totalPages, pageSize) {
- const container = document.getElementById('pagination-container');
- container.innerHTML = ''; // Clear existing pagination
-
- // create back button
- const backLi = document.createElement('li');
- backLi.className = currentPage === 1 ? 'disabled' : 'waves-effect';
- const backA = document.createElement('a');
- const backIcon = document.createElement('i');
- backIcon.className = 'material-icons';
- backIcon.textContent = 'chevron_left';
- backA.appendChild(backIcon);
- backA.onclick = function() {
- if (currentPage > 1) {
- fetchData(API_ENDPOINT, { page: currentPage - 1, page_size: pageSize });
+class AjaxTable {
+ constructor(apiEndpoint, tableHeadersId, tableBodyId, searchBarId, pageSizeSelectorId, paginationContainerId) {
+ this.apiEndpoint = apiEndpoint;
+ this.tableHeadersId = tableHeadersId;
+ this.tableBodyId = tableBodyId;
+ this.searchBarId = searchBarId;
+ this.pageSizeSelectorId = pageSizeSelectorId;
+ this.paginationContainerId = paginationContainerId;
+
+ this.currentPage = 1;
+ this.currentPageSize = 10; // Default page size
+ this.currentSearchQuery = '';
+ this.currentSortField = null;
+ this.currentSortOrder = null;
+
+ this.initEventListeners();
+ this.fetchData({ page: 1, page_size: 10 });
+ }
+
+ initEventListeners() {
+ const debouncedSearchData = this.debounce(this.searchData.bind(this), 500);
+ document.getElementById(this.searchBarId).addEventListener('keyup', debouncedSearchData);
+
+ document.getElementById(this.pageSizeSelectorId).addEventListener('change', () => {
+ this.currentPageSize = document.getElementById(this.pageSizeSelectorId).value;
+ this.currentPage = 1; // Reset to first page on page size change
+ this.fetchData();
+ });
+
+ document.addEventListener('htmx:afterRequest', (evt) => {
+ this.fetchData();
+ });
+
+ document.getElementById(this.tableBodyId).addEventListener('click', event => {
+ if (event.target.classList.contains('delete-button')) {
+ const itemId = event.target.dataset.itemId;
+ this.confirmDelete(this.apiEndpoint, itemId);
+ }
+ });
+
+ this.sortTableByHeader();
+ }
+
+ confirmDelete(apiEndpoint, itemId) {
+ const deleteButton = document.getElementById('confirmDeleteButton');
+ deleteButton.onclick = () => this.performDeletion(apiEndpoint, itemId);
+
+ // Open confirmation modal
+ var instance = M.Modal.getInstance(document.getElementById('deleteConfirmationModal'));
+ instance.open();
+ }
+
+ performDeletion(apiEndpoint, itemId) {
+ const deleteUrl = `${apiEndpoint}/${itemId}`;
+
+ fetch(deleteUrl, { method: 'DELETE' })
+ .then(response => {
+ if(response.ok) {
+ this.handleDeletionSuccess(itemId);
+ } else {
+ console.error('Deletion failed');
+ }
+ })
+ .catch(error => console.error('Error:', error));
+ }
+
+ handleDeletionSuccess(itemId) {
+ const rowToRemove = document.getElementById('itemRow-' + itemId);
+ if (rowToRemove) {
+ rowToRemove.remove();
+ } else {
+ this.fetchData(); // Refresh the table data
}
- };
- backLi.appendChild(backA);
- container.appendChild(backLi);
-
- // Create pagination buttons
- for (let i = 1; i <= totalPages; i++) {
- const li = document.createElement('li');
- li.className = i === currentPage ? 'active' : 'waves-effect';
- const a = document.createElement('a');
- a.textContent = i;
- a.onclick = function() {
- fetchData(API_ENDPOINT, { page: i, page_size: pageSize });
+ }
+
+ // Function to fetch data and update the table
+ fetchData() {
+ const queryParams = {
+ page: this.currentPage,
+ page_size: this.currentPageSize,
+ query: this.currentSearchQuery
};
- li.appendChild(a);
- container.appendChild(li);
+
+ if (this.currentSortField && this.currentSortOrder) {
+ queryParams.sort_by = this.currentSortField;
+ queryParams.sort_order = this.currentSortOrder;
+ }
+
+ const url = new URL(this.apiEndpoint, window.location.origin);
+ Object.keys(queryParams).forEach(key => url.searchParams.append(key, queryParams[key]));
+
+ fetch(url, { headers: { 'Accept': 'text/html' } })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ // Extract custom headers before processing the text
+ const totalPages = response.headers.get('X-total-pages');
+ const currentPage = response.headers.get('X-current-page');
+ this.updatePagination(parseInt(totalPages, 10));
+
+ return response.text();
+ })
+ .then(html => {
+ const tableBody = document.getElementById(this.tableBodyId);
+ tableBody.innerHTML = html;
+ htmx.process(tableBody); // Reinitialize htmx for new content
+ })
+ .catch(error => {
+ console.error('Fetch error:', error);
+ });
}
- // create forward button
- const forwardLi = document.createElement('li');
- forwardLi.className = currentPage === totalPages ? 'disabled' : 'waves-effect';
- const forwardA = document.createElement('a');
- const forwardIcon = document.createElement('i');
- forwardIcon.className = 'material-icons';
- forwardIcon.textContent = 'chevron_right';
- forwardA.appendChild(forwardIcon);
- forwardA.onclick = function() {
- if (currentPage < totalPages) {
- fetchData(API_ENDPOINT, { page: currentPage + 1, page_size: pageSize });
+ updatePagination(totalPages) {
+ console.log(totalPages)
+ const container = document.getElementById(this.paginationContainerId);
+ container.innerHTML = '';
+
+ // Create back button
+ const createBackButton = () => {
+ const li = document.createElement('li');
+ li.className = this.currentPage === 1 ? 'disabled' : 'waves-effect';
+ const a = document.createElement('a');
+ const icon = document.createElement('i');
+ icon.className = 'material-icons';
+ icon.textContent = 'chevron_left';
+ a.appendChild(icon);
+ a.onclick = () => {
+ if (this.currentPage > 1) {
+ this.currentPage -= 1;
+ this.fetchData({ page: this.currentPage - 1, page_size: this.currentPageSize });
+ }
+ };
+ li.appendChild(a);
+ return li;
+ };
+ container.appendChild(createBackButton());
+
+ // Create page buttons
+ for (let i = 1; i <= totalPages; i++) {
+ const li = document.createElement('li');
+ li.className = i === this.currentPage ? 'active' : 'waves-effect';
+ const a = document.createElement('a');
+ a.textContent = i;
+ a.onclick = () => {
+ this.currentPage = i;
+ this.fetchData({ page: i, page_size: this.currentPageSize });
+ };
+ li.appendChild(a);
+ container.appendChild(li);
}
- };
- forwardLi.appendChild(forwardA);
- container.appendChild(forwardLi);
-}
-
-// Function to sort table by clicking on headers
-function sortTableByHeader() {
- document.querySelectorAll('#table-headers th').forEach(header => {
- header.addEventListener('click', () => {
- const sortField = header.getAttribute('data-field');
- const sortOrder = header.getAttribute('data-order');
- const searchInput = document.getElementById('search-bar').value;
- const pageSize = document.getElementById('page-size').value || 10;
- const queryParams = {
- query: searchInput,
- sort_by: sortField,
- sort_order: sortOrder,
- page_size: pageSize
+
+ // Create forward button
+ const createForwardButton = () => {
+ const li = document.createElement('li');
+ li.className = this.currentPage === totalPages ? 'disabled' : 'waves-effect';
+ const a = document.createElement('a');
+ const icon = document.createElement('i');
+ icon.className = 'material-icons';
+ icon.textContent = 'chevron_right';
+ a.appendChild(icon);
+ a.onclick = () => {
+ if (this.currentPage < totalPages) {
+ this.currentPage += 1;
+ this.fetchData({ page: this.currentPage + 1, page_size: this.currentPageSize });
+ }
};
- fetchData(API_ENDPOINT, queryParams);
- header.setAttribute('data-order', sortOrder === 'asc' ? 'desc' : 'asc');
+ li.appendChild(a);
+ return li;
+ };
+ container.appendChild(createForwardButton());
+ }
+
+ debounce(func, delay) {
+ let debounceTimer;
+ return function() {
+ const context = this;
+ const args = arguments;
+ clearTimeout(debounceTimer);
+ debounceTimer = setTimeout(() => func.apply(context, args), delay);
+ };
+ }
+
+ searchData() {
+ this.currentSearchQuery = document.getElementById(this.searchBarId).value;
+ this.fetchData();
+ }
+
+ sortTableByHeader() {
+ document.querySelectorAll(`#${this.tableHeadersId} th`).forEach(header => {
+ header.addEventListener('click', () => {
+ this.currentSortField = header.getAttribute('data-field');
+ this.currentSortOrder = header.getAttribute('data-order') === 'asc' ? 'desc' : 'asc';
+ header.setAttribute('data-order', this.currentSortOrder);
+ this.fetchData();
+ });
});
- });
+ }
}
\ No newline at end of file
diff --git a/templates/component/ajaxtable.html b/templates/component/ajaxtable.html
@@ -1,9 +1,20 @@
+<div id="deleteConfirmationModal" class="modal">
+ <div class="modal-content">
+ <h4>Confirm deletion</h4>
+ <p>Are you sure you want to delete this item?</p>
+ </div>
+ <div class="modal-footer">
+ <a href="#!" class="modal-close waves-effect waves-green btn-flat" id="confirmDeleteButton">Delete</a>
+ <a href="#!" class="modal-close waves-effect waves-red btn-flat">Cancel</a>
+ </div>
+</div>
+
<div class="row">
<div class="input-field col s10">
<input type="text" id="search-bar" placeholder="Search {{ api_endpoint }}...">
</div>
<div class="input-field col s2">
- <select id="page-size" onchange="searchData()">
+ <select id="page-size">
<option value="5">5</option>
<option value="10" selected>10</option>
<option value="25">25</option>
@@ -28,30 +39,15 @@
</table>
<ul class="pagination" id="pagination-container">
- <!-- Pagination -->
+ <!-- Pagination -->
</ul>
<script>
-
const API_ENDPOINT = '/api/{{ api_endpoint }}/';
document.addEventListener('DOMContentLoaded', function() {
M.AutoInit();
- const debouncedSearchData = debounce(searchData, 500);
- document.getElementById('search-bar').addEventListener('keyup', debouncedSearchData);
- sortTableByHeader('id');
-
- fetchData(API_ENDPOINT, { page: 1, page_size: 10 });
+ new AjaxTable(API_ENDPOINT, 'table-headers', 'table-body', 'search-bar', 'page-size', 'pagination-container');
});
- function debounce(func, delay) {
- let debounceTimer;
- return function() {
- const context = this;
- const args = arguments;
- clearTimeout(debounceTimer);
- debounceTimer = setTimeout(() => func.apply(context, args), delay);
- };
- }
-
-</script>
+</script>
+\ No newline at end of file
diff --git a/templates/component/messages_template.html b/templates/component/messages_template.html
@@ -1 +1 @@
-{% for message in messages %}<tr><td>{{ message.id }}</td><td>{{ message.relevance }}</td><td>{{ message.name }}</td><td>{{ message.content }}</td><td><a class="material-icons delete-button" hx-trigger="click" hx-delete="/api/messages/{{ message.id }}">delete</a></td></tr>{% endfor %}
-\ No newline at end of file
+{% for message in messages %}<tr><td>{{ message.id }}</td><td>{{ message.relevance }}</td><td>{{ message.name }}</td><td>{{ message.content }}</td><td><a class="material-icons delete-button" data-item-id="{{ message.id }}">delete</a></td></tr>{% endfor %}