angelovcom.net

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

wp-polyfill-formdata.js (12011B)


      1 /* global FormData self Blob File */
      2 /* eslint-disable no-inner-declarations */
      3 
      4 if (typeof Blob !== 'undefined' && (typeof FormData === 'undefined' || !FormData.prototype.keys)) {
      5   const global = typeof globalThis === 'object'
      6     ? globalThis
      7     : typeof window === 'object'
      8       ? window
      9       : typeof self === 'object' ? self : this
     10 
     11   // keep a reference to native implementation
     12   const _FormData = global.FormData
     13 
     14   // To be monkey patched
     15   const _send = global.XMLHttpRequest && global.XMLHttpRequest.prototype.send
     16   const _fetch = global.Request && global.fetch
     17   const _sendBeacon = global.navigator && global.navigator.sendBeacon
     18   // Might be a worker thread...
     19   const _match = global.Element && global.Element.prototype
     20 
     21   // Unable to patch Request/Response constructor correctly #109
     22   // only way is to use ES6 class extend
     23   // https://github.com/babel/babel/issues/1966
     24 
     25   const stringTag = global.Symbol && Symbol.toStringTag
     26 
     27   // Add missing stringTags to blob and files
     28   if (stringTag) {
     29     if (!Blob.prototype[stringTag]) {
     30       Blob.prototype[stringTag] = 'Blob'
     31     }
     32 
     33     if ('File' in global && !File.prototype[stringTag]) {
     34       File.prototype[stringTag] = 'File'
     35     }
     36   }
     37 
     38   // Fix so you can construct your own File
     39   try {
     40     new File([], '') // eslint-disable-line
     41   } catch (a) {
     42     global.File = function File (b, d, c) {
     43       const blob = new Blob(b, c)
     44       const t = c && void 0 !== c.lastModified ? new Date(c.lastModified) : new Date()
     45 
     46       Object.defineProperties(blob, {
     47         name: {
     48           value: d
     49         },
     50         lastModifiedDate: {
     51           value: t
     52         },
     53         lastModified: {
     54           value: +t
     55         },
     56         toString: {
     57           value () {
     58             return '[object File]'
     59           }
     60         }
     61       })
     62 
     63       if (stringTag) {
     64         Object.defineProperty(blob, stringTag, {
     65           value: 'File'
     66         })
     67       }
     68 
     69       return blob
     70     }
     71   }
     72 
     73   function normalizeValue ([name, value, filename]) {
     74     if (value instanceof Blob) {
     75       // Should always returns a new File instance
     76       // console.assert(fd.get(x) !== fd.get(x))
     77       value = new File([value], filename, {
     78         type: value.type,
     79         lastModified: value.lastModified
     80       })
     81     }
     82 
     83     return [name, value]
     84   }
     85 
     86   function ensureArgs (args, expected) {
     87     if (args.length < expected) {
     88       throw new TypeError(`${expected} argument required, but only ${args.length} present.`)
     89     }
     90   }
     91 
     92   function normalizeArgs (name, value, filename) {
     93     return value instanceof Blob
     94       // normalize name and filename if adding an attachment
     95       ? [String(name), value, filename !== undefined
     96         ? filename + '' // Cast filename to string if 3th arg isn't undefined
     97         : typeof value.name === 'string' // if name prop exist
     98           ? value.name // Use File.name
     99           : 'blob'] // otherwise fallback to Blob
    100 
    101       // If no attachment, just cast the args to strings
    102       : [String(name), String(value)]
    103   }
    104 
    105   // normalize linefeeds for textareas
    106   // https://html.spec.whatwg.org/multipage/form-elements.html#textarea-line-break-normalisation-transformation
    107   function normalizeLinefeeds (value) {
    108     return value.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n')
    109   }
    110 
    111   function each (arr, cb) {
    112     for (let i = 0; i < arr.length; i++) {
    113       cb(arr[i])
    114     }
    115   }
    116 
    117   /**
    118    * @implements {Iterable}
    119    */
    120   class FormDataPolyfill {
    121     /**
    122      * FormData class
    123      *
    124      * @param {HTMLElement=} form
    125      */
    126     constructor (form) {
    127       this._data = []
    128 
    129       const self = this
    130 
    131       form && each(form.elements, elm => {
    132         if (
    133           !elm.name ||
    134           elm.disabled ||
    135           elm.type === 'submit' ||
    136           elm.type === 'button' ||
    137           elm.matches('form fieldset[disabled] *')
    138         ) return
    139 
    140         if (elm.type === 'file') {
    141           const files = elm.files && elm.files.length
    142             ? elm.files
    143             : [new File([], '', { type: 'application/octet-stream' })] // #78
    144 
    145           each(files, file => {
    146             self.append(elm.name, file)
    147           })
    148         } else if (elm.type === 'select-multiple' || elm.type === 'select-one') {
    149           each(elm.options, opt => {
    150             !opt.disabled && opt.selected && self.append(elm.name, opt.value)
    151           })
    152         } else if (elm.type === 'checkbox' || elm.type === 'radio') {
    153           if (elm.checked) self.append(elm.name, elm.value)
    154         } else {
    155           const value = elm.type === 'textarea' ? normalizeLinefeeds(elm.value) : elm.value
    156           self.append(elm.name, value)
    157         }
    158       })
    159     }
    160 
    161     /**
    162      * Append a field
    163      *
    164      * @param   {string}           name      field name
    165      * @param   {string|Blob|File} value     string / blob / file
    166      * @param   {string=}          filename  filename to use with blob
    167      * @return  {undefined}
    168      */
    169     append (name, value, filename) {
    170       ensureArgs(arguments, 2)
    171       this._data.push(normalizeArgs(name, value, filename))
    172     }
    173 
    174     /**
    175      * Delete all fields values given name
    176      *
    177      * @param   {string}  name  Field name
    178      * @return  {undefined}
    179      */
    180     delete (name) {
    181       ensureArgs(arguments, 1)
    182       const result = []
    183       name = String(name)
    184 
    185       each(this._data, entry => {
    186         entry[0] !== name && result.push(entry)
    187       })
    188 
    189       this._data = result
    190     }
    191 
    192     /**
    193      * Iterate over all fields as [name, value]
    194      *
    195      * @return {Iterator}
    196      */
    197     * entries () {
    198       for (var i = 0; i < this._data.length; i++) {
    199         yield normalizeValue(this._data[i])
    200       }
    201     }
    202 
    203     /**
    204      * Iterate over all fields
    205      *
    206      * @param   {Function}  callback  Executed for each item with parameters (value, name, thisArg)
    207      * @param   {Object=}   thisArg   `this` context for callback function
    208      * @return  {undefined}
    209      */
    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 name
    219      * or null if non existen
    220      *
    221      * @param   {string}  name      Field name
    222      * @return  {string|File|null}  value Fields value
    223      */
    224     get (name) {
    225       ensureArgs(arguments, 1)
    226       const entries = this._data
    227       name = String(name)
    228       for (let i = 0; i < entries.length; i++) {
    229         if (entries[i][0] === name) {
    230           return normalizeValue(entries[i])[1]
    231         }
    232       }
    233       return null
    234     }
    235 
    236     /**
    237      * Return all fields values given name
    238      *
    239      * @param   {string}  name  Fields name
    240      * @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(normalizeValue(data)[1])
    248       })
    249 
    250       return result
    251     }
    252 
    253     /**
    254      * Check for field name existence
    255      *
    256      * @param   {string}   name  Field name
    257      * @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 true
    265         }
    266       }
    267       return false
    268     }
    269 
    270     /**
    271      * Iterate over all fields name
    272      *
    273      * @return {Iterator}
    274      */
    275     * keys () {
    276       for (const [name] of this) {
    277         yield name
    278       }
    279     }
    280 
    281     /**
    282      * Overwrite all values given name
    283      *
    284      * @param   {string}    name      Filed name
    285      * @param   {string}    value     Field value
    286      * @param   {string=}   filename  Filename (optional)
    287      * @return  {undefined}
    288      */
    289     set (name, value, filename) {
    290       ensureArgs(arguments, 2)
    291       name = String(name)
    292       const result = []
    293       const args = normalizeArgs(name, value, filename)
    294       let replace = true
    295 
    296       // - replace the first occurrence with same name
    297       // - discards the remaning with same name
    298       // - while keeping the same order items where added
    299       each(this._data, data => {
    300         data[0] === name
    301           ? replace && (replace = !result.push(args))
    302           : result.push(data)
    303       })
    304 
    305       replace && result.push(args)
    306 
    307       this._data = result
    308     }
    309 
    310     /**
    311      * Iterate over all fields
    312      *
    313      * @return {Iterator}
    314      */
    315     * values () {
    316       for (const [, value] of this) {
    317         yield value
    318       }
    319     }
    320 
    321     /**
    322      * Return a native (perhaps degraded) FormData with only a `append` method
    323      * Can throw if it's not supported
    324      *
    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 fd
    335     }
    336 
    337     /**
    338      * [_blob description]
    339      *
    340      * @return {Blob} [description]
    341      */
    342     ['_blob'] () {
    343       const boundary = '----formdata-polyfill-' + Math.random()
    344       const chunks = []
    345 
    346       for (const [name, value] of this) {
    347         chunks.push(`--${boundary}\r\n`)
    348 
    349         if (value instanceof Blob) {
    350           chunks.push(
    351             `Content-Disposition: form-data; name="${name}"; filename="${value.name}"\r\n` +
    352             `Content-Type: ${value.type || 'application/octet-stream'}\r\n\r\n`,
    353             value,
    354             '\r\n'
    355           )
    356         } else {
    357           chunks.push(
    358             `Content-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`
    359           )
    360         }
    361       }
    362 
    363       chunks.push(`--${boundary}--`)
    364 
    365       return new Blob(chunks, {
    366         type: 'multipart/form-data; boundary=' + boundary
    367       })
    368     }
    369 
    370     /**
    371      * The class itself is iterable
    372      * alias for formdata.entries()
    373      *
    374      * @return  {Iterator}
    375      */
    376     [Symbol.iterator] () {
    377       return this.entries()
    378     }
    379 
    380     /**
    381      * Create the default string description.
    382      *
    383      * @return  {string} [object FormData]
    384      */
    385     toString () {
    386       return '[object FormData]'
    387     }
    388   }
    389 
    390   if (_match && !_match.matches) {
    391     _match.matches =
    392       _match.matchesSelector ||
    393       _match.mozMatchesSelector ||
    394       _match.msMatchesSelector ||
    395       _match.oMatchesSelector ||
    396       _match.webkitMatchesSelector ||
    397       function (s) {
    398         var matches = (this.document || this.ownerDocument).querySelectorAll(s)
    399         var i = matches.length
    400         while (--i >= 0 && matches.item(i) !== this) {}
    401         return i > -1
    402       }
    403   }
    404 
    405   if (stringTag) {
    406     /**
    407      * Create the default string description.
    408      * It is accessed internally by the Object.prototype.toString().
    409      */
    410     FormDataPolyfill.prototype[stringTag] = 'FormData'
    411   }
    412 
    413   // Patch xhr's send method to call _blob transparently
    414   if (_send) {
    415     const setRequestHeader = global.XMLHttpRequest.prototype.setRequestHeader
    416 
    417     global.XMLHttpRequest.prototype.setRequestHeader = function (name, value) {
    418       setRequestHeader.call(this, name, value)
    419       if (name.toLowerCase() === 'content-type') this._hasContentType = true
    420     }
    421 
    422     global.XMLHttpRequest.prototype.send = function (data) {
    423       // need to patch send b/c old IE don't send blob's type (#44)
    424       if (data instanceof FormDataPolyfill) {
    425         const blob = data['_blob']()
    426         if (!this._hasContentType) this.setRequestHeader('Content-Type', blob.type)
    427         _send.call(this, blob)
    428       } else {
    429         _send.call(this, data)
    430       }
    431     }
    432   }
    433 
    434   // Patch fetch's function to call _blob transparently
    435   if (_fetch) {
    436     global.fetch = function (input, init) {
    437       if (init && init.body && init.body instanceof FormDataPolyfill) {
    438         init.body = init.body['_blob']()
    439       }
    440 
    441       return _fetch.call(this, input, init)
    442     }
    443   }
    444 
    445   // Patch navigator.sendBeacon to use native FormData
    446   if (_sendBeacon) {
    447     global.navigator.sendBeacon = function (url, data) {
    448       if (data instanceof FormDataPolyfill) {
    449         data = data['_asNative']()
    450       }
    451       return _sendBeacon.call(this, url, data)
    452     }
    453   }
    454 
    455   global['FormData'] = FormDataPolyfill
    456 }