aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--viewer/src/components/LdDropdown.vue92
-rw-r--r--viewer/src/views/MainLayout.vue1
-rw-r--r--viewer/src/views/layout/left/LayoutTagInput.vue93
-rw-r--r--viewer/src/views/layout/top/LayoutCommandSort.vue60
4 files changed, 142 insertions, 104 deletions
diff --git a/viewer/src/components/LdDropdown.vue b/viewer/src/components/LdDropdown.vue
new file mode 100644
index 0000000..2ab3252
--- /dev/null
+++ b/viewer/src/components/LdDropdown.vue
@@ -0,0 +1,92 @@
1<template>
2 <div style="position:relative;">
3 <Transition name="fade">
4 <div
5 v-if="model"
6 ref="dropdown"
7 class="scrollbar"
8 :class="$style.dropdown"
9 v-bind="attrs"
10 >
11 <div
12 v-for="(option,idx) in props.list"
13 :key="listKey ? option[props.listKey] : idx"
14 :tabindex="props.tabindexRoot + idx"
15 @click="emit('select', option)"
16 @keypress.enter.space="emit('select', option)"
17 >
18 <slot
19 name="option"
20 :option="option"
21 />
22 </div>
23 <slot
24 v-if="!props.list.length"
25 name="empty"
26 />
27 </div>
28 </Transition>
29 </div>
30</template>
31
32<script setup lang="ts">
33import { onClickOutside, onKeyStroke, useVModel } from '@vueuse/core';
34import { ref, useAttrs, watch } from 'vue';
35
36const props = defineProps({
37 modelValue: { type: Boolean, required: true },
38 // Vue 3 currently won't allow generics in props
39 // eslint-disable-next-line @typescript-eslint/no-explicit-any
40 list: { type: Array<any>, required: true },
41 listKey: { type: [String, Number], default: null },
42 tabindexRoot: { type: Number, required: true },
43});
44const emit = defineEmits(['update:modelValue', 'select', 'opening', 'closing']);
45const model = useVModel(props, 'modelValue', emit);
46
47const attrs = useAttrs();
48
49const dropdown = ref();
50
51watch(() => model.value, value => {
52 if (value) emit('opening');
53 else emit('closing');
54});
55
56onClickOutside(dropdown, closeDropdown);
57onKeyStroke('Escape', closeDropdown);
58
59function closeDropdown() {
60 setTimeout(() => (model.value = false));
61}
62</script>
63<script lang="ts">
64export default {
65 inheritAttrs: false,
66};
67</script>
68
69<style lang="scss" module>
70@import "~@/assets/scss/theme";
71
72.dropdown {
73 position: absolute;
74 left: 0;
75 z-index: 99;
76 width: $layout-left;
77 color: $input-color;
78 background-color: $dropdown-item-color;
79 padding: 4px 0px;
80 > div {
81 padding: 4px 0;
82 margin: 2px; // For the focus border
83 cursor: pointer;
84 &:hover {
85 background-color: $dropdown-item-hover-color;
86 }
87 &:focus {
88 outline: solid 1px $button-active-color;
89 }
90 }
91}
92 </style>
diff --git a/viewer/src/views/MainLayout.vue b/viewer/src/views/MainLayout.vue
index 9c84116..d8b3300 100644
--- a/viewer/src/views/MainLayout.vue
+++ b/viewer/src/views/MainLayout.vue
@@ -103,6 +103,7 @@ function validateSpashScreen() {
103 overflow: hidden; 103 overflow: hidden;
104 touch-action: none; 104 touch-action: none;
105 background-color: $content-bgcolor; 105 background-color: $content-bgcolor;
106 margin: 0;
106} 107}
107.layout { 108.layout {
108 position: fixed; 109 position: fixed;
diff --git a/viewer/src/views/layout/left/LayoutTagInput.vue b/viewer/src/views/layout/left/LayoutTagInput.vue
index 7ad3ed0..a37c546 100644
--- a/viewer/src/views/layout/left/LayoutTagInput.vue
+++ b/viewer/src/views/layout/left/LayoutTagInput.vue
@@ -27,42 +27,38 @@
27 @keypress.enter="inputEnter" 27 @keypress.enter="inputEnter"
28 @keydown.backspace="inputBackspace" 28 @keydown.backspace="inputBackspace"
29 /> 29 />
30 <div style="position:relative;"> 30 <LdDropdown
31 <Transition name="fade"> 31 ref="dropdown"
32 v-model="showDropdown"
33 :list="filteredTags"
34 list-key="tagfiltered"
35 :tabindex-root="51"
36 :class="$style.dropdown"
37 :style="dropdownStyle"
38 @select="addTag"
39 @opening="emit('opening')"
40 @closing="cleanSearch(); emit('closing');"
41 >
42 <template #option="{option}:{option:TagSearch}">
43 <div v-text="option.display" />
44 <div v-text="option.items.length" />
45 </template>
46 <template #empty>
32 <div 47 <div
33 v-if="openDropdown" 48 :class="$style.nomatch"
34 ref="dropdown" 49 v-text="t('tagInput.nomatch')"
35 class="scrollbar" 50 />
36 :class="$style.dropdown" 51 </template>
37 :style="dropdownStyle" 52 </LdDropdown>
38 >
39 <div
40 v-for="(tag,idx) in filteredTags"
41 :key="tag.tagfiltered"
42 :tabindex="51 + idx"
43 @click="addTag(tag)"
44 @keypress.enter.space="addTag(tag)"
45 >
46 <div v-text="tag.display" />
47 <div v-text="tag.items.length" />
48 </div>
49 <div
50 v-if="!filteredTags.length"
51 class="disaled"
52 :class="$style.nomatch"
53 v-text="t('tagInput.nomatch')"
54 />
55 </div>
56 </Transition>
57 </div>
58</template> 53</template>
59 54
60<script setup lang="ts"> 55<script setup lang="ts">
61import { TagSearch } from '@/@types/tag'; 56import { TagSearch } from '@/@types/tag';
57import LdDropdown from '@/components/LdDropdown.vue';
62import LdInput from '@/components/LdInput.vue'; 58import LdInput from '@/components/LdInput.vue';
63import { useIndexFactory } from '@/services/indexFactory'; 59import { useIndexFactory } from '@/services/indexFactory';
64import { useGalleryStore } from '@/store/galleryStore'; 60import { useGalleryStore } from '@/store/galleryStore';
65import { computedEager, onClickOutside, onKeyStroke, useElementBounding, useFocus, useVModel } from '@vueuse/core'; 61import { computedEager, useElementBounding, useFocus, useVModel } from '@vueuse/core';
66import { computed, ref, StyleValue, watchEffect } from 'vue'; 62import { computed, ref, StyleValue, watchEffect } from 'vue';
67import { useI18n } from 'vue-i18n'; 63import { useI18n } from 'vue-i18n';
68 64
@@ -77,19 +73,15 @@ const galeryStore = useGalleryStore();
77const indexFactory = useIndexFactory(); 73const indexFactory = useIndexFactory();
78 74
79const search = ref(''); 75const search = ref('');
80const openDropdown = computedEager<boolean>(() => !!search.value); 76const showDropdown = ref(false);
81watchEffect(() => { 77
82 if (openDropdown.value) emit('opening'); 78watchEffect(() => (showDropdown.value = !!search.value));
83 else emit('closing');
84});
85 79
86// --- 80// ---
87 81
88const dropdown = ref(); 82const dropdown = ref();
89const { top } = useElementBounding(dropdown); 83const { top } = useElementBounding(dropdown);
90const dropdownStyle = computedEager<StyleValue>(() => ({ height: `calc(100vh - 8px - ${top.value}px)` })); 84const dropdownStyle = computedEager<StyleValue>(() => ({ height: `calc(100vh - 8px - ${top.value}px)` }));
91onClickOutside(dropdown, closeDropdown);
92onKeyStroke('Escape', closeDropdown);
93 85
94const input = ref(); 86const input = ref();
95const { focused } = useFocus(input); 87const { focused } = useFocus(input);
@@ -111,16 +103,16 @@ function addTag(tag?: TagSearch) {
111 const toPush = tag ?? filteredTags.value[0]; 103 const toPush = tag ?? filteredTags.value[0];
112 if (!toPush) return; 104 if (!toPush) return;
113 model.value.push(toPush); 105 model.value.push(toPush);
114 closeDropdown(); 106 cleanSearch();
115} 107}
116function inputEnter() { 108function inputEnter() {
117 if (search.value) addTag(); 109 if (search.value) addTag();
118 else emit('search'); 110 else emit('search');
119} 111}
120function inputBackspace() { 112function inputBackspace() {
121 !openDropdown.value && model.value.pop(); 113 !showDropdown.value && model.value.pop();
122} 114}
123function closeDropdown() {