1/*! 2 * jQuery UI date range picker widget 3 * Copyright (c) 2017 Tamble, Inc. 4 * Licensed under MIT (https://github.com/tamble/jquery-ui-daterangepicker/raw/master/LICENSE.txt) 5 * 6 * Depends: 7 * - jQuery 1.8.3+ 8 * - jQuery UI 1.9.0+ (widget factory, position utility, button, menu, datepicker) 9 * - moment.js 2.3.0+ 10 */ 11 12(function($, window, undefined) { 13 14 var uniqueId = 0; // used for unique ID generation within multiple plugin instances 15 16 $.widget('comiseo.daterangepicker', { 17 version: '0.6.0-beta.1', 18 19 options: { 20 // presetRanges: array of objects; each object describes an item in the presets menu 21 // and must have the properties: text, dateStart, dateEnd. 22 // dateStart, dateEnd are functions returning a moment object 23 presetRanges: [ 24 {text: 'Today', dateStart: function() { return moment() }, dateEnd: function() { return moment() } }, 25 {text: 'Yesterday', dateStart: function() { return moment().subtract('days', 1) }, dateEnd: function() { return moment().subtract('days', 1) } }, 26 {text: 'Last 7 Days', dateStart: function() { return moment().subtract('days', 6) }, dateEnd: function() { return moment() } }, 27 {text: 'Last Week (Mo-Su)', dateStart: function() { return moment().subtract('days', 7).isoWeekday(1) }, dateEnd: function() { return moment().subtract('days', 7).isoWeekday(7) } }, 28 {text: 'Month to Date', dateStart: function() { return moment().startOf('month') }, dateEnd: function() { return moment() } }, 29 {text: 'Previous Month', dateStart: function() { return moment().subtract('month', 1).startOf('month') }, dateEnd: function() { return moment().subtract('month', 1).endOf('month') } }, 30 {text: 'Year to Date', dateStart: function() { return moment().startOf('year') }, dateEnd: function() { return moment() } } 31 ], 32 initialText: 'Select date range...', // placeholder text - shown when nothing is selected 33 icon: 'ui-icon-triangle-1-s', 34 applyButtonText: 'Apply', // use '' to get rid of the button 35 clearButtonText: 'Clear', // use '' to get rid of the button 36 cancelButtonText: 'Cancel', // use '' to get rid of the button 37 rangeSplitter: ' - ', // string to use between dates 38 dateFormat: 'M d, yy', // displayed date format. Available formats: http://api.jqueryui.com/datepicker/#utility-formatDate 39 altFormat: 'yy-mm-dd', // submitted date format - inside JSON {"start":"...","end":"..."} 40 verticalOffset: 0, // offset of the dropdown relative to the closest edge of the trigger button 41 mirrorOnCollision: true, // reverse layout when there is not enough space on the right 42 autoFitCalendars: true, // override datepicker's numberOfMonths option in order to fit widget width 43 applyOnMenuSelect: true, // whether to auto apply menu selections 44 open: null, // callback that executes when the dropdown opens 45 close: null, // callback that executes when the dropdown closes 46 change: null, // callback that executes when the date range changes 47 clear: null, // callback that executes when the clear button is used 48 cancel: null, // callback that executes when the cancel button is used 49 onOpen: null, // @deprecated callback that executes when the dropdown opens 50 onClose: null, // @deprecated callback that executes when the dropdown closes 51 onChange: null, // @deprecated callback that executes when the date range changes 52 onClear: null, // @deprecated callback that executes when the clear button is used 53 datepickerOptions: { // object containing datepicker options. See http://api.jqueryui.com/datepicker/#options 54 numberOfMonths: 3, 55// showCurrentAtPos: 1 // bug; use maxDate instead 56 maxDate: 0 // the maximum selectable date is today (also current month is displayed on the last position) 57 } 58 }, 59 60 _create: function() { 61 this._dateRangePicker = buildDateRangePicker(this.element, this, this.options); 62 }, 63 64 _destroy: function() { 65 this._dateRangePicker.destroy(); 66 }, 67 68 _setOptions: function(options) { 69 this._super(options); 70 this._dateRangePicker.enforceOptions(); 71 }, 72 73 open: function() { 74 this._dateRangePicker.open(); 75 }, 76 77 close: function() { 78 this._dateRangePicker.close(); 79 }, 80 81 setRange: function(range) { 82 this._dateRangePicker.setRange(range); 83 }, 84 85 getRange: function() { 86 return this._dateRangePicker.getRange(); 87 }, 88 89 clearRange: function() { 90 this._dateRangePicker.clearRange(); 91 }, 92 93 widget: function() { 94 return this._dateRangePicker.getContainer(); 95 } 96 }); 97 98 /** 99 * factory for the trigger button (which visually replaces the original input form element) 100 * 101 * @param {jQuery}
101 $originalElement jQuery object containing the input form element used to instantiate this widget instance 102 * @param {String} classnameContext classname of the parent container 103 * @param {Object} options 104 */ 105 function buildTriggerButton($originalElement, classnameContext, options) { 106 var $self, id; 107 108 function fixReferences() { 109 id = 'drp_autogen' + uniqueId++; 110 $('label[for="' + $originalElement.attr('id') + '"]') 111 .attr('for', id); 112 } 113 function fixButton() { 114 if ($.fn.button.noConflict) { 115 var btn = $.fn.button.noConflict(); // reverts $.fn.button to jqueryui btn 116 $.fn.btn = btn; // assigns bootstrap button functionality to $.fn.btn 117 } 118 } 119 120 function init() { 121 fixReferences(); 122 fixButton(); 123 $self = $('<button type="button"></button>') 124 .addClass(classnameContext + '-triggerbutton') 125 .attr({'title': $originalElement.attr('title'), 'tabindex': $originalElement.attr('tabindex'), id: id}) 126 .button({ 127 icons: { 128 secondary: options.icon 129 }, 130 icon: options.icon, 131 iconPosition: 'end', 132 label: options.initialText 133 }); 134 } 135 136 function getLabel() { 137 return $self.button('option', 'label'); 138 } 139 140 function setLabel(value) { 141 $self.button('option', 'label', value); 142 } 143 144 function reset() { 145 $originalElement.val('').change(); 146 setLabel(options.initialText); 147 } 148 149 function enforceOptions() { 150 $self.button('option', { 151 icons: { 152 secondary: options.icon 153 }, 154 icon: options.icon, 155 iconPosition: 'end', 156 label: options.initialText 157 }); 158 } 159 160 init(); 161 return { 162 getElement: function() { return $self; }, 163 getLabel: getLabel, 164 setLabel: setLabel, 165 reset: reset, 166 enforceOptions: enforceOptions 167 }; 168 } 169 170 /** 171 * factory for the presets menu (containing built-in date ranges) 172 * 173 * @param {String} classnameContext classname of the parent container 174 * @param {Object} options 175 * @param {Function} onClick callback that executes when a preset is clicked 176 */ 177 function buildPresetsMenu(classnameContext, options, onClick) { 178 var $self, 179 $menu, 180 menuItemWrapper; 181 182 function init() {
183 $self = $('<div></div>') 184 .addClass(classnameContext + '-presets'); 185 186 $menu = $('<ul></ul>'); 187 188 if ($.ui.menu.prototype.options.items === undefined) { 189 menuItemWrapper = {start: '<li><a href="#">', end: '</a></li>'}; 190 } else { 191 menuItemWrapper = {start: '<li><div>', end: '</div></li>'}; 192 } 193 194 $.each(options.presetRanges, function() { 195 $(menuItemWrapper.start + this.text + menuItemWrapper.end) 196 .data('dateStart', this.dateStart) 197 .data('dateEnd', this.dateEnd) 198 .click(onClick) 199 .appendTo($menu); 200 }); 201 202 $self.append($menu); 203 204 $menu.menu() 205 .data('ui-menu').delay = 0; // disable submenu delays 206 } 207 208 init(); 209 return { 210 getElement: function() { return $self; } 211 }; 212 } 213 214 /** 215 * factory for the multiple month date picker 216 * 217 * @param {String} classnameContext classname of the parent container 218 * @param {Object} options 219 */ 220 function buildCalendar(classnameContext, options) { 221 var $self, 222 range = {start: null, end: null}; // selected range 223 224 function init() { 225 $self = $('<
225div></div>', {'class': classnameContext + '-calendar ui-widget-content'}); 226 227 $self.datepicker($.extend({}, options.datepickerOptions, {beforeShowDay: beforeShowDay, onSelect: onSelectDay})); 228 updateAtMidnight(); 229 } 230 231 function enforceOptions() { 232 $self.datepicker('option', $.extend({}, options.datepickerOptions, {beforeShowDay: beforeShowDay, onSelect: onSelectDay})); 233 } 234 235 // called when a day is selected 236 function onSelectDay(dateText, instance) { 237 var dateFormat = options.datepickerOptions.dateFormat || $.datepicker._defaults.dateFormat, 238 selectedDate = $.datepicker.parseDate(dateFormat, dateText); 239 240 if (!range.start || range.end) { // start not set, or both already set 241 range.start = selectedDate; 242 range.end = null; 243 } else if (selectedDate < range.start) { // start set, but selected date is earlier 244 range.end = range.start; 245 range.start = selectedDate; 246 } else { 247 range.end = selectedDate; 248 } 249 if (options.datepickerOptions.hasOwnProperty('onSelect')) { 250 options.datepickerOptions.onSelect(dateText, instance); 251 } 252 } 253 254 // called for each day in the datepicker before it is displayed 255 function beforeShowDay(date) { 256 var result = [ 257 true, // selectable 258 range.start && ((+date === +range.start) || (range.end && range.start <= date && date <= range.end)) ? 'ui-state-highlight' : '' // class to be added 259 ], 260 userResult = [true, '', '']; 261 262 if (options.datepickerOptions.hasOwnProperty('beforeShowDay')) { 263 userResult = options.datepickerOptions.beforeShowDay(date); 264 } 265 return [ 266 result[0] && userResult[0], 267 result[1] + ' ' + userResult[1], 268 userResult[2] 269 ]; 270 } 271 272 function updateAtMidnight() { 273 setTimeout(function() { 274 refresh(); 275 updateAtMidnight(); 276 }, moment().endOf('day') - moment()); 277 } 278 279 function scrollToRangeStart() { 280 if (range.start) { 281 $self.datepicker('setDate', range.start); 282 } 283 } 284 285 function refresh() { 286 $self.datepicker('refresh'); 287 $self.datepicker('setDate', null); // clear the selected date 288 } 289 290 function reset() { 291 range = {start: null, end: null}; 292 refresh(); 293 } 294 295 init(); 296 return { 297 getElement: function() { return $self; }, 298 scrollToRangeStart: function() { return scrollToRangeStart(); }, 299 getRange: function() { return range; }, 300 setRange: function(value) { range = value; refresh(); }, 301 refresh: refresh, 302 reset: reset, 303 enforceOptions: enforceOptions 304 }; 305 } 306 307 /** 308 * factory for the button panel 309 * 310 * @param {String} classnameContext classname of the parent container 311 * @param {Object} options 312 * @param {Object} handlers contains callbacks for each button 313 */ 314 function buildButtonPanel(classnameContext, options, handlers) { 315 var $self, 316 applyButton, 317 clearButton, 318 cancelButton; 319 320 function init() {
321 $self = $('<div></div>') 322 .addClass(classnameContext + '-buttonpanel'); 323 324 if (options.applyButtonText) { 325 applyButton = $('<button type="button" class="ui-priority-primary"></button>') 326 .text(options.applyButtonText) 327 .button(); 328 329 $self.append(applyButton); 330 } 331 332 if (options.clearButtonText) { 333 clearButton = $('<button type="button" class="ui-priority-secondary"></button>') 334 .text(options.clearButtonText) 335 .button(); 336 337 $self.append(clearButton); 338 } 339 340 if (options.cancelButtonText) { 341 cancelButton = $('<button type="button" class="ui-priority-secondary"></button>') 342 .text(options.cancelButtonText) 343 .button(); 344 345 $self.append(cancelButton); 346 } 347 348 bindEvents(); 349 } 350 351 function enforceOptions() { 352 if (applyButton) { 353 applyButton.button('option', 'label', options.applyButtonText); 354 } 355 356 if (clearButton) { 357 clearButton.button('option', 'label', options.clearButtonText); 358 } 359 360 if (cancelButton) { 361 cancelButton.button('option', 'label', options.cancelButtonText); 362 } 363 } 364 365 function bindEvents() { 366 if (handlers) { 367 if (applyButton) { 368 applyButton.click(handlers.onApply); 369 } 370 371 if (clearButton) { 372 clearButton.click(handlers.onClear); 373 } 374 375 if (cancelButton) { 376 cancelButton.click(handlers.onCancel); 377 } 378 } 379 } 380 381 init(); 382 return { 383 getElement: function() { return $self; }, 384 enforceOptions: enforceOptions 385 }; 386 } 387 388 /** 389 * factory for the widget 390 * 391 * @param {jQuery} $originalElement jQuery object containing the input form element used to instantiate this widget instance 392 * @param {Object} instance 393 * @param {Object} options 394 */ 395 function buildDateRangePicker($originalElement, instance, options) { 396 var classname = 'comiseo-daterangepicker', 397 $container, // the dropdown 398 $mask, // ui helper (z-index fix) 399 triggerButton, 400 presetsMenu, 401 calendar, 402 buttonPanel, 403 isOpen = false, 404 autoFitNeeded = false, 405 LEFT = 0, 406 RIGHT = 1, 407 TOP = 2, 408 BOTTOM = 3, 409 sides = ['left', 'right', 'top', 'bottom'], 410 hSide = RIGHT, // initialized to pick layout styles from CSS 411 vSide = null; 412 413 function init() { 414 triggerButton = buildTriggerButton($originalElement, classname, options); 415 presetsMenu = buildPresetsMenu(classname, options, usePreset); 416 calendar = buildCalendar(classname, options); 417 autoFit.numberOfMonths = options.datepickerOptions.numberOfMonths; // save initial option! 418 if (autoFit.numberOfMonths instanceof Array) { // not implemented 419 options.autoFitCalendars = false; 420 } 421 buttonPanel = buildButtonPanel(classname, options, { 422 onApply: function (event) { 423 close(event); 424 setRange(null, event); 425 }, 426 onClear: function (event) { 427 close(event); 428 clearRange(event); 429 }, 430 onCancel: function (event) { 431 instance._trigger('cancel', event, {instance: instance}); 432 close(event); 433 reset(); 434 } 435 }); 436 render(); 437 autoFit(); 438 reset(); 439 bindEvents(); 440 } 441 442 function render() {
443 $container = $('<div></div>', {'class': classname + ' ' + classname + '-' + sides[hSide] + ' ui-widget ui-widget-content ui-corner-all ui-front'}) 444 .append($('<div></div>', {'class': classname + '-main ui-widget-content'}) 445 .append(presetsMenu.getElement()) 446 .append(calendar.getElement())) 447 .append($('<div class="ui-helper-clearfix"></div>') 448 .append(buttonPanel.getElement())) 449 .hide(); 450 $originalElement.hide().after(triggerButton.getElement()); 451 $mask = $('<div></div>', {'class': 'ui-front ' + classname + '-mask'}).hide(); 452 $('body').append($mask).append($container); 453 } 454 455 // auto adjusts the number of months in the date picker 456 function autoFit() { 457 if (options.autoFitCalendars) { 458 var maxWidth = $(window).width(), 459 initialWidth = $container.outerWidth(true), 460 $calendar = calendar.getElement(), 461 numberOfMonths = $calendar.datepicker('option', 'numberOfMonths'), 462 initialNumberOfMonths = numberOfMonths; 463 464 if (initialWidth > maxWidth) { 465 while (numberOfMonths > 1 && $container.outerWidth(true) > maxWidth) { 466 $calendar.datepicker('option', 'numberOfMonths', --numberOfMonths); 467 } 468 if (numberOfMonths !== initialNumberOfMonths) { 469 autoFit.monthWidth = (initialWidth - $container.outerWidth(true)) / (initialNumberOfMonths - numberOfMonths); 470 } 471 } else { 472 while (numberOfMonths < autoFit.numberOfMonths && (maxWidth - $container.outerWidth(true)) >= autoFit.monthWidth) { 473 $calendar.datepicker('option', 'numberOfMonths', ++numberOfMonths); 474 } 475 } 476 reposition(); 477 autoFitNeeded = false; 478 } 479 } 480 481 function destroy() { 482 $container.remove(); 483 triggerButton.getElement().remove(); 484 $originalElement.show(); 485 } 486 487 function bindEvents() { 488 triggerButton.getElement().click(toggle); 489 triggerButton.getElement().keydown(keyPressTriggerOpenOrClose); 490 $mask.click(function(event) { 491 close(event); 492 reset(); 493 }); 494 $(window).resize(function() { isOpen ? autoFit() : autoFitNeeded = true; }); 495 } 496 497 function formatRangeForDisplay(range) { 498 var dateFormat = options.dateFormat; 499 return $.datepicker.formatDate(dateFormat, range.start) + (+range.end !== +range.start ? options.rangeSplitter + $.datepicker.formatDate(dateFormat, range.end) : ''); 500 } 501 502 // formats a date range as JSON 503 function formatRange(range) { 504 var dateFormat = options.altFormat, 505 formattedRange = {}; 506 formattedRange.start = $.datepicker.formatDate(dateFormat, range.start); 507 formattedRange.end = $.datepicker.formatDate(dateFormat, range.end); 508 return JSON.stringify(formattedRange); 509 } 510 511 // parses a date range in JSON format 512 function parseRange(text) { 513 var dateFormat = options.altFormat, 514 range = null; 515 if (text) { 516 try { 517 range = JSON.parse(text, function(key, value) { 518 return key ? $.datepicker.parseDate(dateFormat, value) : value; 519 }); 520 } catch (e) { 521 } 522 } 523 return range; 524 } 525 526 function reset() { 527 var range = getRange(); 528 if (range) { 529 triggerButton.setLabel(formatRangeForDisplay(range)); 530 calendar.setRange(range); 531 } else { 532 calendar.reset(); 533 } 534 } 535 536 function setRange(value, event) { 537 var range = value || calendar.getRange(); 538 if (!range.start) { 539 return; 540 } 541 if (!range.end) { 542 range.end = range.start; 543 } 544 value && calendar.setRange(range); 545 triggerButton.setLabel(formatRangeForDisplay(range)); 546 $originalElement.val(formatRange(range)).change(); 547 if (options.onChange) { 548 options.onChange(); 549 } 550 instance._trigger('change', event, {instance: instance}); 551 } 552 553 function getRange() { 554 return parseRange($originalElement.val()); 555 } 556 557 function clearRange(event) { 558 triggerButton.reset(); 559 calendar.reset(); 560 if (options.onClear) { 561 options.onClear(); 562 } 563 instance._trigger('clear', event, {instance: instance}); 564 } 565 566 // callback - used when the user clicks a preset range 567 function usePreset(event) { 568 var $this = $(this), 569 start = $this.data('dateStart')().startOf('day').toDate(), 570 end = $this.data('dateEnd')().startOf('day').toDate(); 571 calendar.setRange({ start: start, end: end }); 572 if (options.applyOnMenuSelect) { 573 close(event); 574 setRange(null, event); 575 } 576 return false;
577 } 578 579 // adjusts dropdown's position taking into account the available space 580 function reposition() { 581 $container.position({ 582 my: 'left top', 583 at: 'left bottom' + (options.verticalOffset < 0 ? options.verticalOffset : '+' + options.verticalOffset), 584 of: triggerButton.getElement(), 585 collision : 'flipfit flipfit', 586 using: function(coords, feedback) { 587 var containerCenterX = feedback.element.left + feedback.element.width / 2, 588 triggerButtonCenterX = feedback.target.left + feedback.target.width / 2, 589 prevHSide = hSide, 590 last, 591 containerCenterY = feedback.element.top + feedback.element.height / 2, 592 triggerButtonCenterY = feedback.target.top + feedback.target.height / 2, 593 prevVSide = vSide, 594 vFit; // is the container fit vertically 595 596 hSide = (containerCenterX > triggerButtonCenterX) ? RIGHT : LEFT; 597 if (hSide !== prevHSide) { 598 if (options.mirrorOnCollision) { 599 last = (hSide === LEFT) ? presetsMenu : calendar; 600 $container.children().first().append(last.getElement()); 601 } 602 $container.removeClass(classname + '-' + sides[prevHSide]); 603 $container.addClass(classname + '-' + sides[hSide]); 604 } 605 $container.css({ 606 left: coords.left, 607 top: coords.top 608 }); 609 610 vSide = (containerCenterY > triggerButtonCenterY) ? BOTTOM : TOP; 611 if (vSide !== prevVSide) { 612 if (prevVSide !== null) { 613 triggerButton.getElement().removeClass(classname + '-' + sides[prevVSide]); 614 } 615 triggerButton.getElement().addClass(classname + '-' + sides[vSide]); 616 } 617 vFit = vSide === BOTTOM && feedback.element.top - feedback.target.top !== feedback.target.height + options.verticalOffset 618 || vSide === TOP && feedback.target.top - feedback.element.top !== feedback.element.height + options.verticalOffset; 619 triggerButton.getElement().toggleClass(classname + '-vfit', vFit); 620 } 621 }); 622 } 623 624 function killEvent(event) { 625 event.preventDefault(); 626 event.stopPropagation(); 627 } 628 629 function keyPressTriggerOpenOrClose(event) { 630 switch (event.which) { 631 case $.ui.keyCode.UP: 632 case $.ui.keyCode.DOWN: 633 killEvent(event); 634 open(event); 635 return; 636 case $.ui.keyCode.ESCAPE: 637 killEvent(event); 638 close(event); 639 reset(); 640 return; 641 case $.ui.keyCode.TAB: 642 close(event); 643 return; 644 } 645 } 646 647 function open(event) { 648 if (!isOpen) { 649 triggerButton.getElement().addClass(classname + '-active'); 650 $mask.show(); 651 isOpen = true; 652 autoFitNeeded && autoFit(); 653 calendar.scrollToRangeStart(); 654 $container.show(); 655 reposition(); 656 } 657 if (options.onOpen) { 658 options.onOpen(); 659 } 660 instance._trigger('open', event, {instance: instance}); 661 } 662 663 function close(event) { 664 if (isOpen) { 665 $container.hide(); 666 $mask.hide(); 667 triggerButton.getElement().removeClass(classname + '-active'); 668 isOpen = false; 669 } 670 if (options.onClose) { 671 options.onClose(); 672 } 673 instance._trigger('close', event, {instance: instance}); 674 } 675 676 function toggle(event) { 677 if (isOpen) { 678 close(event); 679 reset(); 680 } 681 else { 682 open(event); 683 } 684 } 685 686 function getContainer(){ 687 return $container; 688 } 689 690 function enforceOptions() { 691 var oldPresetsMenu = presetsMenu; 692 presetsMenu = buildPresetsMenu(classname, options, usePreset); 693 oldPresetsMenu.getElement().replaceWith(presetsMenu.getElement()); 694 calendar.enforceOptions(); 695 buttonPanel.enforceOptions(); 696 triggerButton.enforceOptions(); 697 var range = getRange(); 698 if (range) { 699 triggerButton.setLabel(formatRangeForDisplay(range)); 700 } 701 } 702 703 init(); 704 return { 705 toggle: toggle, 706 destroy: destroy, 707 open: open, 708 close: close, 709 setRange: setRange, 710 getRange: getRange, 711 clearRange: clearRange, 712 reset: reset, 713 enforceOptions: enforceOptions, 714 getContainer: getContainer 715 }; 716 } 717 718})(jQuery, window);
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.