@alaarab/ogrid-js 2.1.2 → 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/esm/index.js +6343 -32
  2. package/package.json +7 -5
  3. package/dist/esm/OGrid.js +0 -578
  4. package/dist/esm/OGridEventWiring.js +0 -178
  5. package/dist/esm/OGridRendering.js +0 -269
  6. package/dist/esm/components/ColumnChooser.js +0 -91
  7. package/dist/esm/components/ContextMenu.js +0 -125
  8. package/dist/esm/components/HeaderFilter.js +0 -281
  9. package/dist/esm/components/InlineCellEditor.js +0 -434
  10. package/dist/esm/components/MarchingAntsOverlay.js +0 -156
  11. package/dist/esm/components/PaginationControls.js +0 -85
  12. package/dist/esm/components/SideBar.js +0 -353
  13. package/dist/esm/components/StatusBar.js +0 -34
  14. package/dist/esm/renderer/TableRenderer.js +0 -846
  15. package/dist/esm/state/ClipboardState.js +0 -111
  16. package/dist/esm/state/ColumnPinningState.js +0 -82
  17. package/dist/esm/state/ColumnReorderState.js +0 -135
  18. package/dist/esm/state/ColumnResizeState.js +0 -55
  19. package/dist/esm/state/EventEmitter.js +0 -28
  20. package/dist/esm/state/FillHandleState.js +0 -206
  21. package/dist/esm/state/GridState.js +0 -324
  22. package/dist/esm/state/HeaderFilterState.js +0 -213
  23. package/dist/esm/state/KeyboardNavState.js +0 -216
  24. package/dist/esm/state/RowSelectionState.js +0 -72
  25. package/dist/esm/state/SelectionState.js +0 -109
  26. package/dist/esm/state/SideBarState.js +0 -41
  27. package/dist/esm/state/TableLayoutState.js +0 -97
  28. package/dist/esm/state/UndoRedoState.js +0 -71
  29. package/dist/esm/state/VirtualScrollState.js +0 -128
  30. package/dist/esm/types/columnTypes.js +0 -1
  31. package/dist/esm/types/gridTypes.js +0 -1
  32. package/dist/esm/types/index.js +0 -2
  33. package/dist/esm/utils/debounce.js +0 -2
  34. package/dist/esm/utils/getCellCoordinates.js +0 -15
  35. package/dist/esm/utils/index.js +0 -2
@@ -1,324 +0,0 @@
1
- import { flattenColumns, processClientSideData, exportToCsv as coreExportToCsv, getCellValue, deriveFilterOptionsFromData, mergeFilter, validateColumns, validateRowIds, } from '@alaarab/ogrid-core';
2
- import { EventEmitter } from './EventEmitter';
3
- export class GridState {
4
- constructor(options) {
5
- this.emitter = new EventEmitter();
6
- this._data = [];
7
- this._filters = {};
8
- this._isLoading = false;
9
- this._serverItems = [];
10
- this._serverTotalCount = 0;
11
- this._fetchId = 0; // Guards against stale fetch responses
12
- this._abortController = null; // Cancels in-flight fetch requests
13
- this._firstDataRendered = false;
14
- // Filter options for client-side data (used by sidebar filters panel & header filter popovers)
15
- this._filterOptions = {};
16
- // Column display order (array of columnIds)
17
- this._columnOrder = [];
18
- // Dirty-flag memoization for visibleColumnDefs getter
19
- this._visibleColsCache = null;
20
- this._visibleColsDirty = true;
21
- this._allColumns = options.columns;
22
- this._columns = flattenColumns(options.columns);
23
- this._getRowId = options.getRowId;
24
- this._data = options.data ?? [];
25
- this._dataSource = options.dataSource;
26
- this._page = options.page ?? 1;
27
- this._pageSize = options.pageSize ?? 20;
28
- this._sort = options.sort;
29
- this._filters = options.filters ?? {};
30
- this._visibleColumns = options.visibleColumns ?? new Set(this._columns.map(c => c.columnId));
31
- this._columnOrder = this._columns.map(c => c.columnId);
32
- this._onError = options.onError;
33
- this._onFirstDataRendered = options.onFirstDataRendered;
34
- this._rowHeight = options.rowHeight;
35
- this._ariaLabel = options.ariaLabel;
36
- this._stickyHeader = options.stickyHeader ?? true;
37
- this._fullScreen = options.fullScreen ?? false;
38
- // Derive initial filter options for client-side data
39
- if (!this._dataSource) {
40
- this._filterOptions = deriveFilterOptionsFromData(this._data, this._columns);
41
- }
42
- // Runtime validation — runs once at construction
43
- validateColumns(this._columns);
44
- if (!this._dataSource && this._data.length > 0) {
45
- validateRowIds(this._data, this._getRowId);
46
- this._firstDataRendered = true;
47
- }
48
- // If server-side, trigger initial fetch
49
- if (this._dataSource) {
50
- this._isLoading = true;
51
- this.fetchServerData();
52
- }
53
- }
54
- // --- Getters ---
55
- get data() { return this._data; }
56
- get page() { return this._page; }
57
- get pageSize() { return this._pageSize; }
58
- get sort() { return this._sort; }
59
- get filters() { return this._filters; }
60
- get visibleColumns() { return this._visibleColumns; }
61
- get isLoading() { return this._isLoading; }
62
- get columns() { return this._columns; }
63
- get allColumns() { return this._allColumns; }
64
- get getRowId() { return this._getRowId; }
65
- get isServerSide() { return this._dataSource != null; }
66
- get stickyHeader() { return this._stickyHeader; }
67
- get fullScreen() { return this._fullScreen; }
68
- get filterOptions() { return this._filterOptions; }
69
- get columnOrder() { return this._columnOrder; }
70
- get rowHeight() { return this._rowHeight; }
71
- get ariaLabel() { return this._ariaLabel; }
72
- /** Get the visible columns in display order (respects column reorder). Memoized via dirty flag. */
73
- get visibleColumnDefs() {
74
- if (!this._visibleColsDirty && this._visibleColsCache)
75
- return this._visibleColsCache;
76
- const visible = this._columns.filter(c => this._visibleColumns.has(c.columnId));
77
- if (this._columnOrder.length === 0) {
78
- this._visibleColsCache = visible;
79
- }
80
- else {
81
- const orderMap = new Map(this._columnOrder.map((id, idx) => [id, idx]));
82
- this._visibleColsCache = [...visible].sort((a, b) => {
83
- const ai = orderMap.get(a.columnId) ?? Infinity;
84
- const bi = orderMap.get(b.columnId) ?? Infinity;
85
- return ai - bi;
86
- });
87
- }
88
- this._visibleColsDirty = false;
89
- return this._visibleColsCache;
90
- }
91
- /** Get processed (sorted, filtered, paginated) items for current page. */
92
- getProcessedItems() {
93
- if (this.isServerSide) {
94
- return { items: this._serverItems, totalCount: this._serverTotalCount };
95
- }
96
- const filtered = processClientSideData(this._data, this._columns, this._filters, this._sort?.field, this._sort?.direction);
97
- const totalCount = filtered.length;
98
- const startIdx = (this._page - 1) * this._pageSize;
99
- const endIdx = startIdx + this._pageSize;
100
- const items = filtered.slice(startIdx, endIdx);
101
- return { items, totalCount };
102
- }
103
- // --- Server-side fetch ---
104
- fetchServerData() {
105
- if (!this._dataSource)
106
- return;
107
- // Cancel any in-flight request before starting a new one
108
- if (this._abortController) {
109
- this._abortController.abort();
110
- }
111
- const id = ++this._fetchId;
112
- this._abortController = new AbortController();
113
- const currentController = this._abortController;
114
- this._isLoading = true;
115
- this.emitter.emit('stateChange', { type: 'loading' });
116
- this._dataSource
117
- .fetchPage({
118
- page: this._page,
119
- pageSize: this._pageSize,
120
- sort: this._sort ? { field: this._sort.field, direction: this._sort.direction } : undefined,
121
- filters: this._filters,
122
- })
123
- .then((res) => {
124
- // Ignore if this request was superseded by a newer one
125
- if (id !== this._fetchId || currentController.signal.aborted)
126
- return;
127
- this._serverItems = res.items;
128
- this._serverTotalCount = res.totalCount;
129
- this._isLoading = false;
130
- if (!this._firstDataRendered && res.items.length > 0) {
131
- this._firstDataRendered = true;
132
- validateRowIds(res.items, this._getRowId);
133
- this._onFirstDataRendered?.();
134
- }
135
- this.emitter.emit('stateChange', { type: 'data' });
136
- })
137
- .catch((err) => {
138
- // Ignore if this request was superseded or aborted
139
- if (id !== this._fetchId || currentController.signal.aborted)
140
- return;
141
- this._onError?.(err);
142
- this._serverItems = [];
143
- this._serverTotalCount = 0;
144
- this._isLoading = false;
145
- this.emitter.emit('stateChange', { type: 'data' });
146
- });
147
- }
148
- // --- Setters ---
149
- setData(data) {
150
- this._data = data;
151
- if (!this.isServerSide) {
152
- this._filterOptions = deriveFilterOptionsFromData(data, this._columns);
153
- }
154
- this.emitter.emit('stateChange', { type: 'data' });
155
- }
156
- setPage(page) {
157
- this._page = page;
158
- if (this.isServerSide) {
159
- this.fetchServerData();
160
- }
161
- else {
162
- this.emitter.emit('stateChange', { type: 'page' });
163
- }
164
- }
165
- setPageSize(pageSize) {
166
- this._pageSize = pageSize;
167
- this._page = 1;
168
- if (this.isServerSide) {
169
- this.fetchServerData();
170
- }
171
- else {
172
- this.emitter.emit('stateChange', { type: 'page' });
173
- }
174
- }
175
- setSort(sort) {
176
- this._sort = sort;
177
- this._page = 1;
178
- if (this.isServerSide) {
179
- this.fetchServerData();
180
- }
181
- else {
182
- this.emitter.emit('stateChange', { type: 'sort' });
183
- }
184
- }
185
- toggleSort(field) {
186
- if (this._sort?.field === field) {
187
- this._sort = this._sort.direction === 'asc'
188
- ? { field, direction: 'desc' }
189
- : undefined;
190
- }
191
- else {
192
- this._sort = { field, direction: 'asc' };
193
- }
194
- this._page = 1;
195
- if (this.isServerSide) {
196
- this.fetchServerData();
197
- }
198
- else {
199
- this.emitter.emit('stateChange', { type: 'sort' });
200
- }
201
- }
202
- setFilter(key, value) {
203
- this._filters = mergeFilter(this._filters, key, value);
204
- this._page = 1;
205
- if (this.isServerSide) {
206
- this.fetchServerData();
207
- }
208
- else {
209
- this.emitter.emit('stateChange', { type: 'filter' });
210
- }
211
- }
212
- clearFilters() {
213
- this._filters = {};
214
- this._page = 1;
215
- if (this.isServerSide) {
216
- this.fetchServerData();
217
- }
218
- else {
219
- this.emitter.emit('stateChange', { type: 'filter' });
220
- }
221
- }
222
- setVisibleColumns(columns) {
223
- this._visibleColumns = columns;
224
- this._visibleColsDirty = true;
225
- this.emitter.emit('stateChange', { type: 'columns' });
226
- }
227
- setColumnOrder(order) {
228
- this._columnOrder = order;
229
- this._visibleColsDirty = true;
230
- this.emitter.emit('stateChange', { type: 'columns' });
231
- }
232
- setLoading(loading) {
233
- this._isLoading = loading;
234
- this.emitter.emit('stateChange', { type: 'loading' });
235
- }
236
- refreshData() {
237
- if (this.isServerSide) {
238
- this.fetchServerData();
239
- }
240
- }
241
- // --- Event subscription ---
242
- onStateChange(handler) {
243
- this.emitter.on('stateChange', handler);
244
- return () => this.emitter.off('stateChange', handler);
245
- }
246
- // --- API ---
247
- getApi() {
248
- return {
249
- setRowData: (data) => {
250
- if (!this.isServerSide)
251
- this.setData(data);
252
- },
253
- setLoading: (loading) => this.setLoading(loading),
254
- getColumnState: () => ({
255
- visibleColumns: Array.from(this._visibleColumns),
256
- sort: this._sort,
257
- filters: Object.keys(this._filters).length > 0 ? this._filters : undefined,
258
- }),
259
- applyColumnState: (state) => {
260
- if (state.visibleColumns)
261
- this._visibleColumns = new Set(state.visibleColumns);
262
- if (state.sort !== undefined)
263
- this._sort = state.sort;
264
- if (state.filters !== undefined)
265
- this._filters = state.filters ?? {};
266
- if (this.isServerSide) {
267
- this.fetchServerData();
268
- }
269
- else {
270
- this.emitter.emit('stateChange', { type: 'columns' });
271
- }
272
- },
273
- setFilterModel: (filters) => {
274
- this._filters = filters;
275
- this._page = 1;
276
- if (this.isServerSide) {
277
- this.fetchServerData();
278
- }
279
- else {
280
- this.emitter.emit('stateChange', { type: 'filter' });
281
- }
282
- },
283
- getSelectedRows: () => [],
284
- setSelectedRows: () => { },
285
- selectAll: () => { },
286
- deselectAll: () => { },
287
- clearFilters: () => this.clearFilters(),
288
- clearSort: () => this.setSort(undefined),
289
- resetGridState: () => {
290
- this.clearFilters();
291
- this.setSort(undefined);
292
- },
293
- getDisplayedRows: () => this.getProcessedItems().items,
294
- refreshData: () => this.refreshData(),
295
- // scrollToRow is wired by OGrid after construction when virtualScrollState is present.
296
- // This stub is replaced by OGrid.ts (see "Wire scrollToRow API method") for virtual scroll.
297
- // For non-virtual grids it remains a no-op (native browser scroll handles row visibility).
298
- scrollToRow: () => { },
299
- getColumnOrder: () => [...this._columnOrder],
300
- setColumnOrder: (order) => this.setColumnOrder(order),
301
- exportToCsv: (filename) => {
302
- const { items } = this.getProcessedItems();
303
- const cols = this.visibleColumnDefs.map(c => ({ columnId: c.columnId, name: c.name }));
304
- coreExportToCsv(items, cols, (item, colId) => {
305
- const col = this._columns.find(c => c.columnId === colId);
306
- if (!col)
307
- return '';
308
- const val = getCellValue(item, col);
309
- if (col.valueFormatter)
310
- return col.valueFormatter(val, item);
311
- return val != null ? String(val) : '';
312
- }, filename);
313
- },
314
- };
315
- }
316
- destroy() {
317
- // Cancel any in-flight fetch request
318
- if (this._abortController) {
319
- this._abortController.abort();
320
- this._abortController = null;
321
- }
322
- this.emitter.removeAllListeners();
323
- }
324
- }
@@ -1,213 +0,0 @@
1
- import { EventEmitter } from './EventEmitter';
2
- /**
3
- * Manages header filter popover state for all columns.
4
- * Equivalent of React's useColumnHeaderFilterState, but class-based.
5
- */
6
- export class HeaderFilterState {
7
- constructor(onFilterChange) {
8
- this.emitter = new EventEmitter();
9
- // Which column's filter is currently open (null = none)
10
- this._openColumnId = null;
11
- // Temporary state for the currently open filter popover
12
- this._tempTextValue = '';
13
- this._tempSelected = new Set();
14
- this._tempDateFrom = '';
15
- this._tempDateTo = '';
16
- this._searchText = '';
17
- // Popover position
18
- this._popoverPosition = null;
19
- // External references
20
- this._filters = {};
21
- this._filterOptions = {};
22
- // Click-outside handler
23
- this._clickOutsideHandler = null;
24
- this._escapeHandler = null;
25
- this._popoverEl = null;
26
- this._headerEl = null;
27
- this._onFilterChange = onFilterChange;
28
- }
29
- get openColumnId() { return this._openColumnId; }
30
- get tempTextValue() { return this._tempTextValue; }
31
- get tempSelected() { return this._tempSelected; }
32
- get tempDateFrom() { return this._tempDateFrom; }
33
- get tempDateTo() { return this._tempDateTo; }
34
- get searchText() { return this._searchText; }
35
- get popoverPosition() { return this._popoverPosition; }
36
- setFilters(filters) {
37
- this._filters = filters;
38
- }
39
- setFilterOptions(options) {
40
- this._filterOptions = options;
41
- }
42
- /** Allow OGrid to update the popover element reference after rendering (for click-outside detection). */
43
- setPopoverEl(el) {
44
- this._popoverEl = el;
45
- }
46
- getFilterOptions(filterField) {
47
- return this._filterOptions[filterField] ?? [];
48
- }
49
- getFilteredOptions(filterField) {
50
- const options = this.getFilterOptions(filterField);
51
- if (!this._searchText)
52
- return options;
53
- const lower = this._searchText.toLowerCase();
54
- return options.filter(opt => opt.toLowerCase().includes(lower));
55
- }
56
- hasActiveFilter(config) {
57
- const fv = this._filters[config.filterField];
58
- if (!fv)
59
- return false;
60
- if (fv.type === 'text')
61
- return fv.value.trim().length > 0;
62
- if (fv.type === 'multiSelect')
63
- return fv.value.length > 0;
64
- if (fv.type === 'date')
65
- return !!(fv.value.from || fv.value.to);
66
- if (fv.type === 'people')
67
- return !!fv.value;
68
- return false;
69
- }
70
- /**
71
- * Open a filter popover for a specific column.
72
- */
73
- open(columnId, config, headerEl, popoverEl) {
74
- // Close any existing popover first
75
- if (this._openColumnId) {
76
- this.close();
77
- }
78
- this._openColumnId = columnId;
79
- this._headerEl = headerEl;
80
- this._popoverEl = popoverEl;
81
- // Initialize temp state from current filter values
82
- const fv = this._filters[config.filterField];
83
- if (config.filterType === 'text') {
84
- this._tempTextValue = fv?.type === 'text' ? fv.value : '';
85
- }
86
- else if (config.filterType === 'multiSelect') {
87
- this._tempSelected = new Set(fv?.type === 'multiSelect' ? fv.value : []);
88
- }
89
- else if (config.filterType === 'date') {
90
- const dv = fv?.type === 'date' ? fv.value : {};
91
- this._tempDateFrom = dv.from ?? '';
92
- this._tempDateTo = dv.to ?? '';
93
- }
94
- this._searchText = '';
95
- // Compute position
96
- const rect = headerEl.getBoundingClientRect();
97
- this._popoverPosition = { top: rect.bottom + 4, left: rect.left };
98
- // Set up click-outside listener
99
- this._clickOutsideHandler = (e) => {
100
- const target = e.target;
101
- if (this._popoverEl && !this._popoverEl.contains(target) &&
102
- this._headerEl && !this._headerEl.contains(target)) {
103
- this.close();
104
- }
105
- };
106
- this._escapeHandler = (e) => {
107
- if (e.key === 'Escape') {
108
- e.preventDefault();
109
- e.stopPropagation();
110
- this.close();
111
- }
112
- };
113
- setTimeout(() => {
114
- if (this._clickOutsideHandler) {
115
- document.addEventListener('mousedown', this._clickOutsideHandler, { passive: true });
116
- }
117
- }, 0);
118
- if (this._escapeHandler) {
119
- document.addEventListener('keydown', this._escapeHandler, true);
120
- }
121
- this.emitter.emit('change');
122
- }
123
- close() {
124
- this._openColumnId = null;
125
- this._popoverPosition = null;
126
- this._popoverEl = null;
127
- this._headerEl = null;
128
- if (this._clickOutsideHandler) {
129
- document.removeEventListener('mousedown', this._clickOutsideHandler);
130
- this._clickOutsideHandler = null;
131
- }
132
- if (this._escapeHandler) {
133
- document.removeEventListener('keydown', this._escapeHandler, true);
134
- this._escapeHandler = null;
135
- }
136
- this.emitter.emit('change');
137
- }
138
- // --- Temp state setters ---
139
- setTempTextValue(v) {
140
- this._tempTextValue = v;
141
- this.emitter.emit('change');
142
- }
143
- setSearchText(v) {
144
- this._searchText = v;
145
- this.emitter.emit('change');
146
- }
147
- setTempDateFrom(v) {
148
- this._tempDateFrom = v;
149
- this.emitter.emit('change');
150
- }
151
- setTempDateTo(v) {
152
- this._tempDateTo = v;
153
- this.emitter.emit('change');
154
- }
155
- // --- Checkbox handlers ---
156
- handleCheckboxChange(option, checked) {
157
- const next = new Set(this._tempSelected);
158
- if (checked)
159
- next.add(option);
160
- else
161
- next.delete(option);
162
- this._tempSelected = next;
163
- this.emitter.emit('change');
164
- }
165
- handleSelectAll(filterField) {
166
- this._tempSelected = new Set(this.getFilterOptions(filterField));
167
- this.emitter.emit('change');
168
- }
169
- handleClearSelection() {
170
- this._tempSelected = new Set();
171
- this.emitter.emit('change');
172
- }
173
- // --- Apply/Clear ---
174
- applyTextFilter(filterField) {
175
- const value = this._tempTextValue.trim();
176
- this._onFilterChange(filterField, value ? { type: 'text', value } : undefined);
177
- this.close();
178
- }
179
- clearTextFilter(filterField) {
180
- this._tempTextValue = '';
181
- this._onFilterChange(filterField, undefined);
182
- this.close();
183
- }
184
- applyMultiSelectFilter(filterField) {
185
- const arr = Array.from(this._tempSelected);
186
- this._onFilterChange(filterField, arr.length > 0 ? { type: 'multiSelect', value: arr } : undefined);
187
- this.close();
188
- }
189
- applyDateFilter(filterField) {
190
- const from = this._tempDateFrom || undefined;
191
- const to = this._tempDateTo || undefined;
192
- this._onFilterChange(filterField, from || to ? { type: 'date', value: { from, to } } : undefined);
193
- this.close();
194
- }
195
- clearDateFilter(filterField) {
196
- this._tempDateFrom = '';
197
- this._tempDateTo = '';
198
- this._onFilterChange(filterField, undefined);
199
- this.close();
200
- }
201
- clearFilter(filterField) {
202
- this._onFilterChange(filterField, undefined);
203
- this.close();
204
- }
205
- onChange(handler) {
206
- this.emitter.on('change', handler);
207
- return () => this.emitter.off('change', handler);
208
- }
209
- destroy() {
210
- this.close();
211
- this.emitter.removeAllListeners();
212
- }
213
- }