A Vue 3 library for creating draggable/sortable list items with hierarchical structure support. Perfect for building complex UI with nested hierarchies, drag & drop functionality, and rich interactions.
- π― Drag & Drop Reordering: Intuitive drag and drop to reorder items
- ποΈ Layer Groups/Nesting: Create hierarchical structures with unlimited nesting
- π Reverse Order Display: Display items in reverse order (bottom-to-top)
- π Reverse Order Groups: Optionally reverse order within groups independently
- π±οΈ Context Menus: Right-click context menus with customizable actions
- π Multi-Select: Select multiple items with Ctrl/Cmd + click or Shift + click
- π¨ Template Slots: Fully customizable item appearance using Vue slots
- π Item Positioning: Access to both current and original item indices
- ποΈ Custom Expand/Collapse: Customizable expand/collapse controls
- ποΈ Visibility Toggle: Show/hide items with visual indicators
- π Lock/Unlock: Prevent modifications to specific items
- β‘ Vue 3 + TypeScript: Built with modern Vue 3 Composition API and full TypeScript support
npm install @amitkhare/layer-vue
<template>
<LayerPanel
v-model:items="layers"
:allow-nesting="true"
:allow-multi-select="true"
:show-context-menu="true"
@item-select="handleItemSelect"
@item-reorder="handleReorder"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { LayerPanel, type LayerItemType } from '@amitkhare/layer-vue'
const layers = ref<LayerItemType[]>([
{
id: 'layer1',
title: 'Background',
visible: true,
locked: false,
selected: false
},
{
id: 'group1',
title: 'UI Group',
visible: true,
locked: false,
selected: false,
collapsed: false,
children: [
{
id: 'layer2',
title: 'Header',
visible: true,
locked: false,
selected: false,
parent: 'group1'
}
]
}
])
function handleItemSelect(item: LayerItemType, selectedItems: LayerItemType[]) {
console.log('Selected:', item.title, 'Total selected:', selectedItems.length)
}
function handleReorder(reorderedItems: LayerItemType[]) {
console.log('Items reordered:', reorderedItems)
}
</script>
The reverseOrder
prop allows you to control the display order of layers, and reverseOrderGroups
controls whether children within groups are also reversed:
<template>
<!-- Traditional order: first item at top -->
<LayerPanel
v-model:items="layers"
:reverse-order="false"
:reverse-order-groups="false"
/>
<!-- Design software order: first item at bottom -->
<LayerPanel
v-model:items="layers"
:reverse-order="true"
:reverse-order-groups="false"
/>
<!-- Both top-level and groups reversed -->
<LayerPanel
v-model:items="layers"
:reverse-order="true"
:reverse-order-groups="true"
/>
<!-- Dynamic controls -->
<div>
<label>
<input type="checkbox" v-model="reverseOrder" />
Reverse Order (Top Level)
</label>
<label>
<input type="checkbox" v-model="reverseOrderGroups" />
Reverse Order Groups (Children)
</label>
<LayerPanel
v-model:items="layers"
:reverse-order="reverseOrder"
:reverse-order-groups="reverseOrderGroups"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const reverseOrder = ref(false)
const reverseOrderGroups = ref(false)
// Given this layer data:
const layers = ref([
{ id: 1, title: 'Background' }, // Index 0, Original Index 0
{ id: 2, title: 'Content' }, // Index 1, Original Index 1
{ id: 3, title: 'Overlay' } // Index 2, Original Index 2
])
// reverseOrder: false displays: Background, Content, Overlay
// reverseOrder: true displays: Overlay, Content, Background
// Original indices remain constant regardless of display order
<
8000
/span></script>
<template>
<LayerPanel v-model:items="layers">
<!-- Custom content with index information -->
<template #item-content="{ item, level, index, originalIndex }">
<div class="custom-layer" :style="{ paddingLeft: (level * 20) + 'px' }">
<div class="layer-icon">
<i :class="getIconClass(item)"></i>
</div>
<span class="layer-name">{{ item.title }}</span>
<div class="layer-info">
<span class="index-info">{{ index }} ({{ originalIndex }})</span>
<span v-if="item.data?.opacity" class="opacity">
{{ item.data.opacity }}%
</span>
</div>
</div>
</template>
<!-- Custom expand/collapse controls -->
<template #item-expand="{ hasChildren, collapsed, toggleCollapse }">
<div v-if="hasChildren">
<button @click.stop="toggleCollapse" class="custom-expand">
{{ collapsed ? 'π' : 'π' }}
</button>
</div>
<div v-else class="expand-spacer"></div>
</template>
<!-- Custom controls -->
<template #item-controls="{ item }">
<div class="custom-controls">
<button @click="duplicateItem(item)">π</button>
<button @click="deleteItem(item)">ποΈ</button>
</div>
</template>
</LayerPanel>
</template>
Prop | Type | Default | Description |
---|---|---|---|
items |
LayerItem[] |
[] |
Array of layer items |
allowNesting |
boolean |
true |
Enable drag & drop nesting |
allowMultiSelect |
boolean |
true |
Enable multi-selection |
showContextMenu |
boolean |
true |
Show right-click context menu |
maxNestingLevel |
number |
10 |
Maximum nesting depth |
reverseOrder |
boolean |
false |
Reverse the display order of top-level items |
reverseOrderGroups |
boolean |
false |
Reverse the display order within groups/children |
interface LayerItem {
id: string | number
title: string
visible?: boolean
locked?: boolean
selected?: boolean
collapsed?: boolean
children?: LayerItem[]
parent?: string | number | null
data?: any
}
Event | Payload | Description |
---|---|---|
update:items |
LayerItem[] |
Emitted when items array changes |
item-select |
(item: LayerItem, selectedItems: LayerItem[]) |
Item selection changed |
item-toggle-visibility |
LayerItem |
Item visibility toggled |
item-toggle-lock |
LayerItem |
Item lock state toggled |
item-reorder |
LayerItem[] |
Items reordered via drag & drop |
item-group |
LayerItem[] |
Items grouped together |
item-ungroup |
LayerItem |
Group ungrouped |
item-duplicate |
LayerItem |
Item duplicated |
item-delete |
LayerItem[] |
Items deleted |
context-menu |
(event: MouseEvent, item: LayerItem) |
Context menu opened |
Customize the main content area of each item.
Slot Props:
item: LayerItem
- The layer item datalevel: number
- Nesting level (0-based)index: number
- Current display index (affected by reverse order)originalIndex: number
- Original index in the array (unaffected by reverse order)
Customize the expand/collapse button for items with children.
Slot Props:
item: LayerItem
- The layer item datalevel: number
- Nesting level (0-based)hasChildren: boolean
- Whether the item has childrencollapsed: boolean
- Whether the item is collapsedtoggleCollapse: () => void
- Function to toggle collapse state
Customize the controls area (right side) of each item.
Slot Props:
item: LayerItem
- The layer item data
Customize the context menu content.
Slot Props:
item: LayerItem
- The clicked itemselectedItems: LayerItem[]
- All selected items
- Ctrl/Cmd + Click: Multi-select items
- Shift + Click: Range select (simplified)
- Right Click: Open context menu
- Drag: Reorder items
- Drag to Group: Nest items inside groups
The component uses scoped styles and CSS custom properties for theming. You can override the default styles by targeting the component classes:
.layer-panel {
--layer-bg: #2d2d2d;
--layer-text: #ffffff;
--layer-selected: #007acc;
--layer-hover: rgba(255, 255, 255, 0.05);
}
.layer-item.selected {
background: var(--layer-selected);
}
.layer-item:hover {
background: var(--layer-hover);
}
# Install dependencies
npm install
# Start development server
npm run dev
# Build library
npm run build-lib
# Build demo
npm run build
To publish to NPM:
# 1. Build the library
npm run build-lib
# 2. Login to NPM (if not already logged in)
npm login
# 3. Publish to NPM
npm publish
The prepublishOnly
script will automatically build the library before publishing.
The project includes comprehensive demos showcasing all features:
Available Demos:
Demo Files:
src/demo/Demo.vue
- Feature-rich demo componentsrc/demo/SimpleDemo.vue
- Minimal demo componentsrc/demo/demo.ts
- Entry point for full demosrc/demo/simple-demo.ts
- Entry point for simple demo
Running Demos:
- Demo:
http://localhost:5173/
(default)
All demo files are organized in the src/demo/
directory for better project structure.
MIT License
Contributions are welcome! Please feel free to submit a Pull Request.