206 lines
5.0 KiB
Vue
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> |