|
|
- /* formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
-
- /* global FormData self Blob File */
- /* eslint-disable no-inner-declarations */
-
- if (typeof Blob !== 'undefined' && (typeof FormData === 'undefined' || !FormData.prototype.keys)) {
- const global = typeof globalThis === 'object'
- ? globalThis
- : 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
- const _sendBeacon = global.navigator && global.navigator.sendBeacon
- // Might be a worker thread...
- const _match = global.Element && global.Element.prototype
-
- // Unable to patch Request/Response constructor correctly #109
- // only way is to use ES6 class extend
- // https://github.com/babel/babel/issues/1966
-
- const stringTag = global.Symbol && Symbol.toStringTag
-
- // 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([], '') // eslint-disable-line
- } catch (a) {
- global.File = function File (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
- },
- lastModified: {
- value: +t
- },
- toString: {
- value () {
- return '[object File]'
- }
- }
- })
-
- if (stringTag) {
- Object.defineProperty(blob, stringTag, {
- value: 'File'
- })
- }
-
- return blob
- }
- }
-
- function ensureArgs (args, expected) {
- if (args.length < expected) {
- throw new TypeError(`${expected} argument required, but only ${args.length} present.`)
- }
- }
-
- /**
- * @param {string} name
- * @param {string | undefined} filename
- * @returns {[string, File|string]}
- */
- function normalizeArgs (name, value, filename) {
- if (value instanceof Blob) {
- filename = filename !== undefined
- ? String(filename + '')
- : typeof value.name === 'string'
- ? value.name
- : 'blob'
-
- if (value.name !== filename || Object.prototype.toString.call(value) === '[object Blob]') {
- value = new File([value], filename)
- }
- return [String(name), value]
- }
- return [String(name), String(value)]
- }
-
- // normalize line feeds for textarea
- // https://html.spec.whatwg.org/multipage/form-elements.html#textarea-line-break-normalisation-transformation
- function normalizeLinefeeds (value) {
- return value.replace(/\r?\n|\r/g, '\r\n')
- }
-
- /**
- * @template T
- * @param {ArrayLike<T>} arr
- * @param {{ (elm: T): void; }} cb
- */
- function each (arr, cb) {
- for (let i = 0; i < arr.length; i++) {
- cb(arr[i])
- }
- }
-
- const escape = str => str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22')
-
- /**
- * @implements {Iterable}
- */
- class FormDataPolyfill {
- /**
- * FormData class
- *
- * @param {HTMLFormElement=} form
- */
- constructor (form) {
- /** @type {[string, string|File][]} */
- this._data = []
-
- const self = this
- form && each(form.elements, (/** @type {HTMLInputElement} */ elm) => {
- if (
- !elm.name ||
- elm.disabled ||
- elm.type === 'submit' ||
- elm.type === 'button' ||
- elm.matches('form fieldset[disabled] *')
- ) return
-
- if (elm.type === 'file') {
- const files = elm.files && elm.files.length
- ? elm.files
- : [new File([], '', { type: 'application/octet-stream' })] // #78
-
- each(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 {
- const value = elm.type === 'textarea' ? normalizeLinefeeds(elm.value) : elm.value
- self.append(elm.name, 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) {
- ensureArgs(arguments, 2)
- this._data.push(normalizeArgs(name, value, filename))
- }
-
- /**
- * Delete all fields values given name
- *
- * @param {string} name Field name
- * @return {undefined}
- */
- delete (name) {
- ensureArgs(arguments, 1)
- const result = []
- name = String(name)
-
- each(this._data, entry => {
- entry[0] !== name && result.push(entry)
- })
-
- this._data = result
- }
-
- /**
- * Iterate over all fields as [name, value]
- *
- * @return {Iterator}
- */
- * entries () {
- for (var i = 0; i < this._data.length; i++) {
- yield this._data[i]
- }
- }
-
- /**
- * Iterate over all fields
- *
- * @param {Function} callback Executed for each item with parameters (value, name, thisArg)
- * @param {Object=} thisArg `this` context for callback function
- */
- forEach (callback, thisArg) {
- ensureArgs(arguments, 1)
- for (const [name, value] of this) {
- callback.call(thisArg, value, name, this)
- }
- }
-
- /**
- * Return first field value given name
- * or null if non existent
- *
- * @param {string} name Field name
- * @return {string|File|null} value Fields value
- */
- get (name) {
- ensureArgs(arguments, 1)
- const entries = this._data
- name = String(name)
- for (let i = 0; i < entries.length; i++) {
- if (entries[i][0] === name) {
- return entries[i][1]
- }
- }
- return null
- }
-
- /**
- * Return all fields values given name
- *
- * @param {string} name Fields name
- * @return {Array} [{String|File}]
- */
- getAll (name) {
- ensureArgs(arguments, 1)
- const result = []
- name = String(name)
- each(this._data, data => {
- data[0] === name && result.push(data[1])
- })
-
- return result
- }
-
- /**
- * Check for field name existence
- *
- * @param {string} name Field name
- * @return {boolean}
- */
- has (name) {
- ensureArgs(arguments, 1)
- name = String(name)
- for (let i = 0; i < this._data.length; i++) {
- if (this._data[i][0] === name) {
- return true
- }
- }
- return false
- }
-
- /**
- * Iterate over all fields name
- *
- * @return {Iterator}
- */
- * keys () {
- for (const [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)
- */
- set (name, value, filename) {
- ensureArgs(arguments, 2)
- name = String(name)
- /** @type {[string, string|File][]} */
- const result = []
- const args = normalizeArgs(name, value, filename)
- let replace = true
-
- // - replace the first occurrence with same name
- // - discards the remaining with same name
- // - while keeping the same order items where added
- each(this._data, data => {
- data[0] === name
- ? replace && (replace = !result.push(args))
- : result.push(data)
- })
-
- replace && result.push(args)
-
- this._data = result
- }
-
- /**
- * Iterate over all fields
- *
- * @return {Iterator}
- */
- * values () {
- for (const [, 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 (const [name, value] of this) {
- fd.append(name, value)
- }
-
- return fd
- }
-
- /**
- * [_blob description]
- *
- * @return {Blob} [description]
- */
- ['_blob'] () {
- const boundary = '----formdata-polyfill-' + Math.random(),
- chunks = [],
- p = `--${boundary}\r\nContent-Disposition: form-data; name="`
- this.forEach((value, name) => typeof value == 'string'
- ? chunks.push(p + escape(normalizeLinefeeds(name)) + `"\r\n\r\n${normalizeLinefeeds(value)}\r\n`)
- : chunks.push(p + escape(normalizeLinefeeds(name)) + `"; filename="${escape(value.name)}"\r\nContent-Type: ${value.type||"application/octet-stream"}\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 (_match && !_match.matches) {
- _match.matches =
- _match.matchesSelector ||
- _match.mozMatchesSelector ||
- _match.msMatchesSelector ||
- _match.oMatchesSelector ||
- _match.webkitMatchesSelector ||
- function (s) {
- var matches = (this.document || this.ownerDocument).querySelectorAll(s)
- var i = matches.length
- while (--i >= 0 && matches.item(i) !== this) {}
- return i > -1
- }
- }
-
- if (stringTag) {
- /**
- * Create the default string description.
- * It is accessed internally by the Object.prototype.toString().
- */
- FormDataPolyfill.prototype[stringTag] = 'FormData'
- }
-
- // Patch xhr's send method to call _blob transparently
- if (_send) {
- const setRequestHeader = global.XMLHttpRequest.prototype.setRequestHeader
-
- global.XMLHttpRequest.prototype.setRequestHeader = function (name, value) {
- setRequestHeader.call(this, name, value)
- if (name.toLowerCase() === 'content-type') this._hasContentType = true
- }
-
- global.XMLHttpRequest.prototype.send = function (data) {
- // need to patch send b/c old IE don't send blob's type (#44)
- if (data instanceof FormDataPolyfill) {
- const blob = data['_blob']()
- if (!this._hasContentType) 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) {
- global.fetch = function (input, init) {
- if (init && init.body && init.body instanceof FormDataPolyfill) {
- init.body = init.body['_blob']()
- }
-
- return _fetch.call(this, input, init)
- }
- }
-
- // Patch navigator.sendBeacon to use native FormData
- if (_sendBeacon) {
- global.navigator.sendBeacon = function (url, data) {
- if (data instanceof FormDataPolyfill) {
- data = data['_asNative']()
- }
- return _sendBeacon.call(this, url, data)
- }
- }
-
- global['FormData'] = FormDataPolyfill
- }
|