390 lines
		
	
	
	
		
			9.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			390 lines
		
	
	
	
		
			9.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| if (typeof FormData === 'undefined' || !FormData.prototype.keys) {
 | |
|   const global = typeof window === 'object'
 | |
|     ? window : typeof self === 'object'
 | |
|     ? self : this
 | |
| 
 | |
|   // keep a reference to native implementation
 | |
|   const _FormData = global.FormData
 | |
| 
 | |
|   // To be monkey patched
 | |
|   const _send = global.XMLHttpRequest && global.XMLHttpRequest.prototype.send
 | |
|   const _fetch = global.Request && global.fetch
 | |
| 
 | |
|   // Unable to patch Request constructor correctly
 | |
|   // const _Request = global.Request
 | |
|   // only way is to use ES6 class extend
 | |
|   // https://github.com/babel/babel/issues/1966
 | |
| 
 | |
|   const stringTag = global.Symbol && Symbol.toStringTag
 | |
|   const map = new WeakMap
 | |
|   const wm = o => map.get(o)
 | |
|   const arrayFrom = Array.from || (obj => [].slice.call(obj))
 | |
| 
 | |
|   // Add missing stringTags to blob and files
 | |
|   if (stringTag) {
 | |
|     if (!Blob.prototype[stringTag]) {
 | |
|       Blob.prototype[stringTag] = 'Blob'
 | |
|     }
 | |
| 
 | |
|     if ('File' in global && !File.prototype[stringTag]) {
 | |
|       File.prototype[stringTag] = 'File'
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Fix so you can construct your own File
 | |
|   try {
 | |
|     new File([], '')
 | |
|   } catch (a) {
 | |
|     global.File = function(b, d, c) {
 | |
|       const blob = new Blob(b, c)
 | |
|       const t = c && void 0 !== c.lastModified ? new Date(c.lastModified) : new Date
 | |
| 
 | |
|       Object.defineProperties(blob, {
 | |
|         name: {
 | |
|           value: d
 | |
|         },
 | |
|         lastModifiedDate: {
 | |
|           value: t
 | |
|         },
 | |
|         lastModified: {
 | |
|           value: +t
 | |
|         },
 | |
|         toString: {
 | |
|           value() {
 | |
|             return '[object File]'
 | |
|           }
 | |
|         }
 | |
|       })
 | |
| 
 | |
|       if (stringTag) {
 | |
|         Object.defineProperty(blob, stringTag, {
 | |
|           value: 'File'
 | |
|         })
 | |
|       }
 | |
| 
 | |
|       return blob
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function normalizeValue([value, filename]) {
 | |
|     if (value instanceof Blob)
 | |
|       // Should always returns a new File instance
 | |
|       // console.assert(fd.get(x) !== fd.get(x))
 | |
|       value = new File([value], filename, {
 | |
|         type: value.type,
 | |
|         lastModified: value.lastModified
 | |
|       })
 | |
| 
 | |
|     return value
 | |
|   }
 | |
| 
 | |
|   function stringify(name) {
 | |
|     if (!arguments.length)
 | |
|       throw new TypeError('1 argument required, but only 0 present.')
 | |
| 
 | |
|     return [name + '']
 | |
|   }
 | |
| 
 | |
|   function normalizeArgs(name, value, filename) {
 | |
|     if (arguments.length < 2)
 | |
|       throw new TypeError(
 | |
|         `2 arguments required, but only ${arguments.length} present.`
 | |
|       )
 | |
| 
 | |
|     return value instanceof Blob
 | |
|       // normalize name and filename if adding an attachment
 | |
|       ? [name + '', value, filename !== undefined
 | |
|         ? filename + '' // Cast filename to string if 3th arg isn't undefined
 | |
|         : typeof value.name === 'string' // if name prop exist
 | |
|           ? value.name // Use File.name
 | |
|           : 'blob'] // otherwise fallback to Blob
 | |
| 
 | |
|       // If no attachment, just cast the args to strings
 | |
|       : [name + '', value + '']
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @implements {Iterable}
 | |
|    */
 | |
|   class FormDataPolyfill {
 | |
| 
 | |
|     /**
 | |
|      * FormData class
 | |
|      *
 | |
|      * @param {HTMLElement=} form
 | |
|      */
 | |
|     constructor(form) {
 | |
|       map.set(this, Object.create(null))
 | |
| 
 | |
|       if (!form)
 | |
|         return this
 | |
| 
 | |
|       for (let elm of arrayFrom(form.elements)) {
 | |
|         if (!elm.name || elm.disabled) continue
 | |
| 
 | |
|         if (elm.type === 'file')
 | |
|           for (let file of arrayFrom(elm.files || []))
 | |
|             this.append(elm.name, file)
 | |
|         else if (elm.type === 'select-multiple' || elm.type === 'select-one')
 | |
|           for (let opt of arrayFrom(elm.options))
 | |
|             !opt.disabled && opt.selected && this.append(elm.name, opt.value)
 | |
|         else if (elm.type === 'checkbox' || elm.type === 'radio') {
 | |
|           if (elm.checked) this.append(elm.name, elm.value)
 | |
|         } else
 | |
|           this.append(elm.name, elm.value)
 | |
|       }
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * Append a field
 | |
|      *
 | |
|      * @param   {String}           name      field name
 | |
|      * @param   {String|Blob|File} value     string / blob / file
 | |
|      * @param   {String=}          filename  filename to use with blob
 | |
|      * @return  {Undefined}
 | |
|      */
 | |
|     append(name, value, filename) {
 | |
|       const map = wm(this)
 | |
| 
 | |
|       if (!map[name])
 | |
|         map[name] = []
 | |
| 
 | |
|       map[name].push([value, filename])
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * Delete all fields values given name
 | |
|      *
 | |
|      * @param   {String}  name  Field name
 | |
|      * @return  {Undefined}
 | |
|      */
 | |
|     delete(name) {
 | |
|       delete wm(this)[name]
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * Iterate over all fields as [name, value]
 | |
|      *
 | |
|      * @return {Iterator}
 | |
|      */
 | |
|     *entries() {
 | |
|       const map = wm(this)
 | |
| 
 | |
|       for (let name in map)
 | |
|         for (let value of map[name])
 | |
|           yield [name, normalizeValue(value)]
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Iterate over all fields
 | |
|      *
 | |
|      * @param   {Function}  callback  Executed for each item with parameters (value, name, thisArg)
 | |
|      * @param   {Object=}   thisArg   `this` context for callback function
 | |
|      * @return  {Undefined}
 | |
|      */
 | |
|     forEach(callback, thisArg) {
 | |
|       for (let [name, value] of this)
 | |
|         callback.call(thisArg, value, name, this)
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * Return first field value given name
 | |
|      * or null if non existen
 | |
|      *
 | |
|      * @param   {String}  name      Field name
 | |
|      * @return  {String|File|null}  value Fields value
 | |
|      */
 | |
|     get(name) {
 | |
|       const map = wm(this)
 | |
|       return map[name] ? normalizeValue(map[name][0]) : null
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * Return all fields values given name
 | |
|      *
 | |
|      * @param   {String}  name  Fields name
 | |
|      * @return  {Array}         [{String|File}]
 | |
|      */
 | |
|     getAll(name) {
 | |
|       return (wm(this)[name] || []).map(normalizeValue)
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * Check for field name existence
 | |
|      *
 | |
|      * @param   {String}   name  Field name
 | |
|      * @return  {boolean}
 | |
|      */
 | |
|     has(name) {
 | |
|       return name in wm(this)
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * Iterate over all fields name
 | |
|      *
 | |
|      * @return {Iterator}
 | |
|      */
 | |
|     *keys() {
 | |
|       for (let [name] of this)
 | |
|         yield name
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * Overwrite all values given name
 | |
|      *
 | |
|      * @param   {String}    name      Filed name
 | |
|      * @param   {String}    value     Field value
 | |
|      * @param   {String=}   filename  Filename (optional)
 | |
|      * @return  {Undefined}
 | |
|      */
 | |
|     set(name, value, filename) {
 | |
|       wm(this)[name] = [[value, filename]]
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * Iterate over all fields
 | |
|      *
 | |
|      * @return {Iterator}
 | |
|      */
 | |
|     *values() {
 | |
|       for (let [name, value] of this)
 | |
|         yield value
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * Return a native (perhaps degraded) FormData with only a `append` method
 | |
|      * Can throw if it's not supported
 | |
|      *
 | |
|      * @return {FormData}
 | |
|      */
 | |
|     ['_asNative']() {
 | |
|       const fd = new _FormData
 | |
| 
 | |
|       for (let [name, value] of this)
 | |
|         fd.append(name, value)
 | |
| 
 | |
|       return fd
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * [_blob description]
 | |
|      *
 | |
|      * @return {Blob} [description]
 | |
|      */
 | |
|     ['_blob']() {
 | |
|       const boundary = '----formdata-polyfill-' + Math.random()
 | |
|       const chunks = []
 | |
| 
 | |
|       for (let [name, value] of this) {
 | |
|         chunks.push(`--${boundary}\r\n`)
 | |
| 
 | |
|         if (value instanceof Blob) {
 | |
|           chunks.push(
 | |
|             `Content-Disposition: form-data; name="${name}"; filename="${value.name}"\r\n`,
 | |
|             `Content-Type: ${value.type || 'application/octet-stream'}\r\n\r\n`,
 | |
|             value,
 | |
|             '\r\n'
 | |
|           )
 | |
|         } else {
 | |
|           chunks.push(
 | |
|             `Content-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`
 | |
|           )
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       chunks.push(`--${boundary}--`)
 | |
| 
 | |
|       return new Blob(chunks, {type: 'multipart/form-data; boundary=' + boundary})
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * The class itself is iterable
 | |
|      * alias for formdata.entries()
 | |
|      *
 | |
|      * @return  {Iterator}
 | |
|      */
 | |
|     [Symbol.iterator]() {
 | |
|       return this.entries()
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * Create the default string description.
 | |
|      *
 | |
|      * @return  {String} [object FormData]
 | |
|      */
 | |
|     toString() {
 | |
|       return '[object FormData]'
 | |
|     }
 | |
|   }
 | |
| 
 | |
| 
 | |
|   if (stringTag) {
 | |
|     /**
 | |
|      * Create the default string description.
 | |
|      * It is accessed internally by the Object.prototype.toString().
 | |
|      *
 | |
|      * @return {String} FormData
 | |
|      */
 | |
|     FormDataPolyfill.prototype[stringTag] = 'FormData'
 | |
|   }
 | |
| 
 | |
|   const decorations = [
 | |
|     ['append', normalizeArgs],
 | |
|     ['delete', stringify],
 | |
|     ['get',    stringify],
 | |
|     ['getAll', stringify],
 | |
|     ['has',    stringify],
 | |
|     ['set',    normalizeArgs]
 | |
|   ]
 | |
| 
 | |
|   decorations.forEach(arr => {
 | |
|     const orig = FormDataPolyfill.prototype[arr[0]]
 | |
|     FormDataPolyfill.prototype[arr[0]] = function() {
 | |
|       return orig.apply(this, arr[1].apply(this, arrayFrom(arguments)))
 | |
|     }
 | |
|   })
 | |
| 
 | |
|   // Patch xhr's send method to call _blob transparently
 | |
|   if (_send) {
 | |
|       XMLHttpRequest.prototype.send = function(data) {
 | |
|       // I would check if Content-Type isn't already set
 | |
|       // But xhr lacks getRequestHeaders functionallity
 | |
|       // https://github.com/jimmywarting/FormData/issues/44
 | |
|       if (data instanceof FormDataPolyfill) {
 | |
|         const blob = data['_blob']()
 | |
|         this.setRequestHeader('Content-Type', blob.type)
 | |
|         _send.call(this, blob)
 | |
|       } else {
 | |
|         _send.call(this, data)
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Patch fetch's function to call _blob transparently
 | |
|   if (_fetch) {
 | |
|     const _fetch = global.fetch
 | |
| 
 | |
|     global.fetch = function(input, init) {
 | |
|       if (init && init.body && init.body instanceof FormDataPolyfill) {
 | |
|         init.body = init.body['_blob']()
 | |
|       }
 | |
| 
 | |
|       return _fetch(input, init)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   global['FormData'] = FormDataPolyfill
 | |
| }
 |