knxdisplay/web-interface/src/components/TreeItem.vue
2026-01-26 09:26:11 +01:00

206 lines
5.0 KiB
Vue

<template>
<div class="tree-node">
<div
class="tree-item"
:class="{
active: store.selectedWidgetId === node.id,
hidden: !node.visible,
'drag-over': isDragOver
}"
@click.stop="store.selectedWidgetId = node.id"
:style="{ paddingLeft: `${level * 16 + 8}px` }"
draggable="true"
@dragstart="onDragStart($event, node)"
@dragover.prevent="onDragOver($event)"
@dragleave="isDragOver = false"
@drop.stop="onDrop($event, node)"
>
<span
class="tree-expander"
@click.stop="toggleExpand"
:style="{ visibility: node.children.length > 0 ? 'visible' : 'hidden' }"
>
{{ expanded ? '▼' : '▶' }}
</span>
<span class="tree-icon-type material-symbols-outlined">{{ getIconForType(node.type) }}</span>
<div class="tree-content">
<span class="tree-name">{{ node.text || TYPE_LABELS[typeKeyFor(node.type)] }}</span>
<span class="tree-type">{{ TYPE_LABELS[typeKeyFor(node.type)] }} #{{ node.id }}</span>
</div>
</div>
<div class="tree-children" v-if="node.children.length > 0 && expanded">
<div class="tree-guide" :style="{ left: `${level * 16 + 15}px` }"></div>
<TreeItem
v-for="child in node.children"
:key="child.id"
:node="child"
:level="level + 1"
/>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useEditorStore } from '../stores/editor';
import { typeKeyFor } from '../utils';
import { TYPE_LABELS, WIDGET_TYPES } from '../constants';
const props = defineProps({
node: Object,
level: { type: Number, default: 0 }
});
const store = useEditorStore();
const expanded = ref(true);
const isDragOver = ref(false);
function toggleExpand() {
expanded.value = !expanded.value;
}
function getIconForType(type) {
switch(type) {
case WIDGET_TYPES.LABEL: return 'text_fields';
case WIDGET_TYPES.BUTTON: return 'smart_button';
case WIDGET_TYPES.LED: return 'light_mode';
case WIDGET_TYPES.ICON: return 'image';
case WIDGET_TYPES.TABVIEW: return 'tab';
case WIDGET_TYPES.TABPAGE: return 'article';
default: return 'widgets';
}
}
function onDragStart(e, node) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', node.id.toString());
store.selectedWidgetId = node.id;
}
function onDragOver(e) {
// Only allow dropping on containers (Button, TabView, TabPage)
// Actually, dropping ON a widget means "put inside".
// Dropping BETWEEN is harder to implement in this simple tree.
// Let's allow dropping on anything that CAN accept children.
const type = props.node.type;
const canAccept = type === WIDGET_TYPES.BUTTON ||
type === WIDGET_TYPES.TABVIEW ||
type === WIDGET_TYPES.TABPAGE;
if (canAccept) {
isDragOver.value = true;
e.dataTransfer.dropEffect = 'move';
} else {
e.dataTransfer.dropEffect = 'none';
}
}
function onDrop(e, targetNode) {
isDragOver.value = false;
const draggedId = parseInt(e.dataTransfer.getData('text/plain'));
if (draggedId === targetNode.id) return; // Drop on self
// Check compatibility
const type = targetNode.type;
const canAccept = type === WIDGET_TYPES.BUTTON ||
type === WIDGET_TYPES.TABVIEW ||
type === WIDGET_TYPES.TABPAGE;
if (canAccept) {
store.reparentWidget(draggedId, targetNode.id);
}
}
</script>
<style scoped>
.tree-node {
position: relative;
}
.tree-item {
display: flex;
align-items: center;
gap: 6px;
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
padding: 4px 6px;
color: var(--text);
cursor: pointer;
margin-bottom: 1px;
user-select: none;
}
.tree-item:hover {
background: var(--panel-2);
}
.tree-item.active {
background: rgba(125, 211, 176, 0.15);
border-color: rgba(125, 211, 176, 0.3);
}
.tree-item.drag-over {
background: rgba(246, 193, 119, 0.2);
border: 1px dashed var(--accent);
}
.tree-item.hidden {
opacity: 0.5;
}
.tree-expander {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 8px;
color: var(--muted);
cursor: pointer;
}
.tree-expander:hover {
color: var(--text);
}
.tree-icon-type {
font-size: 16px;
color: var(--accent);
opacity: 0.8;
}
.tree-content {
display: flex;
flex-direction: column;
overflow: hidden;
}
.tree-name {
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tree-type {
font-size: 9px;
color: var(--muted);
}
.tree-children {
position: relative;
}
.tree-guide {
position: absolute;
top: 0;
bottom: 0;
width: 1px;
background: rgba(255, 255, 255, 0.05);
}
</style>