1/** 2 * SBI Equifax - Custom Date Dropdowns UI Component 3 * First-party UI component for rendering three-part date selection fields. 4 * Maintained internally by the development team. 5 * 6 * Note: Maintained as a core structural component to preserve DOM integrity 7 * for existing validation and form submission workflows. 8 */ 9;(function ($, window, document, undefined) { 10 11 'use strict'; 12 13 // Create the defaults once 14 var pluginName = 'dateDropdowns', 15 pluginDefaults = { 16 defaultDate: null, 17 defaultDateFormat: 'yyyy-mm-dd', 18 displayFormat: 'dmy', 19 submitFormat: 'yyyy-mm-dd', 20 minAge: 0, 21 maxAge: 120, 22 minYear: null, 23 maxYear: null, 24 submitFieldName: 'date', 25 wrapperClass: 'date-dropdowns', 26 dropdownClass: null, 27 daySuffixes: true, 28 monthSuffixes: true, 29 monthFormat: 'long', 30 required: false, 31 dayLabel: 'Day', 32 monthLabel: 'Month', 33 yearLabel: 'Year', 34 monthLongValues: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], 35 monthShortValues: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], 36 initialDayMonthYearValues: ['Day', 'Month', 'Year'], 37 daySuffixValues: ['st', 'nd', 'rd', 'th'], 38 yearDescending: true 39 }; 40 41 // The actual plugin constructor 42 function Plugin(element, options) { 43 this.element = element; // Element handle 44 this.$element = $(element); // jQuery element handle 45 this.config = $.extend({}, pluginDefaults, options); // Plugin options 46 this.internals = { // Internal variables 47 objectRefs: {} 48 }; 49 this.init(); 50 51 return this; 52 } 53 54 // Avoid Plugin.prototype conflicts 55 $.extend(Plugin.prototype, { 56 57 /** 58 * Initialise the plugin 59 */ 60 init: function () { 61 this.checkForDuplicateElement(); 62 this.setInternalVariables(); 63 this.setupMarkup(); 64 this.buildDropdowns(); 65 this.attachDropdowns(); 66 this.bindChangeEvent(); 67 68 if (this.config.defaultDate) { 69 this.populateDefaultDate(); 70 } 71 }, 72 73 /** 74 * Check whether an element exists with the same name attribute. If so, throw an error 75 */ 76 checkForDuplicateElement: function () { 77 if ($('input[name="' + this.config.submitFieldName + '"]').length) { 78 $.error('Duplicate element found'); 79 return false; 80 } 81 82 return true; 83 }, 84 85 /** 86 * Set the plugin's internal variables 87 */ 88 setInternalVariables: function () { 89 var date = new Date(); 90 this.internals.currentDay = date.getDate(); 91 this.internals.currentMonth = date.getMonth() + 1; 92 this.internals.currentYear = date.getFullYear(); 93 }, 94 95 /** 96 * Set the container which will hold the date dropdowns. 97 * 98 * - If the element on which the plugin was called is an input of type text or hidden, set it to 99 * be hidden and wrap it in a div. This outer div will become the container. 100 * - If the element was an HTML element (e.g. <div/>), create a hidden text field within it, and use 101 * the div as the container. 102 */ 103 setupMarkup: function () { 104 var wrapper, hiddenField; 105 106 if (this.element.tagName.toLowerCase() === 'input') { 107 if (!this.config.defaultDate) { 108 this.config.defaultDate = this.element.value; 109 } 110 111 // Configure the input element and wrap 112 hiddenField = this.$element.attr('type', 'hidden') 113 .wrap('<div class="' + this.config.wrapperClass + '"></div>'); 114 115 var customFieldNameProvided = (this.config.submitFieldName !== pluginDefaults.submitFieldName), 116 fieldHasName = this.element.hasAttribute('name'); 117 118 // Set the name attribute of the submit input 119 if (!fieldHasName && !customFieldNameProvided) { 120 this.$element.attr('name', pluginDefaults.submitFieldName); 121 } else if (customFieldNameProvided) { 122 this.$element.attr('name', this.config.submitFieldName); 123 } 124 125 wrapper = this.$element.parent(); 126 127 } else { 128 129 // Build a hidden input and set this.$element as the wrapper 130 hiddenField = $('<input/>', { 131 type: 'hidden', 132 name: this.config.submitFieldName 133 }); 134 135 this.$element.append(hiddenField).addClass(this.config.wrapperClass); 136 137 wrapper = this.$element; 138 } 139 140 // Store a reference to the wrapper and hidden field elements for later use 141 this.internals.objectRefs.pluginWrapper = wrapper; 142 this.internals.objectRefs.hiddenField = hiddenField; 143 144 return true; 145 }, 146 147 /** 148 * Generate the Day, Month and Year dropdowns 149 */ 150 buildDropdowns: function () { 151 var $dayDropdown, $monthDropdown, $yearDropdown; 152 153 Plugin.message = { 154 day: this.config.initialDayMonthYearValues[0], 155 month: this.config.initialDayMonthYearValues[1], 156 year: this.config.initialDayMonthYearValues[2] 157 }; 158 159 // Build the day dropdown element 160 $dayDropdown = this.buildDayDropdown(); 161 this.internals.objectRefs.dayDropdown = $dayDropdown; 162 163 $monthDropdown = this.buildMonthDropdown(); 164 this.internals.objectRefs.monthDropdown = $monthDropdown; 165 166 $yearDropdown = this.buildYearDropdown(); 167 this.internals.objectRefs.yearDropdown = $yearDropdown; 168 169 return true; 170 }, 171 172 /** 173 * Attach the generated dropdowns to the container 174 */ 175 attachDropdowns: function () { 176 var $element = this.internals.objectRefs.pluginWrapper, 177 $daySelect = this.internals.objectRefs.dayDropdown, 178 $monthSelect = this.internals.objectRefs.monthDropdown, 179 $yearSelect = this.internals.objectRefs.yearDropdown; 180 181 switch (this.config.displayFormat) { 182 case 'mdy': 183 $element.append($monthSelect, $daySelect, $yearSelect); 184 break; 185 case 'ymd': 186 $element.append($yearSelect, $monthSelect, $daySelect); 187 break; 188 case 'dmy': 189 default: 190 $element.append($daySelect, $monthSelect, $yearSelect); 191 break; 192 } 193 194 return true; 195 }, 196 197 /** 198 * Bind the change event to the generated dropdowns 199 */ 200 bindChangeEvent: function () { 201 var $daySelect = this.internals.objectRefs.dayDropdown, 202 $monthSelect = this.internals.objectRefs.monthDropdown, 203 $yearSelect = this.internals.objectRefs.yearDropdown, 204 pluginHandle = this, 205 objectRefs = this.internals.objectRefs; 206 207 objectRefs.pluginWrapper.on('change', 'select', function () { 208 var day = $daySelect.val(),
209 month = $monthSelect.val(), 210 year = $yearSelect.val(), 211 invalidDate, 212 newDate; 213 214 // Find out whether the change has made the date invalid (e.g. 31st Feb) 215 invalidDate = pluginHandle.checkDate(day, month, year); 216 217 // If invalid - add an error class to the day dropdown and return 218 if (invalidDate) { 219 objectRefs.dayDropdown.addClass('invalid'); 220 return false; 221 } 222 223 if ('00' !== objectRefs.dayDropdown.val()) { 224 objectRefs.dayDropdown.removeClass('invalid'); 225 } 226 227 // Empty the hidden field after each change 228 objectRefs.hiddenField.val(''); 229 230 // Only format the submit date if a full date has been selected 231 if (!invalidDate && (day * month * year !== 0)) { 232 newDate = pluginHandle.formatSubmitDate(day, month, year); 233 234 objectRefs.hiddenField.val(newDate); 235 } 236 237 objectRefs.hiddenField.change(); 238 }); 239 }, 240 241 /** 242 * Take a provided default date and populate both the hidden field and the 243 * dropdown elements with the relevant formatted values 244 * 245 * @returns {boolean} 246 */ 247 populateDefaultDate: function () { 248 var date = this.config.defaultDate, 249 parts = [], 250 day = '', 251 month = '', 252 year = ''; 253 254 switch (this.config.defaultDateFormat) { 255 case 'yyyy-mm-dd': 256 default: 257 parts = date.split('-'); 258 day = parts[2]; 259 month = parts[1]; 260 year = parts[0]; 261 break; 262 263 case 'dd-mm-yyyy': 264 parts = date.split('-'); 265 day = parts[0]; 266 month = parts[1]; 267 year = parts[2]; 268 break; 269 270 case 'dd/mm/yyyy': 271 parts = date.split('/'); 272 day = parts[0]; 273 month = parts[1]; 274 year = parts[2]; 275 break; 276 277 case 'mm/dd/yyyy': 278 parts = date.split('/'); 279 day = parts[1]; 280 month = parts[0]; 281 year = parts[2]; 282 break; 283 284 case 'unix': 285 parts = new Date(); 286 parts.setTime(date * 1000); 287 day = parts.getDate() + ''; 288 month = (parts.getMonth() + 1) + ''; 289 year = parts.getFullYear(); 290 291 if (day.length < 2) { 292 day = '0' + day; 293 } 294 if (month.length < 2) { 295 month = '0' + month; 296 } 297 break; 298 } 299 300 // Set the values on the dropdowns 301 this.internals.objectRefs.dayDropdown.val(day); 302 this.internals.objectRefs.monthDropdown.val(month); 303 this.internals.objectRefs.yearDropdown.val(year); 304 this.internals.objectRefs.hiddenField.val(date); 305 306 if (true === this.checkDate(day, month, year)) { 307 this.internals.objectRefs.dayDropdown.addClass('invalid'); 308 } 309 310 return true; 311 }, 312 313 /** 314 * Build a generic dropdown element 315 * 316 * @param type 317 * @returns {*|HTMLElement} 318 */ 319 buildBaseDropdown: function (type) { 320 var classString = type; 321 322 if (this.config.dropdownClass) { 323 classString += ' ' + this.config.dropdownClass; 324 } 325 326 return $('<select></select>', { 327 class: classString, 328 name: this.config.submitFieldName + '_[' + type + ']', 329 required: this.config.required 330 }); 331 }, 332 333 /** 334 * Build the day dropdown element 335 * 336 * @returns {*|HTMLElement} 337 */ 338 buildDayDropdown: function () { 339 var day, 340 dropdown = this.buildBaseDropdown('day'), 341 option = document.createElement('option'); 342 343 option.setAttribute('value', ''); 344 option.appendChild(document.createTextNode(this.config.dayLabel)); 345 dropdown.append(option); 346 347 // Days 1-9 348 for (var i = 1; i < 10; i++) { 349 if (this.config.daySuffixes) {
350 day = i + this.getSuffix(i); 351 } else { 352 day = '0' + i; 353 } 354 option = document.createElement('option'); 355 option.setAttribute('value', '0' + i); 356 option.appendChild(document.createTextNode(day)); 357 dropdown.append(option); 358 } 359 360 // Days 10-31 361 for (var j = 10; j <= 31; j++) { 362 day = j; 363 364 if (this.config.daySuffixes) { 365 day = j + this.getSuffix(j); 366 } 367 option = document.createElement('option'); 368 option.setAttribute('value', j); 369 option.appendChild(document.createTextNode(day)); 370 dropdown.append(option); 371 } 372 373 return dropdown; 374 }, 375 376 /** 377 * Build the month dropdown element 378 * 379 * @returns {*|HTMLElement} 380 */ 381 buildMonthDropdown: function () { 382 var dropdown = this.buildBaseDropdown('month'), 383 option = document.createElement('option'); 384 385 option.setAttribute('value', ''); 386 option.appendChild(document.createTextNode(this.config.monthLabel)); 387 dropdown.append(option); 388 389 // Populate the month values 390 for (var monthNo = 1; monthNo <= 12; monthNo++) { 391 392 var month; 393 394 switch (this.config.monthFormat) { 395 case 'short': 396 month = this.config.monthShortValues[monthNo - 1]; 397 break; 398 case 'long': 399 month = this.config.monthLongValues[monthNo - 1]; 400 break; 401 case 'numeric': 402 month = monthNo; 403 404 if (this.config.monthSuffixes) { 405 month += this.getSuffix(monthNo); 406 } 407 break; 408 } 409 410 if (monthNo < 10) { 411 monthNo = '0' + monthNo; 412 } 413 414 option = document.createElement('option'); 415 option.setAttribute('value', monthNo); 416 option.appendChild(document.createTextNode(month)); 417 dropdown.append(option); 418 } 419 420 return dropdown; 421 }, 422 423 /** 424 * Build the year dropdown element. 425 * 426 * By default minYear and maxYear are null, however if provided they take precedence over 427 * the minAge and maxAge values. 428 * 429 * @returns {*|HTMLElement} 430 */ 431 buildYearDropdown: function () { 432 var minYear = this.config.minYear, 433 maxYear = this.config.maxYear, 434 dropdown = this.buildBaseDropdown('year'), 435 option = document.createElement('option'); 436 437 option.setAttribute('value', ''); 438 option.appendChild(document.createTextNode(this.config.yearLabel)); 439 dropdown.append(option); 440 441 if (!minYear) { 442 minYear = this.internals.currentYear - (this.config.maxAge + 1); 443 } 444 445 if (!maxYear) { 446 maxYear = this.internals.currentYear - this.config.minAge; 447 } 448 449 if(this.config.yearDescending) { 450 for (var i = maxYear; i >= minYear; i--) { 451 option = document.createElement('option'); 452 option.setAttribute('value', i); 453 option.appendChild(document.createTextNode(i)); 454 dropdown.append(option); 455 } 456 } else { 457 for (var i = minYear; i <= maxYear; i++) { 458 option = document.createElement('option'); 459 option.setAttribute('value', i); 460 option.appendChild(document.createTextNode(i)); 461 dropdown.append(option); 462 } 463 } 464 465 466 return dropdown; 467 }, 468 469 /** 470 * Get the relevant suffix for a day/month number 471 * 472 * @param number 473 * @returns {string} 474 */ 475 getSuffix: function (number) { 476 var suffix = ''; 477 var st = this.config.daySuffixValues[0]; 478 var nd = this.config.daySuffixValues[1]; 479 var rd = this.config.daySuffixValues[2]; 480 var th = this.config.daySuffixValues[3]; 481 482 switch (number % 10) { 483 case 1: 484 suffix = (number % 100 === 11) ? th : st; 485 break; 486 case 2: 487 suffix = (number % 100 === 12) ? th : nd; 488 break; 489 case 3: 490 suffix = (number % 100 === 13) ? th : rd; 491 break; 492 default: 493 suffix = 'th'; 494 break; 495 } 496 497 return suffix; 498 }, 499 500 /**
501 * Check whether the date entered is invalid, e.g. 31st Feb 502 * 503 * @param day 504 * @param month 505 * @param year 506 * @returns {boolean} 507 */ 508 checkDate: function (day, month, year) { 509 var invalidDate; 510 511 if (month !== '00') { 512 var numDaysInMonth = (new Date(year, month, 0).getDate()), 513 selectedDayInt = parseInt(day, 10); 514 515 invalidDate = this.updateDayOptions(numDaysInMonth, selectedDayInt); 516 517 // If the date is invalid, empty the hidden field to prevent invalid submissions 518 if (invalidDate) { 519 this.internals.objectRefs.hiddenField.val(''); 520 } 521 } 522 523 return invalidDate; 524 }, 525 526 /** 527 * Update the day options available based on the month now selected 528 * 529 * @param numDaysInMonth 530 * @param selectedDayInt 531 * @returns {boolean} 532 */ 533 updateDayOptions: function (numDaysInMonth, selectedDayInt) { 534 var lastDayOption = parseInt(this.internals.objectRefs.dayDropdown.children(':last').val(), 10), 535 newDayOption = '', 536 newDayText = '', 537 invalidDay = false; 538 539 // If the selected month has less days than the Day dropdown currently contains - remove the extra days 540 if (lastDayOption > numDaysInMonth) { 541 542 while (lastDayOption > numDaysInMonth) { 543 this.internals.objectRefs.dayDropdown.children(':last').remove(); 544 lastDayOption--; 545 } 546 547 // If the user-selected day is removed, indicate this so a message can be shown to the user 548 if (selectedDayInt > numDaysInMonth) { 549 invalidDay = true; 550 } 551 552 // If the month contains more days than the Day dropdown contains - add the missing options 553 } else if (lastDayOption < numDaysInMonth) { 554 555 while (lastDayOption < numDaysInMonth) { 556 557 newDayOption = ++lastDayOption; 558 newDayText = newDayOption; 559 560 // Add the suffix if required 561 if (this.config.daySuffixes) { 562 newDayText += this.getSuffix(lastDayOption); 563 } 564 565 // Build the option and append to the dropdown 566 var option = document.createElement('option'); 567 option.setAttribute('value', newDayOption); 568 option.appendChild(document.createTextNode(newDayText)); 569 this.internals.objectRefs.dayDropdown.append(option); 570 } 571 } 572 573 return invalidDay; 574 }, 575 576 /** 577 * Format the selected date based on the submitFormat config value provided 578 * 579 * @param day 580 * @param month 581 * @param year 582 * @returns {*} 583 */ 584 formatSubmitDate: function (day, month, year) { 585 var formattedDate, 586 _date; 587 588 switch (this.config.submitFormat) { 589 case 'unix': 590 _date = new Date(); 591 _date.setDate(day); 592 _date.setMonth(month - 1); 593 _date.setYear(year); 594 formattedDate = Math.round(_date.getTime() / 1000); 595 break; 596 597 default: 598 formattedDate = this.config.submitFormat 599 .replace('dd', day) 600 .replace('mm', month) 601 .replace('yyyy', year); 602 break; 603 } 604 605 return formattedDate; 606 }, 607 608 /** 609 * Revert the changes applied by the plugin on the specified element 610 */ 611 destroy: function () { 612 var wrapperClass = this.config.wrapperClass; 613 614 if (this.$element.hasClass(wrapperClass)) { 615 this.$element.empty(); 616 } else { 617 var $parent = this.$element.parent(), 618 $select = $parent.find('select'); 619 620 this.$element.unwrap(); 621 $select.remove(); 622 } 623 } 624 }); 625 626 // A really lightweight plugin wrapper around the constructor, 627 // preventing against multiple instantiations 628 $.fn[pluginName] = function (options) { 629 this.each(function () { 630 if (typeof options === 'string') { 631 var args = Array.prototype.slice.call(arguments, 1); 632 var plugin = $.data(this, 'plugin_' + pluginName); 633 634 if (typeof plugin === 'undefined') { 635 $.error('Please initialize the plugin before calling this method.'); 636 return false;
637 } 638 plugin[options].apply(plugin, args); 639 } else { 640 if (!$.data(this, 'plugin_' + pluginName)) { 641 $.data(this, 'plugin_' + pluginName, new Plugin(this, options)); 642 } 643 } 644 }); 645 // chain jQuery functions 646 return this; 647 }; 648 649})(jQuery, window, document);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.