465 lines
14 KiB
JavaScript
465 lines
14 KiB
JavaScript
|
|
// region Init Table
|
|
|
|
function initializeDataTable(tableId, columns) {
|
|
$(tableId).DataTable({
|
|
info: false,
|
|
paging: false,
|
|
ordering: false,
|
|
searching: false,
|
|
autoWidth: false,
|
|
order: [],
|
|
language: {
|
|
emptyTable: "No results found"
|
|
},
|
|
select: {
|
|
style: "multi+shift",
|
|
selector: 'th:first-child input[type="checkbox"]'
|
|
},
|
|
columnDefs: [
|
|
{ orderable: false, targets: "no-sort" },
|
|
{
|
|
targets: 0,
|
|
checkboxes: { selectRow: true }
|
|
},
|
|
],
|
|
columns: [
|
|
{
|
|
// Select row checkbox column
|
|
title: '<input type="checkbox" class="form-check-input table-select-all" />',
|
|
data: null,
|
|
orderable: false,
|
|
className: "text-center col-switch-width",
|
|
render: function() {
|
|
return '<input type="checkbox" class="form-check-input table-select-row" />'
|
|
}
|
|
},
|
|
{ data: "id", visible: false },
|
|
...columns
|
|
]
|
|
});
|
|
bindTablePagination(tableId);
|
|
bindTablePageSizer(tableId);
|
|
bindTableSearch(tableId);
|
|
bindRefreshButton(tableId);
|
|
bindTableSelectColumn(tableId);
|
|
}
|
|
|
|
|
|
// region Filters & Ordering
|
|
|
|
// Filter methods
|
|
var _tableFilters = {};
|
|
function getTableFilters(tableId) {
|
|
return _tableFilters[tableId];
|
|
}
|
|
|
|
function setTableFilter(tableId, key, value) {
|
|
if (!_tableFilters[tableId]) {
|
|
_tableFilters[tableId] = {};
|
|
}
|
|
|
|
_tableFilters[tableId][key] = value;
|
|
}
|
|
|
|
// Sort methods
|
|
var _tableOrdering = {};
|
|
function getTableOrdering(tableId) {
|
|
return _tableOrdering[tableId];
|
|
}
|
|
|
|
function setTableOrdering(tableId, value) {
|
|
_tableOrdering[tableId] = value;
|
|
}
|
|
|
|
$(document).on("selectedServerChange", function() {
|
|
_tableFilters = {};
|
|
_tableOrdering = {};
|
|
});
|
|
|
|
|
|
// region Load & Clear Data
|
|
|
|
function wipeTable(tableId) {
|
|
$(`${tableId} thead .table-select-all`).prop("checked", false).prop("indeterminate", false);
|
|
$(tableId).DataTable().clear().draw(false)
|
|
}
|
|
|
|
function populateTable(tableId, data) {
|
|
$(tableId).DataTable().rows.add(data.results).draw(false);
|
|
updateTablePagination(tableId, data);
|
|
updateTableTotalCount(tableId, data);
|
|
}
|
|
|
|
async function loadTableData(tableId, url, method) {
|
|
fixTablePagination(tableId);
|
|
disableTableControls(tableId);
|
|
|
|
// Create querystring for filtering against the API
|
|
const filters = getTableFilters(tableId);
|
|
const ordering = getTableOrdering(tableId);
|
|
const querystring = makeQuerystring(filters, ordering);
|
|
|
|
// API request
|
|
const data = await ajaxRequest(url + querystring, method);
|
|
|
|
// Update table with new data
|
|
wipeTable(tableId);
|
|
populateTable(tableId, data);
|
|
enableTableControls(tableId);
|
|
}
|
|
|
|
|
|
// region Pagination
|
|
|
|
function bindTablePagination(tableId) {
|
|
let $paginationArea = $(tableId).closest('.js-tableBody').siblings('.js-tableControls').find('.pagination');
|
|
$paginationArea.on("click", ".page-link", function() {
|
|
let currentPage = parseInt($paginationArea.data("page"));
|
|
let wantedPage;
|
|
|
|
if ($(this).hasClass("page-prev")) {
|
|
wantedPage = currentPage - 1;
|
|
}
|
|
else if ($(this).hasClass("page-next")) {
|
|
wantedPage = currentPage + 1;
|
|
}
|
|
else {
|
|
wantedPage = $(this).attr("data-page")
|
|
}
|
|
|
|
setTableFilter(tableId, "page", wantedPage);
|
|
$(tableId).trigger("doDataLoad");
|
|
});
|
|
}
|
|
|
|
function fixTablePagination(tableId) {
|
|
let filters = getTableFilters(tableId);
|
|
let $pageSizer = $(tableId).closest('.js-tableBody').siblings('.js-tableControls').find(".table-page-sizer");
|
|
|
|
if (!("page" in filters)) {
|
|
setTableFilter(tableId, "page", 1);
|
|
}
|
|
if (!("page_size" in filters)) {
|
|
setTableFilter(tableId, "page_size", $($pageSizer).val())
|
|
}
|
|
}
|
|
|
|
function updateTablePagination(tableId, data) {
|
|
let filters = getTableFilters(tableId);
|
|
|
|
let $paginationArea = $(tableId).closest('.js-tableBody').siblings('.js-tableControls').find('.pagination');
|
|
$paginationArea.data("page", filters.page); // store the page for later
|
|
|
|
// Remove existing buttons for specific pages
|
|
$paginationArea.find(".page-pick").remove();
|
|
|
|
// Enable/disable 'next' and 'prev' buttons
|
|
$paginationArea.find(".page-prev").toggleClass("disabled", !data.previous);
|
|
$paginationArea.find(".page-next").toggleClass("disabled", !data.next);
|
|
|
|
const pagesToShow = Math.max(Math.ceil(data.count / filters.page_size), 1);
|
|
const maxPagesToShow = 10;
|
|
|
|
let startPage = 1;
|
|
let endPage;
|
|
let page = parseInt(filters.page);
|
|
|
|
// Determine the start and end page
|
|
if (pagesToShow <= maxPagesToShow) {
|
|
endPage = pagesToShow;
|
|
}
|
|
else {
|
|
const halfVisible = Math.floor(maxPagesToShow / 2);
|
|
if (page <= halfVisible) {
|
|
endPage = maxPagesToShow;
|
|
}
|
|
else if (page + halfVisible >= pagesToShow) {
|
|
startPage = pagesToShow - maxPagesToShow + 1;
|
|
endPage = pagesToShow;
|
|
}
|
|
else {
|
|
startPage = page - halfVisible;
|
|
endPage = page + halfVisible;
|
|
}
|
|
}
|
|
|
|
// Add buttons
|
|
if (startPage > 1) {
|
|
AddTablePageButton($paginationArea, 1);
|
|
if (startPage > 2) {
|
|
AddTablePageEllipsis($paginationArea);
|
|
}
|
|
}
|
|
for (i = startPage; i <= endPage; i++) {
|
|
AddTablePageButton($paginationArea, i, page);
|
|
}
|
|
if (endPage < pagesToShow) {
|
|
if (endPage < pagesToShow - 1) {
|
|
AddTablePageEllipsis($paginationArea);
|
|
}
|
|
AddTablePageButton($paginationArea, pagesToShow);
|
|
}
|
|
}
|
|
|
|
function AddTablePageButton($paginationArea, number, currentPage) {
|
|
let pageItem = $("<li>").addClass("page-item");
|
|
let pageLink = $("<button>")
|
|
.attr("type", "button")
|
|
.attr("data-page", number)
|
|
.addClass("page-link page-pick")
|
|
.text(number);
|
|
|
|
if (number === parseInt(currentPage)) {
|
|
pageLink.addClass("disabled").attr("tabindex", -1);
|
|
}
|
|
|
|
pageItem.append(pageLink);
|
|
$paginationArea.find(".page-next").parent().before(pageItem);
|
|
}
|
|
|
|
function AddTablePageEllipsis($paginationArea) {
|
|
let ellipsisItem = $("<li>").addClass("page-item disabled");
|
|
let ellipsisLink = $("<span>").addClass("page-link page-pick").text("...");
|
|
ellipsisItem.append(ellipsisLink);
|
|
$paginationArea.find(".page-next").parent().before(ellipsisItem);
|
|
}
|
|
|
|
|
|
// region Page Sizer
|
|
|
|
function updateTableTotalCount(tableId, data) {
|
|
let $tableControls = $(tableId).closest('.js-tableBody').siblings('.js-tableControls');
|
|
$tableControls.find('.pageinfo-total').text(data.count);
|
|
}
|
|
|
|
function bindTablePageSizer(tableId) {
|
|
let $tableControls = $(tableId).closest('.js-tableBody').siblings('.js-tableControls');
|
|
$tableControls.on("change", ".table-page-sizer", function() {
|
|
setTableFilter(tableId, "page", "1");
|
|
setTableFilter(tableId, "page_size", $(this).val());
|
|
$(tableId).trigger("doDataLoad");
|
|
});
|
|
}
|
|
|
|
|
|
// region Search Filters
|
|
|
|
_searchTimeouts = {};
|
|
function bindTableSearch(tableId) {
|
|
const $tableFilters = $(tableId).closest('.js-tableBody').siblings('.js-tableFilters');
|
|
$tableFilters.on("input", ".table-search-input", function() {
|
|
$(this).data("was-focused", true);
|
|
clearTimeout(_searchTimeouts[tableId]);
|
|
|
|
const searchString = $(this).val();
|
|
setTableFilter(tableId, "search", searchString);
|
|
setTableFilter(tableId, "page", 1); // back to first page, as desired page no. might not exist after filtering
|
|
|
|
_searchTimeouts[tableId] = setTimeout(function() {
|
|
$(tableId).trigger("doDataLoad");
|
|
}, 300);
|
|
});
|
|
}
|
|
|
|
|
|
// region Button Controls
|
|
|
|
function bindRefreshButton(tableId) {
|
|
const $tableFilters = $(tableId).closest('.js-tableBody').siblings('.js-tableFilters');
|
|
$tableFilters.on("click", ".table-refresh-btn", function() {
|
|
$(tableId).trigger("doDataLoad");
|
|
})
|
|
}
|
|
|
|
|
|
// region Select Checkboxes
|
|
|
|
function bindTableSelectColumn(tableId) {
|
|
$(tableId).on("change", "tbody tr .table-select-row", function() {
|
|
let selected = $(this).prop("checked");
|
|
let rowIndex = $(this).closest("tr").index();
|
|
let row = $(tableId).DataTable().row(rowIndex);
|
|
|
|
selected === true ? row.select() : row.deselect();
|
|
determineSelectAllState(tableId);
|
|
});
|
|
|
|
$(tableId).on("change", "thead .table-select-all", function() {
|
|
let selected = $(this).prop("checked");
|
|
let table = $(tableId).DataTable();
|
|
$(tableId).find("tbody tr").each(function(rowIndex) {
|
|
let row = table.row(rowIndex);
|
|
selected === true ? row.select() : row.deselect();
|
|
$(this).find(".table-select-row").prop("checked", selected);
|
|
});
|
|
|
|
determineSelectAllState(tableId);
|
|
});
|
|
}
|
|
|
|
function determineSelectAllState(tableId) {
|
|
let table = $(tableId).DataTable();
|
|
let selectedRowsCount = table.rows(".selected").data().toArray().length;
|
|
let allRowsCount = table.rows().data().toArray().length;
|
|
|
|
let doCheck = selectedRowsCount === allRowsCount;
|
|
let doIndeterminate = !doCheck && selectedRowsCount > 0;
|
|
|
|
$checkbox = $(tableId).find("thead .table-select-all");
|
|
$checkbox.prop("checked", doCheck);
|
|
$checkbox.prop("indeterminate", doIndeterminate);
|
|
|
|
$(tableId).closest(".js-tableBody").siblings(".js-tableFilters").find(".table-del-btn").prop("disabled", !doCheck && !doIndeterminate);
|
|
}
|
|
|
|
|
|
// region On/Off Controls
|
|
|
|
function enableTableControls(tableId) {
|
|
setTableControlsUsability(tableId, false);
|
|
}
|
|
|
|
function disableTableControls(tableId) {
|
|
setTableControlsUsability(tableId, true);
|
|
}
|
|
|
|
function setTableControlsUsability(tableId, disabled) {
|
|
const $table = $(tableId);
|
|
const $tableBody = $table.closest(".js-tableBody");
|
|
const $tableFilters = $tableBody.siblings(".js-tableFilters");
|
|
const $tableControls = $tableBody.siblings(".tableControls");
|
|
|
|
$tableBody.find(".disable-while-loading").prop("disabled", disabled);
|
|
$tableFilters.find(".disable-while-loading").prop("disabled", disabled);
|
|
$tableControls.find(".disable-while-loading").prop("disabled", disabled);
|
|
|
|
// Re-focus search bar if used
|
|
const $search = $tableFilters.find(".disable-while-loading[type=search]");
|
|
$search.data("was-focused") ? $search.focus() : null;
|
|
}
|
|
|
|
|
|
// region Modals
|
|
|
|
|
|
async function openDataModal(modalId, pk, url) {
|
|
$modal = $(modalId);
|
|
$modal.data("primary-key", pk);
|
|
|
|
if (parseInt(pk) === -1) {
|
|
$modal.find(".form-create").show();
|
|
$modal.find(".form-edit").hide();
|
|
setDefaultModalData($modal);
|
|
}
|
|
else {
|
|
$modal.find(".form-create").hide();
|
|
$modal.find(".form-edit").show();
|
|
await loadModalData($modal, url);
|
|
}
|
|
|
|
$modal.modal("show");
|
|
}
|
|
|
|
function setDefaultModalData($modal) {
|
|
$modal.find("[data-field]").each(function() {
|
|
const type = $(this).attr("type");
|
|
const defaultVal = $(this).attr("data-default") || "";
|
|
|
|
if (type === "checkbox") {
|
|
$(this).prop("checked", defaultVal === "true");
|
|
}
|
|
else if (type === "datetime-local") {
|
|
$(this).val(getCurrentDateTime());
|
|
}
|
|
else {
|
|
$(this).val(defaultVal).change();
|
|
}
|
|
});
|
|
}
|
|
|
|
async function loadModalData($modal, url) {
|
|
const data = await ajaxRequest(url, "GET");
|
|
|
|
$modal.find("[data-field]").each(function() {
|
|
const key = $(this).attr("data-field");
|
|
const value = data[key];
|
|
|
|
if (typeof value === "boolean") {
|
|
$(this).prop("checked", value);
|
|
}
|
|
else if (isISODateTimeString(value)) {
|
|
$(this).val(value.split('+')[0].substring(0, 16));
|
|
}
|
|
else {
|
|
$(this).val(value).change();
|
|
}
|
|
});
|
|
}
|
|
|
|
async function onModalSubmit($modal, $table, url) {
|
|
if (!selectedServer) {
|
|
return;
|
|
}
|
|
|
|
let data = { server: selectedServer.id };
|
|
|
|
$modal.find("[data-field]").each(function() {
|
|
const type = $(this).attr("type");
|
|
const key = $(this).attr("data-field");
|
|
if (!key) {
|
|
return;
|
|
}
|
|
|
|
let value;
|
|
if (type === "checkbox") {
|
|
value = $(this).prop("checked");
|
|
}
|
|
else {
|
|
value = $(this).val();
|
|
}
|
|
|
|
data[key] = value;
|
|
});
|
|
|
|
const formData = objectToFormData(data);
|
|
const id = $modal.data("primary-key");
|
|
const isNewItem = parseInt(id) !== -1;
|
|
const method = isNewItem ? "PATCH" : "POST";
|
|
url = isNewItem ? url + `${id}/` : url;
|
|
|
|
ajaxRequest(url, method, formData)
|
|
.then(response => {
|
|
$modal.modal("hide");
|
|
$table.trigger("doDataLoad");
|
|
})
|
|
.catch(error => logError(error));
|
|
}
|
|
|
|
|
|
// region Table Column Types
|
|
|
|
function renderEditColumn(data) {
|
|
const name = sanitise(data);
|
|
return `<button type="button" class="btn btn-link text-start text-decoration-none edit-modal">${name}</button>`;
|
|
}
|
|
|
|
function renderBooleanColumn(data) {
|
|
const iconClass = data ? "bi-check-circle-fill text-success" : "bi-x-circle-fill text-danger";
|
|
return `<i class="bi ${iconClass}"></i>`;
|
|
}
|
|
|
|
|
|
// region Get Table Parts
|
|
|
|
function getTableFiltersComponent(tableId) {
|
|
return $(tableId).closest(".js-tableBody").siblings(".js-tableFilters");
|
|
}
|
|
|
|
function getTableControlsComponent(tableId) {
|
|
return $(tableId).closest(".js-tableBody").siblings(".js-tableControls");
|
|
}
|
|
|
|
function getSelectedTableRows(tableId) {
|
|
return $(tableId).DataTable().rows(".selected").data().toArray();
|
|
} |