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.vue141
1 files changed, 141 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..a37c546
--- /dev/null
+++ b/viewer/src/views/layout/left/LayoutTagInput.vue
@@ -0,0 +1,141 @@
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 <LdDropdown
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>
47 <div
48 :class="$style.nomatch"
49 v-text="t('tagInput.nomatch')"
50 />
51 </template>
52 </LdDropdown>
53</template>
54
55<script setup lang="ts">
56import { TagSearch } from '@/@types/tag';
57import LdDropdown from '@/components/LdDropdown.vue';
58import LdInput from '@/components/LdInput.vue';
59import { useIndexFactory } from '@/services/indexFactory';
60import { useGalleryStore } from '@/store/galleryStore';
61import { computedEager, useElementBounding, useFocus, useVModel } from '@vueuse/core';
62import { computed, ref, StyleValue, watchEffect } from 'vue';
63import { useI18n } from 'vue-i18n';
64
65const props = defineProps({
66 modelValue: { type: Array<TagSearch>, required: true },
67});
68const emit = defineEmits(['update:modelValue', 'search', 'opening', 'closing']);
69const model = useVModel(props, 'modelValue', emit);
70
71const { t } = useI18n();
72const galeryStore = useGalleryStore();
73const indexFactory = useIndexFactory();
74
75const search = ref('');
76const showDropdown = ref(false);
77
78watchEffect(() => (showDropdown.value = !!search.value));
79
80// ---
81
82const dropdown = ref();
83const { top } = useElementBounding(dropdown);
84const dropdownStyle = computedEager<StyleValue>(() => ({ height: `calc(100vh - 8px - ${top.value}px)` }));
85
86const input = ref();
87const { focused } = useFocus(input);
88
89// ---
90
91const filteredTags = computed(() => indexFactory.searchTags(galeryStore.tagsIndex, search.value, false)
92 .filter(filterAlreadyPresent)
93 .sort((a, b) => b.items.length - a.items.length));
94
95function filterAlreadyPresent(newSearch: TagSearch) {
96 return !model.value.find(
97 currentSearch =>
98 currentSearch.tag === newSearch.tag && (!currentSearch.parent || currentSearch.parent === newSearch.parent),
99 );
100}
101
102function addTag(tag?: TagSearch) {
103 const toPush = tag ?? filteredTags.value[0];
104 if (!toPush) return;
105 model.value.push(toPush);
106 cleanSearch();
107}
108function inputEnter() {
109 if (search.value) addTag();
110 else emit('search');
111}
112function inputBackspace() {
113 !showDropdown.value && model.value.pop();
114}
115function cleanSearch() {
116 search.value = '';
117 focused.value = true;
118}
119</script>
120
121<style lang="scss" module>
122@import "~@/assets/scss/theme";
123
124.dropdown {
125 > div {
126 display: flex;
127 justify-content: space-between;
128 > div {
129 padding: 0 4px;
130 }
131 > div:last-child {
132 color: $text-light;
133 }
134 }
135 .nomatch {
136 color: $disabled-color;
137 justify-content: center;
138 cursor: default;
139 }
140}
141</style>