| 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 |
})); |
| 345 |
|