UNPKG

@serenity-is/corelib

Version:
1,390 lines (1,192 loc) 55.1 kB
import { EventData, EventEmitter, Grid, Group, GroupItemMetadataProvider, GroupTotals, gridDefaults } from "@serenity-is/sleekgrid"; import { ListRequest, ListResponse, ServiceOptions, ServiceResponse, htmlEncode, localText, serviceCall } from "../base"; import { deepClone, extend } from "../compat"; import { AggregateFormatting } from "./aggregators"; import { GroupInfo, PagingOptions, SummaryOptions } from "./slicktypes"; export interface RemoteViewOptions { autoLoad?: boolean; idField?: string; contentType?: string; dataType?: string; filter?: any; params?: any; onSubmit?: CancellableViewCallback<any>; url?: string; localSort?: boolean; sortBy?: any; rowsPerPage?: number; seekToPage?: number; onProcessData?: RemoteViewProcessCallback<any>; method?: string; inlineFilters?: boolean; groupItemMetadataProvider?: GroupItemMetadataProvider; onAjaxCall?: RemoteViewAjaxCallback<any>; getItemMetadata?: (p1?: any, p2?: number) => any; errorMsg?: string; } export interface PagingInfo { rowsPerPage: number; page: number, totalCount: number; loading: boolean, error: string; dataView: RemoteView<any> } export type CancellableViewCallback<TEntity> = (view: RemoteView<TEntity>) => boolean | void; export type RemoteViewAjaxCallback<TEntity> = (view: RemoteView<TEntity>, options: ServiceOptions<ListResponse<TEntity>>) => boolean | void; export type RemoteViewFilter<TEntity> = (item: TEntity, view: RemoteView<TEntity>) => boolean; export type RemoteViewProcessCallback<TEntity> = (data: ListResponse<TEntity>, view: RemoteView<TEntity>) => ListResponse<TEntity>; export interface RemoteView<TEntity> { onSubmit: CancellableViewCallback<TEntity>; onDataChanged: EventEmitter; onDataLoading: EventEmitter; onDataLoaded: EventEmitter; onPagingInfoChanged: EventEmitter; onRowCountChanged: EventEmitter; onRowsChanged: EventEmitter; onRowsOrCountChanged: EventEmitter; getPagingInfo(): PagingInfo; onGroupExpanded: EventEmitter, onGroupCollapsed: EventEmitter, onAjaxCall: RemoteViewAjaxCallback<TEntity>; onProcessData: RemoteViewProcessCallback<TEntity>; addData(data: ListResponse<TEntity>): void; beginUpdate(): void; endUpdate(): void; deleteItem(id: any): void; getItems(): TEntity[]; setFilter(filter: RemoteViewFilter<TEntity>): void; getFilter(): RemoteViewFilter<TEntity>; getFilteredItems(): any; getGroupItemMetadataProvider(): GroupItemMetadataProvider; setGroupItemMetadataProvider(value: GroupItemMetadataProvider): void; fastSort: any; setItems(items: any[], newIdProperty?: boolean | string): void; getIdPropertyName(): string; getItemById(id: any): TEntity; getGrandTotals(): any; getGrouping(): GroupInfo<TEntity>[]; getGroups(): any[]; getRowById(id: any): number; getRowByItem(item: any): number; getRows(): any[]; mapItemsToRows(itemArray: any[]): any[]; mapRowsToIds(rowArray: number[]): any[]; mapIdsToRows(idAray: any[]): number[]; setFilterArgs(args: any): void; setRefreshHints(hints: any[]): void; insertItem(insertBefore: number, item: any): void; sortedAddItem(item: any): void; sortedUpdateItem(id: any, item: any): void; syncGridSelection(grid: any, preserveHidden?: boolean, preserveHiddenOnSelectionChange?: boolean): void; syncGridCellCssStyles(grid: any, key: string): void; getItemMetadata(i: number): any; updateItem(id: any, item: TEntity): void; addItem(item: TEntity): void; getIdxById(id: any): any; getItemByIdx(index: number): any; setGrouping(groupInfo: GroupInfo<TEntity>[]): void; collapseAllGroups(level: number): void; expandAllGroups(level: number): void; expandGroup(keys: any[]): void; collapseGroup(keys: any[]): void; setSummaryOptions(options: SummaryOptions): void; setPagingOptions(options: PagingOptions): void; refresh(): void; populate(): void; populateLock(): void; populateUnlock(): void; getItem(row: number): any; getLength(): number; rowsPerPage: number; errormsg: string; params: any; getLocalSort(): boolean; setLocalSort(value: boolean): void; sort(comparer?: (a: any, b: any) => number, ascending?: boolean): void; reSort(): void; sortBy: string[]; url: string; method: string; idField: string; seekToPage?: number; } export class RemoteView<TEntity> { constructor(options: RemoteViewOptions) { var self = this; if (gridDefaults != null && gridDefaults.groupTotalsFormatter === void 0) gridDefaults.groupTotalsFormatter = AggregateFormatting.groupTotalsFormatter; var idProperty: string; var items: any[] = []; var rows: any[] = []; var idxById: Record<any, number> = {}; var rowsById: any = null; var filter: any = null; var updated: any = null; var suspend = 0; var sortAsc = true; var fastSortField: string; var sortComparer: any; var refreshHints: any = {}; var prevRefreshHints: any = {}; var filterArgs: any; var filteredItems: any = []; var compiledFilter: any; var compiledFilterWithCaching: any; var filterCache: any[] = []; var groupingInfoDefaults = { getter: <any>null, formatter: <any>null, comparer: function (a: any, b: any) { return (a.value === b.value ? 0 : (a.value > b.value ? 1 : -1) ); }, predefinedValues: <any[]>[], aggregateEmpty: false, aggregateCollapsed: false, aggregateChildGroups: false, collapsed: false, displayTotalsRow: true, lazyTotalsCalculation: false }; var summaryOptions: any = {}; var groupingInfos: any[] = []; var groups: any[] = []; var toggledGroupsByLevel: any[] = []; var groupingDelimiter = ':|:'; var page = 1; var totalRows = 0; var onDataChanged: EventEmitter = new EventEmitter(); var onDataLoading: EventEmitter = new EventEmitter(); var onDataLoaded: EventEmitter = new EventEmitter(); var onGroupExpanded: EventEmitter = new EventEmitter(); var onGroupCollapsed: EventEmitter = new EventEmitter(); var onPagingInfoChanged: EventEmitter = new EventEmitter(); var onRowCountChanged: EventEmitter = new EventEmitter(); var onRowsChanged: EventEmitter = new EventEmitter(); var onRowsOrCountChanged: EventEmitter = new EventEmitter(); var loading: AbortController | boolean = false; var errorMessage: string = null; var populateLocks = 0; var populateCalls = 0; var contentType: string; var dataType: string; var totalCount: number = null; var groupItemMetadataProvider = options.groupItemMetadataProvider; var localSort: boolean = options.localSort ?? false; var intf: RemoteView<TEntity>; function beginUpdate() { suspend++; } function endUpdate() { suspend--; if (suspend <= 0) refresh(); } function setRefreshHints(hints: any) { refreshHints = hints; } function setFilterArgs(args: any) { filterArgs = args; } function updateIdxById(startingIndex?: number) { startingIndex = startingIndex || 0; var id: any; for (var i = startingIndex, l = items.length; i < l; i++) { id = items[i][idProperty]; if (id === undefined) { var msg = "Each data element must implement a unique '" + idProperty + "' property. Object at index '" + i + "' " + "has no identity value: "; msg += JSON.stringify(items[i]); throw msg; } idxById[id] = i; } } function ensureIdUniqueness() { var id: any; for (var i = 0, l = items.length; i < l; i++) { id = items[i][idProperty]; if (id === undefined || idxById[id] !== i) { var msg = "Each data element must implement a unique '" + idProperty + "' property. Object at index '" + i + "' "; if (id == undefined) msg += "has no identity value: "; else msg += "has repeated identity value '" + id + "': "; msg += JSON.stringify(items[i]); throw msg; } } } function getItems() { return items; } function getIdPropertyName() { return idProperty; } function setItems(data: any[], newIdProperty?: string | boolean) { if (newIdProperty != null && typeof newIdProperty == "string") idProperty = newIdProperty; items = filteredItems = data; if (localSort) { items.sort(getSortComparer()); } idxById = {}; rowsById = null; summaryOptions.totals = {}; updateIdxById(); ensureIdUniqueness(); if (suspend) { recalc(items); } else { refresh(); } onDataChanged.notify({ dataView: self }, null, self); } function setPagingOptions(args: any) { var anyChange = false; if (args.rowsPerPage != undefined && intf.rowsPerPage != args.rowsPerPage) { intf.rowsPerPage = args.rowsPerPage; anyChange = true; } if (args.page != undefined) { var newPage: number; if (!intf.rowsPerPage) newPage = 1; else if (totalCount == null) newPage = args.page; else newPage = Math.min(args.page, Math.ceil(totalCount / intf.rowsPerPage) + 1); if (newPage < 1) newPage = 1; if (newPage != page) { intf.seekToPage = newPage; anyChange = true; } } if (anyChange) populate(); } function getPagingInfo(): PagingInfo { return { rowsPerPage: intf.rowsPerPage, page: page, totalCount: totalCount, loading: loading != null && loading != false, error: errorMessage, dataView: intf }; } function getSortComparer() { if (sortComparer != null) return sortComparer; var cols: string[] = []; var asc: boolean[] = []; var sorts = intf.sortBy || []; for (var s of sorts) { if (s == null) continue; if (s.length > 5 && s.toLowerCase().substring(s.length - 5).toLowerCase() == ' desc') { asc.push(false); cols.push(s.substring(0, s.length - 5)); } else { asc.push(true); cols.push(s); } } return function (a: any, b: any) { for (var i = 0, l = cols.length; i < l; i++) { var field = cols[i]; var sign = asc[i] ? 1 : -1; var value1 = a[field], value2 = b[field]; var result = (value1 == value2 ? 0 : (value1 > value2 ? 1 : -1)) * sign; if (result != 0) { return result; } } return 0; } } function sort(comparer?: (a: any, b: any) => number, ascending?: boolean) { sortAsc = ascending; fastSortField = null; if (ascending === false) { items.reverse(); } sortComparer = comparer; items.sort(getSortComparer()); if (ascending === false) { items.reverse(); } idxById = {}; updateIdxById(); refresh(); } function getLocalSort(): boolean { return localSort; } function setLocalSort(value: boolean) { if (localSort != value) { localSort = value; sort(); } } /*** * Provides a workaround for the extremely slow sorting in IE. * Does a [lexicographic] sort on a give column by temporarily overriding Object.prototype.toString * to return the value of that field and then doing a native Array.sort(). */ function fastSort(field: any, ascending: boolean) { sortAsc = ascending; fastSortField = field; sortComparer = null; var oldToString = Object.prototype.toString; Object.prototype.toString = (typeof field === "function") ? field : function () { return this[field] }; // an extra reversal for descending sort keeps the sort stable // (assuming a stable native sort implementation, which isn't true in some cases) if (ascending === false) { items.reverse(); } items.sort(); Object.prototype.toString = oldToString; if (ascending === false) { items.reverse(); } idxById = {}; updateIdxById(); refresh(); } function reSort() { if (fastSortField) fastSort(fastSortField, sortAsc); else sort(sortComparer, sortAsc); } function getFilteredItems() { return filteredItems; } function getFilter() { return filter; } function setFilter(filterFn: any) { filter = filterFn; if (options.inlineFilters) { compiledFilter = compileFilter(); compiledFilterWithCaching = compileFilterWithCaching(); } refresh(); } function getGrouping() { return groupingInfos; } function setSummaryOptions(summary: any) { summary = summary || {}; summaryOptions.aggregators = summary.aggregators || []; summaryOptions.compiledAccumulators = []; summaryOptions.totals = {}; var idx = summaryOptions.aggregators.length; while (idx--) { summaryOptions.compiledAccumulators[idx] = compileAccumulatorLoop(summaryOptions.aggregators[idx]); } setGrouping(groupingInfos || []); } function getGrandTotals() { summaryOptions.totals = summaryOptions.totals || {}; if (!summaryOptions.totals.initialized) { summaryOptions.aggregators = summaryOptions.aggregators || []; summaryOptions.compiledAccumulators = summaryOptions.compiledAccumulators || []; var agg: any, idx = summaryOptions.aggregators.length; while (idx--) { agg = summaryOptions.aggregators[idx]; agg.init(); summaryOptions.compiledAccumulators[idx].call(agg, items); agg.storeResult(summaryOptions.totals); } summaryOptions.totals.initialized = true; } return summaryOptions.totals; } function setGrouping(groupingInfo: any) { if (!groupItemMetadataProvider) { groupItemMetadataProvider = new GroupItemMetadataProvider(); } groups = []; toggledGroupsByLevel = []; groupingInfo = groupingInfo || []; groupingInfos = (groupingInfo instanceof Array) ? groupingInfo : [groupingInfo]; for (var i = 0; i < groupingInfos.length; i++) { var gi = groupingInfos[i] = extend(extend<any>({}, groupingInfoDefaults), deepClone(groupingInfos[i])); gi.aggregators = gi.aggregators || summaryOptions.aggregators || []; gi.getterIsAFn = typeof gi.getter === "function"; // pre-compile accumulator loops gi.compiledAccumulators = []; var idx = gi.aggregators.length; while (idx--) { gi.compiledAccumulators[idx] = compileAccumulatorLoop(gi.aggregators[idx]); } toggledGroupsByLevel[i] = {}; } refresh(); } function getItemByIdx(i: number) { return items[i]; } function getIdxById(id: any) { return idxById[id]; } function ensureRowsByIdCache() { if (!rowsById) { rowsById = {}; for (var i = 0, l = rows.length; i < l; i++) { rowsById[rows[i][idProperty]] = i; } } } function getRowByItem(item: any) { ensureRowsByIdCache(); return rowsById[item[idProperty]]; } function getRowById(id: any) { ensureRowsByIdCache(); return rowsById[id]; } function getItemById(id: any) { return items[idxById[id]]; } function mapItemsToRows(itemArray: any[]) { var rows = []; ensureRowsByIdCache(); for (var i = 0, l = itemArray.length; i < l; i++) { var row = rowsById[itemArray[i][idProperty]]; if (row != null) { rows[rows.length] = row; } } return rows; } function mapIdsToRows(idArray: any[]) { var rows: any[] = []; ensureRowsByIdCache(); for (var i = 0, l = idArray.length; i < l; i++) { var row = rowsById[idArray[i]]; if (row != null) { rows[rows.length] = row; } } return rows; } function mapRowsToIds(rowArray: any[]) { var ids: any[] = []; for (var i = 0, l = rowArray.length; i < l; i++) { if (rowArray[i] < rows.length) { ids[ids.length] = rows[rowArray[i]][idProperty]; } } return ids; } function updateItem(id: any, item: any) { if (idxById[id] === undefined) { throw new Error("Invalid id"); } if (id !== item[idProperty]) { // make sure the new id is unique: var newId = item[idProperty]; if (newId == null) { throw new Error("Cannot update item to associate with a null id"); } if (idxById[newId] !== undefined) { throw new Error("Cannot update item to associate with a non-unique id"); } idxById[newId] = idxById[id]; delete idxById[id]; if (updated && updated[id]) { delete updated[id]; } id = newId; } items[idxById[id]] = item; if (!updated) { updated = {}; } updated[id] = true; refresh(); } function insertItem(insertBefore: number, item: any) { items.splice(insertBefore, 0, item); updateIdxById(insertBefore); refresh(); } function addItem(item: any) { items.push(item); updateIdxById(items.length - 1); refresh(); } function deleteItem(id: any) { var idx = idxById[id]; if (idx === undefined) { throw "Invalid id"; } delete idxById[id]; items.splice(idx, 1); updateIdxById(idx); refresh(); } function sortedAddItem(item: any) { insertItem(sortedIndex(item), item); } function sortedUpdateItem(id: any, item: any) { if (idxById[id] === undefined || id !== item[idProperty]) { throw new Error("Invalid or non-matching id " + idxById[id]); } var comparer = getSortComparer(); var oldItem = getItemById(id); if (comparer(oldItem, item) !== 0) { // item affects sorting -> must use sorted add deleteItem(id); sortedAddItem(item); } else { // update does not affect sorting -> regular update works fine updateItem(id, item); } } function sortedIndex(searchItem: any) { var low = 0, high = items.length; var comparer = getSortComparer(); while (low < high) { var mid = low + high >>> 1; if (comparer(items[mid], searchItem) === -1) { low = mid + 1; } else { high = mid; } } return low; } function getRows() { return rows; } function getLength() { return rows.length; } function getItem(i: number) { var item = rows[i]; // if this is a group row, make sure totals are calculated and update the title if (item && item.__group && item.totals && !item.totals.initialized) { var gi = groupingInfos[item.level]; if (!gi.displayTotalsRow) { calculateTotals(item.totals); item.title = gi.formatter ? gi.formatter(item) : htmlEncode(item.value); } } // if this is a totals row, make sure it's calculated else if (item && item.__groupTotals && !item.initialized) { calculateTotals(item); } return item; } function getItemMetadata(i: number) { var item = rows[i]; if (item === undefined) { return null; } // overrides for grouping rows if (item.__group) { return groupItemMetadataProvider.getGroupRowMetadata(item); } // overrides for totals rows if (item.__groupTotals) { return groupItemMetadataProvider.getTotalsRowMetadata(item); } return (options.getItemMetadata && options.getItemMetadata(item, i)) || null; } function expandCollapseAllGroups(level: number, collapse: boolean) { if (level == null) { for (var i = 0; i < groupingInfos.length; i++) { toggledGroupsByLevel[i] = {}; groupingInfos[i].collapsed = collapse; if (collapse === true) { onGroupCollapsed.notify({ level: i, groupingKey: null }); } else { onGroupExpanded.notify({ level: i, groupingKey: null }); } } } else { toggledGroupsByLevel[level] = {}; groupingInfos[level].collapsed = collapse; if (collapse === true) { onGroupCollapsed.notify({ level: level, groupingKey: null }); } else { onGroupExpanded.notify({ level: level, groupingKey: null }); } } refresh(); } /** * @param level {Number} Optional level to collapse. If not specified, applies to all levels. */ function collapseAllGroups(level: number) { expandCollapseAllGroups(level, true); } /** * @param level {Number} Optional level to expand. If not specified, applies to all levels. */ function expandAllGroups(level: number) { expandCollapseAllGroups(level, false); } function resolveLevelAndGroupingKey(args: any) { var arg0 = args[0]; if (args.length === 1 && arg0.indexOf(groupingDelimiter) !== -1) { return { level: arg0.split(groupingDelimiter).length - 1, groupingKey: arg0 }; } else { return { level: args.length - 1, groupingKey: args.join(groupingDelimiter) }; } } function expandCollapseGroup(args: any, collapse: any) { var opts = resolveLevelAndGroupingKey(args); toggledGroupsByLevel[opts.level][opts.groupingKey] = groupingInfos[opts.level].collapsed ^ collapse; if (collapse) onGroupCollapsed.notify({ level: opts.level, groupingKey: opts.groupingKey }); else onGroupExpanded.notify({ level: opts.level, groupingKey: opts.groupingKey }); refresh(); } /** * @param varArgs Either a Slick.Group's "groupingKey" property, or a * variable argument list of grouping values denoting a unique path to the row. For * example, calling collapseGroup('high', '10%') will collapse the '10%' subgroup of * the 'high' group. */ function collapseGroup(varArgs: any[]) { var args = Array.prototype.slice.call(arguments); expandCollapseGroup(args, true); } /** * @param varArgs Either a Slick.Group's "groupingKey" property, or a * variable argument list of grouping values denoting a unique path to the row. For * example, calling expandGroup('high', '10%') will expand the '10%' subgroup of * the 'high' group. */ function expandGroup(varArgs: any[]) { var args = Array.prototype.slice.call(arguments); expandCollapseGroup(args, false); } function getGroups() { return groups; } function getOrCreateGroup(groupsByVal: any, val: any, level: number, parentGroup: any, groups: any[]) { var group = groupsByVal[val]; if (!group) { group = new Group<any>(); group.value = val; group.level = level; group.groupingKey = (parentGroup ? parentGroup.groupingKey + groupingDelimiter : '') + val; groups[groups.length] = group; groupsByVal[val] = group; } return group; } function extractGroups(rows: any[], parentGroup?: any) { var group: any; var val: any; var groups: any[] = []; var groupsByVal = {}; var r: any; var level = parentGroup ? parentGroup.level + 1 : 0; var gi = groupingInfos[level]; for (var i = 0, l: number = gi.predefinedValues.length; i < l; i++) { val = gi.predefinedValues[i]; group = getOrCreateGroup(groupsByVal, val, level, parentGroup, groups); } for (var i = 0, l = rows.length; i < l; i++) { r = rows[i]; val = gi.getterIsAFn ? gi.getter(r) : r[gi.getter]; group = getOrCreateGroup(groupsByVal, val, level, parentGroup, groups); group.rows[group.count++] = r; } if (level < groupingInfos.length - 1) { for (var i = 0; i < groups.length; i++) { group = groups[i]; group.groups = extractGroups(group.rows, group); } } if (groups.length) { addTotals(groups, level); } groups.sort(groupingInfos[level].comparer); return groups; } function calculateTotals(totals: any) { var group = totals.group; var gi = groupingInfos[group.level]; var isLeafLevel = (group.level == groupingInfos.length); var agg: any, idx = gi.aggregators.length; if (!isLeafLevel && gi.aggregateChildGroups) { // make sure all the subgroups are calculated var i = group.groups.length; while (i--) { if (!group.groups[i].totals.initialized) { calculateTotals(group.groups[i].totals); } } } while (idx--) { agg = gi.aggregators[idx]; agg.init(); if (!isLeafLevel && gi.aggregateChildGroups) { gi.compiledAccumulators[idx].call(agg, group.groups); } else { gi.compiledAccumulators[idx].call(agg, group.rows); } agg.storeResult(totals); } totals.initialized = true; } function addGroupTotals(group: any) { var gi = groupingInfos[group.level]; var totals = new GroupTotals<TEntity>(); totals.group = group; group.totals = totals; if (!gi.lazyTotalsCalculation) { calculateTotals(totals); } } function addTotals(groups: any[], level?: number) { level = level || 0; var gi = groupingInfos[level]; var groupCollapsed = gi.collapsed; var toggledGroups = toggledGroupsByLevel[level]; var idx = groups.length, g: any; while (idx--) { g = groups[idx]; if (g.collapsed && !gi.aggregateCollapsed) { continue; } // Do a depth-first aggregation so that parent group aggregators can access subgroup totals. if (g.groups) { addTotals(g.groups, level + 1); } if (gi.aggregators.length && ( gi.aggregateEmpty || g.rows.length || (g.groups && g.groups.length))) { addGroupTotals(g); } g.collapsed = groupCollapsed ^ toggledGroups[g.groupingKey]; g.title = gi.formatter ? gi.formatter(g) : htmlEncode(g.value); } } function flattenGroupedRows(groups: any[], level?: number) { level = level || 0; var gi = groupingInfos[level]; var groupedRows: any[] = [], rows: any[], gl = 0, g: any; for (var i = 0, l = groups.length; i < l; i++) { g = groups[i]; groupedRows[gl++] = g; if (!g.collapsed) { rows = g.groups ? flattenGroupedRows(g.groups, level + 1) : g.rows; for (var j = 0, jj = rows.length; j < jj; j++) { groupedRows[gl++] = rows[j]; } } if (g.totals && gi.displayTotalsRow && (!g.collapsed || gi.aggregateCollapsed)) { groupedRows[gl++] = g.totals; } } return groupedRows; } function getFunctionInfo(fn: any) { var fnRegex = /^function[^(]*\(([^)]*)\)\s*{([\s\S]*)}$/; var matches = fn.toString().match(fnRegex); return { params: matches[1].split(","), body: matches[2] }; } function compileAccumulatorLoop(aggregator: any) { var accumulatorInfo = getFunctionInfo(aggregator.accumulate); var fn: any = new Function( "_items", "for (var " + accumulatorInfo.params[0] + ", _i=0, _il=_items.length; _i<_il; _i++) {" + accumulatorInfo.params[0] + " = _items[_i]; " + accumulatorInfo.body + "}" ); return fn; } function compileFilter() { var filterInfo = getFunctionInfo(filter); var filterBody = filterInfo.body .replace(/return false\s*([;}]|$)/gi, "{ continue _coreloop; }$1") .replace(/return true\s*([;}]|$)/gi, "{ _retval[_idx++] = $item$; continue _coreloop; }$1") .replace(/return ([^;}]+?)\s*([;}]|$)/gi, "{ if ($1) { _retval[_idx++] = $item$; }; continue _coreloop; }$2"); // This preserves the function template code after JS compression, // so that replace() commands still work as expected. var tpl = [ //"function(_items, _args) { ", "var _retval = [], _idx = 0; ", "var $item$, $args$ = _args; ", "_coreloop: ", "for (var _i = 0, _il = _items.length; _i < _il; _i++) { ", "$item$ = _items[_i]; ", "$filter$; ", "} ", "return _retval; " //"}" ].join(""); tpl = tpl.replace(/\$filter\$/gi, filterBody); tpl = tpl.replace(/\$item\$/gi, filterInfo.params[0]); tpl = tpl.replace(/\$args\$/gi, filterInfo.params[1]); var fn: any = new Function("_items,_args", tpl); fn.displayName = fn.name = "compiledFilter"; return fn; } function compileFilterWithCaching() { var filterInfo = getFunctionInfo(filter); var filterBody = filterInfo.body .replace(/return false\s*([;}]|$)/gi, "{ continue _coreloop; }$1") .replace(/return true\s*([;}]|$)/gi, "{ _cache[_i] = true;_retval[_idx++] = $item$; continue _coreloop; }$1") .replace(/return ([^;}]+?)\s*([;}]|$)/gi, "{ if ((_cache[_i] = $1)) { _retval[_idx++] = $item$; }; continue _coreloop; }$2"); // This preserves the function template code after JS compression, // so that replace() commands still work as expected. var tpl = [ //"function(_items, _args, _cache) { ", "var _retval = [], _idx = 0; ", "var $item$, $args$ = _args; ", "_coreloop: ", "for (var _i = 0, _il = _items.length; _i < _il; _i++) { ", "$item$ = _items[_i]; ", "if (_cache[_i]) { ", "_retval[_idx++] = $item$; ", "continue _coreloop; ", "} ", "$filter$; ", "} ", "return _retval; " //"}" ].join(""); tpl = tpl.replace(/\$filter\$/gi, filterBody); tpl = tpl.replace(/\$item\$/gi, filterInfo.params[0]); tpl = tpl.replace(/\$args\$/gi, filterInfo.params[1]); var fn: any = new Function("_items,_args,_cache", tpl); var fnName = "compiledFilterWithCaching"; fn.displayName = fnName; fn.name = setFunctionName(fn, fnName); return fn; } /** * In ES5 we could set the function name on the fly but in ES6 this is forbidden and we need to set it through differently * We can use Object.defineProperty and set it the property to writable, see MDN for reference * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty * @param {string} fn * @param {string} fnName */ function setFunctionName(fn: Function, fnName: string) { try { Object.defineProperty(fn, 'name', { writable: true, value: fnName }); } catch (err) { (fn as any).name = fnName; } } function uncompiledFilter(items: any[], args: any) { var retval: any[] = [], idx = 0; for (var i = 0, ii = items.length; i < ii; i++) { if (filter(items[i], args)) { retval[idx++] = items[i]; } } return retval; } function uncompiledFilterWithCaching(items: any[], args: any, cache: any) { var retval: any[] = [], idx = 0, item: any; for (var i = 0, ii = items.length; i < ii; i++) { item = items[i]; if (cache[i]) { retval[idx++] = item; } else if (filter(item, args)) { retval[idx++] = item; cache[i] = true; } } return retval; } function getFilteredAndPagedItems(items: any[]) { if (filter) { var batchFilter = options.inlineFilters ? compiledFilter : uncompiledFilter; var batchFilterWithCaching = options.inlineFilters ? compiledFilterWithCaching : uncompiledFilterWithCaching; if (refreshHints.isFilterNarrowing) { filteredItems = batchFilter(filteredItems, filterArgs); } else if (refreshHints.isFilterExpanding) { filteredItems = batchFilterWithCaching(items, filterArgs, filterCache); } else if (!refreshHints.isFilterUnchanged) { filteredItems = batchFilter(items, filterArgs); } } else { // special case: if not filtering and not paging, the resulting // rows collection needs to be a copy so that changes due to sort // can be caught filteredItems = items.concat(); } // get the current page return { totalRows: filteredItems.length, rows: filteredItems }; } function getRowDiffs(rows: any[], newRows: any[]) { var item: any, r: any, eitherIsNonData: boolean, diff: any[] = []; var from = 0, to = newRows.length; if (refreshHints && refreshHints.ignoreDiffsBefore) { from = Math.max(0, Math.min(newRows.length, refreshHints.ignoreDiffsBefore)); } if (refreshHints && refreshHints.ignoreDiffsAfter) { to = Math.min(newRows.length, Math.max(0, refreshHints.ignoreDiffsAfter)); } for (var i = from, rl = rows.length; i < to; i++) { if (i >= rl) { diff[diff.length] = i; } else { item = newRows[i]; r = rows[i]; if ((groupingInfos.length && (eitherIsNonData = (item.__nonDataRow) || (r.__nonDataRow)) && item.__group !== r.__group || item.__group && !item.equals(r)) || (eitherIsNonData && // no good way to compare totals since they are arbitrary DTOs // deep object comparison is pretty expensive // always considering them 'dirty' seems easier for the time being (item.__groupTotals || r.__groupTotals)) || item[idProperty] != r[idProperty] || (updated && updated[item[idProperty]]) ) { diff[diff.length] = i; } } } return diff; } function recalc(_items: any[]) { rowsById = null; if (refreshHints.isFilterNarrowing != prevRefreshHints.isFilterNarrowing || refreshHints.isFilterExpanding != prevRefreshHints.isFilterExpanding) { filterCache = []; } var filteredItems = getFilteredAndPagedItems(_items); totalRows = filteredItems.totalRows; var newRows = filteredItems.rows; summaryOptions.totals = {}; groups = []; if (groupingInfos.length) { groups = extractGroups(newRows); if (groups.length) { newRows = flattenGroupedRows(groups); } } var diff = getRowDiffs(rows, newRows); rows = newRows; return diff; } function refresh() { if (suspend) { return; } var countBefore = rows.length; var totalRowsBefore = totalRows; var diff = recalc(items); // pass as direct refs to avoid closure perf hit updated = null; prevRefreshHints = refreshHints; refreshHints = {}; if (totalRowsBefore !== totalRows) { onPagingInfoChanged.notify(getPagingInfo(), null, self); } if (countBefore !== rows.length) { onRowCountChanged.notify({ previous: countBefore, current: rows.length, dataView: self }, null, self); } if (diff.length > 0) { onRowsChanged.notify({ rows: diff, dataView: self }, null, self); } if (countBefore !== rows.length || diff.length > 0) { onRowsOrCountChanged.notify({ rowsDiff: diff, previousRowCount: countBefore, currentRowCount: rows.length, rowCountChanged: countBefore !== rows.length, rowsChanged: diff.length > 0, dataView: self }, null, self); } } /*** * Wires the grid and the DataView together to keep row selection tied to item ids. * This is useful since, without it, the grid only knows about rows, so if the items * move around, the same rows stay selected instead of the selection moving along * with the items. * * NOTE: This doesn't work with cell selection model. * * @param grid {Slick.Grid} The grid to sync selection with. * @param preserveHidden {Boolean} Whether to keep selected items that go out of the * view due to them getting filtered out. * @param preserveHiddenOnSelectionChange {Boolean} Whether to keep selected items * that are currently out of the view (see preserveHidden) as selected when selection * changes. * @return {EventEmitter} An event that notifies when an internal list of selected row ids * changes. This is useful since, in combination with the above two options, it allows * access to the full list selected row ids, and not just the ones visible to the grid. * @method syncGridSelection */ function syncGridSelection(grid: any, preserveHidden: boolean, preserveHiddenOnSelectionChange: boolean) { var self = this; var inHandler: any; var selectedRowIds = self.mapRowsToIds(grid.getSelectedRows()); var onSelectedRowIdsChanged = new EventEmitter(); function setSelectedRowIds(rowIds: any[]) { if (selectedRowIds.join(",") == rowIds.join(",")) { return; } selectedRowIds = rowIds; onSelectedRowIdsChanged.notify({ "grid": grid, "ids": selectedRowIds, "dataView": self }, new EventData(), self); } function update() { if (selectedRowIds.length > 0) { inHandler = true; var selectedRows = self.mapIdsToRows(selectedRowIds); if (!preserveHidden) { setSelectedRowIds(self.mapRowsToIds(selectedRows)); } grid.setSelectedRows(selectedRows); inHandler = false; } } grid.onSelectedRowsChanged.subscribe(function (e: any, args: any) { if (inHandler) { return; } var newSelectedRowIds = self.mapRowsToIds(grid.getSelectedRows()); if (!preserveHiddenOnSelectionChange || !grid.getOptions().multiSelect) { setSelectedRowIds(newSelectedRowIds); } else { // keep the ones that are hidden var existing = selectedRowIds.filter((id: any) => self.getRowById(id) === undefined); // add the newly selected ones setSelectedRowIds(existing.concat(newSelectedRowIds)); } }); this.onRowsChanged.subscribe(update); this.onRowCountChanged.subscribe(update); return onSelectedRowIdsChanged; } function syncGridCellCssStyles(grid: Grid, key: string) { var hashById: any; var inHandler: any; // since this method can be called after the cell styles have been set, // get the existing ones right away storeCellCssStyles(grid.getCellCssStyles(key)); function storeCellCssStyles(hash: any) { hashById = {}; for (var row in hash) { var id: any = (rows as any)[row][idProperty]; hashById[id] = hash[row]; } } function update() { if (hashById) { inHandler = true; ensureRowsByIdCache(); var newHash: Record<number, any> = {}; for (var id in hashById) { var row = rowsById[id]; if (row != undefined) { newHash[row] = hashById[id]; } } grid.setCellCssStyles(key, newHash); inHandler = false; } } var subFunc = function (e: any, args: any) { if (inHandler) { return; } if (key != args.key) { return; } if (args.hash) { storeCellCssStyles(args.hash); } else { grid.onCellCssStylesChanged.unsubscribe(subFunc); onRowsOrCountChanged.unsubscribe(update); } }; grid.onCellCssStylesChanged.subscribe(subFunc); onRowsOrCountChanged.subscribe(update); } function addData(data: any) { if (intf.onProcessData && data) data = intf.onProcessData(data, intf) || data; errorMessage = null; loading && typeof loading !== "boolean" && loading.abort(); loading = false; if (!data) { errorMessage = intf.errormsg; onPagingInfoChanged.notify(getPagingInfo()); return false; } data.TotalCount = data.TotalCount || 0; data.Entities = data.Entities || []; if (!data.Skip || (!intf.rowsPerPage && !data.Take)) data.Page = 1; else data.Page = Math.ceil(data.Skip / (data.Take || intf.rowsPerPage)) + 1; page = data.Page; totalCount = data.TotalCount; setItems(data.Entities); onPagingInfoChanged.notify(getPagingInfo()); } function populate() { if (populateLocks > 0) { populateCalls++; return; } populateCalls = 0; loading && typeof loading !== "boolean" && loading.abort(); if (intf.onSubmit) { var gh = intf.onSubmit(intf); if (gh === false) return false; } onDataLoading.notify(this); if (!intf.url) return false; // set loading event if (!intf.seekToPage) intf.seekToPage = 1; var request: ListRequest = {}; var skip = (intf.seekToPage - 1) * intf.rowsPerPage; if (skip) request.Skip = skip; if (intf.rowsPerPage) request.Take = intf.rowsPerPage; if (intf.sortBy && intf.sortBy.length) { if (typeof intf.sortBy !== "string") request.Sort = intf.sortBy; else { request.Sort = [intf.sortBy]; } } if (intf.params) { request = extend(request, intf.params); } const controller = new AbortController(); var serviceOptions: ServiceOptions<ListResponse<TEntity>> = { allowRedirect: false, cache: "no-store", method: intf.method, headers: { "Content-Type": contentType, "Accept": (dataType