import { Location } from "@angular/common"; import { inject, Injectable, OnDestroy } from "@angular/core"; import { MatDialog } from "@angular/material/dialog"; import { MatSnackBar } from "@angular/material/snack-bar"; import { ActivatedRoute } from "@angular/router"; import { BehaviorSubject, Observable, Subscription } from "rxjs"; import { 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"; 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_PUBLICATION: Publication = { id: '', key: '', title: '', text: '', parsedText: '', description: '', creationDate: new Date(), illustrationId: '', categoryId: '', author: { id: '', name: '', image: '' } }; 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 { private readonly activatedRoute = inject(ActivatedRoute); private readonly publicationRestService = inject(PublicationRestService); private readonly location = inject(Location); private readonly snackBar = inject(MatSnackBar); private readonly dialog = inject(MatDialog); private isLoadingSubject = new BehaviorSubject(false); private stateSubject = new BehaviorSubject(copy(DEFAULT_STATE)); private subscriptions: Subscription[] = []; private isSavingSubject = new BehaviorSubject(false); private isPreviewingSubject = new BehaviorSubject(false); ngOnDestroy(): void { this.subscriptions.forEach(subscription => subscription.unsubscribe()); } private get _state(): PublicationEditionState { return this.stateSubject.value; } private _save(state: PublicationEditionState): void { this.stateSubject.next(state); } get isLoading$(): Observable { return this.isLoadingSubject.asObservable(); } get isSaving$(): Observable { return this.isSavingSubject.asObservable(); } get isPreviewing$(): Observable { return this.isPreviewingSubject.asObservable(); } get state$(): Observable { return this.stateSubject.asObservable(); } loadPublication(): void { this.isLoadingSubject.next(true); this.activatedRoute.paramMap.subscribe(params => { const publicationId = params.get('publicationId'); if (publicationId == undefined) { this.snackBar.open('A technical error occurred while loading publication data.', 'Close', {duration: 5000}); this.location.back(); } else { this.publicationRestService.getById(publicationId) .then(publication => { const state = this._state; state.publication = publication; this.stateSubject.next(state); }) .catch(error => { const errorMessage = 'A technical error occurred while loading publication data.'; this.snackBar.open(errorMessage, 'Close', {duration: 5000}); console.error(errorMessage, error) }) .finally(() => this.isLoadingSubject.next(false)); } }); } init(publication: Publication): void { const state = this._state; state.publication = publication; this.stateSubject.next(state); } editTitle(title: string): void { const state = this._state; state.publication.title = title; this._save(state); } editDescription(description: string): void { const state = this._state; state.publication.description = description; this._save(state); } editCategoryId(categoryId: string): void { const state = this._state; state.publication.categoryId = categoryId; this._save(state); } editText(text: string): void { const state = this._state; state.publication.text = text; this._save(state); } editIllustrationId(pictureId: string): void { const state = this._state; state.publication.illustrationId = pictureId this._save(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._save(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._save(state); } 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._save(state); } 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._save(state); } 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._save(state); } loadPreview(): void { const state = this._state; this.isPreviewingSubject.next(true); this.publicationRestService.preview(state.publication.text) .then(parsedText => { state.publication.parsedText = parsedText; this._save(state); setTimeout(() => Prism.highlightAll(), 1000); }) .catch(error => { console.error(error); }) .finally(() => { this.isPreviewingSubject.next(false); }); } }