UNPKG

11.2 kBJavaScriptView Raw
1/* global FormData self Blob File */
2/* eslint-disable no-inner-declarations */
3
4if (typeof Blob === 'function' && (typeof FormData === 'undefined' || !FormData.prototype.keys)) {
5 const global = typeof window === 'object'
6 ? window
7 : typeof self === 'object' ? self : this
8
9 // keep a reference to native implementation
10 const _FormData = global.FormData
11
12 // To be monkey patched
13 const _send = global.XMLHttpRequest && global.XMLHttpRequest.prototype.send
14 const _fetch = global.Request && global.fetch
15 const _sendBeacon = global.navigator && global.navigator.sendBeacon
16
17 // Unable to patch Request constructor correctly
18 // const _Request = global.Request
19 // only way is to use ES6 class extend
20 // https://github.com/babel/babel/issues/1966
21
22 const stringTag = global.Symbol && Symbol.toStringTag
23
24 // Add missing stringTags to blob and files
25 if (stringTag) {
26 if (!Blob.prototype[stringTag]) {
27 Blob.prototype[stringTag] = 'Blob'
28 }
29
30 if ('File' in global && !File.prototype[stringTag]) {
31 File.prototype[stringTag] = 'File'
32 }
33 }
34
35 // Fix so you can construct your own File
36 try {
37 new File([], '') // eslint-disable-line
38 } catch (a) {
39 global.File = function File (b, d, c) {
40 const blob = new Blob(b, c)
41 const t = c && void 0 !== c.lastModified ? new Date(c.lastModified) : new Date()
42
43 Object.defineProperties(blob, {
44 name: {
45 value: d
46 },
47 lastModifiedDate: {
48 value: t
49 },
50 lastModified: {
51 value: +t
52 },
53 toString: {
54 value () {
55 return '[object File]'
56 }
57 }
58 })
59
60 if (stringTag) {
61 Object.defineProperty(blob, stringTag, {
62 value: 'File'
63 })
64 }
65
66 return blob
67 }
68 }
69
70 function normalizeValue ([value, filename]) {
71 if (value instanceof Blob) {
72 // Should always returns a new File instance
73 // console.assert(fd.get(x) !== fd.get(x))
74 value = new File([value], filename, {
75 type: value.type,
76 lastModified: value.lastModified
77 })
78 }
79
80 return value
81 }
82
83 function ensureArgs (args, expected) {
84 if (args.length < expected) {
85 throw new TypeError(`${expected} argument required, but only ${args.length} present.`)
86 }
87 }
88
89 function normalizeArgs (name, value, filename) {
90 return value instanceof Blob
91 // normalize name and filename if adding an attachment
92 ? [String(name), value, filename !== undefined
93 ? filename + '' // Cast filename to string if 3th arg isn't undefined
94 : typeof value.name === 'string' // if name prop exist
95 ? value.name // Use File.name
96 : 'blob'] // otherwise fallback to Blob
97
98 // If no attachment, just cast the args to strings
99 : [String(name), String(value)]
100 }
101
102 // normalize linefeeds for textareas
103 // https://html.spec.whatwg.org/multipage/form-elements.html#textarea-line-break-normalisation-transformation
104 function normalizeLinefeeds (value) {
105 return value.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n')
106 }
107
108 function each (arr, cb) {
109 for (let i = 0; i < arr.length; i++) {
110 cb(arr[i])
111 }
112 }
113
114 /**
115 * @implements {Iterable}
116 */
117 class FormDataPolyfill {
118 /**
119 * FormData class
120 *
121 * @param {HTMLElement=} form
122 */
123 constructor (form) {
124 this._data = Object.create(null)
125
126 if (!form) return this
127
128 const self = this
129
130 each(form.elements, elm => {
131 if (!elm.name || elm.disabled || elm.type === 'submit' || elm.type === 'button') return
132
133 if (elm.type === 'file') {
134 const files = elm.files && elm.files.length
135 ? elm.files
136 : [new File([], '', { type: 'application/octet-stream' })] // #78
137
138 each(files, file => {
139 self.append(elm.name, file)
140 })
141 } else if (elm.type === 'select-multiple' || elm.type === 'select-one') {
142 each(elm.options, opt => {
143 !opt.disabled && opt.selected && self.append(elm.name, opt.value)
144 })
145 } else if (elm.type === 'checkbox' || elm.type === 'radio') {
146 if (elm.checked) self.append(elm.name, elm.value)
147 } else {
148 const value = elm.type === 'textarea' ? normalizeLinefeeds(elm.value) : elm.value
149 self.append(elm.name, value)
150 }
151 })
152 }
153
154 /**
155 * Append a field
156 *
157 * @param {string} name field name
158 * @param {string|Blob|File} value string / blob / file
159 * @param {string=} filename filename to use with blob
160 * @return {undefined}
161 */
162 append (name, value, filename) {
163 ensureArgs(arguments, 2)
164 ;[name, value, filename] = normalizeArgs.apply(null, arguments)
165 const map = this._data
166
167 if (!map[name]) map[name] = []
168
169 map[name].push([value, filename])
170 }
171
172 /**
173 * Delete all fields values given name
174 *
175 * @param {string} name Field name
176 * @return {undefined}
177 */
178 delete (name) {
179 ensureArgs(arguments, 1)
180 delete this._data[String(name)]
181 }
182
183 /**
184 * Iterate over all fields as [name, value]
185 *
186 * @return {Iterator}
187 */
188 * entries () {
189 const map = this._data
190
191 for (let name in map) {
192 for (let value of map[name]) {
193 yield [name, normalizeValue(value)]
194 }
195 }
196 }
197
198 /**
199 * Iterate over all fields
200 *
201 * @param {Function} callback Executed for each item with parameters (value, name, thisArg)
202 * @param {Object=} thisArg `this` context for callback function
203 * @return {undefined}
204 */
205 forEach (callback, thisArg) {
206 ensureArgs(arguments, 1)
207 for (let [name, value] of this) {
208 callback.call(thisArg, value, name, this)
209 }
210 }
211
212 /**
213 * Return first field value given name
214 * or null if non existen
215 *
216 * @param {string} name Field name
217 * @return {string|File|null} value Fields value
218 */
219 get (name) {
220 ensureArgs(arguments, 1)
221 const map = this._data
222 name = String(name)
223 return map[name] ? normalizeValue(map[name][0]) : null
224 }
225
226 /**
227 * Return all fields values given name
228 *
229 * @param {string} name Fields name
230 * @return {Array} [{String|File}]
231 */
232 getAll (name) {
233 ensureArgs(arguments, 1)
234 return (this._data[String(name)] || []).map(normalizeValue)
235 }
236
237 /**
238 * Check for field name existence
239 *
240 * @param {string} name Field name
241 * @return {boolean}
242 */
243 has (name) {
244 ensureArgs(arguments, 1)
245 return String(name) in this._data
246 }
247
248 /**
249 * Iterate over all fields name
250 *
251 * @return {Iterator}
252 */
253 * keys () {
254 for (let [name] of this) {
255 yield name
256 }
257 }
258
259 /**
260 * Overwrite all values given name
261 *
262 * @param {string} name Filed name
263 * @param {string} value Field value
264 * @param {string=} filename Filename (optional)
265 * @return {undefined}
266 */
267 set (name, value, filename) {
268 ensureArgs(arguments, 2)
269 const args = normalizeArgs.apply(null, arguments)
270 this._data[args[0]] = [[args[1], args[2]]]
271 }
272
273 /**
274 * Iterate over all fields
275 *
276 * @return {Iterator}
277 */
278 * values () {
279 for (let [, value] of this) {
280 yield value
281 }
282 }
283
284 /**
285 * Return a native (perhaps degraded) FormData with only a `append` method
286 * Can throw if it's not supported
287 *
288 * @return {FormData}
289 */
290 ['_asNative'] () {
291 const fd = new _FormData()
292
293 for (let [name, value] of this) {
294 fd.append(name, value)
295 }
296
297 return fd
298 }
299
300 /**
301 * [_blob description]
302 *
303 * @return {Blob} [description]
304 */
305 ['_blob'] () {
306 const boundary = '----formdata-polyfill-' + Math.random()
307 const chunks = []
308
309 for (let [name, value] of this) {
310 chunks.push(`--${boundary}\r\n`)
311
312 if (value instanceof Blob) {
313 chunks.push(
314 `Content-Disposition: form-data; name="${name}"; filename="${value.name}"\r\n`,
315 `Content-Type: ${value.type || 'application/octet-stream'}\r\n\r\n`,
316 value,
317 '\r\n'
318 )
319 } else {
320 chunks.push(
321 `Content-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`
322 )
323 }
324 }
325
326 chunks.push(`--${boundary}--`)
327
328 return new Blob(chunks, {
329 type: 'multipart/form-data; boundary=' + boundary
330 })
331 }
332
333 /**
334 * The class itself is iterable
335 * alias for formdata.entries()
336 *
337 * @return {Iterator}
338 */
339 [Symbol.iterator] () {
340 return this.entries()
341 }
342
343 /**
344 * Create the default string description.
345 *
346 * @return {string} [object FormData]
347 */
348 toString () {
349 return '[object FormData]'
350 }
351 }
352
353 if (stringTag) {
354 /**
355 * Create the default string description.
356 * It is accessed internally by the Object.prototype.toString().
357 */
358 FormDataPolyfill.prototype[stringTag] = 'FormData'
359 }
360
361 // Patch xhr's send method to call _blob transparently
362 if (_send) {
363 const setRequestHeader = global.XMLHttpRequest.prototype.setRequestHeader
364
365 /**
366 * @param {string} name
367 * @param {string} value
368 * @returns {undefined}
369 * @see https://xhr.spec.whatwg.org/#dom-xmlhttprequest-setrequestheader
370 */
371 global.XMLHttpRequest.prototype.setRequestHeader = function (name, value) {
372 if (name.toLowerCase() === 'content-type') this._hasContentType = true
373 return setRequestHeader.call(this, name, value)
374 }
375
376 /**
377 * @param {ArrayBuffer|ArrayBufferView|Blob|Document|FormData|string=} data
378 * @return {undefined}
379 * @see https://xhr.spec.whatwg.org/#the-send()-method
380 */
381 global.XMLHttpRequest.prototype.send = function (data) {
382 // I would check if Content-Type isn't already set
383 // But xhr lacks getRequestHeaders functionallity
384 // https://github.com/jimmywarting/FormData/issues/44
385 if (data instanceof FormDataPolyfill) {
386 const blob = data['_blob']()
387 // Check if Content-Type is already set
388 // https://github.com/jimmywarting/FormData/issues/86
389 if (!this._hasContentType) this.setRequestHeader('Content-Type', blob.type)
390 _send.call(this, blob)
391 } else {
392 _send.call(this, data)
393 }
394 }
395 }
396
397 // Patch fetch's function to call _blob transparently
398 if (_fetch) {
399 const _fetch = global.fetch
400
401 global.fetch = function (input, init) {
402 if (init && init.body && init.body instanceof FormDataPolyfill) {
403 init.body = init.body['_blob']()
404 }
405
406 return _fetch.call(this, input, init)
407 }
408 }
409
410 // Patch navigator.sendBeacon to use native FormData
411 if (_sendBeacon) {
412 global.navigator.sendBeacon = function (url, data) {
413 if (data instanceof FormDataPolyfill) {
414 data = data['_asNative']()
415 }
416 return _sendBeacon.call(this, url, data)
417 }
418 }
419
420 global['FormData'] = FormDataPolyfill
421}