@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,278 @@
1
+ import { getCellValue } from '@alaarab/ogrid-core';
2
+ const EDITOR_STYLE = {
3
+ position: 'absolute',
4
+ zIndex: '1000',
5
+ boxSizing: 'border-box',
6
+ border: '2px solid #0078d4',
7
+ outline: 'none',
8
+ fontFamily: 'inherit',
9
+ fontSize: 'inherit',
10
+ };
11
+ export class InlineCellEditor {
12
+ constructor(container) {
13
+ this.editor = null;
14
+ this.editingCell = null;
15
+ this.onCommit = null;
16
+ this.onCancel = null;
17
+ this.container = container;
18
+ }
19
+ startEdit(rowId, columnId, item, column, cell, onCommit, onCancel) {
20
+ this.closeEditor();
21
+ this.editingCell = { rowId, columnId };
22
+ this.onCommit = onCommit;
23
+ this.onCancel = onCancel;
24
+ const value = getCellValue(item, column);
25
+ const rect = cell.getBoundingClientRect();
26
+ const containerRect = this.container.getBoundingClientRect();
27
+ const editor = this.createEditor(column, item, value, cell);
28
+ editor.style.position = 'absolute';
29
+ editor.style.left = `${rect.left - containerRect.left + this.container.scrollLeft}px`;
30
+ editor.style.top = `${rect.top - containerRect.top + this.container.scrollTop}px`;
31
+ editor.style.width = `${rect.width}px`;
32
+ editor.style.height = `${rect.height}px`;
33
+ this.editor = editor;
34
+ this.container.appendChild(editor);
35
+ editor.focus();
36
+ }
37
+ /** Returns the cell currently being edited, or null if no editor is open. */
38
+ getEditingCell() {
39
+ return this.editingCell;
40
+ }
41
+ closeEditor() {
42
+ if (this.editor) {
43
+ this.editor.remove();
44
+ this.editor = null;
45
+ }
46
+ this.editingCell = null;
47
+ this.onCommit = null;
48
+ this.onCancel = null;
49
+ }
50
+ createEditor(column, item, value, cell) {
51
+ const editorType = column.cellEditor;
52
+ if (typeof editorType === 'function') {
53
+ const context = {
54
+ value,
55
+ onValueChange: (newValue) => {
56
+ if (this.editingCell) {
57
+ this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, newValue);
58
+ }
59
+ },
60
+ onCommit: () => this.closeEditor(),
61
+ onCancel: () => {
62
+ this.onCancel?.();
63
+ this.closeEditor();
64
+ },
65
+ item,
66
+ column,
67
+ cell,
68
+ cellEditorParams: column.cellEditorParams,
69
+ };
70
+ return editorType(context);
71
+ }
72
+ // Built-in editor types
73
+ if (editorType === 'checkbox' || column.type === 'boolean') {
74
+ return this.createCheckboxEditor(value);
75
+ }
76
+ if (editorType === 'select') {
77
+ return this.createSelectEditor(value, column);
78
+ }
79
+ if (editorType === 'richSelect') {
80
+ return this.createRichSelectEditor(value, column);
81
+ }
82
+ if (editorType === 'date' || column.type === 'date') {
83
+ return this.createDateEditor(value);
84
+ }
85
+ // Default: text editor
86
+ return this.createTextEditor(value);
87
+ }
88
+ createTextEditor(value) {
89
+ const input = document.createElement('input');
90
+ input.type = 'text';
91
+ input.value = value != null ? String(value) : '';
92
+ Object.assign(input.style, EDITOR_STYLE);
93
+ input.addEventListener('keydown', (e) => {
94
+ if (e.key === 'Enter') {
95
+ e.preventDefault();
96
+ if (this.editingCell) {
97
+ this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, input.value);
98
+ }
99
+ this.closeEditor();
100
+ }
101
+ else if (e.key === 'Escape') {
102
+ e.preventDefault();
103
+ this.onCancel?.();
104
+ this.closeEditor();
105
+ }
106
+ });
107
+ input.addEventListener('blur', () => {
108
+ if (this.editingCell) {
109
+ this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, input.value);
110
+ }
111
+ this.closeEditor();
112
+ });
113
+ setTimeout(() => input.select(), 0);
114
+ return input;
115
+ }
116
+ createCheckboxEditor(value) {
117
+ const input = document.createElement('input');
118
+ input.type = 'checkbox';
119
+ input.checked = Boolean(value);
120
+ Object.assign(input.style, EDITOR_STYLE);
121
+ input.style.width = '20px';
122
+ input.style.height = '20px';
123
+ input.addEventListener('change', () => {
124
+ if (this.editingCell) {
125
+ this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, input.checked);
126
+ }
127
+ this.closeEditor();
128
+ });
129
+ input.addEventListener('keydown', (e) => {
130
+ if (e.key === 'Escape') {
131
+ e.preventDefault();
132
+ this.onCancel?.();
133
+ this.closeEditor();
134
+ }
135
+ });
136
+ return input;
137
+ }
138
+ createDateEditor(value) {
139
+ const input = document.createElement('input');
140
+ input.type = 'date';
141
+ if (value != null) {
142
+ const dateStr = String(value);
143
+ if (dateStr.match(/^\d{4}-\d{2}-\d{2}/)) {
144
+ input.value = dateStr.substring(0, 10);
145
+ }
146
+ }
147
+ Object.assign(input.style, EDITOR_STYLE);
148
+ input.addEventListener('keydown', (e) => {
149
+ if (e.key === 'Enter') {
150
+ e.preventDefault();
151
+ if (this.editingCell) {
152
+ this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, input.value);
153
+ }
154
+ this.closeEditor();
155
+ }
156
+ else if (e.key === 'Escape') {
157
+ e.preventDefault();
158
+ this.onCancel?.();
159
+ this.closeEditor();
160
+ }
161
+ });
162
+ input.addEventListener('blur', () => {
163
+ if (this.editingCell) {
164
+ this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, input.value);
165
+ }
166
+ this.closeEditor();
167
+ });
168
+ return input;
169
+ }
170
+ createSelectEditor(value, column) {
171
+ const select = document.createElement('select');
172
+ const values = column.cellEditorParams?.values ?? [];
173
+ for (const val of values) {
174
+ const option = document.createElement('option');
175
+ option.value = String(val);
176
+ option.textContent = String(val);
177
+ select.appendChild(option);
178
+ }
179
+ select.value = value != null ? String(value) : '';
180
+ Object.assign(select.style, EDITOR_STYLE);
181
+ select.addEventListener('change', () => {
182
+ if (this.editingCell) {
183
+ this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, select.value);
184
+ }
185
+ this.closeEditor();
186
+ });
187
+ select.addEventListener('keydown', (e) => {
188
+ if (e.key === 'Escape') {
189
+ e.preventDefault();
190
+ this.onCancel?.();
191
+ this.closeEditor();
192
+ }
193
+ });
194
+ return select;
195
+ }
196
+ createRichSelectEditor(value, column) {
197
+ const wrapper = document.createElement('div');
198
+ Object.assign(wrapper.style, EDITOR_STYLE);
199
+ wrapper.style.padding = '0';
200
+ const input = document.createElement('input');
201
+ input.type = 'text';
202
+ input.value = value != null ? String(value) : '';
203
+ input.style.width = '100%';
204
+ input.style.border = 'none';
205
+ input.style.outline = 'none';
206
+ input.style.padding = '4px';
207
+ input.style.boxSizing = 'border-box';
208
+ wrapper.appendChild(input);
209
+ const dropdown = document.createElement('div');
210
+ dropdown.style.position = 'absolute';
211
+ dropdown.style.top = '100%';
212
+ dropdown.style.left = '0';
213
+ dropdown.style.width = '100%';
214
+ dropdown.style.maxHeight = '200px';
215
+ dropdown.style.overflowY = 'auto';
216
+ dropdown.style.backgroundColor = 'white';
217
+ dropdown.style.border = '1px solid #ccc';
218
+ dropdown.style.zIndex = '1001';
219
+ wrapper.appendChild(dropdown);
220
+ const values = column.cellEditorParams?.values ?? [];
221
+ const formatValue = column.cellEditorParams?.formatValue ?? ((v) => String(v));
222
+ const renderOptions = (filter) => {
223
+ dropdown.innerHTML = '';
224
+ const filtered = values.filter((v) => String(formatValue(v)).toLowerCase().includes(filter.toLowerCase()));
225
+ for (const val of filtered) {
226
+ const option = document.createElement('div');
227
+ option.textContent = String(formatValue(val));
228
+ option.style.padding = '4px 8px';
229
+ option.style.cursor = 'pointer';
230
+ option.addEventListener('mousedown', (e) => {
231
+ e.preventDefault();
232
+ if (this.editingCell) {
233
+ this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, val);
234
+ }
235
+ this.closeEditor();
236
+ });
237
+ option.addEventListener('mouseenter', () => {
238
+ option.style.backgroundColor = '#f0f0f0';
239
+ });
240
+ option.addEventListener('mouseleave', () => {
241
+ option.style.backgroundColor = 'white';
242
+ });
243
+ dropdown.appendChild(option);
244
+ }
245
+ };
246
+ input.addEventListener('input', () => {
247
+ renderOptions(input.value);
248
+ });
249
+ input.addEventListener('keydown', (e) => {
250
+ if (e.key === 'Enter') {
251
+ e.preventDefault();
252
+ if (this.editingCell) {
253
+ this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, input.value);
254
+ }
255
+ this.closeEditor();
256
+ }
257
+ else if (e.key === 'Escape') {
258
+ e.preventDefault();
259
+ this.onCancel?.();
260
+ this.closeEditor();
261
+ }
262
+ });
263
+ input.addEventListener('blur', () => {
264
+ setTimeout(() => {
265
+ if (this.editingCell) {
266
+ this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, input.value);
267
+ }
268
+ this.closeEditor();
269
+ }, 200);
270
+ });
271
+ renderOptions('');
272
+ setTimeout(() => input.select(), 0);
273
+ return wrapper;
274
+ }
275
+ destroy() {
276
+ this.closeEditor();
277
+ }
278
+ }
@@ -0,0 +1,170 @@
1
+ /** Inject the @keyframes rule once into <head> (deduplicates across multiple instances). */
2
+ function ensureKeyframes() {
3
+ if (typeof document === 'undefined')
4
+ return;
5
+ if (document.getElementById('ogrid-marching-ants-keyframes'))
6
+ return;
7
+ const style = document.createElement('style');
8
+ style.id = 'ogrid-marching-ants-keyframes';
9
+ style.textContent =
10
+ '@keyframes ogrid-marching-ants{to{stroke-dashoffset:-8}}';
11
+ document.head.appendChild(style);
12
+ }
13
+ /** Measure the bounding rect of a range within a container. */
14
+ function measureRange(container, range, colOffset) {
15
+ const startGlobalCol = range.startCol + colOffset;
16
+ const endGlobalCol = range.endCol + colOffset;
17
+ const topLeft = container.querySelector(`[data-row-index="${range.startRow}"][data-col-index="${startGlobalCol}"]`);
18
+ const bottomRight = container.querySelector(`[data-row-index="${range.endRow}"][data-col-index="${endGlobalCol}"]`);
19
+ if (!topLeft || !bottomRight)
20
+ return null;
21
+ const cRect = container.getBoundingClientRect();
22
+ const tlRect = topLeft.getBoundingClientRect();
23
+ const brRect = bottomRight.getBoundingClientRect();
24
+ return {
25
+ top: tlRect.top - cRect.top + container.scrollTop,
26
+ left: tlRect.left - cRect.left + container.scrollLeft,
27
+ width: brRect.right - tlRect.left,
28
+ height: brRect.bottom - tlRect.top,
29
+ };
30
+ }
31
+ function rangesEqual(a, b) {
32
+ if (a === b)
33
+ return true;
34
+ if (!a || !b)
35
+ return false;
36
+ return a.startRow === b.startRow && a.endRow === b.endRow &&
37
+ a.startCol === b.startCol && a.endCol === b.endCol;
38
+ }
39
+ /**
40
+ * MarchingAntsOverlay — renders SVG overlays on top of the grid:
41
+ * 1. Selection range: solid green border
42
+ * 2. Copy/Cut range: animated dashed border (marching ants)
43
+ *
44
+ * Vanilla JS equivalent of React's MarchingAntsOverlay component.
45
+ */
46
+ export class MarchingAntsOverlay {
47
+ constructor(container, colOffset = 0) {
48
+ this.selSvg = null;
49
+ this.clipSvg = null;
50
+ this.selectionRange = null;
51
+ this.copyRange = null;
52
+ this.cutRange = null;
53
+ this.rafHandle = 0;
54
+ this.container = container;
55
+ this.colOffset = colOffset;
56
+ ensureKeyframes();
57
+ // The container must be positioned for absolute SVGs
58
+ const pos = getComputedStyle(container).position;
59
+ if (pos === 'static' || pos === '') {
60
+ container.style.position = 'relative';
61
+ }
62
+ }
63
+ update(selectionRange, copyRange, cutRange) {
64
+ // Skip if nothing changed
65
+ if (rangesEqual(this.selectionRange, selectionRange) &&
66
+ rangesEqual(this.copyRange, copyRange) &&
67
+ rangesEqual(this.cutRange, cutRange)) {
68
+ return;
69
+ }
70
+ this.selectionRange = selectionRange;
71
+ this.copyRange = copyRange;
72
+ this.cutRange = cutRange;
73
+ // Delay one frame so cells are rendered
74
+ if (this.rafHandle)
75
+ cancelAnimationFrame(this.rafHandle);
76
+ this.rafHandle = requestAnimationFrame(() => {
77
+ this.rafHandle = 0;
78
+ this.render();
79
+ });
80
+ }
81
+ render() {
82
+ const clipRange = this.copyRange ?? this.cutRange;
83
+ // Selection range SVG
84
+ const selRect = this.selectionRange
85
+ ? measureRange(this.container, this.selectionRange, this.colOffset)
86
+ : null;
87
+ // When clipboard range matches selection, hide selection border so marching ants show
88
+ const clipRangeMatchesSel = this.selectionRange != null &&
89
+ clipRange != null &&
90
+ rangesEqual(this.selectionRange, clipRange);
91
+ if (selRect && !clipRangeMatchesSel) {
92
+ if (!this.selSvg) {
93
+ this.selSvg = this.createSvg(4);
94
+ this.container.appendChild(this.selSvg);
95
+ }
96
+ this.positionSvg(this.selSvg, selRect);
97
+ const rect = this.selSvg.querySelector('rect');
98
+ if (rect) {
99
+ rect.setAttribute('width', String(Math.max(0, selRect.width - 2)));
100
+ rect.setAttribute('height', String(Math.max(0, selRect.height - 2)));
101
+ rect.setAttribute('stroke', 'var(--ogrid-selection, #217346)');
102
+ rect.setAttribute('stroke-width', '2');
103
+ rect.removeAttribute('stroke-dasharray');
104
+ rect.style.animation = '';
105
+ }
106
+ }
107
+ else {
108
+ this.removeSvg('sel');
109
+ }
110
+ // Copy/Cut range SVG (marching ants)
111
+ const clipRect = clipRange
112
+ ? measureRange(this.container, clipRange, this.colOffset)
113
+ : null;
114
+ if (clipRect) {
115
+ if (!this.clipSvg) {
116
+ this.clipSvg = this.createSvg(5);
117
+ this.container.appendChild(this.clipSvg);
118
+ }
119
+ this.positionSvg(this.clipSvg, clipRect);
120
+ const rect = this.clipSvg.querySelector('rect');
121
+ if (rect) {
122
+ rect.setAttribute('width', String(Math.max(0, clipRect.width - 2)));
123
+ rect.setAttribute('height', String(Math.max(0, clipRect.height - 2)));
124
+ rect.setAttribute('stroke', 'var(--ogrid-selection, #217346)');
125
+ rect.setAttribute('stroke-width', '2');
126
+ rect.setAttribute('stroke-dasharray', '4 4');
127
+ rect.style.animation = 'ogrid-marching-ants 0.5s linear infinite';
128
+ }
129
+ }
130
+ else {
131
+ this.removeSvg('clip');
132
+ }
133
+ }
134
+ createSvg(zIndex) {
135
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
136
+ svg.setAttribute('aria-hidden', 'true');
137
+ svg.style.position = 'absolute';
138
+ svg.style.pointerEvents = 'none';
139
+ svg.style.zIndex = String(zIndex);
140
+ svg.style.overflow = 'visible';
141
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
142
+ rect.setAttribute('x', '1');
143
+ rect.setAttribute('y', '1');
144
+ rect.setAttribute('fill', 'none');
145
+ svg.appendChild(rect);
146
+ return svg;
147
+ }
148
+ positionSvg(svg, rect) {
149
+ svg.style.top = `${rect.top}px`;
150
+ svg.style.left = `${rect.left}px`;
151
+ svg.style.width = `${rect.width}px`;
152
+ svg.style.height = `${rect.height}px`;
153
+ }
154
+ removeSvg(which) {
155
+ if (which === 'sel' && this.selSvg) {
156
+ this.selSvg.remove();
157
+ this.selSvg = null;
158
+ }
159
+ else if (which === 'clip' && this.clipSvg) {
160
+ this.clipSvg.remove();
161
+ this.clipSvg = null;
162
+ }
163
+ }
164
+ destroy() {
165
+ if (this.rafHandle)
166
+ cancelAnimationFrame(this.rafHandle);
167
+ this.removeSvg('sel');
168
+ this.removeSvg('clip');
169
+ }
170
+ }
@@ -0,0 +1,85 @@
1
+ import { getPaginationViewModel } from '@alaarab/ogrid-core';
2
+ export class PaginationControls {
3
+ constructor(container, state) {
4
+ this.el = null;
5
+ this.container = container;
6
+ this.state = state;
7
+ }
8
+ render(totalCount) {
9
+ if (this.el)
10
+ this.el.remove();
11
+ const vm = getPaginationViewModel(this.state.page, this.state.pageSize, totalCount);
12
+ if (!vm)
13
+ return; // No pagination if totalCount is 0
14
+ this.el = document.createElement('div');
15
+ this.el.className = 'ogrid-pagination';
16
+ // Page size selector
17
+ const pageSizeDiv = document.createElement('div');
18
+ pageSizeDiv.className = 'ogrid-pagination-size';
19
+ const label = document.createElement('span');
20
+ label.textContent = 'Rows per page: ';
21
+ pageSizeDiv.appendChild(label);
22
+ const select = document.createElement('select');
23
+ select.className = 'ogrid-page-size-select';
24
+ for (const size of vm.pageSizeOptions) {
25
+ const option = document.createElement('option');
26
+ option.value = String(size);
27
+ option.textContent = String(size);
28
+ option.selected = size === this.state.pageSize;
29
+ select.appendChild(option);
30
+ }
31
+ select.addEventListener('change', () => {
32
+ this.state.setPageSize(Number(select.value));
33
+ });
34
+ pageSizeDiv.appendChild(select);
35
+ this.el.appendChild(pageSizeDiv);
36
+ // Page info
37
+ const info = document.createElement('span');
38
+ info.className = 'ogrid-pagination-info';
39
+ info.textContent = `${vm.startItem}-${vm.endItem} of ${totalCount}`;
40
+ this.el.appendChild(info);
41
+ // Navigation buttons
42
+ const nav = document.createElement('div');
43
+ nav.className = 'ogrid-pagination-nav';
44
+ const prevBtn = document.createElement('button');
45
+ prevBtn.textContent = '\u25C0';
46
+ prevBtn.className = 'ogrid-pagination-btn';
47
+ prevBtn.disabled = this.state.page === 1;
48
+ prevBtn.addEventListener('click', () => this.state.setPage(this.state.page - 1));
49
+ nav.appendChild(prevBtn);
50
+ // Start ellipsis
51
+ if (vm.showStartEllipsis) {
52
+ const ellipsis = document.createElement('span');
53
+ ellipsis.textContent = '...';
54
+ ellipsis.className = 'ogrid-pagination-ellipsis';
55
+ nav.appendChild(ellipsis);
56
+ }
57
+ // Page number buttons
58
+ for (const pageNum of vm.pageNumbers) {
59
+ const btn = document.createElement('button');
60
+ btn.textContent = String(pageNum);
61
+ btn.className = 'ogrid-pagination-btn' + (pageNum === this.state.page ? ' ogrid-pagination-active' : '');
62
+ btn.addEventListener('click', () => this.state.setPage(pageNum));
63
+ nav.appendChild(btn);
64
+ }
65
+ // End ellipsis
66
+ if (vm.showEndEllipsis) {
67
+ const ellipsis = document.createElement('span');
68
+ ellipsis.textContent = '...';
69
+ ellipsis.className = 'ogrid-pagination-ellipsis';
70
+ nav.appendChild(ellipsis);
71
+ }
72
+ const nextBtn = document.createElement('button');
73
+ nextBtn.textContent = '\u25B6';
74
+ nextBtn.className = 'ogrid-pagination-btn';
75
+ nextBtn.disabled = this.state.page === vm.totalPages;
76
+ nextBtn.addEventListener('click', () => this.state.setPage(this.state.page + 1));
77
+ nav.appendChild(nextBtn);
78
+ this.el.appendChild(nav);
79
+ this.container.appendChild(this.el);
80
+ }
81
+ destroy() {
82
+ this.el?.remove();
83
+ this.el = null;
84
+ }
85
+ }