mirror of
https://github.com/transloadit/uppy.git
synced 2026-07-28 12:37:00 +00:00
@uppy/utils: refactor to TS (#4699)
Co-authored-by: Mikael Finstad <finstaden@gmail.com> Co-authored-by: Nick Rutten <2504906+nickrttn@users.noreply.github.com> Co-authored-by: Murderlon <merlijn@soverin.net> Co-authored-by: Artur Paikin <artur@arturpaikin.com>
This commit is contained in:
parent
43178f1c4f
commit
51ecc66e64
99 changed files with 1083 additions and 703 deletions
3
packages/@uppy/core/types/index.d.ts
vendored
3
packages/@uppy/core/types/index.d.ts
vendored
|
|
@ -1,3 +1,6 @@
|
|||
// This references the old types on purpose, to make sure to not create breaking changes for TS consumers.
|
||||
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
||||
/// <reference path="../../utils/types/index.d.ts"/>
|
||||
import * as UppyUtils from '@uppy/utils'
|
||||
|
||||
// Utility types
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
"preact": "^10.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.199",
|
||||
"vitest": "^0.34.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
import hasOwnProperty from './hasProperty.js'
|
||||
/**
|
||||
* Little AbortController proxy module so we can swap out the implementation easily later.
|
||||
*/
|
||||
export const { AbortController } = globalThis
|
||||
export const { AbortSignal } = globalThis
|
||||
export const createAbortError = (message = 'Aborted', options) => {
|
||||
const err = new DOMException(message, 'AbortError')
|
||||
if (options != null && hasOwnProperty(options, 'cause')) {
|
||||
Object.defineProperty(err, 'cause', { __proto__: null, configurable: true, writable: true, value: options.cause })
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { AbortController, AbortSignal } from './AbortController.js'
|
||||
import { AbortController, AbortSignal } from './AbortController.ts'
|
||||
|
||||
function flushInstantTimeouts () {
|
||||
return new Promise(resolve => setTimeout(resolve, 0))
|
||||
function flushInstantTimeouts() {
|
||||
return new Promise((resolve) => setTimeout(resolve, 0))
|
||||
}
|
||||
|
||||
describe('AbortController', () => {
|
||||
22
packages/@uppy/utils/src/AbortController.ts
Normal file
22
packages/@uppy/utils/src/AbortController.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import hasOwnProperty from './hasProperty.ts'
|
||||
/**
|
||||
* Little AbortController proxy module so we can swap out the implementation easily later.
|
||||
*/
|
||||
export const { AbortController } = globalThis
|
||||
export const { AbortSignal } = globalThis
|
||||
export const createAbortError = (
|
||||
message = 'Aborted',
|
||||
options?: Parameters<typeof Error>[1],
|
||||
): DOMException => {
|
||||
const err = new DOMException(message, 'AbortError')
|
||||
if (options != null && hasOwnProperty(options, 'cause')) {
|
||||
Object.defineProperty(err, 'cause', {
|
||||
// @ts-expect-error TS is drunk
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: options.cause,
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import hasProperty from './hasProperty.js'
|
||||
|
||||
class ErrorWithCause extends Error {
|
||||
constructor (message, options = {}) {
|
||||
super(message)
|
||||
this.cause = options.cause
|
||||
if (this.cause && hasProperty(this.cause, 'isNetworkError')) {
|
||||
this.isNetworkError = this.cause.isNetworkError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorWithCause
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import ErrorWithCause from './ErrorWithCause.js'
|
||||
import NetworkError from './NetworkError.js'
|
||||
import isNetworkError from './isNetworkError.js'
|
||||
|
||||
describe('ErrorWithCause', () => {
|
||||
it('should support a `{ cause }` option', () => {
|
||||
const cause = new Error('cause')
|
||||
expect(new ErrorWithCause('message').cause).toEqual(undefined)
|
||||
expect(new ErrorWithCause('message', {}).cause).toEqual(undefined)
|
||||
expect(new ErrorWithCause('message', { cause }).cause).toEqual(cause)
|
||||
})
|
||||
it('should propagate isNetworkError', () => {
|
||||
const regularError = new Error('cause')
|
||||
const networkError = new NetworkError('cause')
|
||||
expect(isNetworkError(new ErrorWithCause('message', { cause: networkError }).isNetworkError)).toEqual(true)
|
||||
expect(isNetworkError(new ErrorWithCause('message', { cause: regularError }).isNetworkError)).toEqual(false)
|
||||
expect(isNetworkError(new ErrorWithCause('message', {}).isNetworkError)).toEqual(false)
|
||||
expect(isNetworkError(new ErrorWithCause('message').isNetworkError)).toEqual(false)
|
||||
})
|
||||
})
|
||||
24
packages/@uppy/utils/src/ErrorWithCause.test.ts
Normal file
24
packages/@uppy/utils/src/ErrorWithCause.test.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import ErrorWithCause from './ErrorWithCause.ts'
|
||||
import NetworkError from './NetworkError.ts'
|
||||
|
||||
describe('ErrorWithCause', () => {
|
||||
it('should support a `{ cause }` option', () => {
|
||||
const cause = new Error('cause')
|
||||
expect(new ErrorWithCause('message').cause).toEqual(undefined)
|
||||
expect(new ErrorWithCause('message', {}).cause).toEqual(undefined)
|
||||
expect(new ErrorWithCause('message', { cause }).cause).toEqual(cause)
|
||||
})
|
||||
it('should propagate isNetworkError', () => {
|
||||
const regularError = new Error('cause')
|
||||
const networkError = new NetworkError('cause')
|
||||
expect(
|
||||
new ErrorWithCause('message', { cause: networkError }).isNetworkError,
|
||||
).toEqual(true)
|
||||
expect(
|
||||
new ErrorWithCause('message', { cause: regularError }).isNetworkError,
|
||||
).toEqual(false)
|
||||
expect(new ErrorWithCause('message', {}).isNetworkError).toEqual(false)
|
||||
expect(new ErrorWithCause('message').isNetworkError).toEqual(false)
|
||||
})
|
||||
})
|
||||
23
packages/@uppy/utils/src/ErrorWithCause.ts
Normal file
23
packages/@uppy/utils/src/ErrorWithCause.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type NetworkError from './NetworkError.ts'
|
||||
import hasProperty from './hasProperty.ts'
|
||||
|
||||
class ErrorWithCause extends Error {
|
||||
public isNetworkError: boolean
|
||||
|
||||
public cause: Error['cause']
|
||||
|
||||
constructor(
|
||||
message?: ConstructorParameters<ErrorConstructor>[0],
|
||||
options?: ConstructorParameters<ErrorConstructor>[1],
|
||||
) {
|
||||
super(message)
|
||||
this.cause = options?.cause
|
||||
if (this.cause && hasProperty(this.cause, 'isNetworkError')) {
|
||||
this.isNetworkError = (this.cause as NetworkError).isNetworkError
|
||||
} else {
|
||||
this.isNetworkError = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorWithCause
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
/**
|
||||
* Create a wrapper around an event emitter with a `remove` method to remove
|
||||
* all events that were added using the wrapped emitter.
|
||||
*/
|
||||
export default class EventManager {
|
||||
#uppy
|
||||
|
||||
#events = []
|
||||
|
||||
constructor (uppy) {
|
||||
this.#uppy = uppy
|
||||
}
|
||||
|
||||
on (event, fn) {
|
||||
this.#events.push([event, fn])
|
||||
return this.#uppy.on(event, fn)
|
||||
}
|
||||
|
||||
remove () {
|
||||
for (const [event, fn] of this.#events.splice(0)) {
|
||||
this.#uppy.off(event, fn)
|
||||
}
|
||||
}
|
||||
|
||||
onFilePause (fileID, cb) {
|
||||
this.on('upload-pause', (targetFileID, isPaused) => {
|
||||
if (fileID === targetFileID) {
|
||||
cb(isPaused)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onFileRemove (fileID, cb) {
|
||||
this.on('file-removed', (file) => {
|
||||
if (fileID === file.id) cb(file.id)
|
||||
})
|
||||
}
|
||||
|
||||
onPause (fileID, cb) {
|
||||
this.on('upload-pause', (targetFileID, isPaused) => {
|
||||
if (fileID === targetFileID) {
|
||||
// const isPaused = this.#uppy.pauseResume(fileID)
|
||||
cb(isPaused)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onRetry (fileID, cb) {
|
||||
this.on('upload-retry', (targetFileID) => {
|
||||
if (fileID === targetFileID) {
|
||||
cb()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onRetryAll (fileID, cb) {
|
||||
this.on('retry-all', () => {
|
||||
if (!this.#uppy.getFile(fileID)) return
|
||||
cb()
|
||||
})
|
||||
}
|
||||
|
||||
onPauseAll (fileID, cb) {
|
||||
this.on('pause-all', () => {
|
||||
if (!this.#uppy.getFile(fileID)) return
|
||||
cb()
|
||||
})
|
||||
}
|
||||
|
||||
onCancelAll (fileID, eventHandler) {
|
||||
this.on('cancel-all', (...args) => {
|
||||
if (!this.#uppy.getFile(fileID)) return
|
||||
eventHandler(...args)
|
||||
})
|
||||
}
|
||||
|
||||
onResumeAll (fileID, cb) {
|
||||
this.on('resume-all', () => {
|
||||
if (!this.#uppy.getFile(fileID)) return
|
||||
cb()
|
||||
})
|
||||
}
|
||||
}
|
||||
115
packages/@uppy/utils/src/EventManager.ts
Normal file
115
packages/@uppy/utils/src/EventManager.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import type {
|
||||
Uppy,
|
||||
UploadPauseCallback,
|
||||
FileRemovedCallback,
|
||||
UploadRetryCallback,
|
||||
GenericEventCallback,
|
||||
} from '@uppy/core'
|
||||
import type { UppyFile } from './UppyFile'
|
||||
/**
|
||||
* Create a wrapper around an event emitter with a `remove` method to remove
|
||||
* all events that were added using the wrapped emitter.
|
||||
*/
|
||||
export default class EventManager {
|
||||
#uppy: Uppy
|
||||
|
||||
#events: Array<Parameters<typeof Uppy.prototype.on>> = []
|
||||
|
||||
constructor(uppy: Uppy) {
|
||||
this.#uppy = uppy
|
||||
}
|
||||
|
||||
on(
|
||||
event: Parameters<typeof Uppy.prototype.on>[0],
|
||||
fn: Parameters<typeof Uppy.prototype.on>[1],
|
||||
): Uppy {
|
||||
this.#events.push([event, fn])
|
||||
return this.#uppy.on(event, fn)
|
||||
}
|
||||
|
||||
remove(): void {
|
||||
for (const [event, fn] of this.#events.splice(0)) {
|
||||
this.#uppy.off(event, fn)
|
||||
}
|
||||
}
|
||||
|
||||
onFilePause(fileID: UppyFile['id'], cb: (isPaused: boolean) => void): void {
|
||||
this.on(
|
||||
'upload-pause',
|
||||
(
|
||||
targetFileID: Parameters<UploadPauseCallback>[0],
|
||||
isPaused: Parameters<UploadPauseCallback>[1],
|
||||
) => {
|
||||
if (fileID === targetFileID) {
|
||||
cb(isPaused)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
onFileRemove(
|
||||
fileID: UppyFile['id'],
|
||||
cb: (isPaused: UppyFile['id']) => void,
|
||||
): void {
|
||||
this.on('file-removed', (file: Parameters<FileRemovedCallback<any>>[0]) => {
|
||||
if (fileID === file.id) cb(file.id)
|
||||
})
|
||||
}
|
||||
|
||||
onPause(fileID: UppyFile['id'], cb: (isPaused: boolean) => void): void {
|
||||
this.on(
|
||||
'upload-pause',
|
||||
(
|
||||
targetFileID: Parameters<UploadPauseCallback>[0],
|
||||
isPaused: Parameters<UploadPauseCallback>[1],
|
||||
) => {
|
||||
if (fileID === targetFileID) {
|
||||
// const isPaused = this.#uppy.pauseResume(fileID)
|
||||
cb(isPaused)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
onRetry(fileID: UppyFile['id'], cb: () => void): void {
|
||||
this.on(
|
||||
'upload-retry',
|
||||
(targetFileID: Parameters<UploadRetryCallback>[0]) => {
|
||||
if (fileID === targetFileID) {
|
||||
cb()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
onRetryAll(fileID: UppyFile['id'], cb: () => void): void {
|
||||
this.on('retry-all', () => {
|
||||
if (!this.#uppy.getFile(fileID)) return
|
||||
cb()
|
||||
})
|
||||
}
|
||||
|
||||
onPauseAll(fileID: UppyFile['id'], cb: () => void): void {
|
||||
this.on('pause-all', () => {
|
||||
if (!this.#uppy.getFile(fileID)) return
|
||||
cb()
|
||||
})
|
||||
}
|
||||
|
||||
onCancelAll(
|
||||
fileID: UppyFile['id'],
|
||||
eventHandler: GenericEventCallback,
|
||||
): void {
|
||||
this.on('cancel-all', (...args: Parameters<GenericEventCallback>) => {
|
||||
if (!this.#uppy.getFile(fileID)) return
|
||||
eventHandler(...args)
|
||||
})
|
||||
}
|
||||
|
||||
onResumeAll(fileID: UppyFile['id'], cb: () => void): void {
|
||||
this.on('resume-all', () => {
|
||||
if (!this.#uppy.getFile(fileID)) return
|
||||
cb()
|
||||
})
|
||||
}
|
||||
}
|
||||
18
packages/@uppy/utils/src/FileProgress.ts
Normal file
18
packages/@uppy/utils/src/FileProgress.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
interface FileProgressBase {
|
||||
progress: number
|
||||
uploadComplete: boolean
|
||||
percentage: number
|
||||
bytesTotal: number
|
||||
}
|
||||
|
||||
// FileProgress is either started or not started. We want to make sure TS doesn't
|
||||
// let us mix the two cases, and for that effect, we have one type for each case:
|
||||
export type FileProgressStarted = FileProgressBase & {
|
||||
uploadStarted: number
|
||||
bytesUploaded: number
|
||||
}
|
||||
export type FileProgressNotStarted = FileProgressBase & {
|
||||
uploadStarted: null
|
||||
bytesUploaded: false
|
||||
}
|
||||
export type FileProgress = FileProgressStarted | FileProgressNotStarted
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
class NetworkError extends Error {
|
||||
constructor (error, xhr = null) {
|
||||
super(`This looks like a network error, the endpoint might be blocked by an internet provider or a firewall.`)
|
||||
|
||||
this.cause = error
|
||||
this.isNetworkError = true
|
||||
this.request = xhr
|
||||
}
|
||||
}
|
||||
|
||||
export default NetworkError
|
||||
19
packages/@uppy/utils/src/NetworkError.ts
Normal file
19
packages/@uppy/utils/src/NetworkError.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
class NetworkError extends Error {
|
||||
public cause: unknown
|
||||
|
||||
public isNetworkError: true
|
||||
|
||||
public request: null | XMLHttpRequest
|
||||
|
||||
constructor(error: unknown, xhr: null | XMLHttpRequest = null) {
|
||||
super(
|
||||
`This looks like a network error, the endpoint might be blocked by an internet provider or a firewall.`,
|
||||
)
|
||||
|
||||
this.cause = error
|
||||
this.isNetworkError = true
|
||||
this.request = xhr
|
||||
}
|
||||
}
|
||||
|
||||
export default NetworkError
|
||||
|
|
@ -5,20 +5,23 @@
|
|||
* Call `timer.done()` when the upload has completed.
|
||||
*/
|
||||
class ProgressTimeout {
|
||||
#aliveTimer
|
||||
#aliveTimer?: ReturnType<typeof setTimeout>
|
||||
|
||||
#isDone = false
|
||||
|
||||
#onTimedOut
|
||||
#onTimedOut: Parameters<typeof setTimeout>[0]
|
||||
|
||||
#timeout
|
||||
#timeout: number
|
||||
|
||||
constructor (timeout, timeoutHandler) {
|
||||
constructor(
|
||||
timeout: number,
|
||||
timeoutHandler: Parameters<typeof setTimeout>[0],
|
||||
) {
|
||||
this.#timeout = timeout
|
||||
this.#onTimedOut = timeoutHandler
|
||||
}
|
||||
|
||||
progress () {
|
||||
progress(): void {
|
||||
// Some browsers fire another progress event when the upload is
|
||||
// cancelled, so we have to ignore progress after the timer was
|
||||
// told to stop.
|
||||
|
|
@ -30,10 +33,10 @@ class ProgressTimeout {
|
|||
}
|
||||
}
|
||||
|
||||
done () {
|
||||
done(): void {
|
||||
if (!this.#isDone) {
|
||||
clearTimeout(this.#aliveTimer)
|
||||
this.#aliveTimer = null
|
||||
this.#aliveTimer = undefined
|
||||
this.#isDone = true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { RateLimitedQueue } from './RateLimitedQueue.js'
|
||||
|
||||
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
import delay from './delay.ts'
|
||||
|
||||
describe('RateLimitedQueue', () => {
|
||||
let pending = 0
|
||||
function fn () {
|
||||
function fn() {
|
||||
pending++
|
||||
return delay(15).then(() => pending--)
|
||||
}
|
||||
|
|
@ -15,9 +14,16 @@ describe('RateLimitedQueue', () => {
|
|||
const fn2 = queue.wrapPromiseFunction(fn)
|
||||
|
||||
const result = Promise.all([
|
||||
fn2(), fn2(), fn2(), fn2(),
|
||||
fn2(), fn2(), fn2(), fn2(),
|
||||
fn2(), fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
])
|
||||
|
||||
expect(pending).toBe(4)
|
||||
|
|
@ -34,9 +40,16 @@ describe('RateLimitedQueue', () => {
|
|||
const fn2 = queue.wrapPromiseFunction(fn)
|
||||
|
||||
const result = Promise.all([
|
||||
fn2(), fn2(), fn2(), fn2(),
|
||||
fn2(), fn2(), fn2(), fn2(),
|
||||
fn2(), fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
])
|
||||
|
||||
expect(pending).toBe(10)
|
||||
|
|
@ -48,13 +61,22 @@ describe('RateLimitedQueue', () => {
|
|||
|
||||
it('should accept non-promise function in wrapPromiseFunction()', () => {
|
||||
const queue = new RateLimitedQueue(1)
|
||||
function syncFn () { return 1 }
|
||||
function syncFn() {
|
||||
return 1
|
||||
}
|
||||
const fn2 = queue.wrapPromiseFunction(syncFn)
|
||||
|
||||
return Promise.all([
|
||||
fn2(), fn2(), fn2(), fn2(),
|
||||
fn2(), fn2(), fn2(), fn2(),
|
||||
fn2(), fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
fn2(),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import Translator from './Translator.js'
|
||||
import Translator, { type Locale } from './Translator.ts'
|
||||
|
||||
const english = {
|
||||
const english: Locale<0 | 1> = {
|
||||
strings: {
|
||||
chooseFile: 'Choose a file',
|
||||
youHaveChosen: 'You have chosen: %{fileName}',
|
||||
|
|
@ -9,12 +9,12 @@ const english = {
|
|||
0: '%{smart_count} file selected',
|
||||
1: '%{smart_count} files selected',
|
||||
},
|
||||
pluralize (n) {
|
||||
if (n === 1) {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
},
|
||||
},
|
||||
pluralize(n) {
|
||||
if (n === 1) {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ const russian = {
|
|||
2: 'Выбрано %{smart_count} файлов',
|
||||
},
|
||||
},
|
||||
pluralize (n) {
|
||||
pluralize(n: number) {
|
||||
if (n % 10 === 1 && n % 100 !== 11) {
|
||||
return 0
|
||||
}
|
||||
|
|
@ -50,41 +50,58 @@ describe('Translator', () => {
|
|||
|
||||
it('should translate a string with non-string elements', () => {
|
||||
const translator = new Translator({
|
||||
pluralize: english.pluralize,
|
||||
strings: {
|
||||
test: 'Hello %{who}!',
|
||||
test2: 'Hello %{who}',
|
||||
},
|
||||
})
|
||||
|
||||
const who = Symbol('who')
|
||||
expect(translator.translateArray('test', { who })).toEqual(['Hello ', who, '!'])
|
||||
const who = Symbol('who') as any as string
|
||||
expect(translator.translateArray('test', { who })).toEqual([
|
||||
'Hello ',
|
||||
who,
|
||||
'!',
|
||||
])
|
||||
// No empty string at the end.
|
||||
expect(translator.translateArray('test2', { who })).toEqual(['Hello ', who])
|
||||
expect(translator.translateArray('test2', { who })).toEqual([
|
||||
'Hello ',
|
||||
who,
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('translation strings inheritance / overriding', () => {
|
||||
const launguagePackLoadedInCore = english
|
||||
const defaultStrings = {
|
||||
pluralize: english.pluralize,
|
||||
strings: {
|
||||
youHaveChosen: 'You have chosen 123: %{fileName}',
|
||||
},
|
||||
}
|
||||
const userSuppliedStrings = {
|
||||
pluralize: english.pluralize,
|
||||
strings: {
|
||||
youHaveChosen: 'Beep boop: %{fileName}',
|
||||
},
|
||||
}
|
||||
|
||||
it('should prioritize language pack strings from Core over default', () => {
|
||||
const translator = new Translator([defaultStrings, launguagePackLoadedInCore])
|
||||
const translator = new Translator([
|
||||
defaultStrings,
|
||||
launguagePackLoadedInCore,
|
||||
])
|
||||
expect(
|
||||
translator.translate('youHaveChosen', { fileName: 'img.jpg' }),
|
||||
).toEqual('You have chosen: img.jpg')
|
||||
})
|
||||
|
||||
it('should prioritize user-supplied strings over language pack from Core', () => {
|
||||
const translator = new Translator([defaultStrings, launguagePackLoadedInCore, userSuppliedStrings])
|
||||
const translator = new Translator([
|
||||
defaultStrings,
|
||||
launguagePackLoadedInCore,
|
||||
userSuppliedStrings,
|
||||
])
|
||||
expect(
|
||||
translator.translate('youHaveChosen', { fileName: 'img.jpg' }),
|
||||
).toEqual('Beep boop: img.jpg')
|
||||
|
|
@ -103,17 +120,17 @@ describe('Translator', () => {
|
|||
describe('pluralization', () => {
|
||||
it('should translate a string', () => {
|
||||
const translator = new Translator(russian)
|
||||
expect(
|
||||
translator.translate('filesChosen', { smart_count: 18 }),
|
||||
).toEqual('Выбрано 18 файлов')
|
||||
expect(translator.translate('filesChosen', { smart_count: 18 })).toEqual(
|
||||
'Выбрано 18 файлов',
|
||||
)
|
||||
|
||||
expect(
|
||||
translator.translate('filesChosen', { smart_count: 1 }),
|
||||
).toEqual('Выбран 1 файл')
|
||||
expect(translator.translate('filesChosen', { smart_count: 1 })).toEqual(
|
||||
'Выбран 1 файл',
|
||||
)
|
||||
|
||||
expect(
|
||||
translator.translate('filesChosen', { smart_count: 0 }),
|
||||
).toEqual('Выбрано 0 файлов')
|
||||
expect(translator.translate('filesChosen', { smart_count: 0 })).toEqual(
|
||||
'Выбрано 0 файлов',
|
||||
)
|
||||
})
|
||||
|
||||
it('should support strings without plural forms', () => {
|
||||
|
|
@ -124,12 +141,12 @@ describe('Translator', () => {
|
|||
pluralize: () => 0,
|
||||
})
|
||||
|
||||
expect(
|
||||
translator.translate('theAmount', { smart_count: 0 }),
|
||||
).toEqual('het aantal is 0')
|
||||
expect(
|
||||
translator.translate('theAmount', { smart_count: 1 }),
|
||||
).toEqual('het aantal is 1')
|
||||
expect(translator.translate('theAmount', { smart_count: 0 })).toEqual(
|
||||
'het aantal is 0',
|
||||
)
|
||||
expect(translator.translate('theAmount', { smart_count: 1 })).toEqual(
|
||||
'het aantal is 1',
|
||||
)
|
||||
expect(
|
||||
translator.translate('theAmount', { smart_count: 1202530 }),
|
||||
).toEqual('het aantal is 1202530')
|
||||
|
|
@ -143,11 +160,14 @@ describe('Translator', () => {
|
|||
1: '%{smart_count} tests',
|
||||
},
|
||||
},
|
||||
pluralize: () => 1,
|
||||
})
|
||||
|
||||
expect(() => {
|
||||
translator.translate('test')
|
||||
}).toThrow('Attempted to use a string with plural forms, but no value was given for %{smart_count}')
|
||||
}).toThrow(
|
||||
'Attempted to use a string with plural forms, but no value was given for %{smart_count}',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,7 +1,23 @@
|
|||
import has from './hasProperty.js'
|
||||
import has from './hasProperty.ts'
|
||||
|
||||
function insertReplacement (source, rx, replacement) {
|
||||
const newParts = []
|
||||
// We're using a generic because languages have different plural rules.
|
||||
export interface Locale<T extends number = number> {
|
||||
strings: Record<string, string | Record<T, string>>
|
||||
pluralize: (n: number) => T
|
||||
}
|
||||
|
||||
type Options = {
|
||||
smart_count?: number
|
||||
} & {
|
||||
[key: string]: string | number
|
||||
}
|
||||
|
||||
function insertReplacement(
|
||||
source: Array<string | unknown>,
|
||||
rx: RegExp,
|
||||
replacement: string,
|
||||
): Array<string | unknown> {
|
||||
const newParts: Array<string | unknown> = []
|
||||
source.forEach((chunk) => {
|
||||
// When the source contains multiple placeholders for interpolation,
|
||||
// we should ignore chunks that are not strings, because those
|
||||
|
|
@ -32,14 +48,16 @@ function insertReplacement (source, rx, replacement) {
|
|||
* @license https://github.com/airbnb/polyglot.js/blob/master/LICENSE
|
||||
* taken from https://github.com/airbnb/polyglot.js/blob/master/lib/polyglot.js#L299
|
||||
*
|
||||
* @param {string} phrase that needs interpolation, with placeholders
|
||||
* @param {object} options with values that will be used to replace placeholders
|
||||
* @returns {any[]} interpolated
|
||||
* @param phrase that needs interpolation, with placeholders
|
||||
* @param options with values that will be used to replace placeholders
|
||||
*/
|
||||
function interpolate (phrase, options) {
|
||||
function interpolate(
|
||||
phrase: string,
|
||||
options?: Options,
|
||||
): Array<string | unknown> {
|
||||
const dollarRegex = /\$/g
|
||||
const dollarBillsYall = '$$$$'
|
||||
let interpolated = [phrase]
|
||||
let interpolated: Array<string | unknown> = [phrase]
|
||||
|
||||
if (options == null) return interpolated
|
||||
|
||||
|
|
@ -55,7 +73,11 @@ function interpolate (phrase, options) {
|
|||
// We create a new `RegExp` each time instead of using a more-efficient
|
||||
// string replace so that the same argument can be replaced multiple times
|
||||
// in the same phrase.
|
||||
interpolated = insertReplacement(interpolated, new RegExp(`%\\{${arg}\\}`, 'g'), replacement)
|
||||
interpolated = insertReplacement(
|
||||
interpolated,
|
||||
new RegExp(`%\\{${arg}\\}`, 'g'),
|
||||
replacement as string,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,13 +96,12 @@ function interpolate (phrase, options) {
|
|||
* Usage example: `translator.translate('files_chosen', {smart_count: 3})`
|
||||
*/
|
||||
export default class Translator {
|
||||
/**
|
||||
* @param {object|Array<object>} locales - locale or list of locales.
|
||||
*/
|
||||
constructor (locales) {
|
||||
protected locale: Locale
|
||||
|
||||
constructor(locales: Locale | Locale[]) {
|
||||
this.locale = {
|
||||
strings: {},
|
||||
pluralize (n) {
|
||||
pluralize(n: number): 0 | 1 {
|
||||
if (n === 1) {
|
||||
return 0
|
||||
}
|
||||
|
|
@ -95,35 +116,36 @@ export default class Translator {
|
|||
}
|
||||
}
|
||||
|
||||
#apply (locale) {
|
||||
#apply(locale?: Locale): void {
|
||||
if (!locale?.strings) {
|
||||
return
|
||||
}
|
||||
|
||||
const prevLocale = this.locale
|
||||
this.locale = { ...prevLocale, strings: { ...prevLocale.strings, ...locale.strings } }
|
||||
this.locale = {
|
||||
...prevLocale,
|
||||
strings: { ...prevLocale.strings, ...locale.strings },
|
||||
} as any
|
||||
this.locale.pluralize = locale.pluralize || prevLocale.pluralize
|
||||
}
|
||||
|
||||
/**
|
||||
* Public translate method
|
||||
*
|
||||
* @param {string} key
|
||||
* @param {object} options with values that will be used later to replace placeholders in string
|
||||
* @returns {string} translated (and interpolated)
|
||||
* @param key
|
||||
* @param options with values that will be used later to replace placeholders in string
|
||||
* @returns string translated (and interpolated)
|
||||
*/
|
||||
translate (key, options) {
|
||||
translate(key: string, options?: Options): string {
|
||||
return this.translateArray(key, options).join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a translation and return the translated and interpolated parts as an array.
|
||||
*
|
||||
* @param {string} key
|
||||
* @param {object} options with values that will be used to replace placeholders
|
||||
* @returns {Array} The translated and interpolated parts, in order.
|
||||
* @returns The translated and interpolated parts, in order.
|
||||
*/
|
||||
translateArray (key, options) {
|
||||
translateArray(key: string, options?: Options): Array<string | unknown> {
|
||||
if (!has(this.locale.strings, key)) {
|
||||
throw new Error(`missing string: ${key}`)
|
||||
}
|
||||
|
|
@ -136,7 +158,9 @@ export default class Translator {
|
|||
const plural = this.locale.pluralize(options.smart_count)
|
||||
return interpolate(string[plural], options)
|
||||
}
|
||||
throw new Error('Attempted to use a string with plural forms, but no value was given for %{smart_count}')
|
||||
throw new Error(
|
||||
'Attempted to use a string with plural forms, but no value was given for %{smart_count}',
|
||||
)
|
||||
}
|
||||
|
||||
return interpolate(string, options)
|
||||
41
packages/@uppy/utils/src/UppyFile.ts
Normal file
41
packages/@uppy/utils/src/UppyFile.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import type { FileProgress } from './FileProgress'
|
||||
|
||||
interface IndexedObject<T> {
|
||||
[key: string]: T
|
||||
[key: number]: T
|
||||
}
|
||||
|
||||
export type InternalMetadata = { name: string; type?: string }
|
||||
|
||||
export interface UppyFile<
|
||||
TMeta = IndexedObject<any>,
|
||||
TBody = IndexedObject<any>,
|
||||
> {
|
||||
data: Blob | File
|
||||
error?: Error
|
||||
extension: string
|
||||
id: string
|
||||
isPaused?: boolean
|
||||
isRestored?: boolean
|
||||
isRemote: boolean
|
||||
meta: InternalMetadata & TMeta
|
||||
name: string
|
||||
preview?: string
|
||||
progress?: FileProgress
|
||||
remote?: {
|
||||
host: string
|
||||
url: string
|
||||
body?: Record<string, unknown>
|
||||
provider?: string
|
||||
companionUrl: string
|
||||
}
|
||||
serverToken: string
|
||||
size: number
|
||||
source?: string
|
||||
type?: string
|
||||
response?: {
|
||||
body: TBody
|
||||
status: number
|
||||
uploadURL: string | undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,11 @@
|
|||
* @param {HTMLCanvasElement} canvas
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export default function canvasToBlob (canvas, type, quality) {
|
||||
export default function canvasToBlob(
|
||||
canvas: HTMLCanvasElement,
|
||||
type: string,
|
||||
quality?: number,
|
||||
): Promise<Blob | null> {
|
||||
return new Promise((resolve) => {
|
||||
canvas.toBlob(resolve, type, quality)
|
||||
})
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import dataURItoBlob from './dataURItoBlob.js'
|
||||
import dataURItoBlob from './dataURItoBlob.ts'
|
||||
import sampleImageDataURI from './sampleImageDataURI.js'
|
||||
|
||||
describe('dataURItoBlob', () => {
|
||||
|
|
@ -1,21 +1,25 @@
|
|||
const DATA_URL_PATTERN = /^data:([^/]+\/[^,;]+(?:[^,]*?))(;base64)?,([\s\S]*)$/
|
||||
|
||||
export default function dataURItoBlob (dataURI, opts, toFile) {
|
||||
export default function dataURItoBlob(
|
||||
dataURI: string,
|
||||
opts: { mimeType?: string; name?: string },
|
||||
toFile?: boolean,
|
||||
): Blob | File {
|
||||
// get the base64 data
|
||||
const dataURIData = DATA_URL_PATTERN.exec(dataURI)
|
||||
|
||||
// user may provide mime type, if not get it from data URI
|
||||
const mimeType = opts.mimeType ?? dataURIData?.[1] ?? 'plain/text'
|
||||
|
||||
let data
|
||||
if (dataURIData[2] != null) {
|
||||
let data!: BlobPart[] // We add `!` to tell TS we're OK with `data` being not defined when the dataURI is invalid.
|
||||
if (dataURIData?.[2] != null) {
|
||||
const binary = atob(decodeURIComponent(dataURIData[3]))
|
||||
const bytes = new Uint8Array(binary.length)
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
bytes[i] = binary.charCodeAt(i)
|
||||
}
|
||||
data = [bytes]
|
||||
} else {
|
||||
} else if (dataURIData?.[3] != null) {
|
||||
data = [decodeURIComponent(dataURIData[3])]
|
||||
}
|
||||
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import dataURItoBlob from './dataURItoBlob.js'
|
||||
|
||||
export default function dataURItoFile (dataURI, opts) {
|
||||
return dataURItoBlob(dataURI, opts, true)
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import dataURItoFile from './dataURItoFile.js'
|
||||
import dataURItoFile from './dataURItoFile.ts'
|
||||
import sampleImageDataURI from './sampleImageDataURI.js'
|
||||
|
||||
describe('dataURItoFile', () => {
|
||||
8
packages/@uppy/utils/src/dataURItoFile.ts
Normal file
8
packages/@uppy/utils/src/dataURItoFile.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import dataURItoBlob from './dataURItoBlob.ts'
|
||||
|
||||
export default function dataURItoFile(
|
||||
dataURI: string,
|
||||
opts: { mimeType?: string; name?: string },
|
||||
): File | Blob {
|
||||
return dataURItoBlob(dataURI, opts, true)
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { AbortController } from './AbortController.js'
|
||||
import delay from './delay.js'
|
||||
import { AbortController } from './AbortController.ts'
|
||||
import delay from './delay.ts'
|
||||
|
||||
describe('delay', () => {
|
||||
it('should wait for the specified time', async () => {
|
||||
|
|
@ -12,9 +12,12 @@ describe('delay', () => {
|
|||
})
|
||||
|
||||
it('should reject if signal is already aborted', async () => {
|
||||
const signal = { aborted: true }
|
||||
const signal = { aborted: true } as any as AbortSignal
|
||||
const start = Date.now()
|
||||
await expect(delay(100, { signal })).rejects.toHaveProperty('name', 'AbortError')
|
||||
await expect(delay(100, { signal })).rejects.toHaveProperty(
|
||||
'name',
|
||||
'AbortError',
|
||||
)
|
||||
// should really be instant but using a very large range in case CI decides
|
||||
// to be super busy and block the event loop for a while.
|
||||
expect(Date.now() - start).toBeLessThan(50)
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
import { createAbortError } from './AbortController.js'
|
||||
import { createAbortError } from './AbortController.ts'
|
||||
|
||||
/**
|
||||
* Return a Promise that resolves after `ms` milliseconds.
|
||||
*
|
||||
* @param {number} ms - Number of milliseconds to wait.
|
||||
* @param {{ signal?: AbortSignal }} [opts] - An abort signal that can be used to cancel the delay early.
|
||||
* @returns {Promise<void>} A Promise that resolves after the given amount of `ms`.
|
||||
*/
|
||||
export default function delay (ms, opts) {
|
||||
export default function delay(
|
||||
ms: number,
|
||||
opts?: { signal: AbortSignal },
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (opts?.signal?.aborted) {
|
||||
return reject(createAbortError())
|
||||
|
|
@ -18,13 +17,13 @@ export default function delay (ms, opts) {
|
|||
resolve()
|
||||
}, ms)
|
||||
|
||||
function onabort () {
|
||||
function onabort(): void {
|
||||
clearTimeout(timeout)
|
||||
cleanup() // eslint-disable-line no-use-before-define
|
||||
reject(createAbortError())
|
||||
}
|
||||
opts?.signal?.addEventListener('abort', onabort)
|
||||
function cleanup () {
|
||||
function cleanup(): void {
|
||||
opts?.signal?.removeEventListener('abort', onabort)
|
||||
}
|
||||
return undefined
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
/**
|
||||
* Low-pass filter using Exponential Moving Averages (aka exponential smoothing)
|
||||
* Filters a sequence of values by updating the mixing the previous output value
|
||||
* with the new input using the exponential window function
|
||||
*
|
||||
* @param {*} newValue the n-th value of the sequence
|
||||
* @param {*} previousSmoothedValue the exponential average of the first n-1 values
|
||||
* @param {*} halfLife value of `dt` to move the smoothed value halfway between `previousFilteredValue` and `newValue`
|
||||
* @param {*} dt time elapsed between adding the (n-1)th and the n-th values
|
||||
* @returns the exponential average of the first n values
|
||||
*/
|
||||
export default function emaFilter (newValue, previousSmoothedValue, halfLife, dt) {
|
||||
if (halfLife === 0 || newValue === previousSmoothedValue) return newValue
|
||||
if (dt === 0) return previousSmoothedValue
|
||||
|
||||
return newValue + (previousSmoothedValue - newValue) * (2 ** (-dt / halfLife))
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import emaFilter from './emaFilter.js'
|
||||
import emaFilter from './emaFilter.ts'
|
||||
|
||||
describe('emaFilter', () => {
|
||||
it('should calculate the exponential average', () => {
|
||||
|
|
@ -25,7 +25,9 @@ describe('emaFilter', () => {
|
|||
let lastFilteredValue = firstValue
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
lastFilteredValue = emaFilter(newValue, lastFilteredValue, halfLife, step)
|
||||
expect(lastFilteredValue).toBeCloseTo(emaFilter(newValue, firstValue, halfLife, step * (i + 1)))
|
||||
expect(lastFilteredValue).toBeCloseTo(
|
||||
emaFilter(newValue, firstValue, halfLife, step * (i + 1)),
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
22
packages/@uppy/utils/src/emaFilter.ts
Normal file
22
packages/@uppy/utils/src/emaFilter.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* Low-pass filter using Exponential Moving Averages (aka exponential smoothing)
|
||||
* Filters a sequence of values by updating the mixing the previous output value
|
||||
* with the new input using the exponential window function
|
||||
*
|
||||
* @param newValue the n-th value of the sequence
|
||||
* @param previousSmoothedValue the exponential average of the first n-1 values
|
||||
* @param halfLife value of `dt` to move the smoothed value halfway between `previousFilteredValue` and `newValue`
|
||||
* @param dt time elapsed between adding the (n-1)th and the n-th values
|
||||
* @returns the exponential average of the first n values
|
||||
*/
|
||||
export default function emaFilter(
|
||||
newValue: number,
|
||||
previousSmoothedValue: number,
|
||||
halfLife: number,
|
||||
dt: number,
|
||||
): number {
|
||||
if (halfLife === 0 || newValue === previousSmoothedValue) return newValue
|
||||
if (dt === 0) return previousSmoothedValue
|
||||
|
||||
return newValue + (previousSmoothedValue - newValue) * 2 ** (-dt / halfLife)
|
||||
}
|
||||
|
|
@ -1,6 +1,12 @@
|
|||
import throttle from 'lodash/throttle.js'
|
||||
import type { UppyFile } from './UppyFile'
|
||||
import type { FileProgress } from './FileProgress'
|
||||
|
||||
function emitSocketProgress (uploader, progressData, file) {
|
||||
function emitSocketProgress(
|
||||
uploader: any,
|
||||
progressData: FileProgress,
|
||||
file: UppyFile,
|
||||
): void {
|
||||
const { progress, bytesUploaded, bytesTotal } = progressData
|
||||
if (progress) {
|
||||
uploader.uppy.log(`Upload progress: ${progress}`)
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import NetworkError from './NetworkError.js'
|
||||
|
||||
/**
|
||||
* Wrapper around window.fetch that throws a NetworkError when appropriate
|
||||
*/
|
||||
export default function fetchWithNetworkError (...options) {
|
||||
return fetch(...options)
|
||||
.catch((err) => {
|
||||
if (err.name === 'AbortError') { // todo maybe instead see npm package is-network-error
|
||||
throw err
|
||||
} else {
|
||||
throw new NetworkError(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
16
packages/@uppy/utils/src/fetchWithNetworkError.ts
Normal file
16
packages/@uppy/utils/src/fetchWithNetworkError.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import NetworkError from './NetworkError.ts'
|
||||
|
||||
/**
|
||||
* Wrapper around window.fetch that throws a NetworkError when appropriate
|
||||
*/
|
||||
export default function fetchWithNetworkError(
|
||||
...options: Parameters<typeof globalThis.fetch>
|
||||
): ReturnType<typeof globalThis.fetch> {
|
||||
return fetch(...options).catch((err) => {
|
||||
if (err.name === 'AbortError') {
|
||||
throw err
|
||||
} else {
|
||||
throw new NetworkError(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
export function filterNonFailedFiles (files) {
|
||||
const hasError = (file) => 'error' in file && file.error
|
||||
|
||||
return files.filter((file) => !hasError(file))
|
||||
}
|
||||
|
||||
// Don't double-emit upload-started for Golden Retriever-restored files that were already started
|
||||
export function filterFilesToEmitUploadStarted (files) {
|
||||
return files.filter((file) => !file.progress.uploadStarted || !file.isRestored)
|
||||
}
|
||||
14
packages/@uppy/utils/src/fileFilters.ts
Normal file
14
packages/@uppy/utils/src/fileFilters.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import type { UppyFile } from './UppyFile'
|
||||
|
||||
export function filterNonFailedFiles(files: UppyFile[]): UppyFile[] {
|
||||
const hasError = (file: UppyFile): boolean => 'error' in file && !!file.error
|
||||
|
||||
return files.filter((file) => !hasError(file))
|
||||
}
|
||||
|
||||
// Don't double-emit upload-started for Golden Retriever-restored files that were already started
|
||||
export function filterFilesToEmitUploadStarted(files: UppyFile[]): UppyFile[] {
|
||||
return files.filter(
|
||||
(file) => !file.progress?.uploadStarted || !file.isRestored,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
import isDOMElement from './isDOMElement.js'
|
||||
import isDOMElement from './isDOMElement.ts'
|
||||
|
||||
/**
|
||||
* Find one or more DOM elements.
|
||||
*
|
||||
* @param {string|Node} element
|
||||
* @returns {Node[]|null}
|
||||
*/
|
||||
export default function findAllDOMElements (element) {
|
||||
export default function findAllDOMElements(
|
||||
element: string | Node,
|
||||
): Node[] | null {
|
||||
if (typeof element === 'string') {
|
||||
const elements = document.querySelectorAll(element)
|
||||
return elements.length === 0 ? null : Array.from(elements)
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import isDOMElement from './isDOMElement.js'
|
||||
import isDOMElement from './isDOMElement.ts'
|
||||
|
||||
/**
|
||||
* Find a DOM element.
|
||||
*
|
||||
* @param {Node|string} element
|
||||
* @returns {Node|null}
|
||||
*/
|
||||
export default function findDOMElement (element, context = document) {
|
||||
export default function findDOMElement(
|
||||
element: Node | string,
|
||||
context = document,
|
||||
): Node | null {
|
||||
if (typeof element === 'string') {
|
||||
return context.querySelector(element)
|
||||
}
|
||||
|
|
@ -3,22 +3,14 @@ import findIndex from './findIndex.js'
|
|||
|
||||
describe('findIndex', () => {
|
||||
it('should return index of an object in an array, that matches a predicate', () => {
|
||||
const arr = [
|
||||
{ name: 'foo' },
|
||||
{ name: 'bar' },
|
||||
{ name: '123' },
|
||||
]
|
||||
const index = findIndex(arr, item => item.name === 'bar')
|
||||
const arr = [{ name: 'foo' }, { name: 'bar' }, { name: '123' }]
|
||||
const index = findIndex(arr, (item) => item.name === 'bar')
|
||||
expect(index).toEqual(1)
|
||||
})
|
||||
|
||||
it('should return -1 when no object in an array matches a predicate', () => {
|
||||
const arr = [
|
||||
{ name: 'foo' },
|
||||
{ name: 'bar' },
|
||||
{ name: '123' },
|
||||
]
|
||||
const index = findIndex(arr, item => item.name === 'hello')
|
||||
const arr = [{ name: 'foo' }, { name: 'bar' }, { name: '123' }]
|
||||
const index = findIndex(arr, (item) => item.name === 'hello')
|
||||
expect(index).toEqual(-1)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import generateFileID from './generateFileID.js'
|
||||
import generateFileID from './generateFileID.ts'
|
||||
|
||||
describe('generateFileID', () => {
|
||||
it('should take the filename object and produce a lowercase file id made up of uppy- prefix, file name (cleaned up to be lowercase, letters and numbers only), type, relative path (folder) from file.meta.relativePath, size and lastModified date', () => {
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
import getFileType from './getFileType.js'
|
||||
import type { UppyFile } from './UppyFile'
|
||||
import getFileType from './getFileType.ts'
|
||||
|
||||
function encodeCharacter (character) {
|
||||
function encodeCharacter(character: string): string {
|
||||
return character.charCodeAt(0).toString(32)
|
||||
}
|
||||
|
||||
function encodeFilename (name) {
|
||||
function encodeFilename(name: string): string {
|
||||
let suffix = ''
|
||||
return name.replace(/[^A-Z0-9]/ig, (character) => {
|
||||
suffix += `-${encodeCharacter(character)}`
|
||||
return '/'
|
||||
}) + suffix
|
||||
return (
|
||||
name.replace(/[^A-Z0-9]/gi, (character) => {
|
||||
suffix += `-${encodeCharacter(character)}`
|
||||
return '/'
|
||||
}) + suffix
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a file object and turns it into fileID, by converting file.name to lowercase,
|
||||
* removing extra characters and adding type, size and lastModified
|
||||
*
|
||||
* @param {object} file
|
||||
* @returns {string} the fileID
|
||||
*/
|
||||
export default function generateFileID (file) {
|
||||
export default function generateFileID(file: UppyFile): string {
|
||||
// It's tempting to do `[items].filter(Boolean).join('-')` here, but that
|
||||
// is slower! simple string concatenation is fast
|
||||
|
||||
|
|
@ -39,8 +39,8 @@ export default function generateFileID (file) {
|
|||
if (file.data.size !== undefined) {
|
||||
id += `-${file.data.size}`
|
||||
}
|
||||
if (file.data.lastModified !== undefined) {
|
||||
id += `-${file.data.lastModified}`
|
||||
if ((file.data as File).lastModified !== undefined) {
|
||||
id += `-${(file.data as File).lastModified}`
|
||||
}
|
||||
|
||||
return id
|
||||
|
|
@ -48,14 +48,20 @@ export default function generateFileID (file) {
|
|||
|
||||
// If the provider has a stable, unique ID, then we can use that to identify the file.
|
||||
// Then we don't have to generate our own ID, and we can add the same file many times if needed (different path)
|
||||
function hasFileStableId (file) {
|
||||
function hasFileStableId(file: UppyFile): boolean {
|
||||
if (!file.isRemote || !file.remote) return false
|
||||
// These are the providers that it seems like have stable IDs for their files. The other's I haven't checked yet.
|
||||
const stableIdProviders = new Set(['box', 'dropbox', 'drive', 'facebook', 'unsplash'])
|
||||
return stableIdProviders.has(file.remote.provider)
|
||||
const stableIdProviders = new Set([
|
||||
'box',
|
||||
'dropbox',
|
||||
'drive',
|
||||
'facebook',
|
||||
'unsplash',
|
||||
])
|
||||
return stableIdProviders.has(file.remote.provider as any)
|
||||
}
|
||||
|
||||
export function getSafeFileId (file) {
|
||||
export function getSafeFileId(file: UppyFile): string {
|
||||
if (hasFileStableId(file)) return file.id
|
||||
|
||||
const fileType = getFileType(file)
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export default function getBytesRemaining (fileProgress) {
|
||||
return fileProgress.bytesTotal - fileProgress.bytesUploaded
|
||||
}
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import getBytesRemaining from './getBytesRemaining.js'
|
||||
import getBytesRemaining from './getBytesRemaining.ts'
|
||||
|
||||
describe('getBytesRemaining', () => {
|
||||
it('should calculate the bytes remaining given a fileProgress object', () => {
|
||||
const fileProgress = {
|
||||
bytesUploaded: 1024,
|
||||
bytesTotal: 3096,
|
||||
progress: 0,
|
||||
uploadComplete: false,
|
||||
percentage: 0,
|
||||
uploadStarted: 0,
|
||||
}
|
||||
expect(getBytesRemaining(fileProgress)).toEqual(2072)
|
||||
})
|
||||
5
packages/@uppy/utils/src/getBytesRemaining.ts
Normal file
5
packages/@uppy/utils/src/getBytesRemaining.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import type { FileProgress } from './FileProgress'
|
||||
|
||||
export default function getBytesRemaining(fileProgress: FileProgress): number {
|
||||
return fileProgress.bytesTotal - (fileProgress.bytesUploaded as number)
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import webkitGetAsEntryApi from './utils/webkitGetAsEntryApi/index.js'
|
||||
import fallbackApi from './utils/fallbackApi.js'
|
||||
import webkitGetAsEntryApi from './utils/webkitGetAsEntryApi/index.ts'
|
||||
import fallbackApi from './utils/fallbackApi.ts'
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves to the array of dropped files (if a folder is
|
||||
|
|
@ -8,22 +8,29 @@ import fallbackApi from './utils/fallbackApi.js'
|
|||
* Each file has .relativePath prop appended to it (e.g. "/docs/Prague/ticket_from_prague_to_ufa.pdf")
|
||||
* if browser supports it. Otherwise it's undefined.
|
||||
*
|
||||
* @param {DataTransfer} dataTransfer
|
||||
* @param {Function} logDropError - a function that's called every time some
|
||||
* @param dataTransfer
|
||||
* @param options
|
||||
* @param options.logDropError - a function that's called every time some
|
||||
* folder or some file error out (e.g. because of the folder name being too long
|
||||
* on Windows). Notice that resulting promise will always be resolved anyway.
|
||||
*
|
||||
* @returns {Promise} - Array<File>
|
||||
*/
|
||||
export default async function getDroppedFiles (dataTransfer, { logDropError = () => {} } = {}) {
|
||||
export default async function getDroppedFiles(
|
||||
dataTransfer: DataTransfer,
|
||||
options?: {
|
||||
logDropError?: any
|
||||
},
|
||||
): Promise<File[]> {
|
||||
// Get all files from all subdirs. Works (at least) in Chrome, Mozilla, and Safari
|
||||
const logDropError = options?.logDropError ?? Function.prototype
|
||||
try {
|
||||
const accumulator = []
|
||||
for await (const file of webkitGetAsEntryApi(dataTransfer, logDropError)) {
|
||||
accumulator.push(file)
|
||||
accumulator.push(file as File)
|
||||
}
|
||||
return accumulator
|
||||
// Otherwise just return all first-order files
|
||||
// Otherwise just return all first-order files
|
||||
} catch {
|
||||
return fallbackApi(dataTransfer)
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
import toArray from '../../toArray.js'
|
||||
import toArray from '../../toArray.ts'
|
||||
|
||||
// .files fallback, should be implemented in any browser
|
||||
export default function fallbackApi (dataTransfer) {
|
||||
export default function fallbackApi(
|
||||
dataTransfer: DataTransfer,
|
||||
): Promise<File[]> {
|
||||
const files = toArray(dataTransfer.files)
|
||||
return Promise.resolve(files)
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
/**
|
||||
* Recursive function, calls the original callback() when the directory is entirely parsed.
|
||||
*
|
||||
* @param {FileSystemDirectoryReader} directoryReader
|
||||
* @param {Array} oldEntries
|
||||
* @param {Function} logDropError
|
||||
* @param {Function} callback - called with ([ all files and directories in that directoryReader ])
|
||||
*/
|
||||
export default function getFilesAndDirectoriesFromDirectory (directoryReader, oldEntries, logDropError, { onSuccess }) {
|
||||
export default function getFilesAndDirectoriesFromDirectory(
|
||||
directoryReader: FileSystemDirectoryReader,
|
||||
oldEntries: FileSystemEntry[],
|
||||
logDropError: (error?: unknown) => void,
|
||||
{ onSuccess }: { onSuccess: (newEntries: FileSystemEntry[]) => void },
|
||||
): void {
|
||||
directoryReader.readEntries(
|
||||
(entries) => {
|
||||
const newEntries = [...oldEntries, ...entries]
|
||||
|
|
@ -14,9 +14,14 @@ export default function getFilesAndDirectoriesFromDirectory (directoryReader, ol
|
|||
// must be called until it calls the onSuccess with an empty array.
|
||||
if (entries.length) {
|
||||
queueMicrotask(() => {
|
||||
getFilesAndDirectoriesFromDirectory(directoryReader, newEntries, logDropError, { onSuccess })
|
||||
getFilesAndDirectoriesFromDirectory(
|
||||
directoryReader,
|
||||
newEntries,
|
||||
logDropError,
|
||||
{ onSuccess },
|
||||
)
|
||||
})
|
||||
// Done iterating this particular directory
|
||||
// Done iterating this particular directory
|
||||
} else {
|
||||
onSuccess(newEntries)
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
import getFilesAndDirectoriesFromDirectory from './getFilesAndDirectoriesFromDirectory.js'
|
||||
|
||||
/**
|
||||
* Polyfill for the new (experimental) getAsFileSystemHandle API (using the popular webkitGetAsEntry behind the scenes)
|
||||
* so that we can switch to the getAsFileSystemHandle API once it (hopefully) becomes standard
|
||||
*/
|
||||
function getAsFileSystemHandleFromEntry (entry, logDropError) {
|
||||
if (entry == null) return entry
|
||||
return {
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
kind: entry.isFile ? 'file' : entry.isDirectory ? 'directory' : undefined,
|
||||
name: entry.name,
|
||||
getFile () {
|
||||
return new Promise((resolve, reject) => entry.file(resolve, reject))
|
||||
},
|
||||
async* values () {
|
||||
// If the file is a directory.
|
||||
const directoryReader = entry.createReader()
|
||||
const entries = await new Promise(resolve => {
|
||||
getFilesAndDirectoriesFromDirectory(directoryReader, [], logDropError, {
|
||||
onSuccess: (dirEntries) => resolve(dirEntries.map(file => getAsFileSystemHandleFromEntry(file, logDropError))),
|
||||
})
|
||||
})
|
||||
yield* entries
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function* createPromiseToAddFileOrParseDirectory (entry, relativePath, lastResortFile = undefined) {
|
||||
const getNextRelativePath = () => `${relativePath}/${entry.name}`
|
||||
|
||||
// For each dropped item, - make sure it's a file/directory, and start deepening in!
|
||||
if (entry.kind === 'file') {
|
||||
const file = await entry.getFile()
|
||||
if (file != null) {
|
||||
file.relativePath = relativePath ? getNextRelativePath() : null
|
||||
yield file
|
||||
} else if (lastResortFile != null) yield lastResortFile
|
||||
} else if (entry.kind === 'directory') {
|
||||
for await (const handle of entry.values()) {
|
||||
// Recurse on the directory, appending the dir name to the relative path
|
||||
yield* createPromiseToAddFileOrParseDirectory(handle, relativePath ? getNextRelativePath() : entry.name)
|
||||
}
|
||||
} else if (lastResortFile != null) yield lastResortFile
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all files from data transfer, and recursively read any directories.
|
||||
* Note that IE is not supported for drag-drop, because IE doesn't support Data Transfers
|
||||
*
|
||||
* @param {DataTransfer} dataTransfer
|
||||
* @param {*} logDropError on error
|
||||
*/
|
||||
export default async function* getFilesFromDataTransfer (dataTransfer, logDropError) {
|
||||
// Retrieving the dropped items must happen synchronously
|
||||
// otherwise only the first item gets treated and the other ones are garbage collected.
|
||||
// https://github.com/transloadit/uppy/pull/3998
|
||||
const fileSystemHandles = await Promise.all(Array.from(dataTransfer.items, async item => {
|
||||
let fileSystemHandle
|
||||
|
||||
// TODO enable getAsFileSystemHandle API once we can get it working with subdirectories
|
||||
// IMPORTANT: Need to check isSecureContext *before* calling getAsFileSystemHandle
|
||||
// or else Chrome will crash when running in HTTP: https://github.com/transloadit/uppy/issues/4133
|
||||
// if (window.isSecureContext && item.getAsFileSystemHandle != null) entry = await item.getAsFileSystemHandle()
|
||||
|
||||
// `webkitGetAsEntry` exists in all popular browsers (including non-WebKit browsers),
|
||||
// however it may be renamed to getAsEntry() in the future, so you should code defensively, looking for both.
|
||||
// from https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry
|
||||
const getAsEntry = () => (typeof item.getAsEntry === 'function' ? item.getAsEntry() : item.webkitGetAsEntry())
|
||||
// eslint-disable-next-line prefer-const
|
||||
fileSystemHandle ??= getAsFileSystemHandleFromEntry(getAsEntry(), logDropError)
|
||||
|
||||
return {
|
||||
fileSystemHandle,
|
||||
lastResortFile: item.getAsFile(), // can be used as a fallback in case other methods fail
|
||||
}
|
||||
}))
|
||||
|
||||
for (const { lastResortFile, fileSystemHandle } of fileSystemHandles) {
|
||||
// fileSystemHandle and lastResortFile can be null when we drop an url.
|
||||
if (fileSystemHandle != null) {
|
||||
try {
|
||||
yield* createPromiseToAddFileOrParseDirectory(fileSystemHandle, '', lastResortFile)
|
||||
} catch (err) {
|
||||
// Example: If dropping a symbolic link, Chromium will throw:
|
||||
// "DOMException: A requested file or directory could not be found at the time an operation was processed.",
|
||||
// So we will use lastResortFile instead. See https://github.com/transloadit/uppy/issues/3505.
|
||||
if (lastResortFile != null) {
|
||||
yield lastResortFile
|
||||
} else {
|
||||
logDropError(err)
|
||||
}
|
||||
}
|
||||
} else if (lastResortFile != null) yield lastResortFile
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
import getFilesAndDirectoriesFromDirectory from './getFilesAndDirectoriesFromDirectory.ts'
|
||||
|
||||
interface FileSystemFileHandle extends FileSystemHandle {
|
||||
getFile(): Promise<File>
|
||||
}
|
||||
interface FileSystemDirectoryHandle extends FileSystemHandle {
|
||||
values(): AsyncGenerator<
|
||||
FileSystemDirectoryHandle | FileSystemFileHandle,
|
||||
void,
|
||||
undefined
|
||||
>
|
||||
}
|
||||
|
||||
/**
|
||||
* Polyfill for the new (experimental) getAsFileSystemHandle API (using the popular webkitGetAsEntry behind the scenes)
|
||||
* so that we can switch to the getAsFileSystemHandle API once it (hopefully) becomes standard
|
||||
*/
|
||||
function getAsFileSystemHandleFromEntry(
|
||||
entry: FileSystemEntry | null | undefined,
|
||||
logDropError: Parameters<typeof getFilesAndDirectoriesFromDirectory>[2],
|
||||
): FileSystemFileHandle | FileSystemDirectoryHandle | null | undefined {
|
||||
if (entry == null) return entry
|
||||
return {
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
kind: entry.isFile
|
||||
? 'file'
|
||||
: entry.isDirectory
|
||||
? 'directory'
|
||||
: (undefined as never),
|
||||
name: entry.name,
|
||||
getFile(): ReturnType<FileSystemFileHandle['getFile']> {
|
||||
return new Promise((resolve, reject) =>
|
||||
(entry as FileSystemFileEntry).file(resolve, reject),
|
||||
)
|
||||
},
|
||||
async *values(): ReturnType<FileSystemDirectoryHandle['values']> {
|
||||
// If the file is a directory.
|
||||
const directoryReader = (entry as FileSystemDirectoryEntry).createReader()
|
||||
const entries = await new Promise<
|
||||
Array<NonNullable<ReturnType<typeof getAsFileSystemHandleFromEntry>>>
|
||||
>((resolve) => {
|
||||
getFilesAndDirectoriesFromDirectory(directoryReader, [], logDropError, {
|
||||
onSuccess: (dirEntries) =>
|
||||
resolve(
|
||||
dirEntries.map(
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
(file) => getAsFileSystemHandleFromEntry(file, logDropError)!,
|
||||
),
|
||||
),
|
||||
})
|
||||
})
|
||||
yield* entries
|
||||
},
|
||||
isSameEntry: undefined as any as FileSystemDirectoryHandle['isSameEntry'],
|
||||
}
|
||||
}
|
||||
|
||||
async function* createPromiseToAddFileOrParseDirectory(
|
||||
entry: FileSystemFileHandle | FileSystemDirectoryHandle,
|
||||
relativePath: string,
|
||||
lastResortFile: File | null | undefined = undefined,
|
||||
): AsyncGenerator<File> {
|
||||
const getNextRelativePath = (): string => `${relativePath}/${entry.name}`
|
||||
|
||||
// For each dropped item, - make sure it's a file/directory, and start deepening in!
|
||||
if (entry.kind === 'file') {
|
||||
const file = await (entry as FileSystemFileHandle).getFile()
|
||||
if (file != null) {
|
||||
;(file as any).relativePath = relativePath ? getNextRelativePath() : null
|
||||
yield file
|
||||
} else if (lastResortFile != null) yield lastResortFile
|
||||
} else if (entry.kind === 'directory') {
|
||||
for await (const handle of (entry as FileSystemDirectoryHandle).values()) {
|
||||
// Recurse on the directory, appending the dir name to the relative path
|
||||
yield* createPromiseToAddFileOrParseDirectory(
|
||||
handle,
|
||||
relativePath ? getNextRelativePath() : entry.name,
|
||||
)
|
||||
}
|
||||
} else if (lastResortFile != null) yield lastResortFile
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all files from data transfer, and recursively read any directories.
|
||||
* Note that IE is not supported for drag-drop, because IE doesn't support Data Transfers
|
||||
*
|
||||
* @param {DataTransfer} dataTransfer
|
||||
* @param {*} logDropError on error
|
||||
*/
|
||||
export default async function* getFilesFromDataTransfer(
|
||||
dataTransfer: DataTransfer,
|
||||
logDropError: Parameters<typeof getFilesAndDirectoriesFromDirectory>[2],
|
||||
): ReturnType<typeof createPromiseToAddFileOrParseDirectory> {
|
||||
// Retrieving the dropped items must happen synchronously
|
||||
// otherwise only the first item gets treated and the other ones are garbage collected.
|
||||
// https://github.com/transloadit/uppy/pull/3998
|
||||
const fileSystemHandles = await Promise.all(
|
||||
Array.from(dataTransfer.items, async (item) => {
|
||||
let fileSystemHandle:
|
||||
| FileSystemFileHandle
|
||||
| FileSystemDirectoryHandle
|
||||
| null
|
||||
| undefined
|
||||
|
||||
// TODO enable getAsFileSystemHandle API once we can get it working with subdirectories
|
||||
// IMPORTANT: Need to check isSecureContext *before* calling getAsFileSystemHandle
|
||||
// or else Chrome will crash when running in HTTP: https://github.com/transloadit/uppy/issues/4133
|
||||
// if (window.isSecureContext && item.getAsFileSystemHandle != null)
|
||||
// fileSystemHandle = await item.getAsFileSystemHandle()
|
||||
|
||||
// `webkitGetAsEntry` exists in all popular browsers (including non-WebKit browsers),
|
||||
// however it may be renamed to getAsEntry() in the future, so you should code defensively, looking for both.
|
||||
// from https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry
|
||||
const getAsEntry = (): ReturnType<
|
||||
DataTransferItem['webkitGetAsEntry']
|
||||
> =>
|
||||
typeof (item as any).getAsEntry === 'function'
|
||||
? (item as any).getAsEntry()
|
||||
: item.webkitGetAsEntry()
|
||||
// eslint-disable-next-line prefer-const
|
||||
fileSystemHandle ??= getAsFileSystemHandleFromEntry(
|
||||
getAsEntry(),
|
||||
logDropError,
|
||||
)
|
||||
|
||||
return {
|
||||
fileSystemHandle,
|
||||
lastResortFile: item.getAsFile(), // can be used as a fallback in case other methods fail
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
for (const { lastResortFile, fileSystemHandle } of fileSystemHandles) {
|
||||
// fileSystemHandle and lastResortFile can be null when we drop an url.
|
||||
if (fileSystemHandle != null) {
|
||||
try {
|
||||
yield* createPromiseToAddFileOrParseDirectory(
|
||||
fileSystemHandle,
|
||||
'',
|
||||
lastResortFile,
|
||||
)
|
||||
} catch (err) {
|
||||
// Example: If dropping a symbolic link, Chromium will throw:
|
||||
// "DOMException: A requested file or directory could not be found at the time an operation was processed.",
|
||||
// So we will use lastResortFile instead. See https://github.com/transloadit/uppy/issues/3505.
|
||||
if (lastResortFile != null) {
|
||||
yield lastResortFile
|
||||
} else {
|
||||
logDropError(err)
|
||||
}
|
||||
}
|
||||
} else if (lastResortFile != null) yield lastResortFile
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import getETA from './getETA.js'
|
||||
import getETA from './getETA.ts'
|
||||
|
||||
describe('getETA', () => {
|
||||
it('should get the ETA remaining based on a fileProgress object', () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import getSpeed from './getSpeed.js'
|
||||
import getBytesRemaining from './getBytesRemaining.js'
|
||||
import getSpeed from './getSpeed.ts'
|
||||
import getBytesRemaining from './getBytesRemaining.ts'
|
||||
import type { FileProgress } from './FileProgress.ts'
|
||||
|
||||
export default function getETA (fileProgress) {
|
||||
export default function getETA(fileProgress: FileProgress): number {
|
||||
if (!fileProgress.bytesUploaded) return 0
|
||||
|
||||
const uploadSpeed = getSpeed(fileProgress)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import getFileNameAndExtension from './getFileNameAndExtension.js'
|
||||
import getFileNameAndExtension from './getFileNameAndExtension.ts'
|
||||
|
||||
describe('getFileNameAndExtension', () => {
|
||||
it('should return the filename and extension as an array', () => {
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
/**
|
||||
* Takes a full filename string and returns an object {name, extension}
|
||||
*
|
||||
* @param {string} fullFileName
|
||||
* @returns {object} {name, extension}
|
||||
*/
|
||||
export default function getFileNameAndExtension (fullFileName) {
|
||||
export default function getFileNameAndExtension(fullFileName: string): {
|
||||
name: string
|
||||
extension: string | undefined
|
||||
} {
|
||||
const lastDot = fullFileName.lastIndexOf('.')
|
||||
// these count as no extension: "no-dot", "trailing-dot."
|
||||
if (lastDot === -1 || lastDot === fullFileName.length - 1) {
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import getFileNameAndExtension from './getFileNameAndExtension.js'
|
||||
import mimeTypes from './mimeTypes.js'
|
||||
|
||||
export default function getFileType (file) {
|
||||
if (file.type) return file.type
|
||||
|
||||
const fileExtension = file.name ? getFileNameAndExtension(file.name).extension?.toLowerCase() : null
|
||||
if (fileExtension && fileExtension in mimeTypes) {
|
||||
// else, see if we can map extension to a mime type
|
||||
return mimeTypes[fileExtension]
|
||||
}
|
||||
// if all fails, fall back to a generic byte stream type
|
||||
return 'application/octet-stream'
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import getFileType from './getFileType.js'
|
||||
import getFileType from './getFileType.ts'
|
||||
import type { UppyFile } from './UppyFile.ts'
|
||||
|
||||
describe('getFileType', () => {
|
||||
it('should trust the filetype if the file comes from a remote source', () => {
|
||||
|
|
@ -7,7 +8,7 @@ describe('getFileType', () => {
|
|||
isRemote: true,
|
||||
type: 'audio/webm',
|
||||
name: 'foo.webm',
|
||||
}
|
||||
} as any as UppyFile
|
||||
expect(getFileType(file)).toEqual('audio/webm')
|
||||
})
|
||||
|
||||
|
|
@ -16,7 +17,7 @@ describe('getFileType', () => {
|
|||
type: 'audio/webm',
|
||||
name: 'foo.webm',
|
||||
data: 'sdfsdfhq9efbicw',
|
||||
}
|
||||
} as any as UppyFile
|
||||
expect(getFileType(file)).toEqual('audio/webm')
|
||||
})
|
||||
|
||||
|
|
@ -24,24 +25,27 @@ describe('getFileType', () => {
|
|||
const fileMP3 = {
|
||||
name: 'foo.mp3',
|
||||
data: 'sdfsfhfh329fhwihs',
|
||||
}
|
||||
} as any as UppyFile
|
||||
const fileYAML = {
|
||||
name: 'bar.yaml',
|
||||
data: 'sdfsfhfh329fhwihs',
|
||||
}
|
||||
} as any as UppyFile
|
||||
const fileMKV = {
|
||||
name: 'bar.mkv',
|
||||
data: 'sdfsfhfh329fhwihs',
|
||||
}
|
||||
} as any as UppyFile
|
||||
const fileDicom = {
|
||||
name: 'bar.dicom',
|
||||
data: 'sdfsfhfh329fhwihs',
|
||||
}
|
||||
} as any as UppyFile
|
||||
const fileWebp = {
|
||||
name: 'bar.webp',
|
||||
data: 'sdfsfhfh329fhwihs',
|
||||
}
|
||||
const toUpper = (file) => ({ ...file, name: file.name.toUpperCase() })
|
||||
} as any as UppyFile
|
||||
const toUpper = (file: UppyFile) => ({
|
||||
...file,
|
||||
name: file.name.toUpperCase(),
|
||||
})
|
||||
expect(getFileType(fileMP3)).toEqual('audio/mp3')
|
||||
expect(getFileType(toUpper(fileMP3))).toEqual('audio/mp3')
|
||||
expect(getFileType(fileYAML)).toEqual('text/yaml')
|
||||
|
|
@ -58,7 +62,7 @@ describe('getFileType', () => {
|
|||
const file = {
|
||||
name: 'foobar',
|
||||
data: 'sdfsfhfh329fhwihs',
|
||||
}
|
||||
} as any as UppyFile
|
||||
expect(getFileType(file)).toEqual('application/octet-stream')
|
||||
})
|
||||
})
|
||||
17
packages/@uppy/utils/src/getFileType.ts
Normal file
17
packages/@uppy/utils/src/getFileType.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import type { UppyFile } from './UppyFile'
|
||||
import getFileNameAndExtension from './getFileNameAndExtension.ts'
|
||||
import mimeTypes from './mimeTypes.ts'
|
||||
|
||||
export default function getFileType(file: UppyFile): string {
|
||||
if (file.type) return file.type
|
||||
|
||||
const fileExtension = file.name
|
||||
? getFileNameAndExtension(file.name).extension?.toLowerCase()
|
||||
: null
|
||||
if (fileExtension && fileExtension in mimeTypes) {
|
||||
// else, see if we can map extension to a mime type
|
||||
return mimeTypes[fileExtension as keyof typeof mimeTypes]
|
||||
}
|
||||
// if all fails, fall back to a generic byte stream type
|
||||
return 'application/octet-stream'
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import getFileTypeExtension from './getFileTypeExtension.js'
|
||||
import getFileTypeExtension from './getFileTypeExtension.ts'
|
||||
|
||||
describe('getFileTypeExtension', () => {
|
||||
it('should return the filetype based on the specified mime type', () => {
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
const mimeToExtensions = {
|
||||
__proto__: null,
|
||||
'audio/mp3': 'mp3',
|
||||
'audio/mp4': 'mp4',
|
||||
'audio/ogg': 'ogg',
|
||||
|
|
@ -15,11 +16,11 @@ const mimeToExtensions = {
|
|||
'video/webm': 'webm',
|
||||
'video/x-matroska': 'mkv',
|
||||
'video/x-msvideo': 'avi',
|
||||
}
|
||||
} as unknown as Record<string, string>
|
||||
|
||||
export default function getFileTypeExtension (mimeType) {
|
||||
export default function getFileTypeExtension(mimeType: string): string | null {
|
||||
// Remove the ; bit in 'video/x-matroska;codecs=avc1'
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
[mimeType] = mimeType.split(';', 1)
|
||||
;[mimeType] = mimeType.split(';', 1)
|
||||
return mimeToExtensions[mimeType] || null
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import getSocketHost from './getSocketHost.js'
|
||||
|
||||
describe('getSocketHost', () => {
|
||||
it('should get the host from the specified url', () => {
|
||||
expect(
|
||||
getSocketHost('https://foo.bar/a/b/cd?e=fghi&l=k&m=n'),
|
||||
).toEqual('wss://foo.bar/a/b/cd?e=fghi&l=k&m=n')
|
||||
|
||||
expect(
|
||||
getSocketHost('Https://foo.bar/a/b/cd?e=fghi&l=k&m=n'),
|
||||
).toEqual('wss://foo.bar/a/b/cd?e=fghi&l=k&m=n')
|
||||
|
||||
expect(
|
||||
getSocketHost('foo.bar/a/b/cd?e=fghi&l=k&m=n'),
|
||||
).toEqual('wss://foo.bar/a/b/cd?e=fghi&l=k&m=n')
|
||||
|
||||
expect(
|
||||
getSocketHost('http://foo.bar/a/b/cd?e=fghi&l=k&m=n'),
|
||||
).toEqual('ws://foo.bar/a/b/cd?e=fghi&l=k&m=n')
|
||||
})
|
||||
})
|
||||
22
packages/@uppy/utils/src/getSocketHost.test.ts
Normal file
22
packages/@uppy/utils/src/getSocketHost.test.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import getSocketHost from './getSocketHost.ts'
|
||||
|
||||
describe('getSocketHost', () => {
|
||||
it('should get the host from the specified url', () => {
|
||||
expect(getSocketHost('https://foo.bar/a/b/cd?e=fghi&l=k&m=n')).toEqual(
|
||||
'wss://foo.bar/a/b/cd?e=fghi&l=k&m=n',
|
||||
)
|
||||
|
||||
expect(getSocketHost('Https://foo.bar/a/b/cd?e=fghi&l=k&m=n')).toEqual(
|
||||
'wss://foo.bar/a/b/cd?e=fghi&l=k&m=n',
|
||||
)
|
||||
|
||||
expect(getSocketHost('foo.bar/a/b/cd?e=fghi&l=k&m=n')).toEqual(
|
||||
'wss://foo.bar/a/b/cd?e=fghi&l=k&m=n',
|
||||
)
|
||||
|
||||
expect(getSocketHost('http://foo.bar/a/b/cd?e=fghi&l=k&m=n')).toEqual(
|
||||
'ws://foo.bar/a/b/cd?e=fghi&l=k&m=n',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
export default function getSocketHost (url) {
|
||||
export default function getSocketHost(url: string): string {
|
||||
// get the host domain
|
||||
const regex = /^(?:https?:\/\/|\/\/)?(?:[^@\n]+@)?(?:www\.)?([^\n]+)/i
|
||||
const host = regex.exec(url)[1]
|
||||
const host = regex.exec(url)?.[1]
|
||||
const socketProtocol = /^http:\/\//i.test(url) ? 'ws' : 'wss'
|
||||
|
||||
return `${socketProtocol}://${host}`
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import getSpeed from './getSpeed.js'
|
||||
import getSpeed from './getSpeed.ts'
|
||||
|
||||
describe('getSpeed', () => {
|
||||
it('should calculate the speed given a fileProgress object', () => {
|
||||
|
|
@ -7,7 +7,11 @@ describe('getSpeed', () => {
|
|||
const date5SecondsAgo = new Date(dateNow.getTime() - 5 * 1000)
|
||||
const fileProgress = {
|
||||
bytesUploaded: 1024,
|
||||
uploadStarted: date5SecondsAgo,
|
||||
uploadStarted: date5SecondsAgo.getTime(),
|
||||
progress: 0,
|
||||
uploadComplete: false,
|
||||
percentage: 0,
|
||||
bytesTotal: 0,
|
||||
}
|
||||
expect(Math.round(getSpeed(fileProgress))).toEqual(Math.round(205))
|
||||
})
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
export default function getSpeed (fileProgress) {
|
||||
import type { FileProgress } from './FileProgress'
|
||||
|
||||
export default function getSpeed(fileProgress: FileProgress): number {
|
||||
if (!fileProgress.bytesUploaded) return 0
|
||||
|
||||
const timeElapsed = Date.now() - fileProgress.uploadStarted
|
||||
|
|
@ -1,11 +1,8 @@
|
|||
/**
|
||||
* Get the declared text direction for an element.
|
||||
*
|
||||
* @param {Node} element
|
||||
* @returns {string|undefined}
|
||||
*/
|
||||
|
||||
function getTextDirection (element) {
|
||||
function getTextDirection(element?: HTMLElement): string | undefined {
|
||||
// There is another way to determine text direction using getComputedStyle(), as done here:
|
||||
// https://github.com/pencil-js/text-direction/blob/2a235ce95089b3185acec3b51313cbba921b3811/text-direction.js
|
||||
//
|
||||
|
|
@ -14,7 +11,7 @@ function getTextDirection (element) {
|
|||
// bidirectional CSS style sheets work.
|
||||
while (element && !element.dir) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
element = element.parentNode
|
||||
element = element.parentNode as HTMLElement
|
||||
}
|
||||
return element?.dir
|
||||
}
|
||||
|
|
@ -1,17 +1,14 @@
|
|||
/**
|
||||
* Adds zero to strings shorter than two characters.
|
||||
*
|
||||
* @param {number} number
|
||||
* @returns {string}
|
||||
*/
|
||||
function pad (number) {
|
||||
function pad(number: number): string {
|
||||
return number < 10 ? `0${number}` : number.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a timestamp in the format of `hours:minutes:seconds`
|
||||
*/
|
||||
export default function getTimeStamp () {
|
||||
export default function getTimeStamp(): string {
|
||||
const date = new Date()
|
||||
const hours = pad(date.getHours())
|
||||
const minutes = pad(date.getMinutes())
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export default function has (object, key) {
|
||||
return Object.prototype.hasOwnProperty.call(object, key)
|
||||
}
|
||||
6
packages/@uppy/utils/src/hasProperty.ts
Normal file
6
packages/@uppy/utils/src/hasProperty.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export default function has(
|
||||
object: Parameters<typeof Object.hasOwn>[0],
|
||||
key: Parameters<typeof Object.hasOwn>[1],
|
||||
): ReturnType<typeof Object.hasOwn> {
|
||||
return Object.prototype.hasOwnProperty.call(object, key)
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
/**
|
||||
* Check if an object is a DOM element. Duck-typing based on `nodeType`.
|
||||
*
|
||||
* @param {*} obj
|
||||
*/
|
||||
export default function isDOMElement (obj) {
|
||||
return obj?.nodeType === Node.ELEMENT_NODE
|
||||
}
|
||||
8
packages/@uppy/utils/src/isDOMElement.ts
Normal file
8
packages/@uppy/utils/src/isDOMElement.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Check if an object is a DOM element. Duck-typing based on `nodeType`.
|
||||
*/
|
||||
export default function isDOMElement(obj: unknown): obj is Element {
|
||||
if (typeof obj !== 'object' || obj === null) return false
|
||||
if (!('nodeType' in obj)) return false
|
||||
return obj.nodeType === Node.ELEMENT_NODE
|
||||
}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
/**
|
||||
* Checks if the browser supports Drag & Drop (not supported on mobile devices, for example).
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export default function isDragDropSupported () {
|
||||
export default function isDragDropSupported(): boolean {
|
||||
const div = document.body
|
||||
|
||||
if (!('draggable' in div) || !('ondragstart' in div && 'ondrop' in div)) {
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
/**
|
||||
* Checks if current device reports itself as “mobile”.
|
||||
* Very simple, not very reliable.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export default function isMobileDevice () {
|
||||
if (typeof window !== 'undefined'
|
||||
&& window.navigator
|
||||
&& window.navigator.userAgent
|
||||
&& window.navigator.userAgent.match(/Mobi/)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import isMobileDevice from './isMobileDevice.js'
|
||||
|
||||
let fakeUserAgent = null
|
||||
|
||||
Object.defineProperty(globalThis.navigator, 'userAgent', {
|
||||
get () {
|
||||
return fakeUserAgent
|
||||
},
|
||||
})
|
||||
|
||||
function setUserAgent (userAgent) {
|
||||
fakeUserAgent = userAgent
|
||||
}
|
||||
|
||||
describe('isMobileDevice', () => {
|
||||
it('should return true if the specified user agent is mobile', () => {
|
||||
setUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1 Mobile/15E148 Safari/604.1')
|
||||
expect(isMobileDevice()).toEqual(true)
|
||||
setUserAgent('Mozilla/5.0 (Linux; Android 7.0; SM-G570M Build/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/192.0.0.34.85;]')
|
||||
expect(isMobileDevice()).toEqual(true)
|
||||
})
|
||||
|
||||
it('should return false if the user agent is not mobile', () => {
|
||||
setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/605.1.15 (KHTML, like Gecko)')
|
||||
expect(isMobileDevice()).toEqual(false)
|
||||
setUserAgent('Mozilla/5.0 (SMART-TV; Linux; Tizen 2.4.0) AppleWebkit/538.1 (KHTML, like Gecko) SamsungBrowser/1.1 TV Safari/538.1')
|
||||
expect(isMobileDevice()).toEqual(false)
|
||||
})
|
||||
})
|
||||
38
packages/@uppy/utils/src/isMobileDevice.test.ts
Normal file
38
packages/@uppy/utils/src/isMobileDevice.test.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import isMobileDevice from './isMobileDevice.ts'
|
||||
|
||||
let fakeUserAgent: string | null = null
|
||||
|
||||
Object.defineProperty(globalThis.navigator, 'userAgent', {
|
||||
get() {
|
||||
return fakeUserAgent
|
||||
},
|
||||
})
|
||||
|
||||
function setUserAgent(userAgent: string | null) {
|
||||
fakeUserAgent = userAgent
|
||||
}
|
||||
|
||||
describe('isMobileDevice', () => {
|
||||
it('should return true if the specified user agent is mobile', () => {
|
||||
setUserAgent(
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1 Mobile/15E148 Safari/604.1',
|
||||
)
|
||||
expect(isMobileDevice()).toEqual(true)
|
||||
setUserAgent(
|
||||
'Mozilla/5.0 (Linux; Android 7.0; SM-G570M Build/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/192.0.0.34.85;]',
|
||||
)
|
||||
expect(isMobileDevice()).toEqual(true)
|
||||
})
|
||||
|
||||
it('should return false if the user agent is not mobile', () => {
|
||||
setUserAgent(
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/605.1.15 (KHTML, like Gecko)',
|
||||
)
|
||||
expect(isMobileDevice()).toEqual(false)
|
||||
setUserAgent(
|
||||
'Mozilla/5.0 (SMART-TV; Linux; Tizen 2.4.0) AppleWebkit/538.1 (KHTML, like Gecko) SamsungBrowser/1.1 TV Safari/538.1',
|
||||
)
|
||||
expect(isMobileDevice()).toEqual(false)
|
||||
})
|
||||
})
|
||||
15
packages/@uppy/utils/src/isMobileDevice.ts
Normal file
15
packages/@uppy/utils/src/isMobileDevice.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Checks if current device reports itself as “mobile”.
|
||||
* Very simple, not very reliable.
|
||||
*/
|
||||
export default function isMobileDevice(): boolean {
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
window.navigator &&
|
||||
window.navigator.userAgent &&
|
||||
window.navigator.userAgent.match(/Mobi/)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import isNetworkError from './isNetworkError.js'
|
||||
import isNetworkError from './isNetworkError.ts'
|
||||
|
||||
describe('isNetworkError', () => {
|
||||
it('should return true if the specified xhr object contains a network error', () => {
|
||||
|
|
@ -7,25 +7,25 @@ describe('isNetworkError', () => {
|
|||
readyState: 4,
|
||||
responseText: '',
|
||||
status: 0,
|
||||
}
|
||||
} as any as XMLHttpRequest
|
||||
|
||||
const xhrNetworkError2Mock = {
|
||||
readyState: 2,
|
||||
responseText: '',
|
||||
status: 300,
|
||||
}
|
||||
} as any as XMLHttpRequest
|
||||
|
||||
const xhrRegularErrorMock = {
|
||||
readyState: 4,
|
||||
responseText: 'Failed',
|
||||
status: 400,
|
||||
}
|
||||
} as any as XMLHttpRequest
|
||||
|
||||
const xhrNetworkSuccessMock = {
|
||||
readyState: 4,
|
||||
responseText: 'Success',
|
||||
status: 200,
|
||||
}
|
||||
} as any as XMLHttpRequest
|
||||
|
||||
expect(isNetworkError(xhrNetworkErrorMock)).toEqual(true)
|
||||
expect(isNetworkError(xhrNetworkError2Mock)).toEqual(true)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
function isNetworkError (xhr) {
|
||||
function isNetworkError(xhr?: XMLHttpRequest): boolean {
|
||||
if (!xhr) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import isObjectURL from './isObjectURL.js'
|
||||
import isObjectURL from './isObjectURL.ts'
|
||||
|
||||
describe('isObjectURL', () => {
|
||||
it('should return true if the specified url is an object url', () => {
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
/**
|
||||
* Check if a URL string is an object URL from `URL.createObjectURL`.
|
||||
*
|
||||
* @param {string} url
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export default function isObjectURL (url) {
|
||||
export default function isObjectURL(url: string): boolean {
|
||||
return url.startsWith('blob:')
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import isPreviewSupported from './isPreviewSupported.js'
|
||||
|
||||
describe('isPreviewSupported', () => {
|
||||
it('should return true for any filetypes that browsers can preview', () => {
|
||||
const supported = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml', 'image/bmp', 'image/jpg', 'image/webp', 'image/avif']
|
||||
supported.forEach(ext => {
|
||||
expect(isPreviewSupported(ext)).toEqual(true)
|
||||
})
|
||||
expect(isPreviewSupported('foo')).toEqual(false)
|
||||
})
|
||||
})
|
||||
22
packages/@uppy/utils/src/isPreviewSupported.test.ts
Normal file
22
packages/@uppy/utils/src/isPreviewSupported.test.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import isPreviewSupported from './isPreviewSupported.ts'
|
||||
|
||||
describe('isPreviewSupported', () => {
|
||||
it('should return true for any filetypes that browsers can preview', () => {
|
||||
const supported = [
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
'image/png',
|
||||
'image/svg',
|
||||
'image/svg+xml',
|
||||
'image/bmp',
|
||||
'image/jpg',
|
||||
'image/webp',
|
||||
'image/avif',
|
||||
]
|
||||
supported.forEach((ext) => {
|
||||
expect(isPreviewSupported(ext)).toEqual(true)
|
||||
})
|
||||
expect(isPreviewSupported('foo')).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
export default function isPreviewSupported (fileType) {
|
||||
export default function isPreviewSupported(fileType: string): boolean {
|
||||
if (!fileType) return false
|
||||
// list of images that browsers can preview
|
||||
return /^[^/]+\/(jpe?g|gif|png|svg|svg\+xml|bmp|webp|avif)$/.test(fileType)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import isTouchDevice from './isTouchDevice.js'
|
||||
import isTouchDevice from './isTouchDevice.ts'
|
||||
|
||||
describe('isTouchDevice', () => {
|
||||
const RealTouchStart = globalThis.window.ontouchstart
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
export default function isTouchDevice () {
|
||||
export default function isTouchDevice(): boolean {
|
||||
return 'ontouchstart' in window || 'maxTouchPoints' in navigator
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
// ___Why not add the mime-types package?
|
||||
// It's 19.7kB gzipped, and we only need mime types for well-known extensions (for file previews).
|
||||
// ___Where to take new extensions from?
|
||||
// https://github.com/jshttp/mime-db/blob/master/db.json
|
||||
|
||||
export default {
|
||||
md: 'text/markdown',
|
||||
markdown: 'text/markdown',
|
||||
mp4: 'video/mp4',
|
||||
mp3: 'audio/mp3',
|
||||
svg: 'image/svg+xml',
|
||||
jpg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
webp: 'image/webp',
|
||||
gif: 'image/gif',
|
||||
heic: 'image/heic',
|
||||
heif: 'image/heif',
|
||||
yaml: 'text/yaml',
|
||||
yml: 'text/yaml',
|
||||
csv: 'text/csv',
|
||||
tsv: 'text/tab-separated-values',
|
||||
tab: 'text/tab-separated-values',
|
||||
avi: 'video/x-msvideo',
|
||||
mks: 'video/x-matroska',
|
||||
mkv: 'video/x-matroska',
|
||||
mov: 'video/quicktime',
|
||||
dicom: 'application/dicom',
|
||||
doc: 'application/msword',
|
||||
docm: 'application/vnd.ms-word.document.macroenabled.12',
|
||||
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
dot: 'application/msword',
|
||||
dotm: 'application/vnd.ms-word.template.macroenabled.12',
|
||||
dotx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
|
||||
xla: 'application/vnd.ms-excel',
|
||||
xlam: 'application/vnd.ms-excel.addin.macroenabled.12',
|
||||
xlc: 'application/vnd.ms-excel',
|
||||
xlf: 'application/x-xliff+xml',
|
||||
xlm: 'application/vnd.ms-excel',
|
||||
xls: 'application/vnd.ms-excel',
|
||||
xlsb: 'application/vnd.ms-excel.sheet.binary.macroenabled.12',
|
||||
xlsm: 'application/vnd.ms-excel.sheet.macroenabled.12',
|
||||
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
xlt: 'application/vnd.ms-excel',
|
||||
xltm: 'application/vnd.ms-excel.template.macroenabled.12',
|
||||
xltx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
|
||||
xlw: 'application/vnd.ms-excel',
|
||||
txt: 'text/plain',
|
||||
text: 'text/plain',
|
||||
conf: 'text/plain',
|
||||
log: 'text/plain',
|
||||
pdf: 'application/pdf',
|
||||
zip: 'application/zip',
|
||||
'7z': 'application/x-7z-compressed',
|
||||
rar: 'application/x-rar-compressed',
|
||||
tar: 'application/x-tar',
|
||||
gz: 'application/gzip',
|
||||
dmg: 'application/x-apple-diskimage',
|
||||
}
|
||||
59
packages/@uppy/utils/src/mimeTypes.ts
Normal file
59
packages/@uppy/utils/src/mimeTypes.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// ___Why not add the mime-types package?
|
||||
// It's 19.7kB gzipped, and we only need mime types for well-known extensions (for file previews).
|
||||
// ___Where to take new extensions from?
|
||||
// https://github.com/jshttp/mime-db/blob/master/db.json
|
||||
|
||||
export default {
|
||||
__proto__: null as never,
|
||||
md: 'text/markdown' as const,
|
||||
markdown: 'text/markdown' as const,
|
||||
mp4: 'video/mp4' as const,
|
||||
mp3: 'audio/mp3' as const,
|
||||
svg: 'image/svg+xml' as const,
|
||||
jpg: 'image/jpeg' as const,
|
||||
png: 'image/png' as const,
|
||||
webp: 'image/webp' as const,
|
||||
gif: 'image/gif' as const,
|
||||
heic: 'image/heic' as const,
|
||||
heif: 'image/heif' as const,
|
||||
yaml: 'text/yaml' as const,
|
||||
yml: 'text/yaml' as const,
|
||||
csv: 'text/csv' as const,
|
||||
tsv: 'text/tab-separated-values' as const,
|
||||
tab: 'text/tab-separated-values' as const,
|
||||
avi: 'video/x-msvideo' as const,
|
||||
mks: 'video/x-matroska' as const,
|
||||
mkv: 'video/x-matroska' as const,
|
||||
mov: 'video/quicktime' as const,
|
||||
dicom: 'application/dicom' as const,
|
||||
doc: 'application/msword' as const,
|
||||
docm: 'application/vnd.ms-word.document.macroenabled.12' as const,
|
||||
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' as const,
|
||||
dot: 'application/msword' as const,
|
||||
dotm: 'application/vnd.ms-word.template.macroenabled.12' as const,
|
||||
dotx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' as const,
|
||||
xla: 'application/vnd.ms-excel' as const,
|
||||
xlam: 'application/vnd.ms-excel.addin.macroenabled.12' as const,
|
||||
xlc: 'application/vnd.ms-excel' as const,
|
||||
xlf: 'application/x-xliff+xml' as const,
|
||||
xlm: 'application/vnd.ms-excel' as const,
|
||||
xls: 'application/vnd.ms-excel' as const,
|
||||
xlsb: 'application/vnd.ms-excel.sheet.binary.macroenabled.12' as const,
|
||||
xlsm: 'application/vnd.ms-excel.sheet.macroenabled.12' as const,
|
||||
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' as const,
|
||||
xlt: 'application/vnd.ms-excel' as const,
|
||||
xltm: 'application/vnd.ms-excel.template.macroenabled.12' as const,
|
||||
xltx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' as const,
|
||||
xlw: 'application/vnd.ms-excel' as const,
|
||||
txt: 'text/plain' as const,
|
||||
text: 'text/plain' as const,
|
||||
conf: 'text/plain' as const,
|
||||
log: 'text/plain' as const,
|
||||
pdf: 'application/pdf' as const,
|
||||
zip: 'application/zip' as const,
|
||||
'7z': 'application/x-7z-compressed' as const,
|
||||
rar: 'application/x-rar-compressed' as const,
|
||||
tar: 'application/x-tar' as const,
|
||||
gz: 'application/gzip' as const,
|
||||
dmg: 'application/x-apple-diskimage' as const,
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import secondsToTime from './secondsToTime.js'
|
||||
|
||||
export default function prettyETA (seconds) {
|
||||
const time = secondsToTime(seconds)
|
||||
|
||||
// Only display hours and minutes if they are greater than 0 but always
|
||||
// display minutes if hours is being displayed
|
||||
// Display a leading zero if the there is a preceding unit: 1m 05s, but 5s
|
||||
const hoursStr = time.hours === 0 ? '' : `${time.hours}h`
|
||||
const minutesStr = time.minutes === 0 ? '' : `${time.hours === 0 ? time.minutes : ` ${time.minutes.toString(10).padStart(2, '0')}`}m`
|
||||
const secondsStr = time.hours !== 0 ? '' : `${time.minutes === 0 ? time.seconds : ` ${time.seconds.toString(10).padStart(2, '0')}`}s`
|
||||
|
||||
return `${hoursStr}${minutesStr}${secondsStr}`
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import prettyETA from './prettyETA.js'
|
||||
import prettyETA from './prettyETA.ts'
|
||||
|
||||
describe('prettyETA', () => {
|
||||
it('should convert the specified number of seconds to a pretty ETA', () => {
|
||||
28
packages/@uppy/utils/src/prettyETA.ts
Normal file
28
packages/@uppy/utils/src/prettyETA.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import secondsToTime from './secondsToTime.ts'
|
||||
|
||||
export default function prettyETA(seconds: number): string {
|
||||
const time = secondsToTime(seconds)
|
||||
|
||||
// Only display hours and minutes if they are greater than 0 but always
|
||||
// display minutes if hours is being displayed
|
||||
// Display a leading zero if the there is a preceding unit: 1m 05s, but 5s
|
||||
const hoursStr = time.hours === 0 ? '' : `${time.hours}h`
|
||||
const minutesStr =
|
||||
time.minutes === 0
|
||||
? ''
|
||||
: `${
|
||||
time.hours === 0
|
||||
? time.minutes
|
||||
: ` ${time.minutes.toString(10).padStart(2, '0')}`
|
||||
}m`
|
||||
const secondsStr =
|
||||
time.hours !== 0
|
||||
? ''
|
||||
: `${
|
||||
time.minutes === 0
|
||||
? time.seconds
|
||||
: ` ${time.seconds.toString(10).padStart(2, '0')}`
|
||||
}s`
|
||||
|
||||
return `${hoursStr}${minutesStr}${secondsStr}`
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import getFileNameAndExtension from './getFileNameAndExtension.js'
|
||||
|
||||
export default function remoteFileObjToLocal (file) {
|
||||
return {
|
||||
...file,
|
||||
type: file.mimeType,
|
||||
extension: file.name ? getFileNameAndExtension(file.name).extension : null,
|
||||
}
|
||||
}
|
||||
19
packages/@uppy/utils/src/remoteFileObjToLocal.ts
Normal file
19
packages/@uppy/utils/src/remoteFileObjToLocal.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import getFileNameAndExtension from './getFileNameAndExtension.ts'
|
||||
|
||||
interface ObjectWithMIMEAndName {
|
||||
name?: string
|
||||
mimeType: unknown
|
||||
}
|
||||
|
||||
export default function remoteFileObjToLocal<T extends ObjectWithMIMEAndName>(
|
||||
file: T,
|
||||
): T & {
|
||||
type: T['mimeType']
|
||||
extension: string | undefined | null
|
||||
} {
|
||||
return {
|
||||
...file,
|
||||
type: file.mimeType,
|
||||
extension: file.name ? getFileNameAndExtension(file.name).extension : null,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import secondsToTime from './secondsToTime.js'
|
||||
import secondsToTime from './secondsToTime.ts'
|
||||
|
||||
describe('secondsToTime', () => {
|
||||
it('converts seconds to an { hours, minutes, seconds } object', () => {
|
||||
|
|
@ -1,4 +1,10 @@
|
|||
export default function secondsToTime (rawSeconds) {
|
||||
interface Time {
|
||||
hours: number
|
||||
minutes: number
|
||||
seconds: number
|
||||
}
|
||||
|
||||
export default function secondsToTime(rawSeconds: number): Time {
|
||||
const hours = Math.floor(rawSeconds / 3600) % 24
|
||||
const minutes = Math.floor(rawSeconds / 60) % 60
|
||||
const seconds = Math.floor(rawSeconds % 60)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import toArray from './toArray.js'
|
||||
import toArray from './toArray.ts'
|
||||
|
||||
describe('toArray', () => {
|
||||
it('should convert a array-like object into an array', () => {
|
||||
|
|
@ -12,12 +12,6 @@ describe('toArray', () => {
|
|||
length: 5,
|
||||
}
|
||||
|
||||
expect(toArray(obj)).toEqual([
|
||||
'zero',
|
||||
'one',
|
||||
'two',
|
||||
'three',
|
||||
'four',
|
||||
])
|
||||
expect(toArray(obj)).toEqual(['zero', 'one', 'two', 'three', 'four'])
|
||||
})
|
||||
})
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import truncateString from './truncateString.js'
|
||||
import truncateString from './truncateString.ts'
|
||||
|
||||
describe('truncateString', () => {
|
||||
it('should truncate the string to the length', () => {
|
||||
|
|
@ -1,19 +1,19 @@
|
|||
/**
|
||||
* Truncates a string to the given number of chars (maxLength) by inserting '...' in the middle of that string.
|
||||
* Partially taken from https://stackoverflow.com/a/5723274/3192470.
|
||||
*
|
||||
* @param {string} string - string to be truncated
|
||||
* @param {number} maxLength - maximum size of the resulting string
|
||||
* @returns {string}
|
||||
*/
|
||||
const separator = '...'
|
||||
export default function truncateString (string, maxLength) {
|
||||
export default function truncateString(
|
||||
string: string,
|
||||
maxLength: number,
|
||||
): string {
|
||||
// Return the empty string if maxLength is zero
|
||||
if (maxLength === 0) return ''
|
||||
// Return original string if it's already shorter than maxLength
|
||||
if (string.length <= maxLength) return string
|
||||
// Return truncated substring appended of the ellipsis char if string can't be meaningfully truncated
|
||||
if (maxLength <= separator.length + 1) return `${string.slice(0, maxLength - 1)}…`
|
||||
if (maxLength <= separator.length + 1)
|
||||
return `${string.slice(0, maxLength - 1)}…`
|
||||
|
||||
const charsToShow = maxLength - separator.length
|
||||
const frontChars = Math.ceil(charsToShow / 2)
|
||||
12
packages/@uppy/utils/tsconfig.build.json
Normal file
12
packages/@uppy/utils/tsconfig.build.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.shared",
|
||||
"compilerOptions": {
|
||||
"outDir": "./lib",
|
||||
"rootDir": "./src",
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["./src/**/*.*"],
|
||||
"exclude": ["./src/**/*.test.ts"],
|
||||
"references": []
|
||||
}
|
||||
10
packages/@uppy/utils/tsconfig.json
Normal file
10
packages/@uppy/utils/tsconfig.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.shared",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"emitDeclarationOnly": false,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/*.ts"],
|
||||
"references": []
|
||||
}
|
||||
|
|
@ -8197,6 +8197,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/lodash@npm:^4.14.199":
|
||||
version: 4.14.199
|
||||
resolution: "@types/lodash@npm:4.14.199"
|
||||
checksum: e68d1fcbbfce953ed87b296a628573f62939227bcda0c934954e862b421e8a34c5e71cad6fea27b9980567909e6a4698f09025692958e36d64ea9ed99ec6fb2e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/mdast@npm:^3.0.0":
|
||||
version: 3.0.10
|
||||
resolution: "@types/mdast@npm:3.0.10"
|
||||
|
|
@ -9805,6 +9812,7 @@ __metadata:
|
|||
version: 0.0.0-use.local
|
||||
resolution: "@uppy/utils@workspace:packages/@uppy/utils"
|
||||
dependencies:
|
||||
"@types/lodash": ^4.14.199
|
||||
lodash: ^4.17.21
|
||||
preact: ^10.5.13
|
||||
vitest: ^0.34.5
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue