# EditingPlugin

> _Since v0.4.0_

Editing Plugin for tbw-grid

Enables inline cell editing in the grid. Provides built-in editors for common data types
and supports custom editor functions for specialized input scenarios.

## Why Opt-In?

Editing is delivered as a plugin rather than built into the core grid:

- **Smaller bundle** — Apps that only display data don't pay for editing code
- **Clear intent** — Explicit plugin registration makes editing capability obvious
- **Runtime validation** — Using `editable: true` without the plugin throws a helpful error

## Installation

```ts
import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';
```

## Edit Triggers

Configure how editing is triggered with the `editOn` option:

| Value | Behavior |
|-------|----------|
| `'click'` | Single click enters edit mode (default) |
| `'dblclick'` | Double-click enters edit mode |

## Keyboard Shortcuts

| Key | Action |
|-----|--------|
| `Enter` | Commit edit and move down |
| `Tab` | Commit edit and move right |
| `Escape` | Cancel edit, restore original value |
| `Arrow Keys` | Navigate between cells (when not editing) |

## [Configuration Options](/grid/plugins/editing/interfaces/editingconfig.md)

| Option | Type | Description |
| ------ | ---- | ----------- |
| `mode?` | <code>row &#124; grid</code> | Editing mode that determines how many rows are editable at once. |
| `dirtyTracking?` | <code>boolean</code> | Enable per-row dirty tracking against deep-cloned baselines. |
| `editOn?` | <code>false &#124; manual &#124; click &#124; dblclick</code> | Controls when editing is triggered (only applies to `mode: 'row'`). - 'click': Edit on single click (default) - 'dblclick': Edit on double click - 'manual': Only via programmatic API (beginEdit) - false: Disable editing entirely |
| `onBeforeEditClose?` | <code>(event: MouseEvent &#124; KeyboardEvent) =&gt; boolean</code> | Callback invoked before a row edit session closes. |
| `focusTrap?` | <code>boolean</code> | When `true`, prevents focus from leaving the grid (or its registered external focus containers) while a row is in edit mode. |

## Examples

### Basic editing with double-click trigger

```ts
grid.gridConfig = {
  columns: [
    { field: 'name', editable: true },
    { field: 'price', type: 'number', editable: true },
    { field: 'active', type: 'boolean', editable: true },
  ],
  plugins: [new EditingPlugin({ editOn: 'dblclick' })],
};

grid.on('cell-commit', ({ field, oldValue, newValue }) => {
  console.log(`${field}: ${oldValue} → ${newValue}`);
});
```

### Custom editor function

```ts
columns: [
  {
    field: 'status',
    editable: true,
    editor: (ctx) => {
      const select = document.createElement('select');
      ['pending', 'active', 'completed'].forEach(opt => {
        const option = document.createElement('option');
        option.value = opt;
        option.textContent = opt;
        option.selected = ctx.value === opt;
        select.appendChild(option);
      });
      select.addEventListener('change', () => ctx.commit(select.value));
      return select;
    },
  },
]
```

## See Also

- [`EditingConfig`](/grid/plugins/editing/interfaces/editingconfig.md) for configuration options
- `ColumnEditorContext` for custom editor context
- [`EditingConfig`](/grid/plugins/editing/interfaces/editingconfig.md) for interactive examples in the docs site

> **Extends** [BaseGridPlugin](/docs/grid-api-plugin-development-classes-basegridplugin--docs)
>
> Inherited methods like `attach()`, `detach()`, `afterRender()`, etc. are documented in the base class.

## Events

| Event | Description | Triggered By |
| ----- | ----------- | ------------ |
| `changed-rows-reset` | Emitted when tracking is reset (unless silent) | `resetChangedRows()` |
| `cell-commit` | Emitted when the cell value is committed (on blur or Enter) | `beginCellEdit()`, `beginBulkEdit()` |
| `row-commit` | Emitted after the row edit is committed | `beginBulkEdit()`, `commitActiveRowEdit()` |

## Accessors

### changedRows

Get all rows that have been modified.
Uses ID-based lookup for stability when rows are reordered.

```ts
readonly changedRows: T[]
```

***

### changedRowIds

Get IDs of all modified rows.

```ts
readonly changedRowIds: string[]
```

***

### activeEditRow

Get the currently active edit row index, or -1 if not editing.

```ts
readonly activeEditRow: number
```

***

### activeEditCol

Get the currently active edit column index, or -1 if not editing.

```ts
readonly activeEditCol: number
```

***

### dirty

Whether any row in the grid is dirty.

```ts
readonly dirty: boolean
```

***

### pristine

Whether all rows are pristine.

```ts
readonly pristine: boolean
```

***

### dirtyRowIds

Get IDs of all dirty rows.

```ts
readonly dirtyRowIds: string[]
```

***

## Methods

### isRowEditing()

Check if a specific row is currently being edited.

```ts
isRowEditing(rowIndex: number): boolean
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `rowIndex` | <code>number</code> |  |

***

### isCellEditing()

Check if a specific cell is currently being edited.

```ts
isCellEditing(rowIndex: number, colIndex: number): boolean
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `rowIndex` | <code>number</code> |  |
| `colIndex` | <code>number</code> |  |

***

### isRowChanged()

Check if a specific row has been modified.

```ts
isRowChanged(rowIndex: number): boolean
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `rowIndex` | <code>number</code> | Row index to check (will be converted to ID internally) |

***

### isRowChangedById()

Check if a row with the given ID has been modified.

```ts
isRowChangedById(rowId: string): boolean
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `rowId` | <code>string</code> | Row ID to check |

***

### isDirty()

Check if a row differs from its baseline. Requires `dirtyTracking: true`.

```ts
isDirty(rowId: string): boolean
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `rowId` | <code>string</code> |  |

***

### isPristine()

Inverse of `isDirty`.

```ts
isPristine(rowId: string): boolean
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `rowId` | <code>string</code> |  |

***

### markAsPristine()

Re-snapshot baseline from current data (call after a successful save).

```ts
markAsPristine(rowId: string): void
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `rowId` | <code>string</code> |  |

***

### markAsNew()

Mark a row as new (auto-called by `insertRow()` when dirty tracking is on).

```ts
markAsNew(rowId: string): void
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `rowId` | <code>string</code> |  |

***

### markAsDirty()

Mark a row as dirty after an external mutation that bypassed the editing pipeline.

```ts
markAsDirty(rowId: string): void
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `rowId` | <code>string</code> |  |

***

### markAllPristine()

Mark all tracked rows as pristine (call after a batch save).

```ts
markAllPristine(): void
```

***

### getOriginalRow()

Get a deep clone of the original (baseline) row data. Returns `undefined` for new rows.

```ts
getOriginalRow(rowId: string): T | undefined
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `rowId` | <code>string</code> |  |

***

### hasBaseline()

Lightweight check for whether a baseline exists (no cloning).

```ts
hasBaseline(rowId: string): boolean
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `rowId` | <code>string</code> |  |

***

### getDirtyRows()

Get all dirty rows with their original and current data.

```ts
getDirtyRows(): DirtyRowEntry<T>[]
```

***

### revertRow()

Revert a row to its baseline values (mutates the current row in-place).
Triggers a re-render.

```ts
revertRow(rowId: string): void
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `rowId` | <code>string</code> | Row ID (from `getRowId`) |

***

### revertAll()

Revert all dirty rows to their baseline values and re-render.

```ts
revertAll(): void
```

***

### setInvalid()

Mark a cell as invalid with an optional validation message.
Invalid cells are marked with a `data-invalid` attribute for styling.

```ts
setInvalid(rowId: string, field: string, message: string): void
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `rowId` | <code>string</code> | The row ID (from getRowId) |
| `field` | <code>string</code> | The field name |
| `message` | <code>string</code> | Optional validation message (for tooltips or display) |

#### Example

```typescript
// In cell-commit handler:
grid.on('cell-commit', (detail, e) => {
  if (detail.field === 'email' && !isValidEmail(detail.value)) {
    detail.setInvalid('Invalid email format');
  }
});

// Or programmatically:
editingPlugin.setInvalid('row-123', 'email', 'Invalid email format');
```

***

### clearInvalid()

Clear the invalid state for a specific cell.

```ts
clearInvalid(rowId: string, field: string): void
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `rowId` | <code>string</code> |  |
| `field` | <code>string</code> |  |

***

### clearRowInvalid()

Clear all invalid cells for a specific row.

```ts
clearRowInvalid(rowId: string): void
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `rowId` | <code>string</code> |  |

***

### clearAllInvalid()

Clear all invalid cell states across all rows.

```ts
clearAllInvalid(): void
```

***

### isCellInvalid()

Check if a specific cell is marked as invalid.

```ts
isCellInvalid(rowId: string, field: string): boolean
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `rowId` | <code>string</code> |  |
| `field` | <code>string</code> |  |

***

### getInvalidMessage()

Get the validation message for an invalid cell.

```ts
getInvalidMessage(rowId: string, field: string): string | undefined
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `rowId` | <code>string</code> |  |
| `field` | <code>string</code> |  |

***

### hasInvalidCells()

Check if a row has any invalid cells.

```ts
hasInvalidCells(rowId: string): boolean
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `rowId` | <code>string</code> |  |

***

### getInvalidFields()

Get all invalid fields for a row.

```ts
getInvalidFields(rowId: string): Map<string, string>
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `rowId` | <code>string</code> |  |

***

### resetChangedRows()

Reset all change tracking.

```ts
resetChangedRows(silent: boolean): void
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `silent` | <code>boolean</code> | If true, suppresses the `changed-rows-reset` event |

#### Events

| Event | Description |
| ----- | ----------- |
| `changed-rows-reset` | Emitted when tracking is reset (unless silent) |

***

### beginCellEdit()

Programmatically begin editing a cell.

```ts
beginCellEdit(rowIndex: number, field: string): void
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `rowIndex` | <code>number</code> | Index of the row to edit |
| `field` | <code>string</code> | Field name of the column to edit |

#### Events

| Event | Description |
| ----- | ----------- |
| `cell-commit` | Emitted when the cell value is committed (on blur or Enter) |

***

### beginBulkEdit()

Programmatically begin editing all editable cells in a row.

```ts
beginBulkEdit(rowIndex: number): void
```

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| `rowIndex` | <code>number</code> | Index of the row to edit |

#### Events

| Event | Description |
| ----- | ----------- |
| `cell-commit` | Emitted for each cell value that is committed |
| `row-commit` | Emitted when focus leaves the row |

***

### commitActiveRowEdit()

Commit the currently active row edit.

```ts
commitActiveRowEdit(): void
```

#### Events

| Event | Description |
| ----- | ----------- |
| `row-commit` | Emitted after the row edit is committed |

***

### cancelActiveRowEdit()

Cancel the currently active row edit.

```ts
cancelActiveRowEdit(): void
```

***
