@keenmate/svelte-treeview
Version:
A high-performance, feature-rich hierarchical tree view component for Svelte 5 with drag & drop support, search functionality, and flexible data structures using LTree.
922 lines (740 loc) • 31.9 kB
Markdown
# @keenmate/svelte-treeview
A high-performance, feature-rich hierarchical tree view component for Svelte 5 with drag & drop support, search functionality, and flexible data structures using LTree.
> [!IMPORTANT]
> **Looking for a framework-agnostic solution?** There's also a web component version that can be used standalone or in other frameworks at https://github.com/KeenMate/web-treeview/
## 🚀 Features
- **Svelte 5 Native**: Built specifically for Svelte 5 with full support for runes and modern Svelte patterns
- **High Performance**: Uses LTree data structure for efficient hierarchical data management
- **Drag & Drop**: Built-in drag and drop support with validation and visual feedback
- **Search & Filter**: Integrated FlexSearch for fast, full-text search capabilities
- **Flexible Data Sources**: Works with any hierarchical data structure
- **Context Menus**: Dynamic right-click menus with callback-based generation, icons, disabled states
- **Visual Customization**: Extensive styling options and icon customization
- **TypeScript Support**: Full TypeScript support with comprehensive type definitions
- **Accessibility**: Built with accessibility in mind
## 📦 Installation
```bash
npm install @keenmate/svelte-treeview
```
## 🔨 Development Setup
For developers working on the project, you can use either standard npm commands or the provided Makefile (which provides a unified interface for all contributors):
```bash
# Using Makefile (recommended for consistency)
make setup # or make install
make dev
# Or using standard npm commands
npm install
npm run dev
```
## 🎨 Importing Styles
The component requires CSS to display correctly. Import the styles in your app:
### Option 1: Import SCSS in your main app file
```javascript
// In your main.js or main.ts
import '@keenmate/svelte-treeview/styles.scss';
```
### Option 2: Import in your Svelte component
```svelte
<style>
@import '@keenmate/svelte-treeview/styles.scss';
</style>
```
### Option 3: Use with your build system
If using Vite, Webpack, or similar, you can import the SCSS:
```javascript
import '@keenmate/svelte-treeview/styles.scss';
```
## 🎯 Quick Start
```svelte
<script lang="ts">
import { Tree } from '@keenmate/svelte-treeview';
const data = [
{ path: '1', name: 'Documents', type: 'folder' },
{ path: '1.1', name: 'Projects', type: 'folder' },
{ path: '1.1.1', name: 'Project A', type: 'folder' },
{ path: '1.1.2', name: 'Project B', type: 'folder' },
{ path: '2', name: 'Pictures', type: 'folder' },
{ path: '2.1', name: 'Vacation', type: 'folder' }
];
</script>
<Tree
{data}
idMember="path"
pathMember="path"
displayValueMember="name"
/>
```
## 🔧 Advanced Usage
### With Custom Node Templates
```svelte
<script lang="ts">
import { Tree } from '@keenmate/svelte-treeview';
const fileData = [
{ path: '1', name: 'Documents', type: 'folder', icon: '📁' },
{ path: '1.1', name: 'report.pdf', type: 'file', icon: '📄', size: '2.3 MB' },
{ path: '2', name: 'Images', type: 'folder', icon: '🖼️' },
{ path: '2.1', name: 'photo.jpg', type: 'file', icon: '🖼️', size: '1.8 MB' }
];
</script>
<Tree
data={fileData}
idMember="path"
pathMember="path"
selectedNodeClass="ltree-selected-bold"
onNodeClicked={(node) => console.log('Clicked:', node.data.name)}
>
{#snippet nodeTemplate(node)}
<div class="d-flex align-items-center">
<span class="me-2">{node.data.icon}</span>
<strong>{node.data.name}</strong>
{#if node.data.size}
<small class="text-muted ms-2">({node.data.size})</small>
{/if}
</div>
{/snippet}
</Tree>
```
### With Search and Filtering
```svelte
<script lang="ts">
import { Tree } from '@keenmate/svelte-treeview';
let searchText = $state('');
const data = [/* your data */];
</script>
<input
type="text"
placeholder="Search..."
bind:value={searchText}
/>
<Tree
{data}
idMember="path"
pathMember="path"
shouldUseInternalSearchIndex={true}
searchValueMember="name"
bind:searchText
/>
```
### With Advanced Search Options
```svelte
<script lang="ts">
import { Tree } from '@keenmate/svelte-treeview';
import type { SearchOptions } from 'flexsearch';
let treeRef;
const data = [/* your data */];
// Programmatic search with FlexSearch options
function performAdvancedSearch(searchTerm: string) {
const searchOptions: SearchOptions = {
suggest: true, // Enable suggestions for typos
limit: 10, // Limit results to 10 items
bool: "and" // Use AND logic for multiple terms
};
const results = treeRef.searchNodes(searchTerm, searchOptions);
console.log('Advanced search results:', results);
}
// Programmatic filtering with options
function filterWithOptions(searchTerm: string) {
const searchOptions: SearchOptions = {
threshold: 0.8, // Similarity threshold
depth: 2 // Search depth
};
treeRef.filterNodes(searchTerm, searchOptions);
}
</script>
<Tree
bind:this={treeRef}
{data}
idMember="path"
pathMember="path"
shouldUseInternalSearchIndex={true}
searchValueMember="name"
/>
<button onclick={() => performAdvancedSearch('document')}>
Advanced Search
</button>
<button onclick={() => filterWithOptions('project')}>
Filter with Options
</button>
```
#### FlexSearch Options Reference
The `searchOptions` parameter accepts any options supported by FlexSearch. Common options include:
| Option | Type | Description | Example |
|--------|------|-------------|---------|
| `suggest` | `boolean` | Enable suggestions for typos | `{ suggest: true }` |
| `limit` | `number` | Maximum number of results | `{ limit: 10 }` |
| `threshold` | `number` | Similarity threshold (0-1) | `{ threshold: 0.8 }` |
| `depth` | `number` | Search depth for nested content | `{ depth: 2 }` |
| `bool` | `string` | Boolean logic: "and", "or" | `{ bool: "and" }` |
| `where` | `object` | Filter by field values | `{ where: { type: "folder" } }` |
For complete FlexSearch documentation, visit: [FlexSearch Options](https://github.com/nextapps-de/flexsearch#options)
### With Drag & Drop
```svelte
<script lang="ts">
import { Tree } from '@keenmate/svelte-treeview';
const sourceData = [
{ path: '1', name: 'Item 1', isDraggable: true },
{ path: '2', name: 'Item 2', isDraggable: true }
];
const targetData = [
{ path: 'zone1', name: 'Drop Zone 1' },
{ path: 'zone2', name: 'Drop Zone 2' }
];
function onDragStart(node, event) {
console.log('Dragging:', node.data.name);
}
function onDrop(dropNode, draggedNode, event) {
console.log(`Dropped ${draggedNode.data.name} onto ${dropNode.data.name}`);
// Handle the drop logic here
}
</script>
<div class="row">
<div class="col-6">
<Tree
data={sourceData}
idMember="path"
pathMember="path"
onNodeDragStart={onDragStart}
/>
</div>
<div class="col-6">
<Tree
data={targetData}
idMember="path"
pathMember="path"
dragOverNodeClass="ltree-dragover-highlight"
onNodeDrop={onDrop}
/>
</div>
</div>
```
### With Context Menus
The tree supports context menus with two approaches: callback-based (recommended) and snippet-based.
#### Callback-Based Context Menus
```svelte
<script lang="ts">
import { Tree } from '@keenmate/svelte-treeview';
import type { ContextMenuItem } from '@keenmate/svelte-treeview';
const data = [
{ path: '1', name: 'Documents', type: 'folder', canEdit: true, canDelete: true },
{ path: '1.1', name: 'report.pdf', type: 'file', canEdit: true, canDelete: false },
{ path: '2', name: 'Images', type: 'folder', canEdit: false, canDelete: true }
];
function createContextMenu(node): ContextMenuItem[] {
const items: ContextMenuItem[] = [];
// Always available
items.push({
icon: '📂',
title: 'Open',
callback: () => alert(`Opening ${node.data.name}`)
});
// Conditional actions based on node data
if (node.data.canEdit) {
items.push({
icon: '✏️',
title: 'Edit',
callback: () => alert(`Editing ${node.data.name}`)
});
}
if (node.data.canDelete) {
items.push({
icon: '🗑️',
title: 'Delete',
callback: () => confirm(`Delete ${node.data.name}?`) && alert('Deleted!')
});
}
// Divider
items.push({ isDivider: true });
// Disabled item example
items.push({
icon: '🔒',
title: 'Restricted Action',
isDisabled: true,
callback: () => {}
});
return items;
}
</script>
<Tree
{data}
idMember="path"
pathMember="path"
contextMenuCallback={createContextMenu}
contextMenuXOffset={8}
contextMenuYOffset={0}
/>
```
#### Snippet-Based Context Menus
```svelte
<Tree
{data}
idMember="path"
pathMember="path"
>
{#snippet contextMenu(node, closeMenu)}
<div class="context-menu-item" onclick={() => { alert(`Open ${node.data.name}`); closeMenu(); }}>
📂 Open
</div>
<div class="context-menu-divider"></div>
<div class="context-menu-item" onclick={() => { alert(`Delete ${node.data.name}`); closeMenu(); }}>
🗑️ Delete
</div>
{/snippet}
</Tree>
```
#### Context Menu Features
- **Dynamic menus**: Generate menu items based on node properties
- **Icons and dividers**: Visual organization and identification
- **Disabled states**: Context-sensitive menu availability
- **Position offset**: `contextMenuXOffset`/`contextMenuYOffset` for cursor clearance
- **Auto-close**: Closes on scroll, click outside, or programmatically
- **Type safety**: Full TypeScript support with `ContextMenuItem` interface
## 🎨 Styling and Customization
The component comes with default styles that provide a clean, modern look. You can customize it extensively:
### CSS Variables
The component uses CSS custom properties for easy theming:
```css
:root {
--tree-node-indent-per-level: 0.5rem; /* Controls indentation for each hierarchy level */
--ltree-primary: #0d6efd;
--ltree-primary-rgb: 13, 110, 253;
--ltree-success: #198754;
--ltree-success-rgb: 25, 135, 84;
--ltree-danger: #dc3545;
--ltree-danger-rgb: 220, 53, 69;
--ltree-light: #f8f9fa;
--ltree-border: #dee2e6;
--ltree-body-color: #212529;
}
```
**Note**: The `--tree-node-indent-per-level` variable controls the consistent indentation applied at each hierarchy level. Each nested level receives this fixed indent amount, creating proper visual hierarchy without exponential indentation growth.
### SCSS Variables (if using SCSS)
If you're building the styles from SCSS source, you can override these variables:
```scss
// Import your overrides before the library styles
$tree-node-indent-per-level: 1rem;
$tree-node-font-family: 'Custom Font', sans-serif;
$primary-color: #custom-color;
@import '@keenmate/svelte-treeview/styles.scss';
```
### CSS Classes
- `.ltree-tree` - Main tree container
- `.ltree-node` - Individual node container
- `.ltree-node-content` - Node content area
- `.ltree-toggle-icon` - Expand/collapse icons
- `.ltree-selected-*` - Selected node styles
- `.ltree-dragover-*` - Drag-over node styles
- `.ltree-draggable` - Draggable nodes
- `.ltree-context-menu` - Context menu styling
- `.ltree-drag-over` - Applied during drag operations
- `.ltree-drop-valid` / `.ltree-drop-invalid` - Drop target validation
### Pre-built Selected Node Styles
The component includes several pre-built classes for styling selected nodes:
```svelte
<Tree
{data}
idMember="path"
pathMember="path"
selectedNodeClass="ltree-selected-bold"
/>
```
**Available Selected Node Classes:**
| Class | Description | Visual Effect |
|-------|-------------|---------------|
| `ltree-selected-bold` | Bold text with primary color | **Bold text** in theme primary color |
| `ltree-selected-border` | Border and background highlight | Solid border with light background |
| `ltree-selected-brackets` | Decorative brackets around text | ❯ **Node Text** ❮ |
**Available Drag-over Node Classes:**
| Class | Description | Visual Effect |
|-------|-------------|---------------|
| `ltree-dragover-highlight` | Dashed border with success color background | Green dashed border with subtle background |
| `ltree-dragover-glow` | Blue glow effect | Glowing shadow effect with primary color theme |
### Custom Icon Classes
```svelte
<Tree
{data}
idMember="path"
pathMember="path"
expandIconClass="custom-expand-icon"
collapseIconClass="custom-collapse-icon"
leafIconClass="custom-leaf-icon"
/>
```
## 📚 API Reference
### Tree Component Props
#### Core Required Properties
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `data` | `T[]` | ✅ | Array of data objects |
| `idMember` | `string` | ✅ | Property name for unique identifiers |
| `pathMember` | `string` | ✅ | Property name for hierarchical paths |
| `sortCallback` | `(items: T[]) => T[]` | ✅ | Function to sort items |
#### Data Mapping Properties
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `treeId` | `string \| null` | `null` | Unique identifier for the tree |
| `parentPathMember` | `string \| null` | `null` | Property name for parent path references |
| `levelMember` | `string \| null` | `null` | Property name for node level |
| `isExpandedMember` | `string \| null` | `null` | Property name for expanded state |
| `isSelectedMember` | `string \| null` | `null` | Property name for selected state |
| `isDraggableMember` | `string \| null` | `null` | Property name for draggable state |
| `isDropAllowedMember` | `string \| null` | `null` | Property name for drop allowed state |
| `hasChildrenMember` | `string \| null` | `null` | Property name for children existence |
| `isSorted` | `boolean \| null` | `null` | Whether items should be sorted |
#### Display & Search Properties
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `displayValueMember` | `string \| null` | `null` | Property name for display text |
| `getDisplayValueCallback` | `(node) => string` | `undefined` | Function to get display value |
| `searchValueMember` | `string \| null` | `null` | Property name for search indexing |
| `getSearchValueCallback` | `(node) => string` | `undefined` | Function to get search value |
| `shouldUseInternalSearchIndex` | `boolean` | `false` | Enable built-in search functionality |
| `initializeIndexCallback` | `() => Index` | `undefined` | Function to initialize search index |
| `searchText` | `string` (bindable) | `undefined` | Current search text |
**Note**: When `shouldUseInternalSearchIndex` is enabled, node indexing is performed asynchronously using `requestIdleCallback` (with fallback to `setTimeout`). This ensures the tree renders immediately while search indexing happens during browser idle time, providing better performance for large datasets.
**⚠️ Important**: For internal search indexing to work, you must:
1. Set `shouldUseInternalSearchIndex={true}`
2. Provide either `searchValueMember` (property name) or `getSearchValueCallback` (function)
Without both requirements, no search indexing will occur.
**Performance Tuning**:
- `indexerBatchSize` controls how many nodes are processed per idle callback. Lower values (10-25) provide smoother UI performance but slower indexing, while higher values (50-100) index faster but may cause brief UI pauses. Default: 25.
- `indexerTimeout` sets the maximum wait time before forcing indexing when the browser is busy. Lower values (25-50ms) ensure more responsive indexing, while higher values (100-200ms) give more time for genuine idle periods. Default: 50ms.
#### Tree Configuration
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `treeId` | `string \| null` | auto-generated | Unique identifier for the tree |
| `treePathSeparator` | `string \| null` | `"."` | Separator character for hierarchical paths (e.g., "." for "1.2.3" or "/" for "1/2/3") |
| `selectedNode` | `LTreeNode<T>` (bindable) | `undefined` | Currently selected node |
| `insertResult` | `InsertArrayResult<T>` (bindable) | `undefined` | Result of the last data insertion including failed nodes |
#### Behavior Properties
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `expandLevel` | `number \| null` | `2` | Automatically expand nodes up to this level |
| `shouldToggleOnNodeClick` | `boolean` | `true` | Toggle expansion on node click |
| `indexerBatchSize` | `number \| null` | `25` | Number of nodes to process per batch during search indexing |
| `indexerTimeout` | `number \| null` | `50` | Maximum time (ms) to wait for idle callback before forcing indexing |
| `shouldDisplayDebugInformation` | `boolean` | `false` | Show debug information panel with tree statistics and enable console debug logging for tree operations and async search indexing |
| `shouldDisplayContextMenuInDebugMode` | `boolean` | `false` | Display persistent context menu at fixed position for styling development |
#### Event Handler Properties
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `onNodeClicked` | `(node) => void` | `undefined` | Node click event handler |
| `onNodeDragStart` | `(node, event) => void` | `undefined` | Drag start event handler |
| `onNodeDragOver` | `(node, event) => void` | `undefined` | Drag over event handler |
| `onNodeDrop` | `(dropNode, draggedNode, event) => void` | `undefined` | Drop event handler |
#### Visual Styling Properties
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `bodyClass` | `string \| null` | `undefined` | CSS class for tree body |
| `selectedNodeClass` | `string \| null` | `undefined` | CSS class for selected nodes |
| `dragOverNodeClass` | `string \| null` | `undefined` | CSS class for nodes being dragged over |
| `expandIconClass` | `string \| null` | `"ltree-icon-expand"` | CSS class for expand icons |
| `collapseIconClass` | `string \| null` | `"ltree-icon-collapse"` | CSS class for collapse icons |
| `leafIconClass` | `string \| null` | `"ltree-icon-leaf"` | CSS class for leaf node icons |
| `scrollHighlightTimeout` | `number \| null` | `4000` | Duration (ms) for scroll highlight animation |
| `scrollHighlightClass` | `string \| null` | `'ltree-scroll-highlight'` | CSS class to apply for scroll highlight effect |
#### Available Slots
| Slot | Description |
|------|-------------|
| `nodeTemplate` | Custom node template |
| `treeHeader` | Tree header content |
| `treeBody` | Tree body content |
| `treeFooter` | Tree footer content |
| `noDataFound` | No data template |
| `contextMenu` | Context menu template |
#### Public Methods
| Method | Parameters | Description |
|--------|------------|-------------|
| `expandNodes` | `nodePath: string` | Expand nodes at specified path |
| `collapseNodes` | `nodePath: string` | Collapse nodes at specified path |
| `expandAll` | `nodePath?: string` | Expand all nodes or nodes under path |
| `collapseAll` | `nodePath?: string` | Collapse all nodes or nodes under path |
| `filterNodes` | `searchText: string, searchOptions?: SearchOptions` | Filter the tree display using internal search index with optional FlexSearch options |
| `searchNodes` | `searchText: string \| null \| undefined, searchOptions?: SearchOptions` | Search nodes using internal search index and return matching nodes with optional FlexSearch options |
| `scrollToPath` | `path: string, options?: ScrollToPathOptions` | Scroll to and highlight a specific node |
| `update` | `updates: Partial<Props>` | Programmatically update component props from external JavaScript |
#### ScrollToPath Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `expand` | `boolean` | `true` | Automatically expand parent nodes to make target visible |
| `highlight` | `boolean` | `true` | Apply temporary highlight animation to the target node |
| `scrollOptions` | `ScrollIntoViewOptions` | `{ behavior: 'smooth', block: 'center' }` | Native browser scroll options |
**Usage Example:**
```typescript
// Basic usage - scroll to path with default options
await tree.scrollToPath('1.2.3');
// Advanced usage - custom options
await tree.scrollToPath('1.2.3', {
expand: false, // Don't auto-expand parent nodes
highlight: false, // Skip highlight animation
scrollOptions: { // Custom scroll behavior
behavior: 'instant',
block: 'start'
}
});
```
**Highlight Classes Example:**
```svelte
<!-- Default background highlight -->
<Tree
{data}
idMember="path"
pathMember="path"
scrollHighlightClass="ltree-scroll-highlight"
scrollHighlightTimeout={5000}
/>
<!-- Red arrow highlight -->
<Tree
{data}
idMember="path"
pathMember="path"
scrollHighlightClass="ltree-scroll-highlight-arrow"
scrollHighlightTimeout={3000}
/>
<!-- Custom highlight class -->
<Tree
{data}
idMember="path"
pathMember="path"
scrollHighlightClass="my-custom-highlight"
scrollHighlightTimeout={2000}
/>
```
**Available Built-in Highlight Classes:**
- `ltree-scroll-highlight` - Background glow with blue color (default)
- `ltree-scroll-highlight-arrow` - Red left arrow indicator
#### Statistics
The tree provides real-time statistics about the loaded data:
| Property | Type | Description |
|----------|------|-------------|
| `statistics` | `{ nodeCount: number; maxLevel: number; filteredNodeCount: number; isIndexing: boolean; pendingIndexCount: number }` | Returns current node count, maximum depth level, filtered nodes count, indexing status, and pending index count |
```typescript
const { nodeCount, maxLevel, filteredNodeCount, isIndexing, pendingIndexCount } = tree.statistics;
console.log(`Tree has ${nodeCount} nodes with maximum depth of ${maxLevel} levels`);
if (filteredNodeCount > 0) {
console.log(`Currently showing ${filteredNodeCount} filtered nodes`);
}
if (isIndexing) {
console.log(`Search indexing in progress: ${pendingIndexCount} nodes pending`);
}
```
#### External Updates (Vanilla JavaScript)
The `update()` method allows you to programmatically update component props from external JavaScript code (outside of Svelte's reactivity system). This is particularly useful for HTML/JavaScript integration or dynamic configuration from non-Svelte code.
```javascript
// Get reference to the tree component
const treeElement = document.querySelector('#my-tree');
// Update multiple props at once
treeElement.update({
searchText: 'Production',
expandLevel: 3,
shouldDisplayDebugInformation: true,
data: newDataArray,
contextMenuXOffset: 10
});
// Update single prop
treeElement.update({ searchText: 'new search' });
// Update data and configuration
treeElement.update({
data: fetchedData,
expandLevel: 5,
selectedNodeClass: 'custom-selected'
});
```
**Updatable Properties:**
All Tree props can be updated except snippets/templates, including:
- Data and state: `data`, `searchText`, `selectedNode`, `expandLevel`
- Members: `idMember`, `pathMember`, `displayValueMember`, `searchValueMember`
- Callbacks: `sortCallback`, `getDisplayValueCallback`, `onNodeClicked`, etc.
- Visual: `bodyClass`, `selectedNodeClass`, `expandIconClass`, etc.
- Context menu: `contextMenuCallback`, `contextMenuXOffset`, `contextMenuYOffset`
- Behavior: `shouldToggleOnNodeClick`, `shouldUseInternalSearchIndex`, etc.
### Debug Information
Enable debug information to see real-time tree statistics and console logging:
```svelte
<Tree
{data}
idMember="path"
pathMember="path"
shouldDisplayDebugInformation={true}
/>
```
#### Debug Panel
The visual debug panel shows:
- Tree ID
- Data array length
- Expand level setting
- Node count
- Maximum depth levels
- Filtered node count (when filtering is active)
- Search indexing progress (when indexing is active)
- Currently dragged node
#### Console Debug Logging
When enabled, the component will log detailed information to the browser console including:
**Tree Operations:**
- Data mapping and sorting performance metrics
- Node filtering and search operations
- Tree structure changes
**Async Search Indexing:**
- Indexer initialization with batch size
- Queue management (items added, queue size)
- Batch processing details (timeout status, items processed, timing)
- Indexing completion and progress updates
This provides valuable insights for performance optimization and troubleshooting, especially when working with large datasets or complex search operations.
### Events
#### onNodeClicked(node)
Triggered when a node is clicked.
#### onNodeDragStart(node, event)
Triggered when drag operation starts.
#### onNodeDragOver(node, event)
Triggered when dragging over a potential drop target.
#### onNodeDrop(dropNode, draggedNode, event)
Triggered when a node is dropped onto another node.
### Slots
#### nodeTemplate
Custom template for rendering node content.
```svelte
{#snippet nodeTemplate(node)}
<!-- Your custom node content -->
{/snippet}
```
#### contextMenu
Custom context menu template (snippet-based approach).
```svelte
{#snippet contextMenu(node, closeMenu)}
<button onclick={() => { /* action */ closeMenu(); }}>
Action
</button>
{/snippet}
```
### Context Menu Properties
#### contextMenuCallback
Function that generates context menu items dynamically.
```typescript
contextMenuCallback: (node: LTreeNode<T>) => ContextMenuItem[]
```
Where `ContextMenuItem` is:
```typescript
interface ContextMenuItem {
icon?: string; // Optional icon (emoji or text)
title: string; // Menu item text
isDisabled?: boolean; // Whether item is disabled
callback: () => void; // Action to perform
isDivider?: boolean; // Render as divider instead of item
}
```
#### contextMenuXOffset
Horizontal offset from cursor position (default: 8px).
#### contextMenuYOffset
Vertical offset from cursor position (default: 0px).
#### shouldDisplayContextMenuInDebugMode
When enabled, displays a persistent context menu at a fixed position for styling development (default: false).
```svelte
<Tree
{data}
contextMenuCallback={createContextMenu}
shouldDisplayContextMenuInDebugMode={true}
shouldDisplayDebugInformation={true}
contextMenuXOffset={10}
contextMenuYOffset={5}
/>
```
**Debug Mode Features:**
- Shows context menu for the second node (or first if only one exists)
- Positions menu 200px right and 100px down from tree's top-left corner
- Persistent display - no need to right-click repeatedly
- Perfect for CSS styling and position testing
- Works with both callback-based and snippet-based context menus
## 🏗️ Data Structure
The component expects hierarchical data with path-based organization:
```typescript
interface NodeData {
path: string; // e.g., "1.2.3" for hierarchical positioning
// ... your custom properties
}
```
### Path Examples
- Root level: `"1"`, `"2"`, `"3"`
- Second level: `"1.1"`, `"1.2"`, `"2.1"`
- Third level: `"1.1.1"`, `"1.2.1"`, `"2.1.1"`
### Sorting Requirements
**Important:** For proper tree construction, your `sortCallback` must sort by **level first** to ensure parent nodes are inserted before their children:
```typescript
const sortCallback = (items: LTreeNode<T>[]) => {
return items.sort((a, b) => {
// First, sort by level (shallower levels first)
const aLevel = a.path.split('.').length;
const bLevel = b.path.split('.').length;
if (aLevel !== bLevel) {
return aLevel - bLevel;
}
// Then sort by your custom criteria
return a.data.name.localeCompare(b.data.name);
});
};
```
**Why this matters:** If deeper level nodes are processed before their parents, you'll get "Could not find parent node" errors during tree construction. Level-first sorting ensures hierarchical integrity and enables progressive rendering for large datasets.
### Insert Result Information
The tree provides detailed information about data insertion through the `insertResult` bindable property:
```typescript
interface InsertArrayResult<T> {
successful: number; // Number of nodes successfully inserted
failed: Array<{ // Nodes that failed to insert
node: LTreeNode<T>; // The processed tree node
originalData: T; // The original data object
error: string; // Error message (usually "Could not find parent...")
}>;
total: number; // Total number of nodes processed
}
```
#### Usage Example
```svelte
<script lang="ts">
import { Tree } from '@keenmate/svelte-treeview';
let insertResult = $state();
const data = [
{ id: '1', path: '1', name: 'Root' },
{ id: '1.2', path: '1.2', name: 'Child' }, // Missing parent "1.1"
{ id: '1.1.1', path: '1.1.1', name: 'Deep' } // Missing parent "1.1"
];
// Check results after tree processes data
$effect(() => {
if (insertResult) {
console.log(`✅ ${insertResult.successful} nodes inserted successfully`);
console.log(`❌ ${insertResult.failed.length} nodes failed to insert`);
insertResult.failed.forEach(failure => {
console.log(`Failed: ${failure.originalData.name} - ${failure.error}`);
});
}
});
</script>
<Tree
{data}
idMember="id"
pathMember="path"
displayValueMember="name"
bind:insertResult
/>
```
#### Benefits
- **Data Validation**: Identify missing parent nodes in hierarchical data
- **Debugging**: Clear error messages with node paths like "Node: 1.1.1 - Could not find parent node: 1.1"
- **Data Integrity**: Handle incomplete datasets gracefully
- **Search Accuracy**: Failed nodes are excluded from search index, ensuring search results match visible tree
- **User Feedback**: Inform users about data issues with detailed failure information
## 🚀 Performance
The component is optimized for large datasets:
- **LTree**: Efficient hierarchical data structure
- **Async Search Indexing**: Uses `requestIdleCallback` for non-blocking search index building
- **Accurate Search Results**: Search index only includes successfully inserted nodes, ensuring results match visible tree structure
- **Consistent Visual Hierarchy**: Optimized CSS-based indentation prevents exponential spacing growth
- **Virtual Scrolling**: (Coming soon)
- **Lazy Loading**: (Coming soon)
- **Search Indexing**: Uses FlexSearch for fast search operations
## 🤝 Contributing
We welcome contributions! Please see our contributing guidelines for details.
## 📄 License
MIT License - see LICENSE file for details.
## 🆘 Support
- **GitHub Issues**: [Report bugs or request features](https://github.com/keenmate/svelte-treeview/issues)
- **Documentation**: [Full documentation](https://github.com/keenmate/svelte-treeview#readme)
---
Built with ❤️ by [KeenMate](https://github.com/keenmate)