strong-tie/inbound-calls
0
1/* formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */2 3/* global FormData self Blob File */4/* eslint-disable no-inner-declarations */5 6if (typeof Blob !== 'undefined' && (typeof FormData === 'undefined' || !FormData.prototype.keys)) {7 const global = typeof globalThis === 'object'8 ? globalThis9 : typeof window === 'object'10 ? window11 : typeof self === 'object' ? self : this12 13 // keep a reference to native implementation14 const _FormData = global.FormData15 16 // To be monkey patched17 const _send = global.XMLHttpRequest && global.XMLHttpRequest.prototype.send18 const _fetch = global.Request && global.fetch19 const _sendBeacon = global.navigator && global.navigator.sendBeacon20 // Might be a worker thread...21 const _match = global.Element && global.Element.prototype22 23 // Unable to patch Request/Response constructor correctly #10924 // only way is to use ES6 class extend25 // https://github.com/babel/babel/issues/196626 27 const stringTag = global.Symbol && Symbol.toStringTag28 29 // Add missing stringTags to blob and files30 if (stringTag) {31 if (!Blob.prototype[stringTag]) {32 Blob.prototype[stringTag] = 'Blob'33 }34 35 if ('File' in global && !File.prototype[stringTag]) {36 File.prototype[stringTag] = 'File'37 }38 }39 40 // Fix so you can construct your own File41 try {42 new File([], '') // eslint-disable-line43 } catch (a) {44 global.File = function File (b, d, c) {45 const blob = new Blob(b, c || {})46 const t = c && void 0 !== c.lastModified ? new Date(c.lastModified) : new Date()47 48 Object.defineProperties(blob, {49 name: {50 value: d51 },52 lastModified: {53 value: +t54 },55 toString: {56 value () {57 return '[object File]'58 }59 }60 })61 62 if (stringTag) {63 Object.defineProperty(blob, stringTag, {64 value: 'File'65 })66 }67 68 return blob69 }70 }71 72 function ensureArgs (args, expected) {73 if (args.length < expected) {74 throw new TypeError(`${expected} argument required, but only ${args.length} present.`)75 }76 }77 78 /**79 * @param {string} name80 * @param {string | undefined} filename81 * @returns {[string, File|string]}82 */83 function normalizeArgs (name, value, filename) {84 if (value instanceof Blob) {85 filename = filename !== undefined86 ? String(filename + '')87 : typeof value.name === 'string'88 ? value.name89 : 'blob'90 91 if (value.name !== filename || Object.prototype.toString.call(value) === '[object Blob]') {92 value = new File([value], filename)93 }94 return [String(name), value]95 }96 return [String(name), String(value)]97 }98 99 // normalize line feeds for textarea100 // https://html.spec.whatwg.org/multipage/form-elements.html#textarea-line-break-normalisation-transformation101 function normalizeLinefeeds (value) {102 return value.replace(/\r?\n|\r/g, '\r\n')103 }104 105 /**106 * @template T107 * @param {ArrayLike<T>} arr108 * @param {{ (elm: T): void; }} cb109 */110 function each (arr, cb) {111 for (let i = 0; i < arr.length; i++) {112 cb(arr[i])113 }114 }115 116 const escape = str => str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22')117 118 /**119 * @implements {Iterable}120 */121 class FormDataPolyfill {122 /**123 * FormData class124 *125 * @param {HTMLFormElement=} form126 */127 constructor (form) {128 /** @type {[string, string|File][]} */129 this._data = []130 131 const self = this132 form && each(form.elements, (/** @type {HTMLInputElement} */ elm) => {133 if (134 !elm.name ||135 elm.disabled ||136 elm.type === 'submit' ||137 elm.type === 'button' ||138 elm.matches('form fieldset[disabled] *')139 ) return140 141 if (elm.type === 'file') {142 const files = elm.files && elm.files.length143 ? elm.files144 : [new File([], '', { type: 'application/octet-stream' })] // #78145 146 each(files, file => {147 self.append(elm.name, file)148 })149 } else if (elm.type === 'select-multiple' || elm.type === 'select-one') {150 each(elm.options, opt => {151 !opt.disabled && opt.selected && self.append(elm.name, opt.value)152 })153 } else if (elm.type === 'checkbox' || elm.type === 'radio') {154 if (elm.checked) self.append(elm.name, elm.value)155 } else {156 const value = elm.type === 'textarea' ? normalizeLinefeeds(elm.value) : elm.value157 self.append(elm.name, value)158 }159 })160 }161 162 /**163 * Append a field164 *165 * @param {string} name field name166 * @param {string|Blob|File} value string / blob / file167 * @param {string=} filename filename to use with blob168 * @return {undefined}169 */170 append (name, value, filename) {171 ensureArgs(arguments, 2)172 this._data.push(normalizeArgs(name, value, filename))173 }174 175 /**176 * Delete all fields values given name177 *178 * @param {string} name Field name179 * @return {undefined}180 */181 delete (name) {182 ensureArgs(arguments, 1)183 const result = []184 name = String(name)185 186 each(this._data, entry => {187 entry[0] !== name && result.push(entry)188 })189 190 this._data = result191 }192 193 /**194 * Iterate over all fields as [name, value]195 *196 * @return {Iterator}197 */198 * entries () {199 for (var i = 0; i < this._data.length; i++) {200 yield this._data[i]201 }202 }203 204 /**205 * Iterate over all fields206 *207 * @param {Function} callback Executed for each item with parameters (value, name, thisArg)208 * @param {Object=} thisArg `this` context for callback function209 */210 forEach (callback, thisArg) {211 ensureArgs(arguments, 1)212 for (const [name, value] of this) {213 callback.call(thisArg, value, name, this)214 }215 }216 217 /**218 * Return first field value given name219 * or null if non existent220 *221 * @param {string} name Field name222 * @return {string|File|null} value Fields value223 */224 get (name) {225 ensureArgs(arguments, 1)226 const entries = this._data227 name = String(name)228 for (let i = 0; i < entries.length; i++) {229 if (entries[i][0] === name) {230 return entries[i][1]231 }232 }233 return null234 }235 236 /**237 * Return all fields values given name238 *239 * @param {string} name Fields name240 * @return {Array} [{String|File}]241 */242 getAll (name) {243 ensureArgs(arguments, 1)244 const result = []245 name = String(name)246 each(this._data, data => {247 data[0] === name && result.push(data[1])248 })249 250 return result251 }252 253 /**254 * Check for field name existence255 *256 * @param {string} name Field name257 * @return {boolean}258 */259 has (name) {260 ensureArgs(arguments, 1)261 name = String(name)262 for (let i = 0; i < this._data.length; i++) {263 if (this._data[i][0] === name) {264 return true265 }266 }267 return false268 }269 270 /**271 * Iterate over all fields name272 *273 * @return {Iterator}274 */275 * keys () {276 for (const [name] of this) {277 yield name278 }279 }280 281 /**282 * Overwrite all values given name283 *284 * @param {string} name Filed name285 * @param {string} value Field value286 * @param {string=} filename Filename (optional)287 */288 set (name, value, filename) {289 ensureArgs(arguments, 2)290 name = String(name)291 /** @type {[string, string|File][]} */292 const result = []293 const args = normalizeArgs(name, value, filename)294 let replace = true295 296 // - replace the first occurrence with same name297 // - discards the remaining with same name298 // - while keeping the same order items where added299 each(this._data, data => {300 data[0] === name301 ? replace && (replace = !result.push(args))302 : result.push(data)303 })304 305 replace && result.push(args)306 307 this._data = result308 }309 310 /**311 * Iterate over all fields312 *313 * @return {Iterator}314 */315 * values () {316 for (const [, value] of this) {317 yield value318 }319 }320 321 /**322 * Return a native (perhaps degraded) FormData with only a `append` method323 * Can throw if it's not supported324 *325 * @return {FormData}326 */327 ['_asNative'] () {328 const fd = new _FormData()329 330 for (const [name, value] of this) {331 fd.append(name, value)332 }333 334 return fd335 }336 337 /**338 * [_blob description]339 *340 * @return {Blob} [description]341 */342 ['_blob'] () {343 const boundary = '----formdata-polyfill-' + Math.random(),344 chunks = [],345 p = `--${boundary}\r\nContent-Disposition: form-data; name="`346 this.forEach((value, name) => typeof value == 'string'347 ? chunks.push(p + escape(normalizeLinefeeds(name)) + `"\r\n\r\n${normalizeLinefeeds(value)}\r\n`)348 : 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`))349 chunks.push(`--${boundary}--`)350 return new Blob(chunks, {351 type: "multipart/form-data; boundary=" + boundary352 })353 }354 355 /**356 * The class itself is iterable357 * alias for formdata.entries()358 *359 * @return {Iterator}360 */361 [Symbol.iterator] () {362 return this.entries()363 }364 365 /**366 * Create the default string description.367 *368 * @return {string} [object FormData]369 */370 toString () {371 return '[object FormData]'372 }373 }374 375 if (_match && !_match.matches) {376 _match.matches =377 _match.matchesSelector ||378 _match.mozMatchesSelector ||379 _match.msMatchesSelector ||380 _match.oMatchesSelector ||381 _match.webkitMatchesSelector ||382 function (s) {383 var matches = (this.document || this.ownerDocument).querySelectorAll(s)384 var i = matches.length385 while (--i >= 0 && matches.item(i) !== this) {}386 return i > -1387 }388 }389 390 if (stringTag) {391 /**392 * Create the default string description.393 * It is accessed internally by the Object.prototype.toString().394 */395 FormDataPolyfill.prototype[stringTag] = 'FormData'396 }397 398 // Patch xhr's send method to call _blob transparently399 if (_send) {400 const setRequestHeader = global.XMLHttpRequest.prototype.setRequestHeader401 402 global.XMLHttpRequest.prototype.setRequestHeader = function (name, value) {403 setRequestHeader.call(this, name, value)404 if (name.toLowerCase() === 'content-type') this._hasContentType = true405 }406 407 global.XMLHttpRequest.prototype.send = function (data) {408 // need to patch send b/c old IE don't send blob's type (#44)409 if (data instanceof FormDataPolyfill) {410 const blob = data['_blob']()411 if (!this._hasContentType) this.setRequestHeader('Content-Type', blob.type)412 _send.call(this, blob)413 } else {414 _send.call(this, data)415 }416 }417 }418 419 // Patch fetch's function to call _blob transparently420 if (_fetch) {421 global.fetch = function (input, init) {422 if (init && init.body && init.body instanceof FormDataPolyfill) {423 init.body = init.body['_blob']()424 }425 426 return _fetch.call(this, input, init)427 }428 }429 430 // Patch navigator.sendBeacon to use native FormData431 if (_sendBeacon) {432 global.navigator.sendBeacon = function (url, data) {433 if (data instanceof FormDataPolyfill) {434 data = data['_asNative']()435 }436 return _sendBeacon.call(this, url, data)437 }438 }439 440 global['FormData'] = FormDataPolyfill441}442 