@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,68 @@
1
+ export class ColumnChooser {
2
+ constructor(container, state) {
3
+ this.el = null;
4
+ this.dropdown = null;
5
+ this.isOpen = false;
6
+ this.container = container;
7
+ this.state = state;
8
+ }
9
+ render() {
10
+ if (this.el)
11
+ this.el.remove();
12
+ this.el = document.createElement('div');
13
+ this.el.className = 'ogrid-column-chooser';
14
+ const btn = document.createElement('button');
15
+ btn.className = 'ogrid-column-chooser-btn';
16
+ btn.textContent = 'Columns';
17
+ btn.addEventListener('click', () => this.toggle());
18
+ this.el.appendChild(btn);
19
+ this.container.appendChild(this.el);
20
+ }
21
+ toggle() {
22
+ if (this.isOpen) {
23
+ this.close();
24
+ }
25
+ else {
26
+ this.open();
27
+ }
28
+ }
29
+ open() {
30
+ if (this.dropdown)
31
+ return;
32
+ this.isOpen = true;
33
+ this.dropdown = document.createElement('div');
34
+ this.dropdown.className = 'ogrid-column-chooser-dropdown';
35
+ for (const col of this.state.columns) {
36
+ const label = document.createElement('label');
37
+ label.className = 'ogrid-column-chooser-item';
38
+ const checkbox = document.createElement('input');
39
+ checkbox.type = 'checkbox';
40
+ checkbox.checked = this.state.visibleColumns.has(col.columnId);
41
+ checkbox.disabled = !!col.required;
42
+ checkbox.addEventListener('change', () => {
43
+ const next = new Set(this.state.visibleColumns);
44
+ if (checkbox.checked) {
45
+ next.add(col.columnId);
46
+ }
47
+ else {
48
+ next.delete(col.columnId);
49
+ }
50
+ this.state.setVisibleColumns(next);
51
+ });
52
+ label.appendChild(checkbox);
53
+ label.appendChild(document.createTextNode(' ' + col.name));
54
+ this.dropdown.appendChild(label);
55
+ }
56
+ this.el.appendChild(this.dropdown);
57
+ }
58
+ close() {
59
+ this.isOpen = false;
60
+ this.dropdown?.remove();
61
+ this.dropdown = null;
62
+ }
63
+ destroy() {
64
+ this.close();
65
+ this.el?.remove();
66
+ this.el = null;
67
+ }
68
+ }
@@ -0,0 +1,122 @@
1
+ import { GRID_CONTEXT_MENU_ITEMS, formatShortcut } from '@alaarab/ogrid-core';
2
+ const MENU_STYLE = {
3
+ position: 'fixed',
4
+ backgroundColor: 'white',
5
+ border: '1px solid #ccc',
6
+ boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
7
+ zIndex: '10000',
8
+ minWidth: '180px',
9
+ padding: '4px 0',
10
+ fontFamily: 'system-ui, -apple-system, sans-serif',
11
+ fontSize: '14px',
12
+ };
13
+ const ITEM_STYLE = {
14
+ padding: '6px 12px',
15
+ cursor: 'pointer',
16
+ display: 'flex',
17
+ justifyContent: 'space-between',
18
+ alignItems: 'center',
19
+ };
20
+ const DIVIDER_STYLE = {
21
+ height: '1px',
22
+ backgroundColor: '#e0e0e0',
23
+ margin: '4px 0',
24
+ };
25
+ export class ContextMenu {
26
+ constructor() {
27
+ this.menu = null;
28
+ this.handlers = null;
29
+ }
30
+ show(x, y, handlers, canUndo, canRedo, selectionRange) {
31
+ this.close();
32
+ this.handlers = handlers;
33
+ this.menu = document.createElement('div');
34
+ Object.assign(this.menu.style, MENU_STYLE);
35
+ this.menu.style.left = `${x}px`;
36
+ this.menu.style.top = `${y}px`;
37
+ for (const item of GRID_CONTEXT_MENU_ITEMS) {
38
+ if (item.dividerBefore) {
39
+ const divider = document.createElement('div');
40
+ Object.assign(divider.style, DIVIDER_STYLE);
41
+ this.menu.appendChild(divider);
42
+ }
43
+ const menuItem = document.createElement('div');
44
+ Object.assign(menuItem.style, ITEM_STYLE);
45
+ const label = document.createElement('span');
46
+ label.textContent = item.label;
47
+ menuItem.appendChild(label);
48
+ if (item.shortcut) {
49
+ const shortcut = document.createElement('span');
50
+ shortcut.textContent = formatShortcut(item.shortcut);
51
+ shortcut.style.marginLeft = '20px';
52
+ shortcut.style.color = '#666';
53
+ shortcut.style.fontSize = '12px';
54
+ menuItem.appendChild(shortcut);
55
+ }
56
+ const isDisabled = (item.id === 'undo' && !canUndo) ||
57
+ (item.id === 'redo' && !canRedo) ||
58
+ (item.disabledWhenNoSelection && selectionRange == null);
59
+ if (isDisabled) {
60
+ menuItem.style.color = '#aaa';
61
+ menuItem.style.cursor = 'not-allowed';
62
+ }
63
+ else {
64
+ menuItem.addEventListener('mouseenter', () => {
65
+ menuItem.style.backgroundColor = '#f0f0f0';
66
+ });
67
+ menuItem.addEventListener('mouseleave', () => {
68
+ menuItem.style.backgroundColor = 'white';
69
+ });
70
+ menuItem.addEventListener('click', () => {
71
+ this.handleItemClick(item.id);
72
+ });
73
+ }
74
+ this.menu.appendChild(menuItem);
75
+ }
76
+ document.body.appendChild(this.menu);
77
+ const handleClickOutside = (e) => {
78
+ if (this.menu && !this.menu.contains(e.target)) {
79
+ this.close();
80
+ document.removeEventListener('mousedown', handleClickOutside);
81
+ }
82
+ };
83
+ setTimeout(() => {
84
+ document.addEventListener('mousedown', handleClickOutside);
85
+ }, 0);
86
+ }
87
+ close() {
88
+ if (this.menu) {
89
+ this.menu.remove();
90
+ this.menu = null;
91
+ }
92
+ this.handlers = null;
93
+ }
94
+ handleItemClick(id) {
95
+ if (!this.handlers)
96
+ return;
97
+ switch (id) {
98
+ case 'undo':
99
+ this.handlers.onUndo();
100
+ break;
101
+ case 'redo':
102
+ this.handlers.onRedo();
103
+ break;
104
+ case 'copy':
105
+ this.handlers.onCopy();
106
+ break;
107
+ case 'cut':
108
+ this.handlers.onCut();
109
+ break;
110
+ case 'paste':
111
+ this.handlers.onPaste();
112
+ break;
113
+ case 'selectAll':
114
+ this.handlers.onSelectAll();
115
+ break;
116
+ }
117
+ this.close();
118
+ }
119
+ destroy() {
120
+ this.close();
121
+ }
122
+ }
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Renders header filter popover (dropdown) in DOM.
3
+ * Instantiated by OGrid, reads state from HeaderFilterState.
4
+ */
5
+ export class HeaderFilter {
6
+ constructor(state) {
7
+ this.popoverEl = null;
8
+ this.state = state;
9
+ }
10
+ /**
11
+ * Render the popover for the currently open filter.
12
+ * Call this whenever HeaderFilterState changes.
13
+ */
14
+ render(config) {
15
+ this.cleanup();
16
+ if (!config || !this.state.openColumnId || !this.state.popoverPosition)
17
+ return;
18
+ const pos = this.state.popoverPosition;
19
+ this.popoverEl = document.createElement('div');
20
+ this.popoverEl.className = 'ogrid-header-filter-popover';
21
+ this.popoverEl.style.position = 'fixed';
22
+ this.popoverEl.style.top = `${pos.top}px`;
23
+ this.popoverEl.style.left = `${pos.left}px`;
24
+ this.popoverEl.style.zIndex = '9999';
25
+ this.popoverEl.style.background = 'var(--ogrid-bg, #fff)';
26
+ this.popoverEl.style.color = 'var(--ogrid-fg, #242424)';
27
+ this.popoverEl.style.border = '1px solid var(--ogrid-border, #e0e0e0)';
28
+ this.popoverEl.style.borderRadius = '4px';
29
+ this.popoverEl.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
30
+ this.popoverEl.style.padding = '8px';
31
+ this.popoverEl.style.minWidth = '200px';
32
+ this.popoverEl.style.maxHeight = '320px';
33
+ this.popoverEl.style.overflowY = 'auto';
34
+ // Stop clicks within popover from propagating
35
+ this.popoverEl.addEventListener('click', (e) => e.stopPropagation());
36
+ this.popoverEl.addEventListener('mousedown', (e) => e.stopPropagation());
37
+ if (config.filterType === 'text') {
38
+ this.renderTextFilter(config);
39
+ }
40
+ else if (config.filterType === 'multiSelect') {
41
+ this.renderMultiSelectFilter(config);
42
+ }
43
+ else if (config.filterType === 'date') {
44
+ this.renderDateFilter(config);
45
+ }
46
+ document.body.appendChild(this.popoverEl);
47
+ }
48
+ renderTextFilter(config) {
49
+ if (!this.popoverEl)
50
+ return;
51
+ const input = document.createElement('input');
52
+ input.type = 'text';
53
+ input.value = this.state.tempTextValue;
54
+ input.placeholder = 'Filter...';
55
+ input.setAttribute('aria-label', 'Text filter');
56
+ input.className = 'ogrid-filter-text-input';
57
+ this.applyInputStyle(input);
58
+ input.style.marginBottom = '8px';
59
+ input.addEventListener('input', () => {
60
+ this.state.setTempTextValue(input.value);
61
+ });
62
+ input.addEventListener('keydown', (e) => {
63
+ if (e.key === 'Enter') {
64
+ this.state.applyTextFilter(config.filterField);
65
+ }
66
+ e.stopPropagation();
67
+ });
68
+ this.popoverEl.appendChild(input);
69
+ // Buttons
70
+ const btnRow = document.createElement('div');
71
+ btnRow.style.display = 'flex';
72
+ btnRow.style.gap = '8px';
73
+ const applyBtn = document.createElement('button');
74
+ applyBtn.textContent = 'Apply';
75
+ applyBtn.className = 'ogrid-filter-apply-btn';
76
+ this.applyButtonStyle(applyBtn);
77
+ applyBtn.addEventListener('click', () => this.state.applyTextFilter(config.filterField));
78
+ btnRow.appendChild(applyBtn);
79
+ const clearBtn = document.createElement('button');
80
+ clearBtn.textContent = 'Clear';
81
+ clearBtn.className = 'ogrid-filter-clear-btn';
82
+ this.applyButtonStyle(clearBtn);
83
+ clearBtn.addEventListener('click', () => this.state.clearTextFilter(config.filterField));
84
+ btnRow.appendChild(clearBtn);
85
+ this.popoverEl.appendChild(btnRow);
86
+ // Focus input
87
+ setTimeout(() => input.focus(), 0);
88
+ }
89
+ renderMultiSelectFilter(config) {
90
+ if (!this.popoverEl)
91
+ return;
92
+ // Search box
93
+ const searchInput = document.createElement('input');
94
+ searchInput.type = 'text';
95
+ searchInput.value = this.state.searchText;
96
+ searchInput.placeholder = 'Search...';
97
+ searchInput.setAttribute('aria-label', 'Search filter options');
98
+ searchInput.className = 'ogrid-filter-search-input';
99
+ this.applyInputStyle(searchInput);
100
+ searchInput.style.marginBottom = '8px';
101
+ searchInput.addEventListener('input', () => {
102
+ this.state.setSearchText(searchInput.value);
103
+ this.updateCheckboxList(config, checkboxContainer);
104
+ });
105
+ searchInput.addEventListener('keydown', (e) => e.stopPropagation());
106
+ this.popoverEl.appendChild(searchInput);
107
+ // Select all / Clear all buttons
108
+ const actionRow = document.createElement('div');
109
+ actionRow.style.display = 'flex';
110
+ actionRow.style.gap = '8px';
111
+ actionRow.style.marginBottom = '8px';
112
+ const selectAllBtn = document.createElement('button');
113
+ selectAllBtn.textContent = 'Select All';
114
+ selectAllBtn.className = 'ogrid-filter-select-all-btn';
115
+ this.applySmallButtonStyle(selectAllBtn);
116
+ selectAllBtn.addEventListener('click', () => {
117
+ this.state.handleSelectAll(config.filterField);
118
+ this.updateCheckboxList(config, checkboxContainer);
119
+ });
120
+ actionRow.appendChild(selectAllBtn);
121
+ const clearSelBtn = document.createElement('button');
122
+ clearSelBtn.textContent = 'Clear';
123
+ clearSelBtn.className = 'ogrid-filter-clear-sel-btn';
124
+ this.applySmallButtonStyle(clearSelBtn);
125
+ clearSelBtn.addEventListener('click', () => {
126
+ this.state.handleClearSelection();
127
+ this.updateCheckboxList(config, checkboxContainer);
128
+ });
129
+ actionRow.appendChild(clearSelBtn);
130
+ this.popoverEl.appendChild(actionRow);
131
+ // Checkbox list container
132
+ const checkboxContainer = document.createElement('div');
133
+ checkboxContainer.className = 'ogrid-filter-checkbox-list';
134
+ checkboxContainer.style.maxHeight = '160px';
135
+ checkboxContainer.style.overflowY = 'auto';
136
+ checkboxContainer.style.marginBottom = '8px';
137
+ checkboxContainer.setAttribute('role', 'group');
138
+ checkboxContainer.setAttribute('aria-label', 'Filter options');
139
+ this.updateCheckboxList(config, checkboxContainer);
140
+ this.popoverEl.appendChild(checkboxContainer);
141
+ // Apply button
142
+ const applyBtn = document.createElement('button');
143
+ applyBtn.textContent = 'Apply';
144
+ applyBtn.className = 'ogrid-filter-apply-btn';
145
+ this.applyButtonStyle(applyBtn);
146
+ applyBtn.addEventListener('click', () => this.state.applyMultiSelectFilter(config.filterField));
147
+ this.popoverEl.appendChild(applyBtn);
148
+ }
149
+ updateCheckboxList(config, container) {
150
+ container.innerHTML = '';
151
+ const options = this.state.getFilteredOptions(config.filterField);
152
+ const selected = this.state.tempSelected;
153
+ for (const opt of options) {
154
+ const label = document.createElement('label');
155
+ label.style.display = 'flex';
156
+ label.style.alignItems = 'center';
157
+ label.style.gap = '4px';
158
+ label.style.padding = '2px 0';
159
+ label.style.cursor = 'pointer';
160
+ label.style.fontSize = '13px';
161
+ const checkbox = document.createElement('input');
162
+ checkbox.type = 'checkbox';
163
+ checkbox.checked = selected.has(opt);
164
+ checkbox.addEventListener('change', () => {
165
+ this.state.handleCheckboxChange(opt, checkbox.checked);
166
+ });
167
+ const text = document.createElement('span');
168
+ text.textContent = opt;
169
+ label.appendChild(checkbox);
170
+ label.appendChild(text);
171
+ container.appendChild(label);
172
+ }
173
+ if (options.length === 0) {
174
+ const empty = document.createElement('div');
175
+ empty.style.color = 'var(--ogrid-muted, #999)';
176
+ empty.style.fontStyle = 'italic';
177
+ empty.style.padding = '4px 0';
178
+ empty.textContent = 'No options';
179
+ container.appendChild(empty);
180
+ }
181
+ }
182
+ renderDateFilter(config) {
183
+ if (!this.popoverEl)
184
+ return;
185
+ const container = document.createElement('div');
186
+ container.style.display = 'flex';
187
+ container.style.flexDirection = 'column';
188
+ container.style.gap = '8px';
189
+ // From date
190
+ const fromLabel = document.createElement('label');
191
+ fromLabel.style.display = 'flex';
192
+ fromLabel.style.alignItems = 'center';
193
+ fromLabel.style.gap = '4px';
194
+ fromLabel.style.fontSize = '13px';
195
+ fromLabel.textContent = 'From: ';
196
+ const fromInput = document.createElement('input');
197
+ fromInput.type = 'date';
198
+ fromInput.value = this.state.tempDateFrom;
199
+ fromInput.setAttribute('aria-label', 'From date');
200
+ this.applyInputStyle(fromInput);
201
+ fromInput.addEventListener('change', () => {
202
+ this.state.setTempDateFrom(fromInput.value);
203
+ });
204
+ fromInput.addEventListener('keydown', (e) => e.stopPropagation());
205
+ fromLabel.appendChild(fromInput);
206
+ container.appendChild(fromLabel);
207
+ // To date
208
+ const toLabel = document.createElement('label');
209
+ toLabel.style.display = 'flex';
210
+ toLabel.style.alignItems = 'center';
211
+ toLabel.style.gap = '4px';
212
+ toLabel.style.fontSize = '13px';
213
+ toLabel.textContent = 'To: ';
214
+ const toInput = document.createElement('input');
215
+ toInput.type = 'date';
216
+ toInput.value = this.state.tempDateTo;
217
+ toInput.setAttribute('aria-label', 'To date');
218
+ this.applyInputStyle(toInput);
219
+ toInput.addEventListener('change', () => {
220
+ this.state.setTempDateTo(toInput.value);
221
+ });
222
+ toInput.addEventListener('keydown', (e) => e.stopPropagation());
223
+ toLabel.appendChild(toInput);
224
+ container.appendChild(toLabel);
225
+ this.popoverEl.appendChild(container);
226
+ // Buttons
227
+ const btnRow = document.createElement('div');
228
+ btnRow.style.display = 'flex';
229
+ btnRow.style.gap = '8px';
230
+ btnRow.style.marginTop = '8px';
231
+ const applyBtn = document.createElement('button');
232
+ applyBtn.textContent = 'Apply';
233
+ applyBtn.className = 'ogrid-filter-apply-btn';
234
+ this.applyButtonStyle(applyBtn);
235
+ applyBtn.addEventListener('click', () => this.state.applyDateFilter(config.filterField));
236
+ btnRow.appendChild(applyBtn);
237
+ const clearBtn = document.createElement('button');
238
+ clearBtn.textContent = 'Clear';
239
+ clearBtn.className = 'ogrid-filter-clear-btn';
240
+ this.applyButtonStyle(clearBtn);
241
+ clearBtn.addEventListener('click', () => this.state.clearDateFilter(config.filterField));
242
+ btnRow.appendChild(clearBtn);
243
+ this.popoverEl.appendChild(btnRow);
244
+ }
245
+ applyInputStyle(input) {
246
+ input.style.width = '100%';
247
+ input.style.boxSizing = 'border-box';
248
+ input.style.padding = '4px 6px';
249
+ input.style.background = 'var(--ogrid-bg, #fff)';
250
+ input.style.color = 'var(--ogrid-fg, #242424)';
251
+ input.style.border = '1px solid var(--ogrid-border, #e0e0e0)';
252
+ input.style.borderRadius = '4px';
253
+ }
254
+ applyButtonStyle(btn) {
255
+ btn.style.flex = '1';
256
+ btn.style.cursor = 'pointer';
257
+ btn.style.padding = '6px 12px';
258
+ btn.style.background = 'var(--ogrid-bg-subtle, #f3f2f1)';
259
+ btn.style.color = 'var(--ogrid-fg, #242424)';
260
+ btn.style.border = '1px solid var(--ogrid-border, #e0e0e0)';
261
+ btn.style.borderRadius = '4px';
262
+ }
263
+ applySmallButtonStyle(btn) {
264
+ btn.style.cursor = 'pointer';
265
+ btn.style.padding = '2px 8px';
266
+ btn.style.fontSize = '12px';
267
+ btn.style.background = 'transparent';
268
+ btn.style.color = 'var(--ogrid-fg, #242424)';
269
+ btn.style.border = '1px solid var(--ogrid-border, #e0e0e0)';
270
+ btn.style.borderRadius = '4px';
271
+ }
272
+ cleanup() {
273
+ if (this.popoverEl) {
274
+ this.popoverEl.remove();
275
+ this.popoverEl = null;
276
+ }
277
+ }
278
+ destroy() {
279
+ this.cleanup();
280
+ }
281
+ }