This repository has been archived by the owner on Jan 21, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 17
/
jquery.addressfield.js
497 lines (439 loc) · 16.7 KB
/
jquery.addressfield.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
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
/*
* Address field
* https://github.com/tableau-mkt/jquery.addressfield
*
* Licensed under the MIT license.
*/
(function(factory) {
/* istanbul ignore next */
if (typeof module === "object" && typeof module.exports === "object") {
factory(require("jquery"));
} else {
factory(jQuery);
}
}(function factory($) {
/**
* Modifies an address field for this wrapped set of fields, given a config
* representing how the country writes its addresses (conforming roughly to
* xNAL standards), and an array of fields you desire to show (again, roughly
* xNAL compatible).
*
* @param options
* A configuration object with the following properties:
* - fields: (Required) An object mapping xNAL field names to jQuery
* selectors corresponding to the associated form elements. Any fields in
* your form that are not listed here will be ignored when mutating your
* postal address form. Note that the "country" field is required at a
* minimum. A common example might look like:
* {
* country: 'select#address-country',
* localityname: 'input.city',
* administrativearea: '#address-state',
* postalcode: '.zipcode'
* }
* - json: One of:
* - A string, representing the path to a JSON resource containing postal
* address field configurations matching the format defined by the
* addressfield.json project. This project comes packaged with a release
* of addressfield.json for ease-of-use, but you can provide your own
* configuration as well!
* - An object, representing the exact same data in the exact same format
* as would be returned by the JSON request for the string version of
* this configuration. Useful in cases where a hard-coded configuration
* would be more advantageous over the extra http request.
* - async: (Optional) Boolean flag that represents whether the request to
* the JSON resource specified above will be performed synchronously or
* asynchronously. Defaults to true (async JSON request).
* - defs: Deprecated; if no JSON config is provided (neither a valid
* path nor a full JavaScript object), you can use this key to apply a
* one-time postal form mutation given a field configuration and field
* map. Useful for quick-and-dirty upgrades from jquery.addressfield 0.x.
* Use of this functionality is highly discouraged.
*
* @returns {*}
* Returns itself (useful for chaining).
*/
$.fn.addressfield = function(options) {
var $container = this,
configs = $.extend({
fields: {},
json: null,
async: true,
// @deprecated Support for manual, synchronous, external control.
defs: {fields: {}}
}, options),
transformedData;
// If a path was given for a JSON resource, load the resource and execute.
if (typeof configs.json === 'string') {
$.ajax({
dataType: "json",
url: configs.json,
async: configs.async,
success: function (data) {
transformedData = $.fn.addressfield.transform(data);
$.fn.addressfield.initCountries.call($container, configs.fields.country, transformedData);
$.fn.addressfield.binder.call($container, configs.fields, transformedData);
$(configs.fields.country).change();
}
});
return $container;
}
// In this case, a direct configuration has been provided inline.
else if (typeof configs.json === 'object' && configs.json !== null) {
transformedData = $.fn.addressfield.transform(configs.json);
$.fn.addressfield.initCountries.call($container, configs.fields.country, transformedData);
$.fn.addressfield.binder.call($container, configs.fields, transformedData);
$(configs.fields.country).change();
return $container;
}
// Legacy support for manual, synchronous, external control.
// @deprecated Remove this functionality in the next major version (2.0.x).
else {
return $.fn.addressfield.apply.call($container, configs.defs, configs.fields);
}
};
/**
* Applies a given field configuration against a given postal address form.
*
* @param config
* The field configuration to be applied to the postal address form.
*
* @param fieldMap
* A mapping of xNAL field names to their selectors within this form.
*
* @returns {*}
* Returns itself (useful for chaining).
*/
$.fn.addressfield.apply = function (config, fieldMap) {
var $container = $(this),
fieldOrder = [],
$element,
selector,
placeholder,
fieldPos,
field;
// Iterate through defined address fields for this country.
for (fieldPos in config.fields) {
// Determine the xNAL name of this field and ignore
if (!config.fields.hasOwnProperty(fieldPos)) {
continue;
}
field = $.fn.addressfield.onlyKey(config.fields[fieldPos]);
// Pick out the existing elements for the given field.
selector = fieldMap.hasOwnProperty(field) ? fieldMap[field] : '.' + field;
$element = $container.find(selector);
// Account for nested fields.
if (config.fields[fieldPos][field] instanceof Array) {
return $.fn.addressfield.apply.call($element, {fields: config.fields[fieldPos][field]}, fieldMap);
}
// Otherwise perform the usual actions.
else {
// When swapping out labels / values for existing fields.
// Ensure the element exists and is configured to be displayed.
if ($element.length && fieldMap.hasOwnProperty(field)) {
// Push this field selector onto the fieldOrder array.
fieldOrder.push(selector);
// Update the options.
if (typeof config.fields[fieldPos][field].options !== 'undefined') {
// If this field has options but it's currently a text field,
// convert it back to a select field.
if (!$element.is('select')) {
$element = $.fn.addressfield.convertToSelect.call($element);
}
$.fn.addressfield.updateOptions.call($element, config.fields[fieldPos][field].options);
}
else {
// If this field does not have options but it's currently a select
// field, convert it back to a text field.
if ($element.is('select')) {
$element = $.fn.addressfield.convertToText.call($element);
}
// Apply a placeholder; empty one if none exists.
placeholder = config.fields[fieldPos][field].hasOwnProperty('eg') ? config.fields[fieldPos][field].eg : '';
$.fn.addressfield.updateEg.call($element, placeholder);
}
// Update the label.
$.fn.addressfield.updateLabel.call($element, config.fields[fieldPos][field].label);
}
// When adding fields that didn't previously exist.
if (!$.fn.addressfield.isVisible.call($element) && fieldMap.hasOwnProperty(field)) {
$.fn.addressfield.showField.call($element);
}
// Add, update, or remove validation handling for this field.
$.fn.addressfield.validate.call($element, field, config.fields[fieldPos][field]);
}
}
// Now check for fields that are still on the page but shouldn't be.
$.each(fieldMap, function (field_name, field_selector) {
var $element = $container.find(field_selector);
if ($element.length && !$.fn.addressfield.hasField(config, field_name)) {
$.fn.addressfield.hideField.call($element);
}
});
// Now ensure the fields are in their given order.
$.fn.addressfield.orderFields.call($container, fieldOrder);
// Trigger an addressfield:after event on the container.
$container.trigger('addressfield:after', {config: config, fieldMap: fieldMap});
return this;
};
/**
* Populates country dropdown with list of countries from provided json file if it is empty.
*
* @param selector
* Field identifying the country dropdown from user-configs.
*
* @param countryMap
* A map of country codes to country names.
*/
$.fn.addressfield.initCountries = function(selector, countryMap) {
var $container = this,
$countrySelect = $container.find(selector + ':not(:has(>option))'),
defaultCountry;
if (!$countrySelect.length) {
return;
}
else {
defaultCountry = $countrySelect.attr('data-country-selected');
}
$.each(countryMap, function(key, value) {
if (typeof defaultCountry !== 'undefined' &&
key.toLowerCase() === defaultCountry.toLowerCase()) {
$countrySelect.append($('<option></option>')
.attr('value', key)
.attr('selected', 'selected')
.text(value.label)
);
}
else {
$countrySelect.append($('<option></option>')
.attr('value', key)
.text(value.label)
);
}
});
};
/**
* Binds a handler to the country form field element, which applies postal
* address form mutations to this form container based on the selected country
* and given xNAL field map.
*
* @param fieldMap
* A map of xNAL fields to jQuery selectors representing their corresponding
* form field elements.
*
* @param countryConfigMap
* A map of field configurations to country ISO codes which should match
* the values associated with the country select element, defined in the
* fieldMap above).
*/
$.fn.addressfield.binder = function(fieldMap, countryConfigMap) {
var $container = this;
$container.find(fieldMap.country).bind('change', function() {
// Trigger the apply method with the country's data.
$.fn.addressfield.apply.call($container, countryConfigMap[this.value], fieldMap);
});
return $container;
};
/**
* Transforms JSON data returned in the instantiation method to the format
* expected by the binder method.
*/
$.fn.addressfield.transform = function(data) {
var countryMap = {},
position;
// Store a map of countries to their associated field configs.
for (position in data.options) {
countryMap[data.options[position].iso] = data.options[position];
}
return countryMap;
};
/**
* Returns the "first" (only) key of a JavaScript object.
*/
$.fn.addressfield.onlyKey = function (obj) {
for (var i in obj) {
return i;
}
};
/**
* Returns whether or not a given configuration contains a given field.
*/
$.fn.addressfield.hasField = function (config, expectedField) {
var pos,
field;
for (pos in config.fields) {
field = $.fn.addressfield.onlyKey(config.fields[pos]);
if (config.fields[pos][field] instanceof Array) {
return $.fn.addressfield.hasField({fields: config.fields[pos][field]}, expectedField);
}
else {
if (field === expectedField) {
return true;
}
}
}
return false;
};
/**
* Updates a given field's label with a given label.
*/
$.fn.addressfield.updateLabel = function (label) {
var $this = $(this),
elementName = $this.attr('id'),
$label = $('label[for="' + elementName + '"]') || $this.prev('label');
$label.text(label);
};
/**
* Updates a given field's expected format. By default, the placeholder text.
*/
$.fn.addressfield.updateEg = function (example) {
var text = example ? 'e.g. ' + example : '';
$(this).attr('placeholder', text);
};
/**
* Updates a given select field's options with given options.
*/
$.fn.addressfield.updateOptions = function (options) {
var $self = $(this),
oldVal = $self.data('_saved') || $self.val();
$self.children('option').remove();
$.each(options, function (optionPos) {
var value = $.fn.addressfield.onlyKey(options[optionPos]);
$self.append($('<option></option>').attr('value', value).text(options[optionPos][value]));
});
// Ensure the old value is still reflected after options are updated.
$self.val(oldVal).change();
// Clean up the data attribute; no-op if it was not previously populated.
$self.removeData('_saved');
};
/**
* Converts a given select field to a regular textarea.
*/
$.fn.addressfield.convertToText = function () {
var $self = $(this),
$input = $('<input />').attr('type', 'text');
// Copy attributes from $self to $input.
$.fn.addressfield.copyAttrsTo.call($self, $input);
// Ensure the old value is still reflected after conversion.
$input.val($self.val());
// Replace the existing element with our new one; also return it.
$self.replaceWith($input);
return $input;
};
/**
* Converts a given input field to a select field.
*/
$.fn.addressfield.convertToSelect = function() {
var $self = $(this),
$select = $('<select></select>');
// Copy attributes from $self to $select.
$.fn.addressfield.copyAttrsTo.call($self, $select);
// Save the old input value to a data attribute, for use in updateOptions.
$select.data('_saved', $self.val());
// Replace the existing element with our new one; also return it.
$self.replaceWith($select);
return $select;
};
/**
* Optional integration with jQuery.validate.
*/
$.fn.addressfield.validate = function(field, config) {
var $this = $(this),
methodName = 'isValid_' + field,
rule = {},
message = "Please check your formatting.";
// Only proceed if jQuery.validator is installed.
if (typeof $.validator !== 'undefined') {
// Support pre-set validation messages.
message = $.validator.messages.hasOwnProperty(methodName) ? $.validator.messages[methodName] : message;
// If the provided field has a specified format...
if (config.hasOwnProperty('format')) {
// Create the validation method.
$.validator.addMethod(methodName, function (value) {
// @todo Drop jQuery 1.3 support. No need for .toString() call.
// Make validation case insenstitve.
return new RegExp(config.format, 'i').test($.trim(value.toString()));
}, message);
// Apply the rule.
rule[methodName] = true;
$this.rules('add', rule);
}
else {
// If there is no format, create the validation method anyway, but have
// it do nothing.
$.validator.addMethod(methodName, function () {return true;}, message);
}
}
};
/**
* Hides the field, but stores it for restoration later, if necessary.
*/
$.fn.addressfield.hideField = function() {
$(this).val('').hide();
$.fn.addressfield.container.call(this).hide();
};
/**
* Shows / restores the field that had been previously hidden.
*/
$.fn.addressfield.showField = function() {
this.show();
$.fn.addressfield.container.call(this).show();
};
/**
* Returns whether or not the field is visible.
*/
$.fn.addressfield.isVisible = function() {
return $(this).is(':visible');
};
/**
* Returns the container element for a given field.
*/
$.fn.addressfield.container = function() {
var $this = $(this),
elementName = $this.attr('id'),
$label = $('label[for="' + elementName + '"]') || $this.prev('label');
// @todo drop support for jQuery 1.3, just use .has()
if (typeof $.fn.has === 'function') {
return $this.parents().has($label).first();
}
else {
return $this.parents().find(':has(label):has(#' + elementName + '):last');
}
};
/**
* Copies select HTML attributes from a given element to the supplied element.
*/
$.fn.addressfield.copyAttrsTo = function($to) {
var attributes = ['class', 'id', 'name', 'propdescname'],
$this = $(this);
$.each($this[0].attributes, function () {
if ($.inArray(this.name, attributes) !== -1) {
// Compatibility for IE8.
if (this.name === 'propdescname') {
$to.attr('name', this.value);
}
else {
$to.attr(this.name, this.value);
}
}
});
};
/**
* Re-orders fields given an array of selectors representing fields. Note that
* this can be called recursively if one of the values passed in the
* order array is itself an array.
*/
$.fn.addressfield.orderFields = function(order) {
var $self = $(this),
// Create an empty jQuery object.
// @todo Remove .not(document) when dropping jQuery 1.3 support.
$orderedContainers = $().not(document);
// Form a jQuery object with container elements in the correct order.
$.each(order, function (index, selector) {
var $container = $.fn.addressfield.container.call($self.find(selector));
$orderedContainers.push($container[0]);
});
// Re-append to parent in the correct order.
$orderedContainers.detach().appendTo($self);
};
}));