UNPKG

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