UNPKG

@revolist/revogrid

Version:

Virtual reactive data grid spreadsheet component - RevoGrid.

255 lines (254 loc) 11.8 kB
/*! * Built by Revolist OU ❤️ */ import { getPhysical, setItems, columnTypes, } from "../../store/index"; import { BasePlugin } from "../base.plugin"; import { FILTER_TRIMMED_TYPE } from "../filter/filter.plugin"; import { SortingPlugin } from "../sorting/sorting.plugin"; import { GROUP_EXPAND_EVENT, GROUPING_ROW_TYPE, PSEUDO_GROUP_COLUMN, } from "./grouping.const"; import { doExpand, doCollapse } from "./grouping.row.expand.service"; import { gatherGrouping, getExpanded, getSource, isGrouping, isGroupingColumn, } from "./grouping.service"; import { filterOutEmptyGroupRows, processDoubleConversionTrimmed, TRIMMED_GROUPING, } from "./grouping.trimmed.service"; export * from './grouping.const'; export * from './grouping.row.expand.service'; export * from './grouping.row.types'; export * from './grouping.service'; export * from './grouping.row.renderer'; export class GroupingRowPlugin extends BasePlugin { getStore(type = GROUPING_ROW_TYPE) { return this.providers.data.stores[type].store; } constructor(revogrid, providers) { super(revogrid, providers); } // befoce cell focus onFocus(e) { if (isGrouping(e.detail.model)) { e.preventDefault(); } } // expand event triggered onExpand({ virtualIndex }) { const { source } = getSource(this.getStore().get('source'), this.getStore().get('proxyItems')); let newTrimmed = this.getStore().get('trimmed')[TRIMMED_GROUPING]; let i = getPhysical(this.getStore(), virtualIndex); const isExpanded = getExpanded(source[i]); if (!isExpanded) { const { trimmed, items } = doExpand(virtualIndex, source, this.getStore().get('items')); newTrimmed = Object.assign(Object.assign({}, newTrimmed), trimmed); if (items) { setItems(this.getStore(), items); } } else { const { trimmed } = doCollapse(i, source); newTrimmed = Object.assign(Object.assign({}, newTrimmed), trimmed); this.revogrid.clearFocus(); } this.getStore().set('source', source); this.revogrid.addTrimmed(newTrimmed, TRIMMED_GROUPING); } setColumnGrouping(cols) { // if 0 column as holder if (cols === null || cols === void 0 ? void 0 : cols.length) { cols[0][PSEUDO_GROUP_COLUMN] = true; return true; } return false; } setColumns({ columns }) { for (let type of columnTypes) { if (this.setColumnGrouping(columns[type])) { break; } } } // evaluate drag between groups onDrag(e) { const { from, to } = e.detail; const isDown = to - from >= 0; const { source } = getSource(this.getStore().get('source'), this.getStore().get('proxyItems')); const items = this.getStore().get('items'); let i = isDown ? from : to; const end = isDown ? to : from; for (; i < end; i++) { const model = source[items[i]]; const isGroup = isGrouping(model); if (isGroup) { e.preventDefault(); return; } } } beforeTrimmedApply(trimmed, type) { /** Filter trim must keep group headers in sync with their visible children. */ if (type === FILTER_TRIMMED_TYPE) { const source = this.getStore().get('source'); const updatedTrimmed = filterOutEmptyGroupRows(source, trimmed); Object.keys(trimmed).forEach(index => delete trimmed[Number.parseInt(index, 10)]); Object.assign(trimmed, updatedTrimmed); } } beforeFilterTrimmed(trimmed) { const source = this.getStore().get('source'); return filterOutEmptyGroupRows(source, trimmed); } isSortingRunning() { const sortingPlugin = this.providers.plugins.getByClass(SortingPlugin); return !!(sortingPlugin === null || sortingPlugin === void 0 ? void 0 : sortingPlugin.sortingPromise); } /** * Starts global source update with group clearing and applying new one * Initiated when need to reapply grouping */ doSourceUpdate(options) { var _a; /** * Get source without grouping * @param newOldIndexMap - provides us mapping with new indexes vs old indexes, we would use it for trimmed mapping */ const store = this.getStore(); const { source, prevExpanded, oldNewIndexes } = getSource(store.get('source'), store.get('proxyItems'), true); const expanded = Object.assign({ prevExpanded }, options); /** * Group again * @param oldNewIndexMap - provides us mapping with new indexes vs old indexes */ const { sourceWithGroups, depth, trimmed, oldNewIndexMap, } = gatherGrouping(source, ((_a = this.options) === null || _a === void 0 ? void 0 : _a.props) || [], expanded); const customRenderer = options === null || options === void 0 ? void 0 : options.groupLabelTemplate; // setup source this.providers.data.setData(sourceWithGroups, GROUPING_ROW_TYPE, this.revogrid.disableVirtualY, { depth, customRenderer }, true); this.updateTrimmed(trimmed, oldNewIndexes !== null && oldNewIndexes !== void 0 ? oldNewIndexes : {}, oldNewIndexMap, sourceWithGroups); } /** * Apply grouping on data set * Clear grouping from source * If source came from other plugin */ onDataSet(data) { var _a, _b; let preservedExpanded = {}; if (((_a = this.options) === null || _a === void 0 ? void 0 : _a.preserveGroupingOnUpdate) !== false) { let { prevExpanded } = getSource(this.getStore().get('source'), this.getStore().get('proxyItems'), true); preservedExpanded = prevExpanded; } const source = data.source.filter(s => !isGrouping(s)); const options = Object.assign(Object.assign({}, (this.revogrid.grouping || {})), { prevExpanded: preservedExpanded }); const { sourceWithGroups, depth, trimmed, oldNewIndexMap, } = gatherGrouping(source, ((_b = this.options) === null || _b === void 0 ? void 0 : _b.props) || [], options); data.source = sourceWithGroups; this.providers.data.setGrouping({ depth }); this.updateTrimmed(trimmed, oldNewIndexMap, undefined, sourceWithGroups); } /** * External call to apply grouping. Called by revogrid when prop changed. */ setGrouping(options) { var _a, _b; // unsubscribe from all events when group applied this.clearSubscriptions(); this.options = options; // clear props, no grouping exists if (!((_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.length)) { this.clearGrouping(); return; } // props exist and source initd const store = this.getStore(); const { source } = getSource(store.get('source'), store.get('proxyItems')); if (source.length) { this.doSourceUpdate(Object.assign({}, options)); } // props exist and columns initd for (let t of columnTypes) { if (this.setColumnGrouping(this.providers.column.getColumns(t))) { this.providers.column.refreshByType(t); break; } } // if has any grouping subscribe to events again /** if grouping present and new data source arrived */ this.addEventListener('beforesourceset', ({ detail }) => { var _a, _b, _c; if (!(((_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.length) && ((_c = detail === null || detail === void 0 ? void 0 : detail.source) === null || _c === void 0 ? void 0 : _c.length))) { return; } // if sorting is running don't apply grouping, wait for sorting, then it'll apply in @aftersortingapply if (this.isSortingRunning()) { return; } this.onDataSet(detail); }); this.addEventListener('beforecolumnsset', ({ detail }) => { this.setColumns(detail); }); /** * filter applied need to clear grouping and apply again * based on new results can be new grouping */ this.addEventListener('beforetrimmed', ({ detail: { trimmed, trimmedType } }) => this.beforeTrimmedApply(trimmed, trimmedType)); /** Filter plugin owns data-row matching; grouping decides which headers remain visible. */ this.addEventListener('beforefiltertrimmed', ({ detail }) => { detail.itemsToFilter = this.beforeFilterTrimmed(detail.itemsToFilter); }); /** * sorting applied need to clear grouping and apply again * based on new results whole grouping order will changed */ this.addEventListener('aftersortingapply', () => { var _a, _b; if (!((_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.length)) { return; } this.doSourceUpdate(Object.assign({}, this.options)); }); /** * Apply logic for focus inside of grouping * We can't focus on grouping rows, navigation only inside of groups for now */ this.addEventListener('beforecellfocus', e => this.onFocus(e)); /** * Prevent rgRow drag outside the group */ this.addEventListener('roworderchanged', e => this.onDrag(e)); /** * When grouping expand icon was clicked */ this.addEventListener(GROUP_EXPAND_EVENT, e => this.onExpand(e.detail)); } // clear grouping clearGrouping() { // clear columns columnTypes.forEach(t => { const cols = this.providers.column.getColumns(t); let deleted = false; cols.forEach(c => { if (isGroupingColumn(c)) { delete c[PSEUDO_GROUP_COLUMN]; deleted = true; } }); // if column store had grouping clear and refresh if (deleted) { this.providers.column.refreshByType(t); } }); // clear rows const { source, oldNewIndexes } = getSource(this.getStore().get('source'), this.getStore().get('proxyItems'), true); this.providers.data.setData(source, GROUPING_ROW_TYPE, this.revogrid.disableVirtualY, undefined, true); this.updateTrimmed(undefined, undefined, oldNewIndexes, source); } updateTrimmed(trimmedGroup = {}, firstLevelMap = {}, secondLevelMap, source = this.getStore().get('source')) { // map previously trimmed data const trimemedOptionsToUpgrade = processDoubleConversionTrimmed(this.getStore().get('trimmed'), firstLevelMap, secondLevelMap); for (let type in trimemedOptionsToUpgrade) { if (type === FILTER_TRIMMED_TYPE) { /** Regrouping changes physical indexes, so filter trim needs fresh group-header state. */ trimemedOptionsToUpgrade[type] = filterOutEmptyGroupRows(source, trimemedOptionsToUpgrade[type]); } this.revogrid.addTrimmed(trimemedOptionsToUpgrade[type], type); } // const emptyGroups = this.filterOutEmptyGroups(trimemedOptionsToUpgrade, childrenByGroup); // setup trimmed data for grouping this.revogrid.addTrimmed(Object.assign({}, trimmedGroup), TRIMMED_GROUPING); } }