import {Location} from "@angular/common"; import {inject, Injectable, OnDestroy, Signal, signal} from "@angular/core"; import {MatDialog} from "@angular/material/dialog"; import {MatSnackBar} from "@angular/material/snack-bar"; import {ActivatedRoute} from "@angular/router"; import {debounceTime, distinctUntilChanged, Subscription} from "rxjs"; import {DEFAULT_PUBLICATION, Publication} from "../../core/rest-services/publications/model/publication"; import {PublicationRestService} from "../../core/rest-services/publications/publication.rest-service"; import {copy} from "../../core/utils/ObjectUtils"; import {CodeBlockDialog} from "./code-block-dialog/code-block-dialog.component"; import {PictureSelectionDialog} from "./picture-selection-dialog/picture-selection-dialog.component"; import {PreviewContentRequest} from "../../core/rest-services/publications/model/preview"; import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms"; declare let Prism: any; export class CursorPosition { start: number; end: number; selectedCharacters: number; constructor(start: number, end: number) { this.start = start; this.end = end; this.selectedCharacters = end - start; } } export interface PublicationEditionState { publication: Publication; cursorPosition: CursorPosition; } const DEFAULT_CURSOR_POSITION = new CursorPosition(0, 0); const DEFAULT_STATE: PublicationEditionState = { publication: DEFAULT_PUBLICATION, cursorPosition: DEFAULT_CURSOR_POSITION }; @Injectable() export class PublicationEditionService implements OnDestroy { readonly #activatedRoute = inject(ActivatedRoute); readonly #dialog = inject(MatDialog); readonly #formBuilder = inject(FormBuilder); readonly #location = inject(Location); readonly #publicationRestService = inject(PublicationRestService); readonly #snackBar = inject(MatSnackBar); #isLoading = signal(false); #state = signal(copy(DEFAULT_STATE)); #isSaving = signal(false); #isPreviewing = signal(false); #subscriptions: Subscription[] = []; publicationEditionForm: FormGroup = this.#formBuilder.group({ title: new FormControl('', [Validators.required]), description: new FormControl('', [Validators.required]), text: new FormControl('', [Validators.required]), illustrationId: new FormControl('', [Validators.required]), categoryId: new FormControl('', [Validators.required]) }); ngOnDestroy(): void { this.#subscriptions.forEach(subscription => subscription.unsubscribe()); } #updateForm(): void { const state = this.#state(); const publication = state.publication; this.publicationEditionForm.controls['title'].setValue(publication.title); this.publicationEditionForm.controls['description'].setValue(publication.description); this.publicationEditionForm.controls['text'].setValue(publication.text); this.publicationEditionForm.controls['illustrationId'].setValue(publication.illustrationId); this.publicationEditionForm.controls['categoryId'].setValue(publication.categoryId); } get isLoading(): Signal { return this.#isLoading.asReadonly(); } get isSaving(): Signal { return this.#isSaving.asReadonly(); } get isPreviewing(): Signal { return this.#isPreviewing.asReadonly(); } get state(): Signal { return this.#state.asReadonly(); } get editedPublication(): Publication { return this.#state().publication; } loadPublication(): void { this.#isLoading.set(true); this.#activatedRoute.paramMap.subscribe(params => { const publicationId = params.get('publicationId'); if (publicationId == undefined) { this.#snackBar.open($localize`A technical error occurred while loading publication data.`, $localize`Close`, {duration: 5000}); this.#location.back(); } else { this.#publicationRestService.getById(publicationId) .then(publication => { const state = this.#state(); state.publication = publication; this.#state.set(state); }) .catch(error => { const errorMessage = $localize`A technical error occurred while loading publication data.`; this.#snackBar.open(errorMessage, $localize`Close`, {duration: 5000}); console.error(errorMessage, error) }) .finally(() => this.#isLoading.set(false)); } }); } init(publication: Publication): void { const state = this.#state(); state.publication = publication; this.#state.set(state); this.#updateForm(); const formValueChangesSubscription = this.publicationEditionForm.valueChanges .pipe( debounceTime(200), distinctUntilChanged() ) .subscribe(formValue => { const state = this.#state(); const publication = state.publication; publication.title = formValue.title; publication.description = formValue.description; publication.categoryId = formValue.categoryId; publication.text = formValue.text; this.#state.set(state); }); this.#subscriptions.push(formValueChangesSubscription); } private editIllustrationId(pictureId: string): void { const state = this.#state(); state.publication.illustrationId = pictureId this.#state.set(state); } displayPictureSectionDialog(): void { const dialogRef = this.#dialog.open(PictureSelectionDialog); const afterDialogCloseSubscription = dialogRef.afterClosed() .subscribe(newPictureId => { if (newPictureId) { this.editIllustrationId(newPictureId); } }); this.#subscriptions.push(afterDialogCloseSubscription); } displayCodeBlockDialog(): void { const dialogRef = this.#dialog.open(CodeBlockDialog, {width: '60em'}); const afterDialogCloseSubscription = dialogRef.afterClosed() .subscribe(codeBlockWithLanguage => { if (codeBlockWithLanguage) { this.insertCodeBlock(codeBlockWithLanguage.programmingLanguage, codeBlockWithLanguage.codeBlock); } }); this.#subscriptions.push(afterDialogCloseSubscription); } editCursorPosition(positionStart: number, positionEnd: number): void { const state = this.#state(); state.cursorPosition.start = positionStart; state.cursorPosition.end = positionEnd; this.#state.set(state); } insertTitle(titleNumber: number): void { if (titleNumber >= 1 && titleNumber <= 3) { const state = this.#state(); const publication = state.publication; const publicationTextLeftPart = publication.text.substring(0, state.cursorPosition.start); const publicationTextMiddlePart = publication.text.substring(state.cursorPosition.start, state.cursorPosition.end); const publicationTextRightPart = publication.text.substring(state.cursorPosition.end); const textWithTags = `${publicationTextLeftPart}[h${titleNumber}]${publicationTextMiddlePart}[/h${titleNumber}]${publicationTextRightPart}`; publication.text = textWithTags; this.#state.set(state); this.#updateForm(); } else { console.error(`Bad value for parameter of function 'insertTitle': '${titleNumber}'.`); } } selectAPicture(): void { const dialogRef = this.#dialog.open(PictureSelectionDialog); const afterDialogCloseSubscription = dialogRef.afterClosed() .subscribe(newPictureId => { if (newPictureId) { this.insertPicture(newPictureId); } }); this.#subscriptions.push(afterDialogCloseSubscription); } insertPicture(pictureId: string): void { const state = this.#state(); const publication = state.publication; const publicationTextLeftPart = publication.text.substring(0, state.cursorPosition.start); const publicationTextRightPart = publication.text.substring(state.cursorPosition.start); const textWithTags = `${publicationTextLeftPart}[img src="/api/pictures/${pictureId}" /]${publicationTextRightPart}`; publication.text = textWithTags; this.#state.set(state); this.#updateForm(); } insertLink(): void { const state = this.#state(); const publication = state.publication; const publicationTextLeftPart = publication.text.substring(0, state.cursorPosition.start); const publicationTextMiddlePart = publication.text.substring(state.cursorPosition.start, state.cursorPosition.end); const publicationTextRightPart = publication.text.substring(state.cursorPosition.end); const textWithTags = `${publicationTextLeftPart}[link href="" txt="${publicationTextMiddlePart}" /]${publicationTextRightPart}`; publication.text = textWithTags; this.#state.set(state); this.#updateForm(); } private insertCodeBlock(programmingLanguage: string, codeBlock: string): void { const state = this.#state(); const publication = state.publication; const publicationTextLeftPart = publication.text.substring(0, state.cursorPosition.start); const publicationTextRightPart = publication.text.substring(state.cursorPosition.start); const codeBlockInstruction = `\n[code lg="${programmingLanguage}"]\n${codeBlock}\n[/code]\n\n`; const textWithTags = `${publicationTextLeftPart}${codeBlockInstruction}${publicationTextRightPart}`; publication.text = textWithTags; this.#state.set(state); this.#updateForm(); } loadPreview(): void { const state = this.#state(); this.#isPreviewing.set(true); const request: PreviewContentRequest = { text: state.publication.text }; this.#publicationRestService.preview(request) .then(response => { state.publication.parsedText = response.text; this.#state.set(state); setTimeout(() => Prism.highlightAll(), 1000); }) .catch(error => { console.error(error); }) .finally(() => { this.#isPreviewing.set(false); }); } }