123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401 |
- 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 + '']
- }
- function each (arr, cb) {
- for (let i = 0; i < arr.length; i++) {
- cb(arr[i])
- }
- }
- /**
- * @implements {Iterable}
- */
- class FormDataPolyfill {
- /**
- * FormData class
- *
- * @param {HTMLElement=} form
- */
- constructor(form) {
- map.set(this, Object.create(null))
- if (!form)
- return this
- const self = this
- each(form.elements, elm => {
- if (!elm.name || elm.disabled || elm.type === 'submit' || elm.type === 'button') return
- if (elm.type === 'file') {
- each(elm.files || [], file => {
- self.append(elm.name, file)
- })
- } else if (elm.type === 'select-multiple' || elm.type === 'select-one') {
- each(elm.options, opt => {
- !opt.disabled && opt.selected && self.append(elm.name, opt.value)
- })
- } else if (elm.type === 'checkbox' || elm.type === 'radio') {
- if (elm.checked) self.append(elm.name, elm.value)
- } else {
- self.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
- }
|