UNPKG

flyonui

Version:

The easiest, free and open-source Tailwind CSS component library with semantic classes.

538 lines (415 loc) 16.3 kB
/* * HSDataTable * @version: 3.2.2 * @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 } from './interfaces' import HSBasePlugin from '../base-plugin' import { ICollectionItem } from '../../interfaces' declare var DataTable: any interface IColumnDef { targets: number orderable: boolean } 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-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-datatable-search]')) ?? null this.pageEntitiesList = Array.from(this.el.querySelectorAll('[data-datatable-page-entities]')) ?? null this.pagingList = Array.from(this.el.querySelectorAll('[data-datatable-paging]')) ?? null this.pagingPagesList = Array.from(this.el.querySelectorAll('[data-datatable-paging-pages]')) ?? null this.pagingPrevList = Array.from(this.el.querySelectorAll('[data-datatable-paging-prev]')) ?? null this.pagingNextList = Array.from(this.el.querySelectorAll('[data-datatable-paging-next]')) ?? null this.infoList = Array.from(this.el.querySelectorAll('[data-datatable-info]')) ?? null if (this.concatOptions?.rowSelectingOptions) this.rowSelectingAll = (this.concatOptions?.rowSelectingOptions?.selectAllSelector ? document.querySelector(this.concatOptions?.rowSelectingOptions?.selectAllSelector) : document.querySelector('[data-datatable-row-selecting-all]')) ?? null if (this.concatOptions?.rowSelectingOptions) this.rowSelectingIndividual = this.concatOptions?.rowSelectingOptions?.individualSelector ?? '[data-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) 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-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-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-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-datatable]:not(.--prevent-on-load-init)').forEach((el: HTMLElement) => { if (!window.$hsDataTableCollection.find(elC => (elC?.element?.el as HTMLElement) === el)) new HSDataTable(el) }) } } declare global { interface Window { HSDataTable: Function $hsDataTableCollection: ICollectionItem<HSDataTable>[] } } window.addEventListener('load', () => { if (document.querySelectorAll('[data-datatable]:not(.--prevent-on-load-init)').length) { if (typeof jQuery === 'undefined') console.error('HSDataTable: jQuery is not available, please add it to the page.') if (typeof DataTable === 'undefined') console.error('HSDataTable: DataTable is not available, please add it to the page.') } if (typeof DataTable !== 'undefined' && typeof jQuery !== 'undefined') HSDataTable.autoInit() // Uncomment for debug // console.log('Datatable collection:', window.$hsDataTableCollection); }) if (typeof window !== 'undefined') { window.HSDataTable = HSDataTable } export default HSDataTable