@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.
@@ -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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cloudron/pankow",
3
3
  "private": false,
4
- "version": "4.0.0",
4
+ "version": "4.1.0",
5
5
  "description": "",
6
6
  "main": "index.js",
7
7
  "types": "types/index.d.ts",
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 };