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" |       ref="deleteRecipieConfirm" | ||||||
|       v-on:confirm="deleteRecipe()" |       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 }"> |       <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-icon>{{ menuIcon }}</v-icon> | ||||||
|         </v-btn> |         </v-btn> | ||||||
|       </template> |       </template> | ||||||
|       <v-list dense> |       <v-list dense> | ||||||
|         <v-list-item |         <v-list-item | ||||||
|           v-for="(item, index) in loggedIn ? userMenu : defaultMenu" |           v-for="(item, index) in loggedIn && cardMenu ? userMenu : defaultMenu" | ||||||
|           :key="index" |           :key="index" | ||||||
|           @click="menuAction(item.action)" |           @click="menuAction(item.action)" | ||||||
|         > |         > | ||||||
| @@ -39,6 +47,18 @@ export default { | |||||||
|     ConfirmationDialog, |     ConfirmationDialog, | ||||||
|   }, |   }, | ||||||
|   props: { |   props: { | ||||||
|  |     menuTop: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: true, | ||||||
|  |     }, | ||||||
|  |     fab: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|  |     color: { | ||||||
|  |       type: String, | ||||||
|  |       default: "primary", | ||||||
|  |     }, | ||||||
|     slug: { |     slug: { | ||||||
|       type: String, |       type: String, | ||||||
|     }, |     }, | ||||||
| @@ -48,6 +68,10 @@ export default { | |||||||
|     name: { |     name: { | ||||||
|       type: String, |       type: String, | ||||||
|     }, |     }, | ||||||
|  |     cardMenu: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: true, | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     loggedIn() { |     loggedIn() { | ||||||
| @@ -118,7 +142,7 @@ export default { | |||||||
|                 url: this.recipeURL, |                 url: this.recipeURL, | ||||||
|               }) |               }) | ||||||
|               .then(() => console.log("Successful share")) |               .then(() => console.log("Successful share")) | ||||||
|               .catch(error => { |               .catch((error) => { | ||||||
|                 console.log("WebShareAPI not supported", error); |                 console.log("WebShareAPI not supported", error); | ||||||
|                 this.updateClipboard(); |                 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> |       </v-img> | ||||||
|       <br v-else /> |       <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"> |       <div v-if="jsonEditor"> | ||||||
|         <!-- Probably not the best way, but it works! --> |         <!-- Probably not the best way, but it works! --> | ||||||
| @@ -29,12 +29,12 @@ import { api } from "@/api"; | |||||||
|  |  | ||||||
| import RecipeEditor from "@/components/Recipe/RecipeEditor"; | import RecipeEditor from "@/components/Recipe/RecipeEditor"; | ||||||
| import VJsoneditor from "v-jsoneditor"; | import VJsoneditor from "v-jsoneditor"; | ||||||
| import EditorButtonRow from "@/components/Recipe/EditorButtonRow"; | import RecipePageActionMenu from "@/components/Recipe/RecipePageActionMenu"; | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     VJsoneditor, |     VJsoneditor, | ||||||
|     RecipeEditor, |     RecipeEditor, | ||||||
|     EditorButtonRow, |     RecipePageActionMenu, | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ | |||||||
|     <NoRecipe v-else-if="loadFailed" /> |     <NoRecipe v-else-if="loadFailed" /> | ||||||
|     <v-card v-else-if="!loadFailed" id="myRecipe" class="d-print-none"> |     <v-card v-else-if="!loadFailed" id="myRecipe" class="d-print-none"> | ||||||
|       <v-img |       <v-img | ||||||
|         :height="hideImage ? '40' : imageHeight" |         :height="hideImage ? '50' : imageHeight" | ||||||
|         @error="hideImage = true" |         @error="hideImage = true" | ||||||
|         :src="getImage(recipeDetails.slug)" |         :src="getImage(recipeDetails.slug)" | ||||||
|         class="d-print-none" |         class="d-print-none" | ||||||
| @@ -20,17 +20,20 @@ | |||||||
|           :performTime="recipeDetails.performTime" |           :performTime="recipeDetails.performTime" | ||||||
|         /> |         /> | ||||||
|       </v-img> |       </v-img> | ||||||
|       <EditorButtonRow |       <RecipePageActionMenu | ||||||
|  |         :slug="recipeDetails.slug" | ||||||
|  |         :name="recipeDetails.name" | ||||||
|         v-if="loggedIn" |         v-if="loggedIn" | ||||||
|         :open="showIcons" |         :open="showIcons" | ||||||
|         @json="jsonEditor = true" |         @close="form = false" | ||||||
|         @editor=" |         @json="jsonEditor = !jsonEditor" | ||||||
|  |         @edit=" | ||||||
|           jsonEditor = false; |           jsonEditor = false; | ||||||
|           form = true; |           form = true; | ||||||
|         " |         " | ||||||
|         @save="saveRecipe" |         @save="saveRecipe" | ||||||
|         @delete="deleteRecipe" |         @delete="deleteRecipe" | ||||||
|         class="sticky" |         class="ml-auto" | ||||||
|       /> |       /> | ||||||
|  |  | ||||||
|       <RecipeViewer v-if="!form" :recipe="recipeDetails" /> |       <RecipeViewer v-if="!form" :recipe="recipeDetails" /> | ||||||
| @@ -42,7 +45,13 @@ | |||||||
|         height="1500px" |         height="1500px" | ||||||
|         :options="jsonEditorOptions" |         :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> |     </v-card> | ||||||
|     <CommentsSection |     <CommentsSection | ||||||
|       class="mt-2 d-print-none" |       class="mt-2 d-print-none" | ||||||
| @@ -56,6 +65,7 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
|  | import RecipePageActionMenu from "@/components/Recipe/RecipePageActionMenu.vue"; | ||||||
| import { api } from "@/api"; | import { api } from "@/api"; | ||||||
| import FavoriteBadge from "@/components/Recipe/FavoriteBadge"; | import FavoriteBadge from "@/components/Recipe/FavoriteBadge"; | ||||||
| import VJsoneditor from "v-jsoneditor"; | import VJsoneditor from "v-jsoneditor"; | ||||||
| @@ -63,7 +73,6 @@ import RecipeViewer from "@/components/Recipe/RecipeViewer"; | |||||||
| import PrintView from "@/components/Recipe/PrintView"; | import PrintView from "@/components/Recipe/PrintView"; | ||||||
| import RecipeEditor from "@/components/Recipe/RecipeEditor"; | import RecipeEditor from "@/components/Recipe/RecipeEditor"; | ||||||
| import RecipeTimeCard from "@/components/Recipe/RecipeTimeCard.vue"; | import RecipeTimeCard from "@/components/Recipe/RecipeTimeCard.vue"; | ||||||
| import EditorButtonRow from "@/components/Recipe/EditorButtonRow.vue"; |  | ||||||
| import NoRecipe from "@/components/Fallbacks/NoRecipe"; | import NoRecipe from "@/components/Fallbacks/NoRecipe"; | ||||||
| import { user } from "@/mixins/user"; | import { user } from "@/mixins/user"; | ||||||
| import { router } from "@/routes"; | import { router } from "@/routes"; | ||||||
| @@ -74,8 +83,8 @@ export default { | |||||||
|     VJsoneditor, |     VJsoneditor, | ||||||
|     RecipeViewer, |     RecipeViewer, | ||||||
|     RecipeEditor, |     RecipeEditor, | ||||||
|     EditorButtonRow, |  | ||||||
|     RecipeTimeCard, |     RecipeTimeCard, | ||||||
|  |     RecipePageActionMenu, | ||||||
|     PrintView, |     PrintView, | ||||||
|     NoRecipe, |     NoRecipe, | ||||||
|     FavoriteBadge, |     FavoriteBadge, | ||||||
| @@ -126,15 +135,13 @@ export default { | |||||||
|     this.jsonEditor = false; |     this.jsonEditor = false; | ||||||
|     this.form = this.$route.query.edit === "true" && this.loggedIn; |     this.form = this.$route.query.edit === "true" && this.loggedIn; | ||||||
|  |  | ||||||
|     if (this.$route.query.print) { |     this.checkPrintRecipe(); | ||||||
|       this.printPage(); |  | ||||||
|       this.$router.push(this.$route.path); |  | ||||||
|     } |  | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   watch: { |   watch: { | ||||||
|     $route: function () { |     $route: function () { | ||||||
|       this.getRecipeDetails(); |       this.getRecipeDetails(); | ||||||
|  |       this.checkPrintRecipe(); | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
| @@ -166,6 +173,13 @@ export default { | |||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|  |     checkPrintRecipe() { | ||||||
|  |       if (this.$route.query.print) { | ||||||
|  |         this.printPage(); | ||||||
|  |         this.$router.push(this.$route.path); | ||||||
|  |         this.$route.query.print = null; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     getImageFile(fileObject) { |     getImageFile(fileObject) { | ||||||
|       this.fileObject = fileObject; |       this.fileObject = fileObject; | ||||||
|       this.saveImage(); |       this.saveImage(); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user