|
| 1 | +/*! |
| 2 | + SerializeJSON jQuery plugin. |
| 3 | + https://github.com/marioizquierdo/jquery.serializeJSON |
| 4 | + version 2.9.0 (Jan, 2018) |
| 5 | +
|
| 6 | + Copyright (c) 2012-2018 Mario Izquierdo |
| 7 | + Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) |
| 8 | + and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. |
| 9 | +*/ |
| 10 | +(function (factory) { |
| 11 | + if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. |
| 12 | + define(['jquery'], factory); |
| 13 | + } else if (typeof exports === 'object') { // Node/CommonJS |
| 14 | + var jQuery = require('jquery'); |
| 15 | + module.exports = factory(jQuery); |
| 16 | + } else { // Browser globals (zepto supported) |
| 17 | + factory(window.jQuery || window.Zepto || window.$); // Zepto supported on browsers as well |
| 18 | + } |
| 19 | + |
| 20 | +}(function ($) { |
| 21 | + "use strict"; |
| 22 | + |
| 23 | + // jQuery('form').serializeJSON() |
| 24 | + $.fn.serializeJSON = function (options) { |
| 25 | + var f, $form, opts, formAsArray, serializedObject, name, value, parsedValue, _obj, nameWithNoType, type, keys, skipFalsy; |
| 26 | + f = $.serializeJSON; |
| 27 | + $form = this; // NOTE: the set of matched elements is most likely a form, but it could also be a group of inputs |
| 28 | + opts = f.setupOpts(options); // calculate values for options {parseNumbers, parseBoolens, parseNulls, ...} with defaults |
| 29 | + |
| 30 | + // Use native `serializeArray` function to get an array of {name, value} objects. |
| 31 | + formAsArray = $form.serializeArray(); |
| 32 | + f.readCheckboxUncheckedValues(formAsArray, opts, $form); // add objects to the array from unchecked checkboxes if needed |
| 33 | + |
| 34 | + // Convert the formAsArray into a serializedObject with nested keys |
| 35 | + serializedObject = {}; |
| 36 | + $.each(formAsArray, function (i, obj) { |
| 37 | + name = obj.name; // original input name |
| 38 | + value = obj.value; // input value |
| 39 | + _obj = f.extractTypeAndNameWithNoType(name); |
| 40 | + nameWithNoType = _obj.nameWithNoType; // input name with no type (i.e. "foo:string" => "foo") |
| 41 | + type = _obj.type; // type defined from the input name in :type colon notation |
| 42 | + if (!type) type = f.attrFromInputWithName($form, name, 'data-value-type'); |
| 43 | + f.validateType(name, type, opts); // make sure that the type is one of the valid types if defined |
| 44 | + |
| 45 | + if (type !== 'skip') { // ignore inputs with type 'skip' |
| 46 | + keys = f.splitInputNameIntoKeysArray(nameWithNoType); |
| 47 | + parsedValue = f.parseValue(value, name, type, opts); // convert to string, number, boolean, null or customType |
| 48 | + |
| 49 | + skipFalsy = !parsedValue && f.shouldSkipFalsy($form, name, nameWithNoType, type, opts); // ignore falsy inputs if specified |
| 50 | + if (!skipFalsy) { |
| 51 | + f.deepSet(serializedObject, keys, parsedValue, opts); |
| 52 | + } |
| 53 | + } |
| 54 | + }); |
| 55 | + return serializedObject; |
| 56 | + }; |
| 57 | + |
| 58 | + // Use $.serializeJSON as namespace for the auxiliar functions |
| 59 | + // and to define defaults |
| 60 | + $.serializeJSON = { |
| 61 | + |
| 62 | + defaultOptions: { |
| 63 | + checkboxUncheckedValue: undefined, // to include that value for unchecked checkboxes (instead of ignoring them) |
| 64 | + |
| 65 | + parseNumbers: false, // convert values like "1", "-2.33" to 1, -2.33 |
| 66 | + parseBooleans: false, // convert "true", "false" to true, false |
| 67 | + parseNulls: false, // convert "null" to null |
| 68 | + parseAll: false, // all of the above |
| 69 | + parseWithFunction: null, // to use custom parser, a function like: function(val){ return parsed_val; } |
| 70 | + |
| 71 | + skipFalsyValuesForTypes: [], // skip serialization of falsy values for listed value types |
| 72 | + skipFalsyValuesForFields: [], // skip serialization of falsy values for listed field names |
| 73 | + |
| 74 | + customTypes: {}, // override defaultTypes |
| 75 | + defaultTypes: { |
| 76 | + "string": function(str) { return String(str); }, |
| 77 | + "number": function(str) { return Number(str); }, |
| 78 | + "boolean": function(str) { var falses = ["false", "null", "undefined", "", "0"]; return falses.indexOf(str) === -1; }, |
| 79 | + "null": function(str) { var falses = ["false", "null", "undefined", "", "0"]; return falses.indexOf(str) === -1 ? str : null; }, |
| 80 | + "array": function(str) { return JSON.parse(str); }, |
| 81 | + "object": function(str) { return JSON.parse(str); }, |
| 82 | + "auto": function(str) { return $.serializeJSON.parseValue(str, null, null, {parseNumbers: true, parseBooleans: true, parseNulls: true}); }, // try again with something like "parseAll" |
| 83 | + "skip": null // skip is a special type that makes it easy to ignore elements |
| 84 | + }, |
| 85 | + |
| 86 | + useIntKeysAsArrayIndex: false // name="foo[2]" value="v" => {foo: [null, null, "v"]}, instead of {foo: ["2": "v"]} |
| 87 | + }, |
| 88 | + |
| 89 | + // Merge option defaults into the options |
| 90 | + setupOpts: function(options) { |
| 91 | + var opt, validOpts, defaultOptions, optWithDefault, parseAll, f; |
| 92 | + f = $.serializeJSON; |
| 93 | + |
| 94 | + if (options == null) { options = {}; } // options ||= {} |
| 95 | + defaultOptions = f.defaultOptions || {}; // defaultOptions |
| 96 | + |
| 97 | + // Make sure that the user didn't misspell an option |
| 98 | + validOpts = ['checkboxUncheckedValue', 'parseNumbers', 'parseBooleans', 'parseNulls', 'parseAll', 'parseWithFunction', 'skipFalsyValuesForTypes', 'skipFalsyValuesForFields', 'customTypes', 'defaultTypes', 'useIntKeysAsArrayIndex']; // re-define because the user may override the defaultOptions |
| 99 | + for (opt in options) { |
| 100 | + if (validOpts.indexOf(opt) === -1) { |
| 101 | + throw new Error("serializeJSON ERROR: invalid option '" + opt + "'. Please use one of " + validOpts.join(', ')); |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + // Helper to get the default value for this option if none is specified by the user |
| 106 | + optWithDefault = function(key) { return (options[key] !== false) && (options[key] !== '') && (options[key] || defaultOptions[key]); }; |
| 107 | + |
| 108 | + // Return computed options (opts to be used in the rest of the script) |
| 109 | + parseAll = optWithDefault('parseAll'); |
| 110 | + return { |
| 111 | + checkboxUncheckedValue: optWithDefault('checkboxUncheckedValue'), |
| 112 | + |
| 113 | + parseNumbers: parseAll || optWithDefault('parseNumbers'), |
| 114 | + parseBooleans: parseAll || optWithDefault('parseBooleans'), |
| 115 | + parseNulls: parseAll || optWithDefault('parseNulls'), |
| 116 | + parseWithFunction: optWithDefault('parseWithFunction'), |
| 117 | + |
| 118 | + skipFalsyValuesForTypes: optWithDefault('skipFalsyValuesForTypes'), |
| 119 | + skipFalsyValuesForFields: optWithDefault('skipFalsyValuesForFields'), |
| 120 | + typeFunctions: $.extend({}, optWithDefault('defaultTypes'), optWithDefault('customTypes')), |
| 121 | + |
| 122 | + useIntKeysAsArrayIndex: optWithDefault('useIntKeysAsArrayIndex') |
| 123 | + }; |
| 124 | + }, |
| 125 | + |
| 126 | + // Given a string, apply the type or the relevant "parse" options, to return the parsed value |
| 127 | + parseValue: function(valStr, inputName, type, opts) { |
| 128 | + var f, parsedVal; |
| 129 | + f = $.serializeJSON; |
| 130 | + parsedVal = valStr; // if no parsing is needed, the returned value will be the same |
| 131 | + |
| 132 | + if (opts.typeFunctions && type && opts.typeFunctions[type]) { // use a type if available |
| 133 | + parsedVal = opts.typeFunctions[type](valStr); |
| 134 | + } else if (opts.parseNumbers && f.isNumeric(valStr)) { // auto: number |
| 135 | + parsedVal = Number(valStr); |
| 136 | + } else if (opts.parseBooleans && (valStr === "true" || valStr === "false")) { // auto: boolean |
| 137 | + parsedVal = (valStr === "true"); |
| 138 | + } else if (opts.parseNulls && valStr == "null") { // auto: null |
| 139 | + parsedVal = null; |
| 140 | + } else if (opts.typeFunctions && opts.typeFunctions["string"]) { // make sure to apply :string type if it was re-defined |
| 141 | + parsedVal = opts.typeFunctions["string"](valStr); |
| 142 | + } |
| 143 | + |
| 144 | + // Custom parse function: apply after parsing options, unless there's an explicit type. |
| 145 | + if (opts.parseWithFunction && !type) { |
| 146 | + parsedVal = opts.parseWithFunction(parsedVal, inputName); |
| 147 | + } |
| 148 | + |
| 149 | + return parsedVal; |
| 150 | + }, |
| 151 | + |
| 152 | + isObject: function(obj) { return obj === Object(obj); }, // is it an Object? |
| 153 | + isUndefined: function(obj) { return obj === void 0; }, // safe check for undefined values |
| 154 | + isValidArrayIndex: function(val) { return /^[0-9]+$/.test(String(val)); }, // 1,2,3,4 ... are valid array indexes |
| 155 | + isNumeric: function(obj) { return obj - parseFloat(obj) >= 0; }, // taken from jQuery.isNumeric implementation. Not using jQuery.isNumeric to support old jQuery and Zepto versions |
| 156 | + |
| 157 | + optionKeys: function(obj) { if (Object.keys) { return Object.keys(obj); } else { var key, keys = []; for(key in obj){ keys.push(key); } return keys;} }, // polyfill Object.keys to get option keys in IE<9 |
| 158 | + |
| 159 | + |
| 160 | + // Fill the formAsArray object with values for the unchecked checkbox inputs, |
| 161 | + // using the same format as the jquery.serializeArray function. |
| 162 | + // The value of the unchecked values is determined from the opts.checkboxUncheckedValue |
| 163 | + // and/or the data-unchecked-value attribute of the inputs. |
| 164 | + readCheckboxUncheckedValues: function (formAsArray, opts, $form) { |
| 165 | + var selector, $uncheckedCheckboxes, $el, uncheckedValue, f, name; |
| 166 | + if (opts == null) { opts = {}; } |
| 167 | + f = $.serializeJSON; |
| 168 | + |
| 169 | + selector = 'input[type=checkbox][name]:not(:checked):not([disabled])'; |
| 170 | + $uncheckedCheckboxes = $form.find(selector).add($form.filter(selector)); |
| 171 | + $uncheckedCheckboxes.each(function (i, el) { |
| 172 | + // Check data attr first, then the option |
| 173 | + $el = $(el); |
| 174 | + uncheckedValue = $el.attr('data-unchecked-value'); |
| 175 | + if (uncheckedValue == null) { |
| 176 | + uncheckedValue = opts.checkboxUncheckedValue; |
| 177 | + } |
| 178 | + |
| 179 | + // If there's an uncheckedValue, push it into the serialized formAsArray |
| 180 | + if (uncheckedValue != null) { |
| 181 | + if (el.name && el.name.indexOf("[][") !== -1) { // identify a non-supported |
| 182 | + throw new Error("serializeJSON ERROR: checkbox unchecked values are not supported on nested arrays of objects like '"+el.name+"'. See https://github.com/marioizquierdo/jquery.serializeJSON/issues/67"); |
| 183 | + } |
| 184 | + formAsArray.push({name: el.name, value: uncheckedValue}); |
| 185 | + } |
| 186 | + }); |
| 187 | + }, |
| 188 | + |
| 189 | + // Returns and object with properties {name_without_type, type} from a given name. |
| 190 | + // The type is null if none specified. Example: |
| 191 | + // "foo" => {nameWithNoType: "foo", type: null} |
| 192 | + // "foo:boolean" => {nameWithNoType: "foo", type: "boolean"} |
| 193 | + // "foo[bar]:null" => {nameWithNoType: "foo[bar]", type: "null"} |
| 194 | + extractTypeAndNameWithNoType: function(name) { |
| 195 | + var match; |
| 196 | + if (match = name.match(/(.*):([^:]+)$/)) { |
| 197 | + return {nameWithNoType: match[1], type: match[2]}; |
| 198 | + } else { |
| 199 | + return {nameWithNoType: name, type: null}; |
| 200 | + } |
| 201 | + }, |
| 202 | + |
| 203 | + |
| 204 | + // Check if this input should be skipped when it has a falsy value, |
| 205 | + // depending on the options to skip values by name or type, and the data-skip-falsy attribute. |
| 206 | + shouldSkipFalsy: function($form, name, nameWithNoType, type, opts) { |
| 207 | + var f = $.serializeJSON; |
| 208 | + |
| 209 | + var skipFromDataAttr = f.attrFromInputWithName($form, name, 'data-skip-falsy'); |
| 210 | + if (skipFromDataAttr != null) { |
| 211 | + return skipFromDataAttr !== 'false'; // any value is true, except if explicitly using 'false' |
| 212 | + } |
| 213 | + |
| 214 | + var optForFields = opts.skipFalsyValuesForFields; |
| 215 | + if (optForFields && (optForFields.indexOf(nameWithNoType) !== -1 || optForFields.indexOf(name) !== -1)) { |
| 216 | + return true; |
| 217 | + } |
| 218 | + |
| 219 | + var optForTypes = opts.skipFalsyValuesForTypes; |
| 220 | + if (type == null) type = 'string'; // assume fields with no type are targeted as string |
| 221 | + if (optForTypes && optForTypes.indexOf(type) !== -1) { |
| 222 | + return true |
| 223 | + } |
| 224 | + |
| 225 | + return false; |
| 226 | + }, |
| 227 | + |
| 228 | + // Finds the first input in $form with this name, and get the given attr from it. |
| 229 | + // Returns undefined if no input or no attribute was found. |
| 230 | + attrFromInputWithName: function($form, name, attrName) { |
| 231 | + var escapedName, selector, $input, attrValue; |
| 232 | + escapedName = name.replace(/(:|\.|\[|\]|\s)/g,'\\$1'); // every non-standard character need to be escaped by \\ |
| 233 | + selector = '[name="' + escapedName + '"]'; |
| 234 | + $input = $form.find(selector).add($form.filter(selector)); // NOTE: this returns only the first $input element if multiple are matched with the same name (i.e. an "array[]"). So, arrays with different element types specified through the data-value-type attr is not supported. |
| 235 | + return $input.attr(attrName); |
| 236 | + }, |
| 237 | + |
| 238 | + // Raise an error if the type is not recognized. |
| 239 | + validateType: function(name, type, opts) { |
| 240 | + var validTypes, f; |
| 241 | + f = $.serializeJSON; |
| 242 | + validTypes = f.optionKeys(opts ? opts.typeFunctions : f.defaultOptions.defaultTypes); |
| 243 | + if (!type || validTypes.indexOf(type) !== -1) { |
| 244 | + return true; |
| 245 | + } else { |
| 246 | + throw new Error("serializeJSON ERROR: Invalid type " + type + " found in input name '" + name + "', please use one of " + validTypes.join(', ')); |
| 247 | + } |
| 248 | + }, |
| 249 | + |
| 250 | + |
| 251 | + // Split the input name in programatically readable keys. |
| 252 | + // Examples: |
| 253 | + // "foo" => ['foo'] |
| 254 | + // "[foo]" => ['foo'] |
| 255 | + // "foo[inn][bar]" => ['foo', 'inn', 'bar'] |
| 256 | + // "foo[inn[bar]]" => ['foo', 'inn', 'bar'] |
| 257 | + // "foo[inn][arr][0]" => ['foo', 'inn', 'arr', '0'] |
| 258 | + // "arr[][val]" => ['arr', '', 'val'] |
| 259 | + splitInputNameIntoKeysArray: function(nameWithNoType) { |
| 260 | + var keys, f; |
| 261 | + f = $.serializeJSON; |
| 262 | + keys = nameWithNoType.split('['); // split string into array |
| 263 | + keys = $.map(keys, function (key) { return key.replace(/\]/g, ''); }); // remove closing brackets |
| 264 | + if (keys[0] === '') { keys.shift(); } // ensure no opening bracket ("[foo][inn]" should be same as "foo[inn]") |
| 265 | + return keys; |
| 266 | + }, |
| 267 | + |
| 268 | + // Set a value in an object or array, using multiple keys to set in a nested object or array: |
| 269 | + // |
| 270 | + // deepSet(obj, ['foo'], v) // obj['foo'] = v |
| 271 | + // deepSet(obj, ['foo', 'inn'], v) // obj['foo']['inn'] = v // Create the inner obj['foo'] object, if needed |
| 272 | + // deepSet(obj, ['foo', 'inn', '123'], v) // obj['foo']['arr']['123'] = v // |
| 273 | + // |
| 274 | + // deepSet(obj, ['0'], v) // obj['0'] = v |
| 275 | + // deepSet(arr, ['0'], v, {useIntKeysAsArrayIndex: true}) // arr[0] = v |
| 276 | + // deepSet(arr, [''], v) // arr.push(v) |
| 277 | + // deepSet(obj, ['arr', ''], v) // obj['arr'].push(v) |
| 278 | + // |
| 279 | + // arr = []; |
| 280 | + // deepSet(arr, ['', v] // arr => [v] |
| 281 | + // deepSet(arr, ['', 'foo'], v) // arr => [v, {foo: v}] |
| 282 | + // deepSet(arr, ['', 'bar'], v) // arr => [v, {foo: v, bar: v}] |
| 283 | + // deepSet(arr, ['', 'bar'], v) // arr => [v, {foo: v, bar: v}, {bar: v}] |
| 284 | + // |
| 285 | + deepSet: function (o, keys, value, opts) { |
| 286 | + var key, nextKey, tail, lastIdx, lastVal, f; |
| 287 | + if (opts == null) { opts = {}; } |
| 288 | + f = $.serializeJSON; |
| 289 | + if (f.isUndefined(o)) { throw new Error("ArgumentError: param 'o' expected to be an object or array, found undefined"); } |
| 290 | + if (!keys || keys.length === 0) { throw new Error("ArgumentError: param 'keys' expected to be an array with least one element"); } |
| 291 | + |
| 292 | + key = keys[0]; |
| 293 | + |
| 294 | + // Only one key, then it's not a deepSet, just assign the value. |
| 295 | + if (keys.length === 1) { |
| 296 | + if (key === '') { |
| 297 | + o.push(value); // '' is used to push values into the array (assume o is an array) |
| 298 | + } else { |
| 299 | + o[key] = value; // other keys can be used as object keys or array indexes |
| 300 | + } |
| 301 | + |
| 302 | + // With more keys is a deepSet. Apply recursively. |
| 303 | + } else { |
| 304 | + nextKey = keys[1]; |
| 305 | + |
| 306 | + // '' is used to push values into the array, |
| 307 | + // with nextKey, set the value into the same object, in object[nextKey]. |
| 308 | + // Covers the case of ['', 'foo'] and ['', 'var'] to push the object {foo, var}, and the case of nested arrays. |
| 309 | + if (key === '') { |
| 310 | + lastIdx = o.length - 1; // asume o is array |
| 311 | + lastVal = o[lastIdx]; |
| 312 | + if (f.isObject(lastVal) && (f.isUndefined(lastVal[nextKey]) || keys.length > 2)) { // if nextKey is not present in the last object element, or there are more keys to deep set |
| 313 | + key = lastIdx; // then set the new value in the same object element |
| 314 | + } else { |
| 315 | + key = lastIdx + 1; // otherwise, point to set the next index in the array |
| 316 | + } |
| 317 | + } |
| 318 | + |
| 319 | + // '' is used to push values into the array "array[]" |
| 320 | + if (nextKey === '') { |
| 321 | + if (f.isUndefined(o[key]) || !$.isArray(o[key])) { |
| 322 | + o[key] = []; // define (or override) as array to push values |
| 323 | + } |
| 324 | + } else { |
| 325 | + if (opts.useIntKeysAsArrayIndex && f.isValidArrayIndex(nextKey)) { // if 1, 2, 3 ... then use an array, where nextKey is the index |
| 326 | + if (f.isUndefined(o[key]) || !$.isArray(o[key])) { |
| 327 | + o[key] = []; // define (or override) as array, to insert values using int keys as array indexes |
| 328 | + } |
| 329 | + } else { // for anything else, use an object, where nextKey is going to be the attribute name |
| 330 | + if (f.isUndefined(o[key]) || !f.isObject(o[key])) { |
| 331 | + o[key] = {}; // define (or override) as object, to set nested properties |
| 332 | + } |
| 333 | + } |
| 334 | + } |
| 335 | + |
| 336 | + // Recursively set the inner object |
| 337 | + tail = keys.slice(1); |
| 338 | + f.deepSet(o[key], tail, value, opts); |
| 339 | + } |
| 340 | + } |
| 341 | + |
| 342 | + }; |
| 343 | + |
| 344 | +})); |
0 commit comments