@cloudron/pankow 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/DirectoryView.vue +16 -1
- package/components/TreeView.vue +68 -0
- package/components/TreeViewNode.vue +222 -0
- package/index.js +2 -0
- package/package.json +1 -1
- package/types/index.d.ts +1 -1
|
@@ -251,6 +251,10 @@ export default {
|
|
|
251
251
|
extractHandler: {
|
|
252
252
|
type: Function,
|
|
253
253
|
default() { console.warn('Missing extractHandler for DirectoryView'); }
|
|
254
|
+
},
|
|
255
|
+
refreshHandler: {
|
|
256
|
+
type: Function,
|
|
257
|
+
default() { console.warn('Missing refreshHandler for DirectoryView'); }
|
|
254
258
|
}
|
|
255
259
|
},
|
|
256
260
|
computed: {
|
|
@@ -325,6 +329,12 @@ export default {
|
|
|
325
329
|
}, {
|
|
326
330
|
separator:true,
|
|
327
331
|
visible: () => { return this.showNewFile || this.showNewFolder; },
|
|
332
|
+
}, {
|
|
333
|
+
label: this.tr('filemanager.toolbar.refresh'),
|
|
334
|
+
icon:'fa-solid fa-arrow-rotate-right',
|
|
335
|
+
action: this.refreshHandler,
|
|
336
|
+
}, {
|
|
337
|
+
separator:true,
|
|
328
338
|
}, {
|
|
329
339
|
label: this.tr('filemanager.toolbar.newFile'),
|
|
330
340
|
icon:'fa-solid fa-file-circle-plus',
|
|
@@ -474,7 +484,12 @@ export default {
|
|
|
474
484
|
onDragExit(event) {
|
|
475
485
|
this.dropTargetActive = false;
|
|
476
486
|
},
|
|
477
|
-
onItemDragStart(event) {
|
|
487
|
+
onItemDragStart(event, item) {
|
|
488
|
+
// auto-select the dragged item if it is not already part of the selection
|
|
489
|
+
if (item && !item.selected) {
|
|
490
|
+
this.onSelectAndFocus(item, {}, false);
|
|
491
|
+
}
|
|
492
|
+
|
|
478
493
|
const dragHandle = document.getElementsByClassName('drag-handle')[0];
|
|
479
494
|
|
|
480
495
|
// clear element
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="tree-view">
|
|
3
|
+
<TreeViewNode
|
|
4
|
+
:entry="rootEntry"
|
|
5
|
+
:list-files="listFiles"
|
|
6
|
+
:active-path="activePath"
|
|
7
|
+
:depth="0"
|
|
8
|
+
:fallback-icon="fallbackIcon"
|
|
9
|
+
:expand-on-mount="expandRoot"
|
|
10
|
+
:drop-handler="dropHandler"
|
|
11
|
+
@navigate="$emit('navigate', $event)"
|
|
12
|
+
/>
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup>
|
|
17
|
+
|
|
18
|
+
import { computed } from 'vue';
|
|
19
|
+
import TreeViewNode from './TreeViewNode.vue';
|
|
20
|
+
|
|
21
|
+
const props = defineProps({
|
|
22
|
+
listFiles: {
|
|
23
|
+
type: Function,
|
|
24
|
+
required: true,
|
|
25
|
+
},
|
|
26
|
+
activePath: {
|
|
27
|
+
type: String,
|
|
28
|
+
default: '/',
|
|
29
|
+
},
|
|
30
|
+
fallbackIcon: {
|
|
31
|
+
type: String,
|
|
32
|
+
default: '',
|
|
33
|
+
},
|
|
34
|
+
rootLabel: {
|
|
35
|
+
type: String,
|
|
36
|
+
default: '/',
|
|
37
|
+
},
|
|
38
|
+
expandRoot: {
|
|
39
|
+
type: Boolean,
|
|
40
|
+
default: true,
|
|
41
|
+
},
|
|
42
|
+
dropHandler: {
|
|
43
|
+
type: Function,
|
|
44
|
+
default: null,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
defineEmits(['navigate']);
|
|
49
|
+
|
|
50
|
+
const rootEntry = computed(() => ({
|
|
51
|
+
name: props.rootLabel,
|
|
52
|
+
path: '/',
|
|
53
|
+
isDirectory: true,
|
|
54
|
+
icon: props.fallbackIcon,
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<style scoped>
|
|
60
|
+
|
|
61
|
+
.tree-view {
|
|
62
|
+
display: flex;
|
|
63
|
+
overflow: auto;
|
|
64
|
+
height: 100%;
|
|
65
|
+
padding: 4px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
</style>
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="tree-view-node">
|
|
3
|
+
<div
|
|
4
|
+
class="tree-view-node-row"
|
|
5
|
+
:class="{ 'active': isActive, 'drop-target': dropTargetActive }"
|
|
6
|
+
:style="{ paddingLeft: (props.depth * 16 + 4) + 'px' }"
|
|
7
|
+
@click="onRowClick"
|
|
8
|
+
@dragover.stop.prevent="onDragOver"
|
|
9
|
+
@dragenter.stop.prevent="onDragEnter"
|
|
10
|
+
@dragleave="onDragLeave"
|
|
11
|
+
@drop.stop.prevent="onDrop"
|
|
12
|
+
>
|
|
13
|
+
<span class="tree-view-node-chevron">
|
|
14
|
+
<i v-if="loading" class="fa-solid fa-spinner fa-spin" />
|
|
15
|
+
<i v-else-if="expanded" class="fa-solid fa-chevron-down" />
|
|
16
|
+
<i v-else class="fa-solid fa-chevron-right" />
|
|
17
|
+
</span>
|
|
18
|
+
<i class="fa-solid tree-view-node-folder-icon" :class="expanded ? 'fa-folder-open' : 'fa-folder'" />
|
|
19
|
+
<span :title="props.entry.name">{{ props.entry.name }}</span>
|
|
20
|
+
</div>
|
|
21
|
+
<div v-if="expanded && children.length > 0" class="tree-view-node-children">
|
|
22
|
+
<TreeViewNode
|
|
23
|
+
v-for="child in children"
|
|
24
|
+
:key="child.name"
|
|
25
|
+
:entry="child"
|
|
26
|
+
:list-files="props.listFiles"
|
|
27
|
+
:active-path="props.activePath"
|
|
28
|
+
:depth="props.depth + 1"
|
|
29
|
+
:fallback-icon="props.fallbackIcon"
|
|
30
|
+
:drop-handler="props.dropHandler"
|
|
31
|
+
@navigate="$emit('navigate', $event)"
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script setup>
|
|
38
|
+
|
|
39
|
+
import { ref, computed, watch, onMounted } from 'vue';
|
|
40
|
+
|
|
41
|
+
const props = defineProps({
|
|
42
|
+
entry: {
|
|
43
|
+
type: Object,
|
|
44
|
+
required: true,
|
|
45
|
+
},
|
|
46
|
+
listFiles: {
|
|
47
|
+
type: Function,
|
|
48
|
+
required: true,
|
|
49
|
+
},
|
|
50
|
+
activePath: {
|
|
51
|
+
type: String,
|
|
52
|
+
default: '/',
|
|
53
|
+
},
|
|
54
|
+
depth: {
|
|
55
|
+
type: Number,
|
|
56
|
+
default: 0,
|
|
57
|
+
},
|
|
58
|
+
fallbackIcon: {
|
|
59
|
+
type: String,
|
|
60
|
+
default: '',
|
|
61
|
+
},
|
|
62
|
+
expandOnMount: {
|
|
63
|
+
type: Boolean,
|
|
64
|
+
default: false,
|
|
65
|
+
},
|
|
66
|
+
dropHandler: {
|
|
67
|
+
type: Function,
|
|
68
|
+
default: null,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const emit = defineEmits(['navigate']);
|
|
73
|
+
|
|
74
|
+
const expanded = ref(false);
|
|
75
|
+
const children = ref([]);
|
|
76
|
+
const loading = ref(false);
|
|
77
|
+
const loaded = ref(false);
|
|
78
|
+
const dropTargetActive = ref(false);
|
|
79
|
+
let dragExpandTimer = null;
|
|
80
|
+
|
|
81
|
+
const isActive = computed(() => props.activePath === props.entry.path);
|
|
82
|
+
|
|
83
|
+
async function expand() {
|
|
84
|
+
if (loading.value) return;
|
|
85
|
+
|
|
86
|
+
loading.value = true;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const entries = await props.listFiles(props.entry.path);
|
|
90
|
+
children.value = entries
|
|
91
|
+
.filter(item => item.isDirectory)
|
|
92
|
+
.map(item => ({
|
|
93
|
+
name: item.name || item.fileName,
|
|
94
|
+
path: props.entry.path === '/'
|
|
95
|
+
? '/' + (item.name || item.fileName)
|
|
96
|
+
: props.entry.path + '/' + (item.name || item.fileName),
|
|
97
|
+
isDirectory: true,
|
|
98
|
+
icon: item.icon || props.fallbackIcon,
|
|
99
|
+
}));
|
|
100
|
+
loaded.value = true;
|
|
101
|
+
expanded.value = true;
|
|
102
|
+
} catch (e) {
|
|
103
|
+
console.error('TreeViewNode: failed to load children', e);
|
|
104
|
+
} finally {
|
|
105
|
+
loading.value = false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function onToggle() {
|
|
110
|
+
if (expanded.value) {
|
|
111
|
+
expanded.value = false;
|
|
112
|
+
} else if (loaded.value) {
|
|
113
|
+
expanded.value = true;
|
|
114
|
+
} else {
|
|
115
|
+
await expand();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function onRowClick() {
|
|
120
|
+
onToggle();
|
|
121
|
+
emit('navigate', { path: props.entry.path });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function onDragOver(event) {
|
|
125
|
+
if (!props.dropHandler) return;
|
|
126
|
+
event.dataTransfer.dropEffect = 'move';
|
|
127
|
+
dropTargetActive.value = true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function onDragEnter(event) {
|
|
131
|
+
if (!props.dropHandler) return;
|
|
132
|
+
event.dataTransfer.dropEffect = 'move';
|
|
133
|
+
if (!expanded.value) dragExpandTimer = setTimeout(expand, 500);
|
|
134
|
+
dropTargetActive.value = true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function onDragLeave() {
|
|
138
|
+
clearTimeout(dragExpandTimer);
|
|
139
|
+
dropTargetActive.value = false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function onDrop(event) {
|
|
143
|
+
dropTargetActive.value = false;
|
|
144
|
+
if (!props.dropHandler) return;
|
|
145
|
+
clearTimeout(dragExpandTimer);
|
|
146
|
+
props.dropHandler(props.entry.path, event);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
onMounted(() => {
|
|
150
|
+
if (props.expandOnMount) expand();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Auto-expand if the active path is a descendant of this node
|
|
154
|
+
watch(() => props.activePath, (newPath) => {
|
|
155
|
+
const prefix = props.entry.path === '/' ? '/' : props.entry.path + '/';
|
|
156
|
+
if (newPath && newPath !== props.entry.path && newPath.startsWith(prefix)) {
|
|
157
|
+
if (!expanded.value) {
|
|
158
|
+
expand();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}, { immediate: true });
|
|
162
|
+
|
|
163
|
+
</script>
|
|
164
|
+
|
|
165
|
+
<style scoped>
|
|
166
|
+
|
|
167
|
+
.tree-view-node {
|
|
168
|
+
display: flex;
|
|
169
|
+
flex-direction: column;
|
|
170
|
+
flex: 1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.tree-view-node-row {
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
white-space: nowrap;
|
|
178
|
+
border-radius: var(--pankow-border-radius);
|
|
179
|
+
padding: 6px 8px 6px 0;
|
|
180
|
+
margin: 2px 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.tree-view-node-row:hover {
|
|
184
|
+
background-color: var(--pankow-color-background-hover);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.tree-view-node-row.active {
|
|
188
|
+
background-color: #dbedfb;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@media (prefers-color-scheme: dark) {
|
|
192
|
+
.tree-view-node-row.active {
|
|
193
|
+
background-color: rgba(255, 255, 255, 0.2);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.tree-view-node-row.drop-target {
|
|
198
|
+
background-color: #dbedfb;
|
|
199
|
+
outline: 2px solid var(--pankow-color-primary, #3b82f6);
|
|
200
|
+
outline-offset: -2px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@media (prefers-color-scheme: dark) {
|
|
204
|
+
.tree-view-node-row.drop-target {
|
|
205
|
+
background-color: rgba(255, 255, 255, 0.2);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.tree-view-node-chevron {
|
|
210
|
+
display: inline-flex;
|
|
211
|
+
align-items: center;
|
|
212
|
+
justify-content: center;
|
|
213
|
+
margin-right: 4px;
|
|
214
|
+
font-size: 10px;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.tree-view-node-folder-icon {
|
|
218
|
+
margin-right: 6px;
|
|
219
|
+
color: var(--pankow-color-secondary);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
</style>
|
package/index.js
CHANGED
|
@@ -43,6 +43,7 @@ import TagInput from './components/TagInput.vue';
|
|
|
43
43
|
import TextInput from './components/TextInput.vue';
|
|
44
44
|
import TextInputRaw from './components/TextInputRaw.vue';
|
|
45
45
|
import TopBar from './components/TopBar.vue';
|
|
46
|
+
import TreeView from './components/TreeView.vue';
|
|
46
47
|
import InputGroup from './components/InputGroup.vue'; // must be at the end for border-radius handling
|
|
47
48
|
|
|
48
49
|
import fetcher from './fetcher.js';
|
|
@@ -90,6 +91,7 @@ export {
|
|
|
90
91
|
TextInput,
|
|
91
92
|
TextInputRaw,
|
|
92
93
|
TopBar,
|
|
94
|
+
TreeView,
|
|
93
95
|
|
|
94
96
|
fetcher,
|
|
95
97
|
gestures,
|
package/package.json
CHANGED
package/types/index.d.ts
CHANGED
|
@@ -3,4 +3,4 @@ import gestures from './gestures.js';
|
|
|
3
3
|
import tooltip from './tooltip.js';
|
|
4
4
|
import fallbackImage from './fallbackImage.js';
|
|
5
5
|
import utils from './utils.js';
|
|
6
|
-
export { BottomBar, Breadcrumb, Button, ButtonGroup, ClipboardAction, ClipboardButton, Checkbox, DateTimeInput, Dialog, DirectoryView, EmailInput, FileUploader, FormGroup, InputGroup, Icon, InputDialog, MainLayout, MaskedInput, Menu, MenuItem, SingleSelect, MultiSelect, Notification, NumberInput, OfflineBanner, PasswordInput, Popover, ProgressBar, Radiobutton, SideBar, Spinner, Switch, TableView, TabView, TagInput, TextInput, TextInputRaw, TopBar, fetcher, gestures, tooltip, fallbackImage, utils };
|
|
6
|
+
export { BottomBar, Breadcrumb, Button, ButtonGroup, ClipboardAction, ClipboardButton, Checkbox, DateTimeInput, Dialog, DirectoryView, EmailInput, FileUploader, FormGroup, InputGroup, Icon, InputDialog, MainLayout, MaskedInput, Menu, MenuItem, SingleSelect, MultiSelect, Notification, NumberInput, OfflineBanner, PasswordInput, Popover, ProgressBar, Radiobutton, SideBar, Spinner, Switch, TableView, TabView, TagInput, TextInput, TextInputRaw, TopBar, TreeView, fetcher, gestures, tooltip, fallbackImage, utils };
|