diff options
-rw-r--r-- | viewer/src/components/LdDropdown.vue | 92 | ||||
-rw-r--r-- | viewer/src/views/MainLayout.vue | 1 | ||||
-rw-r--r-- | viewer/src/views/layout/left/LayoutTagInput.vue | 93 | ||||
-rw-r--r-- | viewer/src/views/layout/top/LayoutCommandSort.vue | 60 |
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"> | ||
33 | import { onClickOutside, onKeyStroke, useVModel } from '@vueuse/core'; | ||
34 | import { ref, useAttrs, watch } from 'vue'; | ||
35 | |||
36 | const 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 | }); | ||
44 | const emit = defineEmits(['update:modelValue', 'select', 'opening', 'closing']); | ||
45 | const model = useVModel(props, 'modelValue', emit); | ||
46 | |||
47 | const attrs = useAttrs(); | ||
48 | |||
49 | const dropdown = ref(); | ||
50 | |||
51 | watch(() => model.value, value => { | ||
52 | if (value) emit('opening'); | ||
53 | else emit('closing'); | ||
54 | }); | ||
55 | |||
56 | onClickOutside(dropdown, closeDropdown); | ||
57 | onKeyStroke('Escape', closeDropdown); | ||
58 | |||
59 | function closeDropdown() { | ||
60 | setTimeout(() => (model.value = false)); | ||
61 | } | ||
62 | </script> | ||
63 | <script lang="ts"> | ||
64 | export 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"> |
61 | import { TagSearch } from '@/@types/tag'; | 56 | import { TagSearch } from '@/@types/tag'; |
57 | import LdDropdown from '@/components/LdDropdown.vue'; | ||
62 | import LdInput from '@/components/LdInput.vue'; | 58 | import LdInput from '@/components/LdInput.vue'; |
63 | import { useIndexFactory } from '@/services/indexFactory'; | 59 | import { useIndexFactory } from '@/services/indexFactory'; |
64 | import { useGalleryStore } from '@/store/galleryStore'; | 60 | import { useGalleryStore } from '@/store/galleryStore'; |
65 | import { computedEager, onClickOutside, onKeyStroke, useElementBounding, useFocus, useVModel } from '@vueuse/core'; | 61 | import { computedEager, useElementBounding, useFocus, useVModel } from '@vueuse/core'; |
66 | import { computed, ref, StyleValue, watchEffect } from 'vue'; | 62 | import { computed, ref, StyleValue, watchEffect } from 'vue'; |
67 | import { useI18n } from 'vue-i18n'; | 63 | import { useI18n } from 'vue-i18n'; |
68 | 64 | ||
@@ -77,19 +73,15 @@ const galeryStore = useGalleryStore(); | |||
77 | const indexFactory = useIndexFactory(); | 73 | const indexFactory = useIndexFactory(); |
78 | 74 | ||
79 | const search = ref(''); | 75 | const search = ref(''); |
80 | const openDropdown = computedEager<boolean>(() => !!search.value); | 76 | const showDropdown = ref(false); |
81 | watchEffect(() => { | 77 | |
82 | if (openDropdown.value) emit('opening'); | 78 | watchEffect(() => (showDropdown.value = !!search.value)); |
83 | else emit('closing'); | ||
84 | }); | ||
85 | 79 | ||
86 | // --- | 80 | // --- |
87 | 81 | ||
88 | const dropdown = ref(); | 82 | const dropdown = ref(); |
89 | const { top } = useElementBounding(dropdown); | 83 | const { top } = useElementBounding(dropdown); |
90 | const dropdownStyle = computedEager<StyleValue>(() => ({ height: `calc(100vh - 8px - ${top.value}px)` })); | 84 | const dropdownStyle = computedEager<StyleValue>(() => ({ height: `calc(100vh - 8px - ${top.value}px)` })); |
91 | onClickOutside(dropdown, closeDropdown); | ||
92 | onKeyStroke('Escape', closeDropdown); | ||
93 | 85 | ||
94 | const input = ref(); | 86 | const input = ref(); |
95 | const { focused } = useFocus(input); | 87 | const { 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 | } |
116 | function inputEnter() { | 108 | function inputEnter() { |
117 | if (search.value) addTag(); | 109 | if (search.value) addTag(); |
118 | else emit('search'); | 110 | else emit('search'); |
119 | } | 111 | } |
120 | function inputBackspace() { | 112 | function inputBackspace() { |
121 | !openDropdown.value && model.value.pop(); | 113 | !showDropdown.value && model.value.pop(); |
122 | } | 114 | } |
123 | function closeDropdown() { | 115 | function cleanSearch() { |
124 | search.value = ''; | 1 |