diff --git a/package-lock.json b/package-lock.json index c347cf5fd8a28..6f82f32040b25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.2.0", "clippie": "4.1.5", - "cropperjs": "1.6.2", + "cropperjs": "2.0.0", "css-loader": "7.1.2", "dayjs": "1.11.13", "dropzone": "6.0.0-beta.2", @@ -433,6 +433,126 @@ "node": ">=14.0.0" } }, + "node_modules/@cropper/element": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/element/-/element-2.0.0.tgz", + "integrity": "sha512-lsthn0nQq73GExUE7Mg/ss6Q3RXADGDv055hxoLFwvl/wGHgy6ZkYlfLZ/VmgBHC6jDK5IgPBFnqrPqlXWSGBA==", + "license": "MIT", + "dependencies": { + "@cropper/utils": "^2.0.0" + } + }, + "node_modules/@cropper/element-canvas": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/element-canvas/-/element-canvas-2.0.0.tgz", + "integrity": "sha512-GPtGJgSm92crJhhhwUsaMw3rz2KfJWWSz7kRAlufFEV/EHTP5+6r6/Z1BCGRna830i+Avqbm435XLOtA7PVJwA==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0", + "@cropper/utils": "^2.0.0" + } + }, + "node_modules/@cropper/element-crosshair": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/element-crosshair/-/element-crosshair-2.0.0.tgz", + "integrity": "sha512-KfPfyrdeFvUC31Ws7ATtcalWWSaMtrC6bMoCipZhqbUOE7wZoL4ecDSL6BUOZxPa74awZUqfzirCDjHvheBfyw==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0", + "@cropper/utils": "^2.0.0" + } + }, + "node_modules/@cropper/element-grid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/element-grid/-/element-grid-2.0.0.tgz", + "integrity": "sha512-i78SQ0IJTLFveKX6P7svkfMYVdgHrQ8ZmmEw8keFy9n1ZVbK+SK0UHK5FNMRNI/gtVhKJOGEnK/zeyjUdj4Iyw==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0", + "@cropper/utils": "^2.0.0" + } + }, + "node_modules/@cropper/element-handle": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/element-handle/-/element-handle-2.0.0.tgz", + "integrity": "sha512-ZJvW+0MkK9E8xYymGdoruaQn2kwjSHFpNSWinjyq6csuVQiCPxlX5ovAEDldmZ9MWePPtWEi3vLKQOo2Yb0T8g==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0", + "@cropper/utils": "^2.0.0" + } + }, + "node_modules/@cropper/element-image": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/element-image/-/element-image-2.0.0.tgz", + "integrity": "sha512-9BxiTS/aHRmrjopaFQb9mQQXmx4ruhYHGkDZMVz24AXpMFjUY6OpqrWse/WjzD9tfhMFvEdu17b3VAekcAgpeg==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0", + "@cropper/element-canvas": "^2.0.0", + "@cropper/utils": "^2.0.0" + } + }, + "node_modules/@cropper/element-selection": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/element-selection/-/element-selection-2.0.0.tgz", + "integrity": "sha512-ensNnbIfJsJ8bhbJTH/RXtk2URFvTOO4TvfRk461n2FPEC588D7rwBmUJxQg74IiTi4y1JbCI+6j+4LyzYBLCQ==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0", + "@cropper/element-canvas": "^2.0.0", + "@cropper/element-image": "^2.0.0", + "@cropper/utils": "^2.0.0" + } + }, + "node_modules/@cropper/element-shade": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/element-shade/-/element-shade-2.0.0.tgz", + "integrity": "sha512-jv/2bbNZnhU4W+T4G0c8ADocLIZvQFTXgCf2RFDNhI5UVxurzWBnDdb8Mx8LnVplnkTqO+xUmHZYve0CwgWo+Q==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0", + "@cropper/element-canvas": "^2.0.0", + "@cropper/element-selection": "^2.0.0", + "@cropper/utils": "^2.0.0" + } + }, + "node_modules/@cropper/element-viewer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/element-viewer/-/element-viewer-2.0.0.tgz", + "integrity": "sha512-zY+3VRN5TvpM8twlphYtXw0tzJL2VgzeK7ufhL1BixVqOdRxwP13TprYIhqwGt9EW/SyJZUiaIu396T89kRX8A==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0", + "@cropper/element-canvas": "^2.0.0", + "@cropper/element-image": "^2.0.0", + "@cropper/element-selection": "^2.0.0", + "@cropper/utils": "^2.0.0" + } + }, + "node_modules/@cropper/elements": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/elements/-/elements-2.0.0.tgz", + "integrity": "sha512-PQkPo1nUjxLFUQuHYu+6atfHxpX9B41Xribao6wpvmvmNIFML6LQdNqqWYb6LyM7ujsu71CZdBiMT5oetjJVoQ==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0", + "@cropper/element-canvas": "^2.0.0", + "@cropper/element-crosshair": "^2.0.0", + "@cropper/element-grid": "^2.0.0", + "@cropper/element-handle": "^2.0.0", + "@cropper/element-image": "^2.0.0", + "@cropper/element-selection": "^2.0.0", + "@cropper/element-shade": "^2.0.0", + "@cropper/element-viewer": "^2.0.0" + } + }, + "node_modules/@cropper/utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/utils/-/utils-2.0.0.tgz", + "integrity": "sha512-cprLYr+7kK3faGgoOsTW9gIn5sefDr2KwOmgyjzIXk+8PLpW8FgFKEg5FoWfRD5zMAmkCBuX6rGKDK3VdUEGrg==", + "license": "MIT" + }, "node_modules/@csstools/css-parser-algorithms": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", @@ -5298,10 +5418,14 @@ } }, "node_modules/cropperjs": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.6.2.tgz", - "integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==", - "license": "MIT" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-2.0.0.tgz", + "integrity": "sha512-TO2j0Qre01kPHbow4FuTrbdEB4jTmGRySxW49jyEIqlJZuEBfrvCTT0vC3eRB2WBXudDfKi1Onako6DKWKxeAQ==", + "license": "MIT", + "dependencies": { + "@cropper/elements": "^2.0.0", + "@cropper/utils": "^2.0.0" + } }, "node_modules/cross-spawn": { "version": "7.0.6", diff --git a/package.json b/package.json index ef1a132994b17..3d197c9113fcc 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.2.0", "clippie": "4.1.5", - "cropperjs": "1.6.2", + "cropperjs": "2.0.0", "css-loader": "7.1.2", "dayjs": "1.11.13", "dropzone": "6.0.0-beta.2", diff --git a/templates/shared/avatar_upload_crop.tmpl b/templates/shared/avatar_upload_crop.tmpl index 2c4166fa9c93c..7f2138f60b9d2 100644 --- a/templates/shared/avatar_upload_crop.tmpl +++ b/templates/shared/avatar_upload_crop.tmpl @@ -4,5 +4,5 @@ {{- /* the cropper-panel must be next sibling of the input "avatar" */ -}}
{{ctx.Locale.Tr "settings.cropper_prompt"}}
-
+
diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl index 91f04e0b53dc6..d94ba0499e0b4 100644 --- a/templates/shared/user/profile_big_avatar.tmpl +++ b/templates/shared/user/profile_big_avatar.tmpl @@ -1,7 +1,7 @@
{{if eq .SignedUserID .ContextUser.ID}} - + {{/* the size doesn't take affect (and no need to take affect), image size(width) should be controlled by the parent container since this is not a flex layout*/}} {{ctx.AvatarUtils.Avatar .ContextUser 256}} diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl index 03c3c18f28c1c..abf9a3f16add9 100644 --- a/templates/user/settings/profile.tmpl +++ b/templates/user/settings/profile.tmpl @@ -97,7 +97,7 @@
-

+

{{ctx.Locale.Tr "settings.avatar"}}

diff --git a/web_src/css/features/cropper.css b/web_src/css/features/cropper.css index f7f8168006056..c9281af7d25bd 100644 --- a/web_src/css/features/cropper.css +++ b/web_src/css/features/cropper.css @@ -1,6 +1,8 @@ -@import "cropperjs/dist/cropper.css"; - .avatar-file-with-cropper + .cropper-panel .cropper-wrapper { - max-width: 400px; + width: 400px; max-height: 400px; } + +cropper-canvas { + height: 400px; +} diff --git a/web_src/js/features/comp/Cropper.ts b/web_src/js/features/comp/Cropper.ts index aaa169115238d..2b4afe21d6165 100644 --- a/web_src/js/features/comp/Cropper.ts +++ b/web_src/js/features/comp/Cropper.ts @@ -1,47 +1,67 @@ -import {showElem, type DOMEvent} from '../../utils/dom.ts'; +import {createElementFromHTML, hideElem, showElem, type DOMEvent} from '../../utils/dom.ts'; +import {debounce} from 'perfect-debounce'; +import type {CropperCanvas, CropperSelection} from 'cropperjs'; type CropperOpts = { container: HTMLElement, - imageSource: HTMLImageElement, + wrapper: HTMLDivElement, fileInput: HTMLInputElement, } -async function initCompCropper({container, fileInput, imageSource}: CropperOpts) { - const {default: Cropper} = await import(/* webpackChunkName: "cropperjs" */'cropperjs'); - let currentFileName = ''; - let currentFileLastModified = 0; - const cropper = new Cropper(imageSource, { - aspectRatio: 1, - viewMode: 2, - autoCrop: false, - crop() { - const canvas = cropper.getCroppedCanvas(); +async function initCompCropper({container, fileInput, wrapper}: CropperOpts) { + await import(/* webpackChunkName: "cropperjs" */'cropperjs'); + + fileInput.addEventListener('input', (e: DOMEvent) => { + if (!e.target.files?.length) { + wrapper.replaceChildren(); + hideElem(container); + return; + } + + const [file] = e.target.files; + const objectUrl = URL.createObjectURL(file); + const cropperCanvas = createElementFromHTML(` + + + + + + + + + + + + + + + + + `); + cropperCanvas.querySelector('cropper-selection').addEventListener('change', debounce(async (e) => { + const selection = e.target as CropperSelection; + if (!selection.width || !selection.height) return; + const canvas = await selection.$toCanvas(); + canvas.toBlob((blob) => { - const croppedFileName = currentFileName.replace(/\.[^.]{3,4}$/, '.png'); - const croppedFile = new File([blob], croppedFileName, {type: 'image/png', lastModified: currentFileLastModified}); const dataTransfer = new DataTransfer(); - dataTransfer.items.add(croppedFile); + dataTransfer.items.add(new File( + [blob], + file.name.replace(/\.[^.]{3,4}$/, '.png'), + {type: 'image/png', lastModified: file.lastModified}, + )); fileInput.files = dataTransfer.files; }); - }, - }); + }, 200)); - fileInput.addEventListener('input', (e: DOMEvent) => { - const files = e.target.files; - if (files?.length > 0) { - currentFileName = files[0].name; - currentFileLastModified = files[0].lastModified; - const fileURL = URL.createObjectURL(files[0]); - imageSource.src = fileURL; - cropper.replace(fileURL); - showElem(container); - } + wrapper.replaceChildren(cropperCanvas); + showElem(container); }); } export async function initAvatarUploaderWithCropper(fileInput: HTMLInputElement) { const panel = fileInput.nextElementSibling as HTMLElement; if (!panel?.matches('.cropper-panel')) throw new Error('Missing cropper panel for avatar uploader'); - const imageSource = panel.querySelector('.cropper-source'); - await initCompCropper({container: panel, fileInput, imageSource}); + const wrapper = panel.querySelector('.cropper-wrapper'); + await initCompCropper({container: panel, fileInput, wrapper}); }