# 📋 Table

Универсальный компонент таблицы, используется в списках сущностей и дашбордах.
Содержит следующую иерархию компонент

- Table
  - Exports
    - CSV
    - Excel
  - Root
    - Head
    - Row
      - Cell

## Компонент Table

Обертка всей таблицы, может применятся как есть, без слотов. Создает контекст, инициализирует контроллер и отвечает за обработку данных

### Props:

- **data** - входные данные в формате строк списка. Если columns не задан, вычисляет из keys

  ```typescript
  type RowData = (Record<Head['name'], Cell> & { class?: string })[];

  // Пример
  const data: RowData = [
  	{ col1: 'cell1', col2: 'cell2' },
  	{ col1: 'cell3', col2: 'cell4' }
  ];
  ```

- **columns** - (опционально) отвечает за свойства колонок и их отображение

  ```typescript
  export enum Types {
  	Number = 'number',
  	String = 'string',
  	Date = 'date',
  	Boolean = 'boolean',
  	List = 'list'
  }

  export interface Head {
  	title?: string; // Отображаемый заголовок
  	name: string; // Ключ колонки, связывает значения в строках
  	width?: number | string; // Ширина колонки
  	type?: Types | string; // Тип 'number' | 'string' | 'date' | 'boolean' | 'list'
  	unit?: string;
  	class?: string; // Класс для стилей колонки
  	skipRender?: boolean;

  	sortable?: boolean; // Можно ли отсортировать колонку
  	resizable?: boolean; // Можно ли менять размер
  	pinnable?: boolean; // Можно ли закрепить колонку

  	pinned?: boolean; // Закреплена ли колонка
  	offsetLeft?: number; // Смещение закрепленной колонки (техническое поле, перерасчитывается само)
  	element?: HTMLElement; // Элемент ресайза (техническое поле)

  	index?: number; // Индекс позиции колонки при инициализации (нужно для возврата на место при изменении pinned)
  	cast?: (cell: Cell) => string; // Кастомный каст колонки в строку
  	onSort?: SortCallback; // Callback сортировки для данной колонки
  }

  // Пример
  const columns: Head[] = [
  	{ name: 'col1', title: 'Первая колонка' },
  	{ name: 'col2', title: 'Вторая колонка' }
  ];
  ```

- **config** - (опционально) отвечает за конфигурацию таблицы и свойств по умолчанию

  ```typescript
  export type Config = {
  	virtualizer?: {
  		overscan?: number;
  		// scrollElement: HTMLDivElement
  		rowHeight: number; // Размер строк в таблице. Нужно как для отображения так и расчета виртуализации
  	};

  	enumerate?: boolean;

  	head?: {
  		// Глобальный конфиг колонок по-умолчанию
  		precision?: Record<string, number>;
  		pinnable?: boolean;
  		resizable?: boolean;
  		sortable?: boolean;
  	};

  	// Некоторые фичи могут еще не работать
  	onSort?: SortCallback;
  	onPagination?: undefined;
  	onFilter?: undefined;
  	replaceNull?: string;
  };
  ```

- controller - (опционально) используется, если мы хотим создать контроллер самостоятельно выше по уровню иерархии компонент

Свойства протекают вглубь компонентов и имеют приоритет по специфичности. То есть глобальная конфигурация может быть перезаписана частным случаем

<p align='center'>
  default &lt; config.head &lt; columns: Head[] &lt;p Head props
</p>

`Head.svelte`

```typescript
export let head: Head;
export let sortable = head?.sortable ?? $config?.head?.sortable ?? true;
export let resizable = head?.sortable ?? $config?.head?.resizable ?? true;
export let pinnable = head?.pinnable ?? $config?.head?.pinnable ?? true;
```

### Slots

Table имеет два слота, с отображением по-умолчанию

- _default_ - Для компонента Root
- _top_ - Для Экспорта данных и других кнопок управления над таблицей

```html
<script>
	import { Table, Exports, Root } from '@smkit/ui/table';
	import { data } from './data';
</script>

<table data="{data.body}" columns="{data.header}">
	<Exports slot="top" />
	<Root />
</table>
```

Пробрасывает наружу пропсы:

- controller - экземпляр класса контроллера
- header - обработанные заголовки для отображения
- body - обработанное тело таблицы в виде массива объектов
- config - обработанный конфиг

```html
<table let:controller let:header let:body let:config>
	<Root />
</table>
```

## Компонент Root

Отвечает за структуру таблицы и ее виртуализацию

### Slots

- _header_ - слот заголовка таблицы, используемый через svelte:fragment

  ```html
  ...

  <svelte:fragment slot="header">
  	{#each header as head}
  	<head {head}>
  		{head.title} {head.width}
  	</head>
  	{/each}
  </svelte:fragment>
  ```

- _row_ - слот для подстановки кастомной строки Row и кастомных клеток. Пробрасывает наружу пропсы:

  - row - объект строки
  - vRow - описание строки от [виртуализации](https://tanstack.com/virtual/latest/docs/api/virtual-item)
  - vIndex - виртуальный индекс

  ```html
  ...

  <Row slot="row" let:row let:vRow let:vIndex>
  	{#each header as head}
  	<Cell {head} />
  	{/each}
  </Row>
  ```

## Компонент Row

Компонент строки, получает информацию о текущем индексе, позиции и данные из контекста. Используется в цикле рендера строк

### Props

- row - (опционально) достается из контекста строки
- vRow - (опционально) достается из контекста строки
- vIndex - (опционально) достается из контекста строки
- style - (опционально) стили всей строки
- class - (опционально) классы стилей всей строки

### Slots

- default - отображение кастомных клеток, через цикл. Порядок клеток регулируется _header_ из **Table**

```html
<table {columns} {data} {controller} let:header>
	<Root>
		<svelte:fragment slot="header">
			{#each header as head}
			<head {head} class="p-1 px-2" />
			{/each}
		</svelte:fragment>
		<Row slot="row">
			{#each header as head}
			<Cell {head} />
			{/each}
		</Row>
	</Root>
</table>
```

## Компонент Cell

Компонент клетки, получает информацию о текущем head из пропсов и о текущем индексе из контекста строки. Имеет два режима, при закрепленном столбце и при обычном

### Props

- value - (на выбор: value/head) - значение клетки напрямую
- head - (на выбор: value/head) - заголовок колонки клетки, значение вычислится само
- class - (опционально) классы стилей клетки

### Slots

- default - Отображение клетки

`Platina360 - team/[teamId]/settings/roles/+page.svelte`

```html
...

<Row
  slot="row"
  class="hover:bg-[#7997FF33]"
>
  <Cell head={{ name: '', width: '5%' }} class="opacity-0 group-hover:opacity-100">
    <Checkbox checked={row.isChecked} class="opacity-0 group-hover:opacity-100" />
  </Cell>

  <Cell head={header[0]}>
    <Account name={row.user.name} email={row.user.email} avatar={row.user.avatar} />
  </Cell>

  <Cell head={header[1]}>
    <span class="text-base-content pr-2">
      {row.roles}
    </span>
    <Popover class="opacity-0 group-hover:opacity-100">
      <Trigger
        slot="trigger"
        class="btn-ghost hover:bg-inherit opacity-0 group-hover:opacity-100 border border-neutral/20 max-h-6 w-6 relative"
        ><span class="hidden"></span></Trigger
      >
      <button class="btn btn-sm bg-base-100 border-none w-full"> Личные </button>
      <button class="btn btn-sm bg-base-100 border-none"> Командные </button>
    </Popover>
  </Cell>

  <Cell head={header[2]}>
    <License license={row.licenses} />
  </Cell>

  <Cell head={header[3]}>
    {row.status}
  </Cell>
</Row>
```

## Компонент Head

Аналогично компоненту клетки, имеет два варианта с закреплением и без. Имеет пропсы, обозначенные в структуре Head выше, с наследованием и приоритетами по специфике

### Props

- head: Head - объект описания заголовка
- sortable - можно ли сортировать по колонке
- resizable - можно ли менять размер колонки
- pinnable - можно ли закреплять колонку

### Slots

- default - отображение клетки заголовка

`Platina - reports/components/table/Table.svelte`

```html
...

<svelte:fragment slot="header">
	{#each header as head}
	<head {head} on:sort="{onSort}">
		<div class="flex flex-col items-start">
			{head.title}
			<div class="flex gap-1">
				{#if head.type === 'number'}
				<button
					title="Фильтр по метрике"
					class="flex h-5 w-5 items-center justify-center transition-colors"
					on:click|stopPropagation|preventDefault="{()"
					=""
				>
					openFilter(head.name)} >
					<FunnelIcon
						bgColor="{globalFilters.includes(head.name)"
						?
						Colors.selectedBg
						:
						Colors.normalBg}
					/>
				</button>
				<button
					title="Отобразить метрику на графике"
					class="flex h-5 w-5 items-center justify-center transition-colors"
					on:click|stopPropagation|preventDefault="{()"
					=""
				>
					{ $chartForm.metric = head.name }} >
					<PieIcon
						bgColor="{$chartForm.metric"
						=""
						=""
						="head.name"
						?
						Colors.selectedBg
						:
						Colors.normalBg}
					/>
				</button>
				{#if columns[head.name].format !== 'percent' && columns[head.name].format !== 'date'}
				<button
					title="Пересчитать в проценты"
					class="flex h-5 w-5 items-center justify-center transition-colors"
					on:click|stopPropagation|preventDefault="{()"
					=""
				>
					togglePercentView(head.name)} >
					<PercentsIcon
						bgColor="{$viewForm.columns?.[head.name]?.isPercent"
						?
						Colors.selectedBg
						:
						Colors.normalBg}
					/>
				</button>
				{/if} {#if $viewForm.columns?.[head.name]?.isRounded !== undefined &&
				!$viewForm.columns[head.name]?.isPercent}
				<button
					title="Не округлять бюджеты до целого"
					class="flex h-5 w-5 items-center justify-center transition-colors"
					on:click|stopPropagation|preventDefault="{()"
					=""
				>
					toggleRoundView(head.name)} >
					<RoundedIcon
						bgColor="{$viewForm.columns?.[head.name]?.isRounded"
						?
						Colors.normalBg
						:
						Colors.selectedBg}
					/>
				</button>
				{/if} {:else if head.type === 'string'}
				<button
					title="Отобразить группу цветом на графике"
					class="flex h-5 w-5 items-center justify-center transition-colors"
					on:click|stopPropagation|preventDefault="{()"
					=""
				>
					{ $chartForm.group = head.name }} >
					<PieIcon
						bgColor="{$chartForm.group"
						=""
						=""
						="head.name"
						?
						Colors.selectedBg
						:
						Colors.normalBg}
					/>
				</button>
				{/if}
			</div>
			{#key showFilterDropdown[head.name]}
			<DropdownCard bind:show="{showFilterDropdown[head.name]}" x="-40px" y="0px">
				<div class="flex w-[300px] flex-col gap-4 px-4 pb-3 pt-4 shadow-lg">
					<div class="flex max-h-10 items-center gap-4" on:click|stopPropagation|preventDefault>
						<select selected="{operator}" options="{operators}" label="Фильтр" />
						<InputFL bind:value="{filterQuery}" label="Значение" />
					</div>
					<button
						class="btn btn-primary btn-sm mt-2 w-full py-2"
						disabled="{!filterQuery"
						||
						Number.isNaN(Number(filterQuery))}
						on:click="{saveFilter}"
					>
						Сохранить
					</button>
				</div>
			</DropdownCard>
			{/key}
		</div>
	</head>
	{/each}
</svelte:fragment>
```

## Компонент Exports

Кнопка экспорта данных, содержащая Excel и CSV по-умолчанию. Должна находиться в контексте таблицы. Можно использовать вне таблицы, если использована ручная инициализация контроллера

```html
<script>
	import { Table, Exports } from '@smkit/ui/table';
	import { data } from './data';
</script>

<table data="{data.body}" columns="{data.header}">
	<Exports slot="top" />
</table>
```

```html
<script>
	import { Table, Exports, Controller } from '$lib/table';
	import { columns, data } from './tableConfig';

	const controller = new Controller();
</script>

<Exports />
<div class="h-[600px]">
	<table {columns} {data} {controller} />
</div>
```
