UNPKG

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