# Tree View

Tree View solutions for massive datasets.

[![npm](https://img.shields.io/badge/npm-v4.2.0-blue)](https://www.npmjs.com/package/@preline/tree-view) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Demo](https://img.shields.io/badge/demo-online-brightgreen)](https://preline.co/plugins/tree-view.html)

## Contents

- [Overview](#overview)
- [Installation](#installation)
- [Basic usage](#basic-usage)
- [Configuration Options](#configuration-options)
- [JavaScript API](#javascript-api)
- [Events](#events)
- [Common Patterns](#common-patterns)
- [License](#license)

## Overview

The Tree View component provides a hierarchical structure for displaying nested data, similar to file explorers. It integrates with the Accordion component to create expandable/collapsible tree nodes and supports both checkbox and button selection modes.

**Key Features:**
- Hierarchical nested structure
- Expandable/collapsible nodes (using Accordion)
- Multiple selection modes (checkbox, button)
- Auto-select children option for directories
- Programmatic control via JavaScript API
- Event system for selection tracking
- Accessibility attributes (ARIA) built-in

## Installation

To get started, install Tree View plugin via npm, else you can skip this step if you are already using Preline UI as a package.

```bash
npm i @preline/tree-view @preline/accordion
```

### CSS

Use [`@source`](https://tailwindcss.com/docs/functions-and-directives#source-directive) to register the plugin's JavaScript path for Tailwind CSS scanning, then [`@import`](https://tailwindcss.com/docs/functions-and-directives#import-directive) the plugin's CSS files into your Tailwind CSS file.

```css
@import "tailwindcss";

/* @preline/accordion */
@source "../node_modules/@preline/accordion/*.js";
@import "./node_modules/@preline/accordion/variants.css";

/* @preline/tree-view */
@source "../node_modules/@preline/tree-view/*.js";
@import "./node_modules/@preline/tree-view/variants.css";
@import "./node_modules/@preline/tree-view/theme.css";
```

### JavaScript

Include the JavaScript that powers the interactive elements near the end of your `</body>` tag:

```html
<script src="./node_modules/@preline/accordion/index.js"></script>
<script src="./node_modules/@preline/tree-view/index.js"></script>
```

### Manual Initialization

Use the `non-auto` entry if you need manual initialization. Tree View depends on Accordion for expand/collapse behavior, so call `HSAccordion.autoInit()` first and then initialize the specific Tree View element manually.

```html
<script type="module">
  import HSAccordion from "@preline/accordion/non-auto.mjs";
  import HSTreeView from "@preline/tree-view/non-auto.mjs";

  HSAccordion.autoInit();
  new HSTreeView(document.querySelector("#tree-view"));
</script>
```

### Via Bundler

When using a bundler (Vite, webpack, etc.), import the plugin directly as an ES module. Initialize `HSAccordion` first, since it powers the expand/collapse nodes inside the tree.

The first example uses the auto-init entries. This means the plugins scan the DOM and initialize matching elements automatically.

The second example uses the manual `non-auto` entries. Use this when you want explicit control over initialization order and timing, or when you need to initialize a specific Tree View instance yourself.

```js
import "@preline/accordion";
import "@preline/tree-view";
```

```js
import HSAccordion from "@preline/accordion/non-auto";
import HSTreeView from "@preline/tree-view/non-auto";

// Initialize all matching accordion nodes first
HSAccordion.autoInit();

// Then initialize all matching tree views
HSTreeView.autoInit();

// Or initialize a specific element manually
const el = document.querySelector("#tree-view");
if (el) new HSTreeView(el);
```

### TypeScript

This package ships with TypeScript type definitions. No additional `@types/` package is needed.

## Basic usage

The following example demonstrates the minimal HTML structure required for a tree view component. This is a base template without custom styling - you can apply your own CSS classes and styles as needed. The example shows a directory structure with expandable folders and files.

```html
<div role="tree" aria-orientation="vertical" data-hs-tree-view>
  <div class="hs-accordion active" role="treeitem" aria-expanded="true" id="hs-tree-view-heading-first" data-hs-tree-view-item='{
    "value": "assets",
    "isDir": true
  }'>
    <div class="hs-accordion-heading">
      <button class="hs-accordion-toggle" aria-expanded="true" aria-controls="hs-tree-view-collapse-first">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path class="hs-accordion-active:hidden block" d="M12 5v14"/></svg>
      </button>
      assets
    </div>

    <div id="hs-tree-view-collapse-first" class="hs-accordion-content overflow-hidden transition-[height] duration-300" role="group" aria-labelledby="hs-tree-view-heading-first">
      <div role="treeitem" data-hs-tree-view-item='{
        "value": "image.jpg"
      }'>image.jpg</div>
    </div>
  </div>
</div>
```

**Structure Requirements:**
- `data-hs-tree-view`: Required on the root container element
- `role="tree"`: Required on the root container
- `aria-orientation="vertical"`: Required on the root container
- `hs-accordion`: Required for each directory/folder node
- `data-hs-tree-view-item`: Required on each tree item (files and directories)
- Proper ARIA attributes (`role="treeitem"`, `aria-expanded`, `aria-controls`, `aria-labelledby`)

**Initial State:**
- Directories can be expanded/collapsed using accordion functionality
- Items can be selected based on the `controlBy` mode

## Configuration Options

### Data Options

Data options are specified in the `data-hs-tree-view` and `data-hs-tree-view-item` attributes.

| Attribute | Target Element | Type | Default | Description |
| --- | --- | --- | --- | --- |
| `data-hs-tree-view` | Root container | - | - | Activate a Tree View by specifying on an element. Should be added to the wrapper container. |
| `:controlBy` | Inside `data-hs-tree-view` | `"checkbox"` \| `"button"` | `"button"` | Tells the plugin which mode is active and processes events accordingly. `checkbox` mode allows multiple selections, `button` mode allows single selection. |
| `:autoSelectChildren` | Inside `data-hs-tree-view` | boolean | `false` | This option is available if `controlBy` is set to `checkbox` and if the item is a directory (`isDir: true`). When `true`, if a parent directory is selected, all nested items receive the same selection state. |
| `:isIndeterminate` | Inside `data-hs-tree-view` | boolean | `true` | Adds indeterminate visual style for checkboxes when `controlBy` is set to `checkbox`. Shows when some but not all children are selected. |
| `data-hs-tree-view-item` | Each tree item | - | - | Determines which element inside the initialized component is the item. Should be added to the item itself. |
| `:id` | Inside `data-hs-tree-view-item` | string | - | Optional. Desired identifier for the item. |
| `:value` | Inside `data-hs-tree-view-item` | string | - | The value that will be passed to the resulting array when item is selected. |
| `:isDir` | Inside `data-hs-tree-view-item` | boolean | `false` | Determines that the element is a directory and processes it accordingly. Directories can contain nested items. |

**Example:**
```html
<div data-hs-tree-view='{
  "controlBy": "checkbox",
  "autoSelectChildren": true,
  "isIndeterminate": true
}'>
  <!-- Tree items -->
</div>
```

### Tailwind Modifiers

| Name | Description |
| --- | --- |
| `hs-tree-view-selected:*` | A modifier that allows you to set Tailwind classes when item has been selected. |
| `hs-tree-view-disabled:*` | A modifier that allows you to set Tailwind classes when item has "disabled" class. |

## JavaScript API

The `HSTreeView` object is available in the global `window` object after the plugin is loaded.

### Instance Methods

These methods are called on a tree view instance.

| Method | Parameters | Return Type | Description |
| --- | --- | --- | --- |
| `update()` | None | `void` | Updates the element. This is suitable, for example, in case of changing the order of elements or dynamically adding/removing items. |
| `getSelectedItems()` | None | `ITreeViewItem[]` | Returns a list of selected items as an array of objects with item properties. |
| `changeItemProp(id, prop, val)` | `id`: `string`<br>`prop`: `string`<br>`val`: `any` | `void` | Changes a property of a specific item. Useful for updating item state programmatically. |
| `destroy()` | None | `void` | Destroys the tree view instance, removes all generated markup, classes, and event listeners. Use when removing tree view from DOM. |

### Static Methods

These methods are called directly on the `HSTreeView` class.

| Method | Parameters | Return Type | Description |
| --- | --- | --- | --- |
| `HSTreeView.getInstance(target, isInstance)` | `target`: `HTMLElement \| string` (CSS selector)<br>`isInstance`: `boolean` (optional) | `HTMLElement \| { id: string \| number, element: HSTreeView } \| null` | Returns the tree view instance or element associated with the target. If `isInstance` is `true`, returns collection item object `{ id, element }` where `element` is the `HSTreeView` instance. If `isInstance` is `false` or omitted, returns the DOM element (`HTMLElement`). Returns `null` if tree view instance is not found. |

### Usage Examples

**Example 1: Getting selected items**
```javascript
// Get the tree view instance
const instance = HSTreeView.getInstance('#hs-tree-view', true);

if (instance) {
  const { element } = instance;
  // Get all selected items
  const selectedItems = element.getSelectedItems();

  console.log('Selected items:', selectedItems);
}
```

**Example 2: Updating tree view after dynamic changes**
```javascript
const instance = HSTreeView.getInstance('#hs-tree-view', true);

if (instance) {
  const { element } = instance;

  // After adding or removing items dynamically
  element.update();
}
```

**Example 3: Changing item properties programmatically**
```javascript
const instance = HSTreeView.getInstance('#hs-tree-view', true);

if (instance) {
  const { element } = instance;

  // Change item property
  element.changeItemProp('item-id', 'isSelected', true);
}
```

**Example 4: Destroying tree view instance**
```javascript
const instance = HSTreeView.getInstance('#hs-tree-view', true);

if (instance) {
  const { element } = instance;
  const removeBtn = document.querySelector('#hs-remove-btn');

  removeBtn.addEventListener('click', () => {
    // Clean up before removing from DOM
    element.destroy();
    // Now safe to remove the element
    document.querySelector('#hs-tree-view').remove();
  });
}
```

## Events

Tree view instances emit events that can be listened to for selection tracking and custom behavior.

| Event Name | When Fired | Callback Parameter | Description |
| --- | --- | --- | --- |
| `on:click` | When any item is selected | `{ id: string, value: string, isDir: boolean, path: string, isSelected: boolean }` | Fires when an item is clicked/selected. Returns an object with:<br>- `id`: Item identifier<br>- `value`: Item value<br>- `isDir`: Whether item is a directory<br>- `path`: Path to the item<br>- `isSelected`: Whether item is currently selected |

### Event Usage Example

```javascript
// Get tree view instance
const instance = HSTreeView.getInstance('#hs-tree-view', true);
if (instance) {
  const { element } = instance;

  // Listen to click event
  element.on('click', ({ id, value, isDir, path, isSelected }) => {
    console.log('Item clicked:', {
      id,
      value,
      isDirectory: isDir,
      path,
      isSelected
    });
    // Perform actions after item selection
    // e.g., update UI, track analytics, load content
  });
}
```

## Common Patterns

### Pattern 1: Checkbox Mode with Auto-select Children

Enable checkbox selection with automatic child selection.

```html
<div data-hs-tree-view='{
  "controlBy": "checkbox",
  "autoSelectChildren": true
}'>
  <!-- Tree items -->
</div>
```

### Pattern 2: Getting Selected Items

Retrieve all selected items programmatically.

```html
<div id="hs-tree-view-first" data-hs-tree-view>
  <!-- Tree items -->
</div>

<button id="hs-get-selected">Get Selected</button>

<script>
  const instance = HSTreeView.getInstance('#hs-tree-view-first', true);

  if (instance) {
    const { element } = instance;

    document.querySelector('#hs-get-selected').addEventListener('click', () => {
      const selected = element.getSelectedItems();
      
      console.log('Selected items:', selected);
    });
  }
</script>
```

## License

Copyright (c) 2024 Htmlstream.

Licensed under the [MIT License](https://opensource.org/licenses/MIT).
