0
# Selection and Focus System
1
2
RevoGrid provides comprehensive selection and focus management with support for single cell focus, range selection, and programmatic navigation control.
3
4
## Focus Management
5
6
### Cell Focus Configuration
7
8
Enable and configure cell focus behavior:
9
10
```typescript { .api }
11
interface FocusConfiguration {
12
canFocus: boolean;
13
range: boolean;
14
}
15
```
16
17
**canFocus** { .api }
18
```typescript
19
canFocus: boolean = true
20
```
21
Enable cell focus functionality. Shows focus border around active cell and enables keyboard navigation.
22
23
**range** { .api }
24
```typescript
25
range: boolean = false
26
```
27
Enable range selection functionality. Allows users to select multiple cells by dragging or using Shift+Click.
28
29
### Focus State Types
30
31
```typescript { .api }
32
namespace Selection {
33
interface Cell {
34
x: ColIndex;
35
y: RowIndex;
36
}
37
38
interface FocusedCells {
39
focus: Cell;
40
end: Cell;
41
}
42
43
interface RangeArea {
44
x: ColIndex;
45
y: RowIndex;
46
x1: ColIndex;
47
y1: RowIndex;
48
}
49
}
50
```
51
52
**Cell Coordinates** { .api }
53
```typescript
54
type ColIndex = number;
55
type RowIndex = number;
56
57
interface Cell {
58
x: ColIndex; // Column index
59
y: RowIndex; // Row index
60
}
61
```
62
63
**Range Area** { .api }
64
```typescript
65
interface RangeArea {
66
x: ColIndex; // Start column index
67
y: RowIndex; // Start row index
68
x1: ColIndex; // End column index
69
y1: RowIndex; // End row index
70
}
71
```
72
73
## Focus Methods
74
75
### Setting Focus
76
77
**setCellsFocus** { .api }
78
```typescript
79
async setCellsFocus(
80
cellStart?: Selection.Cell,
81
cellEnd?: Selection.Cell,
82
colType?: string,
83
rowType?: string
84
): Promise<void>
85
```
86
Set focus on a single cell or range of cells.
87
88
- **cellStart**: Starting cell position
89
- **cellEnd**: Ending cell position (for range selection)
90
- **colType**: Column dimension type
91
- **rowType**: Row dimension type
92
93
```typescript
94
// Focus single cell
95
await grid.setCellsFocus({ x: 2, y: 5 });
96
97
// Focus range of cells
98
await grid.setCellsFocus(
99
{ x: 1, y: 2 }, // Start
100
{ x: 3, y: 5 } // End
101
);
102
103
// Focus with specific dimension types
104
await grid.setCellsFocus(
105
{ x: 0, y: 0 },
106
{ x: 2, y: 2 },
107
'rgCol', // Regular columns
108
'rgRow' // Regular rows
109
);
110
```
111
112
### Getting Focus Information
113
114
**getFocused** { .api }
115
```typescript
116
async getFocused(): Promise<FocusedData|null>
117
```
118
Get information about the currently focused cell.
119
120
```typescript { .api }
121
interface FocusedData {
122
cell: Selection.Cell;
123
model: any;
124
column: RevoGrid.ColumnRegular;
125
rowType: RevoGrid.DimensionRows;
126
colType: RevoGrid.DimensionCols;
127
}
128
```
129
130
```typescript
131
// Get current focus
132
const focusedData = await grid.getFocused();
133
134
if (focusedData) {
135
console.log('Focused cell:', focusedData.cell);
136
console.log('Cell data:', focusedData.model);
137
console.log('Column info:', focusedData.column);
138
}
139
```
140
141
**clearFocus** { .api }
142
```typescript
143
async clearFocus(): Promise<void>
144
```
145
Remove focus from all cells.
146
147
```typescript
148
// Clear current focus
149
await grid.clearFocus();
150
```
151
152
## Selection Management
153
154
### Getting Selection
155
156
**getSelectedRange** { .api }
157
```typescript
158
async getSelectedRange(): Promise<Selection.RangeArea|null>
159
```
160
Get the currently selected range area.
161
162
```typescript
163
// Get current selection
164
const selection = await grid.getSelectedRange();
165
166
if (selection) {
167
console.log('Selection area:', selection);
168
169
// Calculate selection dimensions
170
const width = selection.x1 - selection.x + 1;
171
const height = selection.y1 - selection.y + 1;
172
console.log(`Selected ${width}x${height} cells`);
173
}
174
```
175
176
### Selection State Management
177
178
```typescript { .api }
179
interface SelectionStoreState {
180
// Current selection range
181
range: RangeArea | null;
182
183
// Temporary ranges during interaction
184
tempRange: TempRange | null;
185
186
// Focused cell information
187
focused: FocusedCells | null;
188
189
// Last focused cell for keyboard navigation
190
lastCell: Cell | null;
191
192
// Edit mode state
193
edit: Edition.EditCell | null;
194
}
195
```
196
197
**TempRange** { .api }
198
```typescript
199
interface TempRange {
200
type: string;
201
area: RangeArea;
202
}
203
```
204
205
## Focus Events
206
207
### Focus Change Events
208
209
Listen for focus-related events:
210
211
**beforecellfocus** { .api }
212
```typescript
213
grid.addEventListener('beforecellfocus', (event) => {
214
const detail: Edition.BeforeSaveDataDetails = event.detail;
215
console.log('About to focus cell:', detail);
216
217
// Prevent focus change if needed
218
if (shouldPreventFocus(detail)) {
219
event.preventDefault();
220
}
221
});
222
```
223
224
**beforefocuslost** { .api }
225
```typescript
226
grid.addEventListener('beforefocuslost', (event) => {
227
const focusedData: FocusedData | null = event.detail;
228
console.log('About to lose focus:', focusedData);
229
230
// Prevent focus loss if needed (e.g., validation failed)
231
if (hasUnsavedChanges()) {
232
event.preventDefault();
233
showSavePrompt();
234
}
235
});
236
```
237
238
**afterfocus** { .api }
239
```typescript
240
grid.addEventListener('afterfocus', (event) => {
241
const { model, column } = event.detail;
242
console.log('Focus changed to:', { model, column });
243
244
// Update UI based on focused cell
245
updatePropertyPanel(model, column);
246
});
247
```
248
249
## Range Selection
250
251
### Range Selection Events
252
253
**beforerange** { .api }
254
```typescript
255
grid.addEventListener('beforerange', (event) => {
256
const rangeChange: Selection.ChangedRange = event.detail;
257
console.log('Range selection changing:', rangeChange);
258
});
259
```
260
261
**Selection Change Details** { .api }
262
```typescript
263
interface ChangedRange {
264
type: 'range' | 'focus';
265
newRange: RangeArea;
266
oldRange?: RangeArea;
267
source: 'keyboard' | 'mouse' | 'api';
268
}
269
```
270
271
### Range Operations
272
273
```typescript
274
class SelectionManager {
275
private grid: HTMLRevoGridElement;
276
277
constructor(grid: HTMLRevoGridElement) {
278
this.grid = grid;
279
this.setupRangeSelection();
280
}
281
282
private setupRangeSelection() {
283
// Enable range selection
284
this.grid.range = true;
285
286
// Listen for range changes
287
this.grid.addEventListener('beforerange', (event) => {
288
this.handleRangeChange(event.detail);
289
});
290
}
291
292
async selectRange(startCell: Selection.Cell, endCell: Selection.Cell) {
293
await this.grid.setCellsFocus(startCell, endCell);
294
}
295
296
async selectRow(rowIndex: number) {
297
const columns = await this.grid.getColumns();
298
const startCell = { x: 0, y: rowIndex };
299
const endCell = { x: columns.length - 1, y: rowIndex };
300
301
await this.selectRange(startCell, endCell);
302
}
303
304
async selectColumn(columnIndex: number) {
305
const data = await this.grid.getSource();
306
const startCell = { x: columnIndex, y: 0 };
307
const endCell = { x: columnIndex, y: data.length - 1 };
308
309
await this.selectRange(startCell, endCell);
310
}
311
312
async selectAll() {
313
const columns = await this.grid.getColumns();
314
const data = await this.grid.getSource();
315
316
const startCell = { x: 0, y: 0 };
317
const endCell = { x: columns.length - 1, y: data.length - 1 };
318
319
await this.selectRange(startCell, endCell);
320
}
321
322
async getSelectedData(): Promise<any[][]> {
323
const selection = await this.grid.getSelectedRange();
324
if (!selection) return [];
325
326
const data = await this.grid.getSource();
327
const columns = await this.grid.getColumns();
328
329
const result: any[][] = [];
330
331
for (let y = selection.y; y <= selection.y1; y++) {
332
const row: any[] = [];
333
for (let x = selection.x; x <= selection.x1; x++) {
334
const column = columns[x];
335
const rowData = data[y];
336
row.push(rowData[column.prop]);
337
}
338
result.push(row);
339
}
340
341
return result;
342
}
343
344
private handleRangeChange(rangeChange: Selection.ChangedRange) {
345
console.log('Selection changed:', rangeChange);
346
347
// Update external UI
348
this.updateSelectionInfo(rangeChange.newRange);
349
}
350
351
private updateSelectionInfo(range: Selection.RangeArea) {
352
const width = range.x1 - range.x + 1;
353
const height = range.y1 - range.y + 1;
354
355
// Update selection display
356
document.getElementById('selection-info').textContent =
357
`Selected: ${width} × ${height} cells`;
358
}
359
}
360
```
361
362
## Keyboard Navigation
363
364
### Navigation Keys
365
366
RevoGrid supports standard keyboard navigation:
367
368
- **Arrow Keys**: Move focus one cell in direction
369
- **Tab**: Move focus to next cell (right, then down)
370
- **Shift + Tab**: Move focus to previous cell (left, then up)
371
- **Home**: Move to first column in row
372
- **End**: Move to last column in row
373
- **Ctrl + Home**: Move to first cell (top-left)
374
- **Ctrl + End**: Move to last cell (bottom-right)
375
- **Page Up/Down**: Move one page up/down
376
377
### Range Selection Keys
378
379
When range selection is enabled:
380
381
- **Shift + Arrow**: Extend selection in direction
382
- **Shift + Click**: Extend selection to clicked cell
383
- **Ctrl + A**: Select all cells
384
- **Escape**: Clear selection
385
386
### Custom Navigation Handling
387
388
```typescript
389
class CustomNavigationHandler {
390
private grid: HTMLRevoGridElement;
391
392
constructor(grid: HTMLRevoGridElement) {
393
this.grid = grid;
394
this.setupCustomNavigation();
395
}
396
397
private setupCustomNavigation() {
398
// Listen for keyboard events
399
this.grid.addEventListener('keydown', (event) => {
400
this.handleKeyDown(event as KeyboardEvent);
401
});
402
}
403
404
private async handleKeyDown(event: KeyboardEvent) {
405
const focused = await this.grid.getFocused();
406
if (!focused) return;
407
408
switch (event.key) {
409
case 'Enter':
410
if (event.ctrlKey) {
411
// Ctrl+Enter: Start editing
412
await this.grid.setCellEdit(
413
focused.cell.y,
414
focused.column.prop,
415
focused.rowType
416
);
417
event.preventDefault();
418
} else {
419
// Enter: Move down
420
await this.moveFocus(focused.cell, 0, 1);
421
event.preventDefault();
422
}
423
break;
424
425
case 'F2':
426
// F2: Start editing current cell
427
await this.grid.setCellEdit(
428
focused.cell.y,
429
focused.column.prop,
430
focused.rowType
431
);
432
event.preventDefault();
433
break;
434
435
case 'Delete':
436
// Delete: Clear cell content
437
await this.clearCellContent(focused.cell);
438
event.preventDefault();
439
break;
440
441
case 'c':
442
if (event.ctrlKey) {
443
// Ctrl+C: Copy selection
444
await this.copySelection();
445
event.preventDefault();
446
}
447
break;
448
449
case 'v':
450
if (event.ctrlKey) {
451
// Ctrl+V: Paste
452
await this.pasteSelection();
453
event.preventDefault();
454
}
455
break;
456
}
457
}
458
459
private async moveFocus(currentCell: Selection.Cell, deltaX: number, deltaY: number) {
460
const newCell = {
461
x: Math.max(0, currentCell.x + deltaX),
462
y: Math.max(0, currentCell.y + deltaY)
463
};
464
465
// Validate bounds
466
const columns = await this.grid.getColumns();
467
const data = await this.grid.getSource();
468
469
newCell.x = Math.min(newCell.x, columns.length - 1);
470
newCell.y = Math.min(newCell.y, data.length - 1);
471
472
await this.grid.setCellsFocus(newCell);
473
}
474
475
private async clearCellContent(cell: Selection.Cell) {
476
const columns = await this.grid.getColumns();
477
const data = await this.grid.getSource();
478
479
const column = columns[cell.x];
480
const rowData = data[cell.y];
481
482
if (column && rowData && !column.readonly) {
483
rowData[column.prop] = '';
484
485
// Trigger refresh
486
await this.grid.refresh('rgRow');
487
}
488
}
489
490
private async copySelection() {
491
const selection = await this.grid.getSelectedRange();
492
if (!selection) return;
493
494
const selectedData = await this.getSelectionData(selection);
495
const csvContent = this.convertToCSV(selectedData);
496
497
// Copy to clipboard
498
await navigator.clipboard.writeText(csvContent);
499
}
500
501
private async pasteSelection() {
502
try {
503
const clipboardText = await navigator.clipboard.readText();
504
const pasteData = this.parseCSV(clipboardText);
505
506
const focused = await this.grid.getFocused();
507
if (focused) {
508
await this.pasteData(focused.cell, pasteData);
509
}
510
} catch (error) {
511
console.error('Paste failed:', error);
512
}
513
}
514
515
private convertToCSV(data: any[][]): string {
516
return data.map(row =>
517
row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(',')
518
).join('\n');
519
}
520
521
private parseCSV(csvText: string): string[][] {
522
// Simple CSV parser - enhance as needed
523
return csvText.split('\n').map(line =>
524
line.split(',').map(cell => cell.replace(/^"|"$/g, ''))
525
);
526
}
527
}
528
```
529
530
## Programmatic Navigation
531
532
### Scrolling to Specific Cells
533
534
**scrollToCoordinate** { .api }
535
```typescript
536
async scrollToCoordinate(cell: Partial<Selection.Cell>): Promise<void>
537
```
538
539
**scrollToRow** { .api }
540
```typescript
541
async scrollToRow(coordinate?: number): Promise<void>
542
```
543
544
**scrollToColumnIndex** { .api }
545
```typescript
546
async scrollToColumnIndex(coordinate?: number): Promise<void>
547
```
548
549
**scrollToColumnProp** { .api }
550
```typescript
551
async scrollToColumnProp(prop: RevoGrid.ColumnProp): Promise<void>
552
```
553
554
```typescript
555
// Scroll to specific cell and focus
556
async function navigateToCell(x: number, y: number) {
557
// First scroll to make cell visible
558
await grid.scrollToCoordinate({ x, y });
559
560
// Then set focus
561
await grid.setCellsFocus({ x, y });
562
}
563
564
// Navigate to specific row
565
async function navigateToRow(rowIndex: number) {
566
await grid.scrollToRow(rowIndex);
567
await grid.setCellsFocus({ x: 0, y: rowIndex });
568
}
569
570
// Navigate to column by property
571
async function navigateToColumn(prop: RevoGrid.ColumnProp) {
572
await grid.scrollToColumnProp(prop);
573
574
// Find column index and focus
575
const columns = await grid.getColumns();
576
const columnIndex = columns.findIndex(col => col.prop === prop);
577
if (columnIndex >= 0) {
578
await grid.setCellsFocus({ x: columnIndex, y: 0 });
579
}
580
}
581
```
582
583
## Usage Examples
584
585
### Complete Selection and Focus Manager
586
587
```typescript
588
class GridFocusManager {
589
private grid: HTMLRevoGridElement;
590
private selectionHistory: Selection.RangeArea[] = [];
591
private currentHistoryIndex = -1;
592
593
constructor(grid: HTMLRevoGridElement) {
594
this.grid = grid;
595
this.initialize();
596
}
597
598
private initialize() {
599
// Enable range selection
600
this.grid.range = true;
601
this.grid.canFocus = true;
602
603
// Setup event listeners
604
this.setupEventListeners();
605
606
// Setup keyboard shortcuts
607
this.setupKeyboardShortcuts();
608
}
609
610
private setupEventListeners() {
611
// Track selection changes for history
612
this.grid.addEventListener('beforerange', (event) => {
613
const rangeChange = event.detail;
614
this.addToHistory(rangeChange.newRange);
615
});
616
617
// Handle focus changes
618
this.grid.addEventListener('afterfocus', (event) => {
619
const { model, column } = event.detail;
620
this.onFocusChange(model, column);
621
});
622
}
623
624
private setupKeyboardShortcuts() {
625
document.addEventListener('keydown', (event) => {
626
if (event.target !== this.grid && !this.grid.contains(event.target as Node)) {
627
return;
628
}
629
630
// Custom shortcuts
631
if (event.ctrlKey && event.key === 'z') {
632
this.undoSelection();
633
event.preventDefault();
634
} else if (event.ctrlKey && event.key === 'y') {
635
this.redoSelection();
636
event.preventDefault();
637
}
638
});
639
}
640
641
private addToHistory(range: Selection.RangeArea) {
642
// Remove any future history if we're not at the end
643
this.selectionHistory = this.selectionHistory.slice(0, this.currentHistoryIndex + 1);
644
645
// Add new range to history
646
this.selectionHistory.push({ ...range });
647
this.currentHistoryIndex++;
648
649
// Limit history size
650
if (this.selectionHistory.length > 50) {
651
this.selectionHistory.shift();
652
this.currentHistoryIndex--;
653
}
654
}
655
656
private async undoSelection() {
657
if (this.currentHistoryIndex > 0) {
658
this.currentHistoryIndex--;
659
const range = this.selectionHistory[this.currentHistoryIndex];
660
await this.restoreSelection(range);
661
}
662
}
663
664
private async redoSelection() {
665
if (this.currentHistoryIndex < this.selectionHistory.length - 1) {
666
this.currentHistoryIndex++;
667
const range = this.selectionHistory[this.currentHistoryIndex];
668
await this.restoreSelection(range);
669
}
670
}
671
672
private async restoreSelection(range: Selection.RangeArea) {
673
const startCell = { x: range.x, y: range.y };
674
const endCell = { x: range.x1, y: range.y1 };
675
676
await this.grid.setCellsFocus(startCell, endCell);
677
await this.grid.scrollToCoordinate(startCell);
678
}
679
680
private onFocusChange(model: any, column: RevoGrid.ColumnRegular) {
681
// Update external UI with cell information
682
this.updateCellInfo(model, column);
683
}
684
685
private updateCellInfo(model: any, column: RevoGrid.ColumnRegular) {
686
const info = {
687
column: column.name || column.prop,
688
value: model[column.prop],
689
type: typeof model[column.prop],
690
readonly: column.readonly
691
};
692
693
// Emit custom event for external UI
694
document.dispatchEvent(new CustomEvent('grid-focus-changed', {
695
detail: info
696
}));
697
}
698
699
async selectEntireRow(rowIndex: number) {
700
const columns = await this.grid.getColumns();
701
await this.grid.setCellsFocus(
702
{ x: 0, y: rowIndex },
703
{ x: columns.length - 1, y: rowIndex }
704
);
705
}
706
707
async selectEntireColumn(columnIndex: number) {
708
const data = await this.grid.getSource();
709
await this.grid.setCellsFocus(
710
{ x: columnIndex, y: 0 },
711
{ x: columnIndex, y: data.length - 1 }
712
);
713
}
714
715
async findAndFocus(searchValue: any, columnProp?: RevoGrid.ColumnProp) {
716
const data = await this.grid.getSource();
717
const columns = await this.grid.getColumns();
718
719
// Search through data
720
for (let y = 0; y < data.length; y++) {
721
const row = data[y];
722
723
if (columnProp) {
724
// Search specific column
725
if (row[columnProp] === searchValue) {
726
const colIndex = columns.findIndex(col => col.prop === columnProp);
727
if (colIndex >= 0) {
728
await this.navigateAndFocus(colIndex, y);
729
return true;
730
}
731
}
732
} else {
733
// Search all columns
734
for (let x = 0; x < columns.length; x++) {
735
const column = columns[x];
736
if (row[column.prop] === searchValue) {
737
await this.navigateAndFocus(x, y);
738
return true;
739
}
740
}
741
}
742
}
743
744
return false; // Not found
745
}
746
747
private async navigateAndFocus(x: number, y: number) {
748
await this.grid.scrollToCoordinate({ x, y });
749
await this.grid.setCellsFocus({ x, y });
750
}
751
}
752
753
// Usage
754
const focusManager = new GridFocusManager(grid);
755
756
// Find and focus specific value
757
await focusManager.findAndFocus('John Doe', 'name');
758
759
// Select entire row
760
await focusManager.selectEntireRow(5);
761
762
// Listen for focus changes
763
document.addEventListener('grid-focus-changed', (event) => {
764
const { column, value, type, readonly } = event.detail;
765
console.log(`Focused on ${column}: ${value} (${type})`);
766
});
767
```
768
769
The selection and focus system provides comprehensive tools for building interactive grid applications with sophisticated navigation and selection capabilities.