aboutsummaryrefslogtreecommitdiff
path: root/viewer/src/views/layout/left/LayoutTagInput.vue
diff options
context:
space:
mode:
Diffstat (limited to 'viewer/src/views/layout/left/LayoutTagInput.vue')
-rw-r--r--viewer/src/views/layout/left/LayoutTagInput.vue164
1 files changed, 164 insertions, 0 deletions
diff --git a/viewer/src/views/layout/left/LayoutTagInput.vue b/viewer/src/views/layout/left/LayoutTagInput.vue
new file mode 100644
index 0000000..7ad3ed0
--- /dev/null
+++ b/viewer/src/views/layout/left/LayoutTagInput.vue
@@ -0,0 +1,164 @@
1<!-- ldgallery - A static generator which turns a collection of tagged
2-- pictures into a searchable web gallery.
3--
4-- Copyright (C) 2019-2022 Guillaume FOUET
5--
6-- This program is free software: you can redistribute it and/or modify
7-- it under the terms of the GNU Affero General Public License as
8-- published by the Free Software Foundation, either version 3 of the
9-- License, or (at your option) any later version.
10--
11-- This program is distributed in the hope that it will be useful,
12-- but WITHOUT ANY WARRANTY; without even the implied warranty of
13-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14-- GNU Affero General Public License for more details.
15--
16-- You should have received a copy of the GNU Affero General Public License
17-- along with this program. If not, see <https://www.gnu.org/licenses/>.
18-->
19
20<template>
21 <LdInput
22 ref="input"
23 v-model="search"
24 :placeholder="t('tagInput.placeholder')"
25 :tabindex="50"
26 @focus="e => (e.target as HTMLInputElement).select()"
27 @keypress.enter="inputEnter"
28 @keydown.backspace="inputBackspace"
29 />
30 <div style="position:relative;">
31 <Transition name="fade">
32 <div
33 v-if="openDropdown"
34 ref="dropdown"
35 class="scrollbar"
36 :class="$style.dropdown"
37 :style="dropdownStyle"
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>
59
60<script setup lang="ts">
61import { TagSearch } from '@/@types/tag';
62import LdInput from '@/components/LdInput.vue';
63import { useIndexFactory } from '@/services/indexFactory';
64import { useGalleryStore } from '@/store/galleryStore';
65import { computedEager, onClickOutside, onKeyStroke, useElementBounding, useFocus, useVModel } from '@vueuse/core';
66import { computed, ref, StyleValue, watchEffect } from 'vue';
67import { useI18n } from 'vue-i18n';
68
69const props = defineProps({
70 modelValue: { type: Array<TagSearch>, required: true },
71});
72const emit = defineEmits(['update:modelValue', 'search', 'opening', 'closing']);
73const model = useVModel(props, 'modelValue', emit);
74
75const { t } = useI18n();
76const galeryStore = useGalleryStore();
77const indexFactory = useIndexFactory();
78
79const search = ref('');
80const openDropdown = computedEager<boolean>(() => !!search.value);
81watchEffect(() => {
82 if (openDropdown.value) emit('opening');
83 else emit('closing');
84});
85
86// ---
87
88const dropdown = ref();
89const { top } = useElementBounding(dropdown);
90const dropdownStyle = computedEager<StyleValue>(() => ({ height: `calc(100vh - 8px - ${top.value}px)` }));
91onClickOutside(dropdown, closeDropdown);
92onKeyStroke('Escape', closeDropdown);
93
94const input = ref();
95const { focused } = useFocus(input);
96
97// ---
98
99const filteredTags = computed(() => indexFactory.searchTags(galeryStore.tagsIndex, search.value, false)
100 .filter(filterAlreadyPresent)
101 .sort((a, b) => b.items.length - a.items.length));
102
103function filterAlreadyPresent(newSearch: TagSearch) {
104 return !model.value.find(
105 currentSearch =>
106 currentSearch.tag === newSearch.tag && (!currentSearch.parent || currentSearch.parent === newSearch.parent),
107 );
108}
109
110function addTag(tag?: TagSearch) {
111 const toPush = tag ?? filteredTags.value[0];
112 if (!toPush) return;
113 model.value.push(toPush);
114 closeDropdown();
115}
116function inputEnter() {
117 if (search.value) addTag();
118 else emit('search');
119}
120function inputBackspace() {
121 !openDropdown.value && model.value.pop();
122}
123function closeDropdown() {
124 search.value = '';
125 focused.value = true;
126}
127</script>
128
129<style lang="scss" module>
130@import "~@/assets/scss/theme";
131
132.dropdown {
133 position: absolute;
134 left: 0;
135 z-index: 10;
136 width: $layout-left;
137 color: $input-color;
138 background-color: $dropdown-item-color;
139 padding: 4px 0px;
140 > div {
141 display: flex;
142 justify-content: space-between;
143 padding: 4px 0;
144 margin: 2px; // For the focus border
145 cursor: pointer;
146 > div {
147 padding: 0 4px;
148 }
149 > div:last-child {
150 color: $text-light;
151 }
152 &:hover {
153 background-color: $dropdown-item-hover-color;
154 }
155 &:focus {
156 outline: solid 1px $button-active-color;
157 }
158 &.nomatch {
159 color: $text-light;
160 justify-content: center;
161 }
162 }
163}
164</style>