|
1 | | -import {createElementFromHTML, showElem, type DOMEvent} from '../../utils/dom.ts'; |
2 | | -import type {CropperCanvas, CropperImage} from 'cropperjs'; |
| 1 | +import {createElementFromHTML, hideElem, showElem, type DOMEvent} from '../../utils/dom.ts'; |
| 2 | +import {debounce} from 'perfect-debounce'; |
| 3 | +import type {CropperCanvas, CropperSelection} from 'cropperjs'; |
3 | 4 |
|
4 | 5 | type CropperOpts = { |
5 | 6 | container: HTMLElement, |
6 | | - imageSource: HTMLImageElement, |
| 7 | + wrapper: HTMLDivElement, |
7 | 8 | fileInput: HTMLInputElement, |
8 | 9 | } |
9 | 10 |
|
10 | | -async function initCompCropper({container, fileInput, imageSource}: CropperOpts) { |
| 11 | +async function initCompCropper({container, fileInput, wrapper}: CropperOpts) { |
11 | 12 | await import(/* webpackChunkName: "cropperjs" */'cropperjs'); |
12 | | - let currentFileName = ''; |
13 | | - let currentFileLastModified = 0; |
14 | 13 |
|
15 | | - const canvasEl = createElementFromHTML<CropperCanvas>(` |
16 | | - <cropper-canvas background theme-color="var(--color-primary)"> |
17 | | - <cropper-image src="${imageSource.src}" scalable skewable translatable></cropper-image> |
18 | | - <cropper-shade hidden></cropper-shade> |
19 | | - <cropper-handle action="select" plain></cropper-handle> |
20 | | - <cropper-selection initial-coverage="0.5" initial-aspect-ratio="1" movable resizable outlined> |
21 | | - <cropper-grid role="grid" covered></cropper-grid> |
22 | | - <cropper-crosshair centered></cropper-crosshair> |
23 | | - <cropper-handle action="move" theme-color="#ffffff23"></cropper-handle> |
24 | | - <cropper-handle action="n-resize"></cropper-handle> |
25 | | - <cropper-handle action="e-resize"></cropper-handle> |
26 | | - <cropper-handle action="s-resize"></cropper-handle> |
27 | | - <cropper-handle action="w-resize"></cropper-handle> |
28 | | - <cropper-handle action="ne-resize"></cropper-handle> |
29 | | - <cropper-handle action="nw-resize"></cropper-handle> |
30 | | - <cropper-handle action="se-resize"></cropper-handle> |
31 | | - <cropper-handle action="sw-resize"></cropper-handle> |
32 | | - </cropper-selection> |
33 | | - </cropper-canvas> |
34 | | - `); |
35 | | - |
36 | | - const imgEl = canvasEl.querySelector<CropperImage>('cropper-image'); |
| 14 | + fileInput.addEventListener('input', (e: DOMEvent<Event, HTMLInputElement>) => { |
| 15 | + if (!e.target.files?.length) { |
| 16 | + wrapper.replaceChildren(); |
| 17 | + hideElem(container); |
| 18 | + return; |
| 19 | + } |
37 | 20 |
|
38 | | - canvasEl.addEventListener('action', async (e) => { |
39 | | - const canvas = await (e.target as CropperCanvas).$toCanvas(); |
40 | | - canvas.toBlob((blob) => { |
41 | | - const croppedFileName = currentFileName.replace(/\.[^.]{3,4}$/, '.png'); |
42 | | - const croppedFile = new File([blob], croppedFileName, {type: 'image/png', lastModified: currentFileLastModified}); |
43 | | - const dataTransfer = new DataTransfer(); |
44 | | - dataTransfer.items.add(croppedFile); |
45 | | - fileInput.files = dataTransfer.files; |
46 | | - }); |
47 | | - }); |
| 21 | + const [file] = e.target.files; |
| 22 | + const objectUrl = URL.createObjectURL(file); |
| 23 | + const canvasEl = createElementFromHTML<CropperCanvas>(` |
| 24 | + <cropper-canvas theme-color="var(--color-primary)"> |
| 25 | + <cropper-image src="${objectUrl}" scalable skewable translatable></cropper-image> |
| 26 | + <cropper-shade hidden></cropper-shade> |
| 27 | + <cropper-handle action="select" plain></cropper-handle> |
| 28 | + <cropper-selection aspect-ratio="1" movable resizable> |
| 29 | + <cropper-handle action="move" theme-color="transparent"></cropper-handle> |
| 30 | + <cropper-handle action="n-resize"></cropper-handle> |
| 31 | + <cropper-handle action="e-resize"></cropper-handle> |
| 32 | + <cropper-handle action="s-resize"></cropper-handle> |
| 33 | + <cropper-handle action="w-resize"></cropper-handle> |
| 34 | + <cropper-handle action="ne-resize"></cropper-handle> |
| 35 | + <cropper-handle action="nw-resize"></cropper-handle> |
| 36 | + <cropper-handle action="se-resize"></cropper-handle> |
| 37 | + <cropper-handle action="sw-resize"></cropper-handle> |
| 38 | + </cropper-selection> |
| 39 | + </cropper-canvas> |
| 40 | + `); |
| 41 | + canvasEl.querySelector<CropperSelection>('cropper-selection').addEventListener('change', debounce(async (e) => { |
| 42 | + const selection = e.target as CropperSelection; |
| 43 | + if (!selection.width || !selection.height) return; |
| 44 | + const canvas = await selection.$toCanvas(); |
48 | 45 |
|
49 | | - imageSource.replaceWith(canvasEl); |
| 46 | + canvas.toBlob((blob) => { |
| 47 | + const dataTransfer = new DataTransfer(); |
| 48 | + dataTransfer.items.add(new File( |
| 49 | + [blob], |
| 50 | + file.name.replace(/\.[^.]{3,4}$/, '.png'), |
| 51 | + {type: 'image/png', lastModified: file.lastModified}, |
| 52 | + )); |
| 53 | + fileInput.files = dataTransfer.files; |
| 54 | + }); |
| 55 | + }, 200)); |
50 | 56 |
|
51 | | - fileInput.addEventListener('input', (e: DOMEvent<Event, HTMLInputElement>) => { |
52 | | - const files = e.target.files; |
53 | | - if (files?.length > 0) { |
54 | | - currentFileName = files[0].name; |
55 | | - currentFileLastModified = files[0].lastModified; |
56 | | - const fileURL = URL.createObjectURL(files[0]); |
57 | | - imageSource.src = fileURL; |
58 | | - // @ts-expect-error - https://github.com/go-gitea/gitea/pull/33827 |
59 | | - imgEl.src = fileURL; |
60 | | - showElem(container); |
61 | | - } |
| 57 | + wrapper.replaceChildren(canvasEl); |
| 58 | + showElem(container); |
62 | 59 | }); |
63 | 60 | } |
64 | 61 |
|
65 | 62 | export async function initAvatarUploaderWithCropper(fileInput: HTMLInputElement) { |
66 | 63 | const panel = fileInput.nextElementSibling as HTMLElement; |
67 | 64 | if (!panel?.matches('.cropper-panel')) throw new Error('Missing cropper panel for avatar uploader'); |
68 | | - const imageSource = panel.querySelector<HTMLImageElement>('.cropper-source'); |
69 | | - await initCompCropper({container: panel, fileInput, imageSource}); |
| 65 | + const wrapper = panel.querySelector<HTMLImageElement>('.cropper-wrapper'); |
| 66 | + await initCompCropper({container: panel, fileInput, wrapper}); |
70 | 67 | } |
0 commit comments