npm package: @gjirafatech/gj-tree-select
Description: Hierarchical multi-select dropdown with tree connectors for Vue 2
Author: GjirafaTech
Latest version: 1.1.5

# @gjirafatech/gj-tree-select

A **hierarchical multi-select dropdown** with tree connector lines for **Vue 2**.

Works with any flat data — folders, channels, categories, departments — you provide a `dataAdapter` and the component builds the tree.

## Install

```bash
npm install @gjirafatech/gj-tree-select
```

## Quick Start

### Global registration

```js
import Vue from 'vue'
import GjTreeSelect from '@gjirafatech/gj-tree-select'

Vue.use(GjTreeSelect)
```

### Local registration

```js
import { GjTreeSelect } from '@gjirafatech/gj-tree-select'

export default {
  components: { GjTreeSelect }
}
```

## Basic Usage

```vue
<template>
  <GjTreeSelect
    v-model="selectedIds"
    :items="folders"
    placeholder="Select folder"
  />
</template>

<script>
export default {
  data() {
    return {
      selectedIds: [],
      folders: [
        { id: 1, parentId: 0, name: 'Sports' },
        { id: 2, parentId: 0, name: 'News' },
        { id: 3, parentId: 1, name: 'Football' },
        { id: 4, parentId: 1, name: 'Basketball' },
        { id: 5, parentId: 3, name: 'Premier League' },
      ]
    }
  }
}
</script>
```

## Collapsible Tree

Enable expand/collapse functionality with arrow icons. Nodes are collapsed by default when `collapsible` is enabled:

```vue
<GjTreeSelect
  v-model="selectedIds"
  :items="folders"
  :collapsible="true"
  placeholder="Select folder"
/>
```

You can also customize the expand/collapse icon:

```vue
<GjTreeSelect v-model="selectedIds" :items="folders" :collapsible="true">
  <template #expand-icon="{ node, isExpanded }">
    <GjIcon
      :name="isExpanded ? 'ChevronDown' : 'ChevronRight'"
      size="16"
    />
  </template>
</GjTreeSelect>
```

## Custom Icons (Scoped Slots)

The component renders fallback SVG icons by default. Override them with scoped slots:

```vue
<GjTreeSelect v-model="selectedIds" :items="folders" placeholder="Select folder">
  <!-- Custom node icon -->
  <template #node-icon="{ node, isSelected }">
    <GjIcon name="FolderOutline" size="20" />
  </template>

  <!-- Custom checkbox -->
  <template #check-icon="{ node, isSelected }">
    <GjIcon
      :name="isSelected ? 'CheckButtonSquareEnabledFilled' : 'CheckButtonSquareDisabled'"
      size="16"
    />
  </template>

  <!-- Custom chevron -->
  <template #chevron-icon="{ isOpen }">
    <GjIcon name="ArrowDown" size="16" />
  </template>

  <!-- Custom tag remove -->
  <template #tag-remove-icon="{ tag }">
    <GjIcon name="Close" size="16" />
  </template>
</GjTreeSelect>
```

## Custom Tag Pills

> **Note:** The component handles ellipsis automatically by default. You only need this if you want custom tag styling or icons.

To fully customize tag pills, override the `tag` slot. **Important:** When overriding, you must provide your own CSS for ellipsis to work correctly:

```vue
<GjTreeSelect v-model="selectedIds" :items="folders">
  <template #tag="{ tag, remove }">
    <span class="tag-content">
      <span class="tag-label">{{ tag.name }}</span>
      <button type="button" class="tag-remove-btn" @click="remove">
        <GjIcon name="Close" size="12" />
      </button>
    </span>
  </template>
</GjTreeSelect>

<style scoped>
.tag-content {
  display: flex;
  align-items: center;
  min-width: 0;
  flex: 1;
}

.tag-label {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}

.tag-remove-btn {
  flex-shrink: 0;
  margin-left: 4px;
}
</style>
```

## Different Data Shapes (dataAdapter)

Your API returns `channelId`, `parentChannelId`, `title`? No problem:

```vue
<GjTreeSelect
  v-model="selectedChannels"
  :items="channels"
  :data-adapter="{
    idKey: 'channelId',
    parentIdKey: 'parentChannelId',
    nameKey: 'title',
    rootParentId: null,
  }"
  placeholder="Select channel"
>
  <template #node-icon>
    <GjIcon name="Broadcast" size="20" />
  </template>
</GjTreeSelect>
```

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `value` (v-model) | `Array` | `[]` | Selected IDs. |
| `items` | `Array` | `[]` | Flat array of items (any shape). |
| `data-adapter` | `Object` | `{ idKey: 'id', parentIdKey: 'parentId', nameKey: 'name', rootParentId: 0 }` | Maps your data fields. |
| `placeholder` | `String` | `'Select...'` | Placeholder text. |
| `max-visible-tags` | `Number` | `2` | Tags before `+N` overflow. |
| `searchable` | `Boolean` | `true` | Enable search filtering. |
| `disabled` | `Boolean` | `false` | Disable the component. |
| `collapsible` | `Boolean` | `false` | Enable expand/collapse icons for parent nodes. |
| `show-select-all` | `Boolean` | `true` | "Select all" on parent nodes with children. |
| `select-all-label` | `String` | `'Select all'` | Label for select-all. |
| `deselect-all-label` | `String` | `'Deselect'` | Label for deselect-all. |
| `empty-text` | `String` | `'No items available'` | Empty state text. |
| `connector-geometry` | `Object` | See below | SVG connector pixel constants. |
| `theme` | `Object` | `{}` | CSS custom-property overrides. |

### Default Connector Geometry

```js
{
  laneWidth: 22,
  rowHeight: 32,
  rowCenterY: 16,
  elbowRadius: 8,
  horizontalEndGap: 2,
  verticalOverflow: 10,
  parentEntryOverflow: 8
}
```

## Events

| Event | Payload | Description |
|---|---|---|
| `input` | `Array<id>` | Selection changed (v-model). |
| `open` | — | Dropdown opened. |
| `close` | — | Dropdown closed. |

## Scoped Slots

| Slot | Props | Description |
|---|---|---|
| `node-icon` | `{ node, isSelected }` | Icon before label in each row. |
| `check-icon` | `{ node, isSelected }` | Checkbox icon on the right. |
| `expand-icon` | `{ node, isExpanded }` | Expand/collapse icon (when `collapsible: true`). |
| `chevron-icon` | `{ isOpen }` | Chevron in the control bar. |
| `tag-remove-icon` | `{ tag }` | Remove button inside tag pills. |
| `tag` | `{ tag, remove }` | Full override of a tag pill. |

## Theming (CSS Custom Properties)

Pass a `theme` object to override any CSS variable:

```vue
<GjTreeSelect
  :theme="{
    '--gj-ts-control-border-active': '#3A81F6',
    '--gj-ts-row-selected-bg': '#ECF3FF',
    '--gj-ts-select-all-color': '#3A81F6',
    '--gj-ts-check-active-color': '#3A81F6',
  }"
/>
```

### Available CSS Variables

| Variable | Default | What it styles |
|---|---|---|
| `--gj-ts-control-bg` | `#ffffff` | Control background |
| `--gj-ts-control-border` | `#EDEFF3` | Control border |
| `--gj-ts-control-border-active` | `#F4B261` | Control border when open |
| `--gj-ts-control-radius` | `0.25rem` | Control border radius |
| `--gj-ts-control-min-height` | `35px` | Control min height |
| `--gj-ts-tag-bg` | `#F6F8F9` | Tag pill background |
| `--gj-ts-tag-color` | `#030F28` | Tag pill text |
| `--gj-ts-tag-radius` | `20px` | Tag pill radius |
| `--gj-ts-tag-max-width` | `140px` | Tag pill max width |
| `--gj-ts-placeholder-color` | `#B5C0CE` | Placeholder text |
| `--gj-ts-menu-border` | `#DADFE7` | Dropdown border |
| `--gj-ts-menu-radius` | `6px` | Dropdown radius |
| `--gj-ts-menu-shadow` | `0 4px 12px ...` | Dropdown shadow |
| `--gj-ts-menu-max-height` | `340px` | Dropdown max height |
| `--gj-ts-menu-bg` | `#ffffff` | Dropdown background |
| `--gj-ts-row-height` | `32px` | Tree row height |
| `--gj-ts-row-radius` | `6px` | Tree row radius |
| `--gj-ts-row-hover-bg` | `#F6F8F9` | Row hover background |
| `--gj-ts-row-selected-bg` | `#FDE9D3` | Row selected background |
| `--gj-ts-row-gap` | `8px` | Gap between rows |
| `--gj-ts-label-color` | `#030F28` | Label text color |
| `--gj-ts-label-font-size` | `0.875rem` | Label font size |
| `--gj-ts-expand-color` | `#A3B0C2` | Expand/collapse icon color |
| `--gj-ts-expand-hover-color` | `#030F28` | Expand/collapse icon hover color |
| `--gj-ts-select-all-color` | `#F4B261` | "Select all" button color |
| `--gj-ts-check-color` | `#A3B0C2` | Unchecked checkbox color |
| `--gj-ts-check-active-color` | `#F4B261` | Checked checkbox color |
| `--gj-ts-connector-color` | `#C9CDD3` | Connector line color |
| `--gj-ts-connector-width` | `1` | Connector line width |

## License

MIT