UNPKG

@revolist/revogrid

Version:

Virtual reactive data grid spreadsheet component - RevoGrid.

1,316 lines (1,301 loc) 117 kB
/*! * Built by Revolist OU ❤️ */ 'use strict'; var index = require('./index-Dq8Xzj5l.js'); var column_service = require('./column.service-DvQDqxxx.js'); var dimension_helpers = require('./dimension.helpers-CaIsYC99.js'); var debounce = require('./debounce-CcpHiH2p.js'); var column_drag_plugin = require('./column.drag.plugin-CM_5mKV3.js'); var viewport_store = require('./viewport.store-Dcjud-a-.js'); var theme_service = require('./theme.service-BgnxGIjK.js'); var viewport_helpers = require('./viewport.helpers-BAovztDd.js'); var events = require('./events-DeLDyZlb.js'); require('./filter.button-w6LWnyhi.js'); require('./header-cell-renderer-B1dJwgTO.js'); class ColumnDataProvider { get stores() { return this.dataSources; } constructor() { this.collection = null; this.dataSources = column_service.columnTypes.reduce((sources, k) => { sources[k] = new dimension_helpers.DataStore(k); return sources; }, {}); } column(c, type = 'rgCol') { return this.getColumn(c, type); } getColumn(virtualIndex, type) { return dimension_helpers.getSourceItem(this.dataSources[type].store, virtualIndex); } getRawColumns() { return column_service.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 column_service.columnTypes.reduce((r, t) => [...r, ...columnsByType[t]], []); } getColumnIndexByProp(prop, type) { return dimension_helpers.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) { column_service.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 = column_service.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; dimension_helpers.setSourceByPhysicalIndex(this.dataSources[type].store, colByIndex[type] || {}); } } updateColumn(column, index) { const type = column_service.getColumnType(column); dimension_helpers.setSourceByVirtualIndex(this.dataSources[type].store, { [index]: column }); } } /** * Data source provider * * @dependsOn DimensionProvider */ class DataProvider { constructor(dimensionProvider) { this.dimensionProvider = dimensionProvider; this.stores = column_service.reduce(column_service.rowTypes, (sources, k) => { sources[k] = new dimension_helpers.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 dimension_helpers.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] = dimension_helpers.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 (column_service.isRowType(type)) { this.refreshItems(type); } column_service.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(dimension_helpers.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.debounce((k) => config.realSizeChanged(k), dimension_helpers.RESIZE_INTERVAL); this.stores = column_service.reduce([...column_service.rowTypes, ...column_service.columnTypes], (sources, t) => { sources[t] = new column_drag_plugin.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 = dimension_helpers.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 column_service.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 = column_service.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 column_service.columnTypes) { x += ((_a = this.stores[type]) === null || _a === void 0 ? void 0 : _a.store.get('realSize')) || 0; } for (let type of column_service.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 = dimension_helpers.getItemByIndex(dimension, e.coordinate); return item.start; } setSettings(data, dimensionType) { let stores = []; switch (dimensionType) { case 'rgCol': stores = column_service.columnTypes; break; case 'rgRow': stores = column_service.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 = column_service.reduce([...column_service.rowTypes, ...column_service.columnTypes], (sources, k) => { sources[k] = new viewport_store.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: dimension_helpers.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 column_service.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 = dimension_helpers.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: viewport_helpers.HEADER_SLOT, rgRow: viewport_helpers.CONTENT_SLOT, rowPinEnd: viewport_helpers.FOOTER_SLOT, }; // y position for selection let y = 0; return column_service.rowTypes.reduce((result, type) => { const rgCol = Object.assign(Object.assign({}, data), { position: Object.assign(Object.assign({}, data.position), { y }) }); const partition = viewport_helpers.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 (column_service.columnTypes.includes(key) && (elKey === 'headerRow' || column_service.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 column_drag_plugin.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 column_drag_plugin.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 column_drag_plugin.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 = column_service.cropCellToMax(focus, lastCell); end = column_service.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 = column_service.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 (index.h("div", { class: "draggable-wrapper hidden", ref: e => (service.el = e) }, index.h("div", { class: "draggable", ref: el => (service.draggable = el) }, index.h("span", { class: "revo-alt-icon" }), index.h("span", { ref: e => (service.text = e) })), index.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 column_drag_plugin.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 column_drag_plugin.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