ContactBridge

Log | Files | Refs | README

commit e41db65b1d7f39bc7f0dde4e7eccf3378d318581
parent 11e296483e80674fb33d130f25d4ff01bded9311
Author: William Lindholm <william_lindholm@outlook.com>
Date:   Fri, 10 Nov 2023 19:19:25 +0100

More refactoring.

Diffstat:
Mdatabase/database.db | 0
Mstatic/js/ajaxtable.js | 285+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mtemplates/component/ajaxtable.html | 35++++++++++++++++-------------------
Mtemplates/component/messages_template.html | 3+--
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 %}