@cloudron/pankow 4.1.9 → 4.1.10

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.
@@ -47,7 +47,7 @@
47
47
  @contextmenu="onContextMenuBody($event)"
48
48
  >
49
49
  <template v-if="viewMode === 'list'">
50
- <DirectoryViewListItem v-for="item in filteredSortedItems" :ref="(el) => this.elements[item.id] = el"
50
+ <DirectoryViewListItem v-for="item in filteredSortedItems" :ref="(el) => setItemRef(item.id, el)"
51
51
  :key="item.id"
52
52
  :fallbackIcon="fallbackIcon"
53
53
  :item="item"
@@ -70,7 +70,7 @@
70
70
  />
71
71
  </template>
72
72
  <template v-else-if="viewMode === 'grid'">
73
- <DirectoryViewGridItem v-for="item in filteredSortedItems" :ref="(el) => this.elements[item.id] = el"
73
+ <DirectoryViewGridItem v-for="item in filteredSortedItems" :ref="(el) => setItemRef(item.id, el)"
74
74
  :key="item.id"
75
75
  :fallbackIcon="fallbackIcon"
76
76
  :item="item"
@@ -95,7 +95,9 @@
95
95
  </div>
96
96
  </template>
97
97
 
98
- <script>
98
+ <script setup>
99
+
100
+ import { ref, reactive, computed, useTemplateRef } from 'vue';
99
101
 
100
102
  import SingleSelect from './SingleSelect.vue';
101
103
  import DirectoryViewListItem from './DirectoryViewListItem.vue';
@@ -106,10 +108,10 @@ import Menu from './Menu.vue';
106
108
  import { translation } from '../utils.js';
107
109
 
108
110
  const SORT_BY = {
109
- NAME: 'name',
111
+ NAME: 'name',
110
112
  OWNER: 'owner',
111
- STAR: 'star',
112
- SIZE: 'size',
113
+ STAR: 'star',
114
+ SIZE: 'size',
113
115
  MTIME: 'mtime',
114
116
  };
115
117
 
@@ -120,675 +122,791 @@ const SORT_DIRECTION = {
120
122
 
121
123
  let rawSelected = [];
122
124
 
123
- export default {
124
- name: 'DirectoryView',
125
- components: {
126
- Dialog,
127
- DirectoryViewListItem,
128
- DirectoryViewGridItem,
129
- SingleSelect,
130
- Menu
131
- },
132
- emits: [ 'selectionChanged', 'item-activated' ],
133
- props: {
134
- viewMode: {
135
- type: String,
136
- default: 'list',
137
- validator(value) {
138
- return ['list', 'grid'].includes(value);
139
- }
140
- },
141
- busy: {
142
- type: Boolean,
143
- default: false
144
- },
145
- showOwner: {
146
- type: Boolean,
147
- default: false
148
- },
149
- showSize: {
150
- type: Boolean,
151
- default: false
152
- },
153
- showStar: {
154
- type: Boolean,
155
- default: false
156
- },
157
- showExtract: {
158
- type: Boolean,
159
- default: true
160
- },
161
- showNewFile: {
162
- type: Boolean,
163
- default: true
164
- },
165
- showNewFolder: {
166
- type: Boolean,
167
- default: true
168
- },
169
- showCut: {
170
- type: Boolean,
171
- default: true
172
- },
173
- showCopy: {
174
- type: Boolean,
175
- default: true
176
- },
177
- showPaste: {
178
- type: Boolean,
179
- default: true
180
- },
181
- showRename: {
182
- type: Boolean,
183
- default: true
184
- },
185
- showSelectAll: {
186
- type: Boolean,
187
- default: true
188
- },
189
- showDownload: {
190
- type: Boolean,
191
- default: true
125
+ const props = defineProps({
126
+ viewMode: {
127
+ type: String,
128
+ default: 'list',
129
+ validator(value) {
130
+ return ['list', 'grid'].includes(value);
192
131
  },
193
- showDelete: {
194
- type: Boolean,
195
- default: true
196
- },
197
- showShare: {
198
- type: Boolean,
199
- default: false
200
- },
201
- showModified: {
202
- type: Boolean,
203
- default: false
204
- },
205
- shareIndicatorProperty: {
206
- // the name of the property for existing share indicator
207
- type: String,
208
- default: '',
209
- },
210
- editable: {
211
- type: Boolean,
212
- default: true
213
- },
214
- multiDownload: {
215
- type: Boolean,
216
- default: false
217
- },
218
- tr: {
219
- type: Function,
220
- default(id) { console.warn('Missing tr for DirectoryView, using fallback.'); return translation(id); }
132
+ },
133
+ busy: {
134
+ type: Boolean,
135
+ default: false,
136
+ },
137
+ showOwner: {
138
+ type: Boolean,
139
+ default: false,
140
+ },
141
+ showSize: {
142
+ type: Boolean,
143
+ default: false,
144
+ },
145
+ showStar: {
146
+ type: Boolean,
147
+ default: false,
148
+ },
149
+ showExtract: {
150
+ type: Boolean,
151
+ default: true,
152
+ },
153
+ showNewFile: {
154
+ type: Boolean,
155
+ default: true,
156
+ },
157
+ showNewFolder: {
158
+ type: Boolean,
159
+ default: true,
160
+ },
161
+ showCut: {
162
+ type: Boolean,
163
+ default: true,
164
+ },
165
+ showCopy: {
166
+ type: Boolean,
167
+ default: true,
168
+ },
169
+ showPaste: {
170
+ type: Boolean,
171
+ default: true,
172
+ },
173
+ showRename: {
174
+ type: Boolean,
175
+ default: true,
176
+ },
177
+ showSelectAll: {
178
+ type: Boolean,
179
+ default: true,
180
+ },
181
+ showDownload: {
182
+ type: Boolean,
183
+ default: true,
184
+ },
185
+ showDelete: {
186
+ type: Boolean,
187
+ default: true,
188
+ },
189
+ showShare: {
190
+ type: Boolean,
191
+ default: false,
192
+ },
193
+ showModified: {
194
+ type: Boolean,
195
+ default: false,
196
+ },
197
+ shareIndicatorProperty: {
198
+ type: String,
199
+ default: '',
200
+ },
201
+ editable: {
202
+ type: Boolean,
203
+ default: true,
204
+ },
205
+ multiDownload: {
206
+ type: Boolean,
207
+ default: false,
208
+ },
209
+ tr: {
210
+ type: Function,
211
+ default(id) {
212
+ console.warn('Missing tr for DirectoryView, using fallback.');
213
+ return translation(id);
221
214
  },
222
- items: {
223
- type: Array,
224
- validator(value) {
225
- function fail(prop, type, item) {
226
- console.error(`DirectoryView.items[].${prop} must be a ${type}`, item);
227
- return false;
228
- }
229
-
230
- for (const item of value) {
231
- if (typeof item.id !== 'string') return fail('id', 'string', item);
232
- if (typeof item.name !== 'string') return fail('name', 'string', item);
233
- if (typeof item.icon !== 'string') return fail('icon', 'string', item);
234
- if (typeof item.previewUrl !== 'string') return fail('previewUrl', 'string', item);
235
-
236
- // optional
237
- if (item.href && typeof item.href !== 'string') return fail('href', 'string', item);
238
- if (item.target && typeof item.target !== 'string') return fail('target', 'string', item);
239
- if (item.owner && typeof item.owner !== 'string') return fail('owner', 'string', item);
240
- if (item.size && typeof item.size !== 'number') return fail('size', 'number', item);
241
- if (item.star && typeof item.star !== 'boolean') return fail('star', 'boolean', item);
242
- // if (item.modified && item.modified instanceof Date) return fail('modified', 'Date', item);
243
- }
244
-
245
- return true;
215
+ },
216
+ items: {
217
+ type: Array,
218
+ validator(value) {
219
+ function fail(prop, type, item) {
220
+ console.error(`DirectoryView.items[].${prop} must be a ${type}`, item);
221
+ return false;
246
222
  }
223
+
224
+ for (const item of value) {
225
+ if (typeof item.id !== 'string') return fail('id', 'string', item);
226
+ if (typeof item.name !== 'string') return fail('name', 'string', item);
227
+ if (typeof item.icon !== 'string') return fail('icon', 'string', item);
228
+ if (typeof item.previewUrl !== 'string') return fail('previewUrl', 'string', item);
229
+
230
+ if (item.href && typeof item.href !== 'string') return fail('href', 'string', item);
231
+ if (item.target && typeof item.target !== 'string') return fail('target', 'string', item);
232
+ if (item.owner && typeof item.owner !== 'string') return fail('owner', 'string', item);
233
+ if (item.size && typeof item.size !== 'number') return fail('size', 'number', item);
234
+ if (item.star && typeof item.star !== 'boolean') return fail('star', 'boolean', item);
235
+ }
236
+
237
+ return true;
247
238
  },
248
- ownersModel: {
249
- type: Array,
250
- default: []
251
- },
252
- fallbackIcon: String,
253
- deleteHandler: {
254
- type: Function,
255
- default() { console.warn('Missing deleteHandler for DirectoryView'); }
239
+ },
240
+ ownersModel: {
241
+ type: Array,
242
+ default: () => [],
243
+ },
244
+ fallbackIcon: String,
245
+ showUploadFile: {
246
+ type: Boolean,
247
+ default: false,
248
+ },
249
+ showUploadFolder: {
250
+ type: Boolean,
251
+ default: false,
252
+ },
253
+ deleteHandler: {
254
+ type: Function,
255
+ default() {
256
+ console.warn('Missing deleteHandler for DirectoryView');
256
257
  },
257
- renameHandler: {
258
- type: Function,
259
- default() { console.warn('Missing renameHandler for DirectoryView'); }
258
+ },
259
+ renameHandler: {
260
+ type: Function,
261
+ default() {
262
+ console.warn('Missing renameHandler for DirectoryView');
260
263
  },
261
- changeOwnerHandler: {
262
- type: Function,
263
- default() { console.warn('Missing changeOwnerHandler for DirectoryView'); }
264
+ },
265
+ changeOwnerHandler: {
266
+ type: Function,
267
+ default() {
268
+ console.warn('Missing changeOwnerHandler for DirectoryView');
264
269
  },
265
- pasteHandler: {
266
- type: Function,
267
- default() { console.warn('Missing pasteHandler for DirectoryView'); }
270
+ },
271
+ pasteHandler: {
272
+ type: Function,
273
+ default() {
274
+ console.warn('Missing pasteHandler for DirectoryView');
268
275
  },
269
- newFileHandler: {
270
- type: Function,
271
- default() { console.warn('Missing newFileHandler for DirectoryView'); }
276
+ },
277
+ newFileHandler: {
278
+ type: Function,
279
+ default() {
280
+ console.warn('Missing newFileHandler for DirectoryView');
272
281
  },
273
- newFolderHandler: {
274
- type: Function,
275
- default() { console.warn('Missing newFolderHandler for DirectoryView'); }
282
+ },
283
+ newFolderHandler: {
284
+ type: Function,
285
+ default() {
286
+ console.warn('Missing newFolderHandler for DirectoryView');
276
287
  },
277
- uploadFileHandler: {
278
- type: Function,
279
- default() { console.warn('Missing uploadFileHandler for DirectoryView'); }
288
+ },
289
+ uploadFileHandler: {
290
+ type: Function,
291
+ default() {
292
+ console.warn('Missing uploadFileHandler for DirectoryView');
280
293
  },
281
- uploadFolderHandler: {
282
- type: Function,
283
- default() { console.warn('Missing uploadFolderHandler for DirectoryView'); }
294
+ },
295
+ uploadFolderHandler: {
296
+ type: Function,
297
+ default() {
298
+ console.warn('Missing uploadFolderHandler for DirectoryView');
284
299
  },
285
- shareHandler: {
286
- type: Function,
287
- default() { console.warn('Missing shareHandler for DirectoryView'); }
300
+ },
301
+ shareHandler: {
302
+ type: Function,
303
+ default() {
304
+ console.warn('Missing shareHandler for DirectoryView');
288
305
  },
289
- starHandler: {
290
- type: Function,
291
- default() { console.warn('Missing starHandler for DirectoryView'); }
306
+ },
307
+ starHandler: {
308
+ type: Function,
309
+ default() {
310
+ console.warn('Missing starHandler for DirectoryView');
292
311
  },
293
- dropHandler: {
294
- type: Function,
295
- default() { console.warn('Missing dropHandler for DirectoryView'); }
312
+ },
313
+ dropHandler: {
314
+ type: Function,
315
+ default() {
316
+ console.warn('Missing dropHandler for DirectoryView');
296
317
  },
297
- downloadHandler: {
298
- type: Function,
299
- default() { console.warn('Missing downloadHandler for DirectoryView'); }
318
+ },
319
+ downloadHandler: {
320
+ type: Function,
321
+ default() {
322
+ console.warn('Missing downloadHandler for DirectoryView');
300
323
  },
301
- extractHandler: {
302
- type: Function,
303
- default() { console.warn('Missing extractHandler for DirectoryView'); }
324
+ },
325
+ extractHandler: {
326
+ type: Function,
327
+ default() {
328
+ console.warn('Missing extractHandler for DirectoryView');
304
329
  },
305
- refreshHandler: {
306
- type: Function,
307
- default: null
308
- }
309
330
  },
310
- computed: {
311
- filteredSortedItems() {
312
- this.invalidateSelected();
313
-
314
- // first sort by sort column and direction, then make sure folders are always first
315
- return this.items.sort((a, b) => {
316
- if (a.isDirectory && !b.isDirectory) return -1;
317
- if (!a.isDirectory && b.isDirectory) return 1;
318
-
319
- let sort = 0;
320
- if (this.sortBy === SORT_BY.NAME) {
321
- if (a.name.toLowerCase() < b.name.toLowerCase()) sort = -1;
322
- else if (a.name.toLowerCase() > b.name.toLowerCase()) sort = 1;
323
- } else if (this.sortBy === SORT_BY.OWNER) {
324
- if (a.owner.toLowerCase() < b.owner.toLowerCase()) sort = -1;
325
- else if (a.owner.toLowerCase() > b.owner.toLowerCase()) sort = 1;
326
- } else if (this.sortBy === SORT_BY.STAR) {
327
- if (a.star && !b.star) sort = -1;
328
- if (!a.star && b.star) sort = 1;
329
- } else if (this.sortBy === SORT_BY.SIZE) {
330
- if (a.size < b.size) sort = -1;
331
- if (a.size > b.size) sort = 1;
332
- } else if (this.sortBy === SORT_BY.MTIME) {
333
- if (a.mtime < b.mtime) sort = -1;
334
- if (a.mtime > b.mtime) sort = 1;
335
- } else {
336
- console.error('unknown SORT_BY', this.sortBy);
337
- }
338
-
339
- // tie break by id to avoid recursion
340
- if (sort === 0) {
341
- if (a.id < b.id) sort = -1;
342
- else sort = 1;
343
- }
344
-
345
- return sort *= (this.sortDirection === SORT_DIRECTION.DESC ? -1 : 1);
346
- });
347
- }
331
+ refreshHandler: {
332
+ type: Function,
333
+ default: null,
348
334
  },
349
- data() {
350
- return {
351
- _alreadyActivating: false,
352
- SORT_BY,
353
- SORT_DIRECTION,
354
- elements: {}, // holds references by item.id
355
- focusItem: null,
356
- focusItemIndex: -1,
357
- selectedCount: 0,
358
- dropTargetActive: false,
359
- sortBy: SORT_BY.NAME,
360
- sortDirection: SORT_DIRECTION.ASC,
361
- clipboard: {
362
- action: '', // 'copy' or 'cut'
363
- files: []
364
- },
365
- changeOwnerDialog: {
366
- busy: false,
367
- ownerId: '',
368
- items: []
369
- },
370
- contextMenuBodyModel: [{
371
- label: this.tr('filemanager.list.menu.paste'),
372
- icon:'fa-regular fa-paste',
373
- disabled: () => { return !this.clipboard.files || !this.clipboard.files.length; },
374
- action: () => { this.pasteHandler(this.clipboard.action, this.clipboard.files, null); }
375
- }, {
376
- label: this.tr('filemanager.list.menu.selectAll'),
377
- icon:'fa-solid fa-check-double',
378
- action: this.onSelectAll
379
- }, {
380
- separator:true,
381
- visible: () => { return typeof this.refreshHandler === 'function'; },
382
- }, {
383
- label: this.tr('filemanager.toolbar.refresh'),
384
- icon:'fa-solid fa-arrow-rotate-right',
385
- visible: () => { return typeof this.refreshHandler === 'function'; },
386
- action: this.refreshHandler,
387
- }, {
388
- separator:true,
389
- visible: () => { return this.showNewFile || this.showNewFolder; },
390
- }, {
391
- label: this.tr('filemanager.toolbar.newFile'),
392
- icon:'fa-solid fa-file-circle-plus',
393
- action: this.newFileHandler,
394
- visible: () => { return this.showNewFile; },
395
- }, {
396
- label: this.tr('filemanager.toolbar.newFolder'),
397
- icon:'fa-solid fa-folder-plus',
398
- action: this.newFolderHandler,
399
- visible: () => { return this.showNewFolder; },
400
- }, {
401
- separator:true,
402
- visible: () => { return this.showUploadFile || this.showUploadFolder; },
403
- }, {
404
- label: this.tr('filemanager.toolbar.uploadFile'),
405
- icon:'fa-solid fa-file-arrow-up',
406
- action: this.uploadFileHandler,
407
- visible: () => { return this.showUploadFile; },
408
- }, {
409
- label: this.tr('filemanager.toolbar.uploadFolder'),
410
- icon:'fa-regular fa-folder-open',
411
- action: this.uploadFolderHandler,
412
- visible: () => { return this.showUploadFolder; },
413
- }],
414
- contextMenuModel: [{
415
- label: this.tr('filemanager.list.menu.open'),
416
- icon:'fa-regular fa-folder-open',
417
- disabled: () => { return this.selectedCount > 1; },
418
- action: () => { this.onItemActivated(this.getSelected()[0]); }
419
- }, {
420
- separator:true,
421
- visible: () => { return this.showDownload || this.showShare || this.showCopy || this.showCut || this.showPaste || this.showSelectAll; },
422
- }, {
423
- label: this.tr('filemanager.list.menu.download'),
424
- icon:'fa-solid fa-download',
425
- action: this.onItemDownload,
426
- visible: () => { return this.showDownload; },
427
- disabled: () => { return this.multiDownload ? this.selectedCount === 0 : this.selectedCount !== 1; }
428
- }, {
429
- label: this.tr('filemanager.list.menu.share'),
430
- icon:'fa-solid fa-share-nodes',
431
- action: this.onItemShare,
432
- disabled: () => { return this.selectedCount > 1; },
433
- visible: () => { return this.showShare; },
434
- }, {
435
- label: this.tr('filemanager.list.menu.copy'),
436
- icon:'fa-regular fa-copy',
437
- action: this.onItemsCopy,
438
- visible: () => { return this.showCopy; },
439
- }, {
440
- label: this.tr('filemanager.list.menu.cut'),
441
- icon:'fa-solid fa-scissors',
442
- action: this.onItemsCut,
443
- visible: () => { return this.showCut; },
444
- }, {
445
- label: this.tr('filemanager.list.menu.paste'),
446
- icon:'fa-regular fa-paste',
447
- visible: () => { return this.showPaste; },
448
- disabled: () => { return !(this.focusItem && this.focusItem.isDirectory) || this.selectedCount > 1 || !this.clipboard.files || !this.clipboard.files.length; },
449
- action: () => { this.pasteHandler(this.clipboard.action, this.clipboard.files, this.focusItem); }
450
- }, {
451
- label: this.tr('filemanager.list.menu.selectAll'),
452
- icon:'fa-solid fa-check-double',
453
- visible: () => { return this.showSelectAll; },
454
- action: this.onSelectAll
455
- }, {
456
- label: this.tr('filemanager.list.menu.rename'),
457
- icon:'fa-regular fa-pen-to-square',
458
- action: this.onItemRenameBegin,
459
- visible: () => { return this.showRename; },
460
- disabled: () => { return !this.editable || this.selectedCount > 1; }
461
- }, {
462
- label: this.tr('filemanager.list.menu.chown'),
463
- icon:'fa-solid fa-user-pen',
464
- action: this.onItemsChangeOwner,
465
- visible: () => { return this.showOwner; },
466
- }, {
467
- label: this.tr('filemanager.list.menu.extract'),
468
- action: this.onItemExtract,
469
- visible: () => {
470
- if (!this.showExtract) return false;
471
-
472
- const file = this.getSelected()[0];
473
- return !(this.selectedCount !== 1 || !(
474
- file.name.match(/\.tgz$/) ||
475
- file.name.match(/\.tar$/) ||
476
- file.name.match(/\.7z$/) ||
477
- file.name.match(/\.zip$/) ||
478
- file.name.match(/\.tar\.gz$/) ||
479
- file.name.match(/\.tar\.xz$/) ||
480
- file.name.match(/\.tar\.bz2$/)));
481
- }
482
- }, {
483
- separator:true,
484
- visible: () => { return this.showDelete; },
485
- }, {
486
- label: this.tr('filemanager.list.menu.delete'),
487
- icon:'fa-regular fa-trash-can',
488
- action: () => { this.deleteHandler(this.getSelected()); },
489
- visible: () => { return this.showDelete; },
490
- }],
491
- };
492
- },
493
- methods: {
494
- highlight(item) {
495
- const id = item.id;
496
-
497
- if (!this.elements[id]) return;
498
-
499
- this.elements[id].highlight();
500
- this.elements[id].$el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
501
- },
502
- highlightByName(name) {
503
- const tmp = this.items.find((i) => i.name === name);
504
- if (!tmp) return;
335
+ });
336
+
337
+ const emit = defineEmits(['selectionChanged', 'item-activated']);
338
+
339
+ const gridBody = useTemplateRef('gridBody');
340
+ const contextMenu = useTemplateRef('contextMenu');
341
+ const contextMenuBody = useTemplateRef('contextMenuBody');
342
+ const chownDialog = useTemplateRef('chownDialog');
343
+
344
+ const _alreadyActivating = ref(false);
345
+ const elements = reactive({});
346
+ const focusItem = ref(null);
347
+ const focusItemIndex = ref(-1);
348
+ const selectedCount = ref(0);
349
+ const dropTargetActive = ref(false);
350
+ const sortBy = ref(SORT_BY.NAME);
351
+ const sortDirection = ref(SORT_DIRECTION.ASC);
352
+ const clipboard = reactive({
353
+ action: '',
354
+ files: [],
355
+ });
356
+ const changeOwnerDialog = reactive({
357
+ busy: false,
358
+ ownerId: '',
359
+ items: [],
360
+ });
361
+
362
+ function setItemRef(id, el) {
363
+ if (el) {
364
+ elements[id] = el;
365
+ } else {
366
+ delete elements[id];
367
+ }
368
+ }
505
369
 
506
- this.highlight(tmp);
507
- },
508
- toggleSort(sortBy) {
509
- if (this.sortBy === sortBy) this.sortDirection = this.sortDirection === SORT_DIRECTION.ASC ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC;
510
- else this.sortBy = sortBy;
511
- },
512
- onDrop(targetItem, event) {
513
- this.dropTargetActive = false;
370
+ function scrollItemIntoView(componentRef) {
371
+ componentRef?.$el?.scrollIntoView?.({ behavior: 'smooth', block: 'nearest' });
372
+ }
514
373
 
515
- const targetItemName = targetItem ? targetItem.name : '';
374
+ function getSelected() {
375
+ return rawSelected;
376
+ }
516
377
 
517
- if (event.dataTransfer.getData('application/x-pankow') === 'selected') {
518
- // can't drop selected files on same folder
519
- if (!targetItemName) return;
378
+ function invalidateSelected() {
379
+ rawSelected = props.items.filter((item) => item.selected).slice();
380
+ selectedCount.value = rawSelected.length;
381
+ emit('selectionChanged', rawSelected);
382
+ }
520
383
 
521
- this.dropHandler(targetItemName, null, this.getSelected());
522
- } else if (event.dataTransfer && event.dataTransfer.items[0]) {
523
- // this is dropping files from outside the browser
524
- this.dropHandler(targetItemName, event.dataTransfer, null);
525
- }
526
- },
527
- onCanDropHandler(item) {
528
- if (!item.isDirectory) return false;
529
- return !item.selected;
530
- },
531
- onDragOver(event) {
532
- if (!event.dataTransfer) return;
533
-
534
- if (event.dataTransfer.getData('application/x-pankow') === 'selected') {
535
- this.dropTargetActive = false;
536
- } else {
537
- event.dataTransfer.dropEffect = 'copy';
538
- this.dropTargetActive = true;
539
- }
540
- },
541
- onDragExit(event) {
542
- this.dropTargetActive = false;
543
- },
544
- onItemDragStart(event, item) {
545
- // auto-select the dragged item if it is not already part of the selection
546
- if (item && !item.selected) {
547
- this.onSelectAndFocus(item, {}, false);
548
- }
384
+ function highlight(item) {
385
+ const id = item.id;
386
+ if (!elements[id]) return;
549
387
 
550
- const dragHandle = document.getElementsByClassName('drag-handle')[0];
388
+ elements[id].highlight();
389
+ scrollItemIntoView(elements[id]);
390
+ }
551
391
 
552
- // clear element
553
- dragHandle.replaceChildren();
392
+ function highlightByName(name) {
393
+ const tmp = props.items.find((i) => i.name === name);
394
+ if (!tmp) return;
554
395
 
555
- for (let i = 0; i < Math.min(this.selectedCount, 6); ++i) {
556
- const dragHandleItem = document.createElement('div');
557
- dragHandleItem.style.opacity = 1-((i*2)/10);
558
- const dragHandleItemIcon = document.createElement('img');
559
- dragHandleItemIcon.src = rawSelected[i].previewUrl || rawSelected[i].icon
560
- dragHandleItem.append(dragHandleItemIcon, rawSelected[i].name);
561
- dragHandle.append(dragHandleItem);
562
- }
396
+ highlight(tmp);
397
+ }
563
398
 
564
- event.dataTransfer.dropEffect = 'move'
565
- event.dataTransfer.setData('application/x-pankow', 'selected');
566
- event.dataTransfer.setDragImage(dragHandle, 0, 0);
567
- },
568
- getGridColumns() {
569
- const el = this.$refs.gridBody;
570
- if (this.viewMode !== 'grid' || !el || !el.children.length) return 1;
571
- const firstRowTop = el.children[0].getBoundingClientRect().top;
572
- const tolerance = 2;
573
- let cols = 0;
574
- for (const child of el.children) {
575
- if (Math.abs(child.getBoundingClientRect().top - firstRowTop) <= tolerance) cols++;
576
- else break;
577
- }
578
- return cols || 1;
579
- },
580
- onKeyUp(event) {
581
- const step = this.viewMode === 'grid' ? this.getGridColumns() : 1;
582
- this.onSelectAndFocusIndex(this.focusItemIndex - step, event.shiftKey);
583
- },
584
- onKeyDown(event) {
585
- const step = this.viewMode === 'grid' ? this.getGridColumns() : 1;
586
- this.onSelectAndFocusIndex(this.focusItemIndex + step, event.shiftKey);
587
- },
588
- onKeyLeft(event) {
589
- if (this.viewMode !== 'grid') return;
590
- this.onSelectAndFocusIndex(this.focusItemIndex - 1, event.shiftKey);
591
- },
592
- onKeyRight(event) {
593
- if (this.viewMode !== 'grid') return;
594
- this.onSelectAndFocusIndex(this.focusItemIndex + 1, event.shiftKey);
595
- },
596
- onKeyEnter(event) {
597
- if (this.focusItem) this.onItemActivated(this.focusItem);
598
- },
599
- async onKeyDelete(event) {
600
- if (event.key !== 'Delete') return; // triggered also by backspace
601
- if (!this.focusItem) return;
399
+ function toggleSort(by) {
400
+ if (sortBy.value === by) {
401
+ sortDirection.value = sortDirection.value === SORT_DIRECTION.ASC ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC;
402
+ } else {
403
+ sortBy.value = by;
404
+ }
405
+ }
602
406
 
603
- await this.deleteHandler(this.getSelected());
604
- },
605
- onKeyEscape(event) {
606
- this.onUnselectAll();
607
- },
608
- onKeyEvent(event) {
609
- if (event.key === 'c' && (event.ctrlKey || event.metaKey)) {
610
- this.onItemsCopy();
611
- event.preventDefault();
612
- } else if (event.key === 'v' && (event.ctrlKey || event.metaKey)) {
613
- this.pasteHandler(this.clipboard.action, this.clipboard.files);
614
- event.preventDefault();
615
- } else if (event.key === 'x' && (event.ctrlKey || event.metaKey)) {
616
- this.onItemsCut();
617
- event.preventDefault();
618
- } else if (event.key === 'a' && (event.ctrlKey || event.metaKey)) {
619
- this.onSelectAll();
620
- event.preventDefault();
621
- } else if (event.key === 'End') {
622
- if (event.shiftKey) this.onSelectAndFocusToIndex(this.items.length-1);
623
- else this.onSelectAndFocusIndex(this.items.length-1, event.shiftKey);
624
- event.preventDefault();
625
- } else if (event.key === 'Home') {
626
- if (event.shiftKey) this.onSelectAndFocusToIndex(0);
627
- else this.onSelectAndFocusIndex(0, event.shiftKey);
628
- event.preventDefault();
629
- } else if (event.key === 'PageUp') {
630
- const pageItemCount = parseInt(event.target.clientHeight / 55);
631
- if (event.shiftKey) this.onSelectAndFocusToIndex(this.focusItemIndex-pageItemCount);
632
- else this.onSelectAndFocusIndex(this.focusItemIndex-pageItemCount, event.shiftKey);
633
- event.preventDefault();
634
- } else if (event.key === 'PageDown') {
635
- const pageItemCount = parseInt(event.target.clientHeight / 55);
636
- if (event.shiftKey) this.onSelectAndFocusToIndex(this.focusItemIndex+pageItemCount);
637
- else this.onSelectAndFocusIndex(this.focusItemIndex+pageItemCount, event.shiftKey);
638
- event.preventDefault();
639
- }
640
- },
641
- onContextMenu(item, event) {
642
- this.$refs.contextMenu.open(event);
643
- this.onSelectAndFocus(item, event, false);
644
- event.stopPropagation();
645
- },
646
- onContextMenuBody(event) {
647
- this.$refs.contextMenuBody.open(event);
648
- this.onUnselectAll();
649
- },
650
- onItemActivated(item) {
651
- // prevent concurrent activations for some time
652
- if (this._alreadyActivating) return;
653
- this._alreadyActivating = true;
654
- setTimeout(() => { this._alreadyActivating = false; }, 500);
407
+ function onDrop(targetItem, event) {
408
+ dropTargetActive.value = false;
655
409
 
656
- this.$emit('item-activated', item);
657
- },
658
- onItemsChangeOwner(event) {
659
- if (!this.focusItem) return console.warn('onItemsChangeOwner should only be triggered if at least one item is selected');
410
+ const targetItemName = targetItem ? targetItem.name : '';
660
411
 
661
- this.changeOwnerDialog.items = this.getSelected();
662
- this.changeOwnerDialog.ownerId = this.focusItem.uid;
663
- this.$refs.chownDialog.open();
664
- },
665
- onItemDownload(event) {
666
- if (!this.focusItem) return console.warn('onItemDownload should only be triggered if at lesst one item is selected');
412
+ if (event.dataTransfer.getData('application/x-pankow') === 'selected') {
413
+ if (!targetItemName) return;
667
414
 
668
- this.downloadHandler(this.multiDownload ? this.getSelected() : this.focusItem);
669
- },
670
- onItemExtract(event) {
671
- if (this.selectedCount !== 1) return console.warn('onItemExtract should only be triggered if one item is selected');
415
+ props.dropHandler(targetItemName, null, getSelected());
416
+ } else if (event.dataTransfer && event.dataTransfer.items[0]) {
417
+ props.dropHandler(targetItemName, event.dataTransfer, null);
418
+ }
419
+ }
672
420
 
673
- this.extractHandler(this.focusItem);
674
- },
675
- onItemsCopy(event) {
676
- if (!this.focusItem) return console.warn('onItemsCopy should only be triggered if at least one item is selected');
421
+ function onCanDropHandler(item) {
422
+ if (!item.isDirectory) return false;
423
+ return !item.selected;
424
+ }
677
425
 
678
- this.clipboard.action = 'copy';
679
- this.clipboard.files = this.getSelected();
680
- },
681
- onItemsCut(event) {
682
- if (!this.focusItem) return console.warn('onItemsCut should only be triggered if at least one item is selected');
426
+ function onDragOver(event) {
427
+ if (!event.dataTransfer) return;
683
428
 
684
- this.clipboard.action = 'cut';
685
- this.clipboard.files = this.getSelected();
686
- },
687
- onItemShare(event) {
688
- if (this.selectedCount !== 1) return console.warn('onItemRenameBegin should only be triggered if one item is selected');
429
+ if (event.dataTransfer.getData('application/x-pankow') === 'selected') {
430
+ dropTargetActive.value = false;
431
+ } else {
432
+ event.dataTransfer.dropEffect = 'copy';
433
+ dropTargetActive.value = true;
434
+ }
435
+ }
689
436
 
690
- this.shareHandler(this.focusItem);
691
- },
692
- onItemRenameBegin(event) {
693
- if (this.selectedCount !== 1) return console.warn('onItemRenameBegin should only be triggered if one item is selected');
437
+ function onDragExit() {
438
+ dropTargetActive.value = false;
439
+ }
694
440
 
695
- this.elements[this.focusItem.id].onRenameBegin();
696
- },
697
- async onItemRenameSubmit(item, newName) {
698
- await this.renameHandler(item, newName);
699
- },
700
- async onChangeOwnerDialogSubmit(items, newOwnerUid) {
701
- this.changeOwnerDialog.busy = true;
702
- await this.changeOwnerHandler(items, newOwnerUid);
703
- this.changeOwnerDialog.visible = false;
704
- this.changeOwnerDialog.items = [];
705
- this.changeOwnerDialog.ownerId = this.ownersModel[0].uid;
706
- this.changeOwnerDialog.busy = false;
707
- this.$refs.chownDialog.close();
708
- },
709
- getSelected() {
710
- return rawSelected;
711
- },
712
- invalidateSelected() {
713
- rawSelected = this.items.filter((item) => item.selected).slice();
714
- this.selectedCount = rawSelected.length;
715
- this.$emit('selectionChanged', rawSelected);
716
- },
717
- onSelectAll() {
718
- for (const item of this.items) item.selected = true;
719
- if (!this.focusItem) this.setFocus(this.focusItemIndex);
441
+ function onItemDragStart(event, item) {
442
+ if (item && !item.selected) {
443
+ onSelectAndFocus(item, {}, false);
444
+ }
720
445
 
721
- this.invalidateSelected();
722
- },
723
- onUnselectAll() {
724
- for (const item of this.items) item.selected = false;
725
- if (this.focusItem) this.focusItem = null;
446
+ const dragHandle = document.getElementsByClassName('drag-handle')[0];
726
447
 
727
- this.invalidateSelected();
728
- },
729
- setFocus(index) {
730
- if (this.items[this.focusItemIndex]) this.items[this.focusItemIndex].focused = false;
731
- if (this.focusItem) this.focusItem.focused = false;
732
- if (!this.items[index]) return;
733
-
734
- this.focusItem = this.items[index];
735
- this.focusItem.focused = true;
736
- this.focusItemIndex = index;
737
- },
738
- onSelectAndFocusToIndex(index) {
739
- index = (index < 0) ? 0 : (index > this.items.length-1 ? this.items.length-1 : index); // keep in bounds
448
+ dragHandle.replaceChildren();
740
449
 
741
- this.setFocus(index);
450
+ for (let i = 0; i < Math.min(selectedCount.value, 6); ++i) {
451
+ const dragHandleItem = document.createElement('div');
452
+ dragHandleItem.style.opacity = 1 - ((i * 2) / 10);
453
+ const dragHandleItemIcon = document.createElement('img');
454
+ dragHandleItemIcon.src = rawSelected[i].previewUrl || rawSelected[i].icon;
455
+ dragHandleItem.append(dragHandleItemIcon, rawSelected[i].name);
456
+ dragHandle.append(dragHandleItem);
457
+ }
742
458
 
743
- const item = this.items[index];
744
- const itemIndex = this.items.findIndex(i => i.id === item.id);
745
- const firstSelectedIndex = this.items.findIndex(item => item.selected);
746
- const lastSelectedIndex = this.items.findLastIndex(item => item.selected);
459
+ event.dataTransfer.dropEffect = 'move';
460
+ event.dataTransfer.setData('application/x-pankow', 'selected');
461
+ event.dataTransfer.setDragImage(dragHandle, 0, 0);
462
+ }
747
463
 
748
- if (index < firstSelectedIndex) for (let i = index; i < firstSelectedIndex; ++i) this.items[i].selected = true;
749
- else if (index > lastSelectedIndex) for (let i = lastSelectedIndex; i <= index; ++i) this.items[i].selected = true;
464
+ function getGridColumns() {
465
+ const el = gridBody.value;
466
+ if (props.viewMode !== 'grid' || !el || !el.children.length) return 1;
467
+ const firstRowTop = el.children[0].getBoundingClientRect().top;
468
+ const tolerance = 2;
469
+ let cols = 0;
470
+ for (const child of el.children) {
471
+ if (Math.abs(child.getBoundingClientRect().top - firstRowTop) <= tolerance) cols++;
472
+ else break;
473
+ }
474
+ return cols || 1;
475
+ }
750
476
 
751
- if (this.elements[item.id]) this.elements[item.id].$el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
477
+ function onKeyUp(event) {
478
+ const step = props.viewMode === 'grid' ? getGridColumns() : 1;
479
+ onSelectAndFocusIndex(focusItemIndex.value - step, event.shiftKey);
480
+ }
752
481
 
753
- this.invalidateSelected();
754
- },
755
- onSelectAndFocusIndex(index, keepSelected = false) {
756
- index = (index < 0) ? 0 : (index > this.items.length-1 ? this.items.length-1 : index); // keep in bounds
482
+ function onKeyDown(event) {
483
+ const step = props.viewMode === 'grid' ? getGridColumns() : 1;
484
+ onSelectAndFocusIndex(focusItemIndex.value + step, event.shiftKey);
485
+ }
486
+
487
+ function onKeyLeft(event) {
488
+ if (props.viewMode !== 'grid') return;
489
+ onSelectAndFocusIndex(focusItemIndex.value - 1, event.shiftKey);
490
+ }
491
+
492
+ function onKeyRight(event) {
493
+ if (props.viewMode !== 'grid') return;
494
+ onSelectAndFocusIndex(focusItemIndex.value + 1, event.shiftKey);
495
+ }
496
+
497
+ function onKeyEnter() {
498
+ if (focusItem.value) onItemActivated(focusItem.value);
499
+ }
500
+
501
+ async function onKeyDelete(event) {
502
+ if (event.key !== 'Delete') return;
503
+ if (!focusItem.value) return;
504
+
505
+ await props.deleteHandler(getSelected());
506
+ }
507
+
508
+ function onKeyEscape() {
509
+ onUnselectAll();
510
+ }
511
+
512
+ function onKeyEvent(event) {
513
+ if (event.key === 'c' && (event.ctrlKey || event.metaKey)) {
514
+ onItemsCopy();
515
+ event.preventDefault();
516
+ } else if (event.key === 'v' && (event.ctrlKey || event.metaKey)) {
517
+ props.pasteHandler(clipboard.action, clipboard.files);
518
+ event.preventDefault();
519
+ } else if (event.key === 'x' && (event.ctrlKey || event.metaKey)) {
520
+ onItemsCut();
521
+ event.preventDefault();
522
+ } else if (event.key === 'a' && (event.ctrlKey || event.metaKey)) {
523
+ onSelectAll();
524
+ event.preventDefault();
525
+ } else if (event.key === 'End') {
526
+ if (event.shiftKey) onSelectAndFocusToIndex(props.items.length - 1);
527
+ else onSelectAndFocusIndex(props.items.length - 1, event.shiftKey);
528
+ event.preventDefault();
529
+ } else if (event.key === 'Home') {
530
+ if (event.shiftKey) onSelectAndFocusToIndex(0);
531
+ else onSelectAndFocusIndex(0, event.shiftKey);
532
+ event.preventDefault();
533
+ } else if (event.key === 'PageUp') {
534
+ const pageItemCount = parseInt(event.target.clientHeight / 55, 10);
535
+ if (event.shiftKey) onSelectAndFocusToIndex(focusItemIndex.value - pageItemCount);
536
+ else onSelectAndFocusIndex(focusItemIndex.value - pageItemCount, event.shiftKey);
537
+ event.preventDefault();
538
+ } else if (event.key === 'PageDown') {
539
+ const pageItemCount = parseInt(event.target.clientHeight / 55, 10);
540
+ if (event.shiftKey) onSelectAndFocusToIndex(focusItemIndex.value + pageItemCount);
541
+ else onSelectAndFocusIndex(focusItemIndex.value + pageItemCount, event.shiftKey);
542
+ event.preventDefault();
543
+ }
544
+ }
545
+
546
+ function onContextMenu(item, event) {
547
+ contextMenu.value?.open(event);
548
+ onSelectAndFocus(item, event, false);
549
+ event.stopPropagation();
550
+ }
551
+
552
+ function onContextMenuBody(event) {
553
+ contextMenuBody.value?.open(event);
554
+ onUnselectAll();
555
+ }
556
+
557
+ function onItemActivated(item) {
558
+ if (_alreadyActivating.value) return;
559
+ _alreadyActivating.value = true;
560
+ setTimeout(() => {
561
+ _alreadyActivating.value = false;
562
+ }, 500);
563
+
564
+ emit('item-activated', item);
565
+ }
566
+
567
+ function onItemsChangeOwner() {
568
+ if (!focusItem.value) return console.warn('onItemsChangeOwner should only be triggered if at least one item is selected');
569
+
570
+ changeOwnerDialog.items = getSelected();
571
+ changeOwnerDialog.ownerId = focusItem.value.uid;
572
+ chownDialog.value?.open();
573
+ }
574
+
575
+ function onItemDownload() {
576
+ if (!focusItem.value) return console.warn('onItemDownload should only be triggered if at lesst one item is selected');
577
+
578
+ props.downloadHandler(props.multiDownload ? getSelected() : focusItem.value);
579
+ }
580
+
581
+ function onItemExtract() {
582
+ if (selectedCount.value !== 1) return console.warn('onItemExtract should only be triggered if one item is selected');
583
+
584
+ props.extractHandler(focusItem.value);
585
+ }
586
+
587
+ function onItemsCopy() {
588
+ if (!focusItem.value) return console.warn('onItemsCopy should only be triggered if at least one item is selected');
589
+
590
+ clipboard.action = 'copy';
591
+ clipboard.files = getSelected();
592
+ }
593
+
594
+ function onItemsCut() {
595
+ if (!focusItem.value) return console.warn('onItemsCut should only be triggered if at least one item is selected');
596
+
597
+ clipboard.action = 'cut';
598
+ clipboard.files = getSelected();
599
+ }
600
+
601
+ function onItemShare() {
602
+ if (selectedCount.value !== 1) return console.warn('onItemRenameBegin should only be triggered if one item is selected');
603
+
604
+ props.shareHandler(focusItem.value);
605
+ }
606
+
607
+ function onItemRenameBegin() {
608
+ if (selectedCount.value !== 1) return console.warn('onItemRenameBegin should only be triggered if one item is selected');
609
+
610
+ elements[focusItem.value.id]?.onRenameBegin();
611
+ }
612
+
613
+ async function onChangeOwnerDialogSubmit(items, newOwnerUid) {
614
+ changeOwnerDialog.busy = true;
615
+ await props.changeOwnerHandler(items, newOwnerUid);
616
+ changeOwnerDialog.visible = false;
617
+ changeOwnerDialog.items = [];
618
+ changeOwnerDialog.ownerId = props.ownersModel[0].uid;
619
+ changeOwnerDialog.busy = false;
620
+ chownDialog.value?.close();
621
+ }
622
+
623
+ function onSelectAll() {
624
+ for (const item of props.items) item.selected = true;
625
+ if (!focusItem.value) setFocus(focusItemIndex.value);
626
+
627
+ invalidateSelected();
628
+ }
757
629
 
758
- const item = this.items[index];
630
+ function onUnselectAll() {
631
+ for (const item of props.items) item.selected = false;
632
+ if (focusItem.value) focusItem.value = null;
759
633
 
760
- this.onSelectAndFocus(item, {}, keepSelected);
634
+ invalidateSelected();
635
+ }
636
+
637
+ function setFocus(index) {
638
+ if (props.items[focusItemIndex.value]) props.items[focusItemIndex.value].focused = false;
639
+ if (focusItem.value) focusItem.value.focused = false;
640
+ if (!props.items[index]) return;
641
+
642
+ focusItem.value = props.items[index];
643
+ focusItem.value.focused = true;
644
+ focusItemIndex.value = index;
645
+ }
646
+
647
+ function onSelectAndFocusToIndex(index) {
648
+ index = index < 0 ? 0 : index > props.items.length - 1 ? props.items.length - 1 : index;
649
+
650
+ setFocus(index);
651
+
652
+ const item = props.items[index];
653
+ const firstSelectedIndex = props.items.findIndex((i) => i.selected);
654
+ const lastSelectedIndex = props.items.findLastIndex((i) => i.selected);
655
+
656
+ if (index < firstSelectedIndex) for (let i = index; i < firstSelectedIndex; ++i) props.items[i].selected = true;
657
+ else if (index > lastSelectedIndex) for (let i = lastSelectedIndex; i <= index; ++i) props.items[i].selected = true;
658
+
659
+ scrollItemIntoView(elements[item.id]);
660
+
661
+ invalidateSelected();
662
+ }
663
+
664
+ function onSelectAndFocusIndex(index, keepSelected = false) {
665
+ index = index < 0 ? 0 : index > props.items.length - 1 ? props.items.length - 1 : index;
666
+
667
+ const item = props.items[index];
668
+
669
+ onSelectAndFocus(item, {}, keepSelected);
670
+ }
671
+
672
+ function onSelectAndFocus(item, event, keepSelected = false) {
673
+ const itemIndex = props.items.findIndex((i) => i.id === item.id);
674
+ setFocus(itemIndex);
675
+
676
+ if (event.button === 2 && item.selected) keepSelected = true;
677
+
678
+ if (event.ctrlKey) {
679
+ if (keepSelected) item.selected = true;
680
+ else item.selected = !item.selected;
681
+ } else if (event.shiftKey) {
682
+ const firstSelectedIndex = props.items.findIndex((i) => i.selected);
683
+ const lastSelectedIndex = props.items.findLastIndex((i) => i.selected);
684
+
685
+ if (itemIndex < firstSelectedIndex) for (let i = itemIndex; i < firstSelectedIndex; ++i) props.items[i].selected = true;
686
+ else if (itemIndex > lastSelectedIndex) for (let i = lastSelectedIndex; i <= itemIndex; ++i) props.items[i].selected = true;
687
+ } else {
688
+ if (!keepSelected) for (const it of props.items) it.selected = false;
689
+ item.selected = true;
690
+ }
691
+
692
+ scrollItemIntoView(elements[item.id]);
693
+
694
+ invalidateSelected();
695
+ }
696
+
697
+ const contextMenuBodyModel = [
698
+ {
699
+ label: props.tr('filemanager.list.menu.paste'),
700
+ icon: 'fa-regular fa-paste',
701
+ disabled: () => !clipboard.files || !clipboard.files.length,
702
+ action: () => {
703
+ props.pasteHandler(clipboard.action, clipboard.files, null);
761
704
  },
762
- onSelectAndFocus(item, event, keepSelected = false) {
763
- const itemIndex = this.items.findIndex(i => i.id === item.id);
764
- this.setFocus(itemIndex);
765
-
766
- // do not toggle selection if this is from right click
767
- if (event.button === 2 && item.selected) keepSelected = true;
768
-
769
- if (event.ctrlKey) {
770
- if (keepSelected) item.selected = true;
771
- else item.selected = !item.selected;
772
- } else if (event.shiftKey) {
773
- const firstSelectedIndex = this.items.findIndex(item => item.selected);
774
- const lastSelectedIndex = this.items.findLastIndex(item => item.selected);
775
-
776
- if (itemIndex < firstSelectedIndex) for (let i = itemIndex; i < firstSelectedIndex; ++i) this.items[i].selected = true;
777
- else if (itemIndex > lastSelectedIndex) for (let i = lastSelectedIndex; i <= itemIndex; ++i) this.items[i].selected = true;
778
- } else {
779
- if (!keepSelected) for (const item of this.items) item.selected = false;
780
- item.selected = true;
781
- }
705
+ },
706
+ {
707
+ label: props.tr('filemanager.list.menu.selectAll'),
708
+ icon: 'fa-solid fa-check-double',
709
+ action: onSelectAll,
710
+ },
711
+ {
712
+ separator: true,
713
+ visible: () => typeof props.refreshHandler === 'function',
714
+ },
715
+ {
716
+ label: props.tr('filemanager.toolbar.refresh'),
717
+ icon: 'fa-solid fa-arrow-rotate-right',
718
+ visible: () => typeof props.refreshHandler === 'function',
719
+ action: props.refreshHandler,
720
+ },
721
+ {
722
+ separator: true,
723
+ visible: () => props.showNewFile || props.showNewFolder,
724
+ },
725
+ {
726
+ label: props.tr('filemanager.toolbar.newFile'),
727
+ icon: 'fa-solid fa-file-circle-plus',
728
+ action: props.newFileHandler,
729
+ visible: () => props.showNewFile,
730
+ },
731
+ {
732
+ label: props.tr('filemanager.toolbar.newFolder'),
733
+ icon: 'fa-solid fa-folder-plus',
734
+ action: props.newFolderHandler,
735
+ visible: () => props.showNewFolder,
736
+ },
737
+ {
738
+ separator: true,
739
+ visible: () => props.showUploadFile || props.showUploadFolder,
740
+ },
741
+ {
742
+ label: props.tr('filemanager.toolbar.uploadFile'),
743
+ icon: 'fa-solid fa-file-arrow-up',
744
+ action: props.uploadFileHandler,
745
+ visible: () => props.showUploadFile,
746
+ },
747
+ {
748
+ label: props.tr('filemanager.toolbar.uploadFolder'),
749
+ icon: 'fa-regular fa-folder-open',
750
+ action: props.uploadFolderHandler,
751
+ visible: () => props.showUploadFolder,
752
+ },
753
+ ];
782
754
 
783
- if (this.elements[item.id]) this.elements[item.id].$el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
755
+ const contextMenuModel = [
756
+ {
757
+ label: props.tr('filemanager.list.menu.open'),
758
+ icon: 'fa-regular fa-folder-open',
759
+ disabled: () => selectedCount.value > 1,
760
+ action: () => {
761
+ onItemActivated(getSelected()[0]);
762
+ },
763
+ },
764
+ {
765
+ separator: true,
766
+ visible: () =>
767
+ props.showDownload ||
768
+ props.showShare ||
769
+ props.showCopy ||
770
+ props.showCut ||
771
+ props.showPaste ||
772
+ props.showSelectAll,
773
+ },
774
+ {
775
+ label: props.tr('filemanager.list.menu.download'),
776
+ icon: 'fa-solid fa-download',
777
+ action: onItemDownload,
778
+ visible: () => props.showDownload,
779
+ disabled: () => (props.multiDownload ? selectedCount.value === 0 : selectedCount.value !== 1),
780
+ },
781
+ {
782
+ label: props.tr('filemanager.list.menu.share'),
783
+ icon: 'fa-solid fa-share-nodes',
784
+ action: onItemShare,
785
+ disabled: () => selectedCount.value > 1,
786
+ visible: () => props.showShare,
787
+ },
788
+ {
789
+ label: props.tr('filemanager.list.menu.copy'),
790
+ icon: 'fa-regular fa-copy',
791
+ action: onItemsCopy,
792
+ visible: () => props.showCopy,
793
+ },
794
+ {
795
+ label: props.tr('filemanager.list.menu.cut'),
796
+ icon: 'fa-solid fa-scissors',
797
+ action: onItemsCut,
798
+ visible: () => props.showCut,
799
+ },
800
+ {
801
+ label: props.tr('filemanager.list.menu.paste'),
802
+ icon: 'fa-regular fa-paste',
803
+ visible: () => props.showPaste,
804
+ disabled: () =>
805
+ !(focusItem.value && focusItem.value.isDirectory) ||
806
+ selectedCount.value > 1 ||
807
+ !clipboard.files ||
808
+ !clipboard.files.length,
809
+ action: () => {
810
+ props.pasteHandler(clipboard.action, clipboard.files, focusItem.value);
811
+ },
812
+ },
813
+ {
814
+ label: props.tr('filemanager.list.menu.selectAll'),
815
+ icon: 'fa-solid fa-check-double',
816
+ visible: () => props.showSelectAll,
817
+ action: onSelectAll,
818
+ },
819
+ {
820
+ label: props.tr('filemanager.list.menu.rename'),
821
+ icon: 'fa-regular fa-pen-to-square',
822
+ action: onItemRenameBegin,
823
+ visible: () => props.showRename,
824
+ disabled: () => !props.editable || selectedCount.value > 1,
825
+ },
826
+ {
827
+ label: props.tr('filemanager.list.menu.chown'),
828
+ icon: 'fa-solid fa-user-pen',
829
+ action: onItemsChangeOwner,
830
+ visible: () => props.showOwner,
831
+ },
832
+ {
833
+ label: props.tr('filemanager.list.menu.extract'),
834
+ action: onItemExtract,
835
+ visible: () => {
836
+ if (!props.showExtract) return false;
837
+
838
+ const file = getSelected()[0];
839
+ return !(
840
+ selectedCount.value !== 1 ||
841
+ !file ||
842
+ !(
843
+ file.name.match(/\.tgz$/) ||
844
+ file.name.match(/\.tar$/) ||
845
+ file.name.match(/\.7z$/) ||
846
+ file.name.match(/\.zip$/) ||
847
+ file.name.match(/\.tar\.gz$/) ||
848
+ file.name.match(/\.tar\.xz$/) ||
849
+ file.name.match(/\.tar\.bz2$/)
850
+ )
851
+ );
852
+ },
853
+ },
854
+ {
855
+ separator: true,
856
+ visible: () => props.showDelete,
857
+ },
858
+ {
859
+ label: props.tr('filemanager.list.menu.delete'),
860
+ icon: 'fa-regular fa-trash-can',
861
+ action: () => {
862
+ props.deleteHandler(getSelected());
863
+ },
864
+ visible: () => props.showDelete,
865
+ },
866
+ ];
867
+
868
+ const filteredSortedItems = computed(() => {
869
+ invalidateSelected();
870
+
871
+ return props.items.sort((a, b) => {
872
+ if (a.isDirectory && !b.isDirectory) return -1;
873
+ if (!a.isDirectory && b.isDirectory) return 1;
874
+
875
+ let sort = 0;
876
+ if (sortBy.value === SORT_BY.NAME) {
877
+ if (a.name.toLowerCase() < b.name.toLowerCase()) sort = -1;
878
+ else if (a.name.toLowerCase() > b.name.toLowerCase()) sort = 1;
879
+ } else if (sortBy.value === SORT_BY.OWNER) {
880
+ if (a.owner.toLowerCase() < b.owner.toLowerCase()) sort = -1;
881
+ else if (a.owner.toLowerCase() > b.owner.toLowerCase()) sort = 1;
882
+ } else if (sortBy.value === SORT_BY.STAR) {
883
+ if (a.star && !b.star) sort = -1;
884
+ if (!a.star && b.star) sort = 1;
885
+ } else if (sortBy.value === SORT_BY.SIZE) {
886
+ if (a.size < b.size) sort = -1;
887
+ if (a.size > b.size) sort = 1;
888
+ } else if (sortBy.value === SORT_BY.MTIME) {
889
+ if (a.mtime < b.mtime) sort = -1;
890
+ if (a.mtime > b.mtime) sort = 1;
891
+ } else {
892
+ console.error('unknown SORT_BY', sortBy.value);
893
+ }
784
894
 
785
- this.invalidateSelected();
895
+ if (sort === 0) {
896
+ if (a.id < b.id) sort = -1;
897
+ else sort = 1;
786
898
  }
787
- }
788
- };
789
899
 
790
- </script>
900
+ return (sort *= sortDirection.value === SORT_DIRECTION.DESC ? -1 : 1);
901
+ });
902
+ });
903
+
904
+ defineExpose({
905
+ highlight,
906
+ highlightByName,
907
+ });
791
908
 
909
+ </script>
792
910
  <style scoped>
793
911
 
794
912
  .directory-view {