| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | <template> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |   <section @keyup.ctrl.z="undoMerge"> | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |     <!-- Ingredient Link Editor --> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |     <v-dialog | 
					
						
							|  |  |  |       v-if="dialog" | 
					
						
							|  |  |  |       v-model="dialog" | 
					
						
							|  |  |  |       width="600" | 
					
						
							|  |  |  |     > | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |       <v-card :ripple="false"> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |         <v-sheet | 
					
						
							|  |  |  |           color="primary" | 
					
						
							|  |  |  |           class="mt-n1 mb-3 pa-3 d-flex align-center" | 
					
						
							|  |  |  |           style="border-radius: 6px; width: 100%;" | 
					
						
							|  |  |  |         > | 
					
						
							|  |  |  |           <v-icon | 
					
						
							|  |  |  |             size="large" | 
					
						
							|  |  |  |             start | 
					
						
							|  |  |  |           > | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |             {{ $globals.icons.link }} | 
					
						
							|  |  |  |           </v-icon> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |           <v-toolbar-title class="headline"> | 
					
						
							|  |  |  |             {{ $t("recipe.ingredient-linker") }} | 
					
						
							|  |  |  |           </v-toolbar-title> | 
					
						
							|  |  |  |           <v-spacer /> | 
					
						
							|  |  |  |         </v-sheet> | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         <v-card-text class="pt-4"> | 
					
						
							|  |  |  |           <p> | 
					
						
							|  |  |  |             {{ activeText }} | 
					
						
							|  |  |  |           </p> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |           <v-divider class="mb-4" /> | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |           <v-checkbox | 
					
						
							|  |  |  |             v-for="ing in unusedIngredients" | 
					
						
							|  |  |  |             :key="ing.referenceId" | 
					
						
							|  |  |  |             v-model="activeRefs" | 
					
						
							|  |  |  |             :value="ing.referenceId" | 
					
						
							|  |  |  |             class="mb-n2 mt-n2" | 
					
						
							|  |  |  |           > | 
					
						
							|  |  |  |             <template #label> | 
					
						
							|  |  |  |               <RecipeIngredientHtml :markup="parseIngredientText(ing, recipe.settings.disableAmount)" /> | 
					
						
							|  |  |  |             </template> | 
					
						
							|  |  |  |           </v-checkbox> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           <template v-if="usedIngredients.length > 0"> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |             <h4 class="py-3 ml-1"> | 
					
						
							|  |  |  |               {{ $t("recipe.linked-to-other-step") }} | 
					
						
							|  |  |  |             </h4> | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |             <v-checkbox | 
					
						
							|  |  |  |               v-for="ing in usedIngredients" | 
					
						
							|  |  |  |               :key="ing.referenceId" | 
					
						
							|  |  |  |               v-model="activeRefs" | 
					
						
							|  |  |  |               :value="ing.referenceId" | 
					
						
							|  |  |  |               class="mb-n2 mt-n2" | 
					
						
							|  |  |  |             > | 
					
						
							|  |  |  |               <template #label> | 
					
						
							|  |  |  |                 <RecipeIngredientHtml :markup="parseIngredientText(ing, recipe.settings.disableAmount)" /> | 
					
						
							|  |  |  |               </template> | 
					
						
							|  |  |  |             </v-checkbox> | 
					
						
							|  |  |  |           </template> | 
					
						
							|  |  |  |         </v-card-text> | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |         <v-divider /> | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         <v-card-actions> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |           <BaseButton | 
					
						
							|  |  |  |             cancel | 
					
						
							|  |  |  |             @click="dialog = false" | 
					
						
							|  |  |  |           /> | 
					
						
							|  |  |  |           <v-spacer /> | 
					
						
							| 
									
										
										
										
											2023-11-24 10:40:35 +01:00
										 |  |  |           <div class="d-flex flex-wrap justify-end"> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |             <BaseButton | 
					
						
							|  |  |  |               class="my-1" | 
					
						
							|  |  |  |               color="info" | 
					
						
							|  |  |  |               @click="autoSetReferences" | 
					
						
							|  |  |  |             > | 
					
						
							|  |  |  |               <template #icon> | 
					
						
							|  |  |  |                 {{ $globals.icons.robot }} | 
					
						
							|  |  |  |               </template> | 
					
						
							| 
									
										
										
										
											2023-11-24 10:40:35 +01:00
										 |  |  |               {{ $t("recipe.auto") }} | 
					
						
							|  |  |  |             </BaseButton> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |             <BaseButton | 
					
						
							|  |  |  |               class="ml-2 my-1" | 
					
						
							|  |  |  |               save | 
					
						
							|  |  |  |               @click="setIngredientIds" | 
					
						
							|  |  |  |             /> | 
					
						
							|  |  |  |             <BaseButton | 
					
						
							|  |  |  |               v-if="availableNextStep" | 
					
						
							|  |  |  |               class="ml-2 my-1" | 
					
						
							|  |  |  |               @click="saveAndOpenNextLinkIngredients" | 
					
						
							|  |  |  |             > | 
					
						
							|  |  |  |               <template #icon> | 
					
						
							|  |  |  |                 {{ $globals.icons.forward }} | 
					
						
							|  |  |  |               </template> | 
					
						
							| 
									
										
										
										
											2024-01-13 16:45:59 +01:00
										 |  |  |               {{ $t("recipe.nextStep") }} | 
					
						
							|  |  |  |             </BaseButton> | 
					
						
							| 
									
										
										
										
											2023-11-24 10:40:35 +01:00
										 |  |  |           </div> | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |         </v-card-actions> | 
					
						
							|  |  |  |       </v-card> | 
					
						
							|  |  |  |     </v-dialog> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     <div class="d-flex justify-space-between justify-start"> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |       <h2 | 
					
						
							|  |  |  |         v-if="!isCookMode" | 
					
						
							|  |  |  |         class="mt-1 text-h5 font-weight-medium opacity-80" | 
					
						
							|  |  |  |       > | 
					
						
							|  |  |  |         {{ $t("recipe.instructions") }} | 
					
						
							|  |  |  |       </h2> | 
					
						
							|  |  |  |       <BaseButton | 
					
						
							|  |  |  |         v-if="!isEditForm && !isCookMode" | 
					
						
							|  |  |  |         minor | 
					
						
							|  |  |  |         cancel | 
					
						
							|  |  |  |         color="primary" | 
					
						
							|  |  |  |         @click="toggleCookMode()" | 
					
						
							|  |  |  |       > | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |         <template #icon> | 
					
						
							|  |  |  |           {{ $globals.icons.primary }} | 
					
						
							|  |  |  |         </template> | 
					
						
							|  |  |  |         {{ $t("recipe.cook-mode") }} | 
					
						
							|  |  |  |       </BaseButton> | 
					
						
							|  |  |  |     </div> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |     <VueDraggable | 
					
						
							|  |  |  |       v-model="instructionList" | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |       :disabled="!isEditForm" | 
					
						
							|  |  |  |       handle=".handle" | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |       :delay="250" | 
					
						
							| 
									
										
										
										
											2024-10-25 14:49:07 -05:00
										 |  |  |       :delay-on-touch-only="true" | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |       v-bind="{ | 
					
						
							|  |  |  |         animation: 200, | 
					
						
							| 
									
										
										
										
											2024-06-21 16:42:05 -05:00
										 |  |  |         group: 'recipe-instructions', | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |         ghostClass: 'ghost', | 
					
						
							|  |  |  |       }" | 
					
						
							|  |  |  |       @start="drag = true" | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |       @end="onDragEnd" | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |     > | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |       <TransitionGroup | 
					
						
							|  |  |  |         type="transition" | 
					
						
							|  |  |  |       > | 
					
						
							|  |  |  |         <div | 
					
						
							|  |  |  |           v-for="(step, index) in instructionList" | 
					
						
							|  |  |  |           :key="step.id!" | 
					
						
							|  |  |  |           class="list-group-item" | 
					
						
							|  |  |  |         > | 
					
						
							|  |  |  |           <v-sheet | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |             v-if="step.id && showTitleEditor[step.id]" | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |             color="primary" | 
					
						
							|  |  |  |             class="mt-6 mb-2 d-flex align-center" | 
					
						
							|  |  |  |             :class="isEditForm ? 'pa-2' : 'pa-3'" | 
					
						
							|  |  |  |             style="border-radius: 6px; cursor: pointer; width: 100%;" | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |             @click="toggleCollapseSection(index)" | 
					
						
							|  |  |  |           > | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |             <template v-if="isEditForm"> | 
					
						
							|  |  |  |               <v-text-field | 
					
						
							|  |  |  |                 v-model="step.title" | 
					
						
							|  |  |  |                 class="pa-0" | 
					
						
							|  |  |  |                 density="compact" | 
					
						
							|  |  |  |                 variant="solo" | 
					
						
							|  |  |  |                 flat | 
					
						
							|  |  |  |                 :placeholder="$t('recipe.section-title')" | 
					
						
							|  |  |  |                 bg-color="primary" | 
					
						
							|  |  |  |                 hide-details | 
					
						
							|  |  |  |               /> | 
					
						
							|  |  |  |             </template> | 
					
						
							|  |  |  |             <template v-else> | 
					
						
							|  |  |  |               <v-toolbar-title class="section-title-text"> | 
					
						
							|  |  |  |                 {{ step.title }} | 
					
						
							|  |  |  |               </v-toolbar-title> | 
					
						
							|  |  |  |             </template> | 
					
						
							|  |  |  |           </v-sheet> | 
					
						
							|  |  |  |           <v-hover v-slot="{ isHovering }"> | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |             <v-card | 
					
						
							| 
									
										
										
										
											2023-10-07 21:36:47 +02:00
										 |  |  |               class="my-3" | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |               :class="[{ 'on-hover': isHovering }, isChecked(index)]" | 
					
						
							|  |  |  |               :elevation="isHovering ? 12 : 2" | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |               :ripple="false" | 
					
						
							|  |  |  |               @click="toggleDisabled(index)" | 
					
						
							|  |  |  |             > | 
					
						
							|  |  |  |               <v-card-title :class="{ 'pb-0': !isChecked(index) }"> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |                 <div class="d-flex align-center"> | 
					
						
							|  |  |  |                   <v-text-field | 
					
						
							|  |  |  |                     v-if="isEditForm" | 
					
						
							|  |  |  |                     v-model="step.summary" | 
					
						
							|  |  |  |                     class="headline handle" | 
					
						
							|  |  |  |                     hide-details | 
					
						
							|  |  |  |                     density="compact" | 
					
						
							|  |  |  |                     variant="solo" | 
					
						
							|  |  |  |                     flat | 
					
						
							|  |  |  |                     :placeholder="$t('recipe.step-index', { step: index + 1 })" | 
					
						
							|  |  |  |                   > | 
					
						
							|  |  |  |                     <template #prepend> | 
					
						
							|  |  |  |                       <v-icon size="26"> | 
					
						
							|  |  |  |                         {{ $globals.icons.arrowUpDown }} | 
					
						
							|  |  |  |                       </v-icon> | 
					
						
							|  |  |  |                     </template> | 
					
						
							|  |  |  |                   </v-text-field> | 
					
						
							|  |  |  |                   <span v-else> | 
					
						
							|  |  |  |                     {{ step.summary ? step.summary : $t("recipe.step-index", { step: index + 1 }) }} | 
					
						
							|  |  |  |                   </span> | 
					
						
							|  |  |  |                   <template v-if="isEditForm"> | 
					
						
							|  |  |  |                     <div class="ml-auto"> | 
					
						
							|  |  |  |                       <BaseButtonGroup | 
					
						
							|  |  |  |                         :large="false" | 
					
						
							|  |  |  |                         :buttons="[ | 
					
						
							|  |  |  |                           { | 
					
						
							|  |  |  |                             icon: $globals.icons.delete, | 
					
						
							|  |  |  |                             text: $t('general.delete'), | 
					
						
							|  |  |  |                             event: 'delete', | 
					
						
							|  |  |  |                           }, | 
					
						
							|  |  |  |                           { | 
					
						
							|  |  |  |                             icon: $globals.icons.dotsVertical, | 
					
						
							|  |  |  |                             text: '', | 
					
						
							|  |  |  |                             event: 'open', | 
					
						
							|  |  |  |                             children: [ | 
					
						
							|  |  |  |                               { | 
					
						
							|  |  |  |                                 text: $t('recipe.toggle-section'), | 
					
						
							|  |  |  |                                 event: 'toggle-section', | 
					
						
							|  |  |  |                               }, | 
					
						
							|  |  |  |                               { | 
					
						
							|  |  |  |                                 text: $t('recipe.link-ingredients'), | 
					
						
							|  |  |  |                                 event: 'link-ingredients', | 
					
						
							|  |  |  |                               }, | 
					
						
							|  |  |  |                               { | 
					
						
							|  |  |  |                                 text: $t('recipe.upload-image'), | 
					
						
							|  |  |  |                                 event: 'upload-image', | 
					
						
							|  |  |  |                               }, | 
					
						
							|  |  |  |                               { | 
					
						
							|  |  |  |                                 icon: previewStates[index] ? $globals.icons.edit : $globals.icons.eye, | 
					
						
							|  |  |  |                                 text: previewStates[index] ? $t('recipe.edit-markdown') : $t('markdown-editor.preview-markdown-button-label'), | 
					
						
							|  |  |  |                                 event: 'preview-step', | 
					
						
							|  |  |  |                                 divider: true, | 
					
						
							|  |  |  |                               }, | 
					
						
							|  |  |  |                               { | 
					
						
							|  |  |  |                                 text: $t('recipe.merge-above'), | 
					
						
							|  |  |  |                                 event: 'merge-above', | 
					
						
							|  |  |  |                               }, | 
					
						
							|  |  |  |                               { | 
					
						
							|  |  |  |                                 text: $t('recipe.move-to-top'), | 
					
						
							|  |  |  |                                 event: 'move-to-top', | 
					
						
							|  |  |  |                               }, | 
					
						
							|  |  |  |                               { | 
					
						
							|  |  |  |                                 text: $t('recipe.move-to-bottom'), | 
					
						
							|  |  |  |                                 event: 'move-to-bottom', | 
					
						
							|  |  |  |                               }, | 
					
						
							|  |  |  |                               { | 
					
						
							|  |  |  |                                 text: $t('recipe.insert-above'), | 
					
						
							|  |  |  |                                 event: 'insert-above', | 
					
						
							|  |  |  |                               }, | 
					
						
							|  |  |  |                               { | 
					
						
							|  |  |  |                                 text: $t('recipe.insert-below'), | 
					
						
							|  |  |  |                                 event: 'insert-below', | 
					
						
							|  |  |  |                               }, | 
					
						
							|  |  |  |                             ], | 
					
						
							|  |  |  |                           }, | 
					
						
							|  |  |  |                         ]" | 
					
						
							|  |  |  |                         @merge-above="mergeAbove(index - 1, index)" | 
					
						
							|  |  |  |                         @move-to-top="moveTo('top', index)" | 
					
						
							|  |  |  |                         @move-to-bottom="moveTo('bottom', index)" | 
					
						
							|  |  |  |                         @insert-above="insert(index)" | 
					
						
							|  |  |  |                         @insert-below="insert(index + 1)" | 
					
						
							|  |  |  |                         @toggle-section="toggleShowTitle(step.id!)" | 
					
						
							|  |  |  |                         @link-ingredients="openDialog(index, step.text, step.ingredientReferences)" | 
					
						
							|  |  |  |                         @preview-step="togglePreviewState(index)" | 
					
						
							|  |  |  |                         @upload-image="openImageUpload(index)" | 
					
						
							|  |  |  |                         @delete="instructionList.splice(index, 1)" | 
					
						
							|  |  |  |                       /> | 
					
						
							|  |  |  |                     </div> | 
					
						
							| 
									
										
										
										
											2024-10-23 20:27:47 +11:00
										 |  |  |                   </template> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |                   <v-fade-transition> | 
					
						
							|  |  |  |                     <v-icon | 
					
						
							|  |  |  |                       v-show="isChecked(index)" | 
					
						
							|  |  |  |                       size="24" | 
					
						
							|  |  |  |                       class="ml-auto" | 
					
						
							|  |  |  |                       color="success" | 
					
						
							|  |  |  |                     > | 
					
						
							|  |  |  |                       {{ $globals.icons.checkboxMarkedCircle }} | 
					
						
							|  |  |  |                     </v-icon> | 
					
						
							|  |  |  |                   </v-fade-transition> | 
					
						
							|  |  |  |                 </div> | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |               </v-card-title> | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |               <v-progress-linear | 
					
						
							|  |  |  |                 v-if="isEditForm && loadingStates[index]" | 
					
						
							|  |  |  |                 :active="true" | 
					
						
							|  |  |  |                 :indeterminate="true" | 
					
						
							|  |  |  |               /> | 
					
						
							| 
									
										
										
										
											2023-01-29 02:27:40 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |               <!-- Content --> | 
					
						
							| 
									
										
										
										
											2022-10-21 20:01:08 -08:00
										 |  |  |               <DropZone @drop="(f) => handleImageDrop(index, f)"> | 
					
						
							| 
									
										
										
										
											2022-10-22 12:49:59 -08:00
										 |  |  |                 <v-card-text | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |                   v-if="isEditForm" | 
					
						
							|  |  |  |                   @click="$emit('click-instruction-field', `${index}.text`)" | 
					
						
							| 
									
										
										
										
											2022-10-22 12:49:59 -08:00
										 |  |  |                 > | 
					
						
							| 
									
										
										
										
											2022-10-21 20:01:08 -08:00
										 |  |  |                   <MarkdownEditor | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |                     v-model="instructionList[index]['text']" | 
					
						
							|  |  |  |                     v-model:preview="previewStates[index]" | 
					
						
							| 
									
										
										
										
											2022-10-21 20:01:08 -08:00
										 |  |  |                     class="mb-2" | 
					
						
							|  |  |  |                     :display-preview="false" | 
					
						
							|  |  |  |                     :textarea="{ | 
					
						
							| 
									
										
										
										
											2023-01-29 02:39:51 +01:00
										 |  |  |                       hint: $t('recipe.attach-images-hint'), | 
					
						
							| 
									
										
										
										
											2022-10-21 20:01:08 -08:00
										 |  |  |                       persistentHint: true, | 
					
						
							|  |  |  |                     }" | 
					
						
							|  |  |  |                   /> | 
					
						
							|  |  |  |                   <RecipeIngredientHtml | 
					
						
							|  |  |  |                     v-for="ing in step.ingredientReferences" | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |                     :key="ing.referenceId!" | 
					
						
							|  |  |  |                     :markup="getIngredientByRefId(ing.referenceId!)" | 
					
						
							| 
									
										
										
										
											2022-10-21 20:01:08 -08:00
										 |  |  |                   /> | 
					
						
							|  |  |  |                 </v-card-text> | 
					
						
							|  |  |  |               </DropZone> | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |               <v-expand-transition> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |                 <div | 
					
						
							|  |  |  |                   v-if="!isChecked(index) && !isEditForm" | 
					
						
							|  |  |  |                   class="m-0 p-0" | 
					
						
							|  |  |  |                 > | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |                   <v-card-text class="markdown"> | 
					
						
							| 
									
										
										
										
											2024-11-11 12:21:44 +01:00
										 |  |  |                     <v-row> | 
					
						
							|  |  |  |                       <v-col | 
					
						
							|  |  |  |                         v-if="isCookMode && step.ingredientReferences && step.ingredientReferences.length > 0" | 
					
						
							|  |  |  |                         cols="12" | 
					
						
							|  |  |  |                         sm="5" | 
					
						
							|  |  |  |                       > | 
					
						
							|  |  |  |                         <div class="ml-n4"> | 
					
						
							|  |  |  |                           <RecipeIngredients | 
					
						
							|  |  |  |                             :value="recipe.recipeIngredient.filter((ing) => { | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |                               if (!step.ingredientReferences) return false | 
					
						
							| 
									
										
										
										
											2024-11-11 12:21:44 +01:00
										 |  |  |                               return step.ingredientReferences.map((ref) => ref.referenceId).includes(ing.referenceId || '') | 
					
						
							|  |  |  |                             })" | 
					
						
							|  |  |  |                             :scale="scale" | 
					
						
							|  |  |  |                             :disable-amount="recipe.settings.disableAmount" | 
					
						
							|  |  |  |                             :is-cook-mode="isCookMode" | 
					
						
							|  |  |  |                           /> | 
					
						
							|  |  |  |                         </div> | 
					
						
							|  |  |  |                       </v-col> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |                       <v-divider | 
					
						
							|  |  |  |                         v-if="isCookMode && step.ingredientReferences && step.ingredientReferences.length > 0 && $vuetify.display.smAndUp" | 
					
						
							|  |  |  |                         vertical | 
					
						
							|  |  |  |                       /> | 
					
						
							| 
									
										
										
										
											2024-11-11 12:21:44 +01:00
										 |  |  |                       <v-col> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |                         <SafeMarkdown | 
					
						
							|  |  |  |                           class="markdown" | 
					
						
							|  |  |  |                           :source="step.text" | 
					
						
							|  |  |  |                         /> | 
					
						
							| 
									
										
										
										
											2024-11-11 12:21:44 +01:00
										 |  |  |                       </v-col> | 
					
						
							|  |  |  |                     </v-row> | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |                   </v-card-text> | 
					
						
							|  |  |  |                 </div> | 
					
						
							|  |  |  |               </v-expand-transition> | 
					
						
							|  |  |  |             </v-card> | 
					
						
							|  |  |  |           </v-hover> | 
					
						
							|  |  |  |         </div> | 
					
						
							|  |  |  |       </TransitionGroup> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |     </VueDraggable> | 
					
						
							|  |  |  |     <v-divider | 
					
						
							|  |  |  |       v-if="!isCookMode" | 
					
						
							|  |  |  |       class="mt-10 d-flex d-md-none" | 
					
						
							|  |  |  |     /> | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |   </section> | 
					
						
							|  |  |  | </template> | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | <script setup lang="ts"> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  | import { VueDraggable } from "vue-draggable-plus"; | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | import { computed, nextTick, onMounted, ref, watch } from "vue"; | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | import RecipeIngredientHtml from "../../RecipeIngredientHtml.vue"; | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  | import type { RecipeStep, IngredientReferences, RecipeIngredient, RecipeAsset, Recipe } from "~/lib/api/types/recipe"; | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | import { parseIngredientText } from "~/composables/recipes"; | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  | import { uuid4 } from "~/composables/use-utils"; | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | import { useUserApi, useStaticRoutes } from "~/composables/api"; | 
					
						
							|  |  |  | import { usePageState } from "~/composables/recipe-page/shared-state"; | 
					
						
							| 
									
										
										
										
											2022-12-29 23:00:31 +01:00
										 |  |  | import { useExtractIngredientReferences } from "~/composables/recipe-page/use-extract-ingredient-references"; | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  | import type { NoUndefinedField } from "~/lib/api/types/non-generated"; | 
					
						
							| 
									
										
										
										
											2022-10-21 20:01:08 -08:00
										 |  |  | import DropZone from "~/components/global/DropZone.vue"; | 
					
						
							| 
									
										
										
										
											2024-11-11 12:21:44 +01:00
										 |  |  | import RecipeIngredients from "~/components/Domain/Recipe/RecipeIngredients.vue"; | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | interface MergerHistory { | 
					
						
							|  |  |  |   target: number; | 
					
						
							|  |  |  |   source: number; | 
					
						
							|  |  |  |   targetText: string; | 
					
						
							|  |  |  |   sourceText: string; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | const instructionList = defineModel<RecipeStep[]>("modelValue", { required: true, default: () => [] }); | 
					
						
							|  |  |  | const assets = defineModel<RecipeAsset[]>("assets", { required: true, default: () => [] }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const props = defineProps({ | 
					
						
							|  |  |  |   recipe: { | 
					
						
							|  |  |  |     type: Object as () => NoUndefinedField<Recipe>, | 
					
						
							|  |  |  |     required: true, | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |   }, | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   scale: { | 
					
						
							|  |  |  |     type: Number, | 
					
						
							|  |  |  |     default: 1, | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |   }, | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | }); | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | const emit = defineEmits(["click-instruction-field", "update:assets"]); | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | const BASE_URL = useRequestURL().origin; | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | const { isCookMode, toggleCookMode, isEditForm } = usePageState(props.recipe.slug); | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | const dialog = ref(false); | 
					
						
							|  |  |  | const disabledSteps = ref<number[]>([]); | 
					
						
							|  |  |  | const unusedIngredients = ref<RecipeIngredient[]>([]); | 
					
						
							|  |  |  | const usedIngredients = ref<RecipeIngredient[]>([]); | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | const showTitleEditor = ref<{ [key: string]: boolean }>({}); | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | // ===============================================================
 | 
					
						
							|  |  |  | // UI State Helpers
 | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | function hasSectionTitle(title: string | undefined) { | 
					
						
							|  |  |  |   return !(title === null || title === "" || title === undefined); | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | watch(instructionList, (v) => { | 
					
						
							|  |  |  |   disabledSteps.value = []; | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   v.forEach((element: RecipeStep) => { | 
					
						
							|  |  |  |     if (element.id !== undefined) { | 
					
						
							|  |  |  |       showTitleEditor.value[element.id!] = hasSectionTitle(element.title!); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | }, { deep: true }); | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | const showCookMode = ref(false); | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | onMounted(() => { | 
					
						
							|  |  |  |   instructionList.value.forEach((element: RecipeStep) => { | 
					
						
							|  |  |  |     if (element.id !== undefined) { | 
					
						
							|  |  |  |       showTitleEditor.value[element.id!] = hasSectionTitle(element.title!); | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |     if (showCookMode.value === false && element.ingredientReferences && element.ingredientReferences.length > 0) { | 
					
						
							|  |  |  |       showCookMode.value = true; | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |     showTitleEditor.value = { ...showTitleEditor.value }; | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   if (assets.value === undefined) { | 
					
						
							|  |  |  |     emit("update:assets", []); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | }); | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | function toggleDisabled(stepIndex: number) { | 
					
						
							|  |  |  |   if (isEditForm.value) { | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   if (disabledSteps.value.includes(stepIndex)) { | 
					
						
							|  |  |  |     const index = disabledSteps.value.indexOf(stepIndex); | 
					
						
							|  |  |  |     if (index !== -1) { | 
					
						
							|  |  |  |       disabledSteps.value.splice(index, 1); | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   } | 
					
						
							|  |  |  |   else { | 
					
						
							|  |  |  |     disabledSteps.value.push(stepIndex); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | function isChecked(stepIndex: number) { | 
					
						
							|  |  |  |   if (disabledSteps.value.includes(stepIndex) && !isEditForm.value) { | 
					
						
							|  |  |  |     return "disabled-card"; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | function toggleShowTitle(id?: string) { | 
					
						
							|  |  |  |   if (!id) { | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   showTitleEditor.value[id] = !showTitleEditor.value[id]; | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   const temp = { ...showTitleEditor.value }; | 
					
						
							|  |  |  |   showTitleEditor.value = temp; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | function onDragEnd() { | 
					
						
							|  |  |  |   drag.value = false; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | // ===============================================================
 | 
					
						
							|  |  |  | // Ingredient Linker
 | 
					
						
							|  |  |  | const activeRefs = ref<string[]>([]); | 
					
						
							|  |  |  | const activeIndex = ref(0); | 
					
						
							|  |  |  | const activeText = ref(""); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function openDialog(idx: number, text: string, refs?: IngredientReferences[]) { | 
					
						
							|  |  |  |   if (!refs) { | 
					
						
							|  |  |  |     instructionList.value[idx].ingredientReferences = []; | 
					
						
							|  |  |  |     refs = instructionList.value[idx].ingredientReferences as IngredientReferences[]; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   setUsedIngredients(); | 
					
						
							|  |  |  |   activeText.value = text; | 
					
						
							|  |  |  |   activeIndex.value = idx; | 
					
						
							|  |  |  |   dialog.value = true; | 
					
						
							|  |  |  |   activeRefs.value = refs.map(ref => ref.referenceId ?? ""); | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | const availableNextStep = computed(() => activeIndex.value < instructionList.value.length - 1); | 
					
						
							| 
									
										
										
										
											2024-01-13 16:45:59 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | function setIngredientIds() { | 
					
						
							|  |  |  |   const instruction = instructionList.value[activeIndex.value]; | 
					
						
							|  |  |  |   instruction.ingredientReferences = activeRefs.value.map((ref) => { | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       referenceId: ref, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2024-01-13 16:45:59 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   // Update the visibility of the cook mode button
 | 
					
						
							|  |  |  |   showCookMode.value = false; | 
					
						
							|  |  |  |   instructionList.value.forEach((element) => { | 
					
						
							|  |  |  |     if (showCookMode.value === false && element.ingredientReferences && element.ingredientReferences.length > 0) { | 
					
						
							|  |  |  |       showCookMode.value = true; | 
					
						
							| 
									
										
										
										
											2024-01-13 16:45:59 +01:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   }); | 
					
						
							|  |  |  |   dialog.value = false; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2024-01-13 16:45:59 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | function saveAndOpenNextLinkIngredients() { | 
					
						
							|  |  |  |   const currentStepIndex = activeIndex.value; | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   if (!availableNextStep.value) { | 
					
						
							|  |  |  |     return; // no next step, the button calling this function should not be shown
 | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   setIngredientIds(); | 
					
						
							|  |  |  |   const nextStep = instructionList.value[currentStepIndex + 1]; | 
					
						
							|  |  |  |   // close dialog before opening to reset the scroll position
 | 
					
						
							|  |  |  |   nextTick(() => openDialog(currentStepIndex + 1, nextStep.text, nextStep.ingredientReferences)); | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | function setUsedIngredients() { | 
					
						
							|  |  |  |   const usedRefs: { [key: string]: boolean } = {}; | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   instructionList.value.forEach((element) => { | 
					
						
							|  |  |  |     element.ingredientReferences?.forEach((ref) => { | 
					
						
							|  |  |  |       if (ref.referenceId !== undefined) { | 
					
						
							|  |  |  |         usedRefs[ref.referenceId!] = true; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |     }); | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   }); | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   usedIngredients.value = props.recipe.recipeIngredient.filter((ing) => { | 
					
						
							|  |  |  |     return ing.referenceId !== undefined && ing.referenceId in usedRefs; | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   unusedIngredients.value = props.recipe.recipeIngredient.filter((ing) => { | 
					
						
							|  |  |  |     return !(ing.referenceId !== undefined && ing.referenceId in usedRefs); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | function autoSetReferences() { | 
					
						
							|  |  |  |   useExtractIngredientReferences( | 
					
						
							|  |  |  |     props.recipe.recipeIngredient, | 
					
						
							|  |  |  |     activeRefs.value, | 
					
						
							|  |  |  |     activeText.value, | 
					
						
							|  |  |  |     props.recipe.settings.disableAmount, | 
					
						
							|  |  |  |   ).forEach((ingredient: string) => activeRefs.value.push(ingredient)); | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | const ingredientLookup = computed(() => { | 
					
						
							|  |  |  |   const results: { [key: string]: RecipeIngredient } = {}; | 
					
						
							|  |  |  |   return props.recipe.recipeIngredient.reduce((prev, ing) => { | 
					
						
							|  |  |  |     if (ing.referenceId === undefined) { | 
					
						
							|  |  |  |       return prev; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     prev[ing.referenceId] = ing; | 
					
						
							|  |  |  |     return prev; | 
					
						
							|  |  |  |   }, results); | 
					
						
							|  |  |  | }); | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | function getIngredientByRefId(refId: string | undefined) { | 
					
						
							|  |  |  |   if (refId === undefined) { | 
					
						
							|  |  |  |     return ""; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   const ing = ingredientLookup.value[refId]; | 
					
						
							|  |  |  |   if (!ing) return ""; | 
					
						
							|  |  |  |   return parseIngredientText(ing, props.recipe.settings.disableAmount, props.scale); | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | // ===============================================================
 | 
					
						
							|  |  |  | // Instruction Merger
 | 
					
						
							|  |  |  | const mergeHistory = ref<MergerHistory[]>([]); | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | function mergeAbove(target: number, source: number) { | 
					
						
							|  |  |  |   if (target < 0) { | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2024-03-04 09:41:29 +11:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   mergeHistory.value.push({ | 
					
						
							|  |  |  |     target, | 
					
						
							|  |  |  |     source, | 
					
						
							|  |  |  |     targetText: instructionList.value[target].text, | 
					
						
							|  |  |  |     sourceText: instructionList.value[source].text, | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2024-06-18 06:45:12 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   instructionList.value[target].text += " " + instructionList.value[source].text; | 
					
						
							|  |  |  |   instructionList.value.splice(source, 1); | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | function undoMerge(event: KeyboardEvent) { | 
					
						
							|  |  |  |   if (event.ctrlKey && event.code === "KeyZ") { | 
					
						
							|  |  |  |     if (!(mergeHistory.value?.length > 0)) { | 
					
						
							|  |  |  |       return; | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |     const lastMerge = mergeHistory.value.pop(); | 
					
						
							|  |  |  |     if (!lastMerge) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |     instructionList.value[lastMerge.target].text = lastMerge.targetText; | 
					
						
							|  |  |  |     instructionList.value.splice(lastMerge.source, 0, { | 
					
						
							|  |  |  |       id: uuid4(), | 
					
						
							|  |  |  |       title: "", | 
					
						
							|  |  |  |       text: lastMerge.sourceText, | 
					
						
							|  |  |  |       ingredientReferences: [], | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | function moveTo(dest: string, source: number) { | 
					
						
							|  |  |  |   if (dest === "top") { | 
					
						
							|  |  |  |     instructionList.value.unshift(instructionList.value.splice(source, 1)[0]); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   else { | 
					
						
							|  |  |  |     instructionList.value.push(instructionList.value.splice(source, 1)[0]); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | function insert(dest: number) { | 
					
						
							|  |  |  |   instructionList.value.splice(dest, 0, { id: uuid4(), text: "", title: "", ingredientReferences: [] }); | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | const previewStates = ref<boolean[]>([]); | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | function togglePreviewState(index: number) { | 
					
						
							|  |  |  |   const temp = [...previewStates.value]; | 
					
						
							|  |  |  |   temp[index] = !temp[index]; | 
					
						
							|  |  |  |   previewStates.value = temp; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | function toggleCollapseSection(index: number) { | 
					
						
							|  |  |  |   const sectionSteps: number[] = []; | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   for (let i = index; i < instructionList.value.length; i++) { | 
					
						
							|  |  |  |     if (!(i === index) && hasSectionTitle(instructionList.value[i].title!)) { | 
					
						
							|  |  |  |       break; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     else { | 
					
						
							|  |  |  |       sectionSteps.push(i); | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   const allCollapsed = sectionSteps.every(idx => disabledSteps.value.includes(idx)); | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   if (allCollapsed) { | 
					
						
							|  |  |  |     disabledSteps.value = disabledSteps.value.filter(idx => !sectionSteps.includes(idx)); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   else { | 
					
						
							|  |  |  |     disabledSteps.value = [...disabledSteps.value, ...sectionSteps]; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2023-01-29 02:27:40 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | const drag = ref(false); | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | // ===============================================================
 | 
					
						
							|  |  |  | // Image Uploader
 | 
					
						
							|  |  |  | const api = useUserApi(); | 
					
						
							|  |  |  | const { recipeAssetPath } = useStaticRoutes(); | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | const loadingStates = ref<{ [key: number]: boolean }>({}); | 
					
						
							| 
									
										
										
										
											2023-01-29 02:27:40 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  | async function handleImageDrop(index: number, files: File[]) { | 
					
						
							|  |  |  |   if (!files) { | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   // Check if the file is an image
 | 
					
						
							|  |  |  |   const file = files[0]; | 
					
						
							|  |  |  |   if (!file || !file.type.startsWith("image/")) { | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2023-01-29 02:27:40 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   loadingStates.value[index] = true; | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   const { data } = await api.recipes.createAsset(props.recipe.slug, { | 
					
						
							|  |  |  |     name: file.name, | 
					
						
							|  |  |  |     icon: "mdi-file-image", | 
					
						
							|  |  |  |     file, | 
					
						
							|  |  |  |     extension: file.name.split(".").pop() || "", | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   loadingStates.value[index] = false; | 
					
						
							| 
									
										
										
										
											2023-01-29 02:27:40 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   if (!data) { | 
					
						
							|  |  |  |     return; // TODO: Handle error
 | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-22 22:34:25 +02:00
										 |  |  |   emit("update:assets", [...assets.value, data]); | 
					
						
							|  |  |  |   const assetUrl = BASE_URL + recipeAssetPath(props.recipe.id, data.fileName as string); | 
					
						
							|  |  |  |   const text = `<img src="${assetUrl}" height="100%" width="100%"/>`; | 
					
						
							|  |  |  |   instructionList.value[index].text += text; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function openImageUpload(index: number) { | 
					
						
							|  |  |  |   const input = document.createElement("input"); | 
					
						
							|  |  |  |   input.type = "file"; | 
					
						
							|  |  |  |   input.accept = "image/*"; | 
					
						
							|  |  |  |   input.onchange = async () => { | 
					
						
							|  |  |  |     if (input.files) { | 
					
						
							|  |  |  |       await handleImageDrop(index, Array.from(input.files)); | 
					
						
							|  |  |  |       input.remove(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  |   input.click(); | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | </script> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <style lang="css" scoped> | 
					
						
							|  |  |  | .v-card--link:before { | 
					
						
							|  |  |  |   background: none; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** Select all li under .markdown class */ | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  | .markdown :deep(ul > li) { | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |   display: list-item; | 
					
						
							|  |  |  |   list-style-type: disc !important; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** Select all li under .markdown class */ | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  | .markdown :deep(ol > li) { | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  |   display: list-item; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .flip-list-move { | 
					
						
							|  |  |  |   transition: transform 0.5s; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | .no-move { | 
					
						
							|  |  |  |   transition: transform 0s; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | .ghost { | 
					
						
							|  |  |  |   opacity: 0.5; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | .list-group { | 
					
						
							|  |  |  |   min-height: 38px; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | .list-group-item i { | 
					
						
							|  |  |  |   cursor: pointer; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .blur { | 
					
						
							|  |  |  |   filter: blur(2px); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .upload-overlay { | 
					
						
							|  |  |  |   display: flex; | 
					
						
							|  |  |  |   justify-content: center; | 
					
						
							|  |  |  |   align-items: center; | 
					
						
							|  |  |  |   position: absolute; | 
					
						
							|  |  |  |   top: 0; | 
					
						
							|  |  |  |   left: 0; | 
					
						
							|  |  |  |   width: 100%; | 
					
						
							|  |  |  |   height: 100%; | 
					
						
							|  |  |  |   background: rgba(0, 0, 0, 0.5); | 
					
						
							|  |  |  |   z-index: 1; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  | 
 | 
					
						
							|  |  |  | .v-text-field >>> input { | 
					
						
							|  |  |  |   font-size: 1.5rem; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-08-27 10:44:58 -08:00
										 |  |  | </style> |