@alaarab/ogrid-js 2.0.0-beta

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 (55) hide show
  1. package/dist/esm/OGrid.js +654 -0
  2. package/dist/esm/components/ColumnChooser.js +68 -0
  3. package/dist/esm/components/ContextMenu.js +122 -0
  4. package/dist/esm/components/HeaderFilter.js +281 -0
  5. package/dist/esm/components/InlineCellEditor.js +278 -0
  6. package/dist/esm/components/MarchingAntsOverlay.js +170 -0
  7. package/dist/esm/components/PaginationControls.js +85 -0
  8. package/dist/esm/components/SideBar.js +353 -0
  9. package/dist/esm/components/StatusBar.js +34 -0
  10. package/dist/esm/index.js +26 -0
  11. package/dist/esm/renderer/TableRenderer.js +414 -0
  12. package/dist/esm/state/ClipboardState.js +171 -0
  13. package/dist/esm/state/ColumnPinningState.js +78 -0
  14. package/dist/esm/state/ColumnResizeState.js +55 -0
  15. package/dist/esm/state/EventEmitter.js +27 -0
  16. package/dist/esm/state/FillHandleState.js +218 -0
  17. package/dist/esm/state/GridState.js +261 -0
  18. package/dist/esm/state/HeaderFilterState.js +205 -0
  19. package/dist/esm/state/KeyboardNavState.js +374 -0
  20. package/dist/esm/state/RowSelectionState.js +81 -0
  21. package/dist/esm/state/SelectionState.js +102 -0
  22. package/dist/esm/state/SideBarState.js +41 -0
  23. package/dist/esm/state/TableLayoutState.js +95 -0
  24. package/dist/esm/state/UndoRedoState.js +82 -0
  25. package/dist/esm/types/columnTypes.js +1 -0
  26. package/dist/esm/types/gridTypes.js +1 -0
  27. package/dist/esm/types/index.js +2 -0
  28. package/dist/types/OGrid.d.ts +60 -0
  29. package/dist/types/components/ColumnChooser.d.ts +14 -0
  30. package/dist/types/components/ContextMenu.d.ts +17 -0
  31. package/dist/types/components/HeaderFilter.d.ts +24 -0
  32. package/dist/types/components/InlineCellEditor.d.ts +24 -0
  33. package/dist/types/components/MarchingAntsOverlay.d.ts +25 -0
  34. package/dist/types/components/PaginationControls.d.ts +9 -0
  35. package/dist/types/components/SideBar.d.ts +35 -0
  36. package/dist/types/components/StatusBar.d.ts +8 -0
  37. package/dist/types/index.d.ts +26 -0
  38. package/dist/types/renderer/TableRenderer.d.ts +59 -0
  39. package/dist/types/state/ClipboardState.d.ts +35 -0
  40. package/dist/types/state/ColumnPinningState.d.ts +36 -0
  41. package/dist/types/state/ColumnResizeState.d.ts +23 -0
  42. package/dist/types/state/EventEmitter.d.ts +9 -0
  43. package/dist/types/state/FillHandleState.d.ts +51 -0
  44. package/dist/types/state/GridState.d.ts +68 -0
  45. package/dist/types/state/HeaderFilterState.d.ts +64 -0
  46. package/dist/types/state/KeyboardNavState.d.ts +29 -0
  47. package/dist/types/state/RowSelectionState.d.ts +23 -0
  48. package/dist/types/state/SelectionState.d.ts +37 -0
  49. package/dist/types/state/SideBarState.d.ts +19 -0
  50. package/dist/types/state/TableLayoutState.d.ts +33 -0
  51. package/dist/types/state/UndoRedoState.d.ts +28 -0
  52. package/dist/types/types/columnTypes.d.ts +28 -0
  53. package/dist/types/types/gridTypes.d.ts +69 -0
  54. package/dist/types/types/index.d.ts +2 -0
  55. package/package.json +29 -0
@@ -0,0 +1,353 @@
1
+ const PANEL_WIDTH = 240;
2
+ const TAB_WIDTH = 36;
3
+ const PANEL_LABELS = {
4
+ columns: 'Columns',
5
+ filters: 'Filters',
6
+ };
7
+ const PANEL_ICONS = {
8
+ columns: '\u2261',
9
+ filters: '\u2A65',
10
+ };
11
+ export class SideBar {
12
+ constructor(container, state) {
13
+ this.el = null;
14
+ this.config = null;
15
+ this.container = container;
16
+ this.state = state;
17
+ }
18
+ setConfig(config) {
19
+ this.config = config;
20
+ }
21
+ render() {
22
+ if (this.el)
23
+ this.el.remove();
24
+ if (!this.state.isEnabled || !this.config)
25
+ return;
26
+ this.el = document.createElement('div');
27
+ this.el.className = 'ogrid-sidebar';
28
+ this.el.setAttribute('role', 'complementary');
29
+ this.el.setAttribute('aria-label', 'Side bar');
30
+ this.el.style.display = 'flex';
31
+ this.el.style.flexDirection = 'row';
32
+ this.el.style.flexShrink = '0';
33
+ const position = this.state.position;
34
+ const tabStrip = this.createTabStrip(position);
35
+ const panel = this.createPanel(position);
36
+ if (position === 'left') {
37
+ this.el.appendChild(tabStrip);
38
+ if (panel)
39
+ this.el.appendChild(panel);
40
+ }
41
+ else {
42
+ if (panel)
43
+ this.el.appendChild(panel);
44
+ this.el.appendChild(tabStrip);
45
+ }
46
+ this.container.appendChild(this.el);
47
+ }
48
+ createTabStrip(position) {
49
+ const strip = document.createElement('div');
50
+ strip.style.display = 'flex';
51
+ strip.style.flexDirection = 'column';
52
+ strip.style.width = `${TAB_WIDTH}px`;
53
+ strip.style.background = 'var(--ogrid-header-bg, #f5f5f5)';
54
+ strip.setAttribute('role', 'tablist');
55
+ strip.setAttribute('aria-label', 'Side bar tabs');
56
+ if (position === 'right') {
57
+ strip.style.borderLeft = '1px solid var(--ogrid-border, #e0e0e0)';
58
+ }
59
+ else {
60
+ strip.style.borderRight = '1px solid var(--ogrid-border, #e0e0e0)';
61
+ }
62
+ for (const panelId of this.state.panels) {
63
+ const btn = document.createElement('button');
64
+ btn.setAttribute('role', 'tab');
65
+ btn.setAttribute('aria-selected', String(this.state.activePanel === panelId));
66
+ btn.setAttribute('aria-label', PANEL_LABELS[panelId]);
67
+ btn.title = PANEL_LABELS[panelId];
68
+ btn.textContent = PANEL_ICONS[panelId];
69
+ btn.className = 'ogrid-sidebar-tab';
70
+ btn.style.width = `${TAB_WIDTH}px`;
71
+ btn.style.height = `${TAB_WIDTH}px`;
72
+ btn.style.border = 'none';
73
+ btn.style.cursor = 'pointer';
74
+ btn.style.color = 'var(--ogrid-fg, #242424)';
75
+ btn.style.fontSize = '14px';
76
+ btn.style.display = 'flex';
77
+ btn.style.alignItems = 'center';
78
+ btn.style.justifyContent = 'center';
79
+ if (this.state.activePanel === panelId) {
80
+ btn.style.background = 'var(--ogrid-bg, #fff)';
81
+ btn.style.fontWeight = 'bold';
82
+ }
83
+ else {
84
+ btn.style.background = 'transparent';
85
+ btn.style.fontWeight = 'normal';
86
+ }
87
+ btn.addEventListener('click', () => {
88
+ this.state.toggle(panelId);
89
+ });
90
+ strip.appendChild(btn);
91
+ }
92
+ return strip;
93
+ }
94
+ createPanel(position) {
95
+ if (!this.state.isOpen || !this.state.activePanel)
96
+ return null;
97
+ const panelContainer = document.createElement('div');
98
+ panelContainer.setAttribute('role', 'tabpanel');
99
+ panelContainer.setAttribute('aria-label', PANEL_LABELS[this.state.activePanel]);
100
+ panelContainer.className = 'ogrid-sidebar-panel';
101
+ panelContainer.style.width = `${PANEL_WIDTH}px`;
102
+ panelContainer.style.display = 'flex';
103
+ panelContainer.style.flexDirection = 'column';
104
+ panelContainer.style.overflow = 'hidden';
105
+ panelContainer.style.background = 'var(--ogrid-bg, #fff)';
106
+ panelContainer.style.color = 'var(--ogrid-fg, #242424)';
107
+ if (position === 'right') {
108
+ panelContainer.style.borderLeft = '1px solid var(--ogrid-border, #e0e0e0)';
109
+ }
110
+ else {
111
+ panelContainer.style.borderRight = '1px solid var(--ogrid-border, #e0e0e0)';
112
+ }
113
+ // Header
114
+ const header = document.createElement('div');
115
+ header.style.display = 'flex';
116
+ header.style.justifyContent = 'space-between';
117
+ header.style.alignItems = 'center';
118
+ header.style.padding = '8px 12px';
119
+ header.style.borderBottom = '1px solid var(--ogrid-border, #e0e0e0)';
120
+ header.style.fontWeight = '600';
121
+ const title = document.createElement('span');
122
+ title.textContent = PANEL_LABELS[this.state.activePanel];
123
+ header.appendChild(title);
124
+ const closeBtn = document.createElement('button');
125
+ closeBtn.innerHTML = '×';
126
+ closeBtn.setAttribute('aria-label', 'Close panel');
127
+ closeBtn.style.border = 'none';
128
+ closeBtn.style.background = 'transparent';
129
+ closeBtn.style.cursor = 'pointer';
130
+ closeBtn.style.fontSize = '16px';
131
+ closeBtn.style.color = 'var(--ogrid-fg, #242424)';
132
+ closeBtn.addEventListener('click', () => this.state.close());
133
+ header.appendChild(closeBtn);
134
+ panelContainer.appendChild(header);
135
+ // Body
136
+ const body = document.createElement('div');
137
+ body.style.flex = '1';
138
+ body.style.overflowY = 'auto';
139
+ body.style.padding = '8px 12px';
140
+ if (this.state.activePanel === 'columns') {
141
+ this.renderColumnsPanel(body);
142
+ }
143
+ else if (this.state.activePanel === 'filters') {
144
+ this.renderFiltersPanel(body);
145
+ }
146
+ panelContainer.appendChild(body);
147
+ return panelContainer;
148
+ }
149
+ renderColumnsPanel(body) {
150
+ if (!this.config)
151
+ return;
152
+ const { columns, visibleColumns, onVisibilityChange, onSetVisibleColumns } = this.config;
153
+ const allVisible = columns.every(c => visibleColumns.has(c.columnId));
154
+ // Button row
155
+ const btnRow = document.createElement('div');
156
+ btnRow.style.display = 'flex';
157
+ btnRow.style.gap = '8px';
158
+ btnRow.style.marginBottom = '8px';
159
+ const selectAllBtn = document.createElement('button');
160
+ selectAllBtn.textContent = 'Select All';
161
+ selectAllBtn.disabled = allVisible;
162
+ selectAllBtn.className = 'ogrid-sidebar-action-btn';
163
+ this.applyActionButtonStyle(selectAllBtn);
164
+ selectAllBtn.addEventListener('click', () => {
165
+ const next = new Set(visibleColumns);
166
+ columns.forEach(c => next.add(c.columnId));
167
+ onSetVisibleColumns(next);
168
+ });
169
+ btnRow.appendChild(selectAllBtn);
170
+ const clearAllBtn = document.createElement('button');
171
+ clearAllBtn.textContent = 'Clear All';
172
+ clearAllBtn.className = 'ogrid-sidebar-action-btn';
173
+ this.applyActionButtonStyle(clearAllBtn);
174
+ clearAllBtn.addEventListener('click', () => {
175
+ const next = new Set();
176
+ columns.forEach(c => {
177
+ if (c.required && visibleColumns.has(c.columnId))
178
+ next.add(c.columnId);
179
+ });
180
+ onSetVisibleColumns(next);
181
+ });
182
+ btnRow.appendChild(clearAllBtn);
183
+ body.appendChild(btnRow);
184
+ // Column checkboxes
185
+ for (const col of columns) {
186
+ const label = document.createElement('label');
187
+ label.style.display = 'flex';
188
+ label.style.alignItems = 'center';
189
+ label.style.gap = '6px';
190
+ label.style.padding = '2px 0';
191
+ label.style.cursor = 'pointer';
192
+ const checkbox = document.createElement('input');
193
+ checkbox.type = 'checkbox';
194
+ checkbox.checked = visibleColumns.has(col.columnId);
195
+ checkbox.disabled = !!col.required;
196
+ checkbox.addEventListener('change', () => {
197
+ onVisibilityChange(col.columnId, checkbox.checked);
198
+ });
199
+ const text = document.createElement('span');
200
+ text.textContent = col.name;
201
+ label.appendChild(checkbox);
202
+ label.appendChild(text);
203
+ body.appendChild(label);
204
+ }
205
+ }
206
+ renderFiltersPanel(body) {
207
+ if (!this.config)
208
+ return;
209
+ const { filterableColumns, filters, onFilterChange, filterOptions } = this.config;
210
+ if (filterableColumns.length === 0) {
211
+ const msg = document.createElement('div');
212
+ msg.style.color = 'var(--ogrid-muted, #999)';
213
+ msg.style.fontStyle = 'italic';
214
+ msg.textContent = 'No filterable columns';
215
+ body.appendChild(msg);
216
+ return;
217
+ }
218
+ for (const col of filterableColumns) {
219
+ const group = document.createElement('div');
220
+ group.style.marginBottom = '12px';
221
+ const labelEl = document.createElement('div');
222
+ labelEl.style.fontWeight = '500';
223
+ labelEl.style.marginBottom = '4px';
224
+ labelEl.style.fontSize = '13px';
225
+ labelEl.textContent = col.name;
226
+ group.appendChild(labelEl);
227
+ if (col.filterType === 'text') {
228
+ const input = document.createElement('input');
229
+ input.type = 'text';
230
+ const fv = filters[col.filterField];
231
+ input.value = fv?.type === 'text' ? fv.value : '';
232
+ input.placeholder = `Filter ${col.name}...`;
233
+ input.setAttribute('aria-label', `Filter ${col.name}`);
234
+ this.applyTextInputStyle(input);
235
+ input.addEventListener('input', () => {
236
+ onFilterChange(col.filterField, input.value ? { type: 'text', value: input.value } : undefined);
237
+ });
238
+ group.appendChild(input);
239
+ }
240
+ else if (col.filterType === 'date') {
241
+ const container = document.createElement('div');
242
+ container.style.display = 'flex';
243
+ container.style.flexDirection = 'column';
244
+ container.style.gap = '4px';
245
+ const fvDate = filters[col.filterField];
246
+ const existingDate = fvDate?.type === 'date' ? fvDate.value : {};
247
+ // From date
248
+ const fromLabel = document.createElement('label');
249
+ fromLabel.style.display = 'flex';
250
+ fromLabel.style.alignItems = 'center';
251
+ fromLabel.style.gap = '4px';
252
+ fromLabel.style.fontSize = '12px';
253
+ fromLabel.textContent = 'From: ';
254
+ const fromInput = document.createElement('input');
255
+ fromInput.type = 'date';
256
+ fromInput.value = existingDate.from ?? '';
257
+ fromInput.setAttribute('aria-label', `${col.name} from date`);
258
+ this.applyDateInputStyle(fromInput);
259
+ fromLabel.appendChild(fromInput);
260
+ container.appendChild(fromLabel);
261
+ // To date
262
+ const toLabel = document.createElement('label');
263
+ toLabel.style.display = 'flex';
264
+ toLabel.style.alignItems = 'center';
265
+ toLabel.style.gap = '4px';
266
+ toLabel.style.fontSize = '12px';
267
+ toLabel.textContent = 'To: ';
268
+ const toInput = document.createElement('input');
269
+ toInput.type = 'date';
270
+ toInput.value = existingDate.to ?? '';
271
+ toInput.setAttribute('aria-label', `${col.name} to date`);
272
+ this.applyDateInputStyle(toInput);
273
+ toLabel.appendChild(toInput);
274
+ container.appendChild(toLabel);
275
+ const updateDate = () => {
276
+ const from = fromInput.value || undefined;
277
+ const to = toInput.value || undefined;
278
+ onFilterChange(col.filterField, from || to ? { type: 'date', value: { from, to } } : undefined);
279
+ };
280
+ fromInput.addEventListener('change', updateDate);
281
+ toInput.addEventListener('change', updateDate);
282
+ group.appendChild(container);
283
+ }
284
+ else if (col.filterType === 'multiSelect') {
285
+ const opts = filterOptions[col.filterField] ?? [];
286
+ const container = document.createElement('div');
287
+ container.style.maxHeight = '120px';
288
+ container.style.overflowY = 'auto';
289
+ container.setAttribute('role', 'group');
290
+ container.setAttribute('aria-label', `${col.name} options`);
291
+ const fvMulti = filters[col.filterField];
292
+ const currentValues = fvMulti?.type === 'multiSelect' ? fvMulti.value : [];
293
+ for (const opt of opts) {
294
+ const optLabel = document.createElement('label');
295
+ optLabel.style.display = 'flex';
296
+ optLabel.style.alignItems = 'center';
297
+ optLabel.style.gap = '4px';
298
+ optLabel.style.padding = '1px 0';
299
+ optLabel.style.cursor = 'pointer';
300
+ optLabel.style.fontSize = '13px';
301
+ const checkbox = document.createElement('input');
302
+ checkbox.type = 'checkbox';
303
+ checkbox.checked = currentValues.includes(opt);
304
+ checkbox.addEventListener('change', () => {
305
+ const currentFv = filters[col.filterField];
306
+ const current = currentFv?.type === 'multiSelect' ? [...currentFv.value] : [];
307
+ const next = checkbox.checked
308
+ ? [...current, opt]
309
+ : current.filter(v => v !== opt);
310
+ onFilterChange(col.filterField, next.length > 0 ? { type: 'multiSelect', value: next } : undefined);
311
+ });
312
+ const text = document.createElement('span');
313
+ text.textContent = opt;
314
+ optLabel.appendChild(checkbox);
315
+ optLabel.appendChild(text);
316
+ container.appendChild(optLabel);
317
+ }
318
+ group.appendChild(container);
319
+ }
320
+ body.appendChild(group);
321
+ }
322
+ }
323
+ applyActionButtonStyle(btn) {
324
+ btn.style.flex = '1';
325
+ btn.style.cursor = 'pointer';
326
+ btn.style.background = 'var(--ogrid-bg-subtle, #f3f2f1)';
327
+ btn.style.color = 'var(--ogrid-fg, #242424)';
328
+ btn.style.border = '1px solid var(--ogrid-border, #e0e0e0)';
329
+ btn.style.borderRadius = '4px';
330
+ btn.style.padding = '4px 8px';
331
+ }
332
+ applyTextInputStyle(input) {
333
+ input.style.width = '100%';
334
+ input.style.boxSizing = 'border-box';
335
+ input.style.padding = '4px 6px';
336
+ input.style.background = 'var(--ogrid-bg, #fff)';
337
+ input.style.color = 'var(--ogrid-fg, #242424)';
338
+ input.style.border = '1px solid var(--ogrid-border, #e0e0e0)';
339
+ input.style.borderRadius = '4px';
340
+ }
341
+ applyDateInputStyle(input) {
342
+ input.style.flex = '1';
343
+ input.style.padding = '2px 4px';
344
+ input.style.background = 'var(--ogrid-bg, #fff)';
345
+ input.style.color = 'var(--ogrid-fg, #242424)';
346
+ input.style.border = '1px solid var(--ogrid-border, #e0e0e0)';
347
+ input.style.borderRadius = '4px';
348
+ }
349
+ destroy() {
350
+ this.el?.remove();
351
+ this.el = null;
352
+ }
353
+ }
@@ -0,0 +1,34 @@
1
+ import { getStatusBarParts } from '@alaarab/ogrid-core';
2
+ export class StatusBar {
3
+ constructor(container) {
4
+ this.el = null;
5
+ this.container = container;
6
+ }
7
+ render(props) {
8
+ if (this.el)
9
+ this.el.remove();
10
+ const parts = getStatusBarParts(props);
11
+ if (parts.length === 0 && !props.aggregation)
12
+ return;
13
+ this.el = document.createElement('div');
14
+ this.el.className = 'ogrid-status-bar';
15
+ for (const part of parts) {
16
+ const span = document.createElement('span');
17
+ span.className = 'ogrid-status-part';
18
+ span.textContent = `${part.label}: ${part.value}`;
19
+ this.el.appendChild(span);
20
+ }
21
+ if (props.aggregation) {
22
+ const agg = props.aggregation;
23
+ const aggSpan = document.createElement('span');
24
+ aggSpan.className = 'ogrid-status-aggregation';
25
+ aggSpan.textContent = `Sum: ${agg.sum.toLocaleString()} | Avg: ${agg.avg.toFixed(2)} | Min: ${agg.min} | Max: ${agg.max} | Count: ${agg.count}`;
26
+ this.el.appendChild(aggSpan);
27
+ }
28
+ this.container.appendChild(this.el);
29
+ }
30
+ destroy() {
31
+ this.el?.remove();
32
+ this.el = null;
33
+ }
34
+ }
@@ -0,0 +1,26 @@
1
+ // Re-export core types + utils
2
+ export * from '@alaarab/ogrid-core';
3
+ // Classes
4
+ export { OGrid } from './OGrid';
5
+ export { GridState } from './state/GridState';
6
+ export { EventEmitter } from './state/EventEmitter';
7
+ export { SelectionState } from './state/SelectionState';
8
+ export { KeyboardNavState } from './state/KeyboardNavState';
9
+ export { ClipboardState } from './state/ClipboardState';
10
+ export { UndoRedoState } from './state/UndoRedoState';
11
+ export { ColumnResizeState } from './state/ColumnResizeState';
12
+ export { TableLayoutState } from './state/TableLayoutState';
13
+ export { TableRenderer } from './renderer/TableRenderer';
14
+ export { PaginationControls } from './components/PaginationControls';
15
+ export { StatusBar } from './components/StatusBar';
16
+ export { ColumnChooser } from './components/ColumnChooser';
17
+ export { InlineCellEditor } from './components/InlineCellEditor';
18
+ export { ContextMenu } from './components/ContextMenu';
19
+ export { FillHandleState } from './state/FillHandleState';
20
+ export { RowSelectionState } from './state/RowSelectionState';
21
+ export { ColumnPinningState } from './state/ColumnPinningState';
22
+ export { MarchingAntsOverlay } from './components/MarchingAntsOverlay';
23
+ export { SideBarState } from './state/SideBarState';
24
+ export { HeaderFilterState } from './state/HeaderFilterState';
25
+ export { SideBar } from './components/SideBar';
26
+ export { HeaderFilter } from './components/HeaderFilter';