mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 22:43:34 -07:00
search dialog rewrite
This commit is contained in:
parent
475cafae49
commit
6478df8438
9 changed files with 242 additions and 195 deletions
|
@ -75,6 +75,9 @@ export default {
|
|||
</script>
|
||||
|
||||
<style>
|
||||
.top-dialog {
|
||||
align-self: flex-start;
|
||||
}
|
||||
:root {
|
||||
scrollbar-color: transparent transparent;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<v-row>
|
||||
<SearchDialog ref="mealselect" @select="setSlug" />
|
||||
<SearchDialog ref="mealselect" @selected="setSlug" />
|
||||
<BaseDialog
|
||||
title="Custom Meal"
|
||||
:title-icon="$globals.icons.primary"
|
||||
|
@ -78,7 +78,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import SearchDialog from "../UI/Search/SearchDialog";
|
||||
import SearchDialog from "../UI/Dialogs/SearchDialog";
|
||||
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
|
||||
import { api } from "@/api";
|
||||
import CardImage from "../Recipe/CardImage.vue";
|
||||
|
@ -129,13 +129,13 @@ export default {
|
|||
this.value[this.activeIndex]["meals"][0]["name"] = name;
|
||||
this.value[this.activeIndex]["meals"][0]["description"] = description;
|
||||
},
|
||||
setSlug(name, slug) {
|
||||
setSlug(recipe) {
|
||||
switch (this.mode) {
|
||||
case this.modes.primary:
|
||||
this.setPrimary(name, slug);
|
||||
this.setPrimary(recipe.name, recipe.slug);
|
||||
break;
|
||||
default:
|
||||
this.setSide(name, slug);
|
||||
this.setSide(recipe.name, recipe.slug);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
<template>
|
||||
<v-card :ripple="false" class="mx-auto" hover :to="`/recipe/${slug}`" @click="$emit('selected')">
|
||||
<v-expand-transition>
|
||||
<v-card
|
||||
:ripple="false"
|
||||
class="mx-auto"
|
||||
hover
|
||||
:to="this.$listeners.selected ? undefined : `/recipe/${slug}`"
|
||||
@click="$emit('selected')"
|
||||
>
|
||||
<v-list-item three-line>
|
||||
<v-list-item-avatar tile size="125" class="v-mobile-img rounded-sm my-0 ml-n4">
|
||||
<v-img
|
||||
|
@ -31,6 +38,7 @@
|
|||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-card>
|
||||
</v-expand-transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -115,8 +115,5 @@ export default {
|
|||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.top-dialog {
|
||||
align-self: flex-start;
|
||||
}
|
||||
<style>
|
||||
</style>
|
||||
|
|
161
frontend/src/components/UI/Dialogs/SearchDialog.vue
Normal file
161
frontend/src/components/UI/Dialogs/SearchDialog.vue
Normal file
|
@ -0,0 +1,161 @@
|
|||
<template>
|
||||
<div>
|
||||
<slot v-bind="{ open, close }"> </slot>
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
:width="isMobile ? undefined : '700'"
|
||||
:height="isMobile ? undefined : '0'"
|
||||
:fullscreen="isMobile"
|
||||
content-class="top-dialog"
|
||||
:scrollable="false"
|
||||
>
|
||||
<v-app-bar sticky dark color="primary lighten-1" :rounded="!isMobile">
|
||||
<FuseSearchBar :raw-data="allItems" @results="filterItems" :search="searchString">
|
||||
<v-text-field
|
||||
id="arrow-search"
|
||||
autofocus
|
||||
v-model="searchString"
|
||||
solo
|
||||
flat
|
||||
autocomplete="off"
|
||||
background-color="primary lighten-1"
|
||||
color="white"
|
||||
dense
|
||||
:clearable="!isMobile"
|
||||
class="mx-2 arrow-search"
|
||||
hide-details
|
||||
single-line
|
||||
:placeholder="$t('search.search')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
>
|
||||
</v-text-field>
|
||||
</FuseSearchBar>
|
||||
<v-btn v-if="isMobile" x-small fab light @click="dialog = false">
|
||||
<v-icon>
|
||||
mdi-close
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-app-bar>
|
||||
<v-card class="mt-1 pa-1" relative>
|
||||
<v-card-actions>
|
||||
<div class="mr-auto">
|
||||
Results
|
||||
</div>
|
||||
<router-link to="/search"> Advanced Search </router-link>
|
||||
</v-card-actions>
|
||||
<MobileRecipeCard
|
||||
v-for="(recipe, index) in results.slice(0, 10)"
|
||||
:tabindex="index"
|
||||
:key="index"
|
||||
class="ma-1 arrow-nav"
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
:route="true"
|
||||
v-on="$listeners.selected ? { selected: () => grabRecipe(recipe) } : {}"
|
||||
/>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const SELECTED_EVENT = "selected";
|
||||
import FuseSearchBar from "@/components/UI/Search/FuseSearchBar";
|
||||
import MobileRecipeCard from "@/components/Recipe/MobileRecipeCard";
|
||||
export default {
|
||||
components: {
|
||||
FuseSearchBar,
|
||||
MobileRecipeCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedIndex: -1,
|
||||
dialog: false,
|
||||
searchString: "",
|
||||
searchResults: [],
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.dialog = false;
|
||||
},
|
||||
dialog(val) {
|
||||
if (!val) {
|
||||
this.resetSelected();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch("requestAllRecipes");
|
||||
document.addEventListener("keydown", this.onUpDown);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener("keydown", this.onUpDown);
|
||||
},
|
||||
|
||||
computed: {
|
||||
isMobile() {
|
||||
return this.$vuetify.breakpoint.name === "xs";
|
||||
},
|
||||
allItems() {
|
||||
return this.$store.getters.getAllRecipes;
|
||||
},
|
||||
results() {
|
||||
if (this.searchString != null && this.searchString.length >= 1) {
|
||||
return this.searchResults;
|
||||
}
|
||||
return this.allItems;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.dialog = true;
|
||||
},
|
||||
close() {
|
||||
this.dialog = false;
|
||||
},
|
||||
filterItems(val) {
|
||||
this.searchResults = val.map(x => x.item);
|
||||
},
|
||||
grabRecipe(recipe) {
|
||||
this.dialog = false;
|
||||
this.$emit(SELECTED_EVENT, recipe);
|
||||
},
|
||||
onUpDown(e) {
|
||||
if (e.keyCode === 38) {
|
||||
e.preventDefault();
|
||||
this.selectedIndex--;
|
||||
} else if (e.keyCode === 40) {
|
||||
e.preventDefault();
|
||||
this.selectedIndex++;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
this.selectRecipe();
|
||||
},
|
||||
resetSelected() {
|
||||
this.searchString = "";
|
||||
this.selectedIndex = -1;
|
||||
document.getElementsByClassName("arrow-nav")[0].focus();
|
||||
},
|
||||
selectRecipe() {
|
||||
const recipeCards = document.getElementsByClassName("arrow-nav");
|
||||
if (recipeCards) {
|
||||
if (this.selectedIndex < 0) {
|
||||
this.selectedIndex = -1;
|
||||
document.getElementById("arrow-search").focus();
|
||||
return;
|
||||
}
|
||||
this.selectedIndex >= recipeCards.length ? (this.selectedIndex = recipeCards.length - 1) : null;
|
||||
recipeCards[this.selectedIndex].focus();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style >
|
||||
</style>
|
|
@ -73,4 +73,8 @@ export default {
|
|||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
div {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,24 +1,20 @@
|
|||
<template>
|
||||
<v-menu v-model="menuModel" readonly offset-y offset-overflow max-height="75vh">
|
||||
<template #activator="{ attrs }">
|
||||
<SearchDialog ref="searchDialog">
|
||||
<template v-slot="{ open }">
|
||||
<v-text-field
|
||||
readonly
|
||||
@click="open"
|
||||
ref="searchInput"
|
||||
class="my-auto pt-1"
|
||||
v-model="search"
|
||||
v-bind="attrs"
|
||||
:dense="dense"
|
||||
class="my-auto mt-5 pt-1"
|
||||
dense
|
||||
light
|
||||
dark
|
||||
flat
|
||||
:placeholder="$t('search.search-mealie')"
|
||||
background-color="primary lighten-1"
|
||||
color="white"
|
||||
:solo="solo"
|
||||
:style="`max-width: ${maxWidth};`"
|
||||
@focus="onFocus"
|
||||
@blur="isFocused = false"
|
||||
autocomplete="off"
|
||||
:autofocus="autofocus"
|
||||
solo=""
|
||||
:style="`max-width: 450;`"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<v-icon color="grey lighten-3" size="29">
|
||||
|
@ -27,122 +23,24 @@
|
|||
</template>
|
||||
</v-text-field>
|
||||
</template>
|
||||
<v-card v-if="showResults" max-height="75vh" :max-width="maxWidth" scrollable>
|
||||
<v-card-text class="flex row mx-auto ">
|
||||
<div class="mr-auto">
|
||||
Results
|
||||
</div>
|
||||
<router-link to="/search"> Advanced Search </router-link>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-list scrollable v-if="autoResults">
|
||||
<v-list-item
|
||||
v-for="(item, index) in autoResults.slice(0, 15)"
|
||||
:key="index"
|
||||
:to="navOnClick ? `/recipe/${item.item.slug}` : null"
|
||||
@click="navOnClick ? null : selected(item.item.slug, item.item.name)"
|
||||
>
|
||||
<v-list-item-avatar>
|
||||
<v-img :src="getImage(item.item.slug)"></v-img>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content @click="showResults ? null : selected(item.item.slug, item.item.name)">
|
||||
<v-list-item-title v-html="highlight(item.item.name)"> </v-list-item-title>
|
||||
<v-rating dense v-if="item.item.rating" :value="item.item.rating" size="12"> </v-rating>
|
||||
<v-list-item-subtitle v-html="highlight(item.item.description)"> </v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</SearchDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Fuse from "fuse.js";
|
||||
import { api } from "@/api";
|
||||
import SearchDialog from "@/components/UI/Dialogs/SearchDialog";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
showResults: {
|
||||
default: false,
|
||||
},
|
||||
maxWidth: {
|
||||
default: "450px",
|
||||
},
|
||||
dense: {
|
||||
default: true,
|
||||
},
|
||||
navOnClick: {
|
||||
default: true,
|
||||
},
|
||||
solo: {
|
||||
default: true,
|
||||
},
|
||||
autofocus: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isFocused: false,
|
||||
searchSlug: "",
|
||||
search: "",
|
||||
menuModel: false,
|
||||
result: [],
|
||||
fuseResults: [],
|
||||
options: {
|
||||
shouldSort: true,
|
||||
threshold: 0.6,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
findAllMatches: true,
|
||||
maxPatternLength: 32,
|
||||
minMatchCharLength: 2,
|
||||
keys: ["name", "description"],
|
||||
},
|
||||
};
|
||||
components: {
|
||||
SearchDialog,
|
||||
},
|
||||
|
||||
mounted() {
|
||||
document.addEventListener("keydown", this.onDocumentKeydown);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener("keydown", this.onDocumentKeydown);
|
||||
},
|
||||
computed: {
|
||||
data() {
|
||||
return this.$store.getters.getAllRecipes;
|
||||
},
|
||||
autoResults() {
|
||||
return this.fuseResults.length > 1 ? this.fuseResults : this.results;
|
||||
},
|
||||
fuse() {
|
||||
return new Fuse(this.data, this.options);
|
||||
},
|
||||
isSearching() {
|
||||
return this.search && this.search.length > 0;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
isSearching(val) {
|
||||
val ? (this.menuModel = true) : this.resetSearch();
|
||||
},
|
||||
|
||||
search() {
|
||||
try {
|
||||
this.result = this.fuse.search(this.search.trim());
|
||||
} catch {
|
||||
this.result = this.data.map(x => ({ item: x })).sort((a, b) => (a.name > b.name ? 1 : -1));
|
||||
}
|
||||
this.$emit("results", this.result);
|
||||
|
||||
if (this.showResults === true) {
|
||||
this.fuseResults = this.result;
|
||||
}
|
||||
},
|
||||
|
||||
searchSlug() {
|
||||
this.selected(this.searchSlug);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
highlight(string) {
|
||||
if (!this.search) {
|
||||
|
@ -150,23 +48,7 @@ export default {
|
|||
}
|
||||
return string.replace(new RegExp(this.search, "gi"), match => `<mark>${match}</mark>`);
|
||||
},
|
||||
getImage(image) {
|
||||
return api.recipes.recipeTinyImage(image);
|
||||
},
|
||||
selected(slug, name) {
|
||||
this.$emit("selected", slug, name);
|
||||
},
|
||||
async onFocus() {
|
||||
this.$store.dispatch("requestAllRecipes");
|
||||
this.isFocused = true;
|
||||
},
|
||||
resetSearch() {
|
||||
this.$nextTick(() => {
|
||||
this.search = "";
|
||||
this.isFocused = false;
|
||||
this.menuModel = false;
|
||||
});
|
||||
},
|
||||
|
||||
onDocumentKeydown(e) {
|
||||
if (
|
||||
e.key === "/" &&
|
||||
|
@ -174,23 +56,10 @@ export default {
|
|||
!document.activeElement.id.startsWith("input")
|
||||
) {
|
||||
e.preventDefault();
|
||||
this.$refs.searchInput.focus();
|
||||
this.$refs.searchDialog.open();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.color-transition {
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
.v-menu__content
|
||||
width: 100
|
||||
&, & > *
|
||||
display: flex
|
||||
flex-direction: column
|
||||
</style>
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
<template>
|
||||
<div class="text-center ">
|
||||
<v-dialog v-model="dialog" width="600px" height="0" :fullscreen="isMobile" content-class="top-dialog">
|
||||
<v-card>
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
:width="isMobile ? undefined : '600'"
|
||||
:height="isMobile ? undefined : '0'"
|
||||
:fullscreen="isMobile"
|
||||
content-class="top-dialog"
|
||||
>
|
||||
<v-card relative>
|
||||
<v-app-bar dark color="primary lighten-1" rounded="0">
|
||||
<SearchBar
|
||||
ref="mealSearchBar"
|
||||
@results="updateResults"
|
||||
@selected="emitSelect"
|
||||
:show-results="!isMobile"
|
||||
max-width="568"
|
||||
:dense="false"
|
||||
:nav-on-click="false"
|
||||
:autofocus="true"
|
||||
/>
|
||||
<v-btn icon @click="dialog = false" class="mt-1">
|
||||
<v-icon> mdi-close </v-icon>
|
||||
</v-btn>
|
||||
</v-app-bar>
|
||||
<v-card-text v-if="isMobile">
|
||||
<div v-for="recipe in searchResults.slice(0, 7)" :key="recipe.name">
|
||||
|
@ -31,6 +33,9 @@
|
|||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-btn v-if="isMobile" fab bottom @click="dialog = false" class="ma-2">
|
||||
<v-icon> mdi-close </v-icon>
|
||||
</v-btn>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
<script>
|
||||
import TheSiteMenu from "@/components/UI/TheSiteMenu";
|
||||
import SearchBar from "@/components/UI/Search/SearchBar";
|
||||
import SearchDialog from "@/components/UI/Search/SearchDialog";
|
||||
import SearchDialog from "@/components/UI/Dialogs/SearchDialog";
|
||||
import TheRecipeFab from "@/components/UI/TheRecipeFab";
|
||||
import TheSidebar from "@/components/UI/TheSidebar";
|
||||
import { user } from "@/mixins/user";
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue