obsidian-visualiser/app/components/base/DraggableTreeItem.vue

140 lines
3.6 KiB
Vue

<template>
<TreeItem ref="el" v-bind="forward" v-slot="{ isExpanded, isSelected, isIndeterminate, handleToggle, handleSelect }">
<slot
:is-expanded="isExpanded"
:is-selected="isSelected"
:is-indeterminate="isIndeterminate"
:handle-select="handleSelect"
:handle-toggle="handleToggle"
:isDragging="isDragging"
:isDraggedOver="isDraggedOver"
/>
<div v-if="instruction">
<slot name="hint" :instruction="instruction" />
</div>
</TreeItem>
</template>
<script setup lang="ts" generic="T extends Record<string, any>">
import { useForwardPropsEmits, type FlattenedItem, type TreeItemEmits, type TreeItemProps } from 'radix-vue';
import { draggable, dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
import { type Instruction, attachInstruction, extractInstruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item'
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'
const props = defineProps<TreeItemProps<T> & FlattenedItem<T>>();
const emits = defineEmits<TreeItemEmits<T>>();
defineSlots<{
default: (props: {
isExpanded: boolean
isSelected: boolean
isIndeterminate: boolean | undefined
isDragging: boolean
isDraggedOver: boolean
handleToggle: () => void
handleSelect: () => void
}) => any,
hint: (props: {
instruction: Extract<Instruction, { type: 'reorder-above' | 'reorder-below' | 'make-child' }> | null
}) => any,
}>()
const forward = useForwardPropsEmits(props, emits);
const element = templateRef('el');
const isDragging = ref(false);
const isDraggedOver = ref(false);
const isInitialExpanded = ref(false);
const instruction = ref<Extract<Instruction, { type: 'reorder-above' | 'reorder-below' | 'make-child' }> | null>(null);
const mode = computed(() => {
if (props.hasChildren)
return 'expanded'
if (props.index + 1 === props.parentItem?.children?.length)
return 'last-in-group'
return 'standard'
});
watchEffect((onCleanup) => {
const currentElement = unrefElement(element) as HTMLElement;
if (!currentElement)
return
const item = { ...props.value, level: props.level, id: props._id }
const expandItem = () => {
if (!element.value?.isExpanded) {
element.value?.handleToggle()
}
}
const closeItem = () => {
if (element.value?.isExpanded) {
element.value?.handleToggle()
}
}
const dndFunction = combine(
draggable({
element: currentElement,
getInitialData: () => item,
onDragStart: () => {
isDragging.value = true
isInitialExpanded.value = element.value?.isExpanded ?? false
closeItem()
},
onDrop: () => {
isDragging.value = false
if (isInitialExpanded.value)
expandItem()
},
}),
dropTargetForElements({
element: currentElement,
getData: ({ input, element }) => {
const data = { id: item.id }
return attachInstruction(data, {
input,
element,
indentPerLevel: 16,
currentLevel: props.level,
mode: mode.value,
block: [],
})
},
canDrop: ({ source }) => {
return source.data.id !== item.id
},
onDrag: ({ self }) => {
instruction.value = extractInstruction(self.data) as typeof instruction.value
},
onDragEnter: ({ source }) => {
if (source.data.id !== item.id) {
isDraggedOver.value = true
}
},
onDragLeave: () => {
isDraggedOver.value = false
instruction.value = null
},
onDrop: ({ location }) => {
isDraggedOver.value = false
instruction.value = null
},
getIsSticky: () => true,
}),
monitorForElements({
canMonitor: ({ source }) => {
return source.data.id !== item.id
},
}),
)
// Cleanup dnd function
onCleanup(() => dndFunction())
})
</script>