import { Modal } from './modal'; import Cropper from 'cropperjs'; export class AvatarModal extends Modal { private cropper?: Cropper; private csrf!: string; private stages!: NodeListOf; private fileInput!: HTMLInputElement; private continueBtn!: HTMLElement; private resetBtn!: HTMLElement; private uploadBtn!: HTMLElement; private cropRoot!: HTMLImageElement; private previewRoot!: HTMLImageElement; private currentAvatar!: HTMLImageElement; private cropResultUrl?: string; private cropResultBlob?: Blob; private currentStep = 1; constructor() { super('avatar'); } public reset(): void { super.reset(); if (!this.modal) { return; } if (this.cropper) { this.cropper.destroy(); } this.cropResultUrl = null; this.cropResultBlob = null; this.fileInput.value = null; this.previewRoot.removeAttribute('src'); this.cropRoot.removeAttribute('src'); this.setStep(1); } public initialize(): void { super.initialize(); if (!this.modal) { return; } this.csrf = (document.querySelector('#csrf') as HTMLInputElement).value; this.stages = this.modal?.querySelectorAll( '[data-upload-step]', ) as NodeListOf; this.fileInput = this.modal?.querySelector( '#image-file', ) as HTMLInputElement; this.cropRoot = this.modal?.querySelector('#cropper') as HTMLImageElement; this.previewRoot = this.modal?.querySelector( '#crop-result', ) as HTMLImageElement; this.currentAvatar = document.querySelector( '#current-avatar', ) as HTMLImageElement; this.continueBtn = this.modal?.querySelector('#continue') as HTMLElement; this.resetBtn = this.modal?.querySelector('#reset') as HTMLElement; this.uploadBtn = this.modal?.querySelector('#upload') as HTMLElement; this.setSteps(); this.registerEvents(); } public setSteps(): void { this.stages?.forEach((item) => { const itemState = parseInt(item.getAttribute('data-upload-step'), 10); item.style.display = itemState === this.currentStep ? null : 'none'; }); } public setStep(index: number): void { this.currentStep = index; this.setSteps(); } private startCrop(): void { const imgf = this.fileInput.files[0]; if (!imgf.type.includes('image/') || imgf.type.includes('svg')) { // TODO: error return; } const reader = new FileReader(); reader.onerror = () => { // TODO: error this.reset(); }; reader.onload = () => { this.cropRoot.src = reader.result as string; this.createCropper(); this.setStep(2); }; reader.readAsDataURL(imgf); } private registerEvents(): void { if (!this.modal) { return; } this.fileInput.addEventListener('change', () => { this.startCrop(); }); this.continueBtn.addEventListener('click', () => { this.createCropResult(); this.setStep(3); }); this.resetBtn.addEventListener('click', () => { if (this.cropper) { this.cropper.destroy(); } this.fileInput.value = null; this.cropResultUrl = null; this.cropResultBlob = null; this.previewRoot.removeAttribute('src'); this.setStep(1); }); this.uploadBtn.addEventListener('click', () => { const formData = new FormData(); formData.append('file', this.cropResultBlob); formData.append('_csrf', this.csrf); // TODO: error fetch('/account/avatar', { method: 'POST', body: formData, }) .then((res) => res.json()) .then((data) => { this.reset(); if (data.file) { this.currentAvatar.src = `/uploads/${data.file}`; } }); }); } private createCropper(): void { this.cropper = new Cropper(this.cropRoot, { aspectRatio: 1, viewMode: 1, modal: false, }); } private createCropResult(): void { const cropCanvas = this.cropper.getCroppedCanvas({ maxHeight: 1024, maxWidth: 1024, imageSmoothingEnabled: true, imageSmoothingQuality: 'high', }); this.cropResultUrl = cropCanvas.toDataURL(); this.previewRoot.src = this.cropResultUrl; cropCanvas.toBlob((blob) => { this.cropResultBlob = blob; this.setStep(3); }); } }