import { afterNextRender, Component, computed, ElementRef, inject, Injector, input, OnInit, output, runInInjectionContext, signal, viewChild, } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { MatError, MatFormField, MatInput, MatLabel } from '@angular/material/input'; import { MatButton } from '@angular/material/button'; import { EnterRecipeDataSubmitEvent } from './EnterRecipeDataSubmitEvent'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { faBars, faEllipsis } from '@fortawesome/free-solid-svg-icons'; import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu'; import { MatDialog } from '@angular/material/dialog'; import { ImageUploadDialog } from './image-upload-dialog/image-upload-dialog'; import { IngredientDialog } from './ingredient-dialog/ingredient-dialog'; import { RecipeDraftViewModel } from '../../../../shared/models/RecipeDraftView.model'; import { MatCell, MatCellDef, MatColumnDef, MatHeaderCell, MatHeaderCellDef, MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef, MatTable, } from '@angular/material/table'; import { IngredientDraftClientModel } from '../../../../shared/client-models/IngredientDraftClientModel'; import { CdkDrag, CdkDragDrop, CdkDropList, moveItemInArray } from '@angular/cdk/drag-drop'; import { DatePipe } from '@angular/common'; import { ImageSelect } from './image-select/image-select'; import { ImageView } from '../../../../shared/models/ImageView.model'; @Component({ selector: 'app-enter-recipe-data', imports: [ ReactiveFormsModule, MatFormField, MatLabel, MatInput, MatButton, FaIconComponent, MatMenuTrigger, MatMenu, MatMenuItem, MatTable, MatColumnDef, MatHeaderCell, MatHeaderCellDef, MatCell, MatCellDef, MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef, CdkDropList, CdkDrag, DatePipe, ImageSelect, MatError, ], templateUrl: './enter-recipe-data.html', styleUrl: './enter-recipe-data.css', }) export class EnterRecipeData implements OnInit { public readonly draft = input.required(); public readonly submit = output(); public readonly deleteDraft = output(); protected readonly recipeTextTextarea = viewChild.required>('recipeTextTextarea'); private readonly dialog = inject(MatDialog); protected readonly recipeFormGroup = new FormGroup({ title: new FormControl('', Validators.required), slug: new FormControl('', Validators.required), preparationTime: new FormControl('', Validators.pattern(/^\d+$/)), cookingTime: new FormControl('', Validators.pattern(/^\d+$/)), totalTime: new FormControl('', Validators.pattern(/^\d+$/)), text: new FormControl('', Validators.required), }); protected readonly ingredientsTable = viewChild>('ingredientsTable'); protected readonly ingredientModels = signal([]); private readonly mainImage = signal(null); protected readonly mainImageUsernameFilename = computed(() => { const mainImage = this.mainImage(); if (mainImage) { return [mainImage.owner.username, mainImage.filename] as const; } else { return null; } }); private readonly injector = inject(Injector); public ngOnInit(): void { const draft = this.draft(); this.recipeFormGroup.patchValue({ title: draft.title ?? '', slug: draft.slug ?? '', preparationTime: draft.preparationTime?.toString() ?? '', cookingTime: draft.cookingTime?.toString() ?? '', totalTime: draft.totalTime?.toString() ?? '', text: draft.rawText ?? '', }); if (draft.ingredients) { this.ingredientModels.set( draft.ingredients.map((ingredient, index) => ({ id: index, draft: ingredient, })), ); } runInInjectionContext(this.injector, () => { afterNextRender({ mixedReadWrite: () => { this.updateTextareaHeight(this.recipeTextTextarea().nativeElement); }, }); }); } private updateTextareaHeight(textarea: HTMLTextAreaElement) { const windowScrollX = window.scrollX; const windowScrollY = window.scrollY; textarea.style.height = 'auto'; textarea.style.height = textarea.scrollHeight + 'px'; requestAnimationFrame(() => { window.scrollTo(windowScrollX, windowScrollY); }); } protected onRecipeTextChange(event: Event): void { this.updateTextareaHeight(event.target as HTMLTextAreaElement); } protected addIngredient() { const dialogRef = this.dialog.open( IngredientDialog, { data: { id: this.ingredientModels().length, draft: { name: '', }, }, }, ); dialogRef.afterClosed().subscribe((ingredientModel) => { if (ingredientModel) { this.ingredientModels.update((ingredientModels) => [...ingredientModels, ingredientModel]); this.ingredientsTable()!.renderRows(); } }); } protected editIngredient(model: IngredientDraftClientModel): void { const dialogRef = this.dialog.open( IngredientDialog, { data: model, }, ); dialogRef.afterClosed().subscribe((model) => { if (model) { this.ingredientModels.update((models) => { const updated: IngredientDraftClientModel[] = [...models]; const target = updated.find((search) => search.id === model.id); if (!target) { throw new Error('Ingredient does not exist.'); } target.draft = model.draft; return updated; }); } }); } protected onIngredientDelete(ingredientModel: IngredientDraftClientModel) { this.ingredientModels.update((ingredientModels) => { const updated = ingredientModels.filter((model) => model.id !== ingredientModel.id); updated.sort((model0, model1) => model0.id - model1.id); updated.forEach((model, index) => { model.id = index; }); return updated; }); } protected onIngredientDrop(event: CdkDragDrop): void { this.ingredientModels.update((ingredientModels) => { const modelIndex = ingredientModels.findIndex((m) => m.id === event.previousIndex); moveItemInArray(ingredientModels, modelIndex, event.currentIndex); ingredientModels.forEach((model, index) => { model.id = index; }); return ingredientModels; }); this.ingredientsTable()!.renderRows(); } private getTime(s?: string | null): number | null { if (!s) return null; try { return parseInt(s); } catch (e) { console.error(`Should not have had a parse error because of form validators: ${e}`); return null; } } protected onSubmit(event: SubmitEvent): void { event.preventDefault(); const value = this.recipeFormGroup.value; this.submit.emit({ title: value.title!, slug: value.slug!, ingredients: this.ingredientModels().map((ingredientModel) => ingredientModel.draft), preparationTime: this.getTime(value.preparationTime), cookingTime: this.getTime(value.cookingTime), totalTime: this.getTime(value.totalTime), mainImage: this.mainImage(), rawText: value.text!, }); } protected onDraftDelete(): void { this.deleteDraft.emit(); } protected openImageUploadDialog(): void { this.dialog.open(ImageUploadDialog); } protected onMainImageSelect(imageView: ImageView | null): void { this.mainImage.set(imageView); } protected readonly faEllipsis = faEllipsis; protected readonly faBars = faBars; }