preline
Version:
Preline UI is an open-source set of prebuilt UI components based on the utility-first Tailwind CSS framework.
620 lines (502 loc) • 15.4 kB
text/typescript
/*
* HSDataTable
* @version: 4.2.0
* @author: Preline Labs Ltd.
* @license: Licensed under MIT and Preline UI Fair Use License (https://preline.co/docs/license.html)
* Copyright 2024 Preline Labs Ltd.
*/
import { Api } from 'datatables.net';
import { debounce, htmlToElement, classToClassList } from '../../utils';
import {
IDataTableOptions,
IDataTable,
IColumnDef,
} from '../datatable/interfaces';
import HSBasePlugin from '../base-plugin';
declare var DataTable: any;
class HSDataTable
extends HSBasePlugin<IDataTableOptions>
implements IDataTable
{
private concatOptions: IDataTableOptions;
private dataTable: Api<any>;
private readonly table: HTMLTableElement;
private searches: HTMLElement[] | null;
private pageEntitiesList: (HTMLSelectElement | HTMLInputElement)[] | null;
private pagingList: HTMLElement[] | null;
private pagingPagesList: HTMLElement[] | null;
private pagingPrevList: HTMLElement[] | null;
private pagingNextList: HTMLElement[] | null;
private readonly infoList: HTMLElement[] | null;
private rowSelectingAll: HTMLElement | null;
private rowSelectingIndividual: string | null;
private maxPagesToShow: number;
private isRowSelecting: boolean;
private readonly pageBtnClasses: string | null;
private onSearchInputListener:
| {
el: Element;
fn: (evt: InputEvent) => void;
}[]
| null;
private onPageEntitiesChangeListener:
| {
el: Element;
fn: (evt: InputEvent) => void;
}[]
| null;
private onSinglePagingClickListener:
| {
el: Element;
fn: () => void;
}[]
| null;
private onPagingPrevClickListener:
| {
el: Element;
fn: () => void;
}[]
| null;
private onPagingNextClickListener:
| {
el: Element;
fn: () => void;
}[]
| null;
private onRowSelectingAllChangeListener: () => void;
constructor(el: HTMLElement, options?: IDataTableOptions, events?: {}) {
super(el, options, events);
this.el = typeof el === 'string' ? document.querySelector(el) : el;
// Exclude columns from ordering
const columnDefs: IColumnDef[] = [];
Array.from(this.el.querySelectorAll('thead th, thead td')).forEach(
(th: HTMLElement, ind: number) => {
if (th.classList.contains('--exclude-from-ordering'))
columnDefs.push({
targets: ind,
orderable: false,
});
},
);
const data = this.el.getAttribute('data-hs-datatable');
const dataOptions: IDataTableOptions = data ? JSON.parse(data) : {};
this.concatOptions = {
searching: true,
lengthChange: false,
order: [],
columnDefs: [...columnDefs],
...dataOptions,
...options,
};
this.table = this.el.querySelector('table');
this.searches =
Array.from(this.el.querySelectorAll('[data-hs-datatable-search]')) ??
null;
this.pageEntitiesList =
Array.from(
this.el.querySelectorAll('[data-hs-datatable-page-entities]'),
) ?? null;
this.pagingList =
Array.from(this.el.querySelectorAll('[data-hs-datatable-paging]')) ??
null;
this.pagingPagesList =
Array.from(
this.el.querySelectorAll('[data-hs-datatable-paging-pages]'),
) ?? null;
this.pagingPrevList =
Array.from(this.el.querySelectorAll('[data-hs-datatable-paging-prev]')) ??
null;
this.pagingNextList =
Array.from(this.el.querySelectorAll('[data-hs-datatable-paging-next]')) ??
null;
this.infoList =
Array.from(this.el.querySelectorAll('[data-hs-datatable-info]')) ?? null;
if (this.concatOptions?.rowSelectingOptions)
this.rowSelectingAll =
(this.concatOptions?.rowSelectingOptions?.selectAllSelector
? document.querySelector(
this.concatOptions?.rowSelectingOptions?.selectAllSelector,
)
: document.querySelector('[data-hs-datatable-row-selecting-all]')) ??
null;
if (this.concatOptions?.rowSelectingOptions)
this.rowSelectingIndividual =
this.concatOptions?.rowSelectingOptions?.individualSelector ??
'[data-hs-datatable-row-selecting-individual]';
if (this.pageEntitiesList.length)
this.concatOptions.pageLength = parseInt(this.pageEntitiesList[0].value);
this.maxPagesToShow = 3;
this.isRowSelecting = !!this.concatOptions?.rowSelectingOptions;
this.pageBtnClasses =
this.concatOptions?.pagingOptions?.pageBtnClasses ?? null;
this.onSearchInputListener = [];
this.onPageEntitiesChangeListener = [];
this.onSinglePagingClickListener = [];
this.onPagingPrevClickListener = [];
this.onPagingNextClickListener = [];
this.init();
}
private init() {
this.createCollection(window.$hsDataTableCollection, this);
this.initTable();
if (this.searches.length) this.initSearch();
if (this.pageEntitiesList.length) this.initPageEntities();
if (this.pagingList.length) this.initPaging();
if (this.pagingPagesList.length) this.buildPagingPages();
if (this.pagingPrevList.length) this.initPagingPrev();
if (this.pagingNextList.length) this.initPagingNext();
if (this.infoList.length) this.initInfo();
if (this.isRowSelecting) this.initRowSelecting();
}
private initTable() {
this.dataTable = new DataTable(this.table, this.concatOptions);
if (this.isRowSelecting) this.triggerChangeEventToRow();
this.dataTable.on('draw', () => {
if (this.isRowSelecting) this.updateSelectAllCheckbox();
if (this.isRowSelecting) this.triggerChangeEventToRow();
this.updateInfo();
this.pagingPagesList.forEach((el) => this.updatePaging(el));
});
}
private searchInput(evt: InputEvent) {
this.onSearchInput((evt.target as HTMLInputElement).value);
}
private pageEntitiesChange(evt: Event) {
this.onEntitiesChange(
parseInt((evt.target as HTMLSelectElement).value),
evt.target as HTMLSelectElement,
);
}
private pagingPrevClick() {
this.onPrevClick();
}
private pagingNextClick() {
this.onNextClick();
}
private rowSelectingAllChange() {
this.onSelectAllChange();
}
private singlePagingClick(count: number) {
this.onPageClick(count);
}
// Search
private initSearch() {
this.searches.forEach((el) => {
this.onSearchInputListener.push({
el,
fn: debounce((evt: InputEvent) => this.searchInput(evt)),
});
el.addEventListener(
'input',
this.onSearchInputListener.find((search) => search.el === el).fn,
);
});
}
private onSearchInput(val: string) {
this.dataTable.search(val).draw();
}
// Page entities
private initPageEntities() {
this.pageEntitiesList.forEach((el) => {
this.onPageEntitiesChangeListener.push({
el,
fn: (evt) => this.pageEntitiesChange(evt),
});
el.addEventListener(
'change',
this.onPageEntitiesChangeListener.find(
(pageEntity) => pageEntity.el === el,
).fn,
);
});
}
private onEntitiesChange(entities: number, target: HTMLSelectElement) {
const otherEntities = this.pageEntitiesList.filter((el) => el !== target);
if (otherEntities.length)
otherEntities.forEach((el) => {
if (window.HSSelect) {
// @ts-ignore
const hsSelectInstance = window.HSSelect.getInstance(el, true);
if (hsSelectInstance && 'element' in hsSelectInstance) {
hsSelectInstance.element.setValue(`${entities}`);
}
} else el.value = `${entities}`;
});
this.dataTable.page.len(entities).draw();
}
// Info
private initInfo() {
this.infoList.forEach((el) => {
this.initInfoFrom(el);
this.initInfoTo(el);
this.initInfoLength(el);
});
}
private initInfoFrom(el: HTMLElement) {
const infoFrom =
(el.querySelector('[data-hs-datatable-info-from]') as HTMLElement) ??
null;
const { start } = this.dataTable.page.info();
if (infoFrom) infoFrom.innerText = `${start + 1}`;
}
private initInfoTo(el: HTMLElement) {
const infoTo =
(el.querySelector('[data-hs-datatable-info-to]') as HTMLElement) ?? null;
const { end } = this.dataTable.page.info();
if (infoTo) infoTo.innerText = `${end}`;
}
private initInfoLength(el: HTMLElement) {
const infoLength =
(el.querySelector('[data-hs-datatable-info-length]') as HTMLElement) ??
null;
const { recordsTotal } = this.dataTable.page.info();
if (infoLength) infoLength.innerText = `${recordsTotal}`;
}
private updateInfo() {
this.initInfo();
}
// Paging
private initPaging() {
this.pagingList.forEach((el) => this.hidePagingIfSinglePage(el));
}
private hidePagingIfSinglePage(el: HTMLElement) {
const { pages } = this.dataTable.page.info();
if (pages < 2) {
el.classList.add('hidden');
el.style.display = 'none';
} else {
el.classList.remove('hidden');
el.style.display = '';
}
}
private initPagingPrev() {
this.pagingPrevList.forEach((el) => {
this.onPagingPrevClickListener.push({
el,
fn: () => this.pagingPrevClick(),
});
el.addEventListener(
'click',
this.onPagingPrevClickListener.find(
(pagingPrev) => pagingPrev.el === el,
).fn,
);
});
}
private onPrevClick() {
this.dataTable.page('previous').draw('page');
}
private disablePagingArrow(el: HTMLElement, statement: boolean) {
if (statement) {
el.classList.add('disabled');
el.setAttribute('disabled', 'disabled');
} else {
el.classList.remove('disabled');
el.removeAttribute('disabled');
}
}
private initPagingNext() {
this.pagingNextList.forEach((el) => {
this.onPagingNextClickListener.push({
el,
fn: () => this.pagingNextClick(),
});
el.addEventListener(
'click',
this.onPagingNextClickListener.find(
(pagingNext) => pagingNext.el === el,
).fn,
);
});
}
private onNextClick() {
this.dataTable.page('next').draw('page');
}
private buildPagingPages() {
this.pagingPagesList.forEach((el) => this.updatePaging(el));
}
private updatePaging(pagingPages: HTMLElement) {
const { page, pages, length } = this.dataTable.page.info();
const totalRecords = this.dataTable.rows({ search: 'applied' }).count();
const totalPages = Math.ceil(totalRecords / length);
const currentPage = page + 1;
let startPage = Math.max(
1,
currentPage - Math.floor(this.maxPagesToShow / 2),
);
let endPage = Math.min(totalPages, startPage + (this.maxPagesToShow - 1));
if (endPage - startPage + 1 < this.maxPagesToShow) {
startPage = Math.max(1, endPage - this.maxPagesToShow + 1);
}
pagingPages.innerHTML = '';
if (startPage > 1) {
this.buildPagingPage(1, pagingPages);
if (startPage > 2)
pagingPages.appendChild(
htmlToElement(`<span class="ellipsis">...</span>`),
);
}
for (let i = startPage; i <= endPage; i++) {
this.buildPagingPage(i, pagingPages);
}
if (endPage < totalPages) {
if (endPage < totalPages - 1)
pagingPages.appendChild(
htmlToElement(`<span class="ellipsis">...</span>`),
);
this.buildPagingPage(totalPages, pagingPages);
}
this.pagingPrevList.forEach((el) =>
this.disablePagingArrow(el, page === 0),
);
this.pagingNextList.forEach((el) =>
this.disablePagingArrow(el, page === pages - 1),
);
this.pagingList.forEach((el) => this.hidePagingIfSinglePage(el));
}
private buildPagingPage(counter: number, target: HTMLElement) {
const { page } = this.dataTable.page.info();
const pageEl = htmlToElement(`<button type="button"></button>`);
pageEl.innerText = `${counter}`;
pageEl.setAttribute('data-page', `${counter}`);
if (this.pageBtnClasses) classToClassList(this.pageBtnClasses, pageEl);
if (page === counter - 1) pageEl.classList.add('active');
this.onSinglePagingClickListener.push({
el: pageEl,
fn: () => this.singlePagingClick(counter),
});
pageEl.addEventListener(
'click',
this.onSinglePagingClickListener.find(
(singlePaging) => singlePaging.el === pageEl,
).fn,
);
target.append(pageEl);
}
private onPageClick(counter: number) {
this.dataTable.page(counter - 1).draw('page');
}
// Select row
private initRowSelecting() {
this.onRowSelectingAllChangeListener = () => this.rowSelectingAllChange();
this.rowSelectingAll.addEventListener(
'change',
this.onRowSelectingAllChangeListener,
);
}
private triggerChangeEventToRow() {
this.table
.querySelectorAll(`tbody ${this.rowSelectingIndividual}`)
.forEach((el) => {
el.addEventListener('change', () => {
this.updateSelectAllCheckbox();
});
});
}
private onSelectAllChange() {
let isChecked = (this.rowSelectingAll as HTMLInputElement).checked;
const visibleRows = Array.from(
this.dataTable.rows({ page: 'current', search: 'applied' }).nodes(),
);
visibleRows.forEach((el) => {
const checkbox = el.querySelector(this.rowSelectingIndividual);
if (checkbox) checkbox.checked = isChecked;
});
this.updateSelectAllCheckbox();
}
private updateSelectAllCheckbox() {
const searchRelatedItems = this.dataTable
.rows({ search: 'applied' })
.count();
if (!searchRelatedItems) {
(this.rowSelectingAll as HTMLInputElement).checked = false;
return false;
}
let isChecked = true;
const visibleRows = Array.from(
this.dataTable
.rows({
page: 'current',
search: 'applied',
})
.nodes(),
);
visibleRows.forEach((el) => {
const checkbox = el.querySelector(this.rowSelectingIndividual);
if (checkbox && !checkbox.checked) {
isChecked = false;
return false;
}
});
(this.rowSelectingAll as HTMLInputElement).checked = isChecked;
}
// Public methods
public destroy() {
if (this.searches) {
this.onSearchInputListener.forEach(({ el, fn }) =>
el.removeEventListener('click', fn),
);
// this.searches = null;
}
if (this.pageEntitiesList)
this.onPageEntitiesChangeListener.forEach(({ el, fn }) =>
el.removeEventListener('change', fn),
);
if (this.pagingPagesList.length) {
this.onSinglePagingClickListener.forEach(({ el, fn }) =>
el.removeEventListener('click', fn),
);
this.pagingPagesList.forEach((el) => (el.innerHTML = ''));
}
if (this.pagingPrevList.length)
this.onPagingPrevClickListener.forEach(({ el, fn }) =>
el.removeEventListener('click', fn),
);
if (this.pagingNextList.length)
this.onPagingNextClickListener.forEach(({ el, fn }) =>
el.removeEventListener('click', fn),
);
if (this.rowSelectingAll)
this.rowSelectingAll.removeEventListener(
'change',
this.onRowSelectingAllChangeListener,
);
this.dataTable.destroy();
this.rowSelectingAll = null;
this.rowSelectingIndividual = null;
window.$hsDataTableCollection = window.$hsDataTableCollection.filter(
({ element }) => element.el !== this.el,
);
}
// Static methods
static getInstance(target: HTMLElement | string, isInstance?: boolean) {
const elInCollection = window.$hsDataTableCollection.find(
(el) =>
el.element.el ===
(typeof target === 'string' ? document.querySelector(target) : target),
);
return elInCollection
? isInstance
? elInCollection
: elInCollection.element.el
: null;
}
static autoInit() {
if (!window.$hsDataTableCollection) window.$hsDataTableCollection = [];
if (window.$hsDataTableCollection)
window.$hsDataTableCollection = window.$hsDataTableCollection.filter(
({ element }) => document.contains(element.el),
);
document
.querySelectorAll('[data-hs-datatable]:not(.--prevent-on-load-init)')
.forEach((el: HTMLElement) => {
if (
!window.$hsDataTableCollection.find(
(elC) => (elC?.element?.el as HTMLElement) === el,
)
)
new HSDataTable(el);
});
}
}
export default HSDataTable;