@cloudron/pankow 3.1.8

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.
Files changed (62) hide show
  1. package/.gitlab-ci.yml +30 -0
  2. package/.jshintrc +8 -0
  3. package/LICENSE +21 -0
  4. package/README.md +20 -0
  5. package/components/BottomBar.vue +22 -0
  6. package/components/Breadcrumb.vue +64 -0
  7. package/components/Button.vue +243 -0
  8. package/components/ButtonGroup.vue +37 -0
  9. package/components/Checkbox.vue +112 -0
  10. package/components/Dialog.vue +178 -0
  11. package/components/DirectoryView.vue +772 -0
  12. package/components/DirectoryViewListItem.vue +412 -0
  13. package/components/EmailInput.vue +22 -0
  14. package/components/FileUploader.vue +204 -0
  15. package/components/FormGroup.vue +26 -0
  16. package/components/Icon.vue +12 -0
  17. package/components/InputDialog.vue +170 -0
  18. package/components/InputGroup.vue +32 -0
  19. package/components/MainLayout.vue +63 -0
  20. package/components/Menu.vue +284 -0
  21. package/components/MenuItem.vue +106 -0
  22. package/components/MenuItemLink.vue +52 -0
  23. package/components/MultiSelect.vue +202 -0
  24. package/components/Notification.vue +163 -0
  25. package/components/NumberInput.vue +31 -0
  26. package/components/OfflineBanner.vue +47 -0
  27. package/components/PasswordInput.vue +86 -0
  28. package/components/Popover.vue +185 -0
  29. package/components/ProgressBar.vue +75 -0
  30. package/components/Radiobutton.vue +128 -0
  31. package/components/SideBar.vue +104 -0
  32. package/components/SingleSelect.vue +190 -0
  33. package/components/Spinner.vue +67 -0
  34. package/components/Switch.vue +94 -0
  35. package/components/TabView.vue +161 -0
  36. package/components/TableView.vue +187 -0
  37. package/components/TagInput.vue +104 -0
  38. package/components/TextInput.vue +58 -0
  39. package/components/TopBar.vue +88 -0
  40. package/fallbackImage.js +29 -0
  41. package/fetcher.js +81 -0
  42. package/gallery/CustomMenuItem.vue +40 -0
  43. package/gallery/DirectoryViewDemo.vue +73 -0
  44. package/gallery/Index.vue +790 -0
  45. package/gallery/folder.svg +151 -0
  46. package/gallery/index.html +25 -0
  47. package/gallery/index.js +10 -0
  48. package/gallery/logo.png +0 -0
  49. package/gallery/vite.config.mjs +9 -0
  50. package/gestures.js +60 -0
  51. package/index.js +86 -0
  52. package/logo.png +0 -0
  53. package/logo.svg +78 -0
  54. package/package.json +26 -0
  55. package/style.css +351 -0
  56. package/tooltip.js +83 -0
  57. package/utils.js +383 -0
  58. package/viewers/GenericViewer.vue +84 -0
  59. package/viewers/ImageViewer.vue +239 -0
  60. package/viewers/PdfViewer.vue +82 -0
  61. package/viewers/TextViewer.vue +221 -0
  62. package/viewers.js +11 -0
@@ -0,0 +1,772 @@
1
+ <template>
2
+ <div
3
+ class="directory-view"
4
+ :class="{ 'drop-target-active': dropTargetActive }"
5
+ tabindex="0"
6
+ ref="main"
7
+ @keydown.up.prevent="onKeyUp($event)"
8
+ @keydown.down.prevent="onKeyDown($event)"
9
+ @keydown.enter="onKeyEnter($event)"
10
+ @keydown.delete="onKeyDelete($event)"
11
+ @keydown.esc="onKeyEscape($event)"
12
+ @drop.stop.prevent="onDrop(null, $event)"
13
+ @dragover.stop.prevent="onDragOver($event)"
14
+ @dragenter.prevent
15
+ @dragexit="onDragExit($event)"
16
+ @keydown="onKeyEvent($event)"
17
+ >
18
+ <Menu ref="contextMenu" :model="contextMenuModel" />
19
+ <Menu ref="contextMenuBody" :model="contextMenuBodyModel" />
20
+
21
+ <Dialog ref="chownDialog" :title="tr('filemanager.chownDialog.title')" :confirmLabel="tr('filemanager.chownDialog.change')" @confirm="onChangeOwnerDialogSubmit(changeOwnerDialog.items, changeOwnerDialog.ownerId)">
22
+ <SingleSelect v-model="changeOwnerDialog.ownerId" :options="ownersModel" optionLabel="label" optionKey="uid"/>
23
+ </Dialog>
24
+
25
+ <div class="directory-view-empty-container" v-show="items.length === 0 && !busy" @contextmenu="onContextMenuBody($event)">
26
+ <slot name="empty">
27
+ Folder is empty
28
+ </slot>
29
+ </div>
30
+
31
+ <div class="directory-view-header" v-show="items.length">
32
+ <div class="directory-view-header-icon"></div>
33
+ <div class="directory-view-header-name" @click="toggleSort(SORT_BY.NAME)"><i v-show="sortBy === SORT_BY.NAME" class="fa-solid" :class="{ 'fa-arrow-up': sortDirection === SORT_DIRECTION.DESC, 'fa-arrow-down': sortDirection === SORT_DIRECTION.ASC }"></i> {{ tr('filemanager.list.name') }}</div>
34
+ <div class="directory-view-header-star" v-show="showStar" @click="toggleSort(SORT_BY.STAR)"><i v-show="sortBy === SORT_BY.STAR" class="fa-solid" :class="{ 'fa-arrow-up': sortDirection === SORT_DIRECTION.DESC, 'fa-arrow-down': sortDirection === SORT_DIRECTION.ASC }"></i> <i class="fa-regular fa-star"></i></div>
35
+ <div class="directory-view-header-owner" v-show="showOwner" @click="toggleSort(SORT_BY.OWNER)"><i v-show="sortBy === SORT_BY.OWNER" class="fa-solid" :class="{ 'fa-arrow-up': sortDirection === SORT_DIRECTION.DESC, 'fa-arrow-down': sortDirection === SORT_DIRECTION.ASC }"></i> {{ tr('filemanager.list.owner') }}</div>
36
+ <div class="directory-view-header-size" v-show="showSize" @click="toggleSort(SORT_BY.SIZE)"><i v-show="sortBy === SORT_BY.SIZE" class="fa-solid" :class="{ 'fa-arrow-up': sortDirection === SORT_DIRECTION.DESC, 'fa-arrow-down': sortDirection === SORT_DIRECTION.ASC }"></i> {{ tr('filemanager.list.size') }}</div>
37
+ <div class="directory-view-header-modified" v-show="showModified" @click="toggleSort(SORT_BY.MTIME)"><i v-show="sortBy === SORT_BY.MTIME" class="fa-solid" :class="{ 'fa-arrow-up': sortDirection === SORT_DIRECTION.DESC, 'fa-arrow-down': sortDirection === SORT_DIRECTION.ASC }"></i> {{ tr('filemanager.list.mtime') }}</div>
38
+ <div class="directory-view-header-actions"></div>
39
+ </div>
40
+ <div class="directory-view-body-container" v-show="items.length">
41
+ <div
42
+ class="directory-view-body"
43
+ @contextmenu="onContextMenuBody($event)"
44
+ >
45
+ <DirectoryViewListItem v-for="item in filteredSortedItems" :ref="(el) => this.elements[item.id] = el"
46
+ :key="item.id"
47
+ :fallbackIcon="fallbackIcon"
48
+ :item="item"
49
+ :show-owner="showOwner"
50
+ :show-share="showShare"
51
+ :show-extract="showExtract"
52
+ :show-size="showSize"
53
+ :show-star="showStar"
54
+ :show-modified="showModified"
55
+ :rename-handler="renameHandler"
56
+ :select-handler="onSelectAndFocus"
57
+ :drop-handler="onDrop"
58
+ :can-drop-handler="onCanDropHandler"
59
+ :star-handler="starHandler"
60
+ @contextmenu="onContextMenu(item, $event)"
61
+ @action-menu="onContextMenu"
62
+ @dblclick="onItemActivated(item)"
63
+ @activated="onItemActivated(item)"
64
+ @dragstart="onItemDragStart($event, item)"
65
+ />
66
+ </div>
67
+
68
+ <div class="drag-handle"></div>
69
+ </div>
70
+ </div>
71
+ </template>
72
+
73
+ <script>
74
+
75
+ import SingleSelect from './SingleSelect.vue';
76
+ import DirectoryViewListItem from './DirectoryViewListItem.vue';
77
+ import Dialog from './Dialog.vue';
78
+ import Menu from './Menu.vue';
79
+
80
+ import { translation } from '../utils.js';
81
+
82
+ const SORT_BY = {
83
+ NAME: 'name',
84
+ OWNER: 'owner',
85
+ STAR: 'star',
86
+ SIZE: 'size',
87
+ MTIME: 'mtime',
88
+ };
89
+
90
+ const SORT_DIRECTION = {
91
+ ASC: 'asc',
92
+ DESC: 'desc',
93
+ };
94
+
95
+ let rawSelected = [];
96
+
97
+ export default {
98
+ name: 'DirectoryView',
99
+ components: {
100
+ Dialog,
101
+ DirectoryViewListItem,
102
+ SingleSelect,
103
+ Menu
104
+ },
105
+ emits: [ 'selectionChanged', 'item-activated' ],
106
+ props: {
107
+ busy: {
108
+ type: Boolean,
109
+ default: false
110
+ },
111
+ showOwner: {
112
+ type: Boolean,
113
+ default: false
114
+ },
115
+ showSize: {
116
+ type: Boolean,
117
+ default: false
118
+ },
119
+ showStar: {
120
+ type: Boolean,
121
+ default: false
122
+ },
123
+ showExtract: {
124
+ type: Boolean,
125
+ default: true
126
+ },
127
+ showShare: {
128
+ // if String its the name of the property for existing share indicator
129
+ type: [ Boolean, String ],
130
+ default: false
131
+ },
132
+ showModified: {
133
+ type: Boolean,
134
+ default: false
135
+ },
136
+ editable: {
137
+ type: Boolean,
138
+ default: true
139
+ },
140
+ multiDownload: {
141
+ type: Boolean,
142
+ default: false
143
+ },
144
+ tr: {
145
+ type: Function,
146
+ default(id) { console.warn('Missing tr for DirectoryView, using fallback.'); return translation(id); }
147
+ },
148
+ items: {
149
+ type: Array,
150
+ validator(value) {
151
+ function fail(prop, type, item) {
152
+ console.error(`DirectoryView.items[].${prop} must be a ${type}`, item);
153
+ return false;
154
+ }
155
+
156
+ for (const item of value) {
157
+ if (typeof item.id !== 'string') return fail('id', 'string', item);
158
+ if (typeof item.name !== 'string') return fail('name', 'string', item);
159
+ if (typeof item.icon !== 'string') return fail('icon', 'string', item);
160
+ if (typeof item.previewUrl !== 'string') return fail('previewUrl', 'string', item);
161
+
162
+ // optional
163
+ if (item.href && typeof item.href !== 'string') return fail('href', 'string', item);
164
+ if (item.target && typeof item.target !== 'string') return fail('target', 'string', item);
165
+ if (item.owner && typeof item.owner !== 'string') return fail('owner', 'string', item);
166
+ if (item.size && typeof item.size !== 'number') return fail('size', 'number', item);
167
+ if (item.star && typeof item.star !== 'boolean') return fail('star', 'boolean', item);
168
+ // if (item.modified && item.modified instanceof Date) return fail('modified', 'Date', item);
169
+ }
170
+
171
+ return true;
172
+ }
173
+ },
174
+ ownersModel: {
175
+ type: Array,
176
+ default: []
177
+ },
178
+ fallbackIcon: String,
179
+ deleteHandler: {
180
+ type: Function,
181
+ default() { console.warn('Missing deleteHandler for DirectoryView'); }
182
+ },
183
+ renameHandler: {
184
+ type: Function,
185
+ default() { console.warn('Missing renameHandler for DirectoryView'); }
186
+ },
187
+ changeOwnerHandler: {
188
+ type: Function,
189
+ default() { console.warn('Missing changeOwnerHandler for DirectoryView'); }
190
+ },
191
+ pasteHandler: {
192
+ type: Function,
193
+ default() { console.warn('Missing pasteHandler for DirectoryView'); }
194
+ },
195
+ newFileHandler: {
196
+ type: Function,
197
+ default() { console.warn('Missing newFileHandler for DirectoryView'); }
198
+ },
199
+ newFolderHandler: {
200
+ type: Function,
201
+ default() { console.warn('Missing newFolderHandler for DirectoryView'); }
202
+ },
203
+ uploadFileHandler: {
204
+ type: Function,
205
+ default() { console.warn('Missing uploadFileHandler for DirectoryView'); }
206
+ },
207
+ uploadFolderHandler: {
208
+ type: Function,
209
+ default() { console.warn('Missing uploadFolderHandler for DirectoryView'); }
210
+ },
211
+ shareHandler: {
212
+ type: Function,
213
+ default() { console.warn('Missing shareHandler for DirectoryView'); }
214
+ },
215
+ starHandler: {
216
+ type: Function,
217
+ default() { console.warn('Missing starHandler for DirectoryView'); }
218
+ },
219
+ dropHandler: {
220
+ type: Function,
221
+ default() { console.warn('Missing dropHandler for DirectoryView'); }
222
+ },
223
+ downloadHandler: {
224
+ type: Function,
225
+ default() { console.warn('Missing downloadHandler for DirectoryView'); }
226
+ },
227
+ extractHandler: {
228
+ type: Function,
229
+ default() { console.warn('Missing extractHandler for DirectoryView'); }
230
+ }
231
+ },
232
+ computed: {
233
+ filteredSortedItems() {
234
+ this.invalidateSelected();
235
+
236
+ // first sort by sort column and direction, then make sure folders are always first
237
+ return this.items.sort((a, b) => {
238
+ if (a.isDirectory && !b.isDirectory) return -1;
239
+ if (!a.isDirectory && b.isDirectory) return 1;
240
+
241
+ let sort = 0;
242
+ if (this.sortBy === SORT_BY.NAME) {
243
+ if (a.name.toLowerCase() < b.name.toLowerCase()) sort = -1;
244
+ else if (a.name.toLowerCase() > b.name.toLowerCase()) sort = 1;
245
+ } else if (this.sortBy === SORT_BY.OWNER) {
246
+ if (a.owner.toLowerCase() < b.owner.toLowerCase()) sort = -1;
247
+ else if (a.owner.toLowerCase() > b.owner.toLowerCase()) sort = 1;
248
+ } else if (this.sortBy === SORT_BY.STAR) {
249
+ if (a.star && !b.star) sort = -1;
250
+ if (!a.star && b.star) sort = 1;
251
+ } else if (this.sortBy === SORT_BY.SIZE) {
252
+ if (a.size < b.size) sort = -1;
253
+ if (a.size > b.size) sort = 1;
254
+ } else if (this.sortBy === SORT_BY.MTIME) {
255
+ if (a.mtime < b.mtime) sort = -1;
256
+ if (a.mtime > b.mtime) sort = 1;
257
+ } else {
258
+ console.error('unknown SORT_BY', this.sortBy);
259
+ }
260
+
261
+ // tie break by id to avoid recursion
262
+ if (sort === 0) {
263
+ if (a.id < b.id) sort = -1;
264
+ else sort = 1;
265
+ }
266
+
267
+ return sort *= (this.sortDirection === SORT_DIRECTION.DESC ? -1 : 1);
268
+ });
269
+ }
270
+ },
271
+ data() {
272
+ return {
273
+ _alreadyActivating: false,
274
+ SORT_BY,
275
+ SORT_DIRECTION,
276
+ elements: {}, // holds references by item.id
277
+ focusItem: null,
278
+ focusItemIndex: -1,
279
+ selectedCount: 0,
280
+ dropTargetActive: false,
281
+ sortBy: SORT_BY.NAME,
282
+ sortDirection: SORT_DIRECTION.ASC,
283
+ clipboard: {
284
+ action: '', // 'copy' or 'cut'
285
+ files: []
286
+ },
287
+ changeOwnerDialog: {
288
+ busy: false,
289
+ ownerId: '',
290
+ items: []
291
+ },
292
+ contextMenuBodyModel: [{
293
+ label: this.tr('filemanager.list.menu.paste'),
294
+ icon:'fa-regular fa-paste',
295
+ disabled: () => { return !this.clipboard.files || !this.clipboard.files.length; },
296
+ action: () => { this.pasteHandler(this.clipboard.action, this.clipboard.files, null); }
297
+ }, {
298
+ label: this.tr('filemanager.list.menu.selectAll'),
299
+ icon:'fa-solid fa-check-double',
300
+ action: this.onSelectAll
301
+ }, {
302
+ separator:true
303
+ }, {
304
+ label: this.tr('filemanager.toolbar.newFile'),
305
+ icon:'fa-solid fa-file-circle-plus',
306
+ action: this.newFileHandler
307
+ }, {
308
+ label: this.tr('filemanager.toolbar.newFolder'),
309
+ icon:'fa-solid fa-folder-plus',
310
+ action: this.newFolderHandler
311
+ }, {
312
+ separator:true
313
+ }, {
314
+ label: this.tr('filemanager.toolbar.uploadFile'),
315
+ icon:'fa-solid fa-file-arrow-up',
316
+ action: this.uploadFileHandler
317
+ }, {
318
+ label: this.tr('filemanager.toolbar.uploadFolder'),
319
+ icon:'fa-regular fa-folder-open',
320
+ action: this.uploadFolderHandler
321
+ }],
322
+ contextMenuModel: [{
323
+ label: this.tr('filemanager.list.menu.open'),
324
+ icon:'fa-regular fa-folder-open',
325
+ disabled: () => { return this.selectedCount > 1; },
326
+ action: () => { this.onItemActivated(this.getSelected()[0]); }
327
+ }, {
328
+ separator:true
329
+ }, {
330
+ label: this.tr('filemanager.list.menu.download'),
331
+ icon:'fa-solid fa-download',
332
+ action: this.onItemDownload,
333
+ disabled: () => { return this.multiDownload ? this.selectedCount === 0 : this.selectedCount !== 1; }
334
+ }, {
335
+ label: this.tr('filemanager.list.menu.share'),
336
+ icon:'fa-solid fa-share-nodes',
337
+ action: this.onItemShare,
338
+ disabled: () => { return this.selectedCount > 1; },
339
+ visible: this.showShare
340
+ }, {
341
+ label: this.tr('filemanager.list.menu.copy'),
342
+ icon:'fa-regular fa-copy',
343
+ action: this.onItemsCopy
344
+ }, {
345
+ label: this.tr('filemanager.list.menu.cut'),
346
+ icon:'fa-solid fa-scissors',
347
+ action: this.onItemsCut
348
+ }, {
349
+ label: this.tr('filemanager.list.menu.paste'),
350
+ icon:'fa-regular fa-paste',
351
+ disabled: () => { return !(this.focusItem && this.focusItem.isDirectory) || this.selectedCount > 1 || !this.clipboard.files || !this.clipboard.files.length; },
352
+ action: () => { this.pasteHandler(this.clipboard.action, this.clipboard.files, this.focusItem); }
353
+ }, {
354
+ label: this.tr('filemanager.list.menu.selectAll'),
355
+ icon:'fa-solid fa-check-double',
356
+ action: this.onSelectAll
357
+ }, {
358
+ label: this.tr('filemanager.list.menu.rename'),
359
+ icon:'fa-regular fa-pen-to-square',
360
+ action: this.onItemRenameBegin,
361
+ disabled: () => { return !this.editable || this.selectedCount > 1; }
362
+ }, {
363
+ label: this.tr('filemanager.list.menu.chown'),
364
+ icon:'fa-solid fa-user-pen',
365
+ action: this.onItemsChangeOwner,
366
+ visible: this.showOwner
367
+ }, {
368
+ label: this.tr('filemanager.list.menu.extract'),
369
+ action: this.onItemExtract,
370
+ visible: () => {
371
+ if (!this.showExtract) return false;
372
+
373
+ const file = this.getSelected()[0];
374
+ return !(this.selectedCount !== 1 || !(
375
+ file.name.match(/\.tgz$/) ||
376
+ file.name.match(/\.tar$/) ||
377
+ file.name.match(/\.7z$/) ||
378
+ file.name.match(/\.zip$/) ||
379
+ file.name.match(/\.tar\.gz$/) ||
380
+ file.name.match(/\.tar\.xz$/) ||
381
+ file.name.match(/\.tar\.bz2$/)));
382
+ }
383
+ }, {
384
+ separator:true
385
+ }, {
386
+ label: this.tr('filemanager.list.menu.delete'),
387
+ icon:'fa-regular fa-trash-can',
388
+ action: () => { this.deleteHandler(this.getSelected()); }
389
+ }],
390
+ };
391
+ },
392
+ methods: {
393
+ highlight(item) {
394
+ const id = item.id;
395
+
396
+ if (!this.elements[id]) return;
397
+
398
+ this.elements[id].highlight();
399
+ this.elements[id].$el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
400
+ },
401
+ highlightByName(name) {
402
+ const tmp = this.items.find((i) => i.name === name);
403
+ if (!tmp) return;
404
+
405
+ this.highlight(tmp);
406
+ },
407
+ toggleSort(sortBy) {
408
+ if (this.sortBy === sortBy) this.sortDirection = this.sortDirection === SORT_DIRECTION.ASC ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC;
409
+ else this.sortBy = sortBy;
410
+ },
411
+ onDrop(targetItem, event) {
412
+ this.dropTargetActive = false;
413
+
414
+ const targetItemName = targetItem ? targetItem.name : '';
415
+
416
+ if (event.dataTransfer.getData('application/x-pankow') === 'selected') {
417
+ // can't drop selected files on same folder
418
+ if (!targetItemName) return;
419
+
420
+ this.dropHandler(targetItemName, null, this.getSelected());
421
+ } else if (event.dataTransfer && event.dataTransfer.items[0]) {
422
+ // this is dropping files from outside the browser
423
+ this.dropHandler(targetItemName, event.dataTransfer, null);
424
+ }
425
+ },
426
+ onCanDropHandler(item) {
427
+ if (!item.isDirectory) return false;
428
+ return !item.selected;
429
+ },
430
+ onDragOver(event) {
431
+ if (!event.dataTransfer) return;
432
+
433
+ if (event.dataTransfer.getData('application/x-pankow') === 'selected') {
434
+ this.dropTargetActive = false;
435
+ } else {
436
+ event.dataTransfer.dropEffect = 'copy';
437
+ this.dropTargetActive = true;
438
+ }
439
+ },
440
+ onDragExit(event) {
441
+ this.dropTargetActive = false;
442
+ },
443
+ onItemDragStart(event) {
444
+ const dragHandle = document.getElementsByClassName('drag-handle')[0];
445
+
446
+ // clear element
447
+ dragHandle.replaceChildren();
448
+
449
+ for (let i = 0; i < Math.min(this.selectedCount, 6); ++i) {
450
+ const dragHandleItem = document.createElement('div');
451
+ dragHandleItem.style.opacity = 1-((i*2)/10);
452
+ const dragHandleItemIcon = document.createElement('img');
453
+ dragHandleItemIcon.src = rawSelected[i].previewUrl || rawSelected[i].icon
454
+ dragHandleItem.append(dragHandleItemIcon, rawSelected[i].name);
455
+ dragHandle.append(dragHandleItem);
456
+ }
457
+
458
+ event.dataTransfer.dropEffect = 'move'
459
+ event.dataTransfer.setData('application/x-pankow', 'selected');
460
+ event.dataTransfer.setDragImage(dragHandle, 0, 0);
461
+ },
462
+ onKeyUp(event) {
463
+ this.onSelectAndFocusIndex(this.focusItemIndex-1, event.shiftKey);
464
+ },
465
+ onKeyDown(event) {
466
+ this.onSelectAndFocusIndex(this.focusItemIndex+1, event.shiftKey);
467
+ },
468
+ onKeyEnter(event) {
469
+ if (this.focusItem) this.onItemActivated(this.focusItem);
470
+ },
471
+ async onKeyDelete(event) {
472
+ if (event.key !== 'Delete') return; // triggered also by backspace
473
+ if (!this.focusItem) return;
474
+
475
+ await this.deleteHandler(this.getSelected());
476
+ },
477
+ onKeyEscape(event) {
478
+ this.onUnselectAll();
479
+ },
480
+ onKeyEvent(event) {
481
+ if (event.key === 'c' && (event.ctrlKey || event.metaKey)) {
482
+ this.onItemsCopy();
483
+ event.preventDefault();
484
+ } else if (event.key === 'v' && (event.ctrlKey || event.metaKey)) {
485
+ this.pasteHandler(this.clipboard.action, this.clipboard.files);
486
+ event.preventDefault();
487
+ } else if (event.key === 'x' && (event.ctrlKey || event.metaKey)) {
488
+ this.onItemsCut();
489
+ event.preventDefault();
490
+ } else if (event.key === 'a' && (event.ctrlKey || event.metaKey)) {
491
+ this.onSelectAll();
492
+ event.preventDefault();
493
+ } else if (event.key === 'End') {
494
+ if (event.shiftKey) this.onSelectAndFocusToIndex(this.items.length-1);
495
+ else this.onSelectAndFocusIndex(this.items.length-1, event.shiftKey);
496
+ event.preventDefault();
497
+ } else if (event.key === 'Home') {
498
+ if (event.shiftKey) this.onSelectAndFocusToIndex(0);
499
+ else this.onSelectAndFocusIndex(0, event.shiftKey);
500
+ event.preventDefault();
501
+ } else if (event.key === 'PageUp') {
502
+ const pageItemCount = parseInt(event.target.clientHeight / 55);
503
+ if (event.shiftKey) this.onSelectAndFocusToIndex(this.focusItemIndex-pageItemCount);
504
+ else this.onSelectAndFocusIndex(this.focusItemIndex-pageItemCount, event.shiftKey);
505
+ event.preventDefault();
506
+ } else if (event.key === 'PageDown') {
507
+ const pageItemCount = parseInt(event.target.clientHeight / 55);
508
+ if (event.shiftKey) this.onSelectAndFocusToIndex(this.focusItemIndex+pageItemCount);
509
+ else this.onSelectAndFocusIndex(this.focusItemIndex+pageItemCount, event.shiftKey);
510
+ event.preventDefault();
511
+ }
512
+ },
513
+ onContextMenu(item, event) {
514
+ this.$refs.contextMenu.open(event);
515
+ this.onSelectAndFocus(item, event, false);
516
+ event.stopPropagation();
517
+ },
518
+ onContextMenuBody(event) {
519
+ this.$refs.contextMenuBody.open(event);
520
+ this.onUnselectAll();
521
+ },
522
+ onItemActivated(item) {
523
+ // prevent concurrent activations for some time
524
+ if (this._alreadyActivating) return;
525
+ this._alreadyActivating = true;
526
+ setTimeout(() => { this._alreadyActivating = false; }, 500);
527
+
528
+ this.$emit('item-activated', item);
529
+ },
530
+ onItemsChangeOwner(event) {
531
+ if (!this.focusItem) return console.warn('onItemsChangeOwner should only be triggered if at least one item is selected');
532
+
533
+ this.changeOwnerDialog.items = this.getSelected();
534
+ this.changeOwnerDialog.ownerId = this.focusItem.uid;
535
+ this.$refs.chownDialog.open();
536
+ },
537
+ onItemDownload(event) {
538
+ if (!this.focusItem) return console.warn('onItemDownload should only be triggered if at lesst one item is selected');
539
+
540
+ this.downloadHandler(this.multiDownload ? this.getSelected() : this.focusItem);
541
+ },
542
+ onItemExtract(event) {
543
+ if (this.selectedCount !== 1) return console.warn('onItemExtract should only be triggered if one item is selected');
544
+
545
+ this.extractHandler(this.focusItem);
546
+ },
547
+ onItemsCopy(event) {
548
+ if (!this.focusItem) return console.warn('onItemsCopy should only be triggered if at least one item is selected');
549
+
550
+ this.clipboard.action = 'copy';
551
+ this.clipboard.files = this.getSelected();
552
+ },
553
+ onItemsCut(event) {
554
+ if (!this.focusItem) return console.warn('onItemsCut should only be triggered if at least one item is selected');
555
+
556
+ this.clipboard.action = 'cut';
557
+ this.clipboard.files = this.getSelected();
558
+ },
559
+ onItemShare(event) {
560
+ if (this.selectedCount !== 1) return console.warn('onItemRenameBegin should only be triggered if one item is selected');
561
+
562
+ this.shareHandler(this.focusItem);
563
+ },
564
+ onItemRenameBegin(event) {
565
+ if (this.selectedCount !== 1) return console.warn('onItemRenameBegin should only be triggered if one item is selected');
566
+
567
+ this.elements[this.focusItem.id].onRenameBegin();
568
+ },
569
+ async onItemRenameSubmit(item, newName) {
570
+ await this.renameHandler(item, newName);
571
+ },
572
+ async onChangeOwnerDialogSubmit(items, newOwnerUid) {
573
+ this.changeOwnerDialog.busy = true;
574
+ await this.changeOwnerHandler(items, newOwnerUid);
575
+ this.changeOwnerDialog.visible = false;
576
+ this.changeOwnerDialog.items = [];
577
+ this.changeOwnerDialog.ownerId = this.ownersModel[0].uid;
578
+ this.changeOwnerDialog.busy = false;
579
+ this.$refs.chownDialog.close();
580
+ },
581
+ getSelected() {
582
+ return rawSelected;
583
+ },
584
+ invalidateSelected() {
585
+ rawSelected = this.items.filter((item) => item.selected).slice();
586
+ this.selectedCount = rawSelected.length;
587
+ this.$emit('selectionChanged', rawSelected);
588
+ },
589
+ onSelectAll() {
590
+ for (const item of this.items) item.selected = true;
591
+ if (!this.focusItem) this.setFocus(this.focusItemIndex);
592
+
593
+ this.invalidateSelected();
594
+ },
595
+ onUnselectAll() {
596
+ for (const item of this.items) item.selected = false;
597
+ if (this.focusItem) this.focusItem = null;
598
+
599
+ this.invalidateSelected();
600
+ },
601
+ setFocus(index) {
602
+ if (this.items[this.focusItemIndex]) this.items[this.focusItemIndex].focused = false;
603
+ if (this.focusItem) this.focusItem.focused = false;
604
+ if (!this.items[index]) return;
605
+
606
+ this.focusItem = this.items[index];
607
+ this.focusItem.focused = true;
608
+ this.focusItemIndex = index;
609
+ },
610
+ onSelectAndFocusToIndex(index) {
611
+ index = (index < 0) ? 0 : (index > this.items.length-1 ? this.items.length-1 : index); // keep in bounds
612
+
613
+ this.setFocus(index);
614
+
615
+ const item = this.items[index];
616
+ const itemIndex = this.items.findIndex(i => i.id === item.id);
617
+ const firstSelectedIndex = this.items.findIndex(item => item.selected);
618
+ const lastSelectedIndex = this.items.findLastIndex(item => item.selected);
619
+
620
+ if (index < firstSelectedIndex) for (let i = index; i < firstSelectedIndex; ++i) this.items[i].selected = true;
621
+ else if (index > lastSelectedIndex) for (let i = lastSelectedIndex; i <= index; ++i) this.items[i].selected = true;
622
+
623
+ if (this.elements[item.id]) this.elements[item.id].$el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
624
+
625
+ this.invalidateSelected();
626
+ },
627
+ onSelectAndFocusIndex(index, keepSelected = false) {
628
+ index = (index < 0) ? 0 : (index > this.items.length-1 ? this.items.length-1 : index); // keep in bounds
629
+
630
+ const item = this.items[index];
631
+
632
+ this.onSelectAndFocus(item, {}, keepSelected);
633
+ },
634
+ onSelectAndFocus(item, event, keepSelected = false) {
635
+ const itemIndex = this.items.findIndex(i => i.id === item.id);
636
+ this.setFocus(itemIndex);
637
+
638
+ // do not toggle selection if this is from right click
639
+ if (event.button === 2 && item.selected) keepSelected = true;
640
+
641
+ if (event.ctrlKey) {
642
+ if (keepSelected) item.selected = true;
643
+ else item.selected = !item.selected;
644
+ } else if (event.shiftKey) {
645
+ const firstSelectedIndex = this.items.findIndex(item => item.selected);
646
+ const lastSelectedIndex = this.items.findLastIndex(item => item.selected);
647
+
648
+ if (itemIndex < firstSelectedIndex) for (let i = itemIndex; i < firstSelectedIndex; ++i) this.items[i].selected = true;
649
+ else if (itemIndex > lastSelectedIndex) for (let i = lastSelectedIndex; i <= itemIndex; ++i) this.items[i].selected = true;
650
+ } else {
651
+ if (!keepSelected) for (const item of this.items) item.selected = false;
652
+ item.selected = true;
653
+ }
654
+
655
+ if (this.elements[item.id]) this.elements[item.id].$el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
656
+
657
+ this.invalidateSelected();
658
+ }
659
+ }
660
+ };
661
+
662
+ </script>
663
+
664
+ <style scoped>
665
+
666
+ .directory-view {
667
+ display: block;
668
+ overflow: hidden;
669
+ height: 100%;
670
+ width: 100%;
671
+ user-select: none;
672
+ border: 2px solid transparent;
673
+ }
674
+
675
+ .directory-view-empty-container {
676
+ display: flex;
677
+ justify-content: center;
678
+ align-items: center;
679
+ overflow: hidden;
680
+ height: 100%;
681
+ width: 100%;
682
+ user-select: none;
683
+ border: 2px solid transparent;
684
+ }
685
+
686
+ .directory-view.drop-target-active {
687
+ border: 2px solid var(--pankow-color-primary);
688
+ }
689
+
690
+ .directory-view-header {
691
+ padding: 10px;
692
+ padding-top: 20px;
693
+ display: flex;
694
+ font-weight: bold;
695
+ }
696
+
697
+ .directory-view-body-container {
698
+ overflow: hidden;
699
+ height: calc(100% - 38px); /* 38px is the header size */
700
+ }
701
+
702
+ .directory-view-body {
703
+ padding: 0 10px;
704
+ padding-bottom: 10px;
705
+ height: 100%;
706
+ overflow: auto;
707
+ }
708
+
709
+ .directory-view-header-icon {
710
+ width: 40px;
711
+ }
712
+
713
+ .directory-view-header-name {
714
+ padding-left: 10px;
715
+ flex-grow: 1;
716
+ }
717
+
718
+ .directory-view-header-star {
719
+ width: 80px;
720
+ text-align: center;
721
+ }
722
+
723
+ .directory-view-header-owner {
724
+ width: 100px;
725
+ }
726
+
727
+ .directory-view-header-size {
728
+ width: 100px;
729
+ }
730
+
731
+ .directory-view-header-modified {
732
+ width: 100px;
733
+ }
734
+
735
+ .directory-view-header-actions {
736
+ width: 40px;
737
+ }
738
+
739
+ .dialog-header {
740
+ font-weight: 600;
741
+ font-size: 1.25rem;
742
+ }
743
+
744
+ .dialog-dropdown {
745
+ width: 100%;
746
+ margin-top: 5px;
747
+ margin-bottom: 1.5rem;
748
+ }
749
+
750
+ .dialog-submit {
751
+ margin-top: 5px;
752
+ }
753
+
754
+ .drag-handle {
755
+ position: absolute;
756
+ top: -1000px;
757
+ background: transparent;
758
+ }
759
+
760
+ .drag-handle :deep(div) {
761
+ display: flex;
762
+ align-items: center;
763
+ }
764
+
765
+ .drag-handle :deep(img) {
766
+ width: 45px;
767
+ height: 45px;
768
+ object-fit: cover;
769
+ padding: 5px;
770
+ }
771
+
772
+ </style>