# Multi-Sort Plugin

> Sort by multiple columns with shift-click support.

This plugin is not needed if you only want single-column sorting—just set `sortable: true` on the columns. This is built in to the core. Multi-Sort extends that functionality by enabling sorting by multiple columns at once—hold Shift and click additional column headers to build up a sort stack. Priority badges show the sort order, so users always know which column takes precedence.

## Installation

```ts
import '@toolbox-web/grid/features/multi-sort';
```

## Basic Usage

Mark columns as `sortable: true` and enable the feature. Users Shift+click to add columns to the sort stack, and regular click to reset to single-column sort.

#### TypeScript

```ts
import { queryGrid } from '@toolbox-web/grid';

const grid = queryGrid('tbw-grid');
grid.gridConfig = {
  columns: [
    { field: 'name', header: 'Name', sortable: true },
    { field: 'department', header: 'Department', sortable: true },
    { field: 'salary', header: 'Salary', type: 'number', sortable: true },
    { field: 'startDate', header: 'Start Date', type: 'date', sortable: true },
  ],
  features: {
    multiSort: {
      maxSortColumns: 3,
      showSortIndex: true,
    },
  },
};

// Listen for sort changes
grid.on('sort-change', ({ sortModel }) => {
  console.log('Active sorts:', sortModel);
});
```

#### React

```tsx
import '@toolbox-web/grid-react/features/multi-sort';
import { DataGrid } from '@toolbox-web/grid-react';

function SortableGrid({ data }) {
  return (
    <DataGrid
      rows={data}
      columns={[
        { field: 'name', header: 'Name', sortable: true },
        { field: 'department', header: 'Department', sortable: true },
        { field: 'salary', header: 'Salary', type: 'number', sortable: true },
      ]}
      multiSort={{ maxSortColumns: 3 }}
      onSortChange={(detail) => console.log('Sort changed:', detail.sortModel)}
    />
  );
}
```

#### Vue

```html
<script setup>
import '@toolbox-web/grid-vue/features/multi-sort';
import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';

const data = [
  { name: 'Alice', department: 'Engineering', salary: 95000 },
  { name: 'Bob', department: 'Marketing', salary: 75000 },
];

const onSortChange = (e) => {
  console.log('Sort changed:', e.detail.sortModel);
};
</script>

<template>
  <TbwGrid
    :rows="data"
    :multi-sort="{ maxSortColumns: 3 }"
    @sort-change="onSortChange"
  >
    <TbwGridColumn field="name" header="Name" sortable />
    <TbwGridColumn field="department" header="Department" sortable />
    <TbwGridColumn field="salary" header="Salary" type="number" sortable />
  </TbwGrid>
</template>
```

#### Angular

```typescript
// Feature import - enables the [multiSort] input
import { GridMultiSortDirective } from '@toolbox-web/grid-angular/features/multi-sort';
import { Component } from '@angular/core';
import { Grid } from '@toolbox-web/grid-angular';
import type { ColumnConfig } from '@toolbox-web/grid';

@Component({
  selector: 'app-sortable-grid',
  imports: [Grid, GridMultiSortDirective],
  template: `
    <tbw-grid
      [rows]="rows"
      [columns]="columns"
      [multiSort]="{ maxSortColumns: 3 }"
      (sort-change)="onSortChange($event)"
      style="height: 400px; display: block;">
    </tbw-grid>
  `,
})
export class SortableGridComponent {
  rows = [
    { name: 'Alice', department: 'Engineering', salary: 95000 },
    { name: 'Bob', department: 'Marketing', salary: 75000 },
  ];

  columns: ColumnConfig[] = [
    { field: 'name', header: 'Name', sortable: true },
    { field: 'department', header: 'Department', sortable: true },
    { field: 'salary', header: 'Salary', type: 'number', sortable: true },
  ];

  onSortChange(e: CustomEvent) {
    console.log('Sort changed:', e.detail.sortModel);
  }
}
```

## Demo

Use the controls to explore different multi-sort configurations: adjust the maximum number of sort levels, toggle priority badges, or apply an initial sort.

```ts
// MultiSortDefaultDemo.astro
import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/multi-sort';

const container = document.getElementById('multi-sort-default-demo');
if (container) {
  const sampleData = [
    { id: 1, name: 'Alice', department: 'Engineering', salary: 95000, joined: '2023-01-15' },
    { id: 2, name: 'Bob', department: 'Marketing', salary: 75000, joined: '2022-06-20' },
    { id: 3, name: 'Carol', department: 'Engineering', salary: 105000, joined: '2021-03-10' },
    { id: 4, name: 'Dan', department: 'Engineering', salary: 85000, joined: '2023-08-05' },
    { id: 5, name: 'Eve', department: 'Marketing', salary: 72000, joined: '2024-01-12' },
    { id: 6, name: 'Frank', department: 'Sales', salary: 82000, joined: '2022-11-30' },
  ];
  const columns = [
    { field: 'id', header: 'ID', type: 'number', sortable: true },
    { field: 'name', header: 'Name', sortable: true },
    { field: 'department', header: 'Department', sortable: true },
    { field: 'salary', header: 'Salary', type: 'number', sortable: true },
    { field: 'joined', header: 'Joined', type: 'date', sortable: true },
  ];

  const grid = queryGrid('tbw-grid', container)!;

  function rebuild(maxSortColumns = 3, showSortIndex = true, initialSort = false) {
    grid.gridConfig = {
      columns,
      features: { multiSort: { maxSortColumns, showSortIndex } },
    };
    grid.rows = sampleData;

    if (initialSort) {
      grid.ready().then(() => {
        grid.getPluginByName('multiSort')?.setSortModel([
          { field: 'department', direction: 'asc' },
          { field: 'salary', direction: 'desc' },
        ]);
      });
    }
  }

  rebuild();

  container.addEventListener('control-change', ((e: CustomEvent) => {
    const v = e.detail.allValues;
    rebuild(v.maxSortColumns as number, v.showSortIndex as boolean, v.initialSort as boolean);
  }) as EventListener);
}
```

Hold **Shift** and click column headers to add columns to the sort stack. Set **Max sort columns** to `1` for single-column-only sorting.

## Configuration Options

See [`MultiSortConfig`](./interfaces/multisortconfig/) for the full list of options and their defaults.

## Initial Sort

Use `setSortModel()` to set a pre-configured sort order after the grid initializes:

```ts
const plugin = grid.getPluginByName('multiSort');
plugin.setSortModel([
  { field: 'department', direction: 'asc' },
  { field: 'salary', direction: 'desc' },
]);
```

## Events

The grid fires a `sort-change` event whenever the sort model changes:

```ts
grid.on('sort-change', ({ sortModel }) => {
  console.log('Sort model:', sortModel);
  // [{ field: 'name', direction: 'asc' }, { field: 'salary', direction: 'desc' }]
});
```

## Programmatic API

See [`MultiSortPlugin`](./classes/multisortplugin/) for the full list of methods.

```ts
const plugin = grid.getPluginByName('multiSort');

plugin.setSortModel([{ field: 'department', direction: 'asc' }]);
plugin.getSortModel();               // SortModel[]
plugin.clearSort();
plugin.getSortDirection('salary');    // 'asc' | 'desc' | undefined
plugin.getSortIndex('salary');        // number | undefined
```

## Keyboard Shortcuts

| Shortcut        | Action                              |
| --------------- | ----------------------------------- |
| `Click header`  | Sort by column (clears other sorts) |
| `Shift + Click` | Add column to multi-sort            |

## Styling

The grid uses Light DOM, so standard CSS selectors work for styling sort indicators:

```css
/* Sort indicator icon */
tbw-grid .sort-indicator {
  margin-left: 4px;
}

/* Sort priority badge */
tbw-grid .sort-index {
  background: var(--tbw-multi-sort-badge-bg, var(--tbw-color-panel-bg));
  color: var(--tbw-multi-sort-badge-color, var(--tbw-color-fg));
  width: var(--tbw-multi-sort-badge-size, 1em);
  height: var(--tbw-multi-sort-badge-size, 1em);
}
```

## Row Insertion with Active Sort

When multi-sort is active, assigning `rows` automatically re-sorts the data — so a
newly inserted row will jump to its sorted position. If you want the row to stay
exactly where you placed it (e.g., after a user clicks "Add Row"), use `insertRow()`:

```ts
grid.insertRow(3, newRow); // stays at visible index 3, auto-animates
```

The row is also added to source data, so the next full `grid.rows = freshData`
assignment re-sorts normally. See the
[API Reference → Insert & Remove Rows](/grid/api-reference.md#insert--remove-rows)
for full details.

:::tip
Do **not** use `insertRow()` for data refreshes (API responses,
WebSocket updates). In those cases just set `grid.rows = newData` and let
multi-sort re-apply.
:::

## Custom Sort Logic

`MultiSortPlugin` owns the multi-column sort path and **does not consult**
`gridConfig.sortHandler` — that property is only honored by the single-column
sort path used when `MultiSortPlugin` is absent. To customize sort behavior
when multi-sort is enabled, use **`column.sortComparator`** on the columns
that need it. `sortComparator` is honored by every sort code path in the grid
(core, multi-sort, tree, server-side `sortMode: 'local'`).

```ts
grid.gridConfig = {
  columns: [
    { field: 'name', header: 'Name', sortable: true },
    {
      field: 'salary',
      header: 'Salary',
      type: 'number',
      sortable: true,
      // Custom comparator: pin negative values (debts) to the end regardless of direction
      sortComparator: (a, b) => {
        if (a < 0 && b >= 0) return 1;
        if (b < 0 && a >= 0) return -1;
        return a - b;
      },
    },
  ],
  features: { multiSort: true },
};
```

**Comparator signature:** `(a, b, rowA, rowB) => number` — return `< 0` to put
`a` first, `> 0` for `b`, `0` for equal. Direction (`asc`/`desc`) is applied
automatically by the sort engine.

## Server-Side Sorting

For backends that own sort ordering, use
[`ServerSidePlugin`](../server-side/) — it ships the current `sortModel` (all
columns, in priority order) to your `getRows` handler so the backend can
return rows in the requested order. This is the right pattern for paginated /
infinite-scroll datasets where client-side sort isn't viable.

```ts
import { ServerSidePlugin } from '@toolbox-web/grid/plugins/server-side';
import { MultiSortPlugin } from '@toolbox-web/grid/plugins/multi-sort';

grid.gridConfig = {
  columns: [
    { field: 'name', header: 'Name', sortable: true },
    { field: 'salary', header: 'Salary', sortable: true },
  ],
  plugins: [
    new MultiSortPlugin(),
    new ServerSidePlugin({
      dataSource: {
        getRows: async ({ startNode, endNode, sortModel }) => {
          // sortModel: [{ field: 'name', direction: 'asc' }, { field: 'salary', direction: 'desc' }]
          const sortQuery = sortModel
            .map((s) => `${s.field}:${s.direction}`)
            .join(',');
          const response = await fetch(
            `/api/data?from=${startNode}&to=${endNode}&sort=${sortQuery}`,
          );
          const { rows, totalNodeCount } = await response.json();
          return { rows, totalNodeCount };
        },
      },
    }),
  ],
};
```

`sortComparator` is synchronous, so use it only for custom **client-side**
comparisons. If sorting needs to be owned by your backend, use
`ServerSidePlugin`; if you only need custom single-column sort behavior, use
`sortHandler` instead.

## See Also

- **[Filtering](../filtering/)** — Filter rows by column values
- **[Server-Side Data](../server-side/)** — Block-based virtual scrolling for large datasets
- **[Plugins Overview](/grid/plugins.md)** — Plugin compatibility and combinations
