# arthub-table

> High-performance canvas-based table/grid component for Vue 3 with TypeScript support.

## Features

- 🚀 **Canvas Rendering**: High-performance rendering engine using HTML5 Canvas
- 📊 **Virtual Scrolling**: Efficiently handles large datasets with smooth scrolling
- 🎨 **20+ Built-in Cell Viewers**: Text, Select, Image, Person, Progress, Status, DateTime, Boolean, and more
- 🔌 **Extensible Viewer System**: Register custom cell viewers through the Viewer Registry
- 📦 **Grouped Tables**: Multi-level collapsible group headers
- 📐 **Nested Grids**: Support for nested table within cells
- 📋 **Clipboard**: Copy, paste, and clear cell content
- ✏️ **Cell Editing**: Inline editing with validation support
- 🔒 **Fixed Columns**: Pin columns to left or right
- 📏 **Resizable Columns**: Drag to resize column widths
- 🔄 **Drag & Drop**: Reorder rows and columns via drag and drop
- 🎯 **Cell Selection**: Single and multi-cell selection with keyboard navigation
- ✅ **Validation**: Built-in cell validation with custom rules
- 🌗 **Theming**: Light/dark theme support with custom style registration
- 📱 **TypeScript**: Full TypeScript type definitions

## Installation

```bash
npm install arthub-table
```

## Quick Start

```vue
<template>
  <DataGrid
    :columns="columns"
    :data="data"
    :width="800"
    :height="400"
    @after-edit-cell="handleEdit"
  />
</template>

<script setup lang="ts">
import { ref } from "vue";
import { DataGrid } from "arthub-table";
import type { ColumnConfig } from "arthub-table";

const columns = ref<ColumnConfig[]>([
  { title: "Name", key: "name", width: 200, type: "text" },
  { title: "Age", key: "age", width: 100, type: "number" },
  { title: "Email", key: "email", width: 300, type: "text" },
]);

const data = ref([
  { id: 1, name: "Alice", age: 28, email: "alice@example.com" },
  { id: 2, name: "Bob", age: 32, email: "bob@example.com" },
]);

const handleEdit = (editData: any) => {
  console.log("Cell edited:", editData);
};
</script>
```

## Using Cell Viewers

Cell viewers provide rich data rendering within cells:

```vue
<script setup lang="ts">
import { DataGrid, registerDefaultViewers } from "arthub-table";
import type { ColumnConfig } from "arthub-table";

// Register all built-in viewers (call once at app startup)
registerDefaultViewers();

const columns = ref<ColumnConfig[]>([
  { title: "Task", key: "task", width: 200, type: "text", useViewer: true },
  {
    title: "Status",
    key: "status",
    width: 120,
    type: "status",
    useViewer: true,
    options: [
      { label: "To Do", value: "todo", color: "#909399" },
      { label: "Done", value: "done", color: "#67C23A" },
    ],
  },
  {
    title: "Progress",
    key: "progress",
    width: 180,
    type: "progress",
    useViewer: true,
  },
  {
    title: "Assignee",
    key: "assignee",
    width: 150,
    type: "person",
    useViewer: true,
  },
]);
</script>
```

### Available Viewer Types

| Type                  | Description               | Data Type                     |
| --------------------- | ------------------------- | ----------------------------- |
| `text`                | Plain text display        | `TextViewerData`              |
| `select`              | Dropdown select with tags | `SelectViewerData`            |
| `image`               | Image display             | `ImageViewerData`             |
| `person`              | User avatar and name      | `PersonViewerData`            |
| `progress`            | Progress bar (draggable)  | `ProgressViewerData`          |
| `datetime`            | Date/time display         | `DatetimeViewerData`          |
| `boolean`             | Checkbox toggle           | `BooleanViewerData`           |
| `status`              | Status badge              | `StatusViewerData`            |
| `hyperlink`           | Clickable link            | `HyperlinkTextViewerData`     |
| `file`                | File preview              | `FileViewerData`              |
| `module`              | Module tags               | `ModuleViewerData`            |
| `table-action-button` | Action buttons            | `TableActionButtonViewerData` |
| `nested`              | Nested grid               | `NestedGridViewerData`        |
| `group-header`        | Group header row          | `GroupHeaderViewerData`       |

## API

### DataGrid Props

| Prop             | Type             | Default           | Description                 |
| ---------------- | ---------------- | ----------------- | --------------------------- |
| `rowKey`         | `string`         | `'id'`            | Row unique identifier field |
| `width`          | `number`         | -                 | Table width in pixels       |
| `height`         | `number`         | -                 | Table height in pixels      |
| `columns`        | `ColumnConfig[]` | `[]`              | Column definitions          |
| `data`           | `any[]`          | `[]`              | Table data array            |
| `datePattern`    | `string`         | `'absolute_date'` | Date display type           |
| `customAutofill` | `Function`       | -                 | Custom autofill handler     |

### DataGrid Events

| Event                    | Payload                                    | Description                     |
| ------------------------ | ------------------------------------------ | ------------------------------- |
| `after-edit-cell`        | `data`                                     | Fired after a cell is edited    |
| `after-autofill`         | `data`                                     | Fired after autofill            |
| `after-paste`            | `data`                                     | Fired after paste               |
| `after-clear`            | `data`                                     | Fired after clear               |
| `on-load`                | -                                          | Fired when grid is loaded       |
| `on-cell-text-click`     | `rowData, colId`                           | Fired when cell text is clicked |
| `on-group-toggle`        | `groupRow, expanded`                       | Fired when group is toggled     |
| `on-row-drag-reorder`    | `draggedRows, targetIndex`                 | Fired after row drag reorder    |
| `on-column-drag-reorder` | `draggedColIds, targetColId, insertBefore` | Fired after column reorder      |

### DataGrid Methods (via ref)

| Method                         | Description                    |
| ------------------------------ | ------------------------------ |
| `reload()`                     | Resize and redraw              |
| `loadData(data)`               | Load new data                  |
| `getData()`                    | Get current data               |
| `getCheckedRows()`             | Get checked rows               |
| `getChangedRows()`             | Get changed rows               |
| `validate(callback)`           | Validate all cells             |
| `setTheme(theme)`              | Set theme ('light' or 'dark')  |
| `setStriped(enabled)`          | Enable/disable striped rows    |
| `showLoading(rowIds, colKeys)` | Show loading on specific cells |
| `hideLoading(rowIds, colKeys)` | Hide loading on specific cells |
| `setSelectedRows(rowIds)`      | Set selected rows              |

## TypeScript Types

arthub-table exports comprehensive TypeScript types:

```typescript
import type {
  ColumnConfig,
  CellData,
  RowData,
  HeaderConfig,
  // Viewer types
  CellViewer,
  CellViewerData,
  TextViewerData,
  SelectViewerData,
  ProgressViewerData,
  // Group types
  GroupRowData,
  GroupedTableRowData,
} from "arthub-table";
```

## Local Development (with AssetMatrix)

### Using webpack alias

1. Create `.env.local` in AssetMatrix project root:

```env
USE_LOCAL_CANVAS_TABLE=true
LOCAL_CANVAS_TABLE_PATH=../arthub-table
```

2. Start arthub-table dev server:

```bash
cd arthub-table
npm run serve
```

3. Start AssetMatrix with local arthub-table:

```bash
cd AssetMatrix
npm run dev:local-canvas
```

### Using npm link

```bash
# In arthub-table directory
npm link

# In AssetMatrix directory
npm link arthub-table
```

## Building

```bash
# Build library (CommonJS + UMD + types)
npm run build

# Build demo site
npm run build:demo

# Build only type declarations
npm run build:types
```

## Publishing

```bash
# Bump version and publish
npm version patch  # or minor, or major
npm publish
```

Or push a version tag to trigger CI/CD:

```bash
git tag v1.0.1
git push origin v1.0.1
```

## Table CRUD Operations — Best Practices

> This section documents how to perform Create, Read, Update, Delete operations on cells, rows, and columns using the DataGrid component.

### Core Concepts

| Concept                | Description                                                                              |
| ---------------------- | ---------------------------------------------------------------------------------------- |
| `ref="datagrid"`       | The Vue template ref to access the DataGrid component instance                           |
| `datagrid.value?.grid` | The underlying core `DataGrid` engine (for advanced low-level API)                       |
| `loadData(data)`       | Re-render all rows with new data; resets editor and selection state                      |
| `reload()`             | Recalculate table dimensions and scroll positions (after structural changes)             |
| `markDirty()`          | Lightweight: flags the canvas for repaint on the next animation frame                    |
| Vue Watcher            | Modifying `columns` or `data` refs automatically triggers `loadColumns()` + `loadData()` |

---

### 1. Update a Single Cell

Use `grid.setData(value, { colIndex, rowIndex })` to write a value into a specific cell by its column and row index.

```ts
const grid = (datagrid.value as any)?.grid;

// Update cell at row 0, column 2
grid.setData("New Value", { colIndex: 2, rowIndex: 0 });

// Trigger canvas repaint
grid.markDirty();
```

> **Note:** `setData()` respects `readonly` columns. The cell's `originalValue` is preserved for change tracking.

---

### 2. Update an Entire Column

Loop through all data rows and call `cell.setData()` on each cell object:

```ts
const grid = (datagrid.value as any)?.grid;
const colIndex = 2; // Target column index

gridData.value.forEach((_row, rowIndex) => {
  if (_row.isGroup || _row.isSeparateRow) return; // Skip group/separator rows
  const cell = grid.body.getCell(colIndex, rowIndex);
  if (cell) {
    cell.setData(`Col_Value_${rowIndex}`);
  }
});

grid.markDirty();
```

---

### 3. Batch Update a Rectangular Area

Use `grid.batchSetData({ colIndex, rowIndex, value })` where `value` is a 2D array:

```ts
const grid = (datagrid.value as any)?.grid;

// Write a 3-row × 2-column block starting at [row=0, col=1]
grid.batchSetData({
  colIndex: 1,
  rowIndex: 0,
  value: [
    ["A1", "B1"],
    ["A2", "B2"],
    ["A3", "B3"],
  ],
});

grid.markDirty();
```

---

### 4. Update Rows by `rowKey` Match (Partial Update)

Use `updateData(data)` to merge updates into existing rows matched by the `rowKey` field (default `"id"`):

```ts
datagrid.value?.updateData([
  { id: 1, emp_name: "Updated Name 1", emp_no: "001" },
  { id: 3, job_name: "Updated Job 3" },
]);
```

> **Behavior:** Only the fields you provide will be updated. Other fields in the row are unchanged. This does NOT trigger a full reload; it updates in-place.

---

### 5. Refresh the Visible Area

There are two levels of refresh:

| Method                     | When to Use                                                                                       |
| -------------------------- | ------------------------------------------------------------------------------------------------- |
| `grid.markDirty()`         | After data-only changes (cell values). Lightweight, uses `requestAnimationFrame`.                 |
| `datagrid.value?.reload()` | After structural changes (columns added/removed, row heights changed). Full resize + recalculate. |

```ts
// Lightweight repaint
const grid = (datagrid.value as any)?.grid;
grid.markDirty();

// Full reload (after structural changes)
await nextTick();
datagrid.value?.reload();
```

---

### 6. Add a Row

Append a new row object to the data array and call `loadData()`:

```ts
const newRow = {
  _rowId: `row_${Date.now()}`,
  id: gridData.value.length + 1,
};

// Dynamically fill all column keys with default values
for (const col of columns.value) {
  if (col.key && !(col.key in newRow)) {
    newRow[col.key] = col.type === "number" ? 0 : "";
  }
}

gridData.value.push(newRow);
datagrid.value?.loadData(gridData.value);
```

> **Grouped mode:** Insert the row at the correct position within `groupedDataCache`, then filter and reload. See the `onAddRow()` handler in `App.vue` for a complete example.

---

### 7. Delete a Row

Remove the row from the data array and reload:

```ts
const indexToDelete = gridData.value.findIndex(
  (row) => !row.isGroup && !row.isSeparateRow,
);

if (indexToDelete >= 0) {
  gridData.value.splice(indexToDelete, 1);
  datagrid.value?.loadData(gridData.value);
}
```

---

### 8. Add a Column

Add a new column definition to the `columns` ref. The Vue watcher will automatically trigger `loadColumns()` + `loadData()`:

```ts
const newCol = {
  title: "New Column",
  key: "new_col",
  size: "small",
  align: "left",
};

// Insert at a specific position
const newColumns = [...columns.value];
newColumns.splice(insertIndex, 0, newCol);
columns.value = newColumns;

// Fill data for the new column
gridData.value.forEach((row, i) => {
  if (!row.isGroup) {
    row[newCol.key] = `Value_${i}`;
  }
});
```

---

### 9. Delete a Column

Remove the column from the `columns` ref:

```ts
const colIndexToDelete = columns.value.findIndex(
  (col) => col.key === "target_key",
);

if (colIndexToDelete >= 0) {
  const newColumns = [...columns.value];
  newColumns.splice(colIndexToDelete, 1);
  columns.value = newColumns;
}
```

---

### 10. Read Data

| Method                                  | Returns     | Description                             |
| --------------------------------------- | ----------- | --------------------------------------- |
| `datagrid.value?.getData()`             | `RowData[]` | All row data with merged cell values    |
| `datagrid.value?.getCheckedRows()`      | `RowData[]` | Rows where checkbox is checked          |
| `datagrid.value?.getChangedRows()`      | `RowData[]` | Rows that have been modified since load |
| `grid.body.getRowData(rowIndex)`        | `RowData`   | A single row by index                   |
| `grid.body.getCell(colIndex, rowIndex)` | `Cell`      | A single cell object                    |

```ts
// Get all data
const allData = datagrid.value?.getData();

// Get changed rows only
const changedRows = datagrid.value?.getChangedRows();

// Get a single cell's value
const grid = (datagrid.value as any)?.grid;
const cell = grid.body.getCell(2, 0);
console.log(cell.value); // Current cell value
```

---

### 11. Validation

```ts
// Validate all cells
datagrid.value?.validate((valid) => {
  if (!valid) {
    const errors = datagrid.value?.getValidations();
    console.log("Validation errors:", errors);
  }
});

// Validate only changed rows
datagrid.value?.validateChanged((valid) => {
  /* ... */
});

// Set external validation errors (matched by rowKey)
datagrid.value?.setValidations([
  { id: 1, emp_name: "Name is required", emp_no: "Invalid number" },
  { id: 3, job_name: "Job name too long" },
]);

// Clear all validations
datagrid.value?.clearValidations();
```

---

### Complete Example

See `App.vue` for interactive demonstrations of all these patterns. The toolbar includes buttons for:

- **✏️ 更新单个格子** — Single cell update via `grid.setData()`
- **📝 更新整列** — Column-wide update via `cell.setData()` loop
- **📋 批量更新区域** — Batch area update via `grid.batchSetData()`
- **🔄 刷新可见区域** — Canvas repaint via `grid.markDirty()`
- **➕ 新增一行 / ➕ 新增一列** — Dynamic row/column addition
- **🗑️ 删除首行 / 🗑️ 删除末列** — Row/column deletion

## License

[MIT](LICENSE)
