@revolist/revogrid
Version:
Virtual reactive data grid spreadsheet component - RevoGrid.
1,409 lines (1,381 loc) • 94.7 kB
JavaScript
/*!
* Built by Revolist OU ❤️
*/
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 } from './column.service-e3c41fb8.js';
import { _ as createStore, w as setStore, i as calculateDimensionData, a1 as identity, Z as isArray, b as getSourceItem, o as getScrollbarSize, a0 as toInteger, $ as isIterateeCall, u as timeout, a2 as baseProperty, X as isArrayLike, a3 as getTag, a4 as baseKeys, g as getPhysical, e as setItems, j as getItemByPosition } from './dimension.helpers-87e12689.js';
import { f as calculateRowHeaderSize } from './viewport.store-84060ef5.js';
import { h } from './index-f6fae858.js';
import { b as FILTER_PROP, i as isFilterBtn } from './filter.button-84396156.js';
import { i as isObjectLike, b as baseGetTag, d as debounce } from './debounce-b3166f78.js';
import { d as dispatch, O as ON_COLUMN_CLICK } from './header-cell-renderer-c2acd090.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) {
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.multiFilterItems);
}
onFilterReset(prop) {
delete this.multiFilterItems[prop !== null && prop !== void 0 ? prop : ''];
this.onFilterChange(this.multiFilterItems);
}
/**
* Triggers grid filtering
*/
async doFiltering(collection, source, columns, filterItems) {
const columnsToUpdate = [];
/**
* Loop through the columns and update the columns that need to be updated with the `hasFilter` property.
*/
const columnByProp = {};
columns.forEach(rgCol => {
const column = Object.assign({}, rgCol);
const hasFilter = filterItems[column.prop];
columnByProp[column.prop] = column;
/**
* If the column has a filter and it's not already marked as filtered, update the column.
*/
if (column[this.filterProp] && !hasFilter) {
delete column[this.filterProp];
columnsToUpdate.push(column);
}
/**
* If the column does not have a filter and it's marked as filtered, update the column.
*/
if (!column[this.filterProp] && hasFilter) {
columnsToUpdate.push(column);
column[this.filterProp] = true;
}
});
const itemsToTrim = this.getRowFilter(source, filterItems, columnByProp);
// check is filter event prevented
const { defaultPrevented, detail } = this.emit('beforefiltertrimmed', {
collection,
itemsToFilter: itemsToTrim,
source,
filterItems,
});
if (defaultPrevented) {
return;
}
this.providers.data.setTrimmed({ [FILTER_TRIMMED_TYPE]: detail.itemsToFilter });
// applies the hasFilter to the columns to show filter icon
this.providers.column.updateColumns(columnsToUpdate);
this.emit('afterfilterapply', {
multiFilterItems: filterItems,
source,
collection,
});
}
async clearFiltering() {
this.multiFilterItems = {};
await this.runFiltering(this.multiFilterItems);
}
async runFiltering(multiFilterItems) {
const collection = {};
// handle old filterCollection to return the first filter only (if any) from multiFilterItems
const filterProps = Object.keys(multiFilterItems);
for (const prop of filterProps) {
// check if we have any filter for a column
if (multiFilterItems[prop].length > 0) {
const firstFilterItem = multiFilterItems[prop][0];
collection[prop] = {
type: firstFilterItem.type,
value: firstFilterItem.value,
};
}
}
this.filterCollection = collection;
const columns = this.providers.column.getColumns();
// run the filtering on the main source onl