verstak/frontend/src/TreeNode.svelte

175 lines
4.8 KiB
Svelte

<script>
import TemplateIcon from './lib/TemplateIcon.svelte'
export let nodes = []
export let expanded = {}
export let selectedNodeId = ''
export let level = 0
export let onSelect
export let onToggle
export let onContextMenu
export let onDrop
function iconKind(node) {
if (node.type === 'client' || node.template_id === 'client.default') return 'client'
if (node.type === 'project' || node.template_id === 'project.default') return 'project'
if (node.type === 'document' || node.template_id === 'document.default') return 'document'
if (node.type === 'recipe' || node.template_id === 'recipe.default') return 'recipe'
if (node.type === 'folder' || node.template_id === 'folder.default') return 'folder'
if (node.type === 'case') return 'case'
if (node.type === 'note') return 'note'
if (node.type === 'file') return 'file'
return 'generic'
}
function isContainer(node) {
return ['folder', 'project', 'client', 'document', 'recipe', 'case'].includes(node.type)
}
function hasLoadedChildren(node) {
return node.children !== undefined
}
function canDrop(target, draggedId) {
if (!target || !draggedId) return false
if (draggedId === target.id) return false
if (!isContainer(target)) return false
if (isDescendant(target, draggedId)) return false
return true
}
function isDescendant(node, ancestorId) {
if (!node.children) return false
for (const child of node.children) {
if (child.id === ancestorId) return true
if (isDescendant(child, ancestorId)) return true
}
return false
}
function getDraggedId(e) {
try { return e.dataTransfer.getData('text/plain') } catch { return '' }
}
function handleDragStart(e, node) {
e.stopPropagation()
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.setData('text/plain', node.id)
}
function handleDragOver(e, node) {
const id = getDraggedId(e)
if (!canDrop(node, id)) return
e.preventDefault()
e.stopPropagation()
e.dataTransfer.dropEffect = 'move'
}
function handleDrop(e, node) {
e.preventDefault()
e.stopPropagation()
const draggedId = getDraggedId(e)
if (!canDrop(node, draggedId)) return
if (onDrop) onDrop(draggedId, node.id)
}
</script>
{#each nodes as node}
<div class="tree-item"
class:selected={selectedNodeId === node.id}
style="padding-left: {level * 16 + 4}px"
draggable="true"
on:dragstart={(e) => handleDragStart(e, node)}
on:dragover={(e) => handleDragOver(e, node)}
on:drop={(e) => handleDrop(e, node)}
on:contextmenu|preventDefault={(e) => onContextMenu && onContextMenu(e, node)}>
{#if isContainer(node)}
<button class="tree-toggle" on:click|stopPropagation={() => onToggle && onToggle(node.id)}
on:contextmenu|preventDefault={(e) => e.stopPropagation()}>
{#if expanded[node.id]}
<span class="tree-arrow"></span>
{:else}
<span class="tree-arrow"></span>
{/if}
</button>
{:else}
<span class="tree-toggle-placeholder"></span>
{/if}
<span class="tree-icon"><TemplateIcon kind={iconKind(node)} size={16} /></span>
<span class="tree-label" on:click|stopPropagation={() => onSelect && onSelect(node)}
on:contextmenu|preventDefault={(e) => e.stopPropagation()}>
{node.title}
</span>
</div>
{#if expanded[node.id] && hasLoadedChildren(node)}
<svelte:self nodes={node.children || []} {expanded} {selectedNodeId} level={level + 1}
{onSelect} {onToggle} {onContextMenu} {onDrop} />
{/if}
{/each}
<style>
.tree-item {
display: flex;
align-items: center;
height: 32px;
padding-right: 8px;
cursor: default;
font-size: 13px;
color: #ccc;
user-select: none;
}
.tree-item:hover {
background: #222233;
}
.tree-item.selected {
background: #2a2a4a;
color: #fff;
font-weight: 500;
}
.tree-toggle {
background: none;
border: none;
color: #666;
cursor: pointer;
padding: 0;
width: 20px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-family: inherit;
line-height: 1;
font-size: 12px;
}
.tree-toggle:hover {
color: #888;
}
.tree-toggle-placeholder {
display: inline-block;
width: 20px;
flex-shrink: 0;
}
.tree-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 32px;
flex-shrink: 0;
color: #888;
margin-right: 4px;
}
.tree-item.selected .tree-icon {
color: #a5b4fc;
}
.tree-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
line-height: 32px;
}
</style>