UNPKG

@revolist/revogrid

Version:

Virtual reactive data grid spreadsheet component - RevoGrid.

1,418 lines (1,386 loc) 195 kB
/*! * Built by Revolist OU ❤️ */ import { h, proxyCustomElement, HTMLElement as HTMLElement$1, createEvent, Host } from '@stencil/core/internal/client'; import { K as reduce, L as baseEach, c as columnTypes, D as getColumnType, v as isGrouping, u as getGroupingName, r as rowTypes, C as getCellDataParsed, B as getCellRaw, J as getColumnByProp, j as GROUP_EXPANDED, y as getParsedGroup, e as PSEUDO_GROUP_ITEM_ID, z as isSameGroup, G as GROUP_DEPTH, f as PSEUDO_GROUP_ITEM_VALUE, p as GROUPING_ROW_TYPE, q as getSource, h as PSEUDO_GROUP_COLUMN, t as gatherGrouping, o as GROUP_EXPAND_EVENT, w as isGroupingColumn, s as getExpanded, F as isColGrouping, i as isRowType, E as getColumnSizes, M as EMPTY_INDEX, H as getColumns, N as SelectionStoreConnector } from './column.service.js'; import { n as createStore, i as setStore, r as identity, m as isArray, b as getSourceItem, q as toInteger, o as isIterateeCall, u as baseProperty, j as isArrayLike, v as getTag, w as baseKeys, g as getPhysical, e as setItems, D as DataStore, f as getSourceItemVirtualIndexByProp, d as setSourceByPhysicalIndex, s as setSourceByVirtualIndex, a as getVisibleSourceItem, h as gatherTrimmedItems } from './data.store.js'; import { d as debounce } from './debounce.js'; import { R as RESIZE_INTERVAL } from './consts.js'; import { c as calculateDimensionData, g as getItemByPosition, a as getItemByIndex } from './dimension.helpers.js'; import './platform.js'; import { f as calculateRowHeaderSize, V as ViewportStore, h as defineCustomElement$6 } from './revogr-row-headers2.js'; import { g as getScrollbarSize, t as timeout } from './index2.js'; import { b as FILTER_PROP, i as isFilterBtn } from './filter.button.js'; import { i as isObjectLike, b as baseGetTag } from './toNumber.js'; import { v as viewportDataPartition, H as HEADER_SLOT, C as CONTENT_SLOT, F as FOOTER_SLOT, D as DATA_SLOT, d as defineCustomElement$3 } from './revogr-viewport-scroll2.js'; import { d as dispatch, O as ON_COLUMN_CLICK, b as defineCustomElement$9 } from './revogr-header2.js'; import { g as getPropertyFromEvent } from './selection.utils.js'; import { d as defineCustomElement$f } from './revogr-attribution2.js'; import { d as defineCustomElement$e } from './revogr-clipboard2.js'; import { d as defineCustomElement$d } from './revogr-data2.js'; import { l as defineCustomElement$c } from './revogr-edit2.js'; import { d as defineCustomElement$b } from './revogr-extra2.js'; import { d as defineCustomElement$a } from './revogr-focus2.js'; import { d as defineCustomElement$8 } from './revogr-order-editor2.js'; import { d as defineCustomElement$7 } from './revogr-overlay-selection2.js'; import { d as defineCustomElement$5 } from './revogr-scroll-virtual2.js'; import { d as defineCustomElement$4 } from './revogr-temp-range2.js'; import { d as defineCustomElement$2 } from './vnode-converter.js'; class ThemeCompact { constructor() { this.defaultRowSize = 32; } } class ThemeDefault { constructor() { this.defaultRowSize = 27; } } class ThemeMaterial { constructor() { this.defaultRowSize = 42; } } const DEFAULT_THEME = 'default'; const allowedThemes = [ DEFAULT_THEME, 'material', 'compact', 'darkMaterial', 'darkCompact', ]; class ThemeService { get theme() { return this.currentTheme; } get rowSize() { return this.customRowSize || this.currentTheme.defaultRowSize; } set rowSize(size) { this.customRowSize = size; } constructor(cfg) { this.customRowSize = 0; this.customRowSize = cfg.rowSize; this.register('default'); } register(theme) { const parsedTheme = getTheme(theme); switch (parsedTheme) { case 'material': case 'darkMaterial': this.currentTheme = new ThemeMaterial(); break; case 'compact': case 'darkCompact': this.currentTheme = new ThemeCompact(); break; default: this.currentTheme = new ThemeDefault(); break; } } } function getTheme(theme) { if (theme && allowedThemes.indexOf(theme) > -1) { return theme; } return DEFAULT_THEME; } /** * 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) { switch (k) { case 'count': case 'sizes': case 'originItemSize': { // recalculate realSize let realSize = 0; const count = storeService.store.get('count'); for (let i = 0; i < count; i++) { realSize += storeService.store.get('sizes')[i] || storeService.store.get('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 = 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 reduce(keys, (r, k) => { const data = this.store.get(k); r[k] = data; return r; }, state); } dispose() { setStore(this.store, initialState()); } setStore(data) { setStore(this.store, data); } drop() { 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 = calculateDimensionData(this.store.get('originItemSize'), sizes); 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 && customSizes[originalIndex]) { 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)); } } } /** * 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 = 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 }) { 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, { 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 : 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 = isArray(collection) ? arrayEach : baseEach; return func(collection, castFunction(iteratee)); } /** * Plugin module for revo-grid grid system * Add support for automatic column resize */ const LETTER_BLOCK_SIZE = 7; 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 "autoSizeOnTextOverlap" /* ColumnAutoSizeMode.autoSizeOnTextOverlap */: this.addEventListener('aftersourceset', aftersourceset); this.addEventListener('afteredit', ({ detail }) => { this.afteredit(detail); }); break; case "autoSizeAll" /* ColumnAutoSizeMode.autoSizeAll */: this.addEventListener('aftersourceset', aftersourceset); this.addEventListener('afteredit', ({ detail }) => { this.afterEditAll(detail); }); break; default: this.addEventListener('headerdblclick', ({ detail }) => { const type = 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 = 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 reduce(this.providers.data.stores, (r, s) => { const perStore = reduce(s.store.get('items'), (prev, _row, i) => { const item = 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 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 = 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 = 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) { if (upper !== undefined) { number = number <= upper ? number : upper; } if (lower !== undefined) { 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(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 = toInteger(start); if (start < 0) { start = -start > length ? 0 : (length + start); } end = (end === undefined || end > length) ? length : 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 []; } if (start && typeof start != 'number' && isIterateeCall(array, value, start)) { start = 0; end = length; } 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 (isGrouping(rgRow)) { result += this.parseCell(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 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 = []; 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 = []; 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_PROP; if (config) { this.initConfig(config); } const existingNodes = this.revogrid.registerVNode.filter(n => typeof n === 'object' && n.$tag$ !== FILTE_PANEL); this.revogrid.registerVNode = [ ...existingNodes, 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 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 (!isFilterBtn(el)) { return; } e.preventDefault(); if (!this.pop) { return; } // filter button clicked, open filter dialog const gridPos = this.revogrid.getBoundingClientRect(); const buttonPos = el.getBoundingClientRect(); const prop = e.detail.prop; 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 = filterItems; // run the filtering when the items change this.runFiltering(this.multiFil