// From https://stackoverflow.com/a/48660568/4839162
const stableSort = ( arr, compare ) => arr
.map( ( item, index ) => ( {
item,
index,
} ) )
.sort( ( a, b ) => compare( a.item, b.item ) || a.index - b.index )
.map( ( {item} ) => item );
/**
* @typedef {Object} DisplayHandlerResult
* @property {TRow[]} rows - The actual rows to display
* @property {number} totalRowCount - The total number of rows in the table. It counts also items on other pages.
* The pages in the pagination component are calculated using this value.
*/
/**
* This class exposes the main method used to manipulate table data, like filtering, sorting, or paginating. You can override instance's members to customize the behavior of your datatable.
* Handlers are called in this order: filter, sort, paginate, display.
*
* In case you are overriding *one* of those handlers, make sure that its return value is compatible with subsequent handlers. Otherwise, you'll require to override all of them.
*
* @tutorial ajax-handler
*/
class Handler {
/**
* Comstructs a new handler, defaulting the handlers to the default ones exposed on the class.
*/
constructor(){
/**
* Filter the provided rows, checking if at least a cell contains one of the specified filters. It supports promises. Defaults to {@link Handler#defaultFilterHandler}.
*
* @method filterHandler
* @memberof Handler
* @instance
* @readonly
* @see TableType#setFilterHandler
* @tutorial ajax-handler
* @param {TRow[]|*} data - The data to apply filter on. It is usually an array of rows, but it can be whatever you set in the {@link Datatable#data} property.
* @param {string[] | string | undefined} filters - The strings to search in cells.
* @param {Column[]} columns - The columns of the table.
* @returns {Promise<TRow[]|*>} The filtered data. It is usually an array of rows, but it can be whatever you like.
*/
this.filterHandler = this.defaultFilterHandler;
/**
* Sort the given rows depending on a specific column & sort order. It suports promises. Defaults to {@link Handler#defaultSortHandler}.
*
* @method sortHandler
* @memberof Handler
* @instance
* @readonly
* @see TableType#setSortHandler
* @tutorial ajax-handler
* @param {TRow[]|*} filteredData - Data outputed from {@link Handler#filterHandler}. It is usually an array of rows, but it can be whatever you like.
* @param {Column} sortColumn - The column used for sorting.
* @param {'asc' | 'desc' | null} sortDir - The direction of the sort.
* @returns {Promise<TRow[]|*>} The sorted rows. It is usually an array of rows, but it can be whatever you like.
*/
this.sortHandler = this.defaultSortHandler;
/**
* Split the rows list to display the requested page index. It supports promises. Defaults to {@link Handler#defaultPaginateHandler}.
*
* @method paginateHandler
* @memberof Handler
* @instance
* @readonly
* @see TableType#setPaginateHandler
* @tutorial ajax-handler
* @param {TRow[]|*} sortedData - Data outputed from {@link Handler#sortHandler}. It is usually an array of rows, but it can be whatever you like.
* @param {number} perPage - The total number of items per page.
* @param {number} pageNumber - The index of the page to display.
* @returns {Promise<TRow[]|*>} The requested page's rows. It is usually an array of rows, but it can be whatever you like.
*/
this.paginateHandler = this.defaultPaginateHandler;
/**
* Handler to post-process the paginated data, and determine which data to actually display. It supports promises. Defaults to {@link Handler#defaultDisplayHandler}.
*
* @method displayHandler
* @memberof Handler
* @instance
* @readonly
* @see TableType#setDisplayHandler
* @tutorial ajax-handler
* @param {object} processSteps - The result of each processing steps, stored in an object. Each step is the result of one of the processing function
* ({@link Handler#filterHandler}, {@link Handler#sortHandler}, {@link Handler#paginateHandler}), applied on the previous step.
* @param {TRow[]|*} processSteps.source - The original {@link Datatable#data} property of the datatable.
* @param {TRow[]|*} processSteps.filtered - The return value of {@link Handler#filterHandler}.
* @param {TRow[]|*} processSteps.sorted - The return value of {@link Handler#sortHandler}.
* @param {TRow[]|*} processSteps.paged - The return value of {@link Handler#paginateHandler}.
* @returns {Promise<DisplayHandlerResult>} Processed values to set on the datatable.
*/
this.displayHandler = this.defaultDisplayHandler;
}
/**
* Filter the provided rows, checking if at least a cell contains one of the specified filters.
*
* @param {TRow[]} data - The data to apply filter on.
* @param {string[] | string | undefined} filters - The strings to search in cells.
* @param {Column[]} columns - The columns of the table.
* @returns {Promise<TRow[]>} The filtered data rows.
*/
defaultFilterHandler( data, filters, columns ){
if ( !Array.isArray( filters ) ) {
filters = ( filters || '' ).split( /\s/ ).filter( v => !!v );
}
if ( filters.length === 0 ){
return data;
}
return data.filter( row => filters.some( filter => this.rowMatches( row, filter, columns ) ) );
}
/**
* Sort the given rows depending on a specific column & sort order.
*
* @param {TRow[]} filteredData - Data outputed from {@link Handler#filterHandler}.
* @param {Column} sortColumn - The column used for sorting.
* @param {'asc' | 'desc' | null} sortDir - The direction of the sort.
* @returns {Promise<TRow[]>} The sorted rows.
*/
defaultSortHandler( filteredData, sortColumn, sortDir ){
if ( !sortColumn || sortDir === null ){
return filteredData;
}
return stableSort( filteredData, ( a, b ) => {
const valA = sortColumn.getRepresentation( a );
const valB = sortColumn.getRepresentation( b );
if ( valA === valB ){
return 0;
}
let sortVal = valA > valB ? 1 : -1;
if ( sortDir === 'desc' ){
sortVal *= -1;
}
return sortVal;
} );
}
/**
* Split the rows list to display the requested page index.
*
* @param {TRow[]} sortedData - Data outputed from {@link Handler#sortHandler}.
* @param {number} perPage - The total number of items per page.
* @param {number} pageNumber - The index of the page to display.
* @returns {Promise<TRow[]>} The requested page's rows.
*/
defaultPaginateHandler( sortedData, perPage, pageNumber ){
if ( perPage < 1 || pageNumber < 1 ){
return sortedData;
}
const startIndex = ( pageNumber - 1 ) * perPage;
const endIndex = ( pageNumber * perPage );
return sortedData.slice( startIndex, endIndex );
}
/**
* Handler to post-process the paginated data, and determine which data to actually display.
*
* @param {object} processSteps - The result of each processing steps, stored in an object. Each step is the result of one of the processing function
* ({@link Handler#filterHandler}, {@link Handler#sortHandler}, {@link Handler#paginateHandler}), applied on the previous step.
* @param {TRow[]|*} processSteps.source - The original {@link Datatable#data} property of the datatable.
* @param {TRow[]|*} processSteps.filtered - The return value of {@link Handler#filterHandler}.
* @param {TRow[]|*} processSteps.sorted - The return value of {@link Handler#sortHandler}.
* @param {TRow[]|*} processSteps.paged - The return value of {@link Handler#paginateHandler}.
* @returns {Promise<DisplayHandlerResult>} Processed values to set on the datatable.
*/
defaultDisplayHandler( {
filtered, paged,
} ){
return {
rows: paged,
totalRowCount: filtered.length,
};
}
/**
* Check if the provided row contains the filter string in *any* column.
*
* @param {TRow} row - The data row to search in.
* @param {string} filterString - The string to match in a column.
* @param {Column[]} columns - The list of columns in the table.
* @returns {boolean} `true` if any column contains the searched string.
*/
rowMatches( row, filterString, columns ){
return columns.some( column => column.matches( row, filterString ) );
}
}
export default Handler;