<!-- TreeGroupEditor.vue -->
<template>
<ul class="tree-group-list">
<li v-for="node in nodes" :key="node.id">
<div class="tree-node" :class="{ 'selected': node.id === selectedNodeId }" >
<div class="node-content" @click="selectNode(node)">
<!-- Icon combination for expander and node type -->
<span class="node-icon-wrapper">
<!-- Expander Chevron (rotates) -->
<i v-if="hasChildren(node)"
class="las la-angle-right expand-icon"
:class="{ 'rotated': node.isOpen }"
></i>
<!-- Placeholder for alignment -->
<span v-else class="expand-placeholder"></span>
<!-- Folder/Item Icon -->
<i class="node-icon" :class="getNodeIcon(node)"></i>
</span>
<!-- Name -->
<span class="node-name ellipsis">{{ node.name }}</span>
</div>
<div class="actions d-flex align-items-center">
<button class="btn btn-sm btn-link text-success p-0 me-1" title="Add Child" @click.stop="$emit('add-child-node', node)">
<i class="las la-plus-circle"></i>
</button>
<b-dropdown size="sm" variant="link" toggle-class="text-decoration-none" no-caret right @click.stop>
<template #button-content>
<i class="las la-ellipsis-v text-secondary"></i>
</template>
<b-dropdown-item @click="$emit('edit-node', node)">
<i class="las la-edit me-2"></i>Edit
</b-dropdown-item>
<b-dropdown-divider></b-dropdown-divider>
<b-dropdown-item variant="danger" @click="$emit('delete-node', node)">
<i class="las la-trash me-2"></i>Delete
</b-dropdown-item>
</b-dropdown>
</div>
</div>
<!-- Recursive Rendering -->
<TreeGroupEditor
v-if="hasChildren(node) && node.isOpen"
:nodes="node.children"
:selected-node-id="selectedNodeId"
@node-selected="bubbleSelect"
@edit-node="bubbleEdit"
@add-child-node="bubbleAddChild"
@delete-node="bubbleDelete"
/>
</li>
</ul>
</template>
<script>
export default {
name: 'TreeGroupEditor',
props: {
nodes: { type: Array, required: true },
selectedNodeId: { type: [String, Number], default: null },
},
methods: {
hasChildren(node) {
return node.children && node.children.length > 0;
},
selectNode(node) {
this.$emit('node-selected', node);
if (this.hasChildren(node)) {
this.toggle(node);
}
},
toggle(node) {
this.$set(node, 'isOpen', !node.isOpen);
},
getNodeIcon(node) {
if (this.hasChildren(node)) {
return node.isOpen ? 'las la-folder-open' : 'las la-folder';
}
// Use _type from backend for more specific icons if available
return node._type === 'category' ? 'lar la-bookmark' : 'lar la-circle';
},
// --- Event Bubbling Methods ---
bubbleSelect(node) { this.$emit('node-selected', node); },
bubbleEdit(node) { this.$emit('edit-node', node); },
bubbleAddChild(node) { this.$emit('add-child-node', node); },
bubbleDelete(node) { this.$emit('delete-node', node); },
}
};
</script>
<style scoped>
.tree-group-list { list-style-type: none; padding-left: 1rem; }
.tree-node { padding: 0.4rem 0.5rem; border-radius: 4px; cursor: pointer; display: flex; align-items: center; justify-content: space-between; transition: background-color 0.15s ease-in-out; }
.tree-node:hover { background-color: #f0f3f5; }
.tree-node.selected { background-color: #e0e9f5; font-weight: bold; }
.node-content { display: flex; align-items: center; min-width: 0; flex-grow: 1; }
.node-icon-wrapper { display: flex; align-items: center; margin-right: 5px; }
.expand-icon { transition: transform 0.2s; font-size: 0.8rem; }
.expand-icon.rotated { transform: rotate(90deg); }
.expand-placeholder { width: 0.8rem; /* Match width of expand-icon */ }
.node-icon { font-size: 1.1rem; color: #6c757d; margin-left: 2px; }
.node-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.actions { visibility: hidden; opacity: 0; transition: opacity 0.15s ease-in-out; }
.tree-node:hover .actions { visibility: visible; opacity: 1; }
.actions .btn i { font-size: 1.2rem; }
::v-deep .b-dropdown .btn { padding: 0 0.25rem; }
::v-deep .b-dropdown .dropdown-toggle::after { display: none; }
.dropdown-item { display: flex; align-items: center; font-size: 0.9rem; }
.dropdown-item i { font-size: 1.1rem; }
</style>