diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index 0919d89b1b..525ef70a1e 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -483,17 +483,6 @@ export class Uppy { this.#emitter.emit(event, ...args) } - /** @deprecated */ - on>( - event: K, - callback: DeprecatedUppyEventMap[K], - ): this - - on>( - event: K, - callback: _UppyEventMap[K], - ): this - on>( event: K, callback: UppyEventMap[K], diff --git a/packages/@uppy/thumbnail-generator/.npmignore b/packages/@uppy/thumbnail-generator/.npmignore new file mode 100644 index 0000000000..6c816673f0 --- /dev/null +++ b/packages/@uppy/thumbnail-generator/.npmignore @@ -0,0 +1 @@ +tsconfig.* diff --git a/packages/@uppy/thumbnail-generator/src/index.test.js b/packages/@uppy/thumbnail-generator/src/index.test.js index 4149d3dd8a..7c429d0078 100644 --- a/packages/@uppy/thumbnail-generator/src/index.test.js +++ b/packages/@uppy/thumbnail-generator/src/index.test.js @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, it, expect, vi } from 'vitest' import { UIPlugin } from '@uppy/core' import emitter from 'namespace-emitter' -import ThumbnailGeneratorPlugin from './index.js' +import ThumbnailGeneratorPlugin from './index.ts' const delay = duration => new Promise(resolve => setTimeout(resolve, duration)) diff --git a/packages/@uppy/thumbnail-generator/src/index.js b/packages/@uppy/thumbnail-generator/src/index.ts similarity index 58% rename from packages/@uppy/thumbnail-generator/src/index.js rename to packages/@uppy/thumbnail-generator/src/index.ts index e5e5df3da2..4763a798a7 100644 --- a/packages/@uppy/thumbnail-generator/src/index.js +++ b/packages/@uppy/thumbnail-generator/src/index.ts @@ -1,48 +1,83 @@ -import { UIPlugin } from '@uppy/core' +import { UIPlugin, Uppy, type UIPluginOptions } from '@uppy/core' import dataURItoBlob from '@uppy/utils/lib/dataURItoBlob' import isObjectURL from '@uppy/utils/lib/isObjectURL' import isPreviewSupported from '@uppy/utils/lib/isPreviewSupported' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore untyped import { rotation } from 'exifr/dist/mini.esm.mjs' -import locale from './locale.js' +import type { DefinePluginOpts } from '@uppy/core/lib/BasePlugin.ts' +import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile' +import locale from './locale.ts' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore We don't want TS to generate types for the package.json import packageJson from '../package.json' +declare module '@uppy/core' { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + export interface UppyEventMap { + 'thumbnail:all-generated': () => void + 'thumbnail:generated': (file: UppyFile, preview: string) => void + 'thumbnail:error': (file: UppyFile, error: Error) => void + 'thumbnail:request': (file: UppyFile) => void + 'thumbnail:cancel': (file: UppyFile) => void + } +} + +interface Rotation { + deg: number + rad: number + scaleX: number + scaleY: number + dimensionSwapped: boolean + css: boolean + canvas: boolean +} + /** * Save a element's content to a Blob object. * - * @param {HTMLCanvasElement} canvas - * @returns {Promise} */ -function canvasToBlob (canvas, type, quality) { +function canvasToBlob( + canvas: HTMLCanvasElement, + type: string, + quality: number, +): Promise { try { - canvas.getContext('2d').getImageData(0, 0, 1, 1) + canvas.getContext('2d')!.getImageData(0, 0, 1, 1) } catch (err) { if (err.code === 18) { - return Promise.reject(new Error('cannot read image, probably an svg with external resources')) + return Promise.reject( + new Error('cannot read image, probably an svg with external resources'), + ) } } if (canvas.toBlob) { - return new Promise(resolve => { + return new Promise((resolve) => { canvas.toBlob(resolve, type, quality) }).then((blob) => { if (blob === null) { - throw new Error('cannot read image, probably an svg with external resources') + throw new Error( + 'cannot read image, probably an svg with external resources', + ) } return blob }) } - return Promise.resolve().then(() => { - return dataURItoBlob(canvas.toDataURL(type, quality), {}) - }).then((blob) => { - if (blob === null) { - throw new Error('could not extract blob, probably an old browser') - } - return blob - }) + return Promise.resolve() + .then(() => { + return dataURItoBlob(canvas.toDataURL(type, quality), {}) + }) + .then((blob) => { + if (blob === null) { + throw new Error('could not extract blob, probably an old browser') + } + return blob + }) } -function rotateImage (image, translate) { +function rotateImage(image: HTMLImageElement, translate: Rotation) { let w = image.width let h = image.height @@ -55,13 +90,19 @@ function rotateImage (image, translate) { canvas.width = w canvas.height = h - const context = canvas.getContext('2d') + const context = canvas.getContext('2d')! context.translate(w / 2, h / 2) if (translate.canvas) { context.rotate(translate.rad) context.scale(translate.scaleX, translate.scaleY) } - context.drawImage(image, -image.width / 2, -image.height / 2, image.width, image.height) + context.drawImage( + image, + -image.width / 2, + -image.height / 2, + image.width, + image.height, + ) return canvas } @@ -70,7 +111,7 @@ function rotateImage (image, translate) { * Make sure the image doesn’t exceed browser/device canvas limits. * For ios with 256 RAM and ie */ -function protect (image) { +function protect(image: HTMLCanvasElement): HTMLCanvasElement { // https://stackoverflow.com/questions/6081483/maximum-size-of-a-canvas-element const ratio = image.width / image.height @@ -92,59 +133,81 @@ function protect (image) { const canvas = document.createElement('canvas') canvas.width = maxW canvas.height = maxH - canvas.getContext('2d').drawImage(image, 0, 0, maxW, maxH) + canvas.getContext('2d')!.drawImage(image, 0, 0, maxW, maxH) return canvas } return image } +export interface ThumbnailGeneratorOptions extends UIPluginOptions { + thumbnailWidth?: number | null + thumbnailHeight?: number | null + thumbnailType?: string + waitForThumbnailsBeforeUpload?: boolean + lazy?: boolean +} + +const defaultOptions = { + thumbnailWidth: null, + thumbnailHeight: null, + thumbnailType: 'image/jpeg', + waitForThumbnailsBeforeUpload: false, + lazy: false, +} + +type Opts = DefinePluginOpts< + ThumbnailGeneratorOptions, + keyof typeof defaultOptions +> + /** * The Thumbnail Generator plugin */ -export default class ThumbnailGenerator extends UIPlugin { +export default class ThumbnailGenerator< + M extends Meta, + B extends Body, +> extends UIPlugin { static VERSION = packageJson.version - constructor (uppy, opts) { - super(uppy, opts) + queue: string[] + + queueProcessing: boolean + + defaultThumbnailDimension: number + + thumbnailType: string + + constructor(uppy: Uppy, opts?: ThumbnailGeneratorOptions) { + super(uppy, { ...defaultOptions, ...opts }) this.type = 'modifier' this.id = this.opts.id || 'ThumbnailGenerator' this.title = 'Thumbnail Generator' this.queue = [] this.queueProcessing = false this.defaultThumbnailDimension = 200 - this.thumbnailType = this.opts.thumbnailType || 'image/jpeg' + this.thumbnailType = this.opts.thumbnailType this.defaultLocale = locale - const defaultOptions = { - thumbnailWidth: null, - thumbnailHeight: null, - waitForThumbnailsBeforeUpload: false, - lazy: false, - } - - this.opts = { ...defaultOptions, ...opts } this.i18nInit() if (this.opts.lazy && this.opts.waitForThumbnailsBeforeUpload) { - throw new Error('ThumbnailGenerator: The `lazy` and `waitForThumbnailsBeforeUpload` options are mutually exclusive. Please ensure at most one of them is set to `true`.') + throw new Error( + 'ThumbnailGenerator: The `lazy` and `waitForThumbnailsBeforeUpload` options are mutually exclusive. Please ensure at most one of them is set to `true`.', + ) } } - /** - * Create a thumbnail for the given Uppy file object. - * - * @param {{data: Blob}} file - * @param {number} targetWidth - * @param {number} targetHeight - * @returns {Promise} - */ - createThumbnail (file, targetWidth, targetHeight) { + createThumbnail( + file: UppyFile, + targetWidth: number | null, + targetHeight: number | null, + ): Promise { const originalUrl = URL.createObjectURL(file.data) - const onload = new Promise((resolve, reject) => { + const onload = new Promise((resolve, reject) => { const image = new Image() image.src = originalUrl image.addEventListener('load', () => { @@ -157,16 +220,27 @@ export default class ThumbnailGenerator extends UIPlugin { }) }) - const orientationPromise = rotation(file.data).catch(() => 1) + const orientationPromise = rotation(file.data).catch( + () => 1, + ) as Promise return Promise.all([onload, orientationPromise]) .then(([image, orientation]) => { - const dimensions = this.getProportionalDimensions(image, targetWidth, targetHeight, orientation.deg) + const dimensions = this.getProportionalDimensions( + image, + targetWidth, + targetHeight, + orientation.deg, + ) const rotatedImage = rotateImage(image, orientation) - const resizedImage = this.resizeImage(rotatedImage, dimensions.width, dimensions.height) + const resizedImage = this.resizeImage( + rotatedImage, + dimensions.width, + dimensions.height, + ) return canvasToBlob(resizedImage, this.thumbnailType, 80) }) - .then(blob => { + .then((blob) => { return URL.createObjectURL(blob) }) } @@ -177,9 +251,15 @@ export default class ThumbnailGenerator extends UIPlugin { * account. If neither width nor height are given, the default dimension * is used. */ - getProportionalDimensions (img, width, height, rotation) { // eslint-disable-line no-shadow + getProportionalDimensions( + img: HTMLImageElement, + width: number | null, + height: number | null, + deg: number, + ): { width: number; height: number } { + // eslint-disable-line no-shadow let aspect = img.width / img.height - if (rotation === 90 || rotation === 270) { + if (deg === 90 || deg === 270) { aspect = img.height / img.width } @@ -209,7 +289,11 @@ export default class ThumbnailGenerator extends UIPlugin { * Returns a Canvas with the resized image on it. */ // eslint-disable-next-line class-methods-use-this - resizeImage (image, targetWidth, targetHeight) { + resizeImage( + image: HTMLCanvasElement, + targetWidth: number, + targetHeight: number, + ): HTMLCanvasElement { // Resizing in steps refactored to use a solution from // https://blog.uploadcare.com/image-resize-in-browsers-is-broken-e38eed08df01 @@ -227,7 +311,7 @@ export default class ThumbnailGenerator extends UIPlugin { const canvas = document.createElement('canvas') canvas.width = sW canvas.height = sH - canvas.getContext('2d').drawImage(img, 0, 0, sW, sH) + canvas.getContext('2d')!.drawImage(img, 0, 0, sW, sH) img = canvas sW = Math.round(sW / x) @@ -240,23 +324,26 @@ export default class ThumbnailGenerator extends UIPlugin { /** * Set the preview URL for a file. */ - setPreviewURL (fileID, preview) { + setPreviewURL(fileID: string, preview: string): void { this.uppy.setFileState(fileID, { preview }) } - addToQueue (item) { - this.queue.push(item) + addToQueue(fileID: string): void { + this.queue.push(fileID) if (this.queueProcessing === false) { this.processQueue() } } - processQueue () { + processQueue(): Promise { this.queueProcessing = true if (this.queue.length > 0) { - const current = this.uppy.getFile(this.queue.shift()) + const current = this.uppy.getFile(this.queue.shift() as string) if (!current) { - this.uppy.log('[ThumbnailGenerator] file was removed before a thumbnail could be generated, but not removed from the queue. This is probably a bug', 'error') + this.uppy.log( + '[ThumbnailGenerator] file was removed before a thumbnail could be generated, but not removed from the queue. This is probably a bug', + 'error', + ) return Promise.resolve() } return this.requestThumbnail(current) @@ -269,16 +356,29 @@ export default class ThumbnailGenerator extends UIPlugin { return Promise.resolve() } - requestThumbnail (file) { + requestThumbnail(file: UppyFile): Promise { if (isPreviewSupported(file.type) && !file.isRemote) { - return this.createThumbnail(file, this.opts.thumbnailWidth, this.opts.thumbnailHeight) - .then(preview => { + return this.createThumbnail( + file, + this.opts.thumbnailWidth, + this.opts.thumbnailHeight, + ) + .then((preview) => { this.setPreviewURL(file.id, preview) - this.uppy.log(`[ThumbnailGenerator] Generated thumbnail for ${file.id}`) - this.uppy.emit('thumbnail:generated', this.uppy.getFile(file.id), preview) + this.uppy.log( + `[ThumbnailGenerator] Generated thumbnail for ${file.id}`, + ) + this.uppy.emit( + 'thumbnail:generated', + this.uppy.getFile(file.id), + preview, + ) }) - .catch(err => { - this.uppy.log(`[ThumbnailGenerator] Failed thumbnail for ${file.id}:`, 'warning') + .catch((err) => { + this.uppy.log( + `[ThumbnailGenerator] Failed thumbnail for ${file.id}:`, + 'warning', + ) this.uppy.log(err, 'warning') this.uppy.emit('thumbnail:error', this.uppy.getFile(file.id), err) }) @@ -286,12 +386,12 @@ export default class ThumbnailGenerator extends UIPlugin { return Promise.resolve() } - onFileAdded = (file) => { + onFileAdded = (file: UppyFile): void => { if ( - !file.preview - && file.data - && isPreviewSupported(file.type) - && !file.isRemote + !file.preview && + file.data && + isPreviewSupported(file.type) && + !file.isRemote ) { this.addToQueue(file.id) } @@ -300,7 +400,7 @@ export default class ThumbnailGenerator extends UIPlugin { /** * Cancel a lazy request for a thumbnail if the thumbnail has not yet been generated. */ - onCancelRequest = (file) => { + onCancelRequest = (file: UppyFile): void => { const index = this.queue.indexOf(file.id) if (index !== -1) { this.queue.splice(index, 1) @@ -310,7 +410,7 @@ export default class ThumbnailGenerator extends UIPlugin { /** * Clean up the thumbnail for a file. Cancel lazy requests and free the thumbnail URL. */ - onFileRemoved = (file) => { + onFileRemoved = (file: UppyFile): void => { const index = this.queue.indexOf(file.id) if (index !== -1) { this.queue.splice(index, 1) @@ -322,8 +422,8 @@ export default class ThumbnailGenerator extends UIPlugin { } } - onRestored = () => { - const restoredFiles = this.uppy.getFiles().filter(file => file.isRestored) + onRestored = (): void => { + const restoredFiles = this.uppy.getFiles().filter((file) => file.isRestored) restoredFiles.forEach((file) => { // Only add blob URLs; they are likely invalid after being restored. if (!file.preview || isObjectURL(file.preview)) { @@ -332,11 +432,11 @@ export default class ThumbnailGenerator extends UIPlugin { }) } - onAllFilesRemoved = () => { + onAllFilesRemoved = (): void => { this.queue = [] } - waitUntilAllProcessed = (fileIDs) => { + waitUntilAllProcessed = (fileIDs: string[]): Promise => { fileIDs.forEach((fileID) => { const file = this.uppy.getFile(fileID) this.uppy.emit('preprocess-progress', file, { @@ -365,7 +465,7 @@ export default class ThumbnailGenerator extends UIPlugin { }) } - install () { + install(): void { this.uppy.on('file-removed', this.onFileRemoved) this.uppy.on('cancel-all', this.onAllFilesRemoved) @@ -383,7 +483,7 @@ export default class ThumbnailGenerator extends UIPlugin { } } - uninstall () { + uninstall(): void { this.uppy.off('file-removed', this.onFileRemoved) this.uppy.off('cancel-all', this.onAllFilesRemoved) diff --git a/packages/@uppy/thumbnail-generator/src/locale.js b/packages/@uppy/thumbnail-generator/src/locale.ts similarity index 100% rename from packages/@uppy/thumbnail-generator/src/locale.js rename to packages/@uppy/thumbnail-generator/src/locale.ts diff --git a/packages/@uppy/thumbnail-generator/tsconfig.build.json b/packages/@uppy/thumbnail-generator/tsconfig.build.json new file mode 100644 index 0000000000..1b0ca41093 --- /dev/null +++ b/packages/@uppy/thumbnail-generator/tsconfig.build.json @@ -0,0 +1,25 @@ +{ + "extends": "../../../tsconfig.shared", + "compilerOptions": { + "noImplicitAny": false, + "outDir": "./lib", + "paths": { + "@uppy/utils/lib/*": ["../utils/src/*"], + "@uppy/core": ["../core/src/index.js"], + "@uppy/core/lib/*": ["../core/src/*"] + }, + "resolveJsonModule": false, + "rootDir": "./src", + "skipLibCheck": true + }, + "include": ["./src/**/*.*"], + "exclude": ["./src/**/*.test.ts"], + "references": [ + { + "path": "../utils/tsconfig.build.json" + }, + { + "path": "../core/tsconfig.build.json" + } + ] +} diff --git a/packages/@uppy/thumbnail-generator/tsconfig.json b/packages/@uppy/thumbnail-generator/tsconfig.json new file mode 100644 index 0000000000..a76c3b714a --- /dev/null +++ b/packages/@uppy/thumbnail-generator/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.shared", + "compilerOptions": { + "emitDeclarationOnly": false, + "noEmit": true, + "paths": { + "@uppy/utils/lib/*": ["../utils/src/*"], + "@uppy/core": ["../core/src/index.js"], + "@uppy/core/lib/*": ["../core/src/*"], + }, + }, + "include": ["./package.json", "./src/**/*.*"], + "references": [ + { + "path": "../utils/tsconfig.build.json", + }, + { + "path": "../core/tsconfig.build.json", + }, + ], +}