@cloudron/pankow 4.1.1 → 4.1.3
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 +114 -26
- package/components/DirectoryViewGridItem.vue +363 -0
- package/components/Menu.vue +14 -1
- package/components/MenuItem.vue +20 -2
- package/package.json +3 -3
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
ref="main"
|
|
7
7
|
@keydown.up.prevent="onKeyUp($event)"
|
|
8
8
|
@keydown.down.prevent="onKeyDown($event)"
|
|
9
|
+
@keydown.left.prevent="onKeyLeft($event)"
|
|
10
|
+
@keydown.right.prevent="onKeyRight($event)"
|
|
9
11
|
@keydown.enter="onKeyEnter($event)"
|
|
10
12
|
@keydown.delete="onKeyDelete($event)"
|
|
11
13
|
@keydown.esc="onKeyEscape($event)"
|
|
@@ -28,7 +30,7 @@
|
|
|
28
30
|
</slot>
|
|
29
31
|
</div>
|
|
30
32
|
|
|
31
|
-
<div class="directory-view-header" v-
|
|
33
|
+
<div class="directory-view-header" v-if="items.length && viewMode === 'list'">
|
|
32
34
|
<div class="directory-view-header-icon"></div>
|
|
33
35
|
<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
36
|
<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>
|
|
@@ -37,32 +39,55 @@
|
|
|
37
39
|
<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
40
|
<div class="directory-view-header-actions"></div>
|
|
39
41
|
</div>
|
|
40
|
-
<div class="directory-view-body-container" v-show="items.length">
|
|
42
|
+
<div class="directory-view-body-container" :class="{ 'directory-view-body-container-full': viewMode !== 'list' }" v-show="items.length">
|
|
41
43
|
<div
|
|
44
|
+
ref="gridBody"
|
|
42
45
|
class="directory-view-body"
|
|
46
|
+
:class="{ 'directory-view-body-grid': viewMode === 'grid' }"
|
|
43
47
|
@contextmenu="onContextMenuBody($event)"
|
|
44
48
|
>
|
|
45
|
-
<
|
|
46
|
-
:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
49
|
+
<template v-if="viewMode === 'list'">
|
|
50
|
+
<DirectoryViewListItem v-for="item in filteredSortedItems" :ref="(el) => this.elements[item.id] = el"
|
|
51
|
+
:key="item.id"
|
|
52
|
+
:fallbackIcon="fallbackIcon"
|
|
53
|
+
:item="item"
|
|
54
|
+
:show-owner="showOwner"
|
|
55
|
+
:show-extract="showExtract"
|
|
56
|
+
:show-size="showSize"
|
|
57
|
+
:show-star="showStar"
|
|
58
|
+
:show-modified="showModified"
|
|
59
|
+
:rename-handler="renameHandler"
|
|
60
|
+
:share-indicator-property="shareIndicatorProperty"
|
|
61
|
+
:select-handler="onSelectAndFocus"
|
|
62
|
+
:drop-handler="onDrop"
|
|
63
|
+
:can-drop-handler="onCanDropHandler"
|
|
64
|
+
:star-handler="starHandler"
|
|
65
|
+
@contextmenu="onContextMenu(item, $event)"
|
|
66
|
+
@action-menu="onContextMenu"
|
|
67
|
+
@dblclick="onItemActivated(item)"
|
|
68
|
+
@activated="onItemActivated(item)"
|
|
69
|
+
@dragstart="onItemDragStart($event, item)"
|
|
70
|
+
/>
|
|
71
|
+
</template>
|
|
72
|
+
<template v-else-if="viewMode === 'grid'">
|
|
73
|
+
<DirectoryViewGridItem v-for="item in filteredSortedItems" :ref="(el) => this.elements[item.id] = el"
|
|
74
|
+
:key="item.id"
|
|
75
|
+
:fallbackIcon="fallbackIcon"
|
|
76
|
+
:item="item"
|
|
77
|
+
:show-star="showStar"
|
|
78
|
+
:rename-handler="renameHandler"
|
|
79
|
+
:share-indicator-property="shareIndicatorProperty"
|
|
80
|
+
:select-handler="onSelectAndFocus"
|
|
81
|
+
:drop-handler="onDrop"
|
|
82
|
+
:can-drop-handler="onCanDropHandler"
|
|
83
|
+
:star-handler="starHandler"
|
|
84
|
+
@contextmenu="onContextMenu(item, $event)"
|
|
85
|
+
@action-menu="onContextMenu"
|
|
86
|
+
@dblclick="onItemActivated(item)"
|
|
87
|
+
@activated="onItemActivated(item)"
|
|
88
|
+
@dragstart="onItemDragStart($event, item)"
|
|
89
|
+
/>
|
|
90
|
+
</template>
|
|
66
91
|
</div>
|
|
67
92
|
|
|
68
93
|
<div class="drag-handle"></div>
|
|
@@ -74,6 +99,7 @@
|
|
|
74
99
|
|
|
75
100
|
import SingleSelect from './SingleSelect.vue';
|
|
76
101
|
import DirectoryViewListItem from './DirectoryViewListItem.vue';
|
|
102
|
+
import DirectoryViewGridItem from './DirectoryViewGridItem.vue';
|
|
77
103
|
import Dialog from './Dialog.vue';
|
|
78
104
|
import Menu from './Menu.vue';
|
|
79
105
|
|
|
@@ -99,11 +125,19 @@ export default {
|
|
|
99
125
|
components: {
|
|
100
126
|
Dialog,
|
|
101
127
|
DirectoryViewListItem,
|
|
128
|
+
DirectoryViewGridItem,
|
|
102
129
|
SingleSelect,
|
|
103
130
|
Menu
|
|
104
131
|
},
|
|
105
132
|
emits: [ 'selectionChanged', 'item-activated' ],
|
|
106
133
|
props: {
|
|
134
|
+
viewMode: {
|
|
135
|
+
type: String,
|
|
136
|
+
default: 'list',
|
|
137
|
+
validator(value) {
|
|
138
|
+
return ['list', 'grid'].includes(value);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
107
141
|
busy: {
|
|
108
142
|
type: Boolean,
|
|
109
143
|
default: false
|
|
@@ -140,6 +174,22 @@ export default {
|
|
|
140
174
|
type: Boolean,
|
|
141
175
|
default: true
|
|
142
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
|
|
192
|
+
},
|
|
143
193
|
showDelete: {
|
|
144
194
|
type: Boolean,
|
|
145
195
|
default: true
|
|
@@ -365,11 +415,13 @@ export default {
|
|
|
365
415
|
disabled: () => { return this.selectedCount > 1; },
|
|
366
416
|
action: () => { this.onItemActivated(this.getSelected()[0]); }
|
|
367
417
|
}, {
|
|
368
|
-
separator:true
|
|
418
|
+
separator:true,
|
|
419
|
+
visible: () => { return this.showDownload || this.showShare || this.showCopy || this.showCut || this.showPaste || this.showSelectAll; },
|
|
369
420
|
}, {
|
|
370
421
|
label: this.tr('filemanager.list.menu.download'),
|
|
371
422
|
icon:'fa-solid fa-download',
|
|
372
423
|
action: this.onItemDownload,
|
|
424
|
+
visible: () => { return this.showDownload; },
|
|
373
425
|
disabled: () => { return this.multiDownload ? this.selectedCount === 0 : this.selectedCount !== 1; }
|
|
374
426
|
}, {
|
|
375
427
|
label: this.tr('filemanager.list.menu.share'),
|
|
@@ -390,16 +442,19 @@ export default {
|
|
|
390
442
|
}, {
|
|
391
443
|
label: this.tr('filemanager.list.menu.paste'),
|
|
392
444
|
icon:'fa-regular fa-paste',
|
|
445
|
+
visible: () => { return this.showPaste; },
|
|
393
446
|
disabled: () => { return !(this.focusItem && this.focusItem.isDirectory) || this.selectedCount > 1 || !this.clipboard.files || !this.clipboard.files.length; },
|
|
394
447
|
action: () => { this.pasteHandler(this.clipboard.action, this.clipboard.files, this.focusItem); }
|
|
395
448
|
}, {
|
|
396
449
|
label: this.tr('filemanager.list.menu.selectAll'),
|
|
397
450
|
icon:'fa-solid fa-check-double',
|
|
451
|
+
visible: () => { return this.showSelectAll; },
|
|
398
452
|
action: this.onSelectAll
|
|
399
453
|
}, {
|
|
400
454
|
label: this.tr('filemanager.list.menu.rename'),
|
|
401
455
|
icon:'fa-regular fa-pen-to-square',
|
|
402
456
|
action: this.onItemRenameBegin,
|
|
457
|
+
visible: () => { return this.showRename; },
|
|
403
458
|
disabled: () => { return !this.editable || this.selectedCount > 1; }
|
|
404
459
|
}, {
|
|
405
460
|
label: this.tr('filemanager.list.menu.chown'),
|
|
@@ -508,11 +563,33 @@ export default {
|
|
|
508
563
|
event.dataTransfer.setData('application/x-pankow', 'selected');
|
|
509
564
|
event.dataTransfer.setDragImage(dragHandle, 0, 0);
|
|
510
565
|
},
|
|
566
|
+
getGridColumns() {
|
|
567
|
+
const el = this.$refs.gridBody;
|
|
568
|
+
if (this.viewMode !== 'grid' || !el || !el.children.length) return 1;
|
|
569
|
+
const firstRowTop = el.children[0].getBoundingClientRect().top;
|
|
570
|
+
const tolerance = 2;
|
|
571
|
+
let cols = 0;
|
|
572
|
+
for (const child of el.children) {
|
|
573
|
+
if (Math.abs(child.getBoundingClientRect().top - firstRowTop) <= tolerance) cols++;
|
|
574
|
+
else break;
|
|
575
|
+
}
|
|
576
|
+
return cols || 1;
|
|
577
|
+
},
|
|
511
578
|
onKeyUp(event) {
|
|
512
|
-
this.
|
|
579
|
+
const step = this.viewMode === 'grid' ? this.getGridColumns() : 1;
|
|
580
|
+
this.onSelectAndFocusIndex(this.focusItemIndex - step, event.shiftKey);
|
|
513
581
|
},
|
|
514
582
|
onKeyDown(event) {
|
|
515
|
-
this.
|
|
583
|
+
const step = this.viewMode === 'grid' ? this.getGridColumns() : 1;
|
|
584
|
+
this.onSelectAndFocusIndex(this.focusItemIndex + step, event.shiftKey);
|
|
585
|
+
},
|
|
586
|
+
onKeyLeft(event) {
|
|
587
|
+
if (this.viewMode !== 'grid') return;
|
|
588
|
+
this.onSelectAndFocusIndex(this.focusItemIndex - 1, event.shiftKey);
|
|
589
|
+
},
|
|
590
|
+
onKeyRight(event) {
|
|
591
|
+
if (this.viewMode !== 'grid') return;
|
|
592
|
+
this.onSelectAndFocusIndex(this.focusItemIndex + 1, event.shiftKey);
|
|
516
593
|
},
|
|
517
594
|
onKeyEnter(event) {
|
|
518
595
|
if (this.focusItem) this.onItemActivated(this.focusItem);
|
|
@@ -748,6 +825,10 @@ export default {
|
|
|
748
825
|
height: calc(100% - 38px); /* 38px is the header size */
|
|
749
826
|
}
|
|
750
827
|
|
|
828
|
+
.directory-view-body-container-full {
|
|
829
|
+
height: 100%;
|
|
830
|
+
}
|
|
831
|
+
|
|
751
832
|
.directory-view-body {
|
|
752
833
|
padding: 0 10px;
|
|
753
834
|
padding-bottom: 10px;
|
|
@@ -755,6 +836,13 @@ export default {
|
|
|
755
836
|
overflow: auto;
|
|
756
837
|
}
|
|
757
838
|
|
|
839
|
+
.directory-view-body-grid {
|
|
840
|
+
display: grid;
|
|
841
|
+
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
|
842
|
+
gap: 4px;
|
|
843
|
+
align-content: start;
|
|
844
|
+
}
|
|
845
|
+
|
|
758
846
|
.directory-view-header-icon {
|
|
759
847
|
width: 40px;
|
|
760
848
|
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="grid-item-wrapper"
|
|
3
|
+
:draggable="!rename"
|
|
4
|
+
@mouseup="onSelect($event)"
|
|
5
|
+
@drop="onDrop($event)"
|
|
6
|
+
@dragover="onDragOver($event)"
|
|
7
|
+
@dragexit="onDragExit($event)"
|
|
8
|
+
>
|
|
9
|
+
|
|
10
|
+
<div class="grid-item" :class="{ 'focused': item.focused, 'selected': item.selected, 'drop-target-active': dropTargetActive }">
|
|
11
|
+
<div v-if="visible && showStar" class="grid-item-star" @dblclick.stop.prevent @click.stop.prevent="onToggleStar">
|
|
12
|
+
<Icon :icon="`${item.star ? 'fa-solid' : 'fa-regular'} fa-star`" class="star-icon" :class="{ 'star-visible': item.star }" />
|
|
13
|
+
</div>
|
|
14
|
+
<div v-if="visible" class="grid-item-actions" @mouseup.stop="onActionMenu($event)">
|
|
15
|
+
<i class="fa-solid fa-ellipsis-vertical"/>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="grid-item-icon">
|
|
19
|
+
<template v-if="visible">
|
|
20
|
+
<img :src="item.previewUrl || item.icon" ref="iconImage" @error="iconError($event)"/>
|
|
21
|
+
<i v-show="shareIndicatorProperty !== '' && !!item[shareIndicatorProperty]" class="fa-solid fa-share-nodes is-shared"></i>
|
|
22
|
+
</template>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div v-if="visible && !rename" class="grid-item-label" :title="item.name + (item.target ? ` → ${item.target}` : '')">
|
|
26
|
+
<a v-if="item.href" class="open-action" @dblclick.stop @click="onOpen($event)" :href="item.href">{{ item.name }}</a>
|
|
27
|
+
<span v-else>{{ item.name }}</span>
|
|
28
|
+
</div>
|
|
29
|
+
<div v-if="visible && rename" class="grid-item-label grid-item-rename">
|
|
30
|
+
<TextInput
|
|
31
|
+
ref="renameInput"
|
|
32
|
+
v-model="newName"
|
|
33
|
+
:disabled="renameBusy"
|
|
34
|
+
@blur="onRenameEnd"
|
|
35
|
+
@dblclick.stop
|
|
36
|
+
@keydown.enter.stop="onRenameSubmit"
|
|
37
|
+
@keydown.esc.stop="onRenameEnd"
|
|
38
|
+
@keydown.stop
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<script>
|
|
46
|
+
|
|
47
|
+
import Icon from './Icon.vue';
|
|
48
|
+
import TextInput from './TextInput.vue';
|
|
49
|
+
|
|
50
|
+
export default {
|
|
51
|
+
name: 'DirectoryViewGridItem',
|
|
52
|
+
emits: [ 'activated', 'action-menu' ],
|
|
53
|
+
components: {
|
|
54
|
+
Icon,
|
|
55
|
+
TextInput
|
|
56
|
+
},
|
|
57
|
+
props: {
|
|
58
|
+
item: Object,
|
|
59
|
+
showStar: {
|
|
60
|
+
type: Boolean,
|
|
61
|
+
default: false
|
|
62
|
+
},
|
|
63
|
+
shareIndicatorProperty: {
|
|
64
|
+
type: String,
|
|
65
|
+
default: '',
|
|
66
|
+
},
|
|
67
|
+
fallbackIcon: String,
|
|
68
|
+
renameHandler: {
|
|
69
|
+
type: Function,
|
|
70
|
+
default() {
|
|
71
|
+
console.warn('Missing renameHandler for DirectoryViewGridItem');
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
starHandler: {
|
|
75
|
+
type: Function,
|
|
76
|
+
default() {
|
|
77
|
+
console.warn('Missing starHandler for DirectoryViewGridItem');
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
selectHandler: {
|
|
81
|
+
type: Function,
|
|
82
|
+
default() {
|
|
83
|
+
console.warn('Missing selectHandler for DirectoryViewGridItem');
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
dropHandler: {
|
|
87
|
+
type: Function,
|
|
88
|
+
default() {
|
|
89
|
+
console.warn('Missing dropHandler for DirectoryViewGridItem');
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
canDropHandler: {
|
|
93
|
+
type: Function,
|
|
94
|
+
default() {
|
|
95
|
+
console.warn('Missing canDropHandler for DirectoryViewGridItem');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
data() {
|
|
100
|
+
return {
|
|
101
|
+
visible: false,
|
|
102
|
+
rename: false,
|
|
103
|
+
renameBusy: false,
|
|
104
|
+
newName: '',
|
|
105
|
+
dropTargetActive: false,
|
|
106
|
+
previewRetries: 0
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
methods: {
|
|
110
|
+
highlight() {
|
|
111
|
+
this.$el.classList.add('pankow-directory-view-highlight-animation');
|
|
112
|
+
setTimeout(() => { this.$el.classList.remove('pankow-directory-view-highlight-animation'); }, 4000);
|
|
113
|
+
},
|
|
114
|
+
onToggleStar() {
|
|
115
|
+
this.starHandler(this.item);
|
|
116
|
+
},
|
|
117
|
+
onActionMenu(event) {
|
|
118
|
+
this.onSelect(event, true);
|
|
119
|
+
this.$emit('action-menu', this.item, event);
|
|
120
|
+
},
|
|
121
|
+
onRenameBegin() {
|
|
122
|
+
this.rename = true;
|
|
123
|
+
this.newName = this.item.name;
|
|
124
|
+
|
|
125
|
+
setTimeout(() => {
|
|
126
|
+
const elem = this.$refs.renameInput.$el;
|
|
127
|
+
elem.focus();
|
|
128
|
+
|
|
129
|
+
if (typeof elem.selectionStart !== 'undefined') {
|
|
130
|
+
elem.selectionStart = 0;
|
|
131
|
+
elem.selectionEnd = this.item.name.lastIndexOf('.');
|
|
132
|
+
}
|
|
133
|
+
}, 0);
|
|
134
|
+
},
|
|
135
|
+
onRenameEnd() {
|
|
136
|
+
this.rename = false;
|
|
137
|
+
this.renameBusy = false;
|
|
138
|
+
this.newName = '';
|
|
139
|
+
},
|
|
140
|
+
async onRenameSubmit() {
|
|
141
|
+
if (!this.newName) return;
|
|
142
|
+
|
|
143
|
+
this.renameBusy = true;
|
|
144
|
+
await this.renameHandler(this.item, this.newName);
|
|
145
|
+
this.onRenameEnd();
|
|
146
|
+
},
|
|
147
|
+
onOpen(event) {
|
|
148
|
+
if (event.ctrlKey || event.metaKey) return;
|
|
149
|
+
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
event.stopPropagation();
|
|
152
|
+
|
|
153
|
+
this.$emit('activated', this.item);
|
|
154
|
+
},
|
|
155
|
+
onSelect(event, actionMenu = false) {
|
|
156
|
+
if ((event.button === 2 || actionMenu) && event.ctrlKey) {
|
|
157
|
+
this.selectHandler(this.item, event, true);
|
|
158
|
+
} else {
|
|
159
|
+
this.selectHandler(this.item, event);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
onDrop(event) {
|
|
163
|
+
if (!this.canDropHandler(this.item)) return;
|
|
164
|
+
|
|
165
|
+
event.stopPropagation();
|
|
166
|
+
|
|
167
|
+
this.dropHandler(this.item, event);
|
|
168
|
+
this.dropTargetActive = false;
|
|
169
|
+
},
|
|
170
|
+
onDragOver(event) {
|
|
171
|
+
if (!this.canDropHandler(this.item)) return;
|
|
172
|
+
|
|
173
|
+
event.preventDefault();
|
|
174
|
+
event.stopPropagation();
|
|
175
|
+
|
|
176
|
+
event.dataTransfer.dropEffect = 'move';
|
|
177
|
+
this.dropTargetActive = true;
|
|
178
|
+
},
|
|
179
|
+
onDragExit(event) {
|
|
180
|
+
if (!this.item.isDirectory) return;
|
|
181
|
+
|
|
182
|
+
this.dropTargetActive = false;
|
|
183
|
+
},
|
|
184
|
+
iconError(event) {
|
|
185
|
+
event.target.src = this.fallbackIcon;
|
|
186
|
+
|
|
187
|
+
setTimeout(() => {
|
|
188
|
+
if (this.previewRetries > 5) return;
|
|
189
|
+
++this.previewRetries;
|
|
190
|
+
|
|
191
|
+
event.target.src = this.item.previewUrl || this.item.icon;
|
|
192
|
+
}, 1000);
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
mounted() {
|
|
196
|
+
const observer = new IntersectionObserver(result => {
|
|
197
|
+
if (result[0].isIntersecting) {
|
|
198
|
+
this.visible = true;
|
|
199
|
+
observer.unobserve(this.$el);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
observer.observe(this.$el);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
</script>
|
|
208
|
+
|
|
209
|
+
<style scoped>
|
|
210
|
+
|
|
211
|
+
.grid-item-wrapper {
|
|
212
|
+
--background-color-hover: #ededed;
|
|
213
|
+
--background-color-selected: #dbedfb;
|
|
214
|
+
--border-color-focus: #b3cfe5;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
@media (prefers-color-scheme: dark) {
|
|
218
|
+
.grid-item-wrapper {
|
|
219
|
+
--background-color-hover: rgba(255, 255, 255, 0.1);
|
|
220
|
+
--background-color-selected: rgba(255, 255, 255, 0.2);
|
|
221
|
+
--border-color-focus: rgba(255, 255, 255, 0.3);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.grid-item-wrapper {
|
|
226
|
+
padding: 2px;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.grid-item {
|
|
230
|
+
display: flex;
|
|
231
|
+
flex-direction: column;
|
|
232
|
+
align-items: center;
|
|
233
|
+
padding: 10px 8px 8px;
|
|
234
|
+
border-radius: 6px;
|
|
235
|
+
border: 2px solid transparent;
|
|
236
|
+
position: relative;
|
|
237
|
+
transition: all 100ms;
|
|
238
|
+
cursor: default;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.grid-item-wrapper:hover > .grid-item {
|
|
242
|
+
background-color: var(--background-color-hover);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.grid-item.drop-target-active {
|
|
246
|
+
border: 2px solid var(--pankow-color-primary);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.grid-item.selected {
|
|
250
|
+
background-color: var(--background-color-selected) !important;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.grid-item.focused {
|
|
254
|
+
border: solid 2px var(--border-color-focus);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.grid-item-actions {
|
|
258
|
+
position: absolute;
|
|
259
|
+
top: 4px;
|
|
260
|
+
right: 4px;
|
|
261
|
+
padding: 2px 6px;
|
|
262
|
+
cursor: pointer;
|
|
263
|
+
border-radius: 3px;
|
|
264
|
+
opacity: 0;
|
|
265
|
+
transition: opacity 100ms;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.grid-item-actions:hover {
|
|
269
|
+
background-color: var(--background-color-hover);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.grid-item-wrapper:hover .grid-item-actions,
|
|
273
|
+
.grid-item.focused .grid-item-actions,
|
|
274
|
+
.grid-item.selected .grid-item-actions {
|
|
275
|
+
opacity: 1;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.grid-item-icon {
|
|
279
|
+
width: 80px;
|
|
280
|
+
height: 80px;
|
|
281
|
+
display: flex;
|
|
282
|
+
align-items: center;
|
|
283
|
+
justify-content: center;
|
|
284
|
+
position: relative;
|
|
285
|
+
flex-shrink: 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.grid-item-icon > img {
|
|
289
|
+
max-width: 64px;
|
|
290
|
+
max-height: 64px;
|
|
291
|
+
object-fit: contain;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.is-shared {
|
|
295
|
+
position: absolute;
|
|
296
|
+
right: 2px;
|
|
297
|
+
bottom: 2px;
|
|
298
|
+
font-size: 1rem;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.grid-item-label {
|
|
302
|
+
width: 100%;
|
|
303
|
+
text-align: center;
|
|
304
|
+
overflow: hidden;
|
|
305
|
+
text-overflow: ellipsis;
|
|
306
|
+
white-space: nowrap;
|
|
307
|
+
margin-top: 4px;
|
|
308
|
+
font-size: 0.85rem;
|
|
309
|
+
padding: 0 2px;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.grid-item-rename {
|
|
313
|
+
overflow: hidden;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.grid-item-rename :deep(input) {
|
|
317
|
+
width: 100%;
|
|
318
|
+
box-sizing: border-box;
|
|
319
|
+
text-align: center;
|
|
320
|
+
font-size: 0.85rem;
|
|
321
|
+
padding: 1px 4px;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.grid-item-star {
|
|
325
|
+
position: absolute;
|
|
326
|
+
top: 4px;
|
|
327
|
+
left: 4px;
|
|
328
|
+
padding: 2px 6px;
|
|
329
|
+
cursor: pointer;
|
|
330
|
+
border-radius: 3px;
|
|
331
|
+
opacity: 0;
|
|
332
|
+
transition: opacity 100ms;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.grid-item-star:hover {
|
|
336
|
+
background-color: var(--background-color-hover);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.grid-item-wrapper:hover .grid-item-star,
|
|
340
|
+
.grid-item.focused .grid-item-star,
|
|
341
|
+
.grid-item.selected .grid-item-star {
|
|
342
|
+
opacity: 1;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.star-icon {
|
|
346
|
+
color: #ffcb00;
|
|
347
|
+
cursor: pointer;
|
|
348
|
+
font-size: 0.8rem;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.grid-item-star:has(.star-visible) {
|
|
352
|
+
opacity: 1 !important;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.open-action {
|
|
356
|
+
cursor: pointer;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.open-action:hover {
|
|
360
|
+
text-decoration: underline;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
</style>
|
package/components/Menu.vue
CHANGED
|
@@ -78,6 +78,7 @@ let targetTop = 0;
|
|
|
78
78
|
const offsetY = ref(0);
|
|
79
79
|
const rollUp = ref(false);
|
|
80
80
|
const searchString = ref('');
|
|
81
|
+
const keyboardNav = ref(false);
|
|
81
82
|
const emptyItem = ref({
|
|
82
83
|
label: '',
|
|
83
84
|
disabled: true,
|
|
@@ -193,12 +194,22 @@ async function open(event, element = null) {
|
|
|
193
194
|
else container.value.focus();
|
|
194
195
|
}
|
|
195
196
|
|
|
197
|
+
function onMouseMove(event) {
|
|
198
|
+
keyboardNav.value = false;
|
|
199
|
+
const item = event.target.closest('.pankow-menu-item');
|
|
200
|
+
if (item && !item.classList.contains('pankow-menu-item-disabled') && item.getAttribute('separator') !== 'true') {
|
|
201
|
+
item.focus();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
196
205
|
function onBackdrop(event) {
|
|
197
206
|
close();
|
|
198
207
|
event.preventDefault();
|
|
199
208
|
}
|
|
200
209
|
|
|
201
210
|
function selectUp() {
|
|
211
|
+
keyboardNav.value = true;
|
|
212
|
+
|
|
202
213
|
if (!container.value.children[0]) return;
|
|
203
214
|
|
|
204
215
|
const active = getActiveElement(container.value.children);
|
|
@@ -219,6 +230,8 @@ function selectUp() {
|
|
|
219
230
|
}
|
|
220
231
|
|
|
221
232
|
function selectDown() {
|
|
233
|
+
keyboardNav.value = true;
|
|
234
|
+
|
|
222
235
|
if (!container.value.children[0]) return;
|
|
223
236
|
|
|
224
237
|
const active = getActiveElement(container.value.children);
|
|
@@ -312,7 +325,7 @@ defineExpose({
|
|
|
312
325
|
<teleport to="#app">
|
|
313
326
|
<div class="pankow-menu-backdrop" @click="onBackdrop($event)" @contextmenu="onBackdrop($event)" v-show="isOpen"></div>
|
|
314
327
|
<Transition :name="rollUp ? 'pankow-roll-up' : 'pankow-roll-down'">
|
|
315
|
-
<div class="pankow-menu" v-show="isOpen" ref="container" tabindex="0" @keydown.up.stop="selectUp()" @keydown.down.stop="selectDown()" @keydown.esc.stop="close()" @keydown="onKeyDown">
|
|
328
|
+
<div class="pankow-menu" :class="{ 'pankow-menu--keyboard-nav': keyboardNav }" v-show="isOpen" ref="container" tabindex="0" @keydown.up.stop="selectUp()" @keydown.down.stop="selectDown()" @keydown.esc.stop="close()" @keydown="onKeyDown" @mousemove="onMouseMove">
|
|
316
329
|
<TextInput placeholder="Filter ..." autocomplete="off" @keydown.up.stop="selectUp()" @keydown.down.stop="selectDown()" @keydown.stop @keydown.esc.stop="close()" @click.stop style="width: 100%; border: 0; padding: 8px 12px;" v-model="searchString" v-if="searchThreshold < model.length"/>
|
|
317
330
|
<component v-for="item in visibleItems" ref="itemElements" :is="item.type || (item.href ? MenuItemLink : MenuItem)" @activated="onItemActivated(item)" :item="item" :has-icons="hasIcons" />
|
|
318
331
|
<MenuItem v-if="model.length === 0" :item="emptyItem"/>
|
package/components/MenuItem.vue
CHANGED
|
@@ -77,13 +77,31 @@ function onActivated() {
|
|
|
77
77
|
opacity: .6
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
.pankow-menu-item:focus
|
|
80
|
+
.pankow-menu-item:focus {
|
|
81
|
+
background-color: var(--pankow-color-primary-hover);
|
|
82
|
+
color: white;
|
|
83
|
+
}
|
|
84
|
+
|
|
81
85
|
.pankow-menu-item:hover {
|
|
82
86
|
background-color: var(--pankow-color-primary-hover);
|
|
83
87
|
color: white;
|
|
84
88
|
}
|
|
85
89
|
|
|
86
|
-
.pankow-menu-item
|
|
90
|
+
.pankow-menu--keyboard-nav .pankow-menu-item:hover {
|
|
91
|
+
background-color: inherit;
|
|
92
|
+
color: inherit;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.pankow-menu--keyboard-nav .pankow-menu-item:focus {
|
|
96
|
+
background-color: var(--pankow-color-primary-hover);
|
|
97
|
+
color: white;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.pankow-menu-item-disabled:focus {
|
|
101
|
+
background-color: inherit;
|
|
102
|
+
color: inherit;
|
|
103
|
+
}
|
|
104
|
+
|
|
87
105
|
.pankow-menu-item-disabled:hover {
|
|
88
106
|
background-color: inherit;
|
|
89
107
|
color: inherit;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudron/pankow",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "4.1.
|
|
4
|
+
"version": "4.1.3",
|
|
5
5
|
"description": "",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"types": "types/index.d.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"highlight.js": "^11.11.1",
|
|
35
35
|
"typescript": "^5.9.3",
|
|
36
36
|
"vite": "^7.3.1",
|
|
37
|
-
"vue": "^3.5.
|
|
38
|
-
"vue-router": "^5.0.
|
|
37
|
+
"vue": "^3.5.29",
|
|
38
|
+
"vue-router": "^5.0.3"
|
|
39
39
|
}
|
|
40
40
|
}
|