UNPKG

@revolist/revogrid

Version:

Virtual reactive data grid spreadsheet component - RevoGrid.

307 lines (306 loc) 14 kB
/*! * 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'); } }