-
Notifications
You must be signed in to change notification settings - Fork 3
/
index.js
372 lines (333 loc) · 10.8 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
/**
*
* Get successful control from form and assemble into object.
* @see {@link http://www.w3.org/TR/html401/interact/forms.html#h-17.13.2}
* @module FormSerialization
*/
// types which indicate a submit action and are not successful controls
// these will be ignored
const kRSubmitter = /^(?:submit|button|image|reset|file)$/i;
// node names which could be successful controls
const kRSuccessContrls = /^(?:input|select|textarea|keygen)/i;
// Matches bracket notation.
const brackets = /(\[[^[\]]*])/g;
/**
* @callback module:FormSerialization.Serializer
* @param {PlainObject|string|any} result
* @param {string} key
* @param {string} value
* @returns {PlainObject|string|any} New result
*/
/**
* @typedef {PlainObject} module:FormSerialization.Options
* @property {boolean} [hash] Configure the output type. If true, the
* output will be a JavaScript object.
* @property {module:FormSerialization.Serializer} [serializer] Optional
* serializer function to override the default one. Otherwise, hash
* and URL-encoded string serializers are provided with this module,
* depending on the setting of `hash`.
* @property {boolean} [disabled] If true serialize disabled fields.
* @property {boolean} [empty] If true serialize empty fields
*/
/**
* Serializes form fields.
* @function module:FormSerialization.serialize
* @param {HTMLFormElement} form MUST be an `HTMLFormElement`
* @param {module:FormSerialization.Options} options is an optional argument
* to configure the serialization.
* @returns {any|string|PlainObject} Default output with no options specified is
* a url encoded string
*/
export function serialize (form, options) {
if (typeof options !== 'object') {
options = {hash: Boolean(options)};
} else if (options.hash === undefined) {
options.hash = true;
}
let result = options.hash ? {} : '';
const serializer = options.serializer ||
(options.hash ? hashSerializer : strSerialize);
const elements = form && form.elements ? [...form.elements] : [];
// Object store each radio and set if it's empty or not
const radioStore = Object.create(null);
elements.forEach((element) => {
// ignore disabled fields
if ((!options.disabled && element.disabled) || !element.name) {
return;
}
// ignore anything that is not considered a success field
if (!kRSuccessContrls.test(element.nodeName) ||
kRSubmitter.test(element.type)) {
return;
}
const {name: key, type, name, checked} = element;
let {value} = element;
// We can't just use element.value for checkboxes cause some
// browsers lie to us; they say "on" for value when the
// box isn't checked
if ((type === 'checkbox' || type === 'radio') && !checked) {
value = undefined;
}
// If we want empty elements
if (options.empty) {
// for checkbox
if (type === 'checkbox' && !checked) {
value = '';
}
// for radio
if (type === 'radio') {
if (!radioStore[name] && !checked) {
radioStore[name] = false;
} else if (checked) {
radioStore[name] = true;
}
if (value === undefined) {
return;
}
}
} else if (!value) {
// value-less fields are ignored unless options.empty is true
return;
}
// multi select boxes
if (type === 'select-multiple') {
let isSelectedOptions = false;
[...element.options].forEach((option) => {
const allowedEmpty = options.empty && !option.value;
const hasValue = (option.value || allowedEmpty);
if (option.selected && hasValue) {
isSelectedOptions = true;
// If using a hash serializer be sure to add the
// correct notation for an array in the multi-select
// context. Here the name attribute on the select element
// might be missing the trailing bracket pair. Both names
// "foo" and "foo[]" should be arrays.
if (options.hash && key.slice(-2) !== '[]') {
result = serializer(result, key + '[]', option.value);
} else {
result = serializer(result, key, option.value);
}
}
});
// Serialize if no selected options and options.empty is true
if (!isSelectedOptions && options.empty) {
result = serializer(result, key, '');
}
return;
}
result = serializer(result, key, value);
});
// Check for all empty radio buttons and serialize them with key=""
if (options.empty) {
Object.entries(radioStore).forEach(([key, value]) => {
if (!value) {
result = serializer(result, key, '');
}
});
}
return result;
}
/**
*
* @param {string} string
* @returns {string[]}
*/
function parseKeys (string) {
const keys = [];
const prefix = /^([^[\]]*)/;
const children = new RegExp(brackets);
let match = prefix.exec(string);
if (match[1]) {
keys.push(match[1]);
}
while ((match = children.exec(string)) !== null) {
keys.push(match[1]);
}
return keys;
}
/**
* @typedef {GenericArray} ResultArray
*/
/**
*
* @param {PlainObject|ResultArray} result
* @param {string[]} keys
* @param {string} value
* @returns {string|PlainObject|ResultArray}
*/
function hashAssign (result, keys, value) {
if (keys.length === 0) {
return value;
}
const key = keys.shift();
const between = key.match(/^\[(.+?)]$/);
if (key === '[]') {
result = result || [];
if (Array.isArray(result)) {
result.push(hashAssign(null, keys, value));
} else {
// This might be the result of bad name attributes like "[][foo]",
// in this case the original `result` object will already be
// assigned to an object literal. Rather than coerce the object to
// an array, or cause an exception the attribute "_values" is
// assigned as an array.
result._values = result._values || [];
result._values.push(hashAssign(null, keys, value));
}
return result;
}
// Key is an attribute name and can be assigned directly.
if (!between) {
result[key] = hashAssign(result[key], keys, value);
} else {
const string = between[1];
// +var converts the variable into a number
// better than parseInt because it doesn't truncate away trailing
// letters and actually fails if whole thing is not a number
const index = Number(string);
// If the characters between the brackets is not a number it is an
// attribute name and can be assigned directly.
// Switching to Number.isNaN would require a polyfill for IE11
// eslint-disable-next-line unicorn/prefer-number-properties
if (isNaN(index)) {
result = result || {};
result[string] = hashAssign(result[string], keys, value);
} else {
result = result || [];
result[index] = hashAssign(result[index], keys, value);
}
}
return result;
}
/**
* Object/hash encoding serializer.
* @param {PlainObject} result
* @param {string} key
* @param {string} value
* @returns {PlainObject}
*/
function hashSerializer (result, key, value) {
const hasBrackets = key.match(brackets);
// Has brackets? Use the recursive assignment function to walk the keys,
// construct any missing objects in the result tree and make the assignment
// at the end of the chain.
if (hasBrackets) {
const keys = parseKeys(key);
hashAssign(result, keys, value);
} else {
// Non bracket notation can make assignments directly.
const existing = result[key];
// If the value has been assigned already (for instance when a radio and
// a checkbox have the same name attribute) convert the previous value
// into an array before pushing into it.
//
// NOTE: If this requirement were removed all hash creation and
// assignment could go through `hashAssign`.
if (existing) {
if (!Array.isArray(existing)) {
result[key] = [existing];
}
result[key].push(value);
} else {
result[key] = value;
}
}
return result;
}
/**
* URL form encoding serializer.
* @param {string} result
* @param {string} key
* @param {string} value
* @returns {string} New result
*/
function strSerialize (result, key, value) {
// encode newlines as \r\n cause the html spec says so
value = value.replace(/(\r)?\n/g, '\r\n');
value = encodeURIComponent(value);
// spaces should be '+' rather than '%20'.
value = value.replace(/%20/g, '+');
return result + (result ? '&' : '') + encodeURIComponent(key) + '=' + value;
}
/**
* @function module:FormSerialization.deserialize
* @param {HTMLFormElement} form
* @param {PlainObject} hash
* @returns {void}
*/
export function deserialize (form, hash) {
// input(text|radio|checkbox)|select(multiple)|textarea|keygen
Object.entries(hash).forEach(([name, value]) => {
let control = form[name];
let hasBrackets = false;
// istanbul ignore else
if (!control) { // Try again for jsdom
control = form.querySelector(`[name="${name}"]`);
if (!control) {
// We want this for `RadioNodeList` so setting value
// auto-disables other boxes
control = form[name + '[]'];
// istanbul ignore next
if (!control || typeof control !== 'object' || !('length' in control)) {
// The latter query would only get a single
// element, so if not a `RadioNodeList`, we get
// all values here
control = form.querySelectorAll(`[name="${name}[]"]`);
if (!control.length) {
throw new Error(`Name not found ${name}`);
}
}
hasBrackets = true;
}
}
const {type} = control;
if (type === 'checkbox') {
control.checked = value !== '';
}
if (type === 'radio' || (control[0] && control[0].type === 'radio')) {
[...form.querySelectorAll(
`[name="${name + (hasBrackets ? '[]' : '')}"]`
)].forEach((radio) => {
radio.checked = value === radio.value;
});
}
if (control[0] && control[0].type === 'select-multiple') {
[...control[0].options].forEach((o) => {
if (value.includes(o.value)) {
o.selected = true;
}
});
return;
}
if (Array.isArray(value)) {
// options on a multiple select
if (type === 'select-multiple') {
[...control.options].forEach((o) => {
if (value.includes(o.value)) {
o.selected = true;
}
});
return;
}
value.forEach((v, i) => {
const c = control[i];
if (c.type === 'checkbox') {
const isMatch = c.value === v || v === 'on';
c.checked = isMatch;
return;
}
if (c.type === 'select-multiple') {
[...c.options].forEach((o) => {
if (v.includes(o.value)) {
o.selected = true;
}
});
return;
}
c.value = v;
});
} else {
control.value = value;
}
});
}