@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.
@@ -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-show="items.length">
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
- <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-extract="showExtract"
51
- :show-size="showSize"
52
- :show-star="showStar"
53
- :show-modified="showModified"
54
- :rename-handler="renameHandler"
55
- :share-indicator-property="shareIndicatorProperty"
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
- />
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.onSelectAndFocusIndex(this.focusItemIndex-1, event.shiftKey);
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.onSelectAndFocusIndex(this.focusItemIndex+1, event.shiftKey);
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>
@@ -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"/>
@@ -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-disabled:focus,
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.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.28",
38
- "vue-router": "^5.0.2"
37
+ "vue": "^3.5.29",
38
+ "vue-router": "^5.0.3"
39
39
  }
40
40
  }