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

cell-editing.mddocs/

0

# Cell Editing System

1

2

RevoGrid provides a comprehensive cell editing system with built-in editors, custom editor support, and advanced editing features including validation and range editing.

3

4

## Editing Configuration

5

6

### Basic Editing Setup

7

8

```typescript { .api }

9

interface EditingConfiguration {

10

readonly: boolean;

11

editors: Edition.Editors;

12

useClipboard: boolean;

13

}

14

```

15

16

**readonly** { .api }

17

```typescript

18

readonly: boolean = false

19

```

20

Global read-only mode. When `true`, prevents all cell editing across the grid.

21

22

**editors** { .api }

23

```typescript

24

editors: Edition.Editors = {}

25

```

26

Registry of custom cell editors. Maps editor names to editor constructors.

27

28

**useClipboard** { .api }

29

```typescript

30

useClipboard: boolean = true

31

```

32

Enable clipboard operations (copy/paste) with keyboard shortcuts.

33

34

### Column-Level Editing Control

35

36

Configure editing behavior per column:

37

38

```typescript

39

// Column with custom editor

40

{

41

prop: 'status',

42

name: 'Status',

43

editor: 'select', // Built-in select editor

44

readonly: false

45

}

46

47

// Column with conditional readonly

48

{

49

prop: 'amount',

50

name: 'Amount',

51

readonly: (params) => params.model.locked === true

52

}

53

54

// Column with custom editor instance

55

{

56

prop: 'date',

57

name: 'Date',

58

editor: CustomDateEditor

59

}

60

```

61

62

## Editor Types and Interfaces

63

64

### Base Editor Interface

65

66

```typescript { .api }

67

namespace Edition {

68

interface EditorBase {

69

element?: HTMLElement;

70

editCell?: EditCell;

71

}

72

73

interface EditorCtr {

74

new (column: RevoGrid.ColumnRegular, save?: (value: SaveData, preventFocus?: boolean) => void): EditorBase;

75

}

76

77

type Editors = {[name: string]: EditorCtr};

78

}

79

```

80

81

**EditorCtr** { .api }

82

```typescript

83

interface EditorCtr {

84

new (

85

column: RevoGrid.ColumnRegular,

86

save?: (value: SaveData, preventFocus?: boolean) => void

87

): EditorBase;

88

}

89

```

90

Constructor interface for creating editor instances.

91

92

### Edit Cell State

93

94

```typescript { .api }

95

interface EditCell {

96

x: number; // Column index

97

y: number; // Row index

98

val?: SaveData; // Current edit value

99

prop: RevoGrid.ColumnProp; // Column property

100

type: RevoGrid.DimensionRows; // Row dimension type

101

}

102

```

103

104

**SaveData** { .api }

105

```typescript

106

type SaveData = string;

107

```

108

Type for saved editor data (typically string representation).

109

110

## Built-in Editors

111

112

### Text Editor (Default)

113

114

The default text editor for simple text input:

115

116

```typescript

117

// Automatically used for columns without specific editor

118

{

119

prop: 'name',

120

name: 'Name'

121

// Uses default text editor

122

}

123

```

124

125

### Custom Editor Registration

126

127

Register custom editors in the editors registry:

128

129

```typescript

130

// Define custom editors

131

grid.editors = {

132

'select': SelectEditor,

133

'numeric': NumericEditor,

134

'date': DateEditor,

135

'checkbox': CheckboxEditor

136

};

137

```

138

139

## Creating Custom Editors

140

141

### Basic Custom Editor

142

143

```typescript

144

class CustomSelectEditor implements Edition.EditorBase {

145

public element: HTMLSelectElement;

146

public editCell: Edition.EditCell;

147

148

constructor(

149

public column: RevoGrid.ColumnRegular,

150

private save?: (value: Edition.SaveData, preventFocus?: boolean) => void

151

) {

152

this.createElement();

153

this.setupEventListeners();

154

}

155

156

private createElement() {

157

this.element = document.createElement('select');

158

this.element.className = 'revo-edit-select';

159

160

// Add options based on column configuration

161

const options = this.column.source || ['Option 1', 'Option 2', 'Option 3'];

162

options.forEach(option => {

163

const optionElement = document.createElement('option');

164

optionElement.value = option;

165

optionElement.textContent = option;

166

this.element.appendChild(optionElement);

167

});

168

}

169

170

private setupEventListeners() {

171

// Save on change

172

this.element.addEventListener('change', () => {

173

this.saveValue();

174

});

175

176

// Save on blur

177

this.element.addEventListener('blur', () => {

178

this.saveValue();

179

});

180

181

// Handle keyboard navigation

182

this.element.addEventListener('keydown', (e) => {

183

switch (e.key) {

184

case 'Escape':

185

this.cancel();

186

break;

187

case 'Enter':

188

case 'Tab':

189

this.saveValue();

190

break;

191

}

192

});

193

}

194

195

private saveValue() {

196

if (this.save) {

197

this.save(this.element.value);

198

}

199

}

200

201

private cancel() {

202

// Restore original value and exit edit mode

203

if (this.editCell && this.save) {

204

this.save(this.editCell.val || '');

205

}

206

}

207

}

208

```

209

210

### Advanced Custom Editor

211

212

```typescript

213

class NumericEditor implements Edition.EditorBase {

214

public element: HTMLInputElement;

215

public editCell: Edition.EditCell;

216

private originalValue: string = '';

217

218

constructor(

219

public column: RevoGrid.ColumnRegular,

220

private save?: (value: Edition.SaveData, preventFocus?: boolean) => void

221

) {

222

this.createElement();

223

this.setupValidation();

224

this.setupEventListeners();

225

}

226

227

private createElement() {

228

this.element = document.createElement('input');

229

this.element.type = 'number';

230

this.element.className = 'revo-edit-numeric';

231

232

// Apply column-specific configuration

233

if (this.column.min !== undefined) {

234

this.element.min = String(this.column.min);

235

}

236

if (this.column.max !== undefined) {

237

this.element.max = String(this.column.max);

238

}

239

if (this.column.step !== undefined) {

240

this.element.step = String(this.column.step);

241

}

242

}

243

244

private setupValidation() {

245

this.element.addEventListener('input', () => {

246

this.validateInput();

247

});

248

}

249

250

private validateInput() {

251

const value = this.element.value;

252

const numericValue = parseFloat(value);

253

254

// Remove invalid class first

255

this.element.classList.remove('invalid');

256

257

// Validate numeric value

258

if (value && isNaN(numericValue)) {

259

this.element.classList.add('invalid');

260

return false;

261

}

262

263

// Validate range

264

if (this.column.min !== undefined && numericValue < this.column.min) {

265

this.element.classList.add('invalid');

266

return false;

267

}

268

269

if (this.column.max !== undefined && numericValue > this.column.max) {

270

this.element.classList.add('invalid');

271

return false;

272

}

273

274

return true;

275

}

276

277

private setupEventListeners() {

278

// Store original value when editing starts

279

this.element.addEventListener('focus', () => {

280

this.originalValue = this.element.value;

281

});

282

283

// Save on Enter

284

this.element.addEventListener('keydown', (e) => {

285

switch (e.key) {

286

case 'Enter':

287

if (this.validateInput()) {

288

this.saveValue();

289

} else {

290

this.showValidationError();

291

}

292

e.preventDefault();

293

break;

294

295

case 'Escape':

296

this.cancel();

297

e.preventDefault();

298

break;

299

300

case 'Tab':

301

if (this.validateInput()) {

302

this.saveValue();

303

} else {

304

e.preventDefault();

305

this.showValidationError();

306

}

307

break;

308

}

309

});

310

311

// Save on blur if valid

312

this.element.addEventListener('blur', () => {

313

if (this.validateInput()) {

314

this.saveValue();

315

} else {

316

// Restore original value on invalid blur

317

this.element.value = this.originalValue;

318

this.element.classList.remove('invalid');

319

}

320

});

321

}

322

323

private saveValue() {

324

if (this.save && this.validateInput()) {

325

this.save(this.element.value);

326

}

327

}

328

329

private cancel() {

330

this.element.value = this.originalValue;

331

this.element.classList.remove('invalid');

332

if (this.save) {

333

this.save(this.originalValue);

334

}

335

}

336

337

private showValidationError() {

338

// Custom validation error handling

339

this.element.focus();

340

this.element.select();

341

342

// Show tooltip or other error indication

343

console.warn('Invalid numeric value');

344

}

345

}

346

```

347

348

## Editing Methods

349

350

### Programmatic Editing

351

352

**setCellEdit** { .api }

353

```typescript

354

async setCellEdit(

355

rgRow: number,

356

prop: RevoGrid.ColumnProp,

357

rowSource?: RevoGrid.DimensionRows

358

): Promise<void>

359

```

360

Enter edit mode for a specific cell programmatically.

361

362

- **rgRow**: Row index to edit

363

- **prop**: Column property to edit

364

- **rowSource**: Row dimension type (optional)

365

366

```typescript

367

// Start editing specific cell

368

await grid.setCellEdit(5, 'name');

369

370

// Edit cell in pinned row

371

await grid.setCellEdit(0, 'total', 'rowPinStart');

372

373

// Edit after focusing

374

const focused = await grid.getFocused();

375

if (focused) {

376

await grid.setCellEdit(

377

focused.cell.y,

378

focused.column.prop,

379

focused.rowType

380

);

381

}

382

```

383

384

## Editing Events

385

386

### Edit Lifecycle Events

387

388

**beforeeditstart** { .api }

389

```typescript

390

grid.addEventListener('beforeeditstart', (event) => {

391

const detail: Edition.BeforeSaveDataDetails = event.detail;

392

console.log('About to start editing:', detail);

393

394

// Prevent editing if conditions not met

395

if (!canEditCell(detail)) {

396

event.preventDefault();

397

}

398

});

399

```

400

401

**beforeedit** { .api }

402

```typescript

403

grid.addEventListener('beforeedit', (event) => {

404

const detail: Edition.BeforeSaveDataDetails = event.detail;

405

console.log('Before saving edit:', detail);

406

407

// Validate before saving

408

if (!validateEdit(detail)) {

409

event.preventDefault();

410

showValidationError(detail);

411

}

412

});

413

```

414

415

**afteredit** { .api }

416

```typescript

417

grid.addEventListener('afteredit', (event) => {

418

const detail: Edition.BeforeSaveDataDetails = event.detail;

419

console.log('Edit completed:', detail);

420

421

// Handle post-edit actions

422

onEditComplete(detail);

423

});

424

```

425

426

### Edit Event Details

427

428

```typescript { .api }

429

interface BeforeSaveDataDetails {

430

// Original model data

431

model: RevoGrid.DataType;

432

433

// Column being edited

434

prop: RevoGrid.ColumnProp;

435

436

// New value being saved

437

val: Edition.SaveData;

438

439

// Row index

440

rowIndex: number;

441

442

// Column definition

443

column: RevoGrid.ColumnRegular;

444

445

// Additional context

446

type: RevoGrid.DimensionRows;

447

}

448

```

449

450

## Range Editing

451

452

### Range Edit Events

453

454

**beforerangeedit** { .api }

455

```typescript

456

grid.addEventListener('beforerangeedit', (event) => {

457

const detail: Edition.BeforeRangeSaveDataDetails = event.detail;

458

console.log('Before range edit:', detail);

459

460

// Validate all changes in range

461

if (!validateRangeEdit(detail)) {

462

event.preventDefault();

463

}

464

});

465

```

466

467

**Range Edit Details** { .api }

468

```typescript

469

interface BeforeRangeSaveDataDetails {

470

// Array of all changes in the range

471

data: BeforeSaveDataDetails[];

472

473

// Range area being edited

474

range: Selection.RangeArea;

475

476

// Edit source (paste, autofill, etc.)

477

source: 'paste' | 'autofill' | 'edit';

478

}

479

```

480

481

### Implementing Range Operations

482

483

```typescript

484

class RangeEditManager {

485

private grid: HTMLRevoGridElement;

486

487

constructor(grid: HTMLRevoGridElement) {

488

this.grid = grid;

489

this.setupRangeEditing();

490

}

491

492

private setupRangeEditing() {

493

// Enable range selection for range editing

494

this.grid.range = true;

495

496

// Listen for range edit events

497

this.grid.addEventListener('beforerangeedit', (event) => {

498

this.handleRangeEdit(event);

499

});

500

}

501

502

private handleRangeEdit(event: CustomEvent) {

503

const detail = event.detail as Edition.BeforeRangeSaveDataDetails;

504

505

// Validate each change in the range

506

for (const change of detail.data) {

507

if (!this.validateSingleChange(change)) {

508

event.preventDefault();

509

this.showRangeEditError(change);

510

return;

511

}

512

}

513

514

// Log range edit for audit

515

this.logRangeEdit(detail);

516

}

517

518

private validateSingleChange(change: Edition.BeforeSaveDataDetails): boolean {

519

const { prop, val, column } = change;

520

521

// Check if cell is readonly

522

if (column.readonly) {

523

return false;

524

}

525

526

// Validate by data type

527

switch (column.type) {

528

case 'numeric':

529

return !isNaN(Number(val));

530

case 'email':

531

return this.isValidEmail(val);

532

case 'date':

533

return !isNaN(Date.parse(val));

534

default:

535

return true;

536

}

537

}

538

539

private isValidEmail(email: string): boolean {

540

const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

541

return emailRegex.test(email);

542

}

543

544

private showRangeEditError(change: Edition.BeforeSaveDataDetails) {

545

console.error('Range edit validation failed:', change);

546

// Show user-friendly error message

547

}

548

549

private logRangeEdit(detail: Edition.BeforeRangeSaveDataDetails) {

550

console.log(`Range edit: ${detail.data.length} cells modified via ${detail.source}`);

551

}

552

553

async fillRange(value: any) {

554

const selection = await this.grid.getSelectedRange();

555

if (!selection) return;

556

557

const data = await this.grid.getSource();

558

const columns = await this.grid.getColumns();

559

560

// Create range edit data

561

const rangeData: Edition.BeforeSaveDataDetails[] = [];

562

563

for (let y = selection.y; y <= selection.y1; y++) {

564

for (let x = selection.x; x <= selection.x1; x++) {

565

const column = columns[x];

566

const rowData = data[y];

567

568

rangeData.push({

569

model: rowData,

570

prop: column.prop,

571

val: String(value),

572

rowIndex: y,

573

column: column,

574

type: 'rgRow'

575

});

576

}

577

}

578

579

// Trigger range edit event

580

const event = new CustomEvent('beforerangeedit', {

581

detail: {

582

data: rangeData,

583

range: selection,

584

source: 'api'

585

},

586

cancelable: true

587

});

588

589

this.grid.dispatchEvent(event);

590

591

if (!event.defaultPrevented) {

592

// Apply the changes

593

this.applyRangeChanges(rangeData);

594

}

595

}

596

597

private async applyRangeChanges(changes: Edition.BeforeSaveDataDetails[]) {

598

// Group changes by row for efficient updates

599

const changesByRow = new Map<number, Edition.BeforeSaveDataDetails[]>();

600

601

changes.forEach(change => {

602

if (!changesByRow.has(change.rowIndex)) {

603

changesByRow.set(change.rowIndex, []);

604

}

605

changesByRow.get(change.rowIndex)!.push(change);

606

});

607

608

// Apply changes row by row

609

const rowStore = await this.grid.getSourceStore();

610

611

changesByRow.forEach((rowChanges, rowIndex) => {

612

const currentRow = rowStore.getRow(rowIndex);

613

const updatedRow = { ...currentRow };

614

615

rowChanges.forEach(change => {

616

updatedRow[change.prop] = change.val;

617

});

618

619

rowStore.setRow(rowIndex, updatedRow);

620

});

621

622

// Refresh the grid

623

await this.grid.refresh();

624

}

625

}

626

```

627

628

## Autofill Feature

629

630

### Autofill Events

631

632

**beforeautofill** { .api }

633

```typescript

634

grid.addEventListener('beforeautofill', (event) => {

635

const rangeChange: Selection.ChangedRange = event.detail;

636

console.log('Before autofill:', rangeChange);

637

638

// Customize autofill behavior

639

if (!allowAutofill(rangeChange)) {

640

event.preventDefault();

641

}

642

});

643

```

644

645

### Custom Autofill Implementation

646

647

```typescript

648

class AutofillManager {

649

private grid: HTMLRevoGridElement;

650

651

constructor(grid: HTMLRevoGridElement) {

652

this.grid = grid;

653

this.setupAutofill();

654

}

655

656

private setupAutofill() {

657

this.grid.addEventListener('beforeautofill', (event) => {

658

this.handleAutofill(event);

659

});

660

}

661

662

private async handleAutofill(event: CustomEvent) {

663

const rangeChange = event.detail as Selection.ChangedRange;

664

665

// Get source and target ranges

666

const sourceRange = rangeChange.oldRange;

667

const targetRange = rangeChange.newRange;

668

669

if (!sourceRange || !targetRange) return;

670

671

// Implement custom autofill logic

672

const autofillData = await this.generateAutofillData(sourceRange, targetRange);

673

674

// Apply autofill data

675

if (autofillData.length > 0) {

676

this.applyAutofillData(autofillData);

677

}

678

}

679

680

private async generateAutofillData(

681

sourceRange: Selection.RangeArea,

682

targetRange: Selection.RangeArea

683

): Promise<Edition.BeforeSaveDataDetails[]> {

684

const data = await this.grid.getSource();

685

const columns = await this.grid.getColumns();

686

const autofillData: Edition.BeforeSaveDataDetails[] = [];

687

688

// Determine fill direction and pattern

689

const isHorizontal = targetRange.y === sourceRange.y && targetRange.y1 === sourceRange.y1;

690

const isVertical = targetRange.x === sourceRange.x && targetRange.x1 === sourceRange.x1;

691

692

if (isVertical) {

693

// Vertical autofill

694

for (let x = sourceRange.x; x <= sourceRange.x1; x++) {

695

const column = columns[x];

696

const sourceValues = this.getColumnValues(data, sourceRange, x);

697

const pattern = this.detectPattern(sourceValues);

698

699

let fillIndex = 0;

700

for (let y = Math.max(targetRange.y, sourceRange.y1 + 1); y <= targetRange.y1; y++) {

701

const fillValue = this.generateNextValue(pattern, fillIndex);

702

703

autofillData.push({

704

model: data[y],

705

prop: column.prop,

706

val: String(fillValue),

707

rowIndex: y,

708

column: column,

709

type: 'rgRow'

710

});

711

712

fillIndex++;

713

}

714

}

715

}

716

717

return autofillData;

718

}

719

720

private getColumnValues(

721

data: RevoGrid.DataType[],

722

range: Selection.RangeArea,

723

columnIndex: number

724

): any[] {

725

const columns = this.grid.columns;

726

const column = columns[columnIndex];

727

const values: any[] = [];

728

729

for (let y = range.y; y <= range.y1; y++) {

730

values.push(data[y][column.prop]);

731

}

732

733

return values;

734

}

735

736

private detectPattern(values: any[]): AutofillPattern {

737

// Detect numeric sequence

738

if (values.every(v => !isNaN(Number(v)))) {

739

const numbers = values.map(v => Number(v));

740

const differences = [];

741

742

for (let i = 1; i < numbers.length; i++) {

743

differences.push(numbers[i] - numbers[i - 1]);

744

}

745

746

// Check for arithmetic sequence

747

if (differences.every(d => d === differences[0])) {

748

return {

749

type: 'arithmetic',

750

increment: differences[0],

751

lastValue: numbers[numbers.length - 1]

752

};

753

}

754

}

755

756

// Detect date sequence

757

if (values.every(v => !isNaN(Date.parse(v)))) {

758

const dates = values.map(v => new Date(v));

759

// Implement date pattern detection

760

return {

761

type: 'date',

762

increment: 1, // Default: 1 day

763

lastValue: dates[dates.length - 1]

764

};

765

}

766

767

// Default: repeat pattern

768

return {

769

type: 'repeat',

770

values: values

771

};

772

}

773

774

private generateNextValue(pattern: AutofillPattern, index: number): any {

775

switch (pattern.type) {

776

case 'arithmetic':

777

return pattern.lastValue + pattern.increment * (index + 1);

778

779

case 'date':

780

const nextDate = new Date(pattern.lastValue);

781

nextDate.setDate(nextDate.getDate() + pattern.increment * (index + 1));

782

return nextDate.toISOString().split('T')[0];

783

784

case 'repeat':

785

return pattern.values[index % pattern.values.length];

786

787

default:

788

return '';

789

}

790

}

791

792

private applyAutofillData(autofillData: Edition.BeforeSaveDataDetails[]) {

793

// Similar to range edit application

794

// Implementation would be similar to applyRangeChanges in RangeEditManager

795

}

796

}

797

798

interface AutofillPattern {

799

type: 'arithmetic' | 'date' | 'repeat';

800

increment?: number;

801

lastValue?: any;

802

values?: any[];

803

}

804

```

805

806

## Usage Examples

807

808

### Complete Editing System Setup

809

810

```typescript

811

class GridEditingSystem {

812

private grid: HTMLRevoGridElement;

813

private editHistory: EditHistoryItem[] = [];

814

815

constructor(grid: HTMLRevoGridElement) {

816

this.grid = grid;

817

this.setupEditing();

818

}

819

820

private setupEditing() {

821

// Register custom editors

822

this.grid.editors = {

823

'select': SelectEditor,

824

'numeric': NumericEditor,

825

'date': DateEditor,

826

'email': EmailEditor,

827

'currency': CurrencyEditor

828

};

829

830

// Setup editing events

831

this.setupEditingEvents();

832

833

// Setup validation

834

this.setupValidation();

835

836

// Setup edit history

837

this.setupEditHistory();

838

}

839

840

private setupEditingEvents() {

841

this.grid.addEventListener('beforeeditstart', (event) => {

842

const detail = event.detail;

843

844

// Check permissions

845

if (!this.hasEditPermission(detail)) {

846

event.preventDefault();

847

this.showPermissionError();

848

return;

849

}

850

851

// Log edit start

852

console.log('Edit started:', detail);

853

});

854

855

this.grid.addEventListener('beforeedit', (event) => {

856

const detail = event.detail;

857

858

// Validate the change

859

const validation = this.validateEdit(detail);

860

if (!validation.isValid) {

861

event.preventDefault();

862

this.showValidationError(validation.error);

863

return;

864

}

865

866

// Store in history before applying

867

this.addToHistory(detail);

868

});

869

870

this.grid.addEventListener('afteredit', (event) => {

871

const detail = event.detail;

872

873

// Update dependent calculations

874

this.updateDependentCells(detail);

875

876

// Save to server

877

this.saveToServer(detail);

878

879

// Notify other systems

880

this.notifyEditComplete(detail);

881

});

882

}

883

884

private validateEdit(detail: Edition.BeforeSaveDataDetails): ValidationResult {

885

const { prop, val, column } = detail;

886

887

// Type-specific validation

888

switch (column.type) {

889

case 'numeric':

890

const numValue = Number(val);

891

if (isNaN(numValue)) {

892

return { isValid: false, error: 'Must be a valid number' };

893

}

894

if (column.min !== undefined && numValue < column.min) {

895

return { isValid: false, error: `Must be at least ${column.min}` };

896

}

897

if (column.max !== undefined && numValue > column.max) {

898

return { isValid: false, error: `Must be at most ${column.max}` };

899

}

900

break;

901

902

case 'email':

903

if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)) {

904

return { isValid: false, error: 'Must be a valid email address' };

905

}

906

break;

907

908

case 'required':

909

if (!val || val.trim() === '') {

910

return { isValid: false, error: 'This field is required' };

911

}

912

break;

913

}

914

915

// Custom validation functions

916

if (column.validate) {

917

const customResult = column.validate(val, detail.model);

918

if (!customResult.isValid) {

919

return customResult;

920

}

921

}

922

923

return { isValid: true };

924

}

925

926

private hasEditPermission(detail: Edition.BeforeSaveDataDetails): boolean {

927

// Implement permission checking logic

928

return true; // Simplified

929

}

930

931

private addToHistory(detail: Edition.BeforeSaveDataDetails) {

932

this.editHistory.push({

933

timestamp: new Date(),

934

change: { ...detail },

935

originalValue: detail.model[detail.prop]

936

});

937

938

// Limit history size

939

if (this.editHistory.length > 100) {

940

this.editHistory.shift();

941

}

942

}

943

944

async undoLastEdit() {

945

if (this.editHistory.length === 0) return;

946

947

const lastEdit = this.editHistory.pop();

948

if (lastEdit) {

949

// Restore original value

950

const rowStore = await this.grid.getSourceStore();

951

const currentRow = rowStore.getRow(lastEdit.change.rowIndex);

952

currentRow[lastEdit.change.prop] = lastEdit.originalValue;

953

rowStore.setRow(lastEdit.change.rowIndex, currentRow);

954

955

await this.grid.refresh();

956

}

957

}

958

959

private updateDependentCells(detail: Edition.BeforeSaveDataDetails) {

960

// Example: Update total when amount changes

961

if (detail.prop === 'amount') {

962

this.recalculateTotals();

963

}

964

}

965

966

private async recalculateTotals() {

967

const data = await this.grid.getSource();

968

let total = 0;

969

970

data.forEach(row => {

971

total += Number(row.amount) || 0;

972

});

973

974

// Update summary row

975

this.grid.pinnedBottomSource = [

976

{ ...this.grid.pinnedBottomSource[0], total }

977

];

978

}

979

}

980

981

interface ValidationResult {

982

isValid: boolean;

983

error?: string;

984

}

985

986

interface EditHistoryItem {

987

timestamp: Date;

988

change: Edition.BeforeSaveDataDetails;

989

originalValue: any;

990

}

991

```

992

993

The cell editing system provides powerful capabilities for creating rich, interactive grid applications with comprehensive validation, custom editors, and advanced editing features.