UNPKG

@revolist/revogrid

Version:

Virtual reactive data grid spreadsheet component - RevoGrid.

1,430 lines (1,400 loc) 102 kB
/*! * Built by Revolist OU ❤️ */ 'use strict'; var column_service = require('./column.service-DvQDqxxx.js'); var dimension_helpers = require('./dimension.helpers-CaIsYC99.js'); var viewport_store = require('./viewport.store-Dcjud-a-.js'); var index = require('./index-Dq8Xzj5l.js'); var filter_button = require('./filter.button-w6LWnyhi.js'); var debounce = require('./debounce-CcpHiH2p.js'); var headerCellRenderer = require('./header-cell-renderer-B1dJwgTO.js'); /** * Plugin which recalculates realSize on changes of sizes, originItemSize and count */ const recalculateRealSizePlugin = (storeService) => { /** * Recalculates realSize if size, origin size or count changes */ return { /** * Reacts on changes of count, sizes and originItemSize */ set(k) { var _a; switch (k) { case 'count': case 'sizes': case 'originItemSize': { // recalculate realSize let realSize = 0; const count = storeService.store.get('count'); const sizes = storeService.store.get('sizes'); const originItemSize = storeService.store.get('originItemSize'); for (let i = 0; i < count; i++) { realSize += (_a = sizes[i]) !== null && _a !== void 0 ? _a : originItemSize; } storeService.setStore({ realSize }); break; } } }, }; }; /** * Plugin for trimming * * 1.a. Retrieves the previous sizes value. Saves the resulting trimmed data as a new sizes value. * 1.b. Stores a reference to the trimmed data to prevent further changes. * 2. Removes multiple and shifts the data based on the trimmed value. */ const trimmedPlugin = (storeService) => { let trimmingObject = null; let trimmedPreviousSizes = null; return { set(key, val) { switch (key) { case 'sizes': { // prevent changes after trimming if (trimmingObject && trimmingObject === val) { trimmingObject = null; return; } trimmedPreviousSizes = null; break; } case 'trimmed': { const trim = val; if (!trimmedPreviousSizes) { trimmedPreviousSizes = storeService.store.get('sizes'); } trimmingObject = removeMultipleAndShift(trimmedPreviousSizes, trim || {}); // save a reference to the trimmed object to prevent changes after trimming storeService.setSizes(trimmingObject); break; } } }, }; }; function removeMultipleAndShift(items, toRemove) { const newItems = {}; const sortedIndexes = Object.keys(items || {}) .map(Number) .sort((a, b) => a - b); const lastIndex = sortedIndexes[sortedIndexes.length - 1]; let shift = 0; for (let i = 0; i <= lastIndex; i++) { if (toRemove[i] !== undefined) { shift++; // skip already removed if (items[i] !== undefined) { continue; } } if (items[i] !== undefined) { newItems[i - shift] = items[i]; } } return newItems; } /** * Storing pre-calculated * Dimension information and sizes */ function initialBase() { return { indexes: [], count: 0, // hidden items trimmed: null, // virtual item index to size sizes: {}, // order in indexes[] to coordinate positionIndexToItem: {}, // initial element to coordinate ^ indexToItem: {}, positionIndexes: [], }; } function initialState() { return Object.assign(Object.assign({}, initialBase()), { // size which all items can take realSize: 0, // initial item size if it wasn't changed originItemSize: 0 }); } class DimensionStore { constructor(type) { this.type = type; this.store = dimension_helpers.createStore(initialState()); this.store.use(trimmedPlugin({ store: this.store, setSizes: this.setDimensionSize.bind(this), })); this.store.use(recalculateRealSizePlugin({ store: this.store, setStore: this.setStore.bind(this), })); } getCurrentState() { const state = initialState(); const keys = Object.keys(state); return column_service.reduce(keys, (r, k) => { const data = this.store.get(k); r[k] = data; return r; }, state); } dispose() { dimension_helpers.setStore(this.store, initialState()); } setStore(data) { dimension_helpers.setStore(this.store, data); } drop() { dimension_helpers.setStore(this.store, initialBase()); } /** * Set custom dimension sizes and overwrite old * Generates new indexes based on sizes * @param sizes - sizes to set */ setDimensionSize(sizes = {}) { const dimensionData = dimension_helpers.calculateDimensionData(this.store.get('originItemSize'), sizes); dimension_helpers.setStore(this.store, Object.assign(Object.assign({}, dimensionData), { sizes })); } updateSizesPositionByIndexes(newItemsOrder, prevItemsOrder = []) { // Move custom sizes to new order const customSizes = Object.assign({}, this.store.get('sizes')); if (!Object.keys(customSizes).length) { return; } // Step 1: Create a map of original indices, but allow duplicates by storing arrays of indices const originalIndices = {}; prevItemsOrder.forEach((physIndex, virtIndex) => { if (!originalIndices[physIndex]) { originalIndices[physIndex] = []; } originalIndices[physIndex].push(virtIndex); // Store all indices for each value }); // Step 2: Create new sizes based on new item order const newSizes = {}; newItemsOrder.forEach((physIndex, virtIndex) => { const indices = originalIndices[physIndex]; // Get all original indices for this value if (indices && indices.length > 0) { const originalIndex = indices.shift(); // Get the first available original index if (originalIndex !== undefined && originalIndex !== virtIndex && typeof customSizes[originalIndex] === 'number') { newSizes[virtIndex] = customSizes[originalIndex]; delete customSizes[originalIndex]; } } }); // Step 3: Set new sizes if there are changes if (Object.keys(newSizes).length) { this.setDimensionSize(Object.assign(Object.assign({}, customSizes), newSizes)); } } } /** * Selection store */ function defaultState() { return { range: null, tempRange: null, tempRangeType: null, focus: null, edit: null, lastCell: null, nextFocus: null, }; } class SelectionStore { constructor() { this.unsubscribe = []; this.store = dimension_helpers.createStore(defaultState()); this.store.on('set', (key, newVal) => { if (key === 'tempRange' && !newVal) { this.store.set('tempRangeType', null); } }); } onChange(propName, cb) { this.unsubscribe.push(this.store.onChange(propName, cb)); } clearFocus() { dimension_helpers.setStore(this.store, { focus: null, range: null, edit: null, tempRange: null }); } setFocus(focus, end) { if (!end) { dimension_helpers.setStore(this.store, { focus }); } else { dimension_helpers.setStore(this.store, { focus, range: column_service.getRange(focus, end), edit: null, tempRange: null, }); } } setNextFocus(focus) { dimension_helpers.setStore(this.store, { nextFocus: focus }); } setTempArea(range) { dimension_helpers.setStore(this.store, { tempRange: range === null || range === void 0 ? void 0 : range.area, tempRangeType: range === null || range === void 0 ? void 0 : range.type, edit: null }); } clearTemp() { dimension_helpers.setStore(this.store, { tempRange: null }); } /** Can be applied from selection change or from simple keyboard change clicks */ setRangeArea(range) { dimension_helpers.setStore(this.store, { range, edit: null, tempRange: null }); } setRange(start, end) { const range = column_service.getRange(start, end); this.setRangeArea(range); } setLastCell(lastCell) { dimension_helpers.setStore(this.store, { lastCell }); } setEdit(val) { const focus = this.store.get('focus'); if (focus && typeof val === 'string') { dimension_helpers.setStore(this.store, { edit: { x: focus.x, y: focus.y, val }, }); return; } dimension_helpers.setStore(this.store, { edit: null }); } dispose() { this.unsubscribe.forEach(f => f()); this.store.dispose(); } } /** * Base layer for plugins * Provide minimal starting core for plugins to work * Extend this class to create plugin */ class BasePlugin { constructor(revogrid, providers) { this.revogrid = revogrid; this.providers = providers; this.h = index.h; this.subscriptions = {}; } /** * * @param eventName - event name to subscribe to in revo-grid component (e.g. 'beforeheaderclick') * @param callback - callback function for event */ addEventListener(eventName, callback) { this.revogrid.addEventListener(eventName, callback); this.subscriptions[eventName] = callback; } /** * Subscribe to property change in revo-grid component * You can return false in callback to prevent default value set * * @param prop - property name * @param callback - callback function * @param immediate - trigger callback immediately with current value */ watch(prop, callback, { immediate } = { immediate: false }) { var _a; const nativeValueDesc = Object.getOwnPropertyDescriptor(this.revogrid, prop) || Object.getOwnPropertyDescriptor(this.revogrid.constructor.prototype, prop); // Overwrite property descriptor for this instance Object.defineProperty(this.revogrid, prop, { configurable: true, enumerable: (_a = nativeValueDesc === null || nativeValueDesc === void 0 ? void 0 : nativeValueDesc.enumerable) !== null && _a !== void 0 ? _a : true, set(val) { var _a; const keepDefault = callback(val); if (keepDefault === false) { return; } // Continue with native behavior return (_a = nativeValueDesc === null || nativeValueDesc === void 0 ? void 0 : nativeValueDesc.set) === null || _a === void 0 ? void 0 : _a.call(this, val); }, get() { var _a; // Continue with native behavior return (_a = nativeValueDesc === null || nativeValueDesc === void 0 ? void 0 : nativeValueDesc.get) === null || _a === void 0 ? void 0 : _a.call(this); }, }); if (immediate) { callback(nativeValueDesc === null || nativeValueDesc === void 0 ? void 0 : nativeValueDesc.value); } } /** * Remove event listener * @param eventName */ removeEventListener(eventName) { this.revogrid.removeEventListener(eventName, this.subscriptions[eventName]); delete this.subscriptions[eventName]; } /** * Emit event from revo-grid component * Event can be cancelled by calling event.preventDefault() in callback */ emit(eventName, detail) { const event = new CustomEvent(eventName, { detail, cancelable: true }); this.revogrid.dispatchEvent(event); return event; } /** * Clear all subscriptions */ clearSubscriptions() { for (let type in this.subscriptions) { this.removeEventListener(type); } } /** * Destroy plugin and clear all subscriptions */ destroy() { this.clearSubscriptions(); } } /** * A specialized version of `_.forEach` for arrays without support for * iteratee shorthands. * * @private * @param {Array} [array] The array to iterate over. * @param {Function} iteratee The function invoked per iteration. * @returns {Array} Returns `array`. */ function arrayEach(array, iteratee) { var index = -1, length = array == null ? 0 : array.length; while (++index < length) { if (iteratee(array[index], index, array) === false) { break; } } return array; } /** * Casts `value` to `identity` if it's not a function. * * @private * @param {*} value The value to inspect. * @returns {Function} Returns cast function. */ function castFunction(value) { return typeof value == 'function' ? value : dimension_helpers.identity; } /** * Iterates over elements of `collection` and invokes `iteratee` for each element. * The iteratee is invoked with three arguments: (value, index|key, collection). * Iteratee functions may exit iteration early by explicitly returning `false`. * * **Note:** As with other "Collections" methods, objects with a "length" * property are iterated like arrays. To avoid this behavior use `_.forIn` * or `_.forOwn` for object iteration. * * @static * @memberOf _ * @since 0.1.0 * @alias each * @category Collection * @param {Array|Object} collection The collection to iterate over. * @param {Function} [iteratee=_.identity] The function invoked per iteration. * @returns {Array|Object} Returns `collection`. * @see _.forEachRight * @example * * _.forEach([1, 2], function(value) { * console.log(value); * }); * // => Logs `1` then `2`. * * _.forEach({ 'a': 1, 'b': 2 }, function(value, key) { * console.log(key); * }); * // => Logs 'a' then 'b' (iteration order is not guaranteed). */ function forEach(collection, iteratee) { var func = dimension_helpers.isArray(collection) ? arrayEach : column_service.baseEach; return func(collection, castFunction(iteratee)); } /** * Plugin module for revo-grid grid system * Add support for automatic column resize */ const LETTER_BLOCK_SIZE = 7; exports.ColumnAutoSizeMode = void 0; (function (ColumnAutoSizeMode) { // increases column width on header click according the largest text value ColumnAutoSizeMode["headerClickAutosize"] = "headerClickAutoSize"; // increases column width on data set and text edit, decreases performance ColumnAutoSizeMode["autoSizeOnTextOverlap"] = "autoSizeOnTextOverlap"; // increases and decreases column width based on all items sizes, worst for performance ColumnAutoSizeMode["autoSizeAll"] = "autoSizeAll"; })(exports.ColumnAutoSizeMode || (exports.ColumnAutoSizeMode = {})); class AutoSizeColumnPlugin extends BasePlugin { constructor(revogrid, providers, config) { super(revogrid, providers); this.providers = providers; this.config = config; this.autoSizeColumns = null; /** for edge case when no columns defined before data */ this.dataResolve = null; this.dataReject = null; this.letterBlockSize = (config === null || config === void 0 ? void 0 : config.letterBlockSize) || LETTER_BLOCK_SIZE; // create test container to check text width if (config === null || config === void 0 ? void 0 : config.preciseSize) { this.precsizeCalculationArea = this.initiatePresizeElement(); revogrid.appendChild(this.precsizeCalculationArea); } const aftersourceset = ({ detail: { source }, }) => { this.setSource(source); }; const beforecolumnsset = ({ detail: { columns }, }) => { this.columnSet(columns); }; this.addEventListener('beforecolumnsset', beforecolumnsset); switch (config === null || config === void 0 ? void 0 : config.mode) { case exports.ColumnAutoSizeMode.autoSizeOnTextOverlap: this.addEventListener('aftersourceset', aftersourceset); this.addEventListener('afteredit', ({ detail }) => { this.afteredit(detail); }); break; case exports.ColumnAutoSizeMode.autoSizeAll: this.addEventListener('aftersourceset', aftersourceset); this.addEventListener('afteredit', ({ detail }) => { this.afterEditAll(detail); }); break; default: this.addEventListener('headerdblclick', ({ detail }) => { const type = column_service.getColumnType(detail.column); const size = this.getColumnSize(detail.index, type); if (size) { this.providers.dimension.setCustomSizes(type, { [detail.index]: size, }, true); } }); break; } } async setSource(source) { let autoSize = this.autoSizeColumns; if (this.dataReject) { this.dataReject(); this.clearPromise(); } /** If data set first and no column provided await until get one */ if (!autoSize) { const request = new Promise((resolve, reject) => { this.dataResolve = resolve; this.dataReject = reject; }); try { autoSize = await request; } catch (e) { return; } } // calculate sizes forEach(autoSize, (_v, type) => { const sizes = {}; forEach(autoSize[type], rgCol => { // calculate size rgCol.size = sizes[rgCol.index] = source.reduce((prev, rgRow) => Math.max(prev, this.getLength(rgRow[rgCol.prop])), this.getLength(rgCol.name || '')); }); this.providers.dimension.setCustomSizes(type, sizes, true); }); } getLength(len) { var _a; const padding = 15; if (!len) { return 0; } try { const str = len.toString(); /**if exact calculation required proxy with html element, slow operation */ if ((_a = this.config) === null || _a === void 0 ? void 0 : _a.preciseSize) { this.precsizeCalculationArea.innerText = str; return this.precsizeCalculationArea.scrollWidth + padding * 2; } return str.length * this.letterBlockSize + padding * 2; } catch (e) { return 0; } } afteredit(e) { let data; if (this.isRangeEdit(e)) { data = e.data; } else { data = { 0: { [e.prop]: e.val } }; } forEach(this.autoSizeColumns, (columns, type) => { const sizes = {}; forEach(columns, rgCol => { var _a; // calculate size const size = column_service.reduce(data, (prev, rgRow) => { if (typeof rgRow[rgCol.prop] === 'undefined') { return prev; } return Math.max(prev || 0, this.getLength(rgRow[rgCol.prop])); }, undefined); if (size && ((_a = rgCol.size) !== null && _a !== void 0 ? _a : 0) < size) { rgCol.size = sizes[rgCol.index] = size; } }); this.providers.dimension.setCustomSizes(type, sizes, true); }); } afterEditAll(e) { const props = {}; if (this.isRangeEdit(e)) { forEach(e.data, r => forEach(r, (_v, p) => (props[p] = true))); } else { props[e.prop] = true; } forEach(this.autoSizeColumns, (columns, type) => { const sizes = {}; forEach(columns, rgCol => { if (props[rgCol.prop]) { const size = this.getColumnSize(rgCol.index, type); if (size) { sizes[rgCol.index] = size; } } }); this.providers.dimension.setCustomSizes(type, sizes, true); }); } getColumnSize(index, type) { var _a, _b; const rgCol = (_b = (_a = this.autoSizeColumns) === null || _a === void 0 ? void 0 : _a[type]) === null || _b === void 0 ? void 0 : _b[index]; if (!rgCol) { return 0; } return column_service.reduce(this.providers.data.stores, (r, s) => { const perStore = column_service.reduce(s.store.get('items'), (prev, _row, i) => { const item = dimension_helpers.getSourceItem(s.store, i); return Math.max(prev || 0, this.getLength(item === null || item === void 0 ? void 0 : item[rgCol.prop])); }, 0); return Math.max(r, perStore); }, rgCol.size || 0); } columnSet(columns) { var _a; for (let t of column_service.columnTypes) { const type = t; const cols = columns[type]; for (let i in cols) { if (cols[i].autoSize || ((_a = this.config) === null || _a === void 0 ? void 0 : _a.allColumns)) { if (!this.autoSizeColumns) { this.autoSizeColumns = {}; } if (!this.autoSizeColumns[type]) { this.autoSizeColumns[type] = {}; } this.autoSizeColumns[type][i] = Object.assign(Object.assign({}, cols[i]), { index: parseInt(i, 10) }); } } } if (this.dataResolve) { this.dataResolve(this.autoSizeColumns || {}); this.clearPromise(); } } clearPromise() { this.dataResolve = null; this.dataReject = null; } isRangeEdit(e) { return !!e.data; } initiatePresizeElement() { var _a; const styleForFontTest = { position: 'absolute', fontSize: '14px', height: '0', width: '0', whiteSpace: 'nowrap', top: '0', overflowX: 'scroll', display: 'block', }; const el = document.createElement('div'); for (let s in styleForFontTest) { el.style[s] = (_a = styleForFontTest[s]) !== null && _a !== void 0 ? _a : ''; } el.classList.add('revo-test-container'); return el; } destroy() { var _a; super.destroy(); (_a = this.precsizeCalculationArea) === null || _a === void 0 ? void 0 : _a.remove(); } } class StretchColumn extends BasePlugin { constructor(revogrid, providers) { super(revogrid, providers); this.providers = providers; this.stretchedColumn = null; // calculate scroll bar size for current user session this.scrollSize = dimension_helpers.getScrollbarSize(document); // subscribe to column changes const beforecolumnapplied = ({ detail: { columns }, }) => this.applyStretch(columns); this.addEventListener('beforecolumnapplied', beforecolumnapplied); } setScroll({ type, hasScroll }) { var _a; if (type === 'rgRow' && this.stretchedColumn && ((_a = this.stretchedColumn) === null || _a === void 0 ? void 0 : _a.initialSize) === this.stretchedColumn.size) { if (hasScroll) { this.stretchedColumn.size -= this.scrollSize; this.apply(); this.dropChanges(); } } } activateChanges() { const setScroll = ({ detail }) => this.setScroll(detail); this.addEventListener('scrollchange', setScroll); } dropChanges() { this.stretchedColumn = null; this.removeEventListener('scrollchange'); } apply() { if (!this.stretchedColumn) { return; } const type = 'rgCol'; const sizes = this.providers.dimension.stores[type].store.get('sizes'); this.providers.dimension.setCustomSizes(type, Object.assign(Object.assign({}, sizes), { [this.stretchedColumn.index]: this.stretchedColumn.size }), true); } /** * Apply stretch changes */ applyStretch(columns) { // unsubscribe from all events this.dropChanges(); // calculate grid size let sizeDifference = this.revogrid.clientWidth - 1; forEach(columns, (_, type) => { const realSize = this.providers.dimension.stores[type].store.get('realSize'); sizeDifference -= realSize; }); if (this.revogrid.rowHeaders) { const itemsLength = this.providers.data.stores.rgRow.store.get('source').length; const header = this.revogrid.rowHeaders; const rowHeaderSize = viewport_store.calculateRowHeaderSize(itemsLength, typeof header === 'object' ? header : undefined); if (rowHeaderSize) { sizeDifference -= rowHeaderSize; } } if (sizeDifference > 0) { // currently plugin accepts last column only const index = columns.rgCol.length - 1; const last = columns.rgCol[index]; /** * has column * no auto size applied * size for column shouldn't be defined */ const colSize = (last === null || last === void 0 ? void 0 : last.size) || this.revogrid.colSize || 0; const size = sizeDifference + colSize - 1; if (last && !last.autoSize && colSize < size) { this.stretchedColumn = { initialSize: size, index, size, }; this.apply(); this.activateChanges(); } } } } /** * Check plugin type is Stretch */ function isStretchPlugin(plugin) { return !!plugin.applyStretch; } /** * The base implementation of `_.clamp` which doesn't coerce arguments. * * @private * @param {number} number The number to clamp. * @param {number} [lower] The lower bound. * @param {number} upper The upper bound. * @returns {number} Returns the clamped number. */ function baseClamp(number, lower, upper) { if (number === number) { { number = number <= upper ? number : upper; } { number = number >= lower ? number : lower; } } return number; } /** Used as references for the maximum length and index of an array. */ var MAX_ARRAY_LENGTH = 4294967295; /** * Converts `value` to an integer suitable for use as the length of an * array-like object. * * **Note:** This method is based on * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength). * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to convert. * @returns {number} Returns the converted integer. * @example * * _.toLength(3.2); * // => 3 * * _.toLength(Number.MIN_VALUE); * // => 0 * * _.toLength(Infinity); * // => 4294967295 * * _.toLength('3.2'); * // => 3 */ function toLength(value) { return value ? baseClamp(column_service.toInteger(value), 0, MAX_ARRAY_LENGTH) : 0; } /** * The base implementation of `_.fill` without an iteratee call guard. * * @private * @param {Array} array The array to fill. * @param {*} value The value to fill `array` with. * @param {number} [start=0] The start position. * @param {number} [end=array.length] The end position. * @returns {Array} Returns `array`. */ function baseFill(array, value, start, end) { var length = array.length; start = column_service.toInteger(start); if (start < 0) { start = -start > length ? 0 : (length + start); } end = (end === undefined || end > length) ? length : column_service.toInteger(end); if (end < 0) { end += length; } end = start > end ? 0 : toLength(end); while (start < end) { array[start++] = value; } return array; } /** * Fills elements of `array` with `value` from `start` up to, but not * including, `end`. * * **Note:** This method mutates `array`. * * @static * @memberOf _ * @since 3.2.0 * @category Array * @param {Array} array The array to fill. * @param {*} value The value to fill `array` with. * @param {number} [start=0] The start position. * @param {number} [end=array.length] The end position. * @returns {Array} Returns `array`. * @example * * var array = [1, 2, 3]; * * _.fill(array, 'a'); * console.log(array); * // => ['a', 'a', 'a'] * * _.fill(Array(3), 2); * // => [2, 2, 2] * * _.fill([4, 6, 8, 10], '*', 1, 3); * // => [4, '*', '*', 10] */ function fill(array, value, start, end) { var length = array == null ? 0 : array.length; if (!length) { return []; } return baseFill(array, value, start, end); } const INITIAL = { mime: 'text/csv', fileKind: 'csv', // BOM signature bom: true, columnDelimiter: ',', rowDelimiter: '\r\n', encoding: '', }; // The ASCII character code 13 is called a Carriage Return or CR. const CARRIAGE_RETURN = String.fromCharCode(13); // Chr(13) followed by a Chr(10) that compose a proper CRLF. const LINE_FEED = String.fromCharCode(10); const DOUBLE_QT = String.fromCharCode(34); const NO_BREAK_SPACE = String.fromCharCode(0xfeff); const escapeRegex = new RegExp('"', 'g'); class ExportCsv { constructor(options = {}) { this.options = Object.assign(Object.assign({}, INITIAL), options); } doExport({ data, headers, props }) { let result = this.options.bom ? NO_BREAK_SPACE : ''; // any header if ((headers === null || headers === void 0 ? void 0 : headers.length) > 0) { headers.forEach(header => { // ignore empty if (!header.length) { return; } result += this.prepareHeader(header, this.options.columnDelimiter); result += this.options.rowDelimiter; }); } data.forEach((rgRow, index) => { if (index > 0) { result += this.options.rowDelimiter; } // support grouping if (column_service.isGrouping(rgRow)) { result += this.parseCell(column_service.getGroupingName(rgRow), this.options.columnDelimiter); return; } result += props.map(p => this.parseCell(rgRow[p], this.options.columnDelimiter)).join(this.options.columnDelimiter); }); return result; } prepareHeader(columnHeaders, columnDelimiter) { let result = ''; const newColumnHeaders = columnHeaders.map(v => this.parseCell(v, columnDelimiter, true)); result += newColumnHeaders.join(columnDelimiter); return result; } parseCell(value, columnDelimiter, force = false) { let escape = value; if (typeof value !== 'string') { escape = JSON.stringify(value); } const toEscape = [CARRIAGE_RETURN, DOUBLE_QT, LINE_FEED, columnDelimiter]; if (typeof escape === 'undefined') { return ''; } if (escape !== '' && (force || toEscape.some(i => escape.indexOf(i) >= 0))) { return `"${escape.replace(escapeRegex, '""')}"`; } return escape; } } var ExportTypes; (function (ExportTypes) { ExportTypes["csv"] = "csv"; })(ExportTypes || (ExportTypes = {})); class ExportFilePlugin extends BasePlugin { /** Exports string */ async exportString(options = {}, t = ExportTypes.csv) { const data = await this.beforeexport(); if (!data) { return null; } return this.formatter(t, options).doExport(data); } /** Exports Blob */ async exportBlob(options = {}, t = ExportTypes.csv) { return await this.getBlob(this.formatter(t, options)); } /** Export file */ async exportFile(options = {}, t = ExportTypes.csv) { const formatter = this.formatter(t, options); // url const URL = window.URL || window.webkitURL; const a = document.createElement('a'); const { filename, fileKind } = formatter.options; const name = `${filename}.${fileKind}`; const blob = await this.getBlob(formatter); const url = blob ? URL.createObjectURL(blob) : ''; a.style.display = 'none'; a.setAttribute('href', url); a.setAttribute('download', name); this.revogrid.appendChild(a); a.dispatchEvent(new MouseEvent('click')); this.revogrid.removeChild(a); // delay for revoke, correct for some browsers await dimension_helpers.timeout(120); URL.revokeObjectURL(url); } /** Blob object */ async getBlob(formatter) { const type = `${formatter.options.mime};charset=${formatter.options.encoding}`; if (typeof Blob !== 'undefined') { const data = await this.beforeexport(); if (!data) { return null; } return new Blob([formatter.doExport(data)], { type }); } return null; } // before event async beforeexport() { let data = await this.getData(); const event = this.emit('beforeexport', { data }); if (event.defaultPrevented) { return null; } return event.detail.data; } async getData() { const data = await this.getSource(); const colSource = []; const colPromises = []; column_service.columnTypes.forEach((t, i) => { colPromises.push(this.getColPerSource(t).then(s => (colSource[i] = s))); }); await Promise.all(colPromises); const columns = { headers: [], props: [], }; for (let source of colSource) { source.headers.forEach((h, i) => { if (!columns.headers[i]) { columns.headers[i] = []; } columns.headers[i].push(...h); }); columns.props.push(...source.props); } return Object.assign({ data }, columns); } async getColPerSource(t) { const store = await this.revogrid.getColumnStore(t); const source = store.get('source'); const virtualIndexes = store.get('items'); const depth = store.get('groupingDepth'); const groups = store.get('groups'); const colNames = []; const colProps = []; virtualIndexes.forEach((v) => { const prop = source[v].prop; colNames.push(source[v].name || ''); colProps.push(prop); }); const rows = this.getGroupHeaders(depth, groups, virtualIndexes); rows.push(colNames); return { headers: rows, props: colProps, }; } getGroupHeaders(depth, groups, items) { const rows = []; const template = fill(new Array(items.length), ''); for (let d = 0; d < depth; d++) { const rgRow = [...template]; rows.push(rgRow); if (!groups[d]) { continue; } const levelGroups = groups[d]; // add names of groups levelGroups.forEach((group) => { const minIndex = group.indexes[0]; if (typeof minIndex === 'number') { rgRow[minIndex] = group.name; } }); } return rows; } async getSource() { const data = []; const promisesData = []; column_service.rowTypes.forEach(t => { const dataPart = []; data.push(dataPart); const promise = this.revogrid.getVisibleSource(t).then((d) => dataPart.push(...d)); promisesData.push(promise); }); await Promise.all(promisesData); return data.reduce((r, v) => { r.push(...v); return r; }, []); } // get correct class for future multiple types support formatter(type, options = {}) { switch (type) { case ExportTypes.csv: return new ExportCsv(options); default: throw new Error('Unknown format'); } } } const eq = (value, extra) => { if (typeof value === 'undefined' || (value === null && !extra)) { return true; } if (typeof value !== 'string') { value = JSON.stringify(value); } const filterVal = extra === null || extra === void 0 ? void 0 : extra.toString().toLocaleLowerCase(); if ((filterVal === null || filterVal === void 0 ? void 0 : filterVal.length) === 0) { return true; } return value.toLocaleLowerCase() === filterVal; }; const notEq = (value, extra) => !eq(value, extra); notEq.extra = 'input'; eq.extra = 'input'; const gtThan = function (value, extra) { let conditionValue; if (typeof value === 'number' && typeof extra !== 'undefined' && extra !== null) { conditionValue = parseFloat(extra === null || extra === void 0 ? void 0 : extra.toString()); return value > conditionValue; } return false; }; gtThan.extra = 'input'; const gtThanEq = function (value, extra) { return eq(value, extra) || gtThan(value, extra); }; gtThanEq.extra = 'input'; const lt = function (value, extra) { let conditionValue; if (typeof value === 'number' && typeof extra !== 'undefined' && extra !== null) { conditionValue = parseFloat(extra.toString()); return value < conditionValue; } else { return false; } }; lt.extra = 'input'; const lsEq = function (value, extra) { return eq(value, extra) || lt(value, extra); }; lsEq.extra = 'input'; const set = (value) => !(value === '' || value === null || value === void 0); const notSet = (value) => !set(value); const beginsWith = (value, extra) => { if (!value) { return false; } if (!extra) { return true; } if (typeof value !== 'string') { value = JSON.stringify(value); } if (typeof extra !== 'string') { extra = JSON.stringify(extra); } return value.toLocaleLowerCase().indexOf(extra.toLocaleLowerCase()) === 0; }; beginsWith.extra = 'input'; const contains = (value, extra) => { if (!extra) { return true; } if (!value) { return false; } if (extra) { if (typeof value !== 'string') { value = JSON.stringify(value); } return value.toLocaleLowerCase().indexOf(extra.toString().toLowerCase()) > -1; } return true; }; const notContains = (value, extra) => { return !contains(value, extra); }; notContains.extra = 'input'; contains.extra = 'input'; // filter.indexed.ts const filterCoreFunctionsIndexedByType = { none: () => true, empty: notSet, notEmpty: set, eq: eq, notEq: notEq, begins: beginsWith, contains: contains, notContains: notContains, eqN: eq, neqN: notEq, gt: gtThan, gte: gtThanEq, lt: lt, lte: lsEq, }; const filterTypes = { string: ['notEmpty', 'empty', 'eq', 'notEq', 'begins', 'contains', 'notContains'], number: ['notEmpty', 'empty', 'eqN', 'neqN', 'gt', 'gte', 'lt', 'lte'], }; const filterNames = { none: 'None', empty: 'Not set', notEmpty: 'Set', eq: 'Equal', notEq: 'Not equal', begins: 'Begins with', contains: 'Contains', notContains: 'Does not contain', eqN: '=', neqN: '!=', gt: '>', gte: '>=', lt: '<', lte: '<=', }; // filter.plugin.tsx const FILTER_TRIMMED_TYPE = 'filter'; const FILTER_CONFIG_CHANGED_EVENT = 'filterconfigchanged'; const FILTE_PANEL = 'revogr-filter-panel'; /** * @typedef ColumnFilterConfig * @type {object} * * @property {MultiFilterItem|undefined} multiFilterItems - data for multi filtering with relation * * @property {Record<ColumnProp, FilterCollectionItem>|undefined} collection - preserved filter data, relation for filters will be applied as 'and' * * @property {string[]|undefined} include - filters to be included, if defined everything else out of scope will be ignored * * @property {Record<string, CustomFilter>|undefined} customFilters - hash map of {FilterType:CustomFilter}. * * @property {FilterLocalization|undefined} localization - translation for filter popup captions. * * @property {boolean|undefined} disableDynamicFiltering - disables dynamic filtering. A way to apply filters on Save only. */ /** * @internal */ class FilterPlugin extends BasePlugin { constructor(revogrid, providers, config) { var _a; super(revogrid, providers); this.revogrid = revogrid; this.config = config; this.filterCollection = {}; this.multiFilterItems = {}; /** * Filter types * @example * { * string: ['contains', 'beginswith'], * number: ['eqN', 'neqN', 'gt'] * } */ this.filterByType = Object.assign({}, filterTypes); this.filterNameIndexByType = Object.assign({}, filterNames); this.filterFunctionsIndexedByType = Object.assign({}, filterCoreFunctionsIndexedByType); this.filterProp = filter_button.FILTER_PROP; if (config) { this.initConfig(config); } const existingNodes = this.revogrid.registerVNode.filter(n => typeof n === 'object' && n.$tag$ !== FILTE_PANEL); this.revogrid.registerVNode = [ ...existingNodes, index.h("revogr-filter-panel", { filterNames: this.filterNameIndexByType, filterEntities: this.filterFunctionsIndexedByType, filterCaptions: (_a = config === null || config === void 0 ? void 0 : config.localization) === null || _a === void 0 ? void 0 : _a.captions, onFilterChange: e => this.onFilterChange(e.detail), onResetChange: e => this.onFilterReset(e.detail), disableDynamicFiltering: config === null || config === void 0 ? void 0 : config.disableDynamicFiltering, closeOnOutsideClick: config === null || config === void 0 ? void 0 : config.closeFilterPanelOnOutsideClick, ref: e => (this.pop = e) }, ' ', this.extraContent()), ]; const aftersourceset = async () => { const filterCollectionProps = Object.keys(this.filterCollection); if (filterCollectionProps.length > 0) { // handle old way of filtering by reworking FilterCollection to new MultiFilterItem filterCollectionProps.forEach((prop, index) => { if (!this.multiFilterItems[prop]) { this.multiFilterItems[prop] = [ { id: index, type: this.filterCollection[prop].type, value: this.filterCollection[prop].value, relation: 'and', }, ]; } }); } if (Object.keys(this.multiFilterItems).length === 0) { return; } await dimension_helpers.timeout(); await this.runFiltering(this.multiFilterItems); }; this.addEventListener('headerclick', e => this.headerclick(e)); this.addEventListener(FILTER_CONFIG_CHANGED_EVENT, ({ detail }) => { if (!detail || (typeof detail === 'object' && (!detail.multiFilterItems || !Object.keys(detail.multiFilterItems).length))) { this.clearFiltering(); return; } if (typeof detail === 'object') { this.initConfig(detail); } aftersourceset(); }); this.addEventListener('aftersourceset', aftersourceset); this.addEventListener('filter', ({ detail }) => this.onFilterChange(detail)); } beforeshow(_) { // used as hook for filter panel } extraContent() { return null; } initConfig(config) { if (config.multiFilterItems) { this.multiFilterItems = Object.assign({}, config.multiFilterItems); } else { this.multiFilterItems = {}; } // Add custom filters if (config.customFilters) { for (let customFilterType in config.customFilters) { const cFilter = config.customFilters[customFilterType]; if (!this.filterByType[cFilter.columnFilterType]) { this.filterByType[cFilter.columnFilterType] = []; } // add custom filter type this.filterByType[cFilter.columnFilterType].push(customFilterType); // add custom filter function this.filterFunctionsIndexedByType[customFilterType] = cFilter.func; // add custom filter name this.filterNameIndexByType[customFilterType] = cFilter.name; } } // Add filterProp if provided in config if (config.filterProp) { this.filterProp = config.filterProp; } /** * which filters has to be included/excluded * convenient way to exclude system filters */ const cfgInlcude = config.include; if (cfgInlcude) { const filters = {}; for (let t in this.filterByType) { // validate filters, if appropriate function present const newTypes = this.filterByType[t].filter(f => cfgInlcude.indexOf(f) > -1); if (newTypes.length) { filters[t] = newTypes; } } // if any valid filters provided show them if (Object.keys(filters).length > 0) { this.filterByType = filters; } } if (config.collection) { const filtersWithFilterFunctionPresent = Object.entries(config.collection).filter(([, item]) => this.filterFunctionsIndexedByType[item.type]); this.filterCollection = Object.fromEntries(filtersWithFilterFunctionPresent); } else { this.filterCollection = {}; } if (config.localization) { if (config.localization.filterNames) { Object.entries(config.localization.filterNames).forEach(([k, v]) => { if (this.filterNameIndexByType[k] != void 0) { this.filterNameIndexByType[k] = v; } }); } } } async headerclick(e) { var _a, _b; const el = (_a = e.detail.originalEvent) === null || _a === void 0 ? void 0 : _a.target; if (!filter_button.isFilterBtn(el)) { return; } e.preventDefault(); if (!this.pop) { return; } const prop = e.detail.prop; const currentPanel = await this.pop.getChanges(); if ((currentPanel === null || currentPanel === void 0 ? void 0 : currentPanel.prop) === prop) { await this.pop.show(); return; } // filter button clicked, open filter dialog const gridPos = this.revogrid.getBoundingClientRect(); const buttonPos = el.getBoundingClientRect(); const data = Object.assign(Object.assign(Object.assign({}, e.detail), this.filterCollection[prop]), { x: buttonPos.x - gridPos.x, y: buttonPos.y - gridPos.y + buttonPos.height, autoCorrect: true, filterTypes: this.getColumnFilter(e.detail.filter), filterItems: this.multiFilterItems, extraContent: this.extraHyperContent }); (_b = this.beforeshow) === null || _b === void 0 ? void 0 : _b.call(this, data); this.pop.show(data); } getColumnFilter(type) { let filterType = 'string'; if (!type) { return { [filterType]: this.filterByType[filterType] }; } // if custom column filter if (this.isValidType(type)) { filterType = type; // if multiple filters applied } else if (typeof type === 'object' && type.length) { return type.reduce((r, multiType) => { if (this.isValidType(multiType)) { r[multiType] = this.filterByType[multiType]; } return r; }, {}); } return { [filterType]: this.filterByType[filterType] }; } isValidType(type) { return !!(typeof type === 'string' && this.filterByType[type]); } /** * Called on internal component change */ async onFilterChange(filterItems) { // store the filter items this.multiFilterItems