1 /*jslint 2 white: true, 3 vars: true, 4 plusplus: true 5 */ 6 /*globals 7 HTMLDocument, 8 HTMLFormElement, 9 document, 10 module 11 */ 12 13 /** 14 * @fileOverview atropa-formdata-generator: generates a function to produce 15 * FormData objects based on a given HTML form. 16 * @author <a href="mailto:matthewkastor@gmail.com"> 17 * Matthew Christopher Kastor-Inare III </a><br /> 18 * ☭ Hial Atropa!! ☭ 19 */ 20 21 /** 22 * A function generator that takes an HTML form and writes an equivalent 23 * function which will produce a formData object. The function can be generated 24 * to accept multiple parameters or a single options object. 25 * @author <a href="mailto:matthewkastor@gmail.com"> 26 * Matthew Christopher Kastor-Inare III </a><br /> 27 * ☭ Hial Atropa!! ☭ 28 * @param {HTMLFormElement} form A DOM reference to a form. 29 * @param {String} funcName Optional. The name you want to give your generated 30 * function. Defaults to <code>prettyPinkBike</code> 31 * @param {Boolean} useOptionsObj Optional. Set to true if you want the 32 * generated function to accept a single options object instead of multiple 33 * parameters. 34 * @param {String} tab Optional. The string specified here will be used as tabs. 35 * Defaults to four spaces. 36 * @param {String} eol Optional. The string specified here will be used as the 37 * end of line character. Defaults to <code>\r\n</code>. 38 * @returns {String} Returns a JavaScript function as a string. This function 39 * will accept parameters representing the data which would be entered into the 40 * form elements, and will return a FormData object. 41 */ 42 function formdataGenerator ( 43 form, funcName, useOptionsObj, tab, eol 44 ) { 45 "use strict"; 46 if(!(form instanceof HTMLFormElement)) { 47 throw new TypeError('form must be an HTMLFormElement'); 48 } 49 var defaultsQueue = []; 50 // easier to look at vars than quotes and punctuation. 51 var c = ','; 52 var q = "'"; 53 var sp = ' '; 54 var sc = ';'; 55 // configurable for the whiny sorts. 56 tab = tab || ' '; 57 eol = eol || '\r\n'; 58 // why do they insist on giving us useless data structures from the dom? 59 var elementsArray = Array.prototype.slice.call(form.elements, 0); 60 61 funcName = funcName || 'prettyPinkBike'; 62 // avoid stupid collisions with inherited properties. 63 var funcArgsObj = Object.create(null); 64 if (useOptionsObj) { 65 funcArgsObj.options = 1; 66 } 67 68 var funcBody = tab+'var formData = new FormData()'+sc+eol+eol; 69 70 /** 71 * escapes strings so they can be boxed. 72 * @param {String} str the string to escape. 73 * @returns {String} Returns the escaped string. 74 */ 75 function esc (str) { 76 str = str || ''; 77 return str.replace('\\', '\\\\').replace('\'', '\\\''); 78 } 79 /** 80 * adds an argument to the funcArgsObj for use when the generated function 81 * is not using an options argument. 82 * @param {String} arg The argument to add. 83 */ 84 function addArg (arg) { 85 // Adds property to null object so args are guaranteed to be unique. 86 // Takes everything that comes before the first open brace to be the 87 // name of the argument. Boxed values attached to this name would be 88 // properties of the argument and I have no intention of flattening 89 // them. 90 funcArgsObj[arg.split('[')[0]] = 1; 91 } 92 /** 93 * Gets the current value of the form element and generates a default value 94 * from it. 95 * @param {HTMLElement} formElement The form element to process. 96 * @returns {String} Returns a string representing a method of assigning 97 * the "default value" of the element. 98 */ 99 function getDefaultValue (val, formElement) { 100 var out = val+sp+'='+sp+q+esc(formElement.value)+q+sc; 101 var fakeFile = "new Blob(['here is a simple text file']," + 102 "{ 'type' : 'text/plain' })" 103 ; 104 switch (formElement.type) { 105 case 'file': 106 out = '//'+sp+val+sp+'='+sp+fakeFile+sc; 107 break; 108 case 'checkbox': 109 out = formElement.checked ? out : '//'+sp+out; 110 break; 111 default: 112 break; 113 } 114 return out; 115 } 116 /** 117 * Generates a javascript comment containing information about the 118 * attributes of the given element. 119 * @param {HTMLElement} element The HTML element to document. 120 * @param {String} tabs The string to use as tabs, this allows for padding 121 * the left side of the comment arbitrarily. 122 * @returns {String} Returns the comment as a string. 123 */ 124 function commentGenerator (element, tabs) { 125 tabs = tabs || ''; 126 var elAttrs = ( 127 Array.prototype.slice.call(element.attributes, 0). 128 map(function (item) { 129 return item.name + ' : ' + item.value; 130 }). 131 join(eol+tabs+sp+'*'+sp) 132 ); 133 var out = 134 '/**'+eol+tabs+ 135 sp+'*'+sp+element.tagName+' attributes: '+eol+tabs+ 136 sp+'*'+sp+elAttrs+eol+tabs+ 137 sp+'*/'+eol; 138 return out; 139 } 140 /** 141 * Generates a line for the function body based on properties of the form 142 * element given. 143 * @param {HTMLElement} The form element to process. 144 * @returns {String} Returns the line as a string. 145 */ 146 function generateLine (formElement) { 147 var name = formElement.name; 148 var key = name; 149 var val = name; 150 var line; 151 // this sets up the initial value for the map reduce function 152 // if the user wants a single options arg then everything must be boxed 153 // otherwise, this will be an opitons or array argument for the final 154 // function, if it contain boxes. 155 var p = (funcArgsObj.options === 1) ? 'options' : null; 156 // if there are boxes in the name we need to quote the name and box it 157 // for proper access as a javascript identifier. 158 if(name.indexOf('[') !== -1) { 159 // split on left square bracket 160 val = name.split('[').map(function (item) { 161 // remove the right square bracket from every element in the 162 // array. 163 return item.replace(']', ''); 164 // reduce the array to a single string value. 165 }).reduce(function (prev, curr) { 166 var out; 167 // in the initial case, if p is 'options' then concat 'options' 168 // with the boxed representation of the first array element. 169 // subsequent elements will be boxed and quoted. 170 if (prev) { 171 out = prev + '[\'' + esc(curr) + '\']'; 172 // otherwise, the first element does not need boxing. 173 // subsequent elements will be boxed and quoted. 174 } else { 175 out = curr; 176 } 177 return out; 178 }, p); 179 // if there are no boxes in the name but the user has specified 180 // that the function should accept a single options object, then 181 // everything needs to be boxed and quoted anyway. 182 } else if (funcArgsObj.options === 1) { 183 val = 'options[\'' + esc(name) + '\']'; 184 } 185 // the key is the unaltered name of the form element, the val is an 186 // identifier corresponding to one of the generated functions arguments. 187 line = 188 tab+commentGenerator(formElement, tab)+ 189 tab+'formData.append('+q+key+q+c+sp+val+')'+sc+eol; 190 defaultsQueue.push(getDefaultValue(val, formElement)); 191 return line; 192 } 193 /** 194 * Reducer function used to process the array of form elements into a string 195 * representing the generated function's body. 196 * @param {String} prev The accumulated value of reduction so far. 197 * @param {String} curr The current information to process and add to 198 * `prev`. 199 * @returns {String} Returns the value of reduction so far, to be fed into 200 * the next invocation of this function. 201 */ 202 function reducer (prev, curr) { 203 // carrying over the previous value of the function body. 204 var out = prev; 205 // if the current element doesn't have a name then it wouldn't submit 206 // usable data from the form, so skip it. 207 if(curr.name) { 208 if(!useOptionsObj) { 209 // if not generating a function using an options object, 210 // concatenate the form element name into the list of function 211 // args being genrated in the parent scope. 212 addArg(curr.name); 213 } 214 // append a new line to the function body. 215 out += generateLine(curr); 216 } 217 // return the function body in it's current state. 218 return out; 219 } 220 /** 221 * Generates the function and defaults for output. 222 * @returns {String} Returns the string value of a javascript function 223 * which will accept arguments and generate a FormData representation of 224 * the form processed. 225 */ 226 function generateFunction () { 227 var lints = '/*jslint white: true, sub: true*/'+eol+eol+ 228 '/*globals FormData */'+eol+eol; 229 var strict = tab+'"use strict"'+sc+eol; 230 // beginning with the function body, reduce the elements array to a 231 // single value using the reducer function. Replace the current function 232 // body with these results. 233 funcBody = elementsArray.reduce(reducer, funcBody); 234 // append a return statement to the function body. 235 funcBody += tab+eol+tab+'return formData'+sc+eol; 236 var formInfo = commentGenerator(form); 237 var n = funcName; 238 var b = strict+funcBody; 239 var a = Object.keys(funcArgsObj).reduce(function (prev, curr) { 240 var arg; 241 if(prev !== '') { 242 arg = prev+c+sp+curr; 243 } else { 244 arg = curr; 245 } 246 return arg; 247 }, ''); 248 var defaults = defaultsQueue.sort().join(eol); 249 var generated = lints+formInfo+'function'+sp+n+sp+'('+a+')'+sp+ 250 '{'+eol+b+'}'+eol+eol+defaults; 251 return generated; 252 } 253 254 return generateFunction(); 255 } 256 257 // atropaFormdataGenerator (form, funcName, true); 258 259 try { 260 module.exports = formdataGenerator; 261 //module.exports.forEveryForm = forEveryForm; 262 } catch (ignore) { 263 // module.exports does not exist. 264 } 265 266 267 268