38 // Fix so you can construct your own File |
40 // Fix so you can construct your own File |
39 try { |
41 try { |
40 new File([], '') // eslint-disable-line |
42 new File([], '') // eslint-disable-line |
41 } catch (a) { |
43 } catch (a) { |
42 global.File = function File (b, d, c) { |
44 global.File = function File (b, d, c) { |
43 const blob = new Blob(b, c) |
45 const blob = new Blob(b, c || {}) |
44 const t = c && void 0 !== c.lastModified ? new Date(c.lastModified) : new Date() |
46 const t = c && void 0 !== c.lastModified ? new Date(c.lastModified) : new Date() |
45 |
47 |
46 Object.defineProperties(blob, { |
48 Object.defineProperties(blob, { |
47 name: { |
49 name: { |
48 value: d |
50 value: d |
49 }, |
|
50 lastModifiedDate: { |
|
51 value: t |
|
52 }, |
51 }, |
53 lastModified: { |
52 lastModified: { |
54 value: +t |
53 value: +t |
55 }, |
54 }, |
56 toString: { |
55 toString: { |
68 |
67 |
69 return blob |
68 return blob |
70 } |
69 } |
71 } |
70 } |
72 |
71 |
73 function normalizeValue ([name, value, filename]) { |
|
74 if (value instanceof Blob) { |
|
75 // Should always returns a new File instance |
|
76 // console.assert(fd.get(x) !== fd.get(x)) |
|
77 value = new File([value], filename, { |
|
78 type: value.type, |
|
79 lastModified: value.lastModified |
|
80 }) |
|
81 } |
|
82 |
|
83 return [name, value] |
|
84 } |
|
85 |
|
86 function ensureArgs (args, expected) { |
72 function ensureArgs (args, expected) { |
87 if (args.length < expected) { |
73 if (args.length < expected) { |
88 throw new TypeError(`${expected} argument required, but only ${args.length} present.`) |
74 throw new TypeError(`${expected} argument required, but only ${args.length} present.`) |
89 } |
75 } |
90 } |
76 } |
91 |
77 |
|
78 /** |
|
79 * @param {string} name |
|
80 * @param {string | undefined} filename |
|
81 * @returns {[string, File|string]} |
|
82 */ |
92 function normalizeArgs (name, value, filename) { |
83 function normalizeArgs (name, value, filename) { |
93 return value instanceof Blob |
84 if (value instanceof Blob) { |
94 // normalize name and filename if adding an attachment |
85 filename = filename !== undefined |
95 ? [String(name), value, filename !== undefined |
86 ? String(filename + '') |
96 ? filename + '' // Cast filename to string if 3th arg isn't undefined |
87 : typeof value.name === 'string' |
97 : typeof value.name === 'string' // if name prop exist |
88 ? value.name |
98 ? value.name // Use File.name |
89 : 'blob' |
99 : 'blob'] // otherwise fallback to Blob |
90 |
100 |
91 if (value.name !== filename || Object.prototype.toString.call(value) === '[object Blob]') { |
101 // If no attachment, just cast the args to strings |
92 value = new File([value], filename) |
102 : [String(name), String(value)] |
93 } |
103 } |
94 return [String(name), value] |
104 |
95 } |
105 // normalize linefeeds for textareas |
96 return [String(name), String(value)] |
|
97 } |
|
98 |
|
99 // normalize line feeds for textarea |
106 // https://html.spec.whatwg.org/multipage/form-elements.html#textarea-line-break-normalisation-transformation |
100 // https://html.spec.whatwg.org/multipage/form-elements.html#textarea-line-break-normalisation-transformation |
107 function normalizeLinefeeds (value) { |
101 function normalizeLinefeeds (value) { |
108 return value.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n') |
102 return value.replace(/\r?\n|\r/g, '\r\n') |
109 } |
103 } |
110 |
104 |
|
105 /** |
|
106 * @template T |
|
107 * @param {ArrayLike<T>} arr |
|
108 * @param {{ (elm: T): void; }} cb |
|
109 */ |
111 function each (arr, cb) { |
110 function each (arr, cb) { |
112 for (let i = 0; i < arr.length; i++) { |
111 for (let i = 0; i < arr.length; i++) { |
113 cb(arr[i]) |
112 cb(arr[i]) |
114 } |
113 } |
115 } |
114 } |
|
115 |
|
116 const escape = str => str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22') |
116 |
117 |
117 /** |
118 /** |
118 * @implements {Iterable} |
119 * @implements {Iterable} |
119 */ |
120 */ |
120 class FormDataPolyfill { |
121 class FormDataPolyfill { |
121 /** |
122 /** |
122 * FormData class |
123 * FormData class |
123 * |
124 * |
124 * @param {HTMLElement=} form |
125 * @param {HTMLFormElement=} form |
125 */ |
126 */ |
126 constructor (form) { |
127 constructor (form) { |
|
128 /** @type {[string, string|File][]} */ |
127 this._data = [] |
129 this._data = [] |
128 |
130 |
129 const self = this |
131 const self = this |
130 |
132 form && each(form.elements, (/** @type {HTMLInputElement} */ elm) => { |
131 form && each(form.elements, elm => { |
|
132 if ( |
133 if ( |
133 !elm.name || |
134 !elm.name || |
134 elm.disabled || |
135 elm.disabled || |
135 elm.type === 'submit' || |
136 elm.type === 'submit' || |
136 elm.type === 'button' || |
137 elm.type === 'button' || |
194 * |
195 * |
195 * @return {Iterator} |
196 * @return {Iterator} |
196 */ |
197 */ |
197 * entries () { |
198 * entries () { |
198 for (var i = 0; i < this._data.length; i++) { |
199 for (var i = 0; i < this._data.length; i++) { |
199 yield normalizeValue(this._data[i]) |
200 yield this._data[i] |
200 } |
201 } |
201 } |
202 } |
202 |
203 |
203 /** |
204 /** |
204 * Iterate over all fields |
205 * Iterate over all fields |
205 * |
206 * |
206 * @param {Function} callback Executed for each item with parameters (value, name, thisArg) |
207 * @param {Function} callback Executed for each item with parameters (value, name, thisArg) |
207 * @param {Object=} thisArg `this` context for callback function |
208 * @param {Object=} thisArg `this` context for callback function |
208 * @return {undefined} |
|
209 */ |
209 */ |
210 forEach (callback, thisArg) { |
210 forEach (callback, thisArg) { |
211 ensureArgs(arguments, 1) |
211 ensureArgs(arguments, 1) |
212 for (const [name, value] of this) { |
212 for (const [name, value] of this) { |
213 callback.call(thisArg, value, name, this) |
213 callback.call(thisArg, value, name, this) |
214 } |
214 } |
215 } |
215 } |
216 |
216 |
217 /** |
217 /** |
218 * Return first field value given name |
218 * Return first field value given name |
219 * or null if non existen |
219 * or null if non existent |
220 * |
220 * |
221 * @param {string} name Field name |
221 * @param {string} name Field name |
222 * @return {string|File|null} value Fields value |
222 * @return {string|File|null} value Fields value |
223 */ |
223 */ |
224 get (name) { |
224 get (name) { |
225 ensureArgs(arguments, 1) |
225 ensureArgs(arguments, 1) |
226 const entries = this._data |
226 const entries = this._data |
227 name = String(name) |
227 name = String(name) |
228 for (let i = 0; i < entries.length; i++) { |
228 for (let i = 0; i < entries.length; i++) { |
229 if (entries[i][0] === name) { |
229 if (entries[i][0] === name) { |
230 return normalizeValue(entries[i])[1] |
230 return entries[i][1] |
231 } |
231 } |
232 } |
232 } |
233 return null |
233 return null |
234 } |
234 } |
235 |
235 |
282 * Overwrite all values given name |
282 * Overwrite all values given name |
283 * |
283 * |
284 * @param {string} name Filed name |
284 * @param {string} name Filed name |
285 * @param {string} value Field value |
285 * @param {string} value Field value |
286 * @param {string=} filename Filename (optional) |
286 * @param {string=} filename Filename (optional) |
287 * @return {undefined} |
|
288 */ |
287 */ |
289 set (name, value, filename) { |
288 set (name, value, filename) { |
290 ensureArgs(arguments, 2) |
289 ensureArgs(arguments, 2) |
291 name = String(name) |
290 name = String(name) |
|
291 /** @type {[string, string|File][]} */ |
292 const result = [] |
292 const result = [] |
293 const args = normalizeArgs(name, value, filename) |
293 const args = normalizeArgs(name, value, filename) |
294 let replace = true |
294 let replace = true |
295 |
295 |
296 // - replace the first occurrence with same name |
296 // - replace the first occurrence with same name |
297 // - discards the remaning with same name |
297 // - discards the remaining with same name |
298 // - while keeping the same order items where added |
298 // - while keeping the same order items where added |
299 each(this._data, data => { |
299 each(this._data, data => { |
300 data[0] === name |
300 data[0] === name |
301 ? replace && (replace = !result.push(args)) |
301 ? replace && (replace = !result.push(args)) |
302 : result.push(data) |
302 : result.push(data) |
338 * [_blob description] |
338 * [_blob description] |
339 * |
339 * |
340 * @return {Blob} [description] |
340 * @return {Blob} [description] |
341 */ |
341 */ |
342 ['_blob'] () { |
342 ['_blob'] () { |
343 const boundary = '----formdata-polyfill-' + Math.random() |
343 const boundary = '----formdata-polyfill-' + Math.random(), |
344 const chunks = [] |
344 chunks = [], |
345 |
345 p = `--${boundary}\r\nContent-Disposition: form-data; name="` |
346 for (const [name, value] of this) { |
346 this.forEach((value, name) => typeof value == 'string' |
347 chunks.push(`--${boundary}\r\n`) |
347 ? chunks.push(p + escape(normalizeLinefeeds(name)) + `"\r\n\r\n${normalizeLinefeeds(value)}\r\n`) |
348 |
348 : chunks.push(p + escape(normalizeLinefeeds(name)) + `"; filename="${escape(value.name)}"\r\nContent-Type: ${value.type||"application/octet-stream"}\r\n\r\n`, value, `\r\n`)) |
349 if (value instanceof Blob) { |
349 chunks.push(`--${boundary}--`) |
350 chunks.push( |
350 return new Blob(chunks, { |
351 `Content-Disposition: form-data; name="${name}"; filename="${value.name}"\r\n` + |
351 type: "multipart/form-data; boundary=" + boundary |
352 `Content-Type: ${value.type || 'application/octet-stream'}\r\n\r\n`, |
352 }) |
353 value, |
|
354 '\r\n' |
|
355 ) |
|
356 } else { |
|
357 chunks.push( |
|
358 `Content-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n` |
|
359 ) |
|
360 } |
|
361 } |
|
362 |
|
363 chunks.push(`--${boundary}--`) |
|
364 |
|
365 return new Blob(chunks, { |
|
366 type: 'multipart/form-data; boundary=' + boundary |
|
367 }) |
|
368 } |
353 } |
369 |
354 |
370 /** |
355 /** |
371 * The class itself is iterable |
356 * The class itself is iterable |
372 * alias for formdata.entries() |
357 * alias for formdata.entries() |
373 * |
358 * |
374 * @return {Iterator} |
359 * @return {Iterator} |
375 */ |
360 */ |
376 [Symbol.iterator] () { |
361 [Symbol.iterator] () { |
377 return this.entries() |
362 return this.entries() |
378 } |
363 } |
379 |
364 |