542 lines
19 KiB
JavaScript
542 lines
19 KiB
JavaScript
var timeouts = {};
|
|
tableFilters = {};
|
|
tableSorts = {};
|
|
|
|
$(document).on("selectedServerChange", async function() {
|
|
// Clear filters and sorts when changing server
|
|
tableFilters = {};
|
|
tableSorts = {};
|
|
})
|
|
|
|
function setTableFilter(tableId, key, value) {
|
|
if (!tableFilters[tableId])
|
|
tableFilters[tableId] = {}
|
|
|
|
tableFilters[tableId][key] = value;
|
|
}
|
|
|
|
function setTableSorts(tableId, value) {
|
|
tableSorts[tableId] = value;
|
|
}
|
|
|
|
function ensureTablePagination(tableId) {
|
|
if (!tableFilters[tableId])
|
|
throw new Error(`${tableId} isn't a valid table ID`);
|
|
|
|
if (!tableFilters[tableId]["page"])
|
|
setTableFilter(tableId, "page", "1");
|
|
|
|
if (!tableFilters[tableId]["page_size"])
|
|
setTableFilter(tableId, "page_size", $(`#${tableId}`).closest(".tab-pane").find(".table-page-sizer").val());
|
|
}
|
|
|
|
async function initTable(containingSelector, tableId, loadDataFunc, newRowFunc, deleteSelectedFunc, options=null) {
|
|
let pageSizeId = tableId + "PageSize";
|
|
searchId = tableId + "SearchBar";
|
|
sortDropdownId = tableId + "SortDropdown";
|
|
filterDropdownId = tableId + "FilterDropdown";
|
|
|
|
createSearchRow(containingSelector, searchId, sortDropdownId, filterDropdownId, options, newRowFunc, deleteSelectedFunc);
|
|
createTable(containingSelector, tableId);
|
|
createTableControls(containingSelector, pageSizeId);
|
|
|
|
await bindSearchBar(tableId, searchId, loadDataFunc);
|
|
await bindSortDropdown(tableId, sortDropdownId, loadDataFunc);
|
|
await bindFilterDropdown(tableId, filterDropdownId, loadDataFunc);
|
|
await bindTablePagination(tableId, `${containingSelector} .table-pagination`, loadDataFunc);
|
|
await bindTablePaginationResizer(tableId, `${containingSelector} .table-page-sizer`, loadDataFunc);
|
|
}
|
|
|
|
function createSearchRow(containingSelector, searchId, sortDropdownId, filterDropdownId, options, newRowFunc, deleteSelectedFunc) {
|
|
$(containingSelector).append(`
|
|
<div class="row my-3 px-3 table-search-row">
|
|
<div class="col-lg-4">
|
|
<div class="input-group mb-lg-0 mb-3 rounded-1">
|
|
<span class="input-group-text">
|
|
<i class="bi bi-search"></i>
|
|
</span>
|
|
<input type="search" id="${searchId}" name="${searchId}" class="form-control table-searchbar" placeholder="Search">
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-8 text-end table-search-buttons">
|
|
<div class="d-inline-block ms-3">
|
|
<button type="button" class="table-refresh-btn btn btn-outline-secondary rounded-1">
|
|
<i class="bi bi-arrow-clockwise"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`);
|
|
|
|
// If there is a function that allows the creation of new rows, create a button for it
|
|
if (newRowFunc) {
|
|
$(`${containingSelector} .table-search-row .table-search-buttons`).prepend(`
|
|
<div class="d-inline-block ms-3">
|
|
<button type="button" class="table-new-btn btn btn-primary rounded-1">
|
|
<i class="bi bi-plus-lg"></i>
|
|
</button>
|
|
</div>
|
|
`)
|
|
$(`${containingSelector} .table-search-row .table-search-buttons .table-new-btn`).on("click", async () => {await newRowFunc(-1)});
|
|
}
|
|
|
|
// Show the sort dropdown
|
|
if (options.sort && options.actions.GET) {
|
|
$(`${containingSelector} .table-search-row .table-search-buttons`).append(`
|
|
<div class="d-inline-block ms-3">
|
|
<div id="${sortDropdownId}" class="dropdown table-sort-dropdown">
|
|
<button type="button" class="btn btn-secondary rounded-1" data-bs-toggle="dropdown" data-bs-auto-close="outside">
|
|
<i class="bi bi-sort-alpha-up"></i>
|
|
</button>
|
|
<ul class="dropdown-menu dropdown-menu-end">
|
|
<li><h6 class="dropdown-header">Sort By</h6></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
`);
|
|
populateSortDropdown(sortDropdownId, options.actions.GET, options.sort);
|
|
}
|
|
|
|
// Show the filters dropdown
|
|
if (options.filter && options.actions.GET) {
|
|
$(`${containingSelector} .table-search-row .table-search-buttons`).append(`
|
|
<div class="d-inline-block ms-3">
|
|
<div id="${filterDropdownId}" class="dropdown table-filter-dropdown">
|
|
<button type="button" class="btn btn-secondary rounded-1" data-bs-toggle="dropdown" data-bs-auto-close="outside">
|
|
<i class="bi bi-funnel"></i>
|
|
</button>
|
|
<ul class="dropdown-menu dropdown-menu-end">
|
|
<li><h6 class="dropdown-header">Filter By</h6></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
`);
|
|
populateFilterDropdown(filterDropdownId, options.actions.GET, options.filter);
|
|
}
|
|
|
|
// If there is a function for deleting selected rows, create a button for it
|
|
if (deleteSelectedFunc) {
|
|
$(`${containingSelector} .table-search-row .table-search-buttons`).append(`
|
|
<div class="d-inline-block ms-3">
|
|
<button type="button" class="table-del-btn btn btn-danger rounded-1">
|
|
<i class="bi bi-trash3"></i>
|
|
</button>
|
|
</div>
|
|
`)
|
|
$(`${containingSelector} .table-search-row .table-search-buttons .table-del-btn`).on("click", async () => {await deleteSelectedFunc()});
|
|
}
|
|
}
|
|
|
|
function populateSortDropdown(sortDropdownId, options, sortKeys) {
|
|
for (key in options) {
|
|
if (!sortKeys.includes(key))
|
|
continue;
|
|
|
|
let label = options[key].label;
|
|
$elem = null;
|
|
|
|
$elem = $(`
|
|
<li>
|
|
<button type="button" class="dropdown-item d-flex justify-content-between" data-sortkey="${key}">
|
|
<span class="me-3">${label}</span><i class="bi bi-x"></i>
|
|
</button>
|
|
</li>
|
|
`);
|
|
bindSortBoolean($elem);
|
|
$(`#${sortDropdownId} .dropdown-menu`).append($elem);
|
|
updateSortCheckboxState($elem);
|
|
}
|
|
}
|
|
|
|
function populateFilterDropdown(filterDropdownId, options, filterKeys) {
|
|
for (key in options) {
|
|
if (!filterKeys.includes(key))
|
|
continue;
|
|
|
|
let label = options[key].label;
|
|
id = key + "FilterOption";
|
|
$elem = null;
|
|
|
|
switch (options[key].type) {
|
|
case ("boolean"):
|
|
$elem = $(`
|
|
<li>
|
|
<input type="checkbox" name="${id}" id="${id}" class="btn-check" autocomplete="off" data-key="${key}">
|
|
<label for="${id}" class="dropdown-item d-flex justify-content-between" role="button">
|
|
<span>${label}</span>
|
|
<i class="bi bi-x"></i>
|
|
</label>
|
|
</li>
|
|
`);
|
|
bindFilterBoolean($elem);
|
|
$elem.find('input[type="checkbox"]').prop("checked", options[key].default ? true : false).trigger("change");
|
|
updateFilterCheckboxState($elem);
|
|
break
|
|
|
|
default:
|
|
continue;
|
|
}
|
|
|
|
$(`#${filterDropdownId} .dropdown-menu`).append($elem);
|
|
}
|
|
|
|
// Reset the controls
|
|
$(document).on("selectedServerChange", async function() {
|
|
$(`#${filterDropdownId} .dropdown-menu input[type="checkbox"]`).each(function() {
|
|
$(this).data("state", null);
|
|
updateFilterCheckboxState($(this).parent());
|
|
});
|
|
});
|
|
}
|
|
|
|
function bindSortBoolean($elem) {
|
|
$elem.find("button").on("click", function() {
|
|
|
|
// Reset sibling buttons
|
|
$(this).parent().siblings("li").each(function() {
|
|
$(this).find("button").data("state", null);
|
|
updateSortCheckboxState($(this));
|
|
});
|
|
|
|
let state = $(this).data("state");
|
|
state = determineState(state);
|
|
$(this).data("state", state);
|
|
updateSortCheckboxState($elem)
|
|
});
|
|
}
|
|
|
|
function bindFilterBoolean($elem) {
|
|
$elem.find('label').on("click", function() {
|
|
let $input = $elem.find('input[type="checkbox"]');
|
|
let state = $input.data("state");
|
|
state = determineState(state);
|
|
$input.data("state", state);
|
|
updateFilterCheckboxState($elem);
|
|
});
|
|
}
|
|
|
|
function determineState(state) {
|
|
switch (state) {
|
|
case true:
|
|
return false;
|
|
case false:
|
|
return null;
|
|
case null:
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function updateSortCheckboxState($elem) {
|
|
let state = $elem.find("button").data("state");
|
|
let $icon = $elem.find(".bi");
|
|
$icon.removeClass();
|
|
|
|
switch (state) {
|
|
case true:
|
|
$icon.addClass("bi bi-sort-alpha-up");
|
|
break;
|
|
case false:
|
|
$icon.addClass("bi bi-sort-alpha-down-alt");
|
|
break;
|
|
case null:
|
|
default:
|
|
$icon.addClass("bi bi-slash invisible");
|
|
}
|
|
}
|
|
|
|
function updateFilterCheckboxState($elem) {
|
|
let state = $elem.find('input[type="checkbox"]').data("state");
|
|
let $icon = $elem.find(".bi");
|
|
|
|
switch (state) {
|
|
case true:
|
|
$elem.find('input[type="checkbox"]').prop("checked", true);
|
|
$icon.removeClass().addClass("bi bi-check");
|
|
break;
|
|
case false:
|
|
$elem.find('input[type="checkbox"]').prop("checked", false);
|
|
$icon.removeClass().addClass("bi bi-x");
|
|
break;
|
|
case null:
|
|
default:
|
|
$elem.find('input[type="checkbox"]').prop("checked", false);
|
|
$icon.removeClass().addClass("bi bi-dash invisible");
|
|
break;
|
|
}
|
|
}
|
|
|
|
async function bindSearchBar(tableId, searchBarSelector, loadDataFunc) {
|
|
searchBar = $("#" + searchBarSelector)
|
|
searchBar.on("input", async function() {
|
|
clearTimeout(timeouts[searchBarSelector]);
|
|
var searchString = $(this).val();
|
|
setTableFilter(tableId, "search", searchString);
|
|
timeouts[searchBarSelector] = setTimeout(async function() {
|
|
await loadDataFunc(getCurrentlyActiveServer().guild_id);
|
|
}, 300);
|
|
});
|
|
}
|
|
|
|
async function bindFilterDropdown(tableId, filterDropdownId, loadDataFunc) {
|
|
$(`#${filterDropdownId} .dropdown-menu`).on("change", "input", async function() {
|
|
setTableFilter(tableId, $(this).attr("data-key"), $(this).data("state"));
|
|
setTableFilter(tableId, "page", "1");
|
|
await loadDataFunc(getCurrentlyActiveServer().guild_id);
|
|
});
|
|
}
|
|
|
|
async function bindSortDropdown(tableId, sortDropdownId, loadDataFunc) {
|
|
$(`#${sortDropdownId} .dropdown-menu`).on("click", "button", async function() {
|
|
let state = $(this).data("state");
|
|
sortKey = "";
|
|
|
|
switch(state) {
|
|
case true:
|
|
sortKey = $(this).attr("data-sortkey")
|
|
break;
|
|
case false:
|
|
sortKey = "-" + $(this).attr("data-sortkey")
|
|
break;
|
|
case null:
|
|
default:
|
|
sortKey = ""
|
|
}
|
|
|
|
setTableSorts(tableId, sortKey);
|
|
await loadDataFunc(getCurrentlyActiveServer().guild_id);
|
|
});
|
|
}
|
|
|
|
function createTable(containingSelector, tableId) {
|
|
$(containingSelector).append(`
|
|
<div class="table-responsive my-3 px-3">
|
|
<table id="${tableId}" class="table table-hover align-middle"></table>
|
|
</div>
|
|
`);
|
|
}
|
|
|
|
function createTableControls(containingSelector, pageSizeId) {
|
|
$(containingSelector).append(`
|
|
<div class="table-controls row mb-3 px-3">
|
|
<div class="col-lg-2">
|
|
<div class="table-page-info d-flex justify-content-start align-items-center mx-auto">
|
|
<span class="pageinfo-total"></span>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-8">
|
|
<nav class="table-pagination d-flex justify-content-center">
|
|
<ul class="pagination mb-0">
|
|
<li class="page-item">
|
|
<button type="button" class="page-link page-prev rounded-start-1">
|
|
<i class="bi bi-chevron-left"></i>
|
|
</button>
|
|
</li>
|
|
<li class="page-item">
|
|
<button type="button" class="page-link page-next rounded-end-1">
|
|
<i class="bi bi-chevron-right"></i>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
<div class="col-lg-2">
|
|
<div class="d-flex justify-content-end">
|
|
<label for="${pageSizeId}" class="form-label align-self-center mb-0 me-2">Per Page</label>
|
|
<select name="${pageSizeId}" id="${pageSizeId}" class="select-2 table-page-sizer">
|
|
<option value="10" selected>10 </option>
|
|
<option value="15">15 </option>
|
|
<option value="20">20 </option>
|
|
<option value="25">25 </option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`);
|
|
|
|
$("#" + pageSizeId).select2({
|
|
theme: "bootstrap",
|
|
minimumResultsForSearch: 10,
|
|
});
|
|
}
|
|
|
|
function updateTableContainer(containerId, page, pageSize, itemsCount, totalItemsCount, nextExists, prevExists) {
|
|
if (!page || !pageSize)
|
|
throw new Error(`Missing page data '${containerId}': page=${page} pageSize=${pageSize}`);
|
|
|
|
updateTablePagination(
|
|
`#${containerId} .table-pagination`,
|
|
page,
|
|
pageSize,
|
|
totalItemsCount,
|
|
nextExists,
|
|
prevExists
|
|
);
|
|
updateTablePaginationInfo(
|
|
`#${containerId} .table-page-info`,
|
|
itemsCount,
|
|
totalItemsCount
|
|
);
|
|
}
|
|
|
|
// Updates the pagination text for a given pageInfoId
|
|
function updateTablePaginationInfo(pageInfoId, showing, total) {
|
|
$(`${pageInfoId} .pageinfo-showing`).text(showing);
|
|
$(`${pageInfoId} .pageinfo-total`).text(`${total} Result${total > 1 ? "s" : ""}`);
|
|
}
|
|
|
|
// Updates the pagination buttons for a given pageControlsId
|
|
function updateTablePagination(pageControlsId, currentPage, pageSize, totalItems, nextExists, prevExists) {
|
|
$(pageControlsId).attr("data-page", currentPage);
|
|
|
|
// Remove existing page specific buttons
|
|
$(`${pageControlsId} .page-pick`).remove();
|
|
|
|
// Determine states of 'previous page' 'next page' buttons
|
|
$(`${pageControlsId} .page-prev`).toggleClass("disabled", !prevExists).attr("tabindex", prevExists ? "" : "-1");
|
|
$(`${pageControlsId} .page-next`).toggleClass("disabled", !nextExists).attr("tabindex", nextExists ? "" : "-1");
|
|
|
|
// Calculate amount of pages to account for
|
|
const pages = Math.max(Math.ceil(totalItems / pageSize), 1);
|
|
const maxVisiblePages = 10;
|
|
|
|
let startPage, endPage;
|
|
|
|
currentPage = parseInt(currentPage);
|
|
pageSize = parseInt(pageSize);
|
|
|
|
if (pages <= maxVisiblePages) {
|
|
// If total pages are less than or equal to max visible pages, show all
|
|
startPage = 1;
|
|
endPage = pages;
|
|
} else {
|
|
// Determine the start and end pages
|
|
const halfVisible = Math.floor(maxVisiblePages / 2);
|
|
|
|
if (currentPage <= halfVisible) {
|
|
startPage = 1;
|
|
endPage = maxVisiblePages;
|
|
} else if (currentPage + halfVisible >= pages) {
|
|
startPage = pages - maxVisiblePages + 1;
|
|
endPage = pages;
|
|
} else {
|
|
startPage = currentPage - halfVisible;
|
|
endPage = currentPage + halfVisible;
|
|
}
|
|
}
|
|
|
|
if (startPage > 1) {
|
|
addPageButton(pageControlsId, 1);
|
|
if (startPage > 2) {
|
|
addEllipsis(pageControlsId);
|
|
}
|
|
}
|
|
|
|
for (let i = startPage; i <= endPage; i++) {
|
|
addPageButton(pageControlsId, i, currentPage);
|
|
}
|
|
|
|
if (endPage < pages) {
|
|
if (endPage < pages - 1) {
|
|
addEllipsis(pageControlsId);
|
|
}
|
|
addPageButton(pageControlsId, pages);
|
|
}
|
|
}
|
|
|
|
function addPageButton(pageControlsId, pageNumber, currentPage) {
|
|
let pageItem = $("<li>").addClass("page-item");
|
|
let pageLink = $("<button>")
|
|
.attr("type", "button")
|
|
.attr("data-page", pageNumber)
|
|
.addClass("page-link page-pick")
|
|
.text(pageNumber);
|
|
|
|
if (pageNumber === parseInt(currentPage)) {
|
|
pageLink.addClass("disabled").attr("tabindex", -1);
|
|
}
|
|
|
|
pageItem.append(pageLink);
|
|
$(`${pageControlsId} .pagination .page-next`).parent().before(pageItem);
|
|
}
|
|
|
|
function addEllipsis(pageControlsId) {
|
|
let ellipsisItem = $("<li>").addClass("page-item disabled");
|
|
let ellipsisLink = $("<span>").addClass("page-link page-pick").text("...");
|
|
ellipsisItem.append(ellipsisLink);
|
|
$(`${pageControlsId} .pagination .page-next`).parent().before(ellipsisItem);
|
|
}
|
|
|
|
// Bind the table pagination buttons to control the table pagination
|
|
async function bindTablePagination(tableId, pageControlsId, dataLoadFunc) {
|
|
$(`${pageControlsId} .pagination`).on("click", ".page-link", async function() {
|
|
let wantedPage;
|
|
let currentPage = parseInt($(pageControlsId).attr("data-page"));
|
|
|
|
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);
|
|
await dataLoadFunc(getCurrentlyActiveServer().guild_id)
|
|
});
|
|
}
|
|
|
|
// Bind the table pagination page resizer control
|
|
async function bindTablePaginationResizer(tableId, resizerControlId, dataLoadFunc) {
|
|
$(resizerControlId).on("change", async function() {
|
|
setTableFilter(tableId, "page", "1");
|
|
setTableFilter(tableId, "page_size", $(this).val());
|
|
|
|
await dataLoadFunc(getCurrentlyActiveServer().guild_id);
|
|
});
|
|
}
|
|
|
|
function bindTableCheckboxes(tableSelector, tableObject, deleteButtonSelector) {
|
|
// Select a row via checkbox
|
|
$(tableSelector).on("change", "tbody tr .table-select-row", function() {
|
|
|
|
var selected = $(this).prop("checked");
|
|
rowIndex = $(this).closest("tr").index();
|
|
row = tableObject.row(rowIndex);
|
|
|
|
if (selected) row.select();
|
|
else row.deselect();
|
|
|
|
determineSelectAllState(tableSelector, tableObject, deleteButtonSelector);
|
|
});
|
|
|
|
// Select all rows checkbox
|
|
$(tableSelector).on("change", "thead .table-select-all", function() {
|
|
var selected = $(this).prop("checked");
|
|
$(`${tableSelector} tbody tr`).each(function(rowIndex) {
|
|
var row = tableObject.row(rowIndex);
|
|
|
|
if (selected) row.select();
|
|
else row.deselect();
|
|
|
|
$(this).find('.table-select-row').prop("checked", selected);
|
|
});
|
|
|
|
determineSelectAllState(tableSelector, tableObject, deleteButtonSelector);
|
|
});
|
|
}
|
|
|
|
function determineSelectAllState(tableSelector, tableObject, deleteButtonSelector) {
|
|
var selectedRowsCount = tableObject.rows(".selected").data().toArray().length;
|
|
allRowsCount = tableObject.rows().data().toArray().length;
|
|
|
|
selectAllCheckbox = $(`${tableSelector} thead .table-select-all`);
|
|
|
|
checked = selectedRowsCount === allRowsCount;
|
|
indeterminate = !checked && selectedRowsCount > 0;
|
|
|
|
selectAllCheckbox.prop("checked", checked);
|
|
selectAllCheckbox.prop("indeterminate", indeterminate);
|
|
|
|
$(deleteButtonSelector).prop("disabled", !(checked || indeterminate) || !(allRowsCount > 0));
|
|
}
|