PluginProbe
UsersWP – Front-end login form, User Registration, User Profile & Members Directory plugin for WP / 1.2.74
UsersWP – Front-end login form, User Registration, User Profile & Members Directory plugin for WP v1.2.74
1.2.74 1.2.73 1.2.72 1.2.71 1.2.70 1.2.69 1.2.68 1.2.67 1.2.66 1.2.65 1.2.64 1.2.63 trunk 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.19 1.0.20 1.0.21 All 174 releases
userswp / assets / js / countrySelect.js

countrySelect.js in UsersWP – Front-end login form, User Registration, User Profile & Members Directory plugin for WP 1.2.74, at assets/js/countrySelect.js

547 lines 20.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * A modified version of country select for UWP needs.
3 */
4 // wrap in UMD - see https://github.com/umdjs/umd/blob/master/jqueryPlugin.js
5 (function(factory) {
6 if (typeof define === "function" && define.amd) {
7 define([ "jquery" ], function($) {
8 factory($, window, document);
9 });
10 } else {
11 factory(jQuery, window, document);
12 }
13 })(function($, window, document, undefined) {
14 "use strict";
15 var pluginName = "countrySelect", id = 1, // give each instance its own ID for namespaced event handling
16 defaults = {
17 // Default country
18 defaultCountry: "",
19 // Position the selected flag inside or outside of the input
20 defaultStyling: "inside",
21 // Display only these countries
22 onlyCountries: [],
23 // The countries at the top of the list. Defaults to United States and United Kingdom
24 preferredCountries: [ "us", "gb" ]
25 }, keys = {
26 UP: 38,
27 DOWN: 40,
28 ENTER: 13,
29 ESC: 27,
30 PLUS: 43,
31 A: 65,
32 Z: 90
33 }, windowLoaded = false;
34 // keep track of if the window.load event has fired as impossible to check after the fact
35 $(window).on('load', function() {
36 windowLoaded = true;
37 });
38 function Plugin(element, options) {
39 this.element = element;
40 this.options = $.extend({}, defaults, options);
41 this._defaults = defaults;
42 // event namespace
43 this.ns = "." + pluginName + id++;
44 this._name = pluginName;
45 this.init();
46 }
47 Plugin.prototype = {
48 init: function() {
49 // Process all the data: onlyCountries, preferredCountries, defaultCountry etc
50 this._processCountryData();
51 // Generate the markup
52 this._generateMarkup();
53 // Set the initial state of the input value and the selected flag
54 this._setInitialState();
55 // Start all of the event listeners: input keyup, selectedFlag click
56 this._initListeners();
57 },
58 /********************
59 * PRIVATE METHODS
60 ********************/
61 // prepare all of the country data, including onlyCountries, preferredCountries and
62 // defaultCountry options
63 _processCountryData: function() {
64 // set the instances country data objects
65 this._setInstanceCountryData();
66 // set the preferredCountries property
67 this._setPreferredCountries();
68 },
69 // process onlyCountries array if present
70 _setInstanceCountryData: function() {
71 var that = this;
72 if (this.options.onlyCountries.length) {
73 var newCountries = [];
74 $.each(this.options.onlyCountries, function(i, countryCode) {
75 var countryData = that._getCountryData(countryCode, true);
76 if (countryData) {
77 newCountries.push(countryData);
78 }
79 });
80 this.countries = newCountries;
81 } else {
82 this.countries = allCountries;
83 }
84 },
85 // Process preferred countries - iterate through the preferences,
86 // fetching the country data for each one
87 _setPreferredCountries: function() {
88 var that = this;
89 this.preferredCountries = [];
90 $.each(this.options.preferredCountries, function(i, countryCode) {
91 var countryData = that._getCountryData(countryCode, false);
92 if (countryData) {
93 that.preferredCountries.push(countryData);
94 }
95 });
96 },
97 // generate all of the markup for the plugin: the selected flag overlay, and the dropdown
98 _generateMarkup: function() {
99 // Country input
100 this.countryInput = $(this.element);
101 // containers (mostly for positioning)
102 var mainClass = "country-select";
103 if (this.options.defaultStyling) {
104 mainClass += " " + this.options.defaultStyling;
105 }
106 this.countryInput.wrap($("<div>", {
107 "class": mainClass
108 }));
109 var flagsContainer = $("<div>", {
110 "class": "flag-dropdown"
111 }).insertAfter(this.countryInput);
112 // currently selected flag (displayed to left of input)
113 var selectedFlag = $("<div>", {
114 "class": "selected-flag"
115 }).appendTo(flagsContainer);
116 this.selectedFlagInner = $("<div>", {
117 "class": "flag"
118 }).appendTo(selectedFlag);
119 // CSS triangle
120 $("<div>", {
121 "class": "arrow"
122 }).appendTo(this.selectedFlagInner);
123 // country list contains: preferred countries, then divider, then all countries
124 this.countryList = $("<ul>", {
125 "class": "country-list v-hide"
126 }).appendTo(flagsContainer);
127 if (this.preferredCountries.length) {
128 this._appendListItems(this.preferredCountries, "preferred");
129 $("<li>", {
130 "class": "divider"
131 }).appendTo(this.countryList);
132 }
133 this._appendListItems(this.countries, "");
134 // Add the hidden input for the country code
135 this.countryCodeInput = $("#"+this.countryInput.attr("id")+"_code");
136 if (!this.countryCodeInput) {
137 this.countryCodeInput = $('<input type="hidden" id="'+this.countryInput.attr("id")+'_code" name="'+this.countryInput.attr("name")+'_code" value="" />');
138 this.countryCodeInput.insertAfter(this.countryInput);
139 }
140 // now we can grab the dropdown height, and hide it properly
141 this.dropdownHeight = this.countryList.outerHeight();
142 this.countryList.removeClass("v-hide").addClass("hide");
143 // this is useful in lots of places
144 this.countryListItems = this.countryList.children(".country");
145 },
146 // add a country <li> to the countryList <ul> container
147 _appendListItems: function(countries, className) {
148 // Generate DOM elements as a large temp string, so that there is only
149 // one DOM insert event
150 var tmp = "";
151 // for each country
152 $.each(countries, function(i, c) {
153 // open the list item
154 tmp += '<li class="country ' + className + '" data-country-code="' + c.iso2 + '">';
155 // add the flag
156 tmp += '<div class="flag ' + c.iso2 + '"></div>';
157 // and the country name
158 tmp += '<span class="country-name">' + c.name + '</span>';
159 // close the list item
160 tmp += '</li>';
161 });
162 this.countryList.append(tmp);
163 },
164 // set the initial state of the input value and the selected flag
165 _setInitialState: function() {
166 var flagIsSet = false;
167 // If the input is pre-populated, then just update the selected flag
168 if (this.countryInput.val()) {
169 flagIsSet = this._updateFlagFromInputVal();
170 }
171 // If the country code input is pre-populated, update the name and the selected flag
172 var selectedCode = this.countryCodeInput.val();
173 if (selectedCode) {
174 this.selectCountry(selectedCode);
175 }
176 if (!flagIsSet) {
177 // flag is not set, so set to the default country
178 var defaultCountry;
179 // check the defaultCountry option, else fall back to the first in the list
180 if (this.options.defaultCountry) {
181 defaultCountry = this._getCountryData(this.options.defaultCountry, false);
182 // Did we not find the requested default country?
183 if (!defaultCountry) {
184 defaultCountry = this.preferredCountries.length ? this.preferredCountries[0] : this.countries[0];
185 }
186 } else {
187 defaultCountry = this.preferredCountries.length ? this.preferredCountries[0] : this.countries[0];
188 }
189 this.selectCountry(defaultCountry.iso2);
190 }
191 },
192 // initialise the main event listeners: input keyup, and click selected flag
193 _initListeners: function() {
194 var that = this;
195 // Update flag on keyup.
196 // Use keyup instead of keypress because we want to update on backspace
197 // and instead of keydown because the value hasn't updated when that
198 // event is fired.
199 // NOTE: better to have this one listener all the time instead of
200 // starting it on focus and stopping it on blur, because then you've
201 // got two listeners (focus and blur)
202 this.countryInput.on("keyup" + this.ns, function() {
203 that._updateFlagFromInputVal();
204 });
205 // toggle country dropdown on click
206 var selectedFlag = this.selectedFlagInner.parent();
207 selectedFlag.on("click" + this.ns, function(e) {
208 // only intercept this event if we're opening the dropdown
209 // else let it bubble up to the top ("click-off-to-close" listener)
210 // we cannot just stopPropagation as it may be needed to close another instance
211 if (that.countryList.hasClass("hide") && !that.countryInput.prop("disabled")) {
212 that._showDropdown();
213 }
214 });
215 // Despite above note, added blur to ensure partially spelled country
216 // with correctly chosen flag is spelled out on blur. Also, correctly
217 // selects flag when field is autofilled
218 this.countryInput.on("blur" + this.ns, function() {
219 if (that.countryInput.val() != that.getSelectedCountryData().name) {
220 that.setCountry(that.countryInput.val());
221 }
222 that.countryInput.val(that.getSelectedCountryData().name);
223 });
224 },
225 // Focus input and put the cursor at the end
226 _focus: function() {
227 this.countryInput.focus();
228 var input = this.countryInput[0];
229 // works for Chrome, FF, Safari, IE9+
230 if (input.setSelectionRange) {
231 var len = this.countryInput.val().length;
232 input.setSelectionRange(len, len);
233 }
234 },
235 // Show the dropdown
236 _showDropdown: function() {
237 this._setDropdownPosition();
238 // update highlighting and scroll to active list item
239 var activeListItem = this.countryList.children(".active");
240 this._highlightListItem(activeListItem);
241 // show it
242 this.countryList.removeClass("hide");
243 this._scrollTo(activeListItem);
244 // bind all the dropdown-related listeners: mouseover, click, click-off, keydown
245 this._bindDropdownListeners();
246 // update the arrow
247 this.selectedFlagInner.children(".arrow").addClass("up");
248 },
249 // decide where to position dropdown (depends on position within viewport, and scroll)
250 _setDropdownPosition: function() {
251 var inputTop = this.countryInput.offset().top, windowTop = $(window).scrollTop(),
252 dropdownFitsBelow = inputTop + this.countryInput.outerHeight() + this.dropdownHeight < windowTop + $(window).height(), dropdownFitsAbove = inputTop - this.dropdownHeight > windowTop;
253 // dropdownHeight - 1 for border
254 var cssTop = !dropdownFitsBelow && dropdownFitsAbove ? "-" + (this.dropdownHeight - 1) + "px" : "";
255 this.countryList.css("top", cssTop);
256 },
257 // we only bind dropdown listeners when the dropdown is open
258 _bindDropdownListeners: function() {
259 var that = this;
260 // when mouse over a list item, just highlight that one
261 // we add the class "highlight", so if they hit "enter" we know which one to select
262 this.countryList.on("mouseover" + this.ns, ".country", function(e) {
263 that._highlightListItem($(this));
264 });
265 // listen for country selection
266 this.countryList.on("click" + this.ns, ".country", function(e) {
267 that._selectListItem($(this));
268 });
269 // click off to close
270 // (except when this initial opening click is bubbling up)
271 // we cannot just stopPropagation as it may be needed to close another instance
272 var isOpening = true;
273 $("html").on("click" + this.ns, function(e) {
274 if (!isOpening) {
275 that._closeDropdown();
276 }
277 isOpening = false;
278 });
279 // Listen for up/down scrolling, enter to select, or letters to jump to country name.
280 // Use keydown as keypress doesn't fire for non-char keys and we want to catch if they
281 // just hit down and hold it to scroll down (no keyup event).
282 // Listen on the document because that's where key events are triggered if no input has focus
283 $(document).on("keydown" + this.ns, function(e) {
284 // prevent down key from scrolling the whole page,
285 // and enter key from submitting a form etc
286 e.preventDefault();
287 if (e.which == keys.UP || e.which == keys.DOWN) {
288 // up and down to navigate
289 that._handleUpDownKey(e.which);
290 } else if (e.which == keys.ENTER) {
291 // enter to select
292 that._handleEnterKey();
293 } else if (e.which == keys.ESC) {
294 // esc to close
295 that._closeDropdown();
296 } else if (e.which >= keys.A && e.which <= keys.Z) {
297 // upper case letters (note: keyup/keydown only return upper case letters)
298 // cycle through countries beginning with that letter
299 that._handleLetterKey(e.which);
300 }
301 });
302 },
303 // Highlight the next/prev item in the list (and ensure it is visible)
304 _handleUpDownKey: function(key) {
305 var current = this.countryList.children(".highlight").first();
306 var next = key == keys.UP ? current.prev() : current.next();
307 if (next.length) {
308 // skip the divider
309 if (next.hasClass("divider")) {
310 next = key == keys.UP ? next.prev() : next.next();
311 }
312 this._highlightListItem(next);
313 this._scrollTo(next);
314 }
315 },
316 // select the currently highlighted item
317 _handleEnterKey: function() {
318 var currentCountry = this.countryList.children(".highlight").first();
319 if (currentCountry.length) {
320 this._selectListItem(currentCountry);
321 }
322 },
323 // Iterate through the countries starting with the given letter
324 _handleLetterKey: function(key) {
325 var letter = String.fromCharCode(key);
326 // filter out the countries beginning with that letter
327 var countries = this.countryListItems.filter(function() {
328 return $(this).text().charAt(0) == letter && !$(this).hasClass("preferred");
329 });
330 if (countries.length) {
331 // if one is already highlighted, then we want the next one
332 var highlightedCountry = countries.filter(".highlight").first(), listItem;
333 // if the next country in the list also starts with that letter
334 if (highlightedCountry && highlightedCountry.next() && highlightedCountry.next().text().charAt(0) == letter) {
335 listItem = highlightedCountry.next();
336 } else {
337 listItem = countries.first();
338 }
339 // update highlighting and scroll
340 this._highlightListItem(listItem);
341 this._scrollTo(listItem);
342 }
343 },
344 // Update the selected flag using the input's current value
345 _updateFlagFromInputVal: function() {
346 var that = this;
347 // try and extract valid country from input
348 var value = this.countryInput.val().replace(/(?=[() ])/g, '\\');
349 if (value) {
350 var countryCodes = [];
351 var matcher = new RegExp("^"+value, "i");
352 for (var i = 0; i < this.countries.length; i++) {
353 if (this.countries[i].name.match(matcher)) {
354 countryCodes.push(this.countries[i].iso2);
355 }
356 }
357 // Check if one of the matching countries is already selected
358 var alreadySelected = false;
359 $.each(countryCodes, function(i, c) {
360 if (that.selectedFlagInner.hasClass(c)) {
361 alreadySelected = true;
362 }
363 });
364 if (!alreadySelected) {
365 this._selectFlag(countryCodes[0]);
366 this.countryCodeInput.val(countryCodes[0]).trigger("change");
367 }
368 // Matching country found
369 return true;
370 }
371 // No match found
372 return false;
373 },
374 // remove highlighting from other list items and highlight the given item
375 _highlightListItem: function(listItem) {
376 this.countryListItems.removeClass("highlight");
377 listItem.addClass("highlight");
378 },
379 // find the country data for the given country code
380 // the ignoreOnlyCountriesOption is only used during init() while parsing the onlyCountries array
381 _getCountryData: function(countryCode, ignoreOnlyCountriesOption) {
382 var countryList = ignoreOnlyCountriesOption ? allCountries : this.countries;
383 for (var i = 0; i < countryList.length; i++) {
384 if (countryList[i].iso2 == countryCode) {
385 return countryList[i];
386 }
387 }
388 return null;
389 },
390 // update the selected flag and the active list item
391 _selectFlag: function(countryCode) {
392 if (! countryCode) {
393 return false;
394 }
395 this.selectedFlagInner.attr("class", "flag " + countryCode);
396 // update the title attribute
397 var countryData = this._getCountryData(countryCode);
398 this.selectedFlagInner.parent().attr("title", countryData.name);
399 // update the active list item
400 var listItem = this.countryListItems.children(".flag." + countryCode).first().parent();
401 this.countryListItems.removeClass("active");
402 listItem.addClass("active");
403 },
404 // called when the user selects a list item from the dropdown
405 _selectListItem: function(listItem) {
406 // update selected flag and active list item
407 var countryCode = listItem.attr("data-country-code");
408 this._selectFlag(countryCode);
409 this._closeDropdown();
410 // update input value
411 this._updateName(countryCode);
412 this.countryInput.trigger("change");
413 this.countryCodeInput.trigger("change");
414 // focus the input
415 this._focus();
416 },
417 // close the dropdown and unbind any listeners
418 _closeDropdown: function() {
419 this.countryList.addClass("hide");
420 // update the arrow
421 this.selectedFlagInner.children(".arrow").removeClass("up");
422 // unbind event listeners
423 $(document).off("keydown" + this.ns);
424 $("html").off("click" + this.ns);
425 // unbind both hover and click listeners
426 this.countryList.off(this.ns);
427 },
428 // check if an element is visible within its container, else scroll until it is
429 _scrollTo: function(element) {
430 if (!element || !element.offset()) {
431 return;
432 }
433 var container = this.countryList, containerHeight = container.height(), containerTop = container.offset().top, containerBottom = containerTop + containerHeight, elementHeight = element.outerHeight(), elementTop = element.offset().top, elementBottom = elementTop + elementHeight, newScrollTop = elementTop - containerTop + container.scrollTop();
434 if (elementTop < containerTop) {
435 // scroll up
436 container.scrollTop(newScrollTop);
437 } else if (elementBottom > containerBottom) {
438 // scroll down
439 var heightDifference = containerHeight - elementHeight;
440 container.scrollTop(newScrollTop - heightDifference);
441 }
442 },
443 // Replace any existing country name with the new one
444 _updateName: function(countryCode) {
445 this.countryCodeInput.val(countryCode).trigger("change");
446 this.countryInput.val(this._getCountryData(countryCode).name);
447 },
448 /********************
449 * PUBLIC METHODS
450 ********************/
451 // get the country data for the currently selected flag
452 getSelectedCountryData: function() {
453 // rely on the fact that we only set 2 classes on the selected flag element:
454 // the first is "flag" and the second is the 2-char country code
455 var countryCode = this.selectedFlagInner.attr("class").split(" ")[1];
456 return this._getCountryData(countryCode);
457 },
458 // update the selected flag
459 selectCountry: function(countryCode) {
460 countryCode = countryCode.toLowerCase();
461 // check if already selected
462 if (!this.selectedFlagInner.hasClass(countryCode)) {
463 this._selectFlag(countryCode);
464 this._updateName(countryCode);
465 }
466 },
467 // set the input value and update the flag
468 setCountry: function(country) {
469 this.countryInput.val(country);
470 this._updateFlagFromInputVal();
471 },
472 // remove plugin
473 destroy: function() {
474 // stop listeners
475 this.countryInput.off(this.ns);
476 this.selectedFlagInner.parent().off(this.ns);
477 // remove markup
478 var container = this.countryInput.parent();
479 container.before(this.countryInput).remove();
480 }
481 };
482 // adapted to allow public functions
483 // using https://github.com/jquery-boilerplate/jquery-boilerplate/wiki/Extending-jQuery-Boilerplate
484 $.fn[pluginName] = function(options) {
485 var args = arguments;
486 // Is the first parameter an object (options), or was omitted,
487 // instantiate a new instance of the plugin.
488 if (options === undefined || typeof options === "object") {
489 return this.each(function() {
490 if (!$.data(this, "plugin_" + pluginName)) {
491 $.data(this, "plugin_" + pluginName, new Plugin(this, options));
492 }
493 });
494 } else if (typeof options === "string" && options[0] !== "_" && options !== "init") {
495 // If the first parameter is a string and it doesn't start
496 // with an underscore or "contains" the `init`-function,
497 // treat this as a call to a public method.
498 // Cache the method call to make it possible to return a value
499 var returns;
500 this.each(function() {
501 var instance = $.data(this, "plugin_" + pluginName);
502 // Tests that there's already a plugin-instance
503 // and checks that the requested public method exists
504 if (instance instanceof Plugin && typeof instance[options] === "function") {
505 // Call the method of our plugin instance,
506 // and pass it the supplied arguments.
507 returns = instance[options].apply(instance, Array.prototype.slice.call(args, 1));
508 }
509 // Allow instances to be destroyed via the 'destroy' method
510 if (options === "destroy") {
511 $.data(this, "plugin_" + pluginName, null);
512 }
513 });
514 // If the earlier cached method gives a value back return the value,
515 // otherwise return this to preserve chainability.
516 return returns !== undefined ? returns : this;
517 }
518 };
519 /********************
520 * STATIC METHODS
521 ********************/
522 // get the country data object
523 $.fn[pluginName].getCountryData = function() {
524 return allCountries;
525 };
526 // set the country data object
527 $.fn[pluginName].setCountryData = function(obj) {
528 allCountries = obj;
529 };
530 // Tell JSHint to ignore this warning: "character may get silently deleted by one or more browsers"
531 // jshint -W100
532 // Array of country objects for the flag dropdown.
533 // Each contains a name and country code (ISO 3166-1 alpha-2).
534 //
535 // Note: using single char property names to keep filesize down
536 // n = name
537 // i = iso2 (2-char country code)
538 var ii = 0;
539 var cc = [];
540 var allCountries = $.each(uwp_country_data, function(i, c) {
541 cc[ii] = {name:c,iso2:i};
542 ii++;
543 });
544
545 allCountries = cc;
546 });
547