<script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
<script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
<div class="container">
<h1>Tree View with Drag & Drop</h1>
<ul id="tree" class="tree"></ul>
<div class="info">
<ion-icon name="information-circle-outline"></ion-icon>
Drag and drop items to reorganize. Click arrows to expand/collapse folders.
</div>
</div>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: #f5f7fa;
padding: 40px 20px;
color: #2c3e50;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 30px;
}
h1 {
font-size: 24px;
margin-bottom: 24px;
color: #1a202c;
}
.tree {
list-style: none;
}
.tree-node {
margin: 4px 0;
}
.tree-item {
display: flex;
align-items: center;
padding: 10px 12px;
border-radius: 6px;
cursor: grab;
transition: all 0.2s;
user-select: none;
background: #f8fafc;
border: 1px solid transparent;
}
.tree-item:hover {
background: #e2e8f0;
}
.tree-item.dragging {
opacity: 0.5;
cursor: grabbing;
}
.tree-item.drag-over {
border: 1px solid #3b82f6;
background: #eff6ff;
}
.toggle-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
margin-right: 8px;
cursor: pointer;
border: none;
background: transparent;
color: #64748b;
transition: transform 0.2s;
}
.toggle-btn.collapsed {
transform: rotate(-90deg);
}
.toggle-btn:hover {
color: #3b82f6;
}
.node-icon {
margin-right: 8px;
color: #3b82f6;
}
.node-label {
flex: 1;
font-size: 14px;
color: #334155;
}
.drag-handle {
color: #94a3b8;
margin-left: 8px;
opacity: 0;
transition: opacity 0.2s;
}
.tree-item:hover .drag-handle {
opacity: 1;
}
.tree-children {
list-style: none;
margin-left: 24px;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.tree-children.collapsed {
max-height: 0 !important;
}
.info {
margin-top: 20px;
padding: 12px;
background: #f0f9ff;
border-radius: 6px;
font-size: 13px;
color: #0369a1;
}
const treeData = [
{
id: '1',
label: 'Documents',
type: 'folder',
children: [
{ id: '1-1', label: 'Resume.pdf', type: 'file' },
{ id: '1-2', label: 'Cover Letter.docx', type: 'file' },
{
id: '1-3',
label: 'Projects',
type: 'folder',
children: [
{ id: '1-3-1', label: 'Project A.zip', type: 'file' },
{ id: '1-3-2', label: 'Project B.zip', type: 'file' }
]
}
]
},
{
id: '2',
label: 'Images',
type: 'folder',
children: [
{ id: '2-1', label: 'vacation.jpg', type: 'file' },
{ id: '2-2', label: 'profile.png', type: 'file' }
]
},
{
id: '3',
label: 'Notes.txt',
type: 'file'
}
];
let draggedElement = null;
let draggedNode = null;
function renderTree(data, container) {
data.forEach(node => {
const li = document.createElement('li');
li.className = 'tree-node';
li.dataset.id = node.id;
const item = document.createElement('div');
item.className = 'tree-item';
item.draggable = true;
if (node.type === 'folder' && node.children) {
const toggle = document.createElement('button');
toggle.className = 'toggle-btn';
toggle.innerHTML = '<ion-icon name="chevron-down-outline"></ion-icon>';
toggle.onclick = (e) => {
e.stopPropagation();
toggleNode(li);
};
item.appendChild(toggle);
} else {
const spacer = document.createElement('span');
spacer.style.width = '20px';
spacer.style.display = 'inline-block';
item.appendChild(spacer);
}
const icon = document.createElement('ion-icon');
icon.className = 'node-icon';
icon.name = node.type === 'folder' ? 'folder-outline' : 'document-outline';
item.appendChild(icon);
const label = document.createElement('span');
label.className = 'node-label';
label.textContent = node.label;
item.appendChild(label);
const handle = document.createElement('ion-icon');
handle.className = 'drag-handle';
handle.name = 'menu-outline';
item.appendChild(handle);
item.addEventListener('dragstart', handleDragStart);
item.addEventListener('dragover', handleDragOver);
item.addEventListener('drop', handleDrop);
item.addEventListener('dragend', handleDragEnd);
item.addEventListener('dragleave', handleDragLeave);
li.appendChild(item);
if (node.children) {
const childrenUl = document.createElement('ul');
childrenUl.className = 'tree-children';
childrenUl.style.maxHeight = childrenUl.scrollHeight + 'px';
renderTree(node.children, childrenUl);
li.appendChild(childrenUl);
}
container.appendChild(li);
});
}
function toggleNode(li) {
const children = li.querySelector('.tree-children');
const toggle = li.querySelector('.toggle-btn');
if (children) {
if (children.classList.contains('collapsed')) {
children.style.maxHeight = children.scrollHeight + 'px';
children.classList.remove('collapsed');
toggle.classList.remove('collapsed');
} else {
children.style.maxHeight = '0';
children.classList.add('collapsed');
toggle.classList.add('collapsed');
}
}
}
function handleDragStart(e) {
draggedElement = this;
draggedNode = this.closest('.tree-node');
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', this.innerHTML);
}
function handleDragOver(e) {
if (e.preventDefault) {
e.preventDefault();
}
e.dataTransfer.dropEffect = 'move';
const target = e.target.closest('.tree-item');
if (target && target !== draggedElement) {
target.classList.add('drag-over');
}
return false;
}
function handleDragLeave(e) {
const target = e.target.closest('.tree-item');
if (target) {
target.classList.remove('drag-over');
}
}
function handleDrop(e) {
if (e.stopPropagation) {
e.stopPropagation();
}
const target = e.target.closest('.tree-item');
const targetNode = target.closest('.tree-node');
if (draggedNode !== targetNode && draggedElement !== target) {
targetNode.parentNode.insertBefore(draggedNode, targetNode.nextSibling);
}
target.classList.remove('drag-over');
return false;
}
function handleDragEnd(e) {
this.classList.remove('dragging');
document.querySelectorAll('.tree-item').forEach(item => {
item.classList.remove('drag-over');
});
}
const treeContainer = document.getElementById('tree');
renderTree(treeData, treeContainer);
No comments yet. Be the first!