diff --git a/src/core/utils.ts b/src/core/utils.ts index c1578f4..2c2c863 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -1,5 +1,9 @@ import { VTransmitFile } from "../classes/VTransmitFile"; +export function is_function(x: any): x is Function { + return typeof x == "function"; +} + let idCounter = 0; export function uniqueId(prefix: string): string { return prefix + ++idCounter; diff --git a/src/upload-adapters/xhr.ts b/src/upload-adapters/xhr.ts index da794bd..afe09b9 100644 --- a/src/upload-adapters/xhr.ts +++ b/src/upload-adapters/xhr.ts @@ -5,8 +5,11 @@ import { VTransmitEvents as Events, UploadStatuses as Statuses, ErrType, + is_function, } from "../core/utils"; +export type StaticOrDynamic = T | ((files: VTransmitFile[]) => T); + /** * Responsibilities: * - send and manage upload via transport @@ -19,9 +22,11 @@ import { */ export type XHRUploadOptions = { /** - * A string representing the URL to send the request to. + * A string representing the URL to send the request to + * or a function called with an array of files for the upload + * that returns a string url. */ - url: string; + url: StaticOrDynamic; /** * The HTTP method to use, such as "GET", "POST", "PUT", "DELETE", etc. * Ignored for non-HTTP(S) URLs. @@ -30,14 +35,14 @@ export type XHRUploadOptions = { * // default => "post" * ``` */ - method?: string; + method?: StaticOrDynamic; /** * The XMLHttpRequest.withCredentials property is a Boolean that indicates * whether or not cross-site Access-Control requests should be made using * credentials such as cookies, authorization headers or TLS client * certificates. Setting withCredentials has no effect on same-site requests. */ - withCredentials?: boolean; + withCredentials?: StaticOrDynamic; /** * The XMLHttpRequest.timeout property is an unsigned long representing the * number of milliseconds a request can take before automatically being @@ -46,17 +51,17 @@ export type XHRUploadOptions = { * a document environment or it will throw an InvalidAccessError exception. * When a timeout happens, a timeout event is fired. */ - timeout?: number; + timeout?: StaticOrDynamic; /** * The name of the file param that gets transferred. */ - paramName?: string; + paramName?: StaticOrDynamic; /** * An object of additional parameters to transfer to the server. * This is the same as adding hidden input fields in the form element. */ - params?: { [key: string]: string }; - headers?: { [key: string]: string }; + params?: StaticOrDynamic>; + headers?: StaticOrDynamic>; /** * The XMLHttpRequest.responseType property is an enumerated value that * returns the type of response. It also lets the author change the response @@ -71,7 +76,7 @@ export type XHRUploadOptions = { * null. Also, setting responseType for synchronous requests will throw an * InvalidAccessError exception. */ - responseType?: XMLHttpRequestResponseType; + responseType?: StaticOrDynamic; /** * responseParseFunc is a function that given an XMLHttpRequest * returns a response object. Allows for custom response parsing. @@ -92,18 +97,19 @@ let group_id = 0; export class XHRUploadAdapter implements UploaderInterface { public context: VTransmitUploadContext; - public url: string; - public method: string; - public withCredentials: boolean; - public timeout: number; - public paramName: string; - public params: { [key: string]: string }; - public headers: { [key: string]: string }; - public responseType: XMLHttpRequestResponseType; + public url: StaticOrDynamic; + public method: StaticOrDynamic; + public withCredentials: StaticOrDynamic; + public timeout: StaticOrDynamic; + public paramName: StaticOrDynamic; + public params: StaticOrDynamic>; + public headers: StaticOrDynamic>; + public responseType: StaticOrDynamic; public errUploadError: (xhr: XMLHttpRequest) => string; public errUploadTimeout: (xhr: XMLHttpRequest) => string; public renameFile: (name: string) => string; public responseParseFunc?: (xhr: XMLHttpRequest) => T; + private uploadGroups: { [key: number]: UploadGroup } = Object.create(null); constructor(context: VTransmitUploadContext, options: XHRUploadOptions) { @@ -128,8 +134,13 @@ export class XHRUploadAdapter implements UploaderInterface { renameFile = (name: string) => name, } = options; - this.context = context; + if (!url) { + throw new TypeError( + `The VueTransmit XHRUploadAdapter requires a 'url' parameter. Supply a string or a function returning a string.` + ); + } + this.context = context; this.url = url; this.method = method; this.withCredentials = withCredentials; @@ -160,6 +171,25 @@ export class XHRUploadAdapter implements UploaderInterface { const xhr = new XMLHttpRequest(); const updateProgress = this.handleUploadProgress(files); const id = group_id++; + const url = is_function(this.url) ? this.url(files) : this.url; + const method = is_function(this.method) + ? this.method(files) + : this.method; + const timeout = is_function(this.timeout) + ? this.timeout(files) + : this.timeout; + const withCredentials = is_function(this.withCredentials) + ? this.withCredentials(files) + : this.withCredentials; + const responseType = is_function(this.responseType) + ? this.responseType(files) + : this.responseType; + const params = is_function(this.params) + ? this.params(files) + : this.params; + const headers = is_function(this.headers) + ? this.headers(files) + : this.headers; this.uploadGroups[id] = { id, xhr, files }; @@ -168,12 +198,12 @@ export class XHRUploadAdapter implements UploaderInterface { file.startProgress(); } - xhr.open(this.method, this.url, true); + xhr.open(method, url, true); // Setting the timeout after open because of IE11 issue: // @link https://gitlab.com/meno/dropzone/issues/8 - xhr.timeout = this.timeout; - xhr.withCredentials = Boolean(this.withCredentials); - xhr.responseType = this.responseType; + xhr.timeout = timeout; + xhr.withCredentials = withCredentials; + xhr.responseType = responseType; xhr.addEventListener("error", () => { this.rmGroup(id); @@ -198,7 +228,7 @@ export class XHRUploadAdapter implements UploaderInterface { }, }); }); - xhr.addEventListener("load", _ => { + xhr.addEventListener("load", () => { if ( files[0].status === Statuses.Canceled || xhr.readyState !== XMLHttpRequest.DONE @@ -253,27 +283,26 @@ export class XHRUploadAdapter implements UploaderInterface { }); }); - // Use null proto obj for the following 'for in' loop without hasOwnProperty check - const headers = Object.assign(Object.create(null), this.headers); - for (const headerName in headers) { + for (const headerName of Object.keys(headers)) { if (headers[headerName]) { xhr.setRequestHeader(headerName, headers[headerName]); } } const formData = new FormData(); - for (const key in this.params) { - formData.append(key, this.params[key]); + for (const key of Object.keys(params)) { + formData.append(key, params[key]); } for (const file of files) { this.context.emit(Events.Sending, file, xhr, formData); } + if (this.context.props.uploadMultiple) { this.context.emit(Events.SendingMultiple, files, xhr, formData); } - for (let i = 0; i < files.length; i++) { + for (let i = 0, len = files.length; i < len; i++) { formData.append( this.getParamName(i), files[i].nativeFile,