forked from ampproject/amphtml
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathform-data-wrapper.js
300 lines (271 loc) · 8.94 KB
/
form-data-wrapper.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
import {iterateCursor} from '#core/dom';
import {getFormAsObject, getSubmitButtonUsed} from '#core/dom/form';
import {map} from '#core/types/object';
import {Services} from '#service';
/**
* Create a form data wrapper. The wrapper is necessary to provide a common
* API for FormData objects on all browsers. For example, not all browsers
* support the FormData#entries or FormData#delete functions.
*
* @param {!Window} win
* @param {!HTMLFormElement=} opt_form
* @return {!FormDataWrapperInterface}
*/
export function createFormDataWrapper(win, opt_form) {
const platform = Services.platformFor(win);
if (platform.isIos() && platform.getMajorVersion() == 11) {
return new Ios11NativeFormDataWrapper(opt_form);
} else if (FormData.prototype.entries && FormData.prototype.delete) {
return new NativeFormDataWrapper(opt_form);
} else {
return new PolyfillFormDataWrapper(opt_form);
}
}
/**
* Check if the given object is a FormDataWrapper instance
* @param {*} o
* @return {boolean} True if the object is a FormDataWrapper instance.
*/
export function isFormDataWrapper(o) {
// instanceof doesn't work as expected, so we detect with duck-typing.
return !!o && typeof o.getFormData == 'function';
}
/**
* A polyfill wrapper for a `FormData` object.
*
* If there's no native `FormData#entries`, chances are there are no native
* methods to read the content of the `FormData` after construction, so the
* only way to implement `entries` in this class is to capture the fields in
* the form passed to the constructor (and the arguments passed to the
* `append` method).
*
* For more details on this, see http://mdn.io/FormData.
*
* @implements {FormDataWrapperInterface}
* @visibleForTesting
*/
export class PolyfillFormDataWrapper {
/** @override */
constructor(opt_form = undefined) {
/** @private @const {!{[key: string]: !Array<string>}} */
this.fieldValues_ = opt_form ? getFormAsObject(opt_form) : map();
}
/**
* @param {string} name
* @param {string|!File} value
* @param {string=} opt_filename
* @override
*/
append(name, value, opt_filename) {
// Coercion to string is required to match
// the native FormData.append behavior
const nameString = String(name);
this.fieldValues_[nameString] = this.fieldValues_[nameString] || [];
this.fieldValues_[nameString].push(String(value));
}
/** @override */
delete(name) {
delete this.fieldValues_[name];
}
/** @override */
entries() {
const fieldEntries = [];
Object.keys(this.fieldValues_).forEach((name) => {
const values = this.fieldValues_[name];
values.forEach((value) => fieldEntries.push([name, value]));
});
// Generator functions are not supported by the current Babel configuration,
// so we must manually implement the iterator interface.
let nextIndex = 0;
return /** @type {!Iterator<!Array<string>>} */ ({
next() {
return nextIndex < fieldEntries.length
? {value: fieldEntries[nextIndex++], done: false}
: {value: undefined, done: true};
},
});
}
/** @override */
getFormData() {
const formData = new FormData();
Object.keys(this.fieldValues_).forEach((name) => {
const values = this.fieldValues_[name];
values.forEach((value) => formData.append(name, value));
});
return formData;
}
}
/**
* Wrap the native `FormData` implementation.
*
* NOTE: This differs from the standard `FormData` constructor. This constructor
* includes a submit button if it was used to submit the `opt_form`, where
* the native `FormData` constructor does not include the submit button used to
* submit the form.
* {@link https://xhr.spec.whatwg.org/#dom-formdata}
* @implements {FormDataWrapperInterface}
*/
class NativeFormDataWrapper {
/** @override */
constructor(opt_form) {
/** @private @const {!FormData} */
this.formData_ = new FormData(opt_form);
this.maybeIncludeSubmitButton_(opt_form);
}
/**
* If a submit button is focused (because it was used to submit the form),
* or was the first submit button present, add its name and value to the
* `FormData`, since publishers expect the submit button to be present.
* @param {!HTMLFormElement=} opt_form
* @private
*/
maybeIncludeSubmitButton_(opt_form) {
// If a form is not passed to the constructor,
// we are not in a submitting code path.
if (!opt_form) {
return;
}
const button = getSubmitButtonUsed(opt_form);
if (button && button.name) {
this.append(button.name, button.value);
}
}
/**
* @param {string} name
* @param {string|!File} value
* @param {string=} opt_filename
* @override
*/
append(name, value, opt_filename) {
this.formData_.append(name, value);
}
/** @override */
delete(name) {
this.formData_.delete(name);
}
/** @override */
entries() {
return this.formData_.entries();
}
/** @override */
getFormData() {
return this.formData_;
}
}
/**
* iOS 11 has a bug when submitting empty file inputs.
* This works around the bug by replacing the empty files with Blob objects.
*/
class Ios11NativeFormDataWrapper extends NativeFormDataWrapper {
/** @override */
constructor(opt_form) {
super(opt_form);
if (opt_form) {
iterateCursor(opt_form.elements, (input) => {
if (input.type == 'file' && input.files.length == 0) {
this.formData_.delete(input.name);
this.formData_.append(input.name, new Blob([]), '');
}
});
}
}
/**
* @param {string} name
* @param {string|!File} value
* @param {string=} opt_filename
* @override
*/
append(name, value, opt_filename) {
// Safari 11 breaks on submitting empty File values.
if (value && typeof value == 'object' && isEmptyFile(value)) {
this.formData_.append(name, new Blob([]), opt_filename || '');
} else {
this.formData_.append(name, value);
}
}
}
/**
* A wrapper for a native `FormData` object that allows the retrieval of entries
* in the form data after construction even on browsers that don't natively
* support `FormData.prototype.entries`.
*
* @interface
* Subclassing `FormData` doesn't work in this case as the transpiler
* generates code that calls the super constructor directly using
* `Function.prototype.call`. WebKit (Safari) doesn't allow this and
* enforces that constructors be called with the `new` operator.
*/
class FormDataWrapperInterface {
/**
* Creates a new wrapper for a `FormData` object.
*
* If there's no native `FormData#entries`, chances are there are no native
* methods to read the content of the `FormData` after construction, so the
* only way to implement `entries` in this class is to capture the fields in
* the form passed to the constructor (and the arguments passed to the
* `append` method).
*
* This constructor should also add the submitter element as defined in the
* HTML spec for Form Submission Algorithm, but is not defined by the standard
* when using the `FormData` constructor directly.
*
* For more details on this, see http://mdn.io/FormData.
*
* @param {!HTMLFormElement=} opt_form An HTML `<form>` element — when
* specified, the `FormData` object will be populated with the form's
* current keys/values using the name property of each element for the
* keys and their submitted value for the values. It will also encode file
* input content.
*/
constructor(opt_form) {}
/**
* Appends a new value onto an existing key inside a `FormData` object, or
* adds the key if it does not already exist.
*
* Appending a `File` object is not yet supported and the `filename`
* parameter is ignored for this wrapper.
*
* For more details on this, see http://mdn.io/FormData/append.
*
* TODO(cvializ): Update file support
*
* @param {string} unusedName The name of the field whose data is contained in
* `value`.
* @param {string|!File} unusedValue The field's value.
* @param {string=} opt_filename The filename to use if the value is a file.
*/
append(unusedName, unusedValue, opt_filename) {}
/**
* Remove the given value from the FormData.
*
* For more details on this, see http://mdn.io/FormData/delete.
*
* @param {string} unusedName The name of the field to remove from the FormData.
*/
delete(unusedName) {}
/**
* Returns an iterator of all key/value pairs contained in this object.
*
* For more details on this, see http://mdn.io/FormData/entries.
*
* @return {!Iterator<!Array<string|!File>>}
*/
entries() {}
/**
* Returns the wrapped native `FormData` object.
*
* @return {!FormData}
*/
getFormData() {}
}
/**
* Check if the given file is an empty file, which is the result of submitting
* an empty `<input type="file">`. These cause errors when submitting forms
* in Safari 11.
*
* @param {!File} file
* @return {boolean}
*/
function isEmptyFile(file) {
return file.name == '' && file.size == 0;
}