Drag and drop files between folders
This commit is contained in:
+16
-2
@@ -18,8 +18,8 @@
|
||||
<button :class="{active: view === 'prefs'}" @click="view = 'prefs'">⚙️ Preferences</button>
|
||||
<button v-if="isAdmin" :class="{active: view === 'admin'}" @click="view = 'admin'">👤 Admin</button>
|
||||
</div>
|
||||
<FileTree v-if="view === 'files'" :files="filteredFiles" :selected="currentFile" @select="openFile" @delete="deleteItem" />
|
||||
<FileTree v-if="view === 'shared'" :files="sharedFiles" :selected="currentFile" @select="openFile" @delete="deleteItem" />
|
||||
<FileTree v-if="view === 'files'" :files="filteredFiles" :selected="currentFile" @select="openFile" @delete="deleteItem" @move="moveFile" />
|
||||
<FileTree v-if="view === 'shared'" :files="sharedFiles" :selected="currentFile" @select="openFile" @delete="deleteItem" @move="moveFile" />
|
||||
</aside>
|
||||
<main class="editor-area" v-if="view === 'files' || view === 'shared'">
|
||||
<div class="toolbar">
|
||||
@@ -361,6 +361,20 @@ async function deleteItem(item) {
|
||||
await loadFiles()
|
||||
}
|
||||
|
||||
async function moveFile({ from, to }) {
|
||||
const filename = from.split('/').pop()
|
||||
const newPath = to + '/' + filename
|
||||
try {
|
||||
await api('/api/files/move', { from, to: newPath })
|
||||
if (currentFile.value === from) {
|
||||
currentFile.value = newPath
|
||||
}
|
||||
await loadFiles()
|
||||
} catch (e) {
|
||||
alert('Move failed')
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Shared ──────────────────────────────────────────────────────────────────
|
||||
|
||||
async function loadShared() {
|
||||
|
||||
@@ -3,36 +3,67 @@
|
||||
<div v-for="item in files" :key="item.path" class="tree-item">
|
||||
<div
|
||||
class="tree-node"
|
||||
:class="{ selected: item.path === selected, folder: item.isDir }"
|
||||
:class="{ selected: item.path === selected, folder: item.isDir, 'drop-target': dropTarget === item.path }"
|
||||
@click="item.isDir ? toggleFolder(item) : $emit('select', item.path)"
|
||||
@contextmenu.prevent="showContext($event, item)"
|
||||
@contextmenu.prevent="null"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart($event, item)"
|
||||
@dragover.prevent="onDragOver($event, item)"
|
||||
@dragleave="onDragLeave"
|
||||
@drop="onDrop($event, item)"
|
||||
>
|
||||
<span class="icon">{{ item.isDir ? (expanded[item.path] ? '📂' : '📁') : '📄' }}</span>
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<button class="delete-btn" @click.stop="$emit('delete', item)" title="Delete">×</button>
|
||||
</div>
|
||||
<div v-if="item.isDir && expanded[item.path] && item.children" class="children">
|
||||
<FileTree :files="item.children" :selected="selected" @select="$emit('select', $event)" @delete="$emit('delete', $event)" />
|
||||
<FileTree :files="item.children" :selected="selected" @select="$emit('select', $event)" @delete="$emit('delete', $event)" @move="$emit('move', $event)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive } from 'vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
files: { type: Array, default: () => [] },
|
||||
selected: { type: String, default: '' },
|
||||
})
|
||||
|
||||
defineEmits(['select', 'delete'])
|
||||
const emit = defineEmits(['select', 'delete', 'move'])
|
||||
|
||||
const expanded = reactive({})
|
||||
const dropTarget = ref('')
|
||||
|
||||
function toggleFolder(item) {
|
||||
expanded[item.path] = !expanded[item.path]
|
||||
}
|
||||
|
||||
function onDragStart(e, item) {
|
||||
e.dataTransfer.setData('text/plain', item.path)
|
||||
e.dataTransfer.effectAllowed = 'move'
|
||||
}
|
||||
|
||||
function onDragOver(e, item) {
|
||||
if (item.isDir) {
|
||||
e.dataTransfer.dropEffect = 'move'
|
||||
dropTarget.value = item.path
|
||||
}
|
||||
}
|
||||
|
||||
function onDragLeave() {
|
||||
dropTarget.value = ''
|
||||
}
|
||||
|
||||
function onDrop(e, targetItem) {
|
||||
dropTarget.value = ''
|
||||
const sourcePath = e.dataTransfer.getData('text/plain')
|
||||
if (!sourcePath || !targetItem.isDir || sourcePath === targetItem.path) return
|
||||
// Don't drop into itself
|
||||
if (targetItem.path.startsWith(sourcePath + '/')) return
|
||||
emit('move', { from: sourcePath, to: targetItem.path })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -50,6 +81,7 @@ function toggleFolder(item) {
|
||||
}
|
||||
.tree-node:hover { background: var(--hover-bg, #313244); }
|
||||
.tree-node.selected { background: var(--selected-bg, #45475a); }
|
||||
.tree-node.drop-target { background: var(--accent, #89b4fa); opacity: 0.7; }
|
||||
.icon { font-size: 14px; }
|
||||
.name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.delete-btn {
|
||||
|
||||
Reference in New Issue
Block a user