# TreeView

A hierarchical tree view component for displaying nested data structures. Features expandable/collapsible nodes, lazy loading, context menus, and customizable node display. Perfect for file explorers, organizational charts, and hierarchical data navigation.

## Installation

```bash
npm install @ticatec/uniface-element
```

## Import

```typescript
import TreeView, { type LazyLoader, type NodeVisibleFun, type OnNodeSelectionChange } from "@ticatec/uniface-element/TreeView";
import TreeNodes, { TreeNode, NodeViewOptions } from "@ticatec/uniface-element/TreeNodes";
```

## Basic Usage

```svelte
<script>
  import TreeView from "@ticatec/uniface-element/TreeView";
  import TreeNodes from "@ticatec/uniface-element/TreeNodes";
  
  let activeNode = null;
  
  // Sample data
  const data = [
    { id: 1, name: "Documents", parent: null },
    { id: 2, name: "Projects", parent: null },
    { id: 3, name: "Project A", parent: 2 },
    { id: 4, name: "Project B", parent: 2 },
    { id: 5, name: "file1.txt", parent: 3 },
    { id: 6, name: "file2.txt", parent: 3 }
  ];
  
  // Create tree structure
  const treeNodes = new TreeNodes({
    keyField: 'id',
    textField: 'name',
    parentKeyField: 'parent',
    checkIsRoot: (item) => item.parent === null,
    checkIsDirectory: (node) => node.children != null
  });
  
  treeNodes.setData(data);
  
  function handleSelectionChange(node) {
    console.log('Selected node:', node);
    return true; // Allow selection
  }
</script>

<TreeView 
  nodes={treeNodes.nodes}
  textField="name"
  bind:activeNode
  onchange={handleSelectionChange}
  checkIsDirectory={node => node.children != null}
/>
```

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `nodes` | `Array<TreeNode<any>>` | Required | Array of tree nodes to display |
| `textField` | `string \| GetText<any>` | Required | Field name or function to get node text |
| `activeNode` | `any` | `null` | Currently selected/active node |
| `style` | `string` | `""` | Additional CSS styles |
| `title` | `string` | `undefined` | Optional title displayed at the top of the tree (fixed position, doesn't scroll) |
| `lazyLoader` | `LazyLoader \| null` | `null` | Lazy loading configuration |
| `isVisible` | `NodeVisibleFun` | `null` | Function to determine node visibility |
| `onchange` | `OnNodeSelectionChange` | `null` | Callback when node selection changes |
| `onfocus` | `((event: FocusEvent) => void) \| null` | `null` | Callback when tree gains focus |
| `onblur` | `((event: FocusEvent) => void) \| null` | `null` | Callback when tree loses focus |
| `onContextMenu` | `OnContextMenu` | `null` | Right-click context menu handler |
| `checkIsDirectory` | `CheckIsDirectory<any>` | Required | Function to check if node is a directory |
| `version` | `number \| undefined` | `undefined` | Version number to trigger reactive updates (from TreeNodes object) |
| `class` | `string` | `""` | CSS class name |

## Type Definitions

### TreeNode<T>
```typescript
class TreeNode<T> implements ITreeNode<T> {
  readonly item: T;                           // The actual data item
  get children(): Array<TreeNode<T>> | null;  // Child nodes (shallow copy)
  get level(): number;                        // Node level (root = 0)
  expand: boolean;                            // Whether node is expanded
  loading: boolean;                           // Whether node is loading (for lazy loading)
  readonly parent: TreeNode<T> | null;        // Parent node reference

  // Methods
  append(childItem: T | Array<T>): ITreeNode<T> | Array<ITreeNode<T>>;  // Add child(ren)
  detach(): void;                                                              // Remove this node from parent
  moveTo(newParent: TreeNode<T>): TreeNode<T>;                                 // Move to different parent
  replace(newItem: T): void;                                                   // Replace node data
  removeChildren(): void;                                                      // Remove all children
}
```

### ITreeNode<T>
```typescript
interface ITreeNode<T> {
  expand: boolean;                              // Whether node is expanded
  loading: boolean;                             // Whether node is loading
  get level(): number;                          // Node level (root = 0)
  get children(): Array<ITreeNode<T>> | null;   // Child nodes (read-only)
  get item(): T;                                // Node data

  // Methods
  append(childItem: T | Array<T>): ITreeNode<T> | Array<ITreeNode<T>>;
  detach(): void;
  moveTo(newParent: ITreeNode<T>): ITreeNode<T>;
  replace(newItem: T): void;
  removeChildren(): void;
}
```

### TreeLazyLoader
```typescript
interface TreeLazyLoader {
  isBranch: (node: ITreeNode<any>) => boolean;              // Check if node can have children
  load: (node: ITreeNode<any>) => Promise<Array<any>>;      // Load children data
}
```

### LazyLoader (Deprecated)
```typescript
/**
 * @deprecated Use TreeLazyLoader instead. Will be removed in future versions.
 */
type LazyLoader = TreeLazyLoader;
```

### OnNodeSelectionChange
```typescript
type OnNodeSelectionChange = (node: TreeNode<any>) => Promise<boolean>;
```

### NodeVisibleFun
```typescript
type NodeVisibleFun = (node: TreeNode<any>) => boolean;
```

## Examples

### File System Tree

```svelte
<script>
  import TreeView from "@ticatec/uniface-element/TreeView";
  import TreeNodes from "@ticatec/uniface-element/TreeNodes";
  
  let activeNode = null;
  
  const fileSystemData = [
    { id: 1, name: "=� Documents", type: "folder", parent: null },
    { id: 2, name: "=� Pictures", type: "folder", parent: null },
    { id: 3, name: "=� Work", type: "folder", parent: 1 },
    { id: 4, name: "=� Personal", type: "folder", parent: 1 },
    { id: 5, name: "=� resume.pdf", type: "file", parent: 3 },
    { id: 6, name: "=� cover-letter.doc", type: "file", parent: 3 },
    { id: 7, name: "=� vacation.jpg", type: "file", parent: 2 },
    { id: 8, name: "=� family.png", type: "file", parent: 2 }
  ];
  
  const fileTree = new TreeNodes({
    keyField: 'id',
    textField: 'name',
    parentKeyField: 'parent',
    checkIsRoot: (item) => item.parent === null,
    checkIsDirectory: (node) => node.item.type === 'folder',
    expendDepth: 1 // Expand first level by default
  });
  
  fileTree.setData(fileSystemData);
  
  function handleFileSelection(node) {
    console.log('Selected file:', node.item.name);
    return true;
  }
  
  function isDirectory(node) {
    return node.item.type === 'folder';
  }
</script>

<div class="file-explorer">
  <h3>File Explorer</h3>
  <TreeView 
    nodes={fileTree.nodes}
    textField="name"
    bind:activeNode
    onchange={handleFileSelection}
    checkIsDirectory={isDirectory}
  />
</div>

<style>
  .file-explorer {
    width: 300px;
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 16px;
  }
  
  h3 {
    margin: 0 0 12px 0;
    font-size: 16px;
  }
</style>
```

### Organization Chart

```svelte
<script>
  import TreeView from "@ticatec/uniface-element/TreeView";
  import TreeNodes from "@ticatec/uniface-element/TreeNodes";
  
  let selectedEmployee = null;
  
  const orgData = [
    { id: 1, name: "CEO - John Smith", position: "Chief Executive Officer", manager: null },
    { id: 2, name: "CTO - Jane Doe", position: "Chief Technology Officer", manager: 1 },
    { id: 3, name: "CFO - Bob Wilson", position: "Chief Financial Officer", manager: 1 },
    { id: 4, name: "Dev Manager - Alice Brown", position: "Development Manager", manager: 2 },
    { id: 5, name: "QA Manager - Charlie Davis", position: "QA Manager", manager: 2 },
    { id: 6, name: "Developer - Eve Johnson", position: "Senior Developer", manager: 4 },
    { id: 7, name: "Developer - Frank Miller", position: "Junior Developer", manager: 4 },
    { id: 8, name: "QA Engineer - Grace Lee", position: "QA Engineer", manager: 5 }
  ];
  
  const orgChart = new TreeNodes({
    keyField: 'id',
    textField: 'name',
    parentKeyField: 'manager',
    checkIsRoot: (item) => item.manager === null,
    checkIsDirectory: (node) => node.children && node.children.length > 0,
    expendDepth: 2
  });
  
  orgChart.setData(orgData);
  
  function handleEmployeeSelection(node) {
    selectedEmployee = node.item;
    return true;
  }
  
  function isManager(node) {
    return node.children && node.children.length > 0;
  }
</script>

<div class="org-chart">
  <h3>Organization Chart</h3>
  <TreeView 
    nodes={orgChart.nodes}
    textField="name"
    onchange={handleEmployeeSelection}
    checkIsDirectory={isManager}
  />
  
  {#if selectedEmployee}
    <div class="employee-details">
      <h4>Employee Details</h4>
      <p><strong>Name:</strong> {selectedEmployee.name}</p>
      <p><strong>Position:</strong> {selectedEmployee.position}</p>
    </div>
  {/if}
</div>

<style>
  .org-chart {
    width: 400px;
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 16px;
  }
  
  .employee-details {
    margin-top: 16px;
    padding: 12px;
    background: #f8f9fa;
    border-radius: 4px;
  }
  
  .employee-details h4 {
    margin: 0 0 8px 0;
  }
  
  .employee-details p {
    margin: 4px 0;
  }
</style>
```

### With Lazy Loading

```svelte
<script>
  import TreeView from "@ticatec/uniface-element/TreeView";
  import TreeNodes from "@ticatec/uniface-element/TreeNodes";
  
  let activeNode = null;
  
  // Initial root nodes
  const initialData = [
    { id: 1, name: "Folder A", hasChildren: true },
    { id: 2, name: "Folder B", hasChildren: true },
    { id: 3, name: "File 1.txt", hasChildren: false }
  ];
  
  const treeNodes = new TreeNodes({
    keyField: 'id',
    textField: 'name',
    parentKeyField: 'parent',
    checkIsRoot: (item) => !item.parent,
    checkIsDirectory: (node) => node.item.hasChildren
  });
  
  treeNodes.setData(initialData);
  
  // Lazy loader configuration
  const lazyLoader = {
    isBranch: (node) => node.item.hasChildren,
    
    load: async (node) => {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 1000));
      
      // Generate child nodes
      const children = [];
      for (let i = 1; i <= 3; i++) {
        children.push({
          id: `${node.item.id}-${i}`,
          name: `${node.item.name} - Child ${i}`,
          parent: node.item.id,
          hasChildren: Math.random() > 0.7 // 30% chance of having children
        });
      }
      
      return children.map(child => ({ item: child, children: null }));
    }
  };
  
  function handleSelection(node) {
    console.log('Selected:', node.item.name);
    return true;
  }
  
  function isDirectory(node) {
    return node.item.hasChildren;
  }
</script>

<div class="lazy-tree">
  <h3>Lazy Loading Tree</h3>
  <TreeView 
    nodes={treeNodes.nodes}
    textField="name"
    bind:activeNode
    {lazyLoader}
    onchange={handleSelection}
    checkIsDirectory={isDirectory}
  />
</div>

<style>
  .lazy-tree {
    width: 300px;
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 16px;
  }
</style>
```

### With Context Menu

```svelte
<script>
  import TreeView from "@ticatec/uniface-element/TreeView";
  import TreeNodes from "@ticatec/uniface-element/TreeNodes";
  import ContextMenu from "@ticatec/uniface-element/ContextMenu";
  
  let activeNode = null;
  let contextMenu;
  
  const menuData = [
    { id: 1, name: "Root Folder", type: "folder", parent: null },
    { id: 2, name: "Subfolder", type: "folder", parent: 1 },
    { id: 3, name: "document.txt", type: "file", parent: 2 }
  ];
  
  const menuTree = new TreeNodes({
    keyField: 'id',
    textField: 'name',
    parentKeyField: 'parent',
    checkIsRoot: (item) => item.parent === null,
    checkIsDirectory: (node) => node.item.type === 'folder'
  });
  
  menuTree.setData(menuData);
  
  function handleContextMenu(event, node) {
    const isFolder = node.item.type === 'folder';
    
    const menuItems = [
      {
        text: "Open",
        action: () => console.log('Open:', node.item.name)
      },
      {
        text: "Rename",
        action: () => console.log('Rename:', node.item.name)
      },
      ...(isFolder ? [{
        text: "Add File",
        action: () => console.log('Add file to:', node.item.name)
      }] : []),
      {
        text: "Delete",
        action: () => console.log('Delete:', node.item.name)
      }
    ];
    
    contextMenu.show(event, menuItems);
  }
  
  function isDirectory(node) {
    return node.item.type === 'folder';
  }
</script>

<div class="context-tree">
  <h3>Right-click for Context Menu</h3>
  <TreeView 
    nodes={menuTree.nodes}
    textField="name"
    bind:activeNode
    onContextMenu={handleContextMenu}
    checkIsDirectory={isDirectory}
  />
</div>

<ContextMenu bind:this={contextMenu} />

<style>
  .context-tree {
    width: 300px;
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 16px;
  }
</style>
```

### With Custom Visibility Filter

```svelte
<script>
  import TreeView from "@ticatec/uniface-element/TreeView";
  import TreeNodes from "@ticatec/uniface-element/TreeNodes";
  
  let activeNode = null;
  let showHidden = false;
  
  const fileData = [
    { id: 1, name: "public", hidden: false, parent: null },
    { id: 2, name: ".env", hidden: true, parent: null },
    { id: 3, name: "index.js", hidden: false, parent: 1 },
    { id: 4, name: ".gitignore", hidden: true, parent: 1 },
    { id: 5, name: "config.json", hidden: false, parent: 1 }
  ];
  
  const fileTree = new TreeNodes({
    keyField: 'id',
    textField: 'name',
    parentKeyField: 'parent',
    checkIsRoot: (item) => item.parent === null,
    checkIsDirectory: (node) => node.children && node.children.length > 0
  });
  
  fileTree.setData(fileData);
  
  function isVisible(node) {
    return showHidden || !node.item.hidden;
  }
  
  function isDirectory(node) {
    return node.children && node.children.length > 0;
  }
</script>

<div class="filtered-tree">
  <div class="controls">
    <label>
      <input type="checkbox" bind:checked={showHidden} />
      Show hidden files
    </label>
  </div>
  
  <TreeView 
    nodes={fileTree.nodes}
    textField="name"
    bind:activeNode
    {isVisible}
    checkIsDirectory={isDirectory}
  />
</div>

<style>
  .filtered-tree {
    width: 300px;
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 16px;
  }
  
  .controls {
    margin-bottom: 12px;
  }
  
  label {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 14px;
  }
</style>
```

### With Event Handlers

```svelte
<script>
  import TreeView from "@ticatec/uniface-element/TreeView";
  import TreeNodes from "@ticatec/uniface-element/TreeNodes";
  
  let activeNode = null;
  let isFocused = false;
  let selectionLog = [];
  
  const eventData = [
    { id: 1, name: "Node 1", parent: null },
    { id: 2, name: "Node 2", parent: null },
    { id: 3, name: "Child 1", parent: 1 },
    { id: 4, name: "Child 2", parent: 1 }
  ];
  
  const eventTree = new TreeNodes({
    keyField: 'id',
    textField: 'name',
    parentKeyField: 'parent',
    checkIsRoot: (item) => item.parent === null,
    checkIsDirectory: (node) => node.children && node.children.length > 0
  });
  
  eventTree.setData(eventData);
  
  function handleSelectionChange(node) {
    selectionLog = [...selectionLog, `Selected: ${node.item.name} at ${new Date().toLocaleTimeString()}`];
    return true;
  }
  
  function handleFocus(event) {
    console.log('Tree focused');
    isFocused = true;
  }
  
  function handleBlur(event) {
    console.log('Tree blurred');
    isFocused = false;
  }
  
  function isDirectory(node) {
    return node.children && node.children.length > 0;
  }
</script>

<div class="event-tree">
  <h3>Event Handling Demo</h3>
  <p>Tree is {isFocused ? 'focused' : 'not focused'}</p>
  
  <TreeView 
    nodes={eventTree.nodes}
    textField="name"
    bind:activeNode
    onchange={handleSelectionChange}
    onfocus={handleFocus}
    onblur={handleBlur}
    checkIsDirectory={isDirectory}
  />
  
  <div class="log">
    <h4>Selection Log:</h4>
    {#each selectionLog.slice(-5) as entry}
      <p>{entry}</p>
    {/each}
  </div>
</div>

<style>
  .event-tree {
    width: 400px;
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 16px;
  }
  
  .log {
    margin-top: 16px;
    padding: 12px;
    background: #f8f9fa;
    border-radius: 4px;
    max-height: 150px;
    overflow-y: auto;
  }
  
  .log h4 {
    margin: 0 0 8px 0;
    font-size: 14px;
  }
  
  .log p {
    margin: 2px 0;
    font-size: 12px;
    font-family: monospace;
  }
</style>
```

### Geographic Hierarchy

```svelte
<script>
  import TreeView from "@ticatec/uniface-element/TreeView";
  import TreeNodes from "@ticatec/uniface-element/TreeNodes";
  
  let activeLocation = null;
  
  const locationData = [
    { code: "CN", name: "China", type: "country", parent: null },
    { code: "US", name: "United States", type: "country", parent: null },
    { code: "CN-BJ", name: "Beijing", type: "province", parent: "CN" },
    { code: "CN-SH", name: "Shanghai", type: "province", parent: "CN" },
    { code: "US-CA", name: "California", type: "state", parent: "US" },
    { code: "US-NY", name: "New York", type: "state", parent: "US" },
    { code: "CN-BJ-HD", name: "Haidian District", type: "district", parent: "CN-BJ" },
    { code: "CN-BJ-CY", name: "Chaoyang District", type: "district", parent: "CN-BJ" },
    { code: "US-CA-SF", name: "San Francisco", type: "city", parent: "US-CA" },
    { code: "US-CA-LA", name: "Los Angeles", type: "city", parent: "US-CA" }
  ];
  
  const locationTree = new TreeNodes({
    keyField: 'code',
    textField: 'name',
    parentKeyField: 'parent',
    checkIsRoot: (item) => item.parent === null,
    checkIsDirectory: (node) => {
      const hasChildren = locationData.some(loc => loc.parent === node.item.code);
      return hasChildren;
    },
    expendDepth: 1
  });
  
  locationTree.setData(locationData);
  
  function handleLocationSelection(node) {
    activeLocation = node.item;
    return true;
  }
  
  function isDirectory(node) {
    return locationData.some(loc => loc.parent === node.item.code);
  }
  
  function getLocationIcon(type) {
    switch (type) {
      case 'country': return '<';
      case 'province':
      case 'state': return '<�';
      case 'district':
      case 'city': return '<�';
      default: return '=�';
    }
  }
  
  function displayLocationName(item) {
    return `${getLocationIcon(item.type)} ${item.name}`;
  }
</script>

<div class="location-tree">
  <h3>Geographic Hierarchy</h3>
  <TreeView 
    nodes={locationTree.nodes}
    textField={displayLocationName}
    onchange={handleLocationSelection}
    checkIsDirectory={isDirectory}
  />
  
  {#if activeLocation}
    <div class="location-details">
      <h4>Selected Location</h4>
      <p><strong>Name:</strong> {activeLocation.name}</p>
      <p><strong>Code:</strong> {activeLocation.code}</p>
      <p><strong>Type:</strong> {activeLocation.type}</p>
    </div>
  {/if}
</div>

<style>
  .location-tree {
    width: 350px;
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 16px;
  }
  
  .location-details {
    margin-top: 16px;
    padding: 12px;
    background: #f8f9fa;
    border-radius: 4px;
  }
  
  .location-details h4 {
    margin: 0 0 8px 0;
  }
  
  .location-details p {
    margin: 4px 0;
  }
</style>
```

## Features

- **Hierarchical Display**: Clean tree structure with expandable/collapsible nodes
- **Lazy Loading**: Load child nodes on demand for large datasets
- **Context Menus**: Right-click support for custom actions
- **Keyboard Navigation**: Full keyboard accessibility support
- **Custom Visibility**: Filter nodes based on custom criteria
- **Selection Management**: Track and control node selection
- **Event Handling**: Comprehensive event system for interactions
- **Flexible Data**: Support for any data structure with configurable mapping

## Node Management

The TreeView component works with the `TreeNodes` utility class to manage hierarchical data:

```typescript
const treeNodes = new TreeNodes({
  keyField: 'id',           // Unique identifier field
  textField: 'name',        // Display text field
  parentKeyField: 'parent', // Parent reference field
  checkIsRoot: (item) => item.parent === null,
  checkIsDirectory: (node) => node.children != null,
  expendDepth: 1           // Initial expansion depth
});

treeNodes.setData(flatData); // Convert flat data to tree structure
```

### Using TreeNode Methods

TreeNode is now a class with built-in methods for data manipulation:

```typescript
// Get a node and manipulate it directly
const node = treeNodes.nodes[0];

// Add a child node (or array of children)
node.append({
  id: 'new-child',
  name: 'New Child',
  parent: node.item.id
});

// Or add multiple children at once
node.append([
  { id: 'child1', name: 'Child 1', parent: node.item.id },
  { id: 'child2', name: 'Child 2', parent: node.item.id }
]);

// Remove the node from tree
node.detach();

// Move node to different parent (returns new node instance)
const newParent = treeNodes.nodes[1];
const movedNode = node.moveTo(newParent);

// Replace node data
node.replace({
  id: node.item.id,
  name: 'Updated Name',
  parent: node.item.parent
});

// Remove all children
node.removeChildren();

// Access properties
console.log(node.item);      // Node data
console.log(node.children);  // Shallow copy of children array
console.log(node.level);     // Node level (root = 0)
console.log(node.expand);    // Expanded state
console.log(node.parent);    // Parent node
```

### Using NodeViewOptions

NodeViewOptions allows you to configure how nodes are displayed:

```typescript
const viewOptions = new NodeViewOptions({
  keyField: 'code',
  textField: 'name',
  // Or use a function for dynamic text
  textField: (data) => `${data.code} - ${data.name}`,

  // Optional: icon field
  iconField: 'icon',
  // Or use a function
  iconField: (data) => data.type === 'folder' ? 'folder-icon' : 'file-icon',

  // Optional: CSS class
  cssClassField: 'status',
  // Or use a function
  cssClassField: (data) => data.active ? 'active-node' : 'inactive-node',

  // Optional: dynamic styles
  styleField: (data) => ({
    color: data.color,
    fontWeight: data.important ? 'bold' : 'normal'
  })
});

// Use in TreeView
<TreeView
  nodes={treeNodes.nodes}
  nodeViewOptions={viewOptions}
/>
```

### Working with Tree Title

The title prop adds a fixed header to the tree view:

```typescript
<TreeView
  nodes={zones.nodes}
  title="China"  // Displays "中国" at the top, doesn't scroll
  // Clicking the title clears the activeNode selection
/>
```

The title is useful for:
- Displaying the root entity name (e.g., "China" for provinces)
- Providing a way to clear selection by clicking
- Adding context to hierarchical data
- Organization charts showing company name

## Accessibility

- Keyboard navigation with arrow keys and Enter
- Proper ARIA attributes for screen readers
- Focus management and visual indicators
- Semantic HTML structure
- Tab navigation support

## Best Practices

1. **Data Structure**: Use consistent field names across your data
2. **Performance**: Implement lazy loading for large datasets
3. **Visibility**: Use the `isVisible` prop to filter nodes efficiently
4. **Selection**: Always return a boolean from selection change handlers
5. **Context Menus**: Provide contextually relevant menu items
6. **Accessibility**: Ensure proper keyboard navigation support

## Browser Support

- Modern browsers with full event support
- Compatible with Svelte 5+
- Keyboard navigation support
- Touch-friendly interface
- Full TypeScript support

## Related Components

- `TreeNodes` - Tree data structure utility
- `TreeNode` - Tree node class with data manipulation methods
- `NodeViewOptions` - View configuration for tree nodes
- `ContextMenu` - Right-click context menu component
- `LazyLoader` - Async data loading interface