mirror of https://github.com/requarks/wiki
parent
4e34151d15
commit
0cbeec37d6
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,83 @@
|
||||
<template lang="pug">
|
||||
ul.treeview-level
|
||||
//- ROOT NODE
|
||||
li.treeview-node(v-if='!props.parentId')
|
||||
.treeview-label(@click='setRoot', :class='{ "active": !selection }')
|
||||
q-icon(name='img:/_assets/icons/fluent-ftp.svg', size='sm')
|
||||
em.text-purple root
|
||||
q-menu(
|
||||
touch-position
|
||||
context-menu
|
||||
auto-close
|
||||
transition-show='jump-down'
|
||||
transition-hide='jump-up'
|
||||
)
|
||||
q-card.q-pa-sm
|
||||
q-list(dense, style='min-width: 150px;')
|
||||
q-item(clickable)
|
||||
q-item-section(side)
|
||||
q-icon(name='las la-plus-circle', color='primary')
|
||||
q-item-section New Folder
|
||||
//- NORMAL NODES
|
||||
tree-node(
|
||||
v-for='node of level'
|
||||
:key='node.id'
|
||||
:node='node'
|
||||
:depth='props.depth'
|
||||
:parent-id='props.parentId'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, inject } from 'vue'
|
||||
|
||||
import TreeNode from './TreeNode.vue'
|
||||
|
||||
// PROPS
|
||||
|
||||
const props = defineProps({
|
||||
depth: {
|
||||
required: true,
|
||||
type: Number
|
||||
},
|
||||
parentId: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
// INJECT
|
||||
|
||||
const roots = inject('roots', [])
|
||||
const nodes = inject('nodes')
|
||||
const selection = inject('selection')
|
||||
|
||||
// COMPUTED
|
||||
|
||||
const level = computed(() => {
|
||||
const items = []
|
||||
if (!props.parentId) {
|
||||
for (const root of roots) {
|
||||
items.push({
|
||||
id: root,
|
||||
...nodes[root]
|
||||
})
|
||||
}
|
||||
} else {
|
||||
for (const node of nodes[props.parentId].children) {
|
||||
items.push({
|
||||
id: node,
|
||||
...nodes[node]
|
||||
})
|
||||
}
|
||||
}
|
||||
return items
|
||||
})
|
||||
|
||||
// METHODS
|
||||
|
||||
function setRoot () {
|
||||
selection.value = null
|
||||
}
|
||||
|
||||
</script>
|
@ -1,21 +1,135 @@
|
||||
<template lang="pug">
|
||||
.treenav
|
||||
|
||||
.treeview
|
||||
tree-level(
|
||||
:depth='0'
|
||||
:parent-id='null'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, provide, reactive } from 'vue'
|
||||
import { findKey } from 'lodash-es'
|
||||
|
||||
import TreeLevel from './TreeLevel.vue'
|
||||
|
||||
// PROPS
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
nodes: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
roots: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selected: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
// EMITS
|
||||
|
||||
const emits = defineEmits(['selected'])
|
||||
const emit = defineEmits(['update:selected'])
|
||||
|
||||
// DATA
|
||||
|
||||
const state = reactive({
|
||||
opened: {}
|
||||
})
|
||||
|
||||
// COMPOUTED
|
||||
|
||||
const selection = computed({
|
||||
get () {
|
||||
return props.selected
|
||||
},
|
||||
set (val) {
|
||||
emit('update:selected', val)
|
||||
}
|
||||
})
|
||||
|
||||
// METHODS
|
||||
|
||||
// PROVIDE
|
||||
|
||||
provide('roots', props.roots)
|
||||
provide('nodes', props.nodes)
|
||||
provide('opened', state.opened)
|
||||
provide('selection', selection)
|
||||
|
||||
// MOUNTED
|
||||
|
||||
onMounted(() => {
|
||||
if (props.selected) {
|
||||
let foundRoot = false
|
||||
let currentId = props.selected
|
||||
while (!foundRoot) {
|
||||
const parentId = findKey(props.nodes, n => n.children?.includes(currentId))
|
||||
if (parentId) {
|
||||
state.opened[parentId] = true
|
||||
currentId = parentId
|
||||
} else {
|
||||
foundRoot = true
|
||||
}
|
||||
}
|
||||
state.opened[props.selected] = true
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.treeview {
|
||||
&-level {
|
||||
list-style: none;
|
||||
padding-left: 19px;
|
||||
}
|
||||
|
||||
> .treeview-level {
|
||||
padding-left: 0;
|
||||
|
||||
> .treeview-node {
|
||||
border-left: none;
|
||||
|
||||
> .treeview-label {
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-node {
|
||||
display: block;
|
||||
border-left: 2px solid rgba(0,0,0,.05);
|
||||
}
|
||||
|
||||
&-label {
|
||||
padding: 4px 8px;
|
||||
border-radius: 0 5px 5px 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: background-color .4s ease;
|
||||
|
||||
&:hover, &:focus, &.active {
|
||||
background-color: rgba(0,0,0,.05);
|
||||
}
|
||||
|
||||
> .q-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
// Animations
|
||||
|
||||
&-enter-active, &-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
&-enter-from, &-leave-to {
|
||||
transform: translateY(-10px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -0,0 +1,107 @@
|
||||
<template lang="pug">
|
||||
li.treeview-node
|
||||
//- NODE
|
||||
.treeview-label(@click='toggleNode', :class='{ "active": isActive }')
|
||||
q-icon(:name='icon', size='sm')
|
||||
span {{node.text}}
|
||||
//- RIGHT-CLICK MENU
|
||||
q-menu(
|
||||
touch-position
|
||||
context-menu
|
||||
auto-close
|
||||
transition-show='jump-down'
|
||||
transition-hide='jump-up'
|
||||
@before-show='state.isContextMenuShown = true'
|
||||
@before-hide='state.isContextMenuShown = false'
|
||||
)
|
||||
q-card.q-pa-sm
|
||||
q-list(dense, style='min-width: 150px;')
|
||||
q-item(clickable)
|
||||
q-item-section(side)
|
||||
q-icon(name='las la-plus-circle', color='primary')
|
||||
q-item-section New Folder
|
||||
q-item(clickable)
|
||||
q-item-section(side)
|
||||
q-icon(name='las la-redo', color='teal')
|
||||
q-item-section Rename...
|
||||
q-item(clickable)
|
||||
q-item-section(side)
|
||||
q-icon(name='las la-arrow-right', color='teal')
|
||||
q-item-section Move to...
|
||||
q-item(clickable)
|
||||
q-item-section(side)
|
||||
q-icon(name='las la-trash-alt', color='negative')
|
||||
q-item-section.text-negative Delete
|
||||
//- SUB-LEVEL
|
||||
transition(name='treeview')
|
||||
tree-level(
|
||||
v-if='hasChildren && isOpened'
|
||||
:parent-id='props.node.id'
|
||||
:depth='props.depth + 1'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, inject, reactive } from 'vue'
|
||||
|
||||
import TreeLevel from './TreeLevel.vue'
|
||||
|
||||
// PROPS
|
||||
|
||||
const props = defineProps({
|
||||
depth: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
node: {
|
||||
required: true,
|
||||
type: Object
|
||||
},
|
||||
parentId: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
// INJECT
|
||||
|
||||
const opened = inject('opened')
|
||||
const selection = inject('selection')
|
||||
|
||||
// DATA
|
||||
|
||||
const state = reactive({
|
||||
isContextMenuShown: false
|
||||
})
|
||||
|
||||
// COMPUTED
|
||||
|
||||
const icon = computed(() => {
|
||||
if (props.node.icon) {
|
||||
return props.node.icon
|
||||
}
|
||||
return hasChildren.value && isOpened.value ? 'img:/_assets/icons/fluent-opened-folder.svg' : 'img:/_assets/icons/fluent-folder.svg'
|
||||
})
|
||||
|
||||
const hasChildren = computed(() => {
|
||||
return props.node.children?.length > 0
|
||||
})
|
||||
const isOpened = computed(() => {
|
||||
return opened[props.node.id]
|
||||
})
|
||||
const isActive = computed(() => {
|
||||
return state.isContextMenuShown || selection.value === props.node.id
|
||||
})
|
||||
|
||||
// METHODS
|
||||
|
||||
function toggleNode () {
|
||||
selection.value = props.node.id
|
||||
|
||||
if (selection.value !== props.node.id && opened[props.node.id]) {
|
||||
return
|
||||
}
|
||||
opened[props.node.id] = !(opened[props.node.id] === true)
|
||||
}
|
||||
|
||||
</script>
|
Loading…
Reference in new issue