@revolist/revogrid
Version:
Virtual reactive data grid spreadsheet component - RevoGrid.
307 lines (306 loc) • 14 kB
JavaScript
/*!
* Built by Revolist OU ❤️
*/
import debounce from "lodash/debounce";
import { BasePlugin } from "../base.plugin";
import { getColumnByProp } from "../../utils/column.utils";
import { columnTypes, rowTypes } from "../../store/index";
import { isGrouping } from "../groupingRow/grouping.service";
import { getComparer, getNextOrder, getSortingIndex, hasActiveSorting, sortIndexByItems, } from "./sorting.func";
export * from './sorting.types';
export * from './sorting.func';
export * from './sorting.sign';
function getSortableRowIndexes(indexes, source) {
return indexes.filter(index => !isGrouping(source[index]));
}
function mergeSortedRowsWithGroups(indexes, source, sortedRows) {
if (sortedRows.length === indexes.length) {
return sortedRows;
}
let rowIndex = 0;
return indexes.map(index => {
if (isGrouping(source[index])) {
return index;
}
return sortedRows[rowIndex++];
});
}
/**
* Lifecycle
* 1. @event `beforesorting` - Triggered when sorting just starts. Nothing has happened yet. This can be triggered from a column or from the source. If the type is from rows, the column will be undefined.
* 2. @event `beforesourcesortingapply` - Triggered before the sorting data is applied to the data source. You can prevent this event, and the data will not be sorted.
* 3. @event `beforesortingapply` - Triggered before the sorting data is applied to the data source. You can prevent this event, and the data will not be sorted. This event is only called from a column sorting click.
* 4. @event `aftersortingapply` - Triggered after sorting has been applied and completed. This event occurs for both row and column sorting.
*
* Note: If you prevent an event, it will not proceed to the subsequent steps.
*/
export class SortingPlugin extends BasePlugin {
constructor(revogrid, providers, config) {
super(revogrid, providers);
this.revogrid = revogrid;
/**
* Delayed sorting promise registered in the grid render job queue.
*/
this.sortingPromise = null;
/**
* Debounced sorting entry point.
*
* Sorting can be requested by column changes, source changes, and header
* clicks in quick succession, so the actual sort is delayed and coalesced.
*/
this.postponeSort = debounce((order, comparison, sortingColumns, sortingOrder, ignoreViewportUpdate) => this.runSorting(order, comparison, sortingColumns, sortingOrder, ignoreViewportUpdate), 50);
this.applySortingConfig(config);
this.addEventListener('sortingconfigchanged', ({ detail }) => {
config = detail;
this.applySortingConfig(detail);
this.startSorting(this.sorting, this.sortingFunc, this.sortingColumns, this.sortingOrder);
});
this.addEventListener('beforeheaderrender', ({ detail, }) => {
var _a;
const { data: column } = detail;
if (column.sortable) {
detail.data = Object.assign(Object.assign({}, column), { order: (_a = this.sorting) === null || _a === void 0 ? void 0 : _a[column.prop], sortIndex: getSortingIndex(this.sorting, column.prop, this.sortingOrder) });
}
});
this.addEventListener('beforeanysource', ({ detail: { type }, }) => {
// if sorting was provided - sort data
if (hasActiveSorting(this.sorting) && this.sortingFunc) {
const event = this.emit('beforesourcesortingapply', { type, sorting: this.sorting });
if (event.defaultPrevented) {
return;
}
this.startSorting(this.sorting, this.sortingFunc, this.sortingColumns, this.sortingOrder);
}
});
this.addEventListener('aftercolumnsset', ({ detail: { order }, }) => {
// if config provided - do nothing, read from config
if (config) {
return;
}
const columns = this.providers.column.getColumns();
const sortingFunc = {};
const sortingColumns = {};
const sortingOrder = [];
const sorting = {};
for (let prop in order) {
if (order[prop]) {
const column = getColumnByProp(columns, prop);
const cmp = getComparer(column, order[prop]);
sorting[prop] = order[prop];
sortingFunc[prop] = cmp;
sortingColumns[prop] = column;
sortingOrder.push(prop);
}
}
// set sorting
this.sorting = hasActiveSorting(sorting) ? sorting : undefined;
this.sortingFunc = this.sorting ? sortingFunc : undefined;
this.sortingColumns = this.sorting ? sortingColumns : undefined;
this.sortingOrder = this.sorting ? sortingOrder : undefined;
});
this.addEventListener('beforeheaderclick', (e) => {
var _a, _b, _c, _d;
if (e.defaultPrevented) {
return;
}
if (!((_b = (_a = e.detail) === null || _a === void 0 ? void 0 : _a.column) === null || _b === void 0 ? void 0 : _b.sortable)) {
return;
}
this.headerclick(e.detail.column, (_d = (_c = e.detail) === null || _c === void 0 ? void 0 : _c.originalEvent) === null || _d === void 0 ? void 0 : _d.shiftKey);
});
}
/**
* Creates mutable sorting maps from current state when additive sorting is requested.
*/
createSortingState(additive) {
var _a;
return {
sorting: additive ? Object.assign({}, this.sorting) : {},
sortingFunc: additive ? Object.assign({}, this.sortingFunc) : {},
sortingColumns: additive ? Object.assign({}, this.sortingColumns) : {},
sortingOrder: additive ? [...((_a = this.sortingOrder) !== null && _a !== void 0 ? _a : [])] : [],
};
}
/**
* Stores normalized sorting state, clearing inactive empty maps.
*/
setSortingState({ sorting, sortingFunc, sortingColumns, sortingOrder, }) {
this.sorting = hasActiveSorting(sorting) ? sorting : undefined;
this.sortingFunc = this.sorting ? sortingFunc : undefined;
this.sortingColumns = this.sorting ? sortingColumns : undefined;
this.sortingOrder = this.sorting ? sortingOrder : undefined;
}
/**
* Adds or replaces a column in a sorting state.
*/
setColumnSorting(state, prop, order, cmp, column) {
state.sorting[prop] = order;
state.sortingFunc[prop] = cmp;
state.sortingColumns[prop] = column;
if (!state.sortingOrder.some(sortingProp => String(sortingProp) === String(prop))) {
state.sortingOrder.push(prop);
}
}
/**
* Removes a column from a sorting state.
*/
clearColumnSorting(state, prop) {
delete state.sorting[prop];
delete state.sortingFunc[prop];
delete state.sortingColumns[prop];
const index = state.sortingOrder.findIndex(sortingProp => String(sortingProp) === String(prop));
if (index >= 0) {
state.sortingOrder.splice(index, 1);
}
}
/**
* Normalizes external sorting configuration into internal order,
* comparator, and column metadata maps.
*/
applySortingConfig(cfg) {
var _a;
if (!cfg) {
return;
}
const state = this.createSortingState(cfg.additive);
(_a = cfg.columns) === null || _a === void 0 ? void 0 : _a.forEach(col => {
if (col.order) {
this.setColumnSorting(state, col.prop, col.order, getComparer(col, col.order), col);
return;
}
this.clearColumnSorting(state, col.prop);
});
this.setSortingState(state);
}
startSorting(order, sortingFunc, sortingColumns, sortingOrder, ignoreViewportUpdate) {
if (!this.sortingPromise) {
// add job before render
this.revogrid.jobsBeforeRender.push(new Promise(resolve => {
this.sortingPromise = resolve;
}));
}
if (typeof sortingColumns === 'boolean') {
this.postponeSort(order, sortingFunc, undefined, undefined, sortingColumns);
return;
}
this.postponeSort(order, sortingFunc, sortingColumns, sortingOrder, ignoreViewportUpdate);
}
/**
* Applies sorting requested by a sortable header click.
*
* @param column - Column that initiated sorting.
* @param additive - If true, add/remove this column from the current multi-sort state.
*/
headerclick(column, additive) {
var _a;
const columnProp = column.prop;
let order = getNextOrder((_a = this.sorting) === null || _a === void 0 ? void 0 : _a[columnProp]);
const beforeEvent = this.emit('beforesorting', { column, order, additive });
if (beforeEvent.defaultPrevented) {
return;
}
order = beforeEvent.detail.order;
// apply sort data
const beforeApplyEvent = this.emit('beforesortingapply', {
column: beforeEvent.detail.column,
order,
additive,
});
if (beforeApplyEvent.defaultPrevented) {
return;
}
const cmp = getComparer(beforeApplyEvent.detail.column, beforeApplyEvent.detail.order);
this.applyHeaderSorting(beforeApplyEvent.detail.column, beforeApplyEvent.detail.additive, order, cmp);
this.startSorting(this.sorting, this.sortingFunc, this.sortingColumns, this.sortingOrder);
}
/**
* Applies sorting state produced by a header click.
*/
applyHeaderSorting(column, additive, order, cmp) {
if (!additive) {
this.setSortingState(order ? {
sorting: { [column.prop]: order },
sortingFunc: { [column.prop]: cmp },
sortingColumns: { [column.prop]: column },
sortingOrder: [column.prop],
} : this.createSortingState());
return;
}
const state = this.createSortingState(true);
if (order) {
this.setColumnSorting(state, column.prop, order, cmp, column);
}
else {
this.clearColumnSorting(state, column.prop);
}
this.setSortingState(state);
}
runSorting(order, comparison, sortingColumns, sortingOrder, ignoreViewportUpdate) {
var _a, _b;
if (typeof sortingColumns === 'boolean') {
this.sort(order, comparison, undefined, undefined, undefined, sortingColumns);
(_a = this.sortingPromise) === null || _a === void 0 ? void 0 : _a.call(this);
this.sortingPromise = null;
return;
}
this.sort(order, comparison, sortingColumns, sortingOrder, undefined, ignoreViewportUpdate);
(_b = this.sortingPromise) === null || _b === void 0 ? void 0 : _b.call(this);
this.sortingPromise = null;
}
sort(sorting, sortingFunc, sortingColumns, sortingOrder, types = rowTypes, ignoreViewportUpdate = false) {
let activeSortingColumns;
let activeSortingOrder;
let activeTypes = types;
let activeIgnoreViewportUpdate = ignoreViewportUpdate;
if (Array.isArray(sortingColumns)) {
activeTypes = sortingColumns;
activeIgnoreViewportUpdate = typeof sortingOrder === 'boolean' ? sortingOrder : false;
}
else {
activeSortingColumns = sortingColumns;
activeSortingOrder = Array.isArray(sortingOrder) ? sortingOrder : undefined;
}
// if no sorting - reset
if (!Object.keys(sorting || {}).length) {
for (let type of activeTypes) {
const storeService = this.providers.data.stores[type];
// row data
const source = storeService.store.get('source');
// row indexes
const proxyItems = storeService.store.get('proxyItems');
// row indexes
const newItemsOrder = Array.from({ length: source.length }, (_, i) => i); // recover indexes range(0, source.length)
this.providers.dimension.updateSizesPositionByNewDataIndexes(type, newItemsOrder, proxyItems);
storeService.setData({ proxyItems: newItemsOrder });
}
}
else {
for (let type of activeTypes) {
const storeService = this.providers.data.stores[type];
// row data
const source = storeService.store.get('source');
// row indexes
const proxyItems = storeService.store.get('proxyItems');
const sortItems = getSortableRowIndexes(proxyItems, source);
const sortedItems = sortIndexByItems([...sortItems], source, sortingFunc, sorting, activeSortingColumns, activeSortingOrder);
const newItemsOrder = mergeSortedRowsWithGroups(proxyItems, source, sortedItems);
// take row indexes before trim applied and proxy items
const prevItems = storeService.store.get('items');
storeService.setData({
proxyItems: newItemsOrder,
});
// take currently visible row indexes
const newItems = storeService.store.get('items');
if (!activeIgnoreViewportUpdate) {
this.providers.dimension
.updateSizesPositionByNewDataIndexes(type, newItems, prevItems);
}
}
}
// refresh columns to redraw column headers and show correct icon
columnTypes.forEach((type) => {
this.providers.column.dataSources[type].refresh();
});
this.emit('aftersortingapply');
}
}