or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

cell-editing.mdcolumn-system.mdcore-grid.mddata-management.mdevent-system.mdindex.mdnavigation-scrolling.mdplugin-system.mdselection-focus.mdtypes-interfaces.md

selection-focus.mddocs/

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.