├── .gitattributes ├── .gitignore ├── README.md ├── bower.json ├── LICENSE.txt ├── jquery.comiseo.daterangepicker.css └── jquery.comiseo.daterangepicker.js /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js text eol=lf 2 | *.json text eol=lf 3 | *.css text eol=lf 4 | *.md text eol=lf 5 | *.txt text 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS generated files # 2 | ###################### 3 | .DS_Store 4 | .DS_Store? 5 | ._* 6 | .Spotlight-V100 7 | .Trashes 8 | ehthumbs.db 9 | Thumbs.db 10 | Desktop.ini 11 | *~ 12 | 13 | # IDE generated files # 14 | ####################### 15 | .settings/ 16 | .project 17 | .buildpath 18 | .idea/ 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jQuery UI DateRangePicker 2 | 3 | A jQuery UI widget similar to the date range picker used in Google Analytics; 4 | supports multiple months, custom preset ranges and smart positioning; 5 | [ThemeRoller](http://jqueryui.com/themeroller/)-ready and mobile-friendly. 6 | 7 | To get started, checkout examples and API at http://tamble.github.io/jquery-ui-daterangepicker/ 8 | 9 | ## Dependencies 10 | 11 | - [jQuery](http://jquery.com/) 1.8.3+ 12 | - [jQuery UI](http://jqueryui.com/) 1.9.0+ (widget factory, position utility, button, menu, datepicker) 13 | - [moment.js](http://momentjs.com) 2.3.0+ 14 | 15 | ## Copyright and License 16 | 17 | Copyright (c) 2017 Tamble, Inc. 18 | Released under [the MIT license](LICENSE.txt) 19 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-ui-daterangepicker", 3 | "homepage": "http://tamble.github.io/jquery-ui-daterangepicker/", 4 | "description": "jQuery UI widget similar to the date range picker used in Google Analytics; supports multiple months, custom preset ranges and smart positioning; ThemeRoller-ready and mobile-friendly.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/tamble/jquery-ui-daterangepicker.git" 8 | }, 9 | "main": [ 10 | "jquery.comiseo.daterangepicker.js", 11 | "jquery.comiseo.daterangepicker.css" 12 | ], 13 | "ignore": [ 14 | "/.*" 15 | ], 16 | "keywords": [ 17 | "jquery", 18 | "jquery ui", 19 | "date picker", 20 | "date range picker" 21 | ], 22 | "license": "MIT", 23 | "dependencies": { 24 | "jquery": ">=1.8.3", 25 | "jquery-ui": ">=1.9.0", 26 | "momentjs": "~2.3.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2015 Tamble, Inc 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /jquery.comiseo.daterangepicker.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2017 Tamble, Inc. 3 | * Licensed under MIT (https://github.com/tamble/jquery-ui-daterangepicker/raw/master/LICENSE.txt) 4 | */ 5 | 6 | .comiseo-daterangepicker-triggerbutton.ui-button { 7 | text-align: left; 8 | min-width: 18em; 9 | } 10 | 11 | .comiseo-daterangepicker-triggerbutton .ui-button-icon { /* fix v1.12 */ 12 | position: absolute; 13 | right: 0.5em; 14 | top: 50%; 15 | margin-top: -8px; 16 | } 17 | 18 | .comiseo-daterangepicker { 19 | position: absolute; 20 | padding: 5px; 21 | } 22 | 23 | .comiseo-daterangepicker-mask { 24 | margin: 0; 25 | padding: 0; 26 | position: fixed; 27 | left: 0; 28 | top: 0; 29 | height: 100%; 30 | width: 100%; 31 | /* required for IE */ 32 | background-color: #fff; 33 | opacity: 0; 34 | filter: alpha(opacity = 0); 35 | } 36 | 37 | .comiseo-daterangepicker-presets, 38 | .comiseo-daterangepicker-calendar { 39 | display: table-cell; 40 | vertical-align: top; 41 | height: 230px; 42 | } 43 | 44 | .comiseo-daterangepicker-right .comiseo-daterangepicker-presets { 45 | padding: 2px 7px 7px 2px; 46 | } 47 | 48 | .comiseo-daterangepicker-left .comiseo-daterangepicker-presets { 49 | padding: 2px 2px 7px 7px; 50 | } 51 | 52 | .comiseo-daterangepicker-presets .ui-menu { 53 | padding: 2px; /* fix v1.11 */ 54 | white-space: nowrap; 55 | } 56 | 57 | .comiseo-daterangepicker-presets .ui-menu-item { /* fix v1.11 */ 58 | padding: 0; 59 | } 60 | 61 | .comiseo-daterangepicker-presets .ui-menu-item > * { /* fix v1.11 */ 62 | text-decoration: none; 63 | display: block; 64 | padding: 2px 0.4em; 65 | line-height: 1.5; 66 | min-height: 0; /* support: IE7 */ 67 | } 68 | 69 | .comiseo-daterangepicker .ui-widget-content, 70 | .comiseo-daterangepicker .ui-datepicker .ui-state-highlight { 71 | border-width: 0; 72 | } 73 | 74 | .comiseo-daterangepicker > .comiseo-daterangepicker-main.ui-widget-content { 75 | border-bottom-width: 1px; 76 | } 77 | 78 | .comiseo-daterangepicker .ui-datepicker .ui-datepicker-today .ui-state-highlight { 79 | border-width: 1px; 80 | } 81 | 82 | .comiseo-daterangepicker-right .comiseo-daterangepicker-calendar { 83 | border-left-width: 1px; 84 | padding-left: 5px; 85 | } 86 | 87 | .comiseo-daterangepicker-left .comiseo-daterangepicker-calendar { 88 | border-right-width: 1px; 89 | padding-right: 5px; 90 | } 91 | 92 | .comiseo-daterangepicker-right .comiseo-daterangepicker-buttonpanel { 93 | float: left; 94 | } 95 | 96 | .comiseo-daterangepicker-left .comiseo-daterangepicker-buttonpanel { 97 | float: right; 98 | } 99 | 100 | .comiseo-daterangepicker-buttonpanel > button { 101 | margin-top: 6px; 102 | } 103 | 104 | .comiseo-daterangepicker-right .comiseo-daterangepicker-buttonpanel > button { 105 | margin-right: 6px; 106 | } 107 | 108 | .comiseo-daterangepicker-left .comiseo-daterangepicker-buttonpanel > button { 109 | margin-left: 6px; 110 | } 111 | 112 | /* themeable styles */ 113 | .comiseo-daterangepicker-calendar .ui-state-highlight a.ui-state-default { 114 | background: #b0c4de; 115 | color: #fff; 116 | } -------------------------------------------------------------------------------- /jquery.comiseo.daterangepicker.js: -------------------------------------------------------------------------------- 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} $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 | 114 | function init() { 115 | fixReferences(); 116 | $self = $('') 117 | .addClass(classnameContext + '-triggerbutton') 118 | .attr({'title': $originalElement.attr('title'), 'tabindex': $originalElement.attr('tabindex'), id: id}) 119 | .button({ 120 | icons: { 121 | secondary: options.icon 122 | }, 123 | icon: options.icon, 124 | iconPosition: 'end', 125 | label: options.initialText 126 | }); 127 | } 128 | 129 | function getLabel() { 130 | return $self.button('option', 'label'); 131 | } 132 | 133 | function setLabel(value) { 134 | $self.button('option', 'label', value); 135 | } 136 | 137 | function reset() { 138 | $originalElement.val('').change(); 139 | setLabel(options.initialText); 140 | } 141 | 142 | function enforceOptions() { 143 | $self.button('option', { 144 | icons: { 145 | secondary: options.icon 146 | }, 147 | icon: options.icon, 148 | iconPosition: 'end', 149 | label: options.initialText 150 | }); 151 | } 152 | 153 | init(); 154 | return { 155 | getElement: function() { return $self; }, 156 | getLabel: getLabel, 157 | setLabel: setLabel, 158 | reset: reset, 159 | enforceOptions: enforceOptions 160 | }; 161 | } 162 | 163 | /** 164 | * factory for the presets menu (containing built-in date ranges) 165 | * 166 | * @param {String} classnameContext classname of the parent container 167 | * @param {Object} options 168 | * @param {Function} onClick callback that executes when a preset is clicked 169 | */ 170 | function buildPresetsMenu(classnameContext, options, onClick) { 171 | var $self, 172 | $menu, 173 | menuItemWrapper; 174 | 175 | function init() { 176 | $self = $('
') 177 | .addClass(classnameContext + '-presets'); 178 | 179 | $menu = $(''); 180 | 181 | if ($.ui.menu.prototype.options.items === undefined) { 182 | menuItemWrapper = {start: '
  • ', end: '
  • '}; 183 | } else { 184 | menuItemWrapper = {start: '
  • ', end: '
  • '}; 185 | } 186 | 187 | $.each(options.presetRanges, function() { 188 | $(menuItemWrapper.start + this.text + menuItemWrapper.end) 189 | .data('dateStart', this.dateStart) 190 | .data('dateEnd', this.dateEnd) 191 | .click(onClick) 192 | .appendTo($menu); 193 | }); 194 | 195 | $self.append($menu); 196 | 197 | $menu.menu() 198 | .data('ui-menu').delay = 0; // disable submenu delays 199 | } 200 | 201 | init(); 202 | return { 203 | getElement: function() { return $self; } 204 | }; 205 | } 206 | 207 | /** 208 | * factory for the multiple month date picker 209 | * 210 | * @param {String} classnameContext classname of the parent container 211 | * @param {Object} options 212 | */ 213 | function buildCalendar(classnameContext, options) { 214 | var $self, 215 | range = {start: null, end: null}; // selected range 216 | 217 | function init() { 218 | $self = $('
    ', {'class': classnameContext + '-calendar ui-widget-content'}); 219 | 220 | $self.datepicker($.extend({}, options.datepickerOptions, {beforeShowDay: beforeShowDay, onSelect: onSelectDay})); 221 | updateAtMidnight(); 222 | } 223 | 224 | function enforceOptions() { 225 | $self.datepicker('option', $.extend({}, options.datepickerOptions, {beforeShowDay: beforeShowDay, onSelect: onSelectDay})); 226 | } 227 | 228 | // called when a day is selected 229 | function onSelectDay(dateText, instance) { 230 | var dateFormat = options.datepickerOptions.dateFormat || $.datepicker._defaults.dateFormat, 231 | selectedDate = $.datepicker.parseDate(dateFormat, dateText); 232 | 233 | if (!range.start || range.end) { // start not set, or both already set 234 | range.start = selectedDate; 235 | range.end = null; 236 | } else if (selectedDate < range.start) { // start set, but selected date is earlier 237 | range.end = range.start; 238 | range.start = selectedDate; 239 | } else { 240 | range.end = selectedDate; 241 | } 242 | if (options.datepickerOptions.hasOwnProperty('onSelect')) { 243 | options.datepickerOptions.onSelect(dateText, instance); 244 | } 245 | } 246 | 247 | // called for each day in the datepicker before it is displayed 248 | function beforeShowDay(date) { 249 | var result = [ 250 | true, // selectable 251 | range.start && ((+date === +range.start) || (range.end && range.start <= date && date <= range.end)) ? 'ui-state-highlight' : '' // class to be added 252 | ], 253 | userResult = [true, '', '']; 254 | 255 | if (options.datepickerOptions.hasOwnProperty('beforeShowDay')) { 256 | userResult = options.datepickerOptions.beforeShowDay(date); 257 | } 258 | return [ 259 | result[0] && userResult[0], 260 | result[1] + ' ' + userResult[1], 261 | userResult[2] 262 | ]; 263 | } 264 | 265 | function updateAtMidnight() { 266 | setTimeout(function() { 267 | refresh(); 268 | updateAtMidnight(); 269 | }, moment().endOf('day') - moment()); 270 | } 271 | 272 | function scrollToRangeStart() { 273 | if (range.start) { 274 | $self.datepicker('setDate', range.start); 275 | } 276 | } 277 | 278 | function refresh() { 279 | $self.datepicker('refresh'); 280 | $self.datepicker('setDate', null); // clear the selected date 281 | } 282 | 283 | function reset() { 284 | range = {start: null, end: null}; 285 | refresh(); 286 | } 287 | 288 | init(); 289 | return { 290 | getElement: function() { return $self; }, 291 | scrollToRangeStart: function() { return scrollToRangeStart(); }, 292 | getRange: function() { return range; }, 293 | setRange: function(value) { range = value; refresh(); }, 294 | refresh: refresh, 295 | reset: reset, 296 | enforceOptions: enforceOptions 297 | }; 298 | } 299 | 300 | /** 301 | * factory for the button panel 302 | * 303 | * @param {String} classnameContext classname of the parent container 304 | * @param {Object} options 305 | * @param {Object} handlers contains callbacks for each button 306 | */ 307 | function buildButtonPanel(classnameContext, options, handlers) { 308 | var $self, 309 | applyButton, 310 | clearButton, 311 | cancelButton; 312 | 313 | function init() { 314 | $self = $('
    ') 315 | .addClass(classnameContext + '-buttonpanel'); 316 | 317 | if (options.applyButtonText) { 318 | applyButton = $('') 319 | .text(options.applyButtonText) 320 | .button(); 321 | 322 | $self.append(applyButton); 323 | } 324 | 325 | if (options.clearButtonText) { 326 | clearButton = $('') 327 | .text(options.clearButtonText) 328 | .button(); 329 | 330 | $self.append(clearButton); 331 | } 332 | 333 | if (options.cancelButtonText) { 334 | cancelButton = $('') 335 | .text(options.cancelButtonText) 336 | .button(); 337 | 338 | $self.append(cancelButton); 339 | } 340 | 341 | bindEvents(); 342 | } 343 | 344 | function enforceOptions() { 345 | if (applyButton) { 346 | applyButton.button('option', 'label', options.applyButtonText); 347 | } 348 | 349 | if (clearButton) { 350 | clearButton.button('option', 'label', options.clearButtonText); 351 | } 352 | 353 | if (cancelButton) { 354 | cancelButton.button('option', 'label', options.cancelButtonText); 355 | } 356 | } 357 | 358 | function bindEvents() { 359 | if (handlers) { 360 | if (applyButton) { 361 | applyButton.click(handlers.onApply); 362 | } 363 | 364 | if (clearButton) { 365 | clearButton.click(handlers.onClear); 366 | } 367 | 368 | if (cancelButton) { 369 | cancelButton.click(handlers.onCancel); 370 | } 371 | } 372 | } 373 | 374 | init(); 375 | return { 376 | getElement: function() { return $self; }, 377 | enforceOptions: enforceOptions 378 | }; 379 | } 380 | 381 | /** 382 | * factory for the widget 383 | * 384 | * @param {jQuery} $originalElement jQuery object containing the input form element used to instantiate this widget instance 385 | * @param {Object} instance 386 | * @param {Object} options 387 | */ 388 | function buildDateRangePicker($originalElement, instance, options) { 389 | var classname = 'comiseo-daterangepicker', 390 | $container, // the dropdown 391 | $mask, // ui helper (z-index fix) 392 | triggerButton, 393 | presetsMenu, 394 | calendar, 395 | buttonPanel, 396 | isOpen = false, 397 | autoFitNeeded = false, 398 | LEFT = 0, 399 | RIGHT = 1, 400 | TOP = 2, 401 | BOTTOM = 3, 402 | sides = ['left', 'right', 'top', 'bottom'], 403 | hSide = RIGHT, // initialized to pick layout styles from CSS 404 | vSide = null; 405 | 406 | function init() { 407 | triggerButton = buildTriggerButton($originalElement, classname, options); 408 | presetsMenu = buildPresetsMenu(classname, options, usePreset); 409 | calendar = buildCalendar(classname, options); 410 | autoFit.numberOfMonths = options.datepickerOptions.numberOfMonths; // save initial option! 411 | if (autoFit.numberOfMonths instanceof Array) { // not implemented 412 | options.autoFitCalendars = false; 413 | } 414 | buttonPanel = buildButtonPanel(classname, options, { 415 | onApply: function (event) { 416 | close(event); 417 | setRange(null, event); 418 | }, 419 | onClear: function (event) { 420 | close(event); 421 | clearRange(event); 422 | }, 423 | onCancel: function (event) { 424 | instance._trigger('cancel', event, {instance: instance}); 425 | close(event); 426 | reset(); 427 | } 428 | }); 429 | render(); 430 | autoFit(); 431 | reset(); 432 | bindEvents(); 433 | } 434 | 435 | function render() { 436 | $container = $('
    ', {'class': classname + ' ' + classname + '-' + sides[hSide] + ' ui-widget ui-widget-content ui-corner-all ui-front'}) 437 | .append($('
    ', {'class': classname + '-main ui-widget-content'}) 438 | .append(presetsMenu.getElement()) 439 | .append(calendar.getElement())) 440 | .append($('
    ') 441 | .append(buttonPanel.getElement())) 442 | .hide(); 443 | $originalElement.hide().after(triggerButton.getElement()); 444 | $mask = $('
    ', {'class': 'ui-front ' + classname + '-mask'}).hide(); 445 | $('body').append($mask).append($container); 446 | } 447 | 448 | // auto adjusts the number of months in the date picker 449 | function autoFit() { 450 | if (options.autoFitCalendars) { 451 | var maxWidth = $(window).width(), 452 | initialWidth = $container.outerWidth(true), 453 | $calendar = calendar.getElement(), 454 | numberOfMonths = $calendar.datepicker('option', 'numberOfMonths'), 455 | initialNumberOfMonths = numberOfMonths; 456 | 457 | if (initialWidth > maxWidth) { 458 | while (numberOfMonths > 1 && $container.outerWidth(true) > maxWidth) { 459 | $calendar.datepicker('option', 'numberOfMonths', --numberOfMonths); 460 | } 461 | if (numberOfMonths !== initialNumberOfMonths) { 462 | autoFit.monthWidth = (initialWidth - $container.outerWidth(true)) / (initialNumberOfMonths - numberOfMonths); 463 | } 464 | } else { 465 | while (numberOfMonths < autoFit.numberOfMonths && (maxWidth - $container.outerWidth(true)) >= autoFit.monthWidth) { 466 | $calendar.datepicker('option', 'numberOfMonths', ++numberOfMonths); 467 | } 468 | } 469 | reposition(); 470 | autoFitNeeded = false; 471 | } 472 | } 473 | 474 | function destroy() { 475 | $container.remove(); 476 | triggerButton.getElement().remove(); 477 | $originalElement.show(); 478 | } 479 | 480 | function bindEvents() { 481 | triggerButton.getElement().click(toggle); 482 | triggerButton.getElement().keydown(keyPressTriggerOpenOrClose); 483 | $mask.click(function(event) { 484 | close(event); 485 | reset(); 486 | }); 487 | $(window).resize(function() { isOpen ? autoFit() : autoFitNeeded = true; }); 488 | } 489 | 490 | function formatRangeForDisplay(range) { 491 | var dateFormat = options.dateFormat; 492 | return $.datepicker.formatDate(dateFormat, range.start) + (+range.end !== +range.start ? options.rangeSplitter + $.datepicker.formatDate(dateFormat, range.end) : ''); 493 | } 494 | 495 | // formats a date range as JSON 496 | function formatRange(range) { 497 | var dateFormat = options.altFormat, 498 | formattedRange = {}; 499 | formattedRange.start = $.datepicker.formatDate(dateFormat, range.start); 500 | formattedRange.end = $.datepicker.formatDate(dateFormat, range.end); 501 | return JSON.stringify(formattedRange); 502 | } 503 | 504 | // parses a date range in JSON format 505 | function parseRange(text) { 506 | var dateFormat = options.altFormat, 507 | range = null; 508 | if (text) { 509 | try { 510 | range = JSON.parse(text, function(key, value) { 511 | return key ? $.datepicker.parseDate(dateFormat, value) : value; 512 | }); 513 | } catch (e) { 514 | } 515 | } 516 | return range; 517 | } 518 | 519 | function reset() { 520 | var range = getRange(); 521 | if (range) { 522 | triggerButton.setLabel(formatRangeForDisplay(range)); 523 | calendar.setRange(range); 524 | } else { 525 | calendar.reset(); 526 | } 527 | } 528 | 529 | function setRange(value, event) { 530 | var range = value || calendar.getRange(); 531 | if (!range.start) { 532 | return; 533 | } 534 | if (!range.end) { 535 | range.end = range.start; 536 | } 537 | value && calendar.setRange(range); 538 | triggerButton.setLabel(formatRangeForDisplay(range)); 539 | $originalElement.val(formatRange(range)).change(); 540 | if (options.onChange) { 541 | options.onChange(); 542 | } 543 | instance._trigger('change', event, {instance: instance}); 544 | } 545 | 546 | function getRange() { 547 | return parseRange($originalElement.val()); 548 | } 549 | 550 | function clearRange(event) { 551 | triggerButton.reset(); 552 | calendar.reset(); 553 | if (options.onClear) { 554 | options.onClear(); 555 | } 556 | instance._trigger('clear', event, {instance: instance}); 557 | } 558 | 559 | // callback - used when the user clicks a preset range 560 | function usePreset(event) { 561 | var $this = $(this), 562 | start = $this.data('dateStart')().startOf('day').toDate(), 563 | end = $this.data('dateEnd')().startOf('day').toDate(); 564 | calendar.setRange({ start: start, end: end }); 565 | if (options.applyOnMenuSelect) { 566 | close(event); 567 | setRange(null, event); 568 | } 569 | return false; 570 | } 571 | 572 | // adjusts dropdown's position taking into account the available space 573 | function reposition() { 574 | $container.position({ 575 | my: 'left top', 576 | at: 'left bottom' + (options.verticalOffset < 0 ? options.verticalOffset : '+' + options.verticalOffset), 577 | of: triggerButton.getElement(), 578 | collision : 'flipfit flipfit', 579 | using: function(coords, feedback) { 580 | var containerCenterX = feedback.element.left + feedback.element.width / 2, 581 | triggerButtonCenterX = feedback.target.left + feedback.target.width / 2, 582 | prevHSide = hSide, 583 | last, 584 | containerCenterY = feedback.element.top + feedback.element.height / 2, 585 | triggerButtonCenterY = feedback.target.top + feedback.target.height / 2, 586 | prevVSide = vSide, 587 | vFit; // is the container fit vertically 588 | 589 | hSide = (containerCenterX > triggerButtonCenterX) ? RIGHT : LEFT; 590 | if (hSide !== prevHSide) { 591 | if (options.mirrorOnCollision) { 592 | last = (hSide === LEFT) ? presetsMenu : calendar; 593 | $container.children().first().append(last.getElement()); 594 | } 595 | $container.removeClass(classname + '-' + sides[prevHSide]); 596 | $container.addClass(classname + '-' + sides[hSide]); 597 | } 598 | $container.css({ 599 | left: coords.left, 600 | top: coords.top 601 | }); 602 | 603 | vSide = (containerCenterY > triggerButtonCenterY) ? BOTTOM : TOP; 604 | if (vSide !== prevVSide) { 605 | if (prevVSide !== null) { 606 | triggerButton.getElement().removeClass(classname + '-' + sides[prevVSide]); 607 | } 608 | triggerButton.getElement().addClass(classname + '-' + sides[vSide]); 609 | } 610 | vFit = vSide === BOTTOM && feedback.element.top - feedback.target.top !== feedback.target.height + options.verticalOffset 611 | || vSide === TOP && feedback.target.top - feedback.element.top !== feedback.element.height + options.verticalOffset; 612 | triggerButton.getElement().toggleClass(classname + '-vfit', vFit); 613 | } 614 | }); 615 | } 616 | 617 | function killEvent(event) { 618 | event.preventDefault(); 619 | event.stopPropagation(); 620 | } 621 | 622 | function keyPressTriggerOpenOrClose(event) { 623 | switch (event.which) { 624 | case $.ui.keyCode.UP: 625 | case $.ui.keyCode.DOWN: 626 | killEvent(event); 627 | open(event); 628 | return; 629 | case $.ui.keyCode.ESCAPE: 630 | killEvent(event); 631 | close(event); 632 | reset(); 633 | return; 634 | case $.ui.keyCode.TAB: 635 | close(event); 636 | return; 637 | } 638 | } 639 | 640 | function open(event) { 641 | if (!isOpen) { 642 | triggerButton.getElement().addClass(classname + '-active'); 643 | $mask.show(); 644 | isOpen = true; 645 | autoFitNeeded && autoFit(); 646 | calendar.scrollToRangeStart(); 647 | $container.show(); 648 | reposition(); 649 | } 650 | if (options.onOpen) { 651 | options.onOpen(); 652 | } 653 | instance._trigger('open', event, {instance: instance}); 654 | } 655 | 656 | function close(event) { 657 | if (isOpen) { 658 | $container.hide(); 659 | $mask.hide(); 660 | triggerButton.getElement().removeClass(classname + '-active'); 661 | isOpen = false; 662 | } 663 | if (options.onClose) { 664 | options.onClose(); 665 | } 666 | instance._trigger('close', event, {instance: instance}); 667 | } 668 | 669 | function toggle(event) { 670 | if (isOpen) { 671 | close(event); 672 | reset(); 673 | } 674 | else { 675 | open(event); 676 | } 677 | } 678 | 679 | function getContainer(){ 680 | return $container; 681 | } 682 | 683 | function enforceOptions() { 684 | var oldPresetsMenu = presetsMenu; 685 | presetsMenu = buildPresetsMenu(classname, options, usePreset); 686 | oldPresetsMenu.getElement().replaceWith(presetsMenu.getElement()); 687 | calendar.enforceOptions(); 688 | buttonPanel.enforceOptions(); 689 | triggerButton.enforceOptions(); 690 | var range = getRange(); 691 | if (range) { 692 | triggerButton.setLabel(formatRangeForDisplay(range)); 693 | } 694 | } 695 | 696 | init(); 697 | return { 698 | toggle: toggle, 699 | destroy: destroy, 700 | open: open, 701 | close: close, 702 | setRange: setRange, 703 | getRange: getRange, 704 | clearRange: clearRange, 705 | reset: reset, 706 | enforceOptions: enforceOptions, 707 | getContainer: getContainer 708 | }; 709 | } 710 | 711 | })(jQuery, window); 712 | --------------------------------------------------------------------------------