@revolist/revogrid
Version:
Virtual reactive data grid spreadsheet component - RevoGrid.
1,320 lines (1,307 loc) • 116 kB
JavaScript
/*!
* Built by Revolist OU ❤️
*/
import { h, r as registerInstance, d as createEvent, e as Host, g as getElement } from './index-Chp_81rd.js';
import { c as columnTypes, J as reduce, C as getColumnType, r as rowTypes, i as isRowType, D as getColumnSizes, a as cropCellToMax, n as nextCell, I as getColumnByProp, F as getColumns } from './column.service-Cdz3dYqZ.js';
import { D as DataStore, b as getSourceItem, f as getSourceItemVirtualIndexByProp, d as setSourceByPhysicalIndex, s as setSourceByVirtualIndex, a as getVisibleSourceItem, h as gatherTrimmedItems, k as getItemByIndex, R as RESIZE_INTERVAL, u as timeout } from './dimension.helpers-DzxqJQqN.js';
import { d as debounce } from './debounce-BfO9dz9v.js';
import { D as DimensionStore, S as SelectionStore, B as BasePlugin, G as GroupingRowPlugin, a as StretchColumn, i as isStretchPlugin, A as AutoSizeColumnPlugin, e as FilterPlugin, E as ExportFilePlugin, n as SortingPlugin, l as ColumnMovePlugin } from './column.drag.plugin-DEqZ2qXJ.js';
import { V as ViewportStore } from './viewport.store-saAZJHRo.js';
import { T as ThemeService } from './theme.service-BmnDvr6P.js';
import { v as viewportDataPartition, F as FOOTER_SLOT, C as CONTENT_SLOT, H as HEADER_SLOT, D as DATA_SLOT } from './viewport.helpers-VXhsJZtn.js';
import { g as getPropertyFromEvent } from './events-BvSmBueA.js';
import './filter.button-C8XTWPU2.js';
import './header-cell-renderer-DXhxZMly.js';
class ColumnDataProvider {
get stores() {
return this.dataSources;
}
constructor() {
this.collection = null;
this.dataSources = columnTypes.reduce((sources, k) => {
sources[k] = new DataStore(k);
return sources;
}, {});
}
column(c, type = 'rgCol') {
return this.getColumn(c, type);
}
getColumn(virtualIndex, type) {
return getSourceItem(this.dataSources[type].store, virtualIndex);
}
getRawColumns() {
return reduce(this.dataSources, (result, item, type) => {
result[type] = item.store.get('source');
return result;
}, {
rgCol: [],
colPinStart: [],
colPinEnd: [],
});
}
getColumns(type = 'all') {
const columnsByType = this.getRawColumns();
if (type !== 'all') {
return columnsByType[type];
}
return columnTypes.reduce((r, t) => [...r, ...columnsByType[t]], []);
}
getColumnIndexByProp(prop, type) {
return getSourceItemVirtualIndexByProp(this.dataSources[type].store, prop);
}
getColumnByProp(prop) {
var _a;
return (_a = this.collection) === null || _a === void 0 ? void 0 : _a.columnByProp[prop];
}
refreshByType(type) {
this.dataSources[type].refresh();
}
/**
* Main method to set columns
*/
setColumns(data) {
columnTypes.forEach(k => {
// set columns data
this.dataSources[k].updateData(data.columns[k], {
// max depth level
depth: data.maxLevel,
// groups
groups: data.columnGrouping[k].reduce((res, g) => {
if (!res[g.level]) {
res[g.level] = [];
}
res[g.level].push(g);
return res;
}, {}),
});
});
this.collection = data;
return data;
}
/**
* Used in plugins
* Modify columns in store
*/
updateColumns(updatedColumns) {
// collect column by type and propert
const columnByKey = updatedColumns.reduce((res, c) => {
const type = getColumnType(c);
if (!res[type]) {
res[type] = {};
}
res[type][c.prop] = c;
return res;
}, {});
// find indexes in source
const colByIndex = {};
for (const t in columnByKey) {
if (!columnByKey.hasOwnProperty(t)) {
continue;
}
const type = t;
const colsToUpdate = columnByKey[type];
const sourceItems = this.dataSources[type].store.get('source');
colByIndex[type] = {};
for (let i = 0; i < sourceItems.length; i++) {
const column = sourceItems[i];
const colToUpdateIfExists = colsToUpdate === null || colsToUpdate === void 0 ? void 0 : colsToUpdate[column.prop];
// update column if exists in source
if (colToUpdateIfExists) {
colByIndex[type][i] = colToUpdateIfExists;
}
}
}
for (const t in colByIndex) {
if (!colByIndex.hasOwnProperty(t)) {
continue;
}
const type = t;
setSourceByPhysicalIndex(this.dataSources[type].store, colByIndex[type] || {});
}
}
updateColumn(column, index) {
const type = getColumnType(column);
setSourceByVirtualIndex(this.dataSources[type].store, { [index]: column });
}
}
/**
* Data source provider
*
* @dependsOn DimensionProvider
*/
class DataProvider {
constructor(dimensionProvider) {
this.dimensionProvider = dimensionProvider;
this.stores = reduce(rowTypes, (sources, k) => {
sources[k] = new DataStore(k);
return sources;
}, {});
}
/**
* Replaces the data source for a row type and synchronizes the related dimension metadata.
*
* `rgRow` updates also refresh the virtual row model unless `disableVirtualRows` is set.
* Pinned row types skip virtual row recalculation because they are rendered directly.
*
* @param data Full source data assigned to the target row store.
* @param type Row collection to update. Defaults to the main body rows.
* @param disableVirtualRows Prevents recalculating virtual rows for the main row store.
* @param grouping Optional grouping metadata applied together with the new data set.
* @param silent Preserves the current trimmed state instead of resetting it during the update.
* @param preserveTrimmed Re-applies current trimmed indexes after a silent update.
* @returns The same `data` array that was provided to the method.
*/
setData(data, type = 'rgRow', disableVirtualRows = false, grouping,
// if true, store will be updated without resetting trimmed state
silent = false, preserveTrimmed = false) {
// set rgRow data
this.stores[type].updateData([...data], grouping, silent, preserveTrimmed);
// for pinned row no need virtual data
const noVirtual = type !== 'rgRow' || disableVirtualRows;
this.dimensionProvider.setData(data.length, type, noVirtual);
return data;
}
getModel(virtualIndex, type = 'rgRow') {
const store = this.stores[type].store;
return getSourceItem(store, virtualIndex);
}
changeOrder({ rowType = 'rgRow', from, to }) {
const storeService = this.stores[rowType];
// take currently visible row indexes
const newItemsOrder = [...storeService.store.get('proxyItems')];
const prevItems = storeService.store.get('items');
// take out
const toMove = newItemsOrder.splice(newItemsOrder.indexOf(prevItems[from]), // get index in proxy
1);
// insert before
newItemsOrder.splice(newItemsOrder.indexOf(prevItems[to]), // get index in proxy
0, ...toMove);
storeService.setData({
proxyItems: newItemsOrder,
});
// take currently visible row indexes
const newItems = storeService.store.get('items');
this.dimensionProvider.updateSizesPositionByNewDataIndexes(rowType, newItems, prevItems);
}
setCellData({ type, rowIndex, prop, val }, mutate = true) {
const model = this.getModel(rowIndex, type);
model[prop] = val;
this.stores[type].setSourceData({ [rowIndex]: model }, mutate);
}
setRangeData(data, type) {
const items = {};
for (let rowIndex in data) {
const oldModel = (items[rowIndex] = getSourceItem(this.stores[type].store, parseInt(rowIndex, 10)));
if (!oldModel) {
continue;
}
for (let prop in data[rowIndex]) {
oldModel[prop] = data[rowIndex][prop];
}
}
this.stores[type].setSourceData(items);
}
refresh(type = 'all') {
if (isRowType(type)) {
this.refreshItems(type);
}
rowTypes.forEach((t) => this.refreshItems(t));
}
refreshItems(type = 'rgRow') {
const items = this.stores[type].store.get('items');
this.stores[type].setData({ items: [...items] });
}
setGrouping({ depth }, type = 'rgRow') {
this.stores[type].setData({ groupingDepth: depth });
}
setTrimmed(trimmed, type = 'rgRow') {
const store = this.stores[type];
store.addTrimmed(trimmed);
this.dimensionProvider.setTrimmed(trimmed, type);
if (type === 'rgRow') {
this.dimensionProvider.setData(getVisibleSourceItem(store.store).length, type);
}
}
}
/**
* Dimension provider
* Stores dimension information and custom sizes
*
* @dependsOn ViewportProvider
*/
class DimensionProvider {
constructor(viewports, config) {
this.viewports = viewports;
const sizeChanged = debounce((k) => config.realSizeChanged(k), RESIZE_INTERVAL);
this.stores = reduce([...rowTypes, ...columnTypes], (sources, t) => {
sources[t] = new DimensionStore(t);
sources[t].store.onChange('realSize', () => sizeChanged(t));
return sources;
}, {});
}
/**
* Clear old sizes from dimension and viewports
* @param type - dimension type
* @param count - count of items
*/
clearSize(t, count) {
this.stores[t].drop();
// after we done with drop trigger viewport recalculaction
this.viewports.stores[t].setOriginalSizes(this.stores[t].store.get('originItemSize'));
this.setItemCount(count, t);
}
/**
* Apply new custom sizes to dimension and view port
* @param type - dimension type
* @param sizes - new custom sizes
* @param keepOld - keep old sizes merge new with old
*/
setCustomSizes(type, sizes, keepOld = false) {
let newSizes = sizes;
if (keepOld) {
const oldSizes = this.stores[type].store.get('sizes');
newSizes = Object.assign(Object.assign({}, oldSizes), sizes);
}
this.stores[type].setDimensionSize(newSizes);
this.setViewPortCoordinate({
type,
force: true,
});
}
setItemCount(realCount, type) {
this.viewports.stores[type].setViewport({ realCount });
this.stores[type].setStore({ count: realCount });
}
/**
* Apply trimmed items
* @param trimmed - trimmed items
* @param type
*/
setTrimmed(trimmed, type) {
const allTrimmed = gatherTrimmedItems(trimmed);
const dimStoreType = this.stores[type];
dimStoreType.setStore({ trimmed: allTrimmed });
this.setViewPortCoordinate({
type,
force: true,
});
}
/**
* Sets dimension data and viewport coordinate
* @param itemCount
* @param type - dimension type
* @param noVirtual - disable virtual data
*/
setData(itemCount, type, noVirtual = false) {
this.setItemCount(itemCount, type);
// Virtualization will get disabled
if (noVirtual) {
const dimension = this.stores[type].getCurrentState();
this.viewports.stores[type].setViewport({
virtualSize: dimension.realSize,
});
}
this.setViewPortCoordinate({
type,
});
}
/**
* Applies new columns to the dimension provider
* @param columns - new columns data
* @param disableVirtualX - disable virtual data for X axis
*/
applyNewColumns(columns, disableVirtualX, keepOld = false) {
// Apply new columns to dimension provider
for (let type of columnTypes) {
if (!keepOld) {
// Clear existing data in the dimension provider
this.stores[type].drop();
}
// Get the new columns for the current type
const items = columns[type];
// Determine if virtual data should be disabled for the current type
const noVirtual = type !== 'rgCol' || disableVirtualX;
// Set the items count in the dimension provider
this.stores[type].setStore({ count: items.length });
// Set the custom sizes for the columns
const newSizes = getColumnSizes(items);
this.stores[type].setDimensionSize(newSizes);
// Update the viewport with new data
const vpUpdate = {
// This triggers drop on realCount change
realCount: items.length,
};
// If virtual data is disabled, set the virtual size to the real size
if (noVirtual) {
vpUpdate.virtualSize = this.stores[type].getCurrentState().realSize;
}
// Update the viewport
this.viewports.stores[type].setViewport(vpUpdate);
this.setViewPortCoordinate({
type,
});
}
}
/**
* Gets the full size of the grid by summing up the sizes of all dimensions
* Goes through all dimensions columnTypes (x) and rowTypes (y) and sums up their sizes
*/
getFullSize() {
var _a, _b;
let x = 0;
let y = 0;
for (let type of columnTypes) {
x += ((_a = this.stores[type]) === null || _a === void 0 ? void 0 : _a.store.get('realSize')) || 0;
}
for (let type of rowTypes) {
y += ((_b = this.stores[type]) === null || _b === void 0 ? void 0 : _b.store.get('realSize')) || 0;
}
return { y, x };
}
setViewPortCoordinate({ type, coordinate = this.viewports.stores[type].lastCoordinate, force = false, }) {
const dimension = this.stores[type].getCurrentState();
this.viewports.stores[type].setViewPortCoordinate(coordinate, dimension, force);
}
getViewPortPos(e) {
const dimension = this.stores[e.dimension].getCurrentState();
const item = getItemByIndex(dimension, e.coordinate);
return item.start;
}
setSettings(data, dimensionType) {
let stores = [];
switch (dimensionType) {
case 'rgCol':
stores = columnTypes;
break;
case 'rgRow':
stores = rowTypes;
break;
}
for (let s of stores) {
this.stores[s].setStore(data);
}
}
updateSizesPositionByNewDataIndexes(type, newItemsOrder, prevItemsOrder = []) {
// Move custom sizes to new order
this.stores[type].updateSizesPositionByIndexes(newItemsOrder, prevItemsOrder);
this.setViewPortCoordinate({
type,
force: true,
});
}
}
class ViewportProvider {
constructor() {
this.stores = reduce([...rowTypes, ...columnTypes], (sources, k) => {
sources[k] = new ViewportStore(k);
return sources;
}, {});
}
setViewport(type, data) {
this.stores[type].setViewport(data);
}
}
/** Collect Column data */
function gatherColumnData(data) {
const colDimension = data.dimensions[data.colType].store;
const realWidth = colDimension.get('realSize');
const prop = {
contentWidth: realWidth,
class: data.colType,
contentHeight: data.contentHeight,
key: data.colType,
colType: data.colType,
noHorizontalScrollTransfer: data.noHorizontalScrollTransfer,
onResizeviewport: data.onResizeviewport,
// set viewport size to real size
style: data.fixWidth ? { minWidth: `${realWidth}px` } : undefined,
};
const headerProp = {
colData: getVisibleSourceItem(data.colStore),
dimensionCol: colDimension,
type: data.colType,
groups: data.colStore.get('groups'),
groupingDepth: data.colStore.get('groupingDepth'),
resizeHandler: data.colType === 'colPinEnd' ? ['l'] : undefined,
onHeaderresize: data.onHeaderresize,
};
return {
prop,
type: data.colType,
position: data.position,
headerProp,
viewportCol: data.viewports[data.colType].store,
};
}
class ViewportService {
constructor(config, contentHeight) {
// ----------- Handle columns ----------- //
var _a;
this.config = config;
// Transform data from stores and apply it to different components
const columns = [];
let x = 0; // we increase x only if column present
columnTypes.forEach(val => {
const colStore = config.columnProvider.stores[val].store;
// only columns that have data show
if (!colStore.get('items').length) {
return;
}
const column = {
colType: val,
position: { x, y: 1 },
contentHeight,
// only central column has dynamic width
fixWidth: val !== 'rgCol',
viewports: config.viewportProvider.stores,
dimensions: config.dimensionProvider.stores,
rowStores: config.dataProvider.stores,
noHorizontalScrollTransfer: config.noHorizontalScrollTransfer,
colStore,
onHeaderresize: e => this.onColumnResize(val, e, colStore),
};
if (val === 'rgCol') {
column.onResizeviewport = (e) => {
var _a;
const vpState = {
clientSize: e.detail.size,
};
// virtual size will be handled by dimension provider if disabled
if ((e.detail.dimension === 'rgRow' && !config.disableVirtualY)
|| (e.detail.dimension === 'rgCol' && !config.disableVirtualX)) {
vpState.virtualSize = e.detail.size;
}
(_a = config.viewportProvider) === null || _a === void 0 ? void 0 : _a.setViewport(e.detail.dimension, vpState);
};
}
const colData = gatherColumnData(column);
const columnSelectionStore = this.registerCol(colData.position.x, val);
// render per each column data collections vertically
const dataPorts = this.dataViewPort(column).reduce((r, rgRow) => {
// register selection store for Segment
const segmentSelection = this.registerSegment(rgRow.position, rgRow.lastCell);
// register selection store for Row
const rowSelectionStore = this.registerRow(rgRow.position.y, rgRow.type);
const rowDef = Object.assign(Object.assign({ colType: val }, rgRow), { rowSelectionStore, selectionStore: segmentSelection.store, onSetrange: e => {
segmentSelection.setRangeArea(e.detail);
}, onSettemprange: e => segmentSelection.setTempArea(e.detail), onFocuscell: e => {
// todo: multi focus
segmentSelection.clearFocus();
config.selectionStoreConnector.focus(segmentSelection, e.detail);
} });
r.push(rowDef);
return r;
}, []);
columns.push(Object.assign(Object.assign({}, colData), { columnSelectionStore,
dataPorts }));
x++;
});
this.columns = columns;
// ----------- Handle columns end ----------- //
(_a = this.config.scrollingService) === null || _a === void 0 ? void 0 : _a.unregister();
}
onColumnResize(type, { detail }, store) {
var _a;
// apply to dimension provider
(_a = this.config.dimensionProvider) === null || _a === void 0 ? void 0 : _a.setCustomSizes(type, detail, true);
// set resize event
const changedItems = {};
for (const [i, size] of Object.entries(detail || {})) {
const virtualIndex = parseInt(i, 10);
const item = getSourceItem(store, virtualIndex);
if (item) {
changedItems[virtualIndex] = Object.assign(Object.assign({}, item), { size });
}
}
this.config.resize(changedItems);
}
/** register selection store for Segment */
registerSegment(position, lastCell) {
const store = this.config.selectionStoreConnector.register(position);
store.setLastCell(lastCell);
return store;
}
/** register selection store for Row */
registerRow(y, type) {
return this.config.selectionStoreConnector.registerRow(y, type).store;
}
/** register selection store for Column */
registerCol(x, type) {
return this.config.selectionStoreConnector.registerColumn(x, type).store;
}
/** Collect Row data */
dataViewPort(data) {
const slots = {
rowPinStart: HEADER_SLOT,
rgRow: CONTENT_SLOT,
rowPinEnd: FOOTER_SLOT,
};
// y position for selection
let y = 0;
return rowTypes.reduce((result, type) => {
const rgCol = Object.assign(Object.assign({}, data), { position: Object.assign(Object.assign({}, data.position), { y }) });
const partition = viewportDataPartition(rgCol, type, slots[type], type !== 'rgRow');
result.push(partition);
y++;
return result;
}, []);
}
scrollToCell(cell) {
for (let key in cell) {
const coordinate = cell[key];
if (typeof coordinate === 'number') {
this.config.scrollingService.proxyScroll({
dimension: key === 'x' ? 'rgCol' : 'rgRow',
coordinate,
});
}
}
}
/**
* Clear current grid focus
*/
clearFocused() {
this.config.selectionStoreConnector.clearAll();
}
clearEdit() {
this.config.selectionStoreConnector.setEdit(false);
}
/**
* Collect focused element data
*/
getFocused() {
const focused = this.config.selectionStoreConnector.focusedStore;
if (!focused) {
return null;
}
// get column data
const colType = this.config.selectionStoreConnector.storesXToType[focused.position.x];
const column = this.config.columnProvider.getColumn(focused.cell.x, colType);
// get row data
const rowType = this.config.selectionStoreConnector.storesYToType[focused.position.y];
const model = this.config.dataProvider.getModel(focused.cell.y, rowType);
return {
column,
model,
cell: focused.cell,
colType,
rowType,
};
}
getStoreCoordinateByType(colType, rowType) {
const stores = this.config.selectionStoreConnector.storesByType;
if (typeof stores[colType] === 'undefined' || typeof stores[rowType] === 'undefined') {
return;
}
return {
x: stores[colType],
y: stores[rowType],
};
}
setFocus(colType, rowType, start, end) {
var _a;
const coordinate = this.getStoreCoordinateByType(colType, rowType);
if (coordinate) {
(_a = this.config.selectionStoreConnector) === null || _a === void 0 ? void 0 : _a.focusByCell(coordinate, start, end);
}
}
getSelectedRange() {
const focused = this.config.selectionStoreConnector.focusedStore;
if (!focused) {
return null;
}
// get column data
const colType = this.config.selectionStoreConnector.storesXToType[focused.position.x];
// get row data
const rowType = this.config.selectionStoreConnector.storesYToType[focused.position.y];
const range = focused.entity.store.get('range');
if (!range) {
return null;
}
return Object.assign(Object.assign({}, range), { colType,
rowType });
}
setEdit(rowIndex, colIndex, colType, rowType) {
var _a;
const coordinate = this.getStoreCoordinateByType(colType, rowType);
if (coordinate) {
(_a = this.config.selectionStoreConnector) === null || _a === void 0 ? void 0 : _a.setEditByCell(coordinate, { x: colIndex, y: rowIndex });
}
}
}
class GridScrollingService {
constructor(setViewport) {
this.setViewport = setViewport;
this.elements = {};
}
async proxyScroll(e, key, skipEvent) {
var _a;
let newEventPromise;
let event = e;
for (let elKey in (skipEvent ? {} : this.elements)) {
// skip
if (e.dimension === 'rgCol' && elKey === 'headerRow') {
continue;
// pinned column only
}
else if (this.isPinnedColumn(key) && e.dimension === 'rgCol') {
if (elKey === key || !e.delta) {
continue;
}
for (let el of this.elements[elKey]) {
if (el.changeScroll) {
newEventPromise = el.changeScroll(e);
}
}
}
else {
for (let el of this.elements[elKey]) {
await ((_a = el.setScroll) === null || _a === void 0 ? void 0 : _a.call(el, e));
}
}
}
const newEvent = await newEventPromise;
if (newEvent) {
event = newEvent;
}
this.setViewport(skipEvent && this.isPinnedColumn(key)
? Object.assign(Object.assign({}, event), { dimension: key }) : event);
}
/**
* Silent scroll update for mobile devices when we have negative scroll top
*/
async scrollSilentService(e, key) {
var _a;
for (let elKey in this.elements) {
// skip same element update
if (elKey === key) {
continue;
}
if (columnTypes.includes(key) &&
(elKey === 'headerRow' ||
columnTypes.includes(elKey))) {
for (let el of this.elements[elKey]) {
await ((_a = el.changeScroll) === null || _a === void 0 ? void 0 : _a.call(el, e, true));
}
continue;
}
}
}
isPinnedColumn(key) {
return !!key && ['colPinStart', 'colPinEnd'].indexOf(key) > -1;
}
registerElements(els) {
this.elements = els;
}
/**
* Register new element for farther scroll support
* @param el - can be null if holder removed
* @param key - element key
*/
registerElement(el, key) {
if (!this.elements[key]) {
this.elements[key] = [];
}
// new element added
if (el) {
this.elements[key].push(el);
}
else if (this.elements[key]) {
// element removed
delete this.elements[key];
}
}
unregister() {
this.elements = {};
}
}
class SelectionStoreConnector {
constructor() {
this.stores = {};
this.columnStores = {};
this.rowStores = {};
/**
* Helpers for data conversion
*/
this.storesByType = {};
this.storesXToType = {};
this.storesYToType = {};
}
get focusedStore() {
var _a;
for (let y in this.stores) {
for (let x in this.stores[y]) {
const focused = (_a = this.stores[y][x]) === null || _a === void 0 ? void 0 : _a.store.get('focus');
if (focused) {
return {
entity: this.stores[y][x],
cell: focused,
position: {
x: parseInt(x, 10),
y: parseInt(y, 10),
},
};
}
}
}
return null;
}
get edit() {
var _a;
return (_a = this.focusedStore) === null || _a === void 0 ? void 0 : _a.entity.store.get('edit');
}
get focused() {
var _a;
return (_a = this.focusedStore) === null || _a === void 0 ? void 0 : _a.entity.store.get('focus');
}
get selectedRange() {
var _a;
return (_a = this.focusedStore) === null || _a === void 0 ? void 0 : _a.entity.store.get('range');
}
registerColumn(x, type) {
if (this.columnStores[x]) {
return this.columnStores[x];
}
this.columnStores[x] = new SelectionStore();
// build cross-linking type to position
this.storesByType[type] = x;
this.storesXToType[x] = type;
return this.columnStores[x];
}
registerRow(y, type) {
if (this.rowStores[y]) {
return this.rowStores[y];
}
this.rowStores[y] = new SelectionStore();
// build cross linking type to position
this.storesByType[type] = y;
this.storesYToType[y] = type;
return this.rowStores[y];
}
/**
* Cross store proxy, based on multiple dimensions
*/
register({ x, y }) {
if (!this.stores[y]) {
this.stores[y] = {};
}
let store = this.stores[y][x];
if (store) {
// Store already registered. Do not register twice
return store;
}
this.stores[y][x] = store = new SelectionStore();
// proxy update, column store trigger only range area
store.onChange('range', c => {
this.columnStores[x].setRangeArea(c);
this.rowStores[y].setRangeArea(c);
});
// clean up on remove
store.store.on('dispose', () => this.destroy(x, y));
return store;
}
destroy(x, y) {
var _a, _b;
(_a = this.columnStores[x]) === null || _a === void 0 ? void 0 : _a.dispose();
(_b = this.rowStores[y]) === null || _b === void 0 ? void 0 : _b.dispose();
delete this.rowStores[y];
delete this.columnStores[x];
// clear x cross-link
if (this.storesXToType[x]) {
const type = this.storesXToType[x];
delete this.storesXToType[x];
delete this.storesByType[type];
}
// clear y cross-link
if (this.storesYToType[y]) {
const type = this.storesYToType[y];
delete this.storesYToType[y];
delete this.storesByType[type];
}
if (this.stores[y]) {
delete this.stores[y][x];
}
// clear empty rows
if (!Object.keys(this.stores[y] || {}).length) {
delete this.stores[y];
}
}
setEditByCell(storePos, editCell) {
this.focusByCell(storePos, editCell, editCell);
this.setEdit('');
}
/**
* Sets the next focus cell before the current one.
*
* @param focus - The cell to set as the next focus.
*/
beforeNextFocusCell(focus) {
var _a;
// If there is no focused store, return early.
if (!this.focusedStore) {
return;
}
// Get the next store based on the current focus and the last cell.
const lastCell = this.focusedStore.entity.store.get('lastCell');
const next = lastCell && this.getNextStore(focus, this.focusedStore.position, lastCell);
// Set the next focus cell in the store.
(_a = next === null || next === void 0 ? void 0 : next.store) === null || _a === void 0 ? void 0 : _a.setNextFocus(Object.assign(Object.assign({}, focus), next.item));
}
focusByCell(storePos, start, end) {
const store = this.stores[storePos.y][storePos.x];
this.focus(store, { focus: start, end });
}
focus(store, { focus, end }) {
const currentStorePointer = this.getCurrentStorePointer(store);
if (!currentStorePointer) {
return null;
}
// check for the focus in nearby store/viewport
const lastCell = store.store.get('lastCell');
const next = lastCell && this.getNextStore(focus, currentStorePointer, lastCell);
// if next store present - update
if (next === null || next === void 0 ? void 0 : next.store) {
const item = Object.assign(Object.assign({}, focus), next.item);
this.focus(next.store, { focus: item, end: item });
return null;
}
if (lastCell) {
focus = cropCellToMax(focus, lastCell);
end = cropCellToMax(end, lastCell);
}
store.setFocus(focus, end);
return focus;
}
/**
* Retrieves the current store pointer based on the active store.
* Clears focus from all stores except the active one.
*/
getCurrentStorePointer(store) {
let currentStorePointer;
// Iterate through all stores
for (let y in this.stores) {
for (let x in this.stores[y]) {
const s = this.stores[y][x];
// Clear focus from stores other than the active one
if (s !== store) {
s.clearFocus();
}
else {
// Update the current store pointer with the active store coordinates
currentStorePointer = {
x: parseInt(x, 10),
y: parseInt(y, 10)
};
}
}
}
return currentStorePointer;
}
/**
* Retrieves the next store based on the focus cell and current store pointer.
* If the next store exists, returns an object with the next store and the item in the new store.
* If the next store does not exist, returns null.
*/
getNextStore(focus, currentStorePointer, lastCell) {
// item in new store
const nextItem = nextCell(focus, lastCell);
let nextStore;
if (nextItem) {
Object.entries(nextItem).forEach(([type, nextItemCoord]) => {
let stores;
switch (type) {
case 'x':
// Get the X stores for the current Y coordinate of the current store pointer
stores = this.getXStores(currentStorePointer.y);
break;
case 'y':
// Get the Y stores for the current X coordinate of the current store pointer
stores = this.getYStores(currentStorePointer.x);
break;
}
// Get the next store based on the item in the new store
if (nextItemCoord >= 0) {
nextStore = stores[++currentStorePointer[type]];
}
else {
nextStore = stores[--currentStorePointer[type]];
const nextLastCell = nextStore === null || nextStore === void 0 ? void 0 : nextStore.store.get('lastCell');
if (nextLastCell) {
nextItem[type] = nextLastCell[type] + nextItemCoord;
}
}
});
}
// if last cell is empty store is empty, no next store
const lastCellNext = nextStore === null || nextStore === void 0 ? void 0 : nextStore.store.get('lastCell');
if (!(lastCellNext === null || lastCellNext === void 0 ? void 0 : lastCellNext.x) || !(lastCellNext === null || lastCellNext === void 0 ? void 0 : lastCellNext.y)) {
nextStore = undefined;
}
return {
store: nextStore,
item: nextItem,
};
}
clearAll() {
var _a;
for (let y in this.stores) {
for (let x in this.stores[y]) {
(_a = this.stores[y][x]) === null || _a === void 0 ? void 0 : _a.clearFocus();
}
}
}
setEdit(val) {
if (!this.focusedStore) {
return;
}
this.focusedStore.entity.setEdit(val);
}
/**
* Select all cells across all stores
*/
selectAll() {
for (let y in this.stores) {
for (let x in this.stores[y]) {
const store = this.stores[y][x];
if (!store) {
continue;
}
const lastCell = store.store.get('lastCell');
if (lastCell) {
store.setRange({ x: 0, y: 0 }, { x: lastCell.x - 1, y: lastCell.y - 1 });
}
}
}
}
getXStores(y) {
return this.stores[y];
}
getYStores(x) {
const stores = {};
for (let i in this.stores) {
stores[i] = this.stores[i][x];
}
return stores;
}
}
/**
* Draw drag
*/
class OrdererService {
constructor() {
this.parentY = 0;
}
start(parent, { pos, text, event }) {
var _a;
const { top } = parent.getBoundingClientRect();
this.parentY = top;
if (this.text) {
this.text.innerText = text;
}
this.move(pos);
this.moveTip({ x: event.x, y: event.y });
(_a = this.el) === null || _a === void 0 ? void 0 : _a.classList.remove('hidden');
}
end() {
var _a;
(_a = this.el) === null || _a === void 0 ? void 0 : _a.classList.add('hidden');
}
move(pos) {
this.moveElement(pos.end - this.parentY);
}
moveTip({ x, y }) {
if (!this.draggable) {
return;
}
this.draggable.style.left = `${x}px`;
this.draggable.style.top = `${y}px`;
}
moveElement(y) {
if (!this.rgRow) {
return;
}
this.rgRow.style.transform = `translateY(${y}px)`;
}
}
const OrderRenderer = ({ ref }) => {
const service = new OrdererService();
ref(service);
return (h("div", { class: "draggable-wrapper hidden", ref: e => (service.el = e) }, h("div", { class: "draggable", ref: el => (service.draggable = el) }, h("span", { class: "revo-alt-icon" }), h("span", { ref: e => (service.text = e) })), h("div", { class: "drag-position", ref: e => (service.rgRow = e) })));
};
const rowDefinitionByType = (newVal = []) => {
const result = {};
for (const v of newVal) {
let rowDefs = result[v.type];
if (!rowDefs) {
rowDefs = result[v.type] = {};
}
if (typeof v.size === 'number') {
if (!rowDefs.sizes) {
rowDefs.sizes = {};
}
rowDefs.sizes[v.index] = v.size;
}
}
return result;
};
const rowDefinitionRemoveByType = (oldVal = []) => {
const result = {};
for (const v of oldVal) {
let rowDefs = result[v.type];
if (!rowDefs) {
rowDefs = result[v.type] = [];
}
if (typeof v.size === 'number') {
rowDefs.push(v.index);
}
}
return result;
};
function isMobileDevice() {
return /Mobi/i.test(navigator.userAgent) || /Android/i.test(navigator.userAgent) || navigator.maxTouchPoints > 0;
}
/**
* WCAG Plugin is responsible for enhancing the accessibility features of the RevoGrid component.
* It ensures that the grid is fully compliant with Web Content Accessibility Guidelines (WCAG) 2.1.
* This plugin should be the last plugin you add, as it modifies the grid's default behavior.
*
* The WCAG Plugin performs the following tasks:
* - Sets the 'dir' attribute to 'ltr' for left-to-right text direction.
* - Sets the 'role' attribute to 'treegrid' for treelike hierarchical structure.
* - Sets the 'aria-keyshortcuts' attribute to 'Enter' and 'Esc' for keyboard shortcuts.
* - Adds event listeners for keyboard navigation and editing.
*
* By default, the plugin adds ARIA roles and properties to the grid elements, providing semantic information
* for assistive technologies. These roles include 'grid', 'row', and 'gridcell'. The plugin also sets
* ARIA attributes such as 'aria-rowindex', 'aria-colindex', and 'aria-selected'.
*
* The WCAG Plugin ensures that the grid is fully functional and usable for users with various disabilities,
* including visual impairments, deaf-blindness, and cognitive disabilities.
*
* Note: The WCAG Plugin should be added as the last plugin in the list of plugins, as it modifies the grid's
* default behavior and may conflict with other plugins if added earlier.
*/
class WCAGPlugin extends BasePlugin {
constructor(revogrid, providers) {
super(revogrid, providers);
revogrid.setAttribute('role', 'treegrid');
revogrid.setAttribute('aria-keyshortcuts', 'Enter');
revogrid.setAttribute('aria-multiselectable', 'true');
revogrid.setAttribute('tabindex', '0');
/**
* Before Columns Set Event
*/
this.addEventListener('beforecolumnsset', ({ detail }) => {
const columns = [
...detail.columns.colPinStart,
...detail.columns.rgCol,
...detail.columns.colPinEnd,
];
revogrid.setAttribute('aria-colcount', `${columns.length}`);
columns.forEach((column, index) => {
const { columnProperties, cellProperties } = column;
column.columnProperties = (...args) => {
const result = (columnProperties === null || columnProperties === void 0 ? void 0 : columnProperties(...args)) || {};
result.role = 'columnheader';
result['aria-colindex'] = `${index}`;
return result;
};
column.cellProperties = (...args) => {
const wcagProps = {
['role']: 'gridcell',
['aria-colindex']: `${index}`,
['aria-rowindex']: `${args[0].rowIndex}`,
['tabindex']: -1,
};
const columnProps = (cellProperties === null || cellProperties === void 0 ? void 0 : cellProperties(...args)) || {};
return Object.assign(Object.assign({}, wcagProps), columnProps);
};
});
});
/**
* Before Row Set Event
*/
this.addEventListener('beforesourceset', ({ detail, }) => {
revogrid.setAttribute('aria-rowcount', `${detail.source.length}`);
});
this.addEventListener('beforerowrender', ({ detail, }) => {
detail.node.$attrs$ = Object.assign(Object.assign({}, detail.node.$attrs$), { role: 'row', ['aria-rowindex']: detail.item.itemIndex });
});
// focuscell
this.addEventListener('afterfocus', async (e) => {
if (e.defaultPrevented) {
return;
}
const el = this.revogrid.querySelector(`revogr-data[type="${e.detail.rowType}"][col-type="${e.detail.colType}"] [data-rgrow="${e.detail.rowIndex}"][data-rgcol="${e.detail.colIndex}"]`);
if (el instanceof HTMLElement) {
el.focus();
}
});
}
}
/**
* Plugin service
* Manages plugins
*/
class PluginService {
constructor() {
/**
* Plugins
* Define plugins collection
*/
this.internalPlugins = [];
}
/**
* Get all plugins
*/
get() {
return [...this.internalPlugins];
}
/**
* Add plugin to collection
*/
add(plugin) {
this.internalPlugins.push(plugin);
}
/**
* Add user plugins and create
*/
addUserPluginsAndCreate(element, plugins = [], prevPlugins, pluginData) {
if (!pluginData) {
return;
}
// Step 1: Identify plugins to remove, compare new and old plugins
const pluginsToRemove = (prevPlugins === null || prevPlugins === void 0 ? void 0 : prevPlugins.filter(prevPlugin => !plugins.some(userPlugin => userPlugin === prevPlugin))) || [];
// Step 2: Remove old plugins
pluginsToRemove.forEach(plugin => {
var _a, _b;
const index = this.internalPlugins.findIndex(createdPlugin => createdPlugin instanceof plugin);
if (index !== -1) {
(_b = (_a = this.internalPlugins[index]).destroy) === null || _b === void 0 ? void 0 : _b.call(_a);
this.internalPlugins.splice(index, 1); // Remove the plugin
}
});
// Step 3: Register user plugins
plugins === null || plugins === void 0 ? void 0 : plugins.forEach(userPlugin => {
// check if plugin already exists, if so, skip
const existingPlugin = this.internalPlugins.find(createdPlugin => createdPlugin instanceof userPlugin);
if (existingPlugin) {
return;
}
this.add(new userPlugin(element, pluginData));
});
}
/**
* Get plugin by class
*/
getByClass(pluginClass) {
return this.internalPlugins.find(p => p instanceof pluginClass);
}
/**
* Remove plugin
*/
remove(plugin) {
var _a, _b;
const index = this.internalPlugins.indexOf(plugin);
if (index > -1) {
(_b = (_a = this.internalPlugins[index]).destroy) === null || _b === void 0 ? void 0 : _b.call(_a);
this.internalPlugins.splice(index, 1);
}
}
/**
* Remove all plugins
*/
destroy() {
this.internalPlugins.forEach(p => { var _a; return (_a = p.destroy) === null || _a === void 0 ? void 0 : _a.call(p); });
this.internalPlugins = [];
}
}
/**
* RTL (Right-to-Left) Plugin for RevoGrid
*
* This plugin handles RTL transformation by subscribing to the beforecolumnsset event
* and applying column order reversal when RTL mode is enabled.
*/
class RTLPlugin extends BasePlugin {
constructor(revogrid, providers) {
super(revogrid, providers);
this.isRTLEnabled = false;
this.init();
}
init() {
// Subscribe to beforecolumnsset event to apply RTL transformation
this.addEventListener('beforecolumnsset', (event) => {
this.handleBeforeColumnsSet(event);
});
// Listen for RTL property changes
this.addEventListener('aftergridinit', () => {
this.updateRTLState();
});
// Watch for RTL property changes
this.watch('rtl', (value) => {
this.isRTLEnabled = value;
this.emit('rtlstatechanged', { rtl: this.isRTLEnabled });
}, { immediate: true });
}
/**
* Handle the beforecolumnsset event to apply RTL transformation
*/
handleBeforeColumnsSet(event) {
if (!this.isRTLEnabled) {
return; // No transformation needed if RTL is disabled
}
const columnCollection = event.detail;
// Apply RTL transformation to all column types
const transformedColumns = this.applyRTLTransformationToCollection(columnCollection);
// Update the event detail with transformed columns
event.detail.columns = transformedColumns.columns;
event.detail.columnByProp = transformedColumns.columnByProp;
event.detail.columnGrouping = transformedColumns.columnGrouping;
}
/**
* Apply RTL transformation to the entire column collection
*/
applyRTLTransformationToCollection(collection) {
const transformedCollection = {
columns: {
rgCol: [],
colPinStart: [],
colPinEnd: [],
},
columnByProp: Object.assign({}, collection.columnByProp),
columnGrouping: {
rgCol: [],
colPinStart: [],
colPinEnd: [],
},
maxLevel: collection.maxLevel,
sort: Object.assign({}, collection.sort),
};
// Transform each column type
Object.keys(collection.columns).forEach((type) => {
const columnType = type;
const columns = collection.columns[columnType];
// Apply RTL transformation to columns - create new reversed array
const reversedColumns = [...columns].reverse();
transformedCollection.columns[columnType] = reversedColumns;
// Transform column grouping for this type
transformedCollection.columnGrouping[columnType] = this.applyRTLTransformationToGroups(collection.columnGrouping[columnType], columns.length);
});
return transformedCollection;
}
/**
* Apply RTL transformation to column groups
*/
applyRTLTransformationToGroups(groups, totalColumns) {
return groups.map(group => {
// Reverse the indexes for RTL
const reversedIndexes = group.indexes.map((index) => totalColumns - 1 - index).reverse();
return Object.assign(Object.assign({}, group), { indexes: reversedIndexes });
}).reverse(); // Reverse the group order
}
/**
* Update RTL state based on the grid's rtl property
*/
updateRTLState() {
const grid = this.revogrid;
if (grid