|
import { HttpClient } from '@angular/common/http'; |
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, inject } from '@angular/core'; |
|
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; |
|
import { EditorModule } from '@tinymce/tinymce-angular'; |
|
import { lastValueFrom } from 'rxjs'; |
|
|
|
interface TinyMceBlobInfo { |
|
id: () => string; |
|
name: () => string; |
|
filename: () => string; |
|
blob: () => Blob; |
|
base64: () => string; |
|
blobUri: () => string; |
|
} |
|
type OnChangeCallback = (value: string) => void; |
|
type OnTouchedCallback = () => void; |
|
|
|
@Component({ |
|
// eslint-disable-next-line @angular-eslint/component-selector |
|
selector: 'tinymce-editor', |
|
imports: [EditorModule, FormsModule], |
|
template: `<editor [(ngModel)]="value" (ngModelChange)="onEditorChange($event)" [init]="editorConfig" [disabled]="disabled" (onBlur)="onTouched()"></editor>`, |
|
changeDetection: ChangeDetectionStrategy.OnPush, |
|
providers: [ |
|
{ |
|
provide: NG_VALUE_ACCESSOR, |
|
useExisting: forwardRef(() => TinymceEditorComponent), |
|
multi: true, |
|
}, |
|
], |
|
}) |
|
export class TinymceEditorComponent implements ControlValueAccessor { |
|
private http = inject(HttpClient); |
|
private cdr = inject(ChangeDetectorRef); |
|
|
|
value = ''; |
|
|
|
onChange: OnChangeCallback = (_value: string) => { return; }; |
|
onTouched: OnTouchedCallback = () => { return; }; |
|
disabled = false; |
|
|
|
editorConfig = { |
|
base_url: '/assets/tinymce', |
|
suffix: '.min', |
|
plugins: 'advlist lists link image media table code codesample searchreplace directionality charmap emoticons visualblocks visualchars fullscreen insertdatetime nonbreaking pagebreak save help', |
|
toolbar: 'blocks removeformat align bold italic underline strikethrough subscript superscript forecolor link image media table outdent indent code searchreplace | fontfamily fontsize bullist numlist blockquote charmap codesample undo redo selectall', |
|
height: 300, |
|
menubar: true, |
|
paste_as_text: false, |
|
paste_block_drop: false, |
|
paste_data_images: true, |
|
images_upload_handler: async (blobInfo: TinyMceBlobInfo): Promise<string> => { |
|
/* |
|
BE request: form-data: file |
|
BE response: { location: 'url_ảnh_vừa_upload' } |
|
*/ |
|
const formData = new FormData(); |
|
formData.append('file', blobInfo.blob(), blobInfo.filename()); |
|
const uploadUrl = 'https://api.yourserver.com/upload-endpoint'; |
|
try { |
|
const response = await lastValueFrom(this.http.post<{ location: string; }>(uploadUrl, formData)); |
|
if (response?.location) { |
|
this.cdr.markForCheck(); |
|
return response.location; |
|
} |
|
throw new Error('Server response missing location'); |
|
} catch (error: unknown) { |
|
if (error instanceof Error) { |
|
console.error(error.message); |
|
} |
|
throw error; |
|
} |
|
}, |
|
}; |
|
|
|
writeValue(val: unknown): void { |
|
this.value = typeof val === 'string' ? val : ''; |
|
this.cdr.markForCheck(); |
|
} |
|
|
|
registerOnChange(fn: OnChangeCallback): void { |
|
this.onChange = fn; |
|
} |
|
|
|
registerOnTouched(fn: OnTouchedCallback): void { |
|
this.onTouched = fn; |
|
} |
|
|
|
setDisabledState?(isDisabled: boolean): void { |
|
this.disabled = isDisabled; |
|
this.cdr.markForCheck(); |
|
} |
|
|
|
onEditorChange(newValue: string): void { |
|
this.value = newValue; |
|
this.onChange(newValue); |
|
this.cdr.markForCheck(); |
|
} |
|
} |