mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 02:03:35 -04:00 
			
		
		
		
	Rewrite Recipe Editor Buttons Bar (#482)
* rewrite editor button row * add context menu items to recipe page Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
		| @@ -8,15 +8,23 @@ | ||||
|       ref="deleteRecipieConfirm" | ||||
|       v-on:confirm="deleteRecipe()" | ||||
|     /> | ||||
|     <v-menu offset-y top left> | ||||
|     <v-menu | ||||
|       offset-y | ||||
|       left | ||||
|       :bottom="!menuTop" | ||||
|       :nudge-bottom="!menuTop ? '5' : '0'" | ||||
|       :top="menuTop" | ||||
|       :nudge-top="menuTop ? '5' : '0'" | ||||
|       allow-overflow | ||||
|     > | ||||
|       <template v-slot:activator="{ on, attrs }"> | ||||
|         <v-btn color="primary" icon dark v-bind="attrs" v-on="on" @click.prevent> | ||||
|         <v-btn :fab="fab" small="fab" :color="color" :icon="!fab" dark v-bind="attrs" v-on="on" @click.prevent> | ||||
|           <v-icon>{{ menuIcon }}</v-icon> | ||||
|         </v-btn> | ||||
|       </template> | ||||
|       <v-list dense> | ||||
|         <v-list-item | ||||
|           v-for="(item, index) in loggedIn ? userMenu : defaultMenu" | ||||
|           v-for="(item, index) in loggedIn && cardMenu ? userMenu : defaultMenu" | ||||
|           :key="index" | ||||
|           @click="menuAction(item.action)" | ||||
|         > | ||||
| @@ -39,6 +47,18 @@ export default { | ||||
|     ConfirmationDialog, | ||||
|   }, | ||||
|   props: { | ||||
|     menuTop: { | ||||
|       type: Boolean, | ||||
|       default: true, | ||||
|     }, | ||||
|     fab: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|     color: { | ||||
|       type: String, | ||||
|       default: "primary", | ||||
|     }, | ||||
|     slug: { | ||||
|       type: String, | ||||
|     }, | ||||
| @@ -48,6 +68,10 @@ export default { | ||||
|     name: { | ||||
|       type: String, | ||||
|     }, | ||||
|     cardMenu: { | ||||
|       type: Boolean, | ||||
|       default: true, | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     loggedIn() { | ||||
| @@ -118,7 +142,7 @@ export default { | ||||
|                 url: this.recipeURL, | ||||
|               }) | ||||
|               .then(() => console.log("Successful share")) | ||||
|               .catch(error => { | ||||
|               .catch((error) => { | ||||
|                 console.log("WebShareAPI not supported", error); | ||||
|                 this.updateClipboard(); | ||||
|               }); | ||||
|   | ||||
| @@ -1,97 +0,0 @@ | ||||
| <template> | ||||
|   <v-expand-transition> | ||||
|     <v-toolbar | ||||
|       class="card-btn pt-1" | ||||
|       flat | ||||
|       :height="isSticky ? null : '0'" | ||||
|       :extension-height="isSticky ? '20' : '0'" | ||||
|       color="rgb(255, 0, 0, 0.0)" | ||||
|     > | ||||
|       <ConfirmationDialog | ||||
|         :title="$t('recipe.delete-recipe')" | ||||
|         :message="$t('recipe.delete-confirmation')" | ||||
|         color="error" | ||||
|         icon="mdi-alert-circle" | ||||
|         ref="deleteRecipieConfirm" | ||||
|         v-on:confirm="deleteRecipe()" | ||||
|       /> | ||||
|       <template v-slot:extension> | ||||
|         <v-col></v-col> | ||||
|         <div v-if="open"> | ||||
|           <v-btn class="mr-2" fab dark small color="error" @click="deleteRecipeConfrim"> | ||||
|             <v-icon>{{ $globals.icons.delete }}</v-icon> | ||||
|           </v-btn> | ||||
|  | ||||
|           <v-btn class="mr-2" fab dark small color="success" @click="save"> | ||||
|             <v-icon>{{ $globals.icons.save }}</v-icon> | ||||
|           </v-btn> | ||||
|           <v-btn class="mr-5" fab dark small color="secondary" @click="json"> | ||||
|             <v-icon>mdi-code-braces</v-icon> | ||||
|           </v-btn> | ||||
|         </div> | ||||
|         <v-btn color="accent" fab dark small @click="editor"> | ||||
|           <v-icon>{{ $globals.icons.edit }}</v-icon> | ||||
|         </v-btn> | ||||
|       </template> | ||||
|     </v-toolbar> | ||||
|   </v-expand-transition> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog.vue"; | ||||
|  | ||||
| export default { | ||||
|   props: { | ||||
|     open: { | ||||
|       type: Boolean, | ||||
|       default: true, | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   components: { | ||||
|     ConfirmationDialog, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       stickyTop: 50, | ||||
|       scrollPosition: null, | ||||
|     }; | ||||
|   }, | ||||
|   mounted() { | ||||
|     window.addEventListener("scroll", this.updateScroll); | ||||
|   }, | ||||
|   destroy() { | ||||
|     window.removeEventListener("scroll", this.updateScroll); | ||||
|   }, | ||||
|  | ||||
|   computed: { | ||||
|     isSticky() { | ||||
|       return this.scrollPosition >= 500; | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   methods: { | ||||
|     editor() { | ||||
|       this.$emit("editor"); | ||||
|     }, | ||||
|     save() { | ||||
|       this.$emit("save"); | ||||
|     }, | ||||
|     updateScroll() { | ||||
|       this.scrollPosition = window.scrollY; | ||||
|     }, | ||||
|  | ||||
|     deleteRecipeConfrim() { | ||||
|       this.$refs.deleteRecipieConfirm.open(); | ||||
|     }, | ||||
|     deleteRecipe() { | ||||
|       this.$emit("delete"); | ||||
|     }, | ||||
|     json() { | ||||
|       this.$emit("json"); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style></style> | ||||
							
								
								
									
										164
									
								
								frontend/src/components/Recipe/RecipePageActionMenu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								frontend/src/components/Recipe/RecipePageActionMenu.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,164 @@ | ||||
| <template> | ||||
|   <v-toolbar | ||||
|     rounded | ||||
|     height="0" | ||||
|     class="fixed-bar mt-0" | ||||
|     color="rgb(255, 0, 0, 0.0)" | ||||
|     flat | ||||
|     style="z-index: 2; position: sticky" | ||||
|     :class="{ 'fixed-bar-mobile': $vuetify.breakpoint.xs }" | ||||
|   > | ||||
|     <ConfirmationDialog | ||||
|       :title="$t('recipe.delete-recipe')" | ||||
|       :message="$t('recipe.delete-confirmation')" | ||||
|       color="error" | ||||
|       icon="mdi-alert-circle" | ||||
|       ref="deleteRecipieConfirm" | ||||
|       v-on:confirm="emitDelete()" | ||||
|     /> | ||||
|     <v-spacer></v-spacer> | ||||
|     <div v-if="!edit" class="custom-btn-group ma-1"> | ||||
|       <v-btn | ||||
|         fab | ||||
|         small | ||||
|         class="mx-1" | ||||
|         color="info" | ||||
|         @click=" | ||||
|           edit = true; | ||||
|           $emit('edit'); | ||||
|         " | ||||
|       > | ||||
|         <v-icon> {{ $globals.icons.edit }} </v-icon> | ||||
|       </v-btn> | ||||
|       <ContextMenu | ||||
|         :menu-top="false" | ||||
|         :slug="slug" | ||||
|         :name="name" | ||||
|         menu-icon="mdi-dots-horizontal" | ||||
|         fab | ||||
|         color="info" | ||||
|         :card-menu="false" | ||||
|       /> | ||||
|     </div> | ||||
|     <div v-if="edit" class="custom-btn-group mb-"> | ||||
|       <v-btn | ||||
|         v-for="(btn, index) in editorButtons" | ||||
|         :key="index" | ||||
|         :fab="$vuetify.breakpoint.xs" | ||||
|         :small="$vuetify.breakpoint.xs" | ||||
|         class="mx-1" | ||||
|         :color="btn.color" | ||||
|         @click="emitHandler(btn.event)" | ||||
|       > | ||||
|         <v-icon :left="!$vuetify.breakpoint.xs">{{ btn.icon }}</v-icon> | ||||
|         {{ $vuetify.breakpoint.xs ? "" : btn.text }} | ||||
|       </v-btn> | ||||
|     </div> | ||||
|   </v-toolbar> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog.vue"; | ||||
| import ContextMenu from "@/components/Recipe/ContextMenu.vue"; | ||||
| const SAVE_EVENT = "save"; | ||||
| const DELETE_EVENT = "delete"; | ||||
| const CLOSE_EVENT = "close"; | ||||
| const JSON_EVENT = "json"; | ||||
|  | ||||
| export default { | ||||
|   components: { ConfirmationDialog, ContextMenu }, | ||||
|   props: { | ||||
|     slug: { | ||||
|       type: String, | ||||
|     }, | ||||
|     name: { | ||||
|       type: String, | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       edit: false, | ||||
|       editorButtons: [ | ||||
|         { | ||||
|           text: "Delete", | ||||
|           icon: this.$globals.icons.delete, | ||||
|           event: DELETE_EVENT, | ||||
|           color: "error", | ||||
|         }, | ||||
|         { | ||||
|           text: "JSON", | ||||
|           icon: "mdi-code-braces", | ||||
|           event: JSON_EVENT, | ||||
|           color: "accent", | ||||
|         }, | ||||
|         { | ||||
|           text: "Close", | ||||
|           icon: "mdi-close", | ||||
|           event: CLOSE_EVENT, | ||||
|           color: undefined, | ||||
|         }, | ||||
|         { | ||||
|           text: "Save", | ||||
|           icon: this.$globals.icons.save, | ||||
|           event: SAVE_EVENT, | ||||
|           color: "success", | ||||
|         }, | ||||
|       ], | ||||
|     }; | ||||
|   }, | ||||
|   methods: { | ||||
|     emitHandler(event) { | ||||
|       switch (event) { | ||||
|         case CLOSE_EVENT: | ||||
|           this.$emit(CLOSE_EVENT); | ||||
|           this.edit = false; | ||||
|           break; | ||||
|         case SAVE_EVENT: | ||||
|           this.$emit(SAVE_EVENT); | ||||
|           this.edit = false; | ||||
|           break; | ||||
|         case JSON_EVENT: | ||||
|           this.$emit(JSON_EVENT); | ||||
|           break; | ||||
|         case DELETE_EVENT: | ||||
|           this.$refs.deleteRecipieConfirm.open(); | ||||
|           break; | ||||
|         default: | ||||
|           break; | ||||
|       } | ||||
|     }, | ||||
|     emitDelete() { | ||||
|       this.$emit(DELETE_EVENT); | ||||
|       this.edit = false; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
| .custom-btn-group { | ||||
|   flex: 0, 1, auto; | ||||
|   display: inline-flex; | ||||
| } | ||||
|  | ||||
| .vertical { | ||||
|   flex-direction: column !important; | ||||
| } | ||||
|  | ||||
| .sticky { | ||||
|   margin-left: auto; | ||||
|   position: fixed !important; | ||||
|   margin-top: 4.25rem; | ||||
| } | ||||
|  | ||||
| .fixed-bar { | ||||
|   position: sticky; | ||||
|   position: -webkit-sticky; /* for Safari */ | ||||
|   top: 4.5em; | ||||
|   z-index: 2; | ||||
| } | ||||
|  | ||||
| .fixed-bar-mobile { | ||||
|   top: 1.5em !important; | ||||
| } | ||||
| </style> | ||||
| @@ -10,7 +10,7 @@ | ||||
|       </v-img> | ||||
|       <br v-else /> | ||||
|  | ||||
|       <EditorButtonRow @json="jsonEditor = true" @editor="jsonEditor = false" @save="createRecipe" /> | ||||
|       <RecipePageActionMenu @json="jsonEditor = true" @edit="jsonEditor = false" @save="createRecipe" /> | ||||
|  | ||||
|       <div v-if="jsonEditor"> | ||||
|         <!-- Probably not the best way, but it works! --> | ||||
| @@ -29,12 +29,12 @@ import { api } from "@/api"; | ||||
|  | ||||
| import RecipeEditor from "@/components/Recipe/RecipeEditor"; | ||||
| import VJsoneditor from "v-jsoneditor"; | ||||
| import EditorButtonRow from "@/components/Recipe/EditorButtonRow"; | ||||
| import RecipePageActionMenu from "@/components/Recipe/RecipePageActionMenu"; | ||||
| export default { | ||||
|   components: { | ||||
|     VJsoneditor, | ||||
|     RecipeEditor, | ||||
|     EditorButtonRow, | ||||
|     RecipePageActionMenu, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|   | ||||
| @@ -6,7 +6,7 @@ | ||||
|     <NoRecipe v-else-if="loadFailed" /> | ||||
|     <v-card v-else-if="!loadFailed" id="myRecipe" class="d-print-none"> | ||||
|       <v-img | ||||
|         :height="hideImage ? '40' : imageHeight" | ||||
|         :height="hideImage ? '50' : imageHeight" | ||||
|         @error="hideImage = true" | ||||
|         :src="getImage(recipeDetails.slug)" | ||||
|         class="d-print-none" | ||||
| @@ -20,17 +20,20 @@ | ||||
|           :performTime="recipeDetails.performTime" | ||||
|         /> | ||||
|       </v-img> | ||||
|       <EditorButtonRow | ||||
|       <RecipePageActionMenu | ||||
|         :slug="recipeDetails.slug" | ||||
|         :name="recipeDetails.name" | ||||
|         v-if="loggedIn" | ||||
|         :open="showIcons" | ||||
|         @json="jsonEditor = true" | ||||
|         @editor=" | ||||
|         @close="form = false" | ||||
|         @json="jsonEditor = !jsonEditor" | ||||
|         @edit=" | ||||
|           jsonEditor = false; | ||||
|           form = true; | ||||
|         " | ||||
|         @save="saveRecipe" | ||||
|         @delete="deleteRecipe" | ||||
|         class="sticky" | ||||
|         class="ml-auto" | ||||
|       /> | ||||
|  | ||||
|       <RecipeViewer v-if="!form" :recipe="recipeDetails" /> | ||||
| @@ -42,7 +45,13 @@ | ||||
|         height="1500px" | ||||
|         :options="jsonEditorOptions" | ||||
|       /> | ||||
|       <RecipeEditor v-else v-model="recipeDetails" ref="recipeEditor" @upload="getImageFile" /> | ||||
|       <RecipeEditor | ||||
|         v-else | ||||
|         v-model="recipeDetails" | ||||
|         :class="$vuetify.breakpoint.xs ? 'mt-5' : undefiend" | ||||
|         ref="recipeEditor" | ||||
|         @upload="getImageFile" | ||||
|       /> | ||||
|     </v-card> | ||||
|     <CommentsSection | ||||
|       class="mt-2 d-print-none" | ||||
| @@ -56,6 +65,7 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import RecipePageActionMenu from "@/components/Recipe/RecipePageActionMenu.vue"; | ||||
| import { api } from "@/api"; | ||||
| import FavoriteBadge from "@/components/Recipe/FavoriteBadge"; | ||||
| import VJsoneditor from "v-jsoneditor"; | ||||
| @@ -63,7 +73,6 @@ import RecipeViewer from "@/components/Recipe/RecipeViewer"; | ||||
| import PrintView from "@/components/Recipe/PrintView"; | ||||
| import RecipeEditor from "@/components/Recipe/RecipeEditor"; | ||||
| import RecipeTimeCard from "@/components/Recipe/RecipeTimeCard.vue"; | ||||
| import EditorButtonRow from "@/components/Recipe/EditorButtonRow.vue"; | ||||
| import NoRecipe from "@/components/Fallbacks/NoRecipe"; | ||||
| import { user } from "@/mixins/user"; | ||||
| import { router } from "@/routes"; | ||||
| @@ -74,8 +83,8 @@ export default { | ||||
|     VJsoneditor, | ||||
|     RecipeViewer, | ||||
|     RecipeEditor, | ||||
|     EditorButtonRow, | ||||
|     RecipeTimeCard, | ||||
|     RecipePageActionMenu, | ||||
|     PrintView, | ||||
|     NoRecipe, | ||||
|     FavoriteBadge, | ||||
| @@ -126,15 +135,13 @@ export default { | ||||
|     this.jsonEditor = false; | ||||
|     this.form = this.$route.query.edit === "true" && this.loggedIn; | ||||
|  | ||||
|     if (this.$route.query.print) { | ||||
|       this.printPage(); | ||||
|       this.$router.push(this.$route.path); | ||||
|     } | ||||
|     this.checkPrintRecipe(); | ||||
|   }, | ||||
|  | ||||
|   watch: { | ||||
|     $route: function () { | ||||
|       this.getRecipeDetails(); | ||||
|       this.checkPrintRecipe(); | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
| @@ -166,6 +173,13 @@ export default { | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     checkPrintRecipe() { | ||||
|       if (this.$route.query.print) { | ||||
|         this.printPage(); | ||||
|         this.$router.push(this.$route.path); | ||||
|         this.$route.query.print = null; | ||||
|       } | ||||
|     }, | ||||
|     getImageFile(fileObject) { | ||||
|       this.fileObject = fileObject; | ||||
|       this.saveImage(); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user