├── .eslintrc.json ├── .gitignore ├── .npmignore ├── README.md ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ ├── callbacks.js │ ├── defaultPropertiesAndBehavior.js │ ├── errors.js │ └── options.js ├── pickerProperties.js ├── plugins │ └── index.js ├── selectors.js └── support │ ├── commands.js │ └── index.js ├── datepicker.d.ts ├── dist ├── datepicker.min.css └── datepicker.min.js ├── images ├── calendar.png ├── chinese-days.png ├── daterange.gif ├── events.png ├── overlay-button.png ├── overlay-custom-months.png ├── overlay-default.png ├── overlay-placeholder.png ├── overlay.png ├── show-all-dates-on.png └── spanish-months.png ├── package.json ├── postcss.config.js ├── sandbox ├── index.ejs ├── sandbox.css └── sandbox.js ├── src ├── datepicker.js └── datepicker.scss └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 3 4 | }, 5 | "ignorePatterns": ["webpack.config.js", "test-app.js", "sandbox.js"] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files 2 | .DS_Store 3 | .idea 4 | package-lock.json 5 | 6 | # Folders 7 | node_modules 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Files 2 | .DS_Store 3 | .idea 4 | *.png 5 | *.gif 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ____ __ __ 3 | /\ _`\ /\ \__ __ /\ \ 4 | \ \ \/\ \ __ \ \ ,_\ __ _____ /\_\ ___\ \ \/'\ __ _ __ 5 | \ \ \ \ \ /'__`\\ \ \/ /'__`\/\ '__`\/\ \ /'___\ \ , < /'__`\/\`'__\ 6 | \ \ \_\ \/\ \L\.\\ \ \_/\ __/\ \ \L\ \ \ \/\ \__/\ \ \\`\ /\ __/\ \ \/ 7 | \ \____/\ \__/.\_\ \__\ \____\\ \ ,__/\ \_\ \____\\ \_\ \_\ \____\\ \_\ 8 | \/___/ \/__/\/_/\/__/\/____/ \ \ \/ \/_/\/____/ \/_/\/_/\/____/ \/_/ 9 | \ \_\ 10 | \/_/ By: The Qodesmith 11 | ``` 12 | 13 | # Datepicker.js · [![npm version](https://badge.fury.io/js/js-datepicker.svg)](https://badge.fury.io/js/js-datepicker) 14 | Get a date with JavaScript! Or a daterange, but that's not a good pun. Datepicker has **no dependencies** and weighs in at **5.9kb gzipped**! Datepicker is simple to use and looks sexy on the screen. A calendar pops up and you pick a date. #Boom. 15 | 16 | ![Datepicker screenshot](https://raw.githubusercontent.com/qodesmith/datepicker/master/images/calendar.png "Get a date with JavaScript!") 17 | 18 | 19 | ### Table of Contents 20 | 21 | * [Installation](#installation) 22 | * [Basic Usage](#basic-usage) 23 | * [Custom Elements / Shadow DOM Usage](#custom-elements--shadow-dom-usage) 24 | * [Manual Year & Month Navigation](#manual-year--month-navigation) 25 | * [Using As A Daterange Picker](#using-as-a-daterange-picker) 26 | * [Calendar Examples](#examples) 27 | * [Sizing The Calendar](#sizing-the-calendar) 28 | * [Properties & Values](#properties--values) 29 | 30 | 31 | #### Event Callbacks 32 | 33 | * [onSelect](#onselect) 34 | * [onShow](#onshow) 35 | * [onHide](#onhide) 36 | * [onMonthChange](#onmonthchange) 37 | 38 | #### Customizations 39 | 40 | * [formatter](#formatter) 41 | * [position](#position) 42 | * [startDay](#startday) 43 | * [customDays](#customdays) 44 | * [customMonths](#custommonths) 45 | * [customOverlayMonths](#customoverlaymonths) 46 | * [defaultView](#defaultView) 47 | * [overlayButton](#overlaybutton) 48 | * [overlayPlaceholder](#overlayplaceholder) 49 | * [events](#events) 50 | 51 | #### Settings 52 | 53 | * [alwaysShow](#alwaysshow) 54 | * [dateSelected](#dateselected) 55 | * [maxDate](#maxdate) 56 | * [minDate](#mindate) 57 | * [startDate](#startdate) 58 | * [showAllDates](#showalldates) 59 | * [respectDisabledReadOnly](#respectdisabledreadonly) 60 | 61 | #### Disabling Things 62 | 63 | * [noWeekends](#noweekends) 64 | * [disabler](#disabler) 65 | * [disabledDates](#disableddates) 66 | * [disableMobile](#disablemobile) 67 | * [disableYearOverlay](#disableyearoverlay) 68 | * [disabled](#disabled) 69 | 70 | #### ID - Daterange 71 | 72 | * [id](#id) 73 | 74 | #### Instance Methods 75 | 76 | * [navigate](#navigate) 77 | * [remove](#remove) 78 | * [setDate](#setdate) 79 | * [setMin](#setmin) 80 | * [setMax](#setmax) 81 | * [show](#show) 82 | * [hide](#hide) 83 | * [Show / Hide "Gotcha"](#show--hide-gotcha) 84 | * [toggleOverlay](#toggleOverlay) 85 | * [getRange](#getrange) _(daterange only)_ 86 | 87 | 88 | See the [examples](#examples) below. 89 | 90 | 91 | ## Installation 92 | 93 | #### Manually 94 | 95 | Simply include `datepicker.min.css` in the ``... 96 | ```html 97 | 98 | ... 99 | 100 | 101 | 102 | 103 | ``` 104 | 105 | and include `datepicker.min.js` just above your closing `` tag... 106 | ```html 107 | 108 | ... 109 | 110 | 111 | 112 | 113 | ``` 114 | 115 | If you downloaded the package via zip file from Github, these files are located in the `dist` folder. Otherwise, you can use the Unpkg CDN as shown in the examples above. 116 | 117 | 118 | #### Via NPM 119 | ``` 120 | npm install js-datepicker 121 | ``` 122 | 123 | Files & locations: 124 | 125 | | File | Folder | Description | 126 | | ------------------ | ------------------------------- | --------------------------------------- | 127 | | datepicker.min.js | node_modules/js-datepicker/dist | production build - (ES5, 5.9kb gzipped) | 128 | | datepicker.min.css | node_modules/js-datepicker/dist | production stylesheet | 129 | | datepicker.scss | node_modules/js-datepicker/src | Scss file. Use it in your own builds. | 130 | 131 | 132 | ## Basic Usage 133 | 134 | Importing the library if you're using it in Node: 135 | ```javascript 136 | import datepicker from 'js-datepicker' 137 | // or 138 | const datepicker = require('js-datepicker') 139 | ``` 140 | 141 | Using it in your code: 142 | ```javascript 143 | const picker = datepicker(selector, options) 144 | ``` 145 | 146 | Importing the styles into your project using Node: 147 | ```javascript 148 | // From within a scss file, 149 | // import datepickers scss file... 150 | @import '~js-datepicker/src/datepicker'; 151 | 152 | // or import datepickers css file. 153 | @import '~js-datepicker/dist/datepicker.min.css'; 154 | ``` 155 | 156 | Datepicker takes 2 arguments: 157 | 158 | 1. `selector` - two possibilities: 159 | 1. `string` - a CSS selector, such as `'.my-class'`, `'#my-id'`, or `'div'`. 160 | 2. `DOM node` - provide a DOM node, such as `document.querySelector('#my-id')`. 161 | 2. (optional) An object full of [options](#options). 162 | 163 | The return value of the `datepicker` function is the datepicker instance. See the methods and properties below. 164 | 165 | You can use Datepicker with any type of element you want. If used with an `` element (the common use case), then the ``'s value will automatically be set when selecting a date. 166 | 167 | _NOTE: Datepicker will not change the value of input fields with a type of_ `date` - ``. _This is because those input's already have a built in calendar and can cause problems. Use_ `` _instead._ 168 | 169 | 170 | ### Manual Year & Month Navigation 171 | 172 | By clicking on the year or month an overlay will show revealing an input field and a list of months. You can either enter a year in the input, click a month, or both: 173 | 174 | ![Datepicker screenshot](https://raw.githubusercontent.com/qodesmith/datepicker/master/images/overlay-default.png "Get a date with JavaScript!") 175 | 176 | 177 | ### Using As A Daterange Picker 178 | 179 | Want 2 calendars linked together to form ~~Voltron~~ a daterange picker? It's as simple as giving them both the same [id](#id)! By using the [id](#id) option, Datepicker handles all the logic to keep both calendars in sync. 180 | 181 | ![Datepicker daterange screenshot](https://raw.githubusercontent.com/qodesmith/datepicker/master/images/daterange.gif "Animated GIF opf a daterange pair") 182 | 183 | The 1st calendar will serve as the minimum date and the 2nd calendar as the maximum. Dates will be enabled / disabled on each calendar automatically when the user selects a date on either. The [getRange](#getrange) method will conveniently give you an object with the `start` and `end` date selections. It's as simple as creating 2 instances with the same `id` to form a daterange picker: 184 | 185 | ```javascript 186 | const start = datepicker('.start', { id: 1 }) 187 | const end = datepicker('.end', { id: 1 }) 188 | ``` 189 | 190 | And when you want to get your start and end values, simply call [getRange](#getrange) on _either_ instance: 191 | ```javascript 192 | start.getRange() // { start: , end: } 193 | end.getRange() // Gives you the same as above! 194 | ``` 195 | 196 | ## Custom Elements / Shadow DOM Usage 197 | 198 | You can use Datepicker within a Shadow DOM and custom elements. In order to do so, must pass a ___node___ as the 1st argument: 199 | 200 | ```javascript 201 | class MyElement extends HTMLElement { 202 | constructor() { 203 | super() 204 | const shadowRoot = this.attachShadow({ mode: 'open' }) 205 | shadowRoot.innerHTML = ` 206 |
207 | 208 | 209 |
210 | ` 211 | 212 | // Create the node we'll pass to datepicker. 213 | this.input = shadowRoot.querySelector('input') 214 | } 215 | 216 | connectedCallback() { 217 | // Pass datepicker a node within the shadow DOM. 218 | datepicker(this.input) 219 | } 220 | } 221 | 222 | customElements.define('my-element', MyElement) 223 | ``` 224 | 225 | All other options work as expected, including dateranges. You can even have a date range pair with one calendar in the shadow DOM and another outside it! 226 | 227 |
228 | 229 | 230 | ## Options - Event Callbacks 231 | 232 | Use these options if you want to fire off your own functions after something happens with the calendar. 233 | 234 | 235 | ### onSelect 236 | 237 | Callback function after a date has been selected. The 2nd argument is the selected date when a date is being selected and `undefined` when a date is being unselected. You unselect a date by clicking it again. 238 | 239 | ```javascript 240 | const picker = datepicker('.some-input', { 241 | onSelect: (instance, date) => { 242 | // Do stuff when a date is selected (or unselected) on the calendar. 243 | // You have access to the datepicker instance for convenience. 244 | } 245 | }) 246 | ``` 247 | * Arguments: 248 | 1. `instance` - the current datepicker instance. 249 | 2. `date`: 250 | * JavaScript date object when a date is being selected. 251 | * `undefined` when a date is being unselected. 252 | 253 | _NOTE: This will not fire when using the [instance methods](#methods) to manually change the calendar._ 254 | 255 | 256 | ### onShow 257 | 258 | Callback function when the calendar is shown. 259 | 260 | ```javascript 261 | const picker = datepicker('.some-input', { 262 | onShow: instance => { 263 | // Do stuff when the calendar is shown. 264 | // You have access to the datepicker instance for convenience. 265 | } 266 | }) 267 | ``` 268 | * Arguments: 269 | 1. `instance` - the current datepicker instance. 270 | 271 | _NOTE: This **will** fire when using the [show](#show) instance method._ 272 | 273 | 274 | ### onHide 275 | 276 | Callback function when the calendar is hidden. 277 | 278 | ```javascript 279 | const picker = datepicker('.some-input', { 280 | onHide: instance => { 281 | // Do stuff once the calendar goes away. 282 | // You have access to the datepicker instance for convenience. 283 | } 284 | }) 285 | ``` 286 | * Arguments: 287 | 1. `instance` - the current datepicker instance. 288 | 289 | _NOTE: This **will** fire when using the [hide](#hide) instance method._ 290 | 291 | 292 | ### onMonthChange 293 | 294 | Callback function when the month has changed. 295 | 296 | ```javascript 297 | const picker = datepicker('.some-input', { 298 | onMonthChange: instance => { 299 | // Do stuff when the month changes. 300 | // You have access to the datepicker instance for convenience. 301 | } 302 | }) 303 | ``` 304 | * Arguments: 305 | 1. `instance` - the current datepicker instance. 306 | 307 | 308 | ## Options - Customizations 309 | 310 | These options help you customize the calendar to your suit your needs. Some of these are especially helpful if you're using a language other than English. 311 | 312 | 313 | ### formatter 314 | 315 | Using an input field with your datepicker? Want to customize its value anytime a date is selected? Provide a function that manually sets the provided input's value with your own formatting. 316 | 317 | ```javascript 318 | const picker = datepicker('.some-input', { 319 | formatter: (input, date, instance) => { 320 | const value = date.toLocaleDateString() 321 | input.value = value // => '1/1/2099' 322 | } 323 | }) 324 | ``` 325 | * Default - default format is `date.toDateString()` 326 | * Arguments: 327 | 1. `input` - the input field that the datepicker is associated with. 328 | 2. `date` - a JavaScript date object of the currently selected date. 329 | 3. `instance` - the current datepicker instance. 330 | 331 | _Note: The_ `formatter` _function will only run if the datepicker instance is associated with an_ `` _field._ 332 | 333 | 334 | ### position 335 | 336 | This option positions the calendar relative to the `` field it's associated with. This can be 1 of 5 values: `'tr'`, `'tl'`, `'br'`, `'bl'`, `'c'` representing top-right, top-left, bottom-right, bottom-left, and centered respectively. Datepicker will position itself accordingly relative to the element you reference in the 1st argument. For a value of `'c'`, Datepicker will position itself fixed, smack in the middle of the screen. This can be desirable for mobile devices. 337 | 338 | ```javascript 339 | // The calendar will be positioned to the top-left of the input field. 340 | const picker = datepicker('.some-input', { position: 'tl' }) 341 | ``` 342 | * Type - string 343 | * Default - `'bl'` 344 | 345 | 346 | ### startDay 347 | 348 | Specify the day of the week your calendar starts on. `0` = Sunday, `1` = Monday, etc. Plays nice with the [`customDays`](#customdays) option. 349 | 350 | ```javascript 351 | // The first day of the week on this calendar is Monday. 352 | const picker = datepicker('.some-input', { startDay: 1 }) 353 | ``` 354 | * Type - number (`0` - `6`) 355 | * Default - `0` (Sunday starts the week) 356 | 357 | 358 | ### customDays 359 | 360 | You can customize the display of days on the calendar by providing an array of 7 values. This can be used with the [`startDay`](#startday) option if your week starts on a day other than Sunday. 361 | 362 | 363 | ```javascript 364 | const picker = datepicker('.some-input', { 365 | customDays: ['天', '一', '二', '三', '四', '五', '六'] 366 | }) 367 | ``` 368 | 369 | ![Custom days screenshot](https://raw.githubusercontent.com/qodesmith/datepicker/master/images/chinese-days.png "Example with Chinese custom days") 370 | 371 | * Type - array 372 | * Default - `['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']` 373 | 374 | 375 | ### customMonths 376 | 377 | You can customize the display of the month name at the top of the calendar by providing an array of 12 strings. 378 | 379 | ```javascript 380 | const picker = datepicker('.some-input', { 381 | customMonths: ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'] 382 | }) 383 | ``` 384 | 385 | ![Custom months screenshot](https://raw.githubusercontent.com/qodesmith/datepicker/master/images/spanish-months.png "Example with Spanish custom months") 386 | 387 | * Type - array 388 | * Default - `['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']` 389 | 390 | 391 | ### customOverlayMonths 392 | 393 | You can customize the display of the month names in the overlay view by providing an array of 12 strings. Keep in mind that if the values are too long, it could produce undesired results in the UI. 394 | 395 | Here's what the default looks like: 396 | 397 | ![Custom overlay months default screenshot](https://raw.githubusercontent.com/qodesmith/datepicker/master/images/overlay-default.png "Example with default custom overlay months") 398 | 399 | Here's an example with an array of custom values: 400 | 401 | ```javascript 402 | const picker = datepicker('.some-input', { 403 | customOverlayMonths: ['😀', '😂', '😎', '😍', '🤩', '😜', '😬', '😳', '🤪', '🤓 ', '😝', '😮'] 404 | }) 405 | ``` 406 | 407 | ![Custom overlay months screenshot](https://raw.githubusercontent.com/qodesmith/datepicker/master/images/overlay-custom-months.png "Example with custom overlay months") 408 | 409 | * Type - array 410 | * Default - The first 3 characters of each item in `customMonths`. 411 | 412 | 413 | ### defaultView 414 | 415 | Want the overlay to be the default view when opening the calendar? This property is for you. Simply set this property to `'overlay'` and you're done. This is helpful if you want a month picker to be front and center. 416 | 417 | ```javascript 418 | const picker = datepicker('.some-input', {defaultView: 'overlay'}) 419 | ``` 420 | 421 | * Type - string (`'calendar'` or `'overlay'`) 422 | * Default - `'calendar'` 423 | 424 | 425 | ### overlayButton 426 | 427 | Custom text for the year overlay submit button. 428 | 429 | ```javascript 430 | const picker = datepicker('.some-input', { 431 | overlayButton: "¡Vamanos!" 432 | }) 433 | ``` 434 | 435 | ![Custom overlay text screenshot](https://raw.githubusercontent.com/qodesmith/datepicker/master/images/overlay-button.png "Example with custom overlay text") 436 | 437 | * Type - string 438 | * Default - `'Submit'` 439 | 440 | 441 | ### overlayPlaceholder 442 | 443 | Custom placeholder text for the year overlay. 444 | 445 | ```javascript 446 | const picker = datepicker('.some-input', { 447 | overlayPlaceholder: 'Entrar un año' 448 | }) 449 | ``` 450 | 451 | ![Custom overlay placeholder screenshot](https://raw.githubusercontent.com/qodesmith/datepicker/master/images/overlay-placeholder.png "Example with custom overlay placeholder text") 452 | 453 | * Type - string 454 | * Default - `'4-digit year'` 455 | 456 | 457 | ### events 458 | 459 | An array of dates which indicate something is happening - a meeting, birthday, etc. I.e. an _event_. 460 | ```javascript 461 | const picker = datepicker('.some-input', { 462 | events: [ 463 | new Date(2019, 10, 1), 464 | new Date(2019, 10, 10), 465 | new Date(2019, 10, 20), 466 | ] 467 | }) 468 | ``` 469 | 470 | ![Events on calendar screenshot](https://raw.githubusercontent.com/qodesmith/datepicker/master/images/events.png "Example with events on calendar") 471 | 472 | * Type - array of JS date objects 473 | 474 | 475 | ## Options - Settings 476 | 477 | Use these options to set the calendar the way you want. 478 | 479 | 480 | ### alwaysShow 481 | 482 | By default, the datepicker will hide & show itself automatically depending on where you click or focus on the page. If you want the calendar to always be on the screen, use this option. 483 | 484 | ```javascript 485 | const picker = datepicker('.some-input', { alwaysShow: true }) 486 | ``` 487 | * Type - boolean 488 | * Default - `false` 489 | 490 | 491 | ### dateSelected 492 | 493 | This will start the calendar with a date already selected. If Datepicker is used with an `` element, that field will be populated with this date as well. 494 | 495 | ```javascript 496 | const picker = datepicker('.some-input', { dateSelected: new Date(2099, 0, 5) }) 497 | ``` 498 | * Type - JS date object 499 | 500 | 501 | ### maxDate 502 | 503 | This will be the maximum threshold of selectable dates. Anything after it will be unselectable. 504 | 505 | ```javascript 506 | const picker = datepicker('.some-input', { maxDate: new Date(2099, 0, 1) }) 507 | ``` 508 | * Type - JavaScript date object. 509 | 510 | _NOTE: When using a [daterange](#using-as-a-daterange-picker) pair, if you set_ `maxDate` _on the first instance options it will be ignored on the 2nd instance options._ 511 | 512 | 513 | ### minDate 514 | 515 | This will be the minumum threshold of selectable dates. Anything prior will be unselectable. 516 | 517 | ```javascript 518 | const picker = datepicker('.some-input', { minDate: new Date(2018, 0, 1) }) 519 | ``` 520 | * Type - JavaScript date object. 521 | 522 | _NOTE: When using a [daterange](#using-as-a-daterange-picker) pair, if you set_ `minDate` _on the first instance options it will be ignored on the 2nd instance options._ 523 | 524 | 525 | ### startDate 526 | 527 | The date you provide will determine the month that the calendar starts off at. 528 | 529 | ```javascript 530 | const picker = datepicker('.some-input', { startDate: new Date(2099, 0, 1) }) 531 | ``` 532 | * Type - JavaScript date object. 533 | * Default - today's month 534 | 535 | 536 | ### showAllDates 537 | 538 | By default, the datepicker will not put date numbers on calendar days that fall outside the current month. They will be empty squares. Sometimes you want to see those preceding and trailing days. This is the option for you. 539 | 540 | ```javascript 541 | const picker = datepicker('.some-input', { showAllDates: true }) 542 | ``` 543 | 544 | ![Show all dates on screenshot](https://raw.githubusercontent.com/qodesmith/datepicker/master/images/show-all-dates-on.png "Example with show all dates option on") 545 | 546 | * Type - boolean 547 | * Default - `false` 548 | 549 | 550 | ### respectDisabledReadOnly 551 | 552 | ``'s can have a `disabled` or `readonly` attribute applied to them. In those cases, you might want to prevent Datepicker from selecting a date and changing the input's value. Set this option to `true` if that's the case. The calendar will still be functional in that you can change months and enter a year, but dates will not be selectable (or deselectable). 553 | 554 | ```javascript 555 | const picker = datepicker('.some-input', { respectDisabledReadOnly: true }) 556 | ``` 557 | 558 | * Type - boolean 559 | * Default - `false` 560 | 561 | 562 | ## Options - Disabling Things 563 | 564 | These options are associated with disabled various things. 565 | 566 | 567 | ### noWeekends 568 | 569 | Provide `true` to disable selecting weekends. Weekends are Saturday & Sunday. If your weekends are a set of different days or you need more control over disabled dates, check out the [`disabler`](#disabler) option. 570 | 571 | ```javascript 572 | const picker = datepicker('.some-input', { noWeekends: true }) 573 | ``` 574 | * Type - boolean 575 | * Default - `false` 576 | 577 | 578 | ### disabler 579 | 580 | Sometimes you need more control over which dates to disable. The [`disabledDates`](#disableddates) option is limited to an explicit array of dates and the [`noWeekends`](#noweekends) option is limited to Saturdays & Sundays. Provide a function that takes a JavaScript date as it's only argument and returns `true` if the date should be disabled. When the calendar builds, each date will be run through this function to determine whether or not it should be disabled. 581 | 582 | ```javascript 583 | const picker1 = datepicker('.some-input1', { 584 | // Disable every Tuesday on the calendar (for any given month). 585 | disabler: date => date.getDay() === 2 586 | }) 587 | 588 | const picker2 = datepicker('.some-input2', { 589 | // Disable every day in the month of October (for any given year). 590 | disabler: date => date.getMonth() === 9 591 | }) 592 | ``` 593 | * Arguments: 594 | 1. `date` - JavaScript date object representing a given day on a calendar. 595 | 596 | 597 | ### disabledDates 598 | 599 | Provide an array of JS date objects that will be disabled on the calendar. This array cannot include the same date as [`dateSelected`](#dateselected). If you need more control over which dates are disabled, see the [`disabler`](#disabler) option. 600 | 601 | ```javascript 602 | const picker = datepicker('.some-input', { 603 | disabledDates: [ 604 | new Date(2099, 0, 5), 605 | new Date(2099, 0, 6), 606 | new Date(2099, 0, 7), 607 | ] 608 | }) 609 | ``` 610 | * Type - array of JS date objects 611 | 612 | 613 | ### disableMobile 614 | 615 | Optionally disable Datepicker on mobile devices. This is handy if you'd like to trigger the mobile device's native date picker instead. If that's the case, make sure to use an input with a type of "date" - `` 616 | 617 | ```javascript 618 | const picker = datepicker('.some-input', { disableMobile: true }) 619 | ``` 620 | * Type - boolean 621 | * Default - `false` 622 | 623 | 624 | ### disableYearOverlay 625 | 626 | Clicking the year or month name on the calendar triggers an overlay to show, allowing you to enter a year manually. If you want to disable this feature, set this option to `true`. 627 | 628 | ```javascript 629 | const picker = datepicker('.some-input', { disableYearOverlay: true }) 630 | ``` 631 | * Type - boolean 632 | * Default - `false` 633 | 634 | 635 | ### disabled 636 | 637 | Want to completely disable the calendar? Simply set the `disabled` property on the datepicker instance to `true` to render it impotent. Maybe you don't want the calendar to show in a given situation. Maybe the calendar is showing but you don't want it to do anything until some other field is filled out in a form. Either way, have fun. 638 | 639 | Example: 640 | ```javascript 641 | const picker = datepicker('.some-input') 642 | 643 | function disablePicker() { 644 | picker.disabled = true 645 | } 646 | 647 | function enablePicker() { 648 | picker.disabled = false 649 | } 650 | 651 | function togglePicker() { 652 | picker.disabled = !picker.disabled 653 | } 654 | ``` 655 | 656 | 657 | ## Options - Other 658 | 659 | ### id 660 | 661 | Now we're getting _fancy!_ If you want to link two instances together to help form a daterange picker, this is your option. Only two picker instances can share an `id`. The datepicker instance declared first will be considered the "start" picker in the range. There's a fancy [getRange](#getrange) method for you to use as well. 662 | 663 | ```javascript 664 | const start = datepicker('.start', { id: 1 }) 665 | const end = datepicker('.end', { id: 1 }) 666 | ``` 667 | 668 | * Type - anything but `null` or `undefined` 669 | 670 | 671 | ## Methods 672 | 673 | Each instance of Datepicker has methods to allow you to programmatically manipulate the calendar. 674 | 675 | 676 | ### remove 677 | 678 | Performs cleanup. This will remove the current instance from the DOM, leaving all others in tact. If this is the only instance left, it will also remove the event listeners that Datepicker previously set up. 679 | 680 | ```javascript 681 | const picker = datepicker('.some-input') 682 | 683 | /* ...so many things... */ 684 | 685 | picker.remove() // So fresh & so clean clean. 686 | ``` 687 | 688 | 689 | ### navigate 690 | 691 | Programmatically navigates the calendar to the date you provide. This doesn't select a date, it's literally just for navigation. You can optionally trigger the `onMonthChange` callback with the 2nd argument. 692 | 693 | ```javascript 694 | const picker = datepicker('.some-input') 695 | const date = new Date(2020, 3, 1) 696 | 697 | /* ...so many things... */ 698 | 699 | // Navigate to a new month. 700 | picker.navigate(date) 701 | 702 | // Navigate to a new month AND trigger the `onMonthChange` callback. 703 | picker.navigate(date, true) 704 | ``` 705 | 706 | * Arguments: 707 | 1. `date` - JavaScript date object. 708 | 2. `trigger onMonthChange` - boolean (default is `false`) 709 | 710 | 711 | ### setDate 712 | 713 | Allows you to programmatically select or unselect a date on the calendar. To select a date on the calendar, pass in a JS date object for the 1st argument. If you set a date on a month other than what's currently displaying _and_ you want the calendar to automatically change to it, pass in `true` as the 2nd argument. 714 | 715 | Want to unselect a date? Simply run the function with no arguments. 716 | 717 | ```javascript 718 | // Select a date on the calendar. 719 | const picker = datepicker('.some-input') 720 | 721 | // Selects January 1st 2099 on the calendar 722 | // *and* changes the calendar to that date. 723 | picker.setDate(new Date(2099, 0, 1), true) 724 | 725 | // Selects November 1st 2099 but does *not* change the calendar. 726 | picker.setDate(new Date(2099, 10, 1)) 727 | 728 | // Remove the selection simply by omitting any arguments. 729 | picker.setDate() 730 | ``` 731 | * Arguments: 732 | 1. `date` - JavaScript date object. 733 | 2. `changeCalendar` - boolean (default is `false`) 734 | 735 | _Note: This will not trigger the_ [`onSelect`]('#onselect') _callback._ 736 | 737 | 738 | ### setMin 739 | 740 | Allows you to programmatically set the minimum selectable date or unset it. If this instance is part of a [daterange](#using-as-a-daterange-picker) instance (see the [`id`](#id) option) then the other instance will be changed as well. To unset a minimum date, simply run the function with no arguments. 741 | 742 | ```javascript 743 | // Set a minimum selectable date. 744 | const picker = datepicker('.some-input') 745 | picker.setMin(new Date(2018, 0, 1)) 746 | 747 | // Remove the minimum selectable date. 748 | picker.setMin() 749 | ``` 750 | * Arguments: 751 | 1. `date` - JavaScript date object. 752 | 753 | 754 | ### setMax 755 | 756 | Allows you to programmatically set the maximum selectable date or unset it. If this instance is part of a [daterange](#using-as-a-daterange-picker) instance (see the [`id`](#id) option) then the other instance will be changed as well. To unset a maximum date, simply run the function with no arguments. 757 | 758 | ```javascript 759 | // Set a maximum selectable date. 760 | const picker = datepicker('.some-input') 761 | picker.setMax(new Date(2099, 0, 1)) 762 | 763 | // Remove the maximum selectable date. 764 | picker.setMax() 765 | ``` 766 | * Arguments: 767 | 1. `date` - JavaScript date object. 768 | 769 | 770 | ### show 771 | 772 | Allows you to programmatically show the calendar. Using this method will trigger the `onShow` callback if your instance has one. 773 | 774 | ```javascript 775 | const picker = datepicker('.some-input') 776 | picker.show() 777 | ``` 778 | 779 | _Note: see the "[gotcha](#show--hide-gotcha)" below for implementing this method in an event handler._ 780 | 781 | 782 | ### hide 783 | 784 | Allows you to programmatically hide the calendar. If the `alwaysShow` property was set on the instance then this method will have no effect. Using this method will trigger the `onHide` callback if your instance has one. 785 | 786 | ```javascript 787 | const picker1 = datepicker('.some-input') 788 | const picker2 = datepicker('.some-other-input', { alwaysShow: true }) 789 | 790 | picker1.hide() // This works. 791 | picker2.hide() // This does not work because of `alwaysShow`. 792 | ``` 793 | 794 | _Note: see the "[gotcha](#show--hide-gotcha)" below for implementing this method in an event handler._ 795 | 796 | 797 | #### Show / Hide "Gotcha" 798 | 799 | Want to show / hide the calendar programmatically with a button or by clicking some element? Make [sure](https://github.com/qodesmith/datepicker/issues/71#issuecomment-553363045) to use `stopPropagation` in your event callback! If you don't, any click event in the DOM will bubble up to Datepicker's internal `oneHandler` event listener, triggering logic to close the calendar since it "sees" the click event _outside_ the calendar. Here's an example on how to use the `show` and `hide` methods in a click event handler: 800 | 801 | ```javascript 802 | // Attach the picker to an input element. 803 | const picker = datepicker(inputElement, options) 804 | 805 | // Toggle the calendar when a button is clicked. 806 | button.addEventListener('click', e => { 807 | // THIS!!! Prevent Datepicker's event handler from hiding the calendar. 808 | e.stopPropagation() 809 | 810 | // Toggle the calendar. 811 | const isHidden = picker.calendarContainer.classList.contains('qs-hidden') 812 | picker[isHidden ? 'show' : 'hide']() 813 | }) 814 | ``` 815 | 816 | 817 | ### toggleOverlay 818 | 819 | Call this method on the picker to programmatically toggle the overlay. This will only work if the calendar is showing! 820 | 821 | ```javascript 822 | const picker = datepicker('.some-input') 823 | 824 | // Click the input to show the calendar... 825 | 826 | picker.toggleOverlay() 827 | ``` 828 | 829 | 830 | ### getRange 831 | 832 | This method is only available on [daterange](#using-as-a-daterange-picker) pickers. It will return an object with `start` and `end` properties whose values are JavaScript date objects representing what the user selected on both calendars. 833 | 834 | ```javascript 835 | const start = datepicker('.start', { id: 1 }) 836 | const end = datepicker('.end', { id: 1 }) 837 | 838 | // ... 839 | 840 | start.getRange() // { start: , end: } 841 | end.getRange() // Gives you the same as above! 842 | ``` 843 | 844 | 845 | ## Properties & Values 846 | 847 | If you take a look at the datepicker instance, you'll notice plenty of values that you can grab and use however you'd like. Below details some helpful properties and values that are available on the picker instance. 848 | 849 | | Property | Value | 850 | | -------- | ----- | 851 | | `calendar` | The calendar element. | 852 | | `calendarContainer` | The container element that houses the calendar. Use it to [size](#sizing-the-calendar) the calendar or programmatically [check if the calendar is showing](#show--hide-gotcha). | 853 | | `currentMonth` | A 0-index number representing the current month. For example, `0` represents January. | 854 | | `currentMonthName` | Calendar month in plain english. E.x. `January` | 855 | | `currentYear` | The current year. E.x. `2099` | 856 | | `dateSelected` | The value of the selected date. This will be `undefined` if no date has been selected yet. | 857 | | `el` | The element datepicker is relatively positioned against (unless centered). | 858 | | `minDate` | The minimum selectable date. | 859 | | `maxDate` | The maximum selectable date. | 860 | | `sibling` | If two datepickers have the same `id` option then this property will be available and refer to the other instance. | 861 | 862 | 863 | ## Sizing The Calendar 864 | 865 | You can control the size of the calendar dynamically with the `font-size` property! 866 | 867 | Every element you see on the calendar is relatively sized in `em`'s. The calendar has a container `
` with a class name of `qs-datepicker-container` and a `font-size: 1rem` style on it in the CSS. Simply override that property with inline styles set via JavaScript and watch the calendar resize! For ease, you can access the containing div via the `calendarContainer` property on each instance. For example: 868 | 869 | ```javascript 870 | // Instantiate a datepicker instance. 871 | const picker = datepicker('.some-class') 872 | 873 | // Use JavaScript to change the calendar size. 874 | picker.calendarContainer.style.setProperty('font-size', '1.5rem') 875 | ``` 876 | 877 | 878 | ## Examples 879 | 880 | Simplest usage: 881 | ```javascript 882 | const picker = datepicker('#some-id') 883 | ``` 884 | 885 | Setting up a daterange picker: 886 | ```javascript 887 | const start = datepicker('.start', { id: 1 }) 888 | const end = datepicker('.end', { id: 1 }) 889 | 890 | // NOTE: Any of the other options, as shown below, are valid for range pickers as well. 891 | ``` 892 | 893 | With all other options declared: 894 | ```javascript 895 | const picker = datepicker('#some-id', { 896 | // Event callbacks. 897 | onSelect: instance => { 898 | // Show which date was selected. 899 | console.log(instance.dateSelected) 900 | }, 901 | onShow: instance => { 902 | console.log('Calendar showing.') 903 | }, 904 | onHide: instance => { 905 | console.log('Calendar hidden.') 906 | }, 907 | onMonthChange: instance => { 908 | // Show the month of the selected date. 909 | console.log(instance.currentMonthName) 910 | }, 911 | 912 | // Customizations. 913 | formatter: (input, date, instance) => { 914 | // This will display the date as `1/1/2019`. 915 | input.value = date.toDateString() 916 | }, 917 | position: 'tr', // Top right. 918 | startDay: 1, // Calendar week starts on a Monday. 919 | customDays: ['S', 'M', 'T', 'W', 'Th', 'F', 'S'], 920 | customMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], 921 | customOverlayMonths: ['😀', '😂', '😎', '😍', '🤩', '😜', '😬', '😳', '🤪', '🤓 ', '😝', '😮'], 922 | overlayButton: 'Go!', 923 | overlayPlaceholder: 'Enter a 4-digit year', 924 | 925 | // Settings. 926 | alwaysShow: true, // Never hide the calendar. 927 | dateSelected: new Date(), // Today is selected. 928 | maxDate: new Date(2099, 0, 1), // Jan 1st, 2099. 929 | minDate: new Date(2016, 5, 1), // June 1st, 2016. 930 | startDate: new Date(), // This month. 931 | showAllDates: true, // Numbers for leading & trailing days outside the current month will show. 932 | 933 | // Disabling things. 934 | noWeekends: true, // Saturday's and Sunday's will be unselectable. 935 | disabler: date => (date.getDay() === 2 && date.getMonth() === 9), // Disabled every Tuesday in October 936 | disabledDates: [new Date(2050, 0, 1), new Date(2050, 0, 3)], // Specific disabled dates. 937 | disableMobile: true, // Conditionally disabled on mobile devices. 938 | disableYearOverlay: true, // Clicking the year or month will *not* bring up the year overlay. 939 | 940 | // ID - be sure to provide a 2nd picker with the same id to create a daterange pair. 941 | id: 1 942 | }) 943 | ``` 944 | 945 | 946 | ## License 947 | 948 | ### MIT 949 | 950 | Copyright 2017 - present, Aaron Cordova 951 | 952 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 953 | 954 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 955 | 956 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 957 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodeVersion": "system" 3 | } 4 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /cypress/integration/callbacks.js: -------------------------------------------------------------------------------- 1 | import selectors from '../selectors' 2 | 3 | const { singleDatepickerInput, single } = selectors 4 | 5 | describe('Callback functions provided to datepicker', function() { 6 | beforeEach(function() { 7 | cy.visit('http://localhost:9001') 8 | 9 | /* 10 | We can't simply import the datepicker library up at the top because it will not 11 | be associated with the correct window object. Instead, we can use a Cypress alias 12 | that will expose what we want on `this`, so long as we avoid using arrow functions. 13 | This is possible because datepicker is assigned a value on the window object in `sandbox.js`. 14 | */ 15 | cy.window().then(global => cy.wrap(global.datepicker).as('datepicker')) 16 | }) 17 | 18 | describe('onSelect', function() { 19 | it('should be called after a date has been selected', function() { 20 | const options = { onSelect: () => {} } 21 | const spy = cy.spy(options, 'onSelect') 22 | this.datepicker(singleDatepickerInput, options) 23 | 24 | cy.get(singleDatepickerInput).click() 25 | cy.get(`${single.squaresContainer} [data-direction="0"]`).first().click().then(() => { 26 | expect(spy).to.be.calledOnce 27 | }) 28 | }) 29 | 30 | it('should be called with the correct arguments', function() { 31 | let picker 32 | const today = new Date() 33 | const options = { 34 | onSelect: (...args) => { 35 | expect(args.length, 'onSelect arguments length').to.eq(2) 36 | expect(args[0], 'onSelect 1st arg should be the instance').to.eq(picker) 37 | 38 | /* 39 | We can't use `instanceof Date` because `Date` is a different constructor 40 | than the one on the window object that Cypress uses. Essentially, 41 | we're dealing with 2 different window object. So it's easier to just do 42 | the whole toString thingy. 43 | */ 44 | expect(({}).toString.call(args[1]), 'onSelect 2nd arg should be a date').to.eq('[object Date]') 45 | expect(args[1].getFullYear(), `onSelect 2nd arg year should be today's year`).to.eq(today.getFullYear()) 46 | expect(args[1].getMonth(), `onSelect 2nd arg month should be today's month`).to.eq(today.getMonth()) 47 | } 48 | } 49 | 50 | picker = this.datepicker(singleDatepickerInput, options) 51 | cy.get(singleDatepickerInput).click() 52 | cy.get(`${single.squaresContainer} [data-direction="0"]`).first().click() 53 | }) 54 | }) 55 | 56 | describe('onShow', function() { 57 | it('should be called after the calendar is shown', function() { 58 | const options = { onShow: () => {} } 59 | const spy = cy.spy(options, 'onShow') 60 | this.datepicker(singleDatepickerInput, options) 61 | 62 | expect(options.onShow).not.to.be.called 63 | cy.get(singleDatepickerInput).click().then(() => { 64 | expect(spy).to.be.calledOnce 65 | }) 66 | }) 67 | 68 | it('should be called with the instance as the only argument', function() { 69 | let instance 70 | const options = { 71 | onShow: (...args) => { 72 | expect(args.length, 'onShow arguments length').to.eq(1) 73 | expect(args[0], 'onShow argument should be the instance').to.eq(instance) 74 | } 75 | } 76 | 77 | instance = this.datepicker(singleDatepickerInput, options) 78 | cy.get(singleDatepickerInput).click() 79 | }) 80 | }) 81 | 82 | describe('onHide', function() { 83 | it('should be called after the calendar is hidden', function() { 84 | const options = { onHide: () => {} } 85 | const spy = cy.spy(options, 'onHide') 86 | this.datepicker(singleDatepickerInput, options) 87 | 88 | cy.get(singleDatepickerInput).click().then(() => { 89 | expect(spy).not.to.be.called 90 | 91 | cy.get('body').click().then(() => { 92 | expect(spy).to.be.calledOnce 93 | }) 94 | }) 95 | }) 96 | 97 | it('should be called with the instance as the only argument', function() { 98 | let instance 99 | const options = { 100 | onHide: (...args) => { 101 | expect(args.length, 'onHide arguments length').to.eq(1) 102 | expect(args[0], 'onHide argument should be the instance').to.eq(instance) 103 | } 104 | } 105 | 106 | instance = this.datepicker(singleDatepickerInput, options) 107 | cy.get(singleDatepickerInput).click() 108 | }) 109 | }) 110 | 111 | describe('onMonthChange', function() { 112 | it('should be called when the arrows are clicked', function() { 113 | const options = { onMonthChange: () => {} } 114 | const spy = cy.spy(options, 'onMonthChange') 115 | this.datepicker(singleDatepickerInput, options) 116 | 117 | cy.get(singleDatepickerInput).click() 118 | cy.get(`${single.controls} .qs-arrow.qs-right`).click() 119 | cy.get(`${single.controls} .qs-arrow.qs-left`).click().then(() => { 120 | expect(spy).to.be.calledTwice 121 | }) 122 | }) 123 | 124 | it('should be called with the datepicker instance as the only argument', function() { 125 | let instance 126 | const options = { 127 | onMonthChange: (...args) => { 128 | expect(args.length, 'onMonthChange arguments length').to.eq(1) 129 | expect(args[0], 'onMonthChange argument should be the instance').to.eq(instance) 130 | } 131 | } 132 | 133 | instance = this.datepicker(singleDatepickerInput, options) 134 | cy.get(singleDatepickerInput).click() 135 | cy.get(`${single.controls} .qs-arrow.qs-right`).click() 136 | cy.get(`${single.controls} .qs-arrow.qs-left`).click() 137 | }) 138 | }) 139 | }) 140 | -------------------------------------------------------------------------------- /cypress/integration/errors.js: -------------------------------------------------------------------------------- 1 | import selectors from '../selectors' 2 | 3 | const { 4 | singleDatepickerInput, 5 | daterangeInputStart, 6 | daterangeInputEnd, 7 | } = selectors 8 | 9 | function createMyElement({ global, datepicker, shouldThrow }) { 10 | class MyElement extends global.HTMLElement { 11 | constructor() { 12 | super() 13 | const shadowRoot = this.attachShadow({ mode: 'open' }) 14 | this.root = shadowRoot 15 | shadowRoot.innerHTML = ` 16 |
17 |

Cypress Single Instance Shadow DOM Error Test

18 |
(no styles for this calendar, so the html will explode, lol)
19 | 20 |
21 | ` 22 | 23 | // Create the node we'll pass to datepicker. 24 | this.input = shadowRoot.querySelector('input') 25 | } 26 | 27 | connectedCallback() { 28 | if (shouldThrow) { 29 | expect(() => datepicker(this.root)).to.throw('Using a shadow DOM as your selector is not supported.') 30 | } else { 31 | expect(() => datepicker(this.input)).not.to.throw() 32 | } 33 | } 34 | } 35 | 36 | return MyElement 37 | } 38 | 39 | describe('Errors thrown by datepicker', function() { 40 | beforeEach(function() { 41 | cy.visit('http://localhost:9001') 42 | 43 | /* 44 | We can't simply import the datepicker library up at the top because it will not 45 | be associated with the correct window object. Instead, we can use a Cypress alias 46 | that will expose what we want on `this`, so long as we avoid using arrow functions. 47 | This is possible because datepicker is assigned a value on the window object in `sandbox.js`. 48 | */ 49 | cy.window().then(global => cy.wrap(global.datepicker).as('datepicker')) 50 | }) 51 | 52 | describe('Options', function() { 53 | it('should throw if "events" contains something other than date objects', function() { 54 | const fxnThatThrows = () => this.datepicker(singleDatepickerInput, { events: [new Date(), Date.now()] }) 55 | expect(fxnThatThrows).to.throw('"options.events" must only contain valid JavaScript Date objects.') 56 | }) 57 | 58 | it('should not throw if "events" contains only date objects', function() { 59 | const noThrow = () => this.datepicker(singleDatepickerInput, { events: [new Date(), new Date('1/1/2000')] }) 60 | expect(noThrow).not.to.throw() 61 | }) 62 | 63 | it(`should throw if "startDate", "dateSelected", "minDate", or "maxDate" aren't date objects`, function() { 64 | ['startDate', 'dateSelected', 'minDate', 'maxDate'].forEach(option => { 65 | expect(() => this.datepicker(singleDatepickerInput, { [option]: 'nope' }), `${option} - should throw`) 66 | .to.throw(`"options.${option}" needs to be a valid JavaScript Date object.`) 67 | }) 68 | }) 69 | 70 | it('should not throw if "startDate", "dateSelected", "minDate", and "maxDate" are all date objects', function() { 71 | const today = new Date() 72 | const noThrow = () => this.datepicker(singleDatepickerInput, { 73 | startDate: today, 74 | dateSelected: new Date(today.getFullYear(), today.getMonth(), 5), 75 | minDate: new Date(today.getFullYear(), today.getMonth(), 2), 76 | maxDate: new Date(today.getFullYear(), today.getMonth(), 10), 77 | }) 78 | 79 | expect(noThrow).not.to.throw() 80 | }) 81 | 82 | it(`should throw if "disabledDates" doesn't contain only date objects`, function() { 83 | expect(() => this.datepicker(singleDatepickerInput, { disabledDates: [new Date(), 55]})) 84 | .to.throw('You supplied an invalid date to "options.disabledDates".') 85 | }) 86 | 87 | it('should not throw if "disabledDates" contains only date objects', function() { 88 | const disabledDates = [ 89 | new Date('1/1/1997'), 90 | new Date('1/2/1997'), 91 | new Date('1/3/1997'), 92 | new Date('1/4/1997'), 93 | ] 94 | expect(() => this.datepicker(singleDatepickerInput, { disabledDates })).not.to.throw() 95 | }) 96 | 97 | it('should throw if "disabledDates" contains the same date as "dateSelected"', function() { 98 | const disabledDates = [ 99 | new Date('1/1/1997'), 100 | new Date('1/2/1997'), 101 | new Date('1/3/1997'), 102 | new Date('1/4/1997'), 103 | ] 104 | expect(() => this.datepicker(singleDatepickerInput, { disabledDates, dateSelected: disabledDates[0] })) 105 | .to.throw('"disabledDates" cannot contain the same date as "dateSelected".') 106 | }) 107 | 108 | it('should throw if "id" is null of undefined', function() { 109 | const shouldThrow1 = () => this.datepicker(singleDatepickerInput, { id: null }) 110 | const shouldThrow2 = () => this.datepicker(singleDatepickerInput, { id: undefined }) 111 | 112 | expect(shouldThrow1).to.throw('`id` cannot be `null` or `undefined`') 113 | expect(shouldThrow2).to.throw('`id` cannot be `null` or `undefined`') 114 | }) 115 | 116 | it('should not throw if "id" is not null or undefined', function() { 117 | expect(() => this.datepicker(singleDatepickerInput, { id: () => {} })).to.not.throw() 118 | }) 119 | 120 | it('should throw if more than 2 datepickers try to share an "id"', function() { 121 | const id = Date.now() 122 | const noThrow1 = () => this.datepicker(daterangeInputStart, { id }) 123 | const noThrow2 = () => this.datepicker(daterangeInputEnd, { id }) 124 | const shouldThrow = () => this.datepicker(singleDatepickerInput, { id }) 125 | 126 | expect(noThrow1).to.not.throw() 127 | expect(noThrow2).to.not.throw() 128 | expect(shouldThrow).to.throw('Only two datepickers can share an id.') 129 | }) 130 | 131 | it(`should throw if "position" isn't one of - 'tr', 'tl', 'br', 'bl', or 'c'`, function() { 132 | expect(() => this.datepicker(singleDatepickerInput, { position: 'nope' })) 133 | .to.throw('"options.position" must be one of the following: tl, tr, bl, br, or c.') 134 | }) 135 | 136 | // This test is dependent upon `datepicker.remove()`. 137 | it(`should not throw if "position" is one of - 'tr', 'tl', 'br', 'bl', or 'c'`, function() { 138 | ['tr', 'tl', 'br', 'bl', 'c'].forEach(position => { 139 | let picker 140 | const noThrow = () => { 141 | picker = this.datepicker(singleDatepickerInput, { position }) 142 | } 143 | expect(noThrow).not.to.throw() 144 | picker.remove() 145 | }) 146 | }) 147 | 148 | it('should throw if "maxDate" is less than "minDate" by at least a day', function() { 149 | const minDate = new Date() 150 | const maxDate = new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate() - 1) 151 | 152 | expect(() => this.datepicker(singleDatepickerInput, { maxDate, minDate })) 153 | .to.throw('"maxDate" in options is less than "minDate".') 154 | }) 155 | 156 | it('should not throw if "maxDate" is on the same day as "minDate" (even with different times)', function() { 157 | const today = new Date() 158 | const minDate = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 0, 1) // 1 minute into today. 159 | const maxDate = new Date(today.getFullYear(), today.getMonth(), today.getDate()) // 1 minute BEFORE 'minDate', so "technically" less than. 160 | 161 | expect(() => this.datepicker(singleDatepickerInput, { minDate, maxDate })).not.to.throw() 162 | }) 163 | 164 | // This test is dependent upon `datepicker.remove()`. 165 | it('should not throw if "maxDate" is greater than "minDate" by at least a day', function() { 166 | let picker 167 | const noThrow1 = () => { 168 | const minDate = new Date() 169 | const maxDate = new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate() + 1) 170 | 171 | picker = this.datepicker(singleDatepickerInput, { minDate, maxDate }) 172 | } 173 | const noThrow2 = () => { 174 | // 1 millisecond into the previous day. Since datepicker strips time, this should be converted to the very beginning of the day. 175 | const minDate = new Date(1980, 1, 1, 0, 0, -1) 176 | 177 | // 1 millisecond ahead of 'minDate' - but these should be internally converted to exactly 1 day apart. 178 | const maxDate = new Date(1980, 1, 1) 179 | 180 | this.datepicker(singleDatepickerInput, { minDate, maxDate }) 181 | } 182 | 183 | expect(noThrow1).not.to.throw() 184 | picker.remove() 185 | expect(noThrow2).not.to.throw() 186 | }) 187 | 188 | it('should throw if "dateSelected" is less than "minDate"', function() { 189 | const minDate = new Date(2000, 1, 1) 190 | const dateSelected = new Date(2000, 1, 0) 191 | 192 | expect(() => this.datepicker(singleDatepickerInput, { minDate, dateSelected })) 193 | .to.throw('"dateSelected" in options is less than "minDate".') 194 | }) 195 | 196 | // This test is dependent upon `datepicker.remove()`. 197 | it('should not throw if "dateSelected" is greater than or equal to "minDate"', function() { 198 | const minDate = new Date(2000, 1, 1) 199 | let picker 200 | const noThrow1 = () => { 201 | picker = this.datepicker(singleDatepickerInput, { minDate, dateSelected: minDate }) 202 | } 203 | const noThrow2 = () => { 204 | const dateSelected = new Date(2000, 1, 2) 205 | this.datepicker(singleDatepickerInput, { minDate, dateSelected }) 206 | } 207 | 208 | expect(noThrow1).not.to.throw() 209 | picker.remove() 210 | expect(noThrow2).not.to.throw() 211 | }) 212 | 213 | it('should throw if "dateSelected" is greater than "maxDate"', function() { 214 | const maxDate = new Date(1993, 10, 1) 215 | const dateSelected = new Date(1993, 10, 2) 216 | 217 | expect(() => this.datepicker(singleDatepickerInput, { maxDate, dateSelected })) 218 | .to.throw('"dateSelected" in options is greater than "maxDate".') 219 | }) 220 | 221 | // This test is dependent upon `datepicker.remove()`. 222 | it('should not throw if "dateSelected" is less than or equal to "maxDate"', function() { 223 | const maxDate = new Date(2095, 5, 15) 224 | let picker 225 | const noThrow1 = () => { 226 | picker = this.datepicker(singleDatepickerInput, { maxDate, dateSelected: maxDate }) 227 | } 228 | const noThrow2 = () => { 229 | const dateSelected = new Date(2095, 5, 5) 230 | this.datepicker(singleDatepickerInput, { maxDate, dateSelected }) 231 | } 232 | 233 | expect(noThrow1).not.to.throw() 234 | picker.remove() 235 | expect(noThrow2).not.to.throw() 236 | }) 237 | 238 | it(`should throw if "customDays" isn't an array of 7 strings`, function() { 239 | const customDays1 = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] 240 | const customDays2 = ['a', 'b', 'c', 'd', 'e', 'f'] 241 | const customDays3 = ['a', 'b', 'c', 'd', 'e', 'f', 5] 242 | const customDays4 = [] 243 | const customDays5 = { not: 'happening' } 244 | const customDays6 = () => {} 245 | 246 | [customDays1, customDays2, customDays3, customDays4, customDays5, customDays6].forEach(customDays => { 247 | expect(() => this.datepicker(singleDatepickerInput, { customDays })) 248 | .to.throw('"customDays" must be an array with 7 strings.') 249 | }) 250 | }) 251 | 252 | it('should not throw if "customDays" is an array of 7 strings', function() { 253 | const customDays = [1, 2, 3, 4, 5, 6, 7].map(String) 254 | expect(() => this.datepicker(singleDatepickerInput, { customDays })).not.to.throw() 255 | }) 256 | 257 | it(`should throw if "customMonths" or "customOverlayMonths" isn't an array of 12 strings`, function() { 258 | const arr1 = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm'] 259 | const arr2 = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'] 260 | const arr3 = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 5] 261 | const arr4 = [] 262 | const obj = { not: 'happening' } 263 | const fxn = () => {} 264 | 265 | ['customMonths', 'customOverlayMonths'].forEach(option => { 266 | [arr1, arr2, arr3, arr4, obj, fxn].forEach(optionValue => { 267 | expect(() => this.datepicker(singleDatepickerInput, { [option]: optionValue })) 268 | .to.throw(`"${option}" must be an array with 12 strings.`) 269 | }) 270 | }) 271 | }) 272 | 273 | it('should not throw if "customMonths" and "customOverlayMonths" are arrays of 12 strings', function() { 274 | const customMonths = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map(String) 275 | const customOverlayMonths = customMonths 276 | 277 | expect(() => this.datepicker(singleDatepickerInput, { customMonths, customOverlayMonths })) 278 | .not.to.throw() 279 | }) 280 | 281 | it('should throw if "defaultView" is not the correct value', function() { 282 | expect(() => this.datepicker(singleDatepickerInput, {defaultView: 'nope'})) 283 | .to.throw('options.defaultView must either be "calendar" or "overlay".') 284 | }) 285 | 286 | // This test is dependent upon `datepicker.remove()`. 287 | it('should not throw if "defaultView" is the correct value', function() { 288 | let picker 289 | const noThrow1 = () => { 290 | picker = this.datepicker(singleDatepickerInput, {defaultView: 'calendar'}) 291 | } 292 | const noThrow2 = () => { 293 | picker = this.datepicker(singleDatepickerInput, {defaultView: 'overlay'}) 294 | } 295 | 296 | expect(noThrow1).not.to.throw() 297 | picker.remove() 298 | expect(noThrow2).not.to.throw() 299 | }) 300 | }) 301 | 302 | describe('General Errors', function() { 303 | it('should throw if a shadow DOM is used as the selector', function() { 304 | const datepicker = this.datepicker 305 | 306 | cy.window().then(global => { 307 | cy.document().then(doc => { 308 | const MyElement = createMyElement({ global, datepicker, shouldThrow: true }) 309 | global.customElements.define('my-element', MyElement) 310 | 311 | const myElement = doc.createElement('my-element') 312 | doc.body.prepend(myElement) 313 | }) 314 | }) 315 | }) 316 | 317 | it('should not throw if an element within a shadow DOM is used as the seletor', function() { 318 | const datepicker = this.datepicker 319 | 320 | cy.window().then(global => { 321 | cy.document().then(doc => { 322 | const MyElement = createMyElement({ global, datepicker, shouldThrow: false }) 323 | global.customElements.define('my-element', MyElement) 324 | 325 | const myElement = doc.createElement('my-element') 326 | doc.body.prepend(myElement) 327 | }) 328 | }) 329 | }) 330 | 331 | it('should throw if no selector is provided or the provided selector is not found in the DOM', function() { 332 | expect(() => this.datepicker(`#nope-${Math.random()}`)).to.throw('No selector / element found.') 333 | expect(() => this.datepicker()).to.throw() 334 | }) 335 | 336 | it('should throw if we try to use the same selector/element twice', function() { 337 | this.datepicker(singleDatepickerInput) 338 | expect(() => this.datepicker(singleDatepickerInput)).to.throw('A datepicker already exists on that element.') 339 | }) 340 | }) 341 | 342 | describe('Methods', function() { 343 | describe('setDate', function() { 344 | // This test is dependent upon `datepicker.remove()`. 345 | it(`should throw if the first argument isn't a date object`, function() { 346 | let picker 347 | const toThrow1 = () => { 348 | picker = this.datepicker(singleDatepickerInput) 349 | picker.setDate('hi') 350 | } 351 | const toThrow2 = () => { 352 | picker = this.datepicker(singleDatepickerInput) 353 | picker.setDate(Date.now()) 354 | } 355 | 356 | expect(toThrow1).to.throw('`setDate` needs a JavaScript Date object.') 357 | picker.remove() 358 | expect(toThrow2).to.throw('`setDate` needs a JavaScript Date object.') 359 | }) 360 | 361 | // This test is dependent upon `datepicker.remove()`. 362 | it(`should not throw if the first argument is 'null' or 'undefined'`, function() { 363 | const picker = this.datepicker(singleDatepickerInput) 364 | 365 | expect(() => picker.setDate(null)).not.to.throw() 366 | expect(() => picker.setDate(undefined)).not.to.throw() 367 | expect(() => picker.setDate()).not.to.throw() 368 | }) 369 | 370 | it('should throw if the first argument is a date contained in "disabledDates"', function() { 371 | const today = new Date() 372 | const disabledDates = [today] 373 | const picker = this.datepicker(singleDatepickerInput, { disabledDates }) 374 | 375 | expect(() => picker.setDate(today)).to.throw("You can't manually set a date that's disabled.") 376 | }) 377 | 378 | it('should not throw if the first argument is not a date contained in "disabledDates"', function() { 379 | const picker = this.datepicker(singleDatepickerInput, { disabledDates: [] }) 380 | expect(() => picker.setDate(new Date())).not.to.throw() 381 | }) 382 | }) 383 | 384 | describe('setMin', function() { 385 | it('should throw if an invalid date is given', function() { 386 | const picker = this.datepicker(singleDatepickerInput) 387 | expect(() => picker.setMin('not a date!')).to.throw('Invalid date passed to setMin') 388 | }) 389 | 390 | it('(daterange - first) should throw if new date is > date selected', function() { 391 | const dateSelected = new Date() 392 | const newDate = new Date(dateSelected.getFullYear(), dateSelected.getMonth(), dateSelected.getDate() + 1) 393 | const startPicker = this.datepicker(daterangeInputStart, { dateSelected }) 394 | this.datepicker(daterangeInputEnd, { dateSelected }) 395 | 396 | expect(() => startPicker.setMin(newDate)).to.throw('Out-of-range date passed to setMin') 397 | }) 398 | 399 | it('(daterange - second) should throw if new date is > date selected', function() { 400 | const dateSelected = new Date() 401 | const newDate = new Date(dateSelected.getFullYear(), dateSelected.getMonth(), dateSelected.getDate() + 1) 402 | this.datepicker(daterangeInputStart, { dateSelected }) 403 | const endPicker = this.datepicker(daterangeInputEnd, { dateSelected }) 404 | 405 | expect(() => endPicker.setMin(newDate)).to.throw('Out-of-range date passed to setMin') 406 | }) 407 | 408 | it('should throw if new date is > date selected', function() { 409 | const dateSelected = new Date() 410 | const newDate = new Date(dateSelected.getFullYear(), dateSelected.getMonth(), dateSelected.getDate() + 1) 411 | const picker = this.datepicker(singleDatepickerInput, { dateSelected }) 412 | 413 | expect(() => picker.setMin(newDate)).to.throw('Out-of-range date passed to setMin') 414 | }) 415 | }) 416 | 417 | describe('setMax', function() { 418 | it('should throw if an invalid date is given', function() { 419 | const picker = this.datepicker(singleDatepickerInput) 420 | expect(() => picker.setMax('not a date!')).to.throw('Invalid date passed to setMax') 421 | }) 422 | 423 | it('(daterange - first) should throw if new date is < date selected', function() { 424 | const dateSelected = new Date() 425 | const newDate = new Date(dateSelected.getFullYear(), dateSelected.getMonth(), dateSelected.getDate() - 1) 426 | const startPicker = this.datepicker(daterangeInputStart, { dateSelected }) 427 | this.datepicker(daterangeInputEnd, { dateSelected }) 428 | 429 | expect(() => startPicker.setMax(newDate)).to.throw('Out-of-range date passed to setMax') 430 | }) 431 | 432 | it('(daterange - second) should throw if new date is < date selected', function() { 433 | const dateSelected = new Date() 434 | const newDate = new Date(dateSelected.getFullYear(), dateSelected.getMonth(), dateSelected.getDate() - 1) 435 | this.datepicker(daterangeInputStart, { dateSelected }) 436 | const endPicker = this.datepicker(daterangeInputEnd, { dateSelected }) 437 | 438 | expect(() => endPicker.setMax(newDate)).to.throw('Out-of-range date passed to setMax') 439 | }) 440 | 441 | it('should throw if new date is < date selected', function() { 442 | const dateSelected = new Date() 443 | const newDate = new Date(dateSelected.getFullYear(), dateSelected.getMonth(), dateSelected.getDate() - 1) 444 | const picker = this.datepicker(singleDatepickerInput, { dateSelected }) 445 | 446 | expect(() => picker.setMax(newDate)).to.throw('Out-of-range date passed to setMax') 447 | }) 448 | }) 449 | 450 | describe('navigate', function() { 451 | it('should throw if an invalid date is given', function() { 452 | const picker = this.datepicker(singleDatepickerInput) 453 | expect(() => picker.navigate('not a date!')).to.throw('Invalid date passed to `navigate`') 454 | }) 455 | }) 456 | }) 457 | }) 458 | -------------------------------------------------------------------------------- /cypress/integration/options.js: -------------------------------------------------------------------------------- 1 | import selectors from '../selectors' 2 | import pickerProperties from '../pickerProperties' 3 | 4 | const { 5 | singleDatepickerInput, 6 | single, 7 | range, 8 | common, 9 | singleDatepickerInputParent, 10 | daterangeInputStart, 11 | daterangeInputEnd, 12 | } = selectors 13 | 14 | describe('User options', function() { 15 | beforeEach(function() { 16 | cy.visit('http://localhost:9001') 17 | 18 | /* 19 | We can't simply import the datepicker library up at the top because it will not 20 | be associated with the correct window object. Instead, we can use a Cypress alias 21 | that will expose what we want on `this`, so long as we avoid using arrow functions. 22 | This is possible because datepicker is assigned a value on the window object in `sandbox.js`. 23 | */ 24 | cy.window().then(global => cy.wrap(global.datepicker).as('datepicker')) 25 | }) 26 | 27 | describe('Customizations', function() { 28 | describe('formatter', function() { 29 | it('should customize the input value when a date is selected', function() { 30 | const expectedValue = 'datepicker rulez' 31 | const options = { 32 | formatter: (input, date, instance) => { 33 | input.value = expectedValue 34 | } 35 | } 36 | this.datepicker(singleDatepickerInput, options) 37 | 38 | cy.get(singleDatepickerInput).should('have.value', '').click() 39 | cy.get(`${single.calendarContainer} [data-direction="0"]`).first().click() 40 | cy.get(singleDatepickerInput).should('have.value', expectedValue) 41 | }) 42 | 43 | it('should be called with the correct arguments', function() { 44 | let picker 45 | const today = new Date() 46 | const selectedDate = new Date(today.getFullYear(), today.getMonth(), 1) 47 | const options = { 48 | formatter: (input, date, instance) => { 49 | expect(input, '1st arg to `formatter` should be the input').to.eq(instance.el) 50 | 51 | /* 52 | We can't use `instanceof Date` because `Date` is a different constructor 53 | than the one on the window object that Cypress uses. Essentially, 54 | we're dealing with 2 different window object. So it's easier to just do 55 | the whole toString thingy. 56 | */ 57 | expect(({}).toString.call(date), '2nd arg to `formatter` should be the date selected').to.eq('[object Date]') 58 | expect(+instance.dateSelected, 'the date should === instance.dateSelected').to.eq(+date) 59 | expect(+selectedDate, 'the selected date should have the correct value').to.eq(+date) 60 | expect(instance, '3rd arg to `formatter` should be the instance').to.eq(picker) 61 | } 62 | } 63 | 64 | picker = this.datepicker(singleDatepickerInput, options) 65 | cy.get(singleDatepickerInput).click() 66 | cy.get(`${single.calendarContainer} [data-direction="0"]`).first().click() 67 | }) 68 | 69 | it(`should not be called if the picker doesn't have an associated input`, function() { 70 | const options = { formatter: () => {} } 71 | const spy = cy.spy(options, 'formatter') 72 | this.datepicker(singleDatepickerInputParent, options) 73 | 74 | cy.get(singleDatepickerInputParent).click({ force: true }) 75 | cy.get(`${common.squaresContainer} [data-direction="0"]`).first().click().then(() => { 76 | expect(spy).not.to.be.called 77 | }) 78 | }) 79 | }) 80 | 81 | describe('position', function() { 82 | it('should position the calendar relative to the input - default (bottom left)', function() { 83 | this.datepicker(singleDatepickerInput) 84 | 85 | cy.get(singleDatepickerInput).click() 86 | cy.get(single.calendarContainer).should('have.attr', 'style') 87 | cy.get(single.calendarContainer).then($calendarContainer => { 88 | const {top, right, bottom, left} = $calendarContainer[0].style 89 | 90 | expect(+top.replace('px', '')).to.be.greaterThan(0) 91 | expect(right).to.equal('') 92 | expect(bottom).to.equal('') 93 | expect(left).to.equal('0px') 94 | }) 95 | }) 96 | 97 | it('should position the calendar relative to the input - bottom left', function() { 98 | this.datepicker(singleDatepickerInput, {position: 'bl'}) 99 | 100 | cy.get(singleDatepickerInput).click() 101 | cy.get(single.calendarContainer).should('have.attr', 'style') 102 | cy.get(single.calendarContainer).then($calendarContainer => { 103 | const {top, right, bottom, left} = $calendarContainer[0].style 104 | 105 | expect(+top.replace('px', '')).to.be.greaterThan(0) 106 | expect(right).to.equal('') 107 | expect(bottom).to.equal('') 108 | expect(left).to.equal('0px') 109 | }) 110 | }) 111 | 112 | it('should position the calendar relative to the input - bottom right', function() { 113 | this.datepicker(singleDatepickerInput, {position: 'br'}) 114 | 115 | cy.get(singleDatepickerInput).click() 116 | cy.get(single.calendarContainer).should('have.attr', 'style') 117 | cy.get(single.calendarContainer).then($calendarContainer => { 118 | const {top, right, bottom, left} = $calendarContainer[0].style 119 | 120 | expect(+top.replace('px', '')).to.be.greaterThan(0) 121 | expect(right).to.equal('') 122 | expect(bottom).to.equal('') 123 | expect(+left.replace('px', '')).to.be.greaterThan(0) 124 | }) 125 | }) 126 | 127 | it('should position the calendar relative to the input - top left', function() { 128 | this.datepicker(singleDatepickerInput, {position: 'tl'}) 129 | 130 | cy.get(singleDatepickerInput).click() 131 | cy.get(single.calendarContainer).should('have.attr', 'style') 132 | cy.get(single.calendarContainer).then($calendarContainer => { 133 | const {top, right, bottom, left} = $calendarContainer[0].style 134 | 135 | expect(+top.replace('px', '')).to.be.lessThan(0) 136 | expect(right).to.equal('') 137 | expect(bottom).to.equal('') 138 | expect(left).to.equal('0px') 139 | }) 140 | }) 141 | 142 | it('should position the calendar relative to the input - top right', function() { 143 | this.datepicker(singleDatepickerInput, {position: 'tr'}) 144 | 145 | cy.get(singleDatepickerInput).click() 146 | cy.get(single.calendarContainer).should('have.attr', 'style') 147 | cy.get(single.calendarContainer).then($calendarContainer => { 148 | const {top, right, bottom, left} = $calendarContainer[0].style 149 | 150 | expect(+top.replace('px', '')).to.be.lessThan(0) 151 | expect(right).to.equal('') 152 | expect(bottom).to.equal('') 153 | expect(+left.replace('px', '')).to.be.greaterThan(0) 154 | }) 155 | }) 156 | }) 157 | 158 | describe('startDay', function() { 159 | it('should start the calendar on the specified day of the week (Monday)', function() { 160 | this.datepicker(singleDatepickerInput, {startDay: 1}) 161 | 162 | cy.get(single.squares).then($squares => { 163 | const [firstSquare] = $squares 164 | expect(firstSquare.textContent).to.equal('Mon') 165 | }) 166 | }) 167 | 168 | it('should start the calendar on the specified day of the week (Thursday)', function() { 169 | this.datepicker(singleDatepickerInput, {startDay: 4}) 170 | 171 | cy.get(single.squares).then($squares => { 172 | const [firstSquare] = $squares 173 | expect(firstSquare.textContent).to.equal('Thu') 174 | }) 175 | }) 176 | }) 177 | 178 | describe('customDays', function() { 179 | it('should display custom days in the calendar header', function() { 180 | const customDays = 'abcdefg'.split('') 181 | this.datepicker(singleDatepickerInput, {customDays}) 182 | 183 | cy.get(single.squares).then($squares => { 184 | Array 185 | .from($squares) 186 | .slice(0, 7) 187 | .map(el => el.textContent) 188 | .forEach((text, i) => expect(text).to.equal(customDays[i])) 189 | }) 190 | }) 191 | }) 192 | 193 | describe('customMonths', function() { 194 | const customMonths = [ 195 | 'Enero', 196 | 'Febrero', 197 | 'Marzo', 198 | 'Abril', 199 | 'Mayo', 200 | 'Junio', 201 | 'Julio', 202 | 'Agosto', 203 | 'Septiembre', 204 | 'Octubre', 205 | 'Noviembre', 206 | 'Diciembre' 207 | ] 208 | 209 | it('should display custom month names in the header', function() { 210 | let currentMonthIndex = new Date().getMonth() 211 | 212 | // `alwaysShow` is used simply to avoid having to click to open the calendar each time. 213 | this.datepicker(singleDatepickerInput, {customMonths, alwaysShow: 1}) 214 | 215 | // https://stackoverflow.com/a/53487016/2525633 - Use array iteration as opposed to a for-loop. 216 | Array.from({length: 12}, (_, i) => { 217 | if (currentMonthIndex > 11) currentMonthIndex = 0 218 | 219 | cy.get(`${single.controls} .qs-month`).should('have.text', customMonths[currentMonthIndex]) 220 | cy.get(`${single.controls} .qs-arrow.qs-right`).click() 221 | 222 | currentMonthIndex++ 223 | }) 224 | }) 225 | 226 | it('should display abbreviated custom month names in the overlay', function() { 227 | this.datepicker(singleDatepickerInput, {customMonths}) 228 | 229 | cy.get(common.overlayMonth).then($months => { 230 | Array.from($months).forEach((month, i) => { 231 | expect(month.textContent).to.equal(customMonths[i].slice(0, 3)) 232 | }) 233 | }) 234 | }) 235 | }) 236 | 237 | describe('customOverlayMonths', function() { 238 | const customOverlayMonths = 'abcdefghijkl'.split('') 239 | 240 | it('should display custom abbreviated month names in the overlay', function() { 241 | this.datepicker(singleDatepickerInput, {customOverlayMonths}) 242 | 243 | cy.get(common.overlayMonth).then($months => { 244 | Array.from($months).forEach((month, i) => { 245 | expect(month.textContent).to.equal(customOverlayMonths[i]) 246 | }) 247 | }) 248 | }) 249 | 250 | it('should display custom abbreviated month names in the overlay, unaffected by `customMonths`', function() { 251 | const customMonths = [ 252 | 'Enero', 253 | 'Febrero', 254 | 'Marzo', 255 | 'Abril', 256 | 'Mayo', 257 | 'Junio', 258 | 'Julio', 259 | 'Agosto', 260 | 'Septiembre', 261 | 'Octubre', 262 | 'Noviembre', 263 | 'Diciembre' 264 | ] 265 | this.datepicker(singleDatepickerInput, {customOverlayMonths, customMonths}) 266 | 267 | cy.get(common.overlayMonth).then($months => { 268 | Array.from($months).forEach((month, i) => { 269 | expect(month.textContent).to.equal(customOverlayMonths[i]) 270 | }) 271 | }) 272 | }) 273 | }) 274 | 275 | describe('overlayButton', function() { 276 | it('should display custom text for the overlay button', function() { 277 | const overlayButton = '¡Vamanos!' 278 | this.datepicker(singleDatepickerInput, {overlayButton}) 279 | 280 | cy.get(common.overlaySubmit).should('have.text', overlayButton) 281 | }) 282 | }) 283 | 284 | describe('overlayPlaceholder', function() { 285 | it('should display custom placeholder text for the overlay year-input', function() { 286 | const overlayPlaceholder = 'Entrar un año' 287 | this.datepicker(singleDatepickerInput, {overlayPlaceholder}) 288 | 289 | cy.get(common.overlayYearInput).should('have.attr', 'placeholder', overlayPlaceholder) 290 | }) 291 | }) 292 | 293 | describe('events', function() { 294 | it('should show a blue dot next to each day for the dates provided', function() { 295 | const today = new Date() 296 | const year = today.getFullYear() 297 | const month = today.getMonth() 298 | const days = [5, 10, 15] 299 | const events = days.map(day => new Date(year, month, day)) 300 | this.datepicker(singleDatepickerInput, {events}) 301 | 302 | cy.get(singleDatepickerInput).click() 303 | cy.get(common.squareWithNum).then($squares => { 304 | Array.from($squares).forEach((square, i) => { 305 | const day = i + 1 306 | 307 | if (days.includes(day)) { 308 | // https://stackoverflow.com/a/55517628/2525633 - get pseudo element styles in Cypress. 309 | const win = square.ownerDocument.defaultView 310 | const after = win.getComputedStyle(square, 'after') 311 | 312 | expect(square.classList.contains('qs-event'), day).to.equal(true) 313 | expect(after.backgroundColor).to.equal('rgb(0, 119, 255)') 314 | expect(after.borderRadius).to.equal('50%') 315 | } else { 316 | expect(square.classList.contains('qs-event'), day).to.equal(false) 317 | } 318 | }) 319 | }) 320 | }) 321 | }) 322 | }) 323 | 324 | describe('Settings', function() { 325 | describe('alwaysShow', function() { 326 | it('should always show the calendar', function() { 327 | const todaysDate = new Date().getDate() 328 | this.datepicker(singleDatepickerInput, {alwaysShow: true}) 329 | 330 | cy.get(single.calendarContainer).should('be.visible') 331 | cy.get('body').click() 332 | cy.get(single.calendarContainer).should('be.visible') 333 | cy.get(common.squareWithNum).eq(todaysDate === 1 ? 1 : 0).click() 334 | cy.get(single.calendarContainer).should('be.visible') 335 | }) 336 | }) 337 | 338 | describe('dateSelected', function() { 339 | it('should have a date selected on the calendar', function() { 340 | const dateSelected = new Date() 341 | const date = dateSelected.getDate() 342 | this.datepicker(singleDatepickerInput, {dateSelected}) 343 | 344 | cy.get('.qs-active').should('have.text', date) 345 | cy.get(singleDatepickerInput).should('have.value', dateSelected.toDateString()) 346 | }) 347 | }) 348 | 349 | describe('maxDate', function() { 350 | it('should disable dates beyond the date provided', function() { 351 | const maxDate = new Date() 352 | const todaysDate = maxDate.getDate() 353 | this.datepicker(singleDatepickerInput, {maxDate}) 354 | 355 | cy.get(singleDatepickerInput).click() 356 | cy.get(common.squareWithNum).then($squares => { 357 | Array.from($squares).forEach((square, i) => { 358 | const win = square.ownerDocument.defaultView 359 | const {opacity, cursor} = win.getComputedStyle(square) 360 | 361 | if (i + 1 > todaysDate) { 362 | expect(square.classList.contains('qs-disabled')).to.equal(true) 363 | expect(opacity, 'disabled date opacity').to.equal('0.2') 364 | expect(cursor, 'disabled date cursor').to.equal('not-allowed') 365 | } else { 366 | expect(square.classList.contains('qs-disabled')).to.equal(false) 367 | expect(opacity, 'enabled date opacity').to.equal('1') 368 | expect(cursor, 'enabled date cursor').to.equal('pointer') 369 | } 370 | }) 371 | }) 372 | 373 | // Check the next month. 374 | cy.get(`${single.controls} .qs-arrow.qs-right`).click() 375 | cy.get(common.squareWithNum).then($squares => { 376 | Array.from($squares).forEach((square, i) => { 377 | const win = square.ownerDocument.defaultView 378 | const {opacity, cursor} = win.getComputedStyle(square) 379 | 380 | expect(square.classList.contains('qs-disabled')).to.equal(true) 381 | expect(opacity, 'disabled date opacity').to.equal('0.2') 382 | expect(cursor, 'disabled date cursor').to.equal('not-allowed') 383 | }) 384 | }) 385 | 386 | // Check the month before. 387 | cy.get(`${single.controls} .qs-arrow.qs-left`).click() 388 | cy.get(`${single.controls} .qs-arrow.qs-left`).click() 389 | cy.get(common.squareWithNum).then($squares => { 390 | Array.from($squares).forEach((square, i) => { 391 | const win = square.ownerDocument.defaultView 392 | const {opacity, cursor} = win.getComputedStyle(square) 393 | 394 | expect(square.classList.contains('qs-disabled')).to.equal(false) 395 | expect(opacity, 'enabled date opacity').to.equal('1') 396 | expect(cursor, 'enabled date cursor').to.equal('pointer') 397 | }) 398 | }) 399 | }) 400 | }) 401 | 402 | describe('minDate', function() { 403 | it('should disable dates beyond the date provided', function() { 404 | const minDate = new Date() 405 | const todaysDate = minDate.getDate() 406 | this.datepicker(singleDatepickerInput, {minDate}) 407 | 408 | cy.get(singleDatepickerInput).click() 409 | cy.get(common.squareWithNum).then($squares => { 410 | Array.from($squares).forEach((square, i) => { 411 | const win = square.ownerDocument.defaultView 412 | const {opacity, cursor} = win.getComputedStyle(square) 413 | 414 | if (i + 1 < todaysDate) { 415 | expect(square.classList.contains('qs-disabled')).to.equal(true) 416 | expect(opacity, 'disabled date opacity').to.equal('0.2') 417 | expect(cursor, 'disabled date cursor').to.equal('not-allowed') 418 | } else { 419 | expect(square.classList.contains('qs-disabled')).to.equal(false) 420 | expect(opacity, 'enabled date opacity').to.equal('1') 421 | expect(cursor, 'enabled date cursor').to.equal('pointer') 422 | } 423 | }) 424 | }) 425 | 426 | // Check the next month. 427 | cy.get(`${single.controls} .qs-arrow.qs-right`).click() 428 | cy.get(common.squareWithNum).then($squares => { 429 | Array.from($squares).forEach((square, i) => { 430 | const win = square.ownerDocument.defaultView 431 | const {opacity, cursor} = win.getComputedStyle(square) 432 | 433 | expect(square.classList.contains('qs-disabled')).to.equal(false) 434 | expect(opacity, 'enabled date opacity').to.equal('1') 435 | expect(cursor, 'enabled date cursor').to.equal('pointer') 436 | }) 437 | }) 438 | 439 | // Check the month before. 440 | cy.get(`${single.controls} .qs-arrow.qs-left`).click() 441 | cy.get(`${single.controls} .qs-arrow.qs-left`).click() 442 | cy.get(common.squareWithNum).then($squares => { 443 | Array.from($squares).forEach((square, i) => { 444 | const win = square.ownerDocument.defaultView 445 | const {opacity, cursor} = win.getComputedStyle(square) 446 | 447 | expect(square.classList.contains('qs-disabled')).to.equal(true) 448 | expect(opacity, 'disabled date opacity').to.equal('0.2') 449 | expect(cursor, 'disabled date cursor').to.equal('not-allowed') 450 | }) 451 | }) 452 | }) 453 | }) 454 | 455 | describe('startDate', function() { 456 | it('should start the calendar in the month & year of the date provided', function() { 457 | const {months} = pickerProperties 458 | const startDate = new Date() 459 | const year = startDate.getFullYear() 460 | const currentMonthName = months[startDate.getMonth()] 461 | this.datepicker(singleDatepickerInput, {startDate}) 462 | 463 | cy.get(`${single.controls} .qs-month`).should('have.text', currentMonthName) 464 | cy.get(`${single.controls} .qs-year`).should('have.text', year) 465 | }) 466 | }) 467 | 468 | describe('showAllDates', function() { 469 | it('should show numbers for dates outside the current month', function() { 470 | this.datepicker(singleDatepickerInput, {showAllDates: true}) 471 | 472 | cy.get(`${single.squaresContainer} .qs-outside-current-month`).then($squares => { 473 | Array.from($squares).forEach(square => { 474 | const win = square.ownerDocument.defaultView 475 | const {opacity, cursor} = win.getComputedStyle(square) 476 | const num = +square.textContent 477 | 478 | const {dataset} = square 479 | expect(dataset.hasOwnProperty('direction')) 480 | expect(opacity, 'date outside current month - opacity').to.equal('0.2') 481 | expect(cursor, 'date outside current month - cursor').to.equal('pointer') 482 | expect(num, 'number inside square').to.be.greaterThan(0) 483 | }) 484 | }) 485 | }) 486 | }) 487 | 488 | describe('respectDisabledReadOnly', function() { 489 | it('should show a non-selectable calendar when the input has the `disabled` property', function() { 490 | cy.window().then(global => { 491 | const input = global.document.querySelector(singleDatepickerInput) 492 | input.setAttribute('disabled', '') 493 | 494 | // Using `alwaysShow` otherwise the calendar won't be able to be shown since the input is disabled. 495 | global.datepicker(singleDatepickerInput, {respectDisabledReadOnly: true, alwaysShow: true}) 496 | 497 | // Selecting days should have no effect. 498 | cy.get(`${single.squaresContainer} .qs-active`).should('have.length', 0) 499 | cy.get(`${single.squaresContainer} .qs-num`).first().click() 500 | cy.get(`${single.squaresContainer} .qs-active`).should('have.length', 0) 501 | 502 | // You should be able to change months. 503 | const initialMonthName = global.document.querySelector('.qs-month').textContent 504 | cy.get(`${single.controls} .qs-arrow.qs-right`).click().then(() => { 505 | const nextMonthName = global.document.querySelector('.qs-month').textContent 506 | expect(initialMonthName).not.to.equal(nextMonthName) 507 | 508 | // You should be able to use the overlay. 509 | const initialYear = global.document.querySelector('.qs-year').textContent 510 | cy.get('.qs-month-year').click() 511 | cy.get(common.overlayYearInput).type('2099') 512 | cy.get(common.overlaySubmit).click().then(() => { 513 | const otherYear = global.document.querySelector('.qs-year').textContent 514 | expect(initialYear).not.to.equal(otherYear) 515 | cy.get('.qs-year').should('have.text', '2099') 516 | }) 517 | }) 518 | }) 519 | }) 520 | 521 | it('should show a non-selectable calendar when the input has the `readonly` property', function() { 522 | cy.window().then(global => { 523 | const input = global.document.querySelector(singleDatepickerInput) 524 | input.setAttribute('readonly', '') 525 | global.datepicker(singleDatepickerInput, {respectDisabledReadOnly: true}) 526 | 527 | // Selecting days should have no effect. 528 | cy.get(singleDatepickerInput).click() 529 | cy.get(`${single.squaresContainer} .qs-active`).should('have.length', 0) 530 | cy.get(`${single.squaresContainer} .qs-num`).first().click() 531 | cy.get(`${single.squaresContainer} .qs-active`).should('have.length', 0) 532 | 533 | // You should be able to change months. 534 | const initialMonthName = global.document.querySelector('.qs-month').textContent 535 | cy.get(`${single.controls} .qs-arrow.qs-right`).click().then(() => { 536 | const nextMonthName = global.document.querySelector('.qs-month').textContent 537 | expect(initialMonthName).not.to.equal(nextMonthName) 538 | 539 | // You should be able to use the overlay. 540 | const initialYear = global.document.querySelector('.qs-year').textContent 541 | cy.get('.qs-month-year').click() 542 | cy.get(common.overlayYearInput).type('2099') 543 | cy.get(common.overlaySubmit).click().then(() => { 544 | const otherYear = global.document.querySelector('.qs-year').textContent 545 | expect(initialYear).not.to.equal(otherYear) 546 | cy.get('.qs-year').should('have.text', '2099') 547 | }) 548 | }) 549 | }) 550 | }) 551 | }) 552 | }) 553 | 554 | describe('Disabling Things', function() { 555 | describe('noWeekends', function() { 556 | it('should disable Saturday and Sunday', function() { 557 | const date = new Date() 558 | this.datepicker(singleDatepickerInput, {noWeekends: true}) 559 | 560 | cy.get(common.squareWithNum).then($squares => { 561 | const newDate = new Date(date.getFullYear(), date.getMonth(), 1) 562 | let index = newDate.getDay() 563 | 564 | Array.from($squares).forEach(square => { 565 | if (index === 7) index = 0 566 | 567 | if ((index === 0 || index === 6) && !square.classList.contains('qs-outside-current-month')) { 568 | expect(square.classList.contains('qs-disabled'), square.textContent).to.equal(true) 569 | } else { 570 | expect(square.classList.contains('qs-disabled'), square.textContent).to.equal(false) 571 | } 572 | 573 | index++ 574 | }) 575 | }) 576 | }) 577 | 578 | it('should disable Saturday and Sunday even when Sunday is not the start day', function() { 579 | const date = new Date() 580 | this.datepicker(singleDatepickerInput, {noWeekends: true, startDay: 3}) 581 | 582 | cy.get(common.squareWithNum).then($squares => { 583 | const newDate = new Date(date.getFullYear(), date.getMonth(), 1) 584 | let index = newDate.getDay() 585 | 586 | Array.from($squares).forEach(square => { 587 | if (index === 7) index = 0 588 | 589 | if ((index === 0 || index === 6) && !square.classList.contains('qs-outside-current-month')) { 590 | expect(square.classList.contains('qs-disabled'), square.textContent).to.equal(true) 591 | } else { 592 | expect(square.classList.contains('qs-disabled'), square.textContent).to.equal(false) 593 | } 594 | 595 | index++ 596 | }) 597 | }) 598 | }) 599 | 600 | it('should disable Saturday and Sunday even when `showAllDates` is true', function() { 601 | const date = new Date() 602 | this.datepicker(singleDatepickerInput, {noWeekends: true, showAllDates: true}) 603 | 604 | cy.get(common.squareWithNum).then($squares => { 605 | const newDate = new Date(date.getFullYear(), date.getMonth(), 1) 606 | let index = 0 607 | 608 | Array.from($squares).forEach(square => { 609 | if (index === 7) index = 0 610 | 611 | if (index === 0 || index === 6) { 612 | expect(square.classList.contains('qs-disabled'), square.textContent).to.equal(true) 613 | } else { 614 | expect(square.classList.contains('qs-disabled'), square.textContent).to.equal(false) 615 | } 616 | 617 | index++ 618 | }) 619 | }) 620 | }) 621 | }) 622 | 623 | describe('disabler', function() { 624 | it('should disable all odd days', function() { 625 | this.datepicker(singleDatepickerInput, { 626 | disabler: date => date.getDate() % 2, 627 | }) 628 | 629 | cy.get(singleDatepickerInput).click() 630 | cy.get(common.squareWithNum).first().click() 631 | cy.get(singleDatepickerInput).should('have.value', '') 632 | cy.get(common.squareWithNum).eq(1).click() 633 | cy.get(singleDatepickerInput).should('not.have.value', '') 634 | }) 635 | 636 | it('should disable days outside the calendar month as well', function() { 637 | this.datepicker(singleDatepickerInput, { 638 | disabler: date => true, 639 | showAllDates: true, 640 | }) 641 | 642 | cy.get(singleDatepickerInput).click() 643 | cy.get(common.squareOutsideCurrentMonth).should($squares => { 644 | Array.from($squares).forEach(square => { 645 | expect(square.classList.contains('qs-disabled')).to.equal(true) 646 | }) 647 | }) 648 | }) 649 | }) 650 | 651 | describe('disabledDates', function() { 652 | it('should disable the dates provided', function() { 653 | const today = new Date() 654 | this.datepicker(singleDatepickerInput, { 655 | disabledDates: [ 656 | new Date(today.getFullYear(), today.getMonth(), 1), 657 | new Date(today.getFullYear(), today.getMonth(), 17), 658 | new Date(today.getFullYear(), today.getMonth(), 25), 659 | ], 660 | }) 661 | 662 | cy.get(`${single.squaresContainer} .qs-disabled`) 663 | .should('have.length', 3) 664 | .should($days => { 665 | expect($days.eq(0).text()).to.equal('1') 666 | expect($days.eq(1).text()).to.equal('17') 667 | expect($days.eq(2).text()).to.equal('25') 668 | }) 669 | }) 670 | }) 671 | 672 | /* 673 | I don't know how to test `disableMobile` since there is logic with that option 674 | to check if the device is mobile or not. 675 | */ 676 | 677 | describe('disableYearOverlay', function() { 678 | it('should disable showing the overlay', function() { 679 | this.datepicker(singleDatepickerInput, {disableYearOverlay: true}) 680 | 681 | cy.get(singleDatepickerInput).click() 682 | cy.get(single.overlay).should('have.class', 'qs-hidden') 683 | cy.get(`${single.controls} .qs-month-year`).should('have.class', 'qs-disabled-year-overlay') 684 | cy.get(`${single.controls} .qs-month-year`).click() 685 | cy.get(single.overlay).should('have.class', 'qs-hidden') 686 | }) 687 | }) 688 | 689 | describe('disabled - an instance property, not an option property', function() { 690 | it('should disable the calendar from showing', function() { 691 | const instance = this.datepicker(singleDatepickerInput) 692 | instance.disabled = true 693 | 694 | cy.get(singleDatepickerInput).click() 695 | cy.get(single.calendarContainer).should('not.be.visible') 696 | }) 697 | 698 | it('should disable all functionality for a calendar that is showing', function() { 699 | const instance = this.datepicker(singleDatepickerInput, {alwaysShow: true}) 700 | instance.disabled = true 701 | const currentMonthName = pickerProperties.months[new Date().getMonth()] 702 | 703 | // Clicking a day. 704 | cy.get(common.squareWithNum).eq(0).click() 705 | cy.get(singleDatepickerInput).should('have.value', '') 706 | 707 | // Clicking the arrows. 708 | cy.get(`${single.controls} .qs-month`).should('have.text', currentMonthName) 709 | cy.get(`${single.controls} .qs-arrow.qs-left`).click() 710 | cy.get(`${single.controls} .qs-month`).should('have.text', currentMonthName) 711 | 712 | // Clicking to try to show the overlay. 713 | cy.get(`${single.controls} .qs-month-year`).click() 714 | cy.get(common.overlay).should('not.be.visible') 715 | }) 716 | }) 717 | 718 | describe('id - only for daterange pickers', function() { 719 | it('should create and link a daterange pair', function() { 720 | const id = 1 721 | const pickerStart = this.datepicker(daterangeInputStart, {id}) 722 | const pickerEnd = this.datepicker(daterangeInputEnd, {id}) 723 | const pickerSingle = this.datepicker(singleDatepickerInput) 724 | 725 | // `id` property. 726 | expect(pickerStart.id).to.equal(id) 727 | expect(pickerEnd.id).to.equal(id) 728 | expect(pickerSingle.id).to.equal(undefined) 729 | 730 | // Only ranges should have the `getRange` method. 731 | expect(typeof pickerStart.getRange, 'getRange - start').to.equal('function') 732 | expect(typeof pickerEnd.getRange, 'getRange - end').to.equal('function') 733 | expect(pickerSingle.getRange, `getRange shouldn't exist on a single picker`).to.equal(undefined) 734 | }) 735 | }) 736 | }) 737 | }) 738 | -------------------------------------------------------------------------------- /cypress/pickerProperties.js: -------------------------------------------------------------------------------- 1 | import selectors from './selectors' 2 | 3 | const getFirstElement = elements => elements[0] 4 | const tempDate = new Date() 5 | const date = new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate()) 6 | const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] 7 | const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] 8 | 9 | const singleDatepickerProperties = [ 10 | { 11 | property: 'alwaysShow', 12 | defaultValue: false, 13 | }, 14 | { 15 | property: 'calendar', 16 | defaultValue: getFirstElement, 17 | domElement: true, 18 | selector: selectors.single.calendar, 19 | }, 20 | { 21 | property: 'calendarContainer', 22 | defaultValue: getFirstElement, 23 | domElement: true, 24 | selector: selectors.single.calendarContainer, 25 | }, 26 | { 27 | property: 'currentMonth', 28 | defaultValue: date.getMonth(), 29 | }, 30 | { 31 | property: 'currentMonthName', 32 | defaultValue: months[date.getMonth()], 33 | }, 34 | { 35 | property: 'currentYear', 36 | defaultValue: date.getFullYear(), 37 | }, 38 | { 39 | property: 'customElement', 40 | defaultValue: undefined, 41 | }, 42 | { 43 | property: 'dateSelected', 44 | defaultValue: undefined, 45 | }, 46 | { 47 | property: 'days', 48 | defaultValue: days, 49 | deepEqual: true, 50 | }, 51 | { 52 | property: 'defaultView', 53 | defaultValue: 'calendar', 54 | }, 55 | { 56 | property: 'disableMobile', 57 | defaultValue: false, 58 | }, 59 | { 60 | property: 'disableYearOverlay', 61 | defaultValue: false, 62 | }, 63 | { 64 | property: 'disabledDates', 65 | defaultValue: {}, 66 | deepEqual: true, 67 | }, 68 | { 69 | property: 'disabler', 70 | // defaultValue: '', 71 | isFunction: true, 72 | }, 73 | { 74 | property: 'el', 75 | defaultValue: getFirstElement, 76 | domElement: true, 77 | selector: selectors.singleDatepickerInput, 78 | }, 79 | { 80 | property: 'events', 81 | defaultValue: {}, 82 | deepEqual: true, 83 | }, 84 | { 85 | property: 'first', 86 | defaultValue: undefined, 87 | }, 88 | { 89 | property: 'formatter', 90 | // defaultValue: '', 91 | isFunction: true, 92 | }, 93 | { 94 | property: 'hide', 95 | // defaultValue: '', 96 | isFunction: true, 97 | }, 98 | { 99 | property: 'id', 100 | defaultValue: undefined, 101 | }, 102 | { 103 | property: 'inlinePosition', 104 | defaultValue: true, 105 | notOwnProperty: true, 106 | }, 107 | { 108 | property: 'isMobile', 109 | defaultValue: false, 110 | }, 111 | { 112 | property: 'maxDate', 113 | defaultValue: undefined, 114 | }, 115 | { 116 | property: 'minDate', 117 | defaultValue: undefined, 118 | }, 119 | { 120 | property: 'months', 121 | defaultValue: months, 122 | deepEqual: true, 123 | }, 124 | { 125 | property: 'navigate', 126 | // defaultValue: '', 127 | isFunction: true, 128 | }, 129 | { 130 | property: 'noPosition', 131 | defaultValue: false, 132 | }, 133 | { 134 | property: 'noWeekends', 135 | defaultValue: false, 136 | }, 137 | { 138 | property: 'nonInput', 139 | defaultValue: false, 140 | }, 141 | { 142 | property: 'onHide', 143 | // defaultValue: '', 144 | isFunction: true, 145 | }, 146 | { 147 | property: 'onMonthChange', 148 | // defaultValue: '', 149 | isFunction: true, 150 | }, 151 | { 152 | property: 'onSelect', 153 | // defaultValue: '', 154 | isFunction: true, 155 | }, 156 | { 157 | property: 'onShow', 158 | // defaultValue: '', 159 | isFunction: true, 160 | }, 161 | { 162 | property: 'overlayButton', 163 | defaultValue: 'Submit', 164 | }, 165 | { 166 | property: 'overlayMonths', 167 | defaultValue: months.map(month => month.slice(0, 3)), 168 | deepEqual: true, 169 | }, 170 | { 171 | property: 'overlayPlaceholder', 172 | defaultValue: '4-digit year', 173 | }, 174 | { 175 | property: 'parent', 176 | defaultValue: getFirstElement, 177 | domElement: true, 178 | selector: selectors.singleDatepickerInputParent, 179 | }, 180 | { 181 | property: 'position', 182 | defaultValue: { bottom: 1, left: 1 }, 183 | deepEqual: true, 184 | }, 185 | { 186 | property: 'positionedEl', 187 | defaultValue: getFirstElement, 188 | domElement: true, 189 | selector: selectors.singleDatepickerInputParent, 190 | }, 191 | { 192 | property: 'remove', 193 | // defaultValue: '', 194 | isFunction: true, 195 | }, 196 | { 197 | property: 'respectDisabledReadOnly', 198 | defaultValue: false, 199 | }, 200 | { 201 | property: 'second', 202 | defaultValue: undefined, 203 | }, 204 | { 205 | property: 'setDate', 206 | // defaultValue: '', 207 | isFunction: true, 208 | }, 209 | { 210 | property: 'setMax', 211 | // defaultValue: '', 212 | isFunction: true, 213 | }, 214 | { 215 | property: 'setMin', 216 | // defaultValue: '', 217 | isFunction: true, 218 | }, 219 | { 220 | property: 'shadowDom', 221 | defaultValue: undefined, 222 | }, 223 | { 224 | property: 'show', 225 | // defaultValue: '', 226 | isFunction: true, 227 | }, 228 | { 229 | property: 'showAllDates', 230 | defaultValue: false, 231 | }, 232 | { 233 | property: 'startDate', 234 | defaultValue: date, 235 | deepEqual: true, 236 | }, 237 | { 238 | property: 'startDay', 239 | defaultValue: 0, 240 | }, 241 | { 242 | property: 'toggleOverlay', 243 | isFunction: true, 244 | }, 245 | { 246 | property: 'weekendIndices', 247 | defaultValue: [6, 0], 248 | deepEqual: true, 249 | }, 250 | ] 251 | 252 | function mergeProperties(singlePickerProps, daterangeProps) { 253 | const properties = [] 254 | 255 | for (let i = 0; i < singlePickerProps.length; i++) { 256 | const oldObj = singlePickerProps[i] 257 | const overwriteIdx = daterangeProps.findIndex(obj => obj.property === oldObj.property) 258 | 259 | // Add the new item instead of the old one. 260 | if (overwriteIdx > -1) { 261 | // Add this item to the final array. 262 | properties.push(daterangeProps[overwriteIdx]) 263 | 264 | // Get rid of this item in daterangeProps. 265 | daterangeProps[overwriteIdx] = null 266 | daterangeProps = daterangeProps.filter(Boolean) 267 | 268 | // Add the old item. 269 | } else { 270 | properties.push(singlePickerProps[i]) 271 | } 272 | } 273 | 274 | // Include any remaining objects not found in the original singlePickerProps. 275 | return properties.concat(daterangeProps) 276 | } 277 | 278 | function getDaterangeProperties(type /* 'start' or 'end' */, startPicker, endPicker) { 279 | const daterangeProperties = [ 280 | { 281 | property: 'calendar', 282 | defaultValue: getFirstElement, 283 | domElement: true, 284 | selector: selectors.range[type].calendar, 285 | }, 286 | { 287 | property: 'calendarContainer', 288 | defaultValue: getFirstElement, 289 | domElement: true, 290 | selector: selectors.range[type].calendarContainer, 291 | }, 292 | { 293 | property: 'el', 294 | defaultValue: getFirstElement, 295 | domElement: true, 296 | selector: selectors[`daterangeInput${type === 'start' ? 'Start' : 'End'}`], 297 | }, 298 | { 299 | property: 'first', 300 | defaultValue: type === 'start' || undefined, 301 | }, 302 | { 303 | property: 'getRange', 304 | // defaultValue: '', 305 | isFunction: true, 306 | }, 307 | { 308 | property: 'id', 309 | // defaultValue: '', 310 | }, 311 | { 312 | property: 'originalMaxDate', 313 | defaultValue: undefined, 314 | }, 315 | { 316 | property: 'originalMinDate', 317 | defaultValue: undefined, 318 | }, 319 | { 320 | property: 'parent', 321 | defaultValue: getFirstElement, 322 | domElement: true, 323 | selector: selectors[`daterangeInputs${type === 'start' ? 'Start' : 'End'}Container`], 324 | }, 325 | { 326 | property: 'positionedEl', 327 | defaultValue: getFirstElement, 328 | domElement: true, 329 | selector: selectors[`daterangeInputs${type === 'start' ? 'Start' : 'End'}Container`], 330 | }, 331 | { 332 | property: 'second', 333 | defaultValue: type === 'end' || undefined, 334 | }, 335 | { 336 | property: 'sibling', 337 | defaultValue: type === 'start' ? endPicker : startPicker, 338 | }, 339 | ] 340 | 341 | return mergeProperties(singleDatepickerProperties, daterangeProperties) 342 | } 343 | 344 | const pickerProperties = { 345 | singleDatepickerProperties, 346 | getDaterangeProperties, 347 | months, 348 | days, 349 | } 350 | 351 | export default pickerProperties 352 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | const webpackPreprocessor = require('@cypress/webpack-preprocessor') 15 | 16 | 17 | module.exports = (on, config) => { 18 | // `on` is used to hook into various events Cypress emits 19 | // `config` is the resolved Cypress config 20 | 21 | const env = {} 22 | const options = { 23 | webpackOptions: require('../../webpack.config')(env), 24 | watchOptions: {}, 25 | } 26 | 27 | on('file:preprocessor',webpackPreprocessor(options)) 28 | } 29 | -------------------------------------------------------------------------------- /cypress/selectors.js: -------------------------------------------------------------------------------- 1 | const appSelectors = { 2 | singleDatepickerInput: '[data-cy="single-datepicker-input"]', 3 | daterangeInputStart: '[data-cy="daterange-input-start"]', 4 | daterangeInputEnd: '[data-cy="daterange-input-end"]', 5 | singleDatepickerInputParent: '[data-cy="single-datepicker-input-parent"]', 6 | daterangeInputsParent: '[data-cy="daterange-inputs-parent"]', 7 | daterangeInputsStartContainer: '[data-cy="daterange-input-start-container"]', 8 | daterangeInputsEndContainer: '[data-cy="daterange-input-end-container"]', 9 | } 10 | 11 | const container = '.qs-datepicker-container' 12 | const calendar = '.qs-datepicker' 13 | const controls = '.qs-controls' 14 | const squaresContainer = '.qs-squares' 15 | const everySquare = '.qs-square' 16 | const squareDayHeader = '.qs-day' 17 | const squareOutsideCurrentMonth = '.qs-outside-current-month' 18 | const squareWithNum = '.qs-num' 19 | const squareCurrentDay = '.qs-current' 20 | const overlay = '.qs-overlay' 21 | const overlayInputContainer = '.qs-overlay > div:nth-of-type(1)' 22 | const overlayYearInput = '.qs-overlay-year' 23 | const overlayClose = '.qs-close' 24 | const overlayMonthContainer = '.qs-overlay-month-container' 25 | const overlayMonth = '.qs-overlay-month' 26 | const overlaySubmit = '.qs-submit' 27 | 28 | const datepickerSelectors = { 29 | single: { 30 | calendarContainer: `${appSelectors.singleDatepickerInputParent} ${container}`, 31 | calendar: `${appSelectors.singleDatepickerInputParent} ${calendar}`, 32 | controls: `${appSelectors.singleDatepickerInputParent} ${calendar} ${controls}`, 33 | squaresContainer: `${appSelectors.singleDatepickerInputParent} ${calendar} ${squaresContainer}`, 34 | squares: `${appSelectors.singleDatepickerInputParent} ${calendar} ${squaresContainer} ${everySquare}`, 35 | overlay: `${appSelectors.singleDatepickerInputParent} ${calendar} ${overlay}`, 36 | overlayInputContainer: `${appSelectors.singleDatepickerInputParent} ${calendar} ${overlayInputContainer}`, 37 | overlayMonthContainer: `${appSelectors.singleDatepickerInputParent} ${calendar} ${overlay} ${overlayMonthContainer}`, 38 | }, 39 | range: { 40 | start: { 41 | calendarContainer: `${appSelectors.daterangeInputsStartContainer} ${container}`, 42 | calendar: `${appSelectors.daterangeInputsStartContainer} ${container} ${calendar}`, 43 | controls: `${appSelectors.daterangeInputsStartContainer} ${container} ${calendar} ${controls}`, 44 | squaresContainer: `${appSelectors.daterangeInputsStartContainer} ${container} ${calendar} ${squaresContainer}`, 45 | overlay: `${appSelectors.daterangeInputsStartContainer} ${container} ${calendar} ${overlay}`, 46 | overlayInputContainer: `${appSelectors.daterangeInputsStartContainer} ${container} ${calendar} ${overlayInputContainer}`, 47 | overlayMonthContainer: `${appSelectors.daterangeInputsStartContainer} ${container} ${calendar} ${overlay} ${overlayMonthContainer}`, 48 | }, 49 | end: { 50 | calendarContainer: `${appSelectors.daterangeInputsEndContainer} ${container}`, 51 | calendar: `${appSelectors.daterangeInputsEndContainer} ${container} ${calendar}`, 52 | controls: `${appSelectors.daterangeInputsEndContainer} ${container} ${calendar} ${controls}`, 53 | squaresContainer: `${appSelectors.daterangeInputsEndContainer} ${container} ${calendar} ${squaresContainer}`, 54 | overlay: `${appSelectors.daterangeInputsEndContainer} ${container} ${calendar} ${overlay}`, 55 | overlayInputContainer: `${appSelectors.daterangeInputsEndContainer} ${container} ${calendar} ${overlayInputContainer}`, 56 | overlayMonthContainer: `${appSelectors.daterangeInputsEndContainer} ${container} ${calendar} ${overlay} ${overlayMonthContainer}`, 57 | }, 58 | }, 59 | common: { 60 | container, 61 | calendar, 62 | controls, 63 | squaresContainer, 64 | overlay, 65 | 66 | everySquare, 67 | squareDayHeader, 68 | squareOutsideCurrentMonth, 69 | squareWithNum, 70 | squareCurrentDay, 71 | 72 | overlayInputContainer, 73 | overlayYearInput, 74 | overlayClose, 75 | overlayMonthContainer, 76 | overlayMonth, 77 | overlaySubmit, 78 | } 79 | } 80 | 81 | const selectors = { 82 | ...appSelectors, 83 | ...datepickerSelectors, 84 | } 85 | 86 | export default selectors 87 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /datepicker.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'js-datepicker' { 2 | export type DatepickerOptions = { 3 | /** 4 | * Callback function after a date has been selected. The 2nd argument is the selected date when a date is being selected and `undefined` when a date is being unselected. You unselect a date by clicking it again. 5 | */ 6 | onSelect?(instance: DatepickerInstance, date?: Date): void 7 | 8 | /** 9 | * Callback function when the calendar is shown. 10 | */ 11 | onShow?(instance: DatepickerInstance): void 12 | 13 | /** 14 | * Callback function when the calendar is hidden. 15 | */ 16 | onHide?(instance: DatepickerInstance): void 17 | 18 | /** 19 | * Callback function when the month has changed. 20 | */ 21 | onMonthChange?(instance: DatepickerInstance): void 22 | 23 | /** 24 | * Using an input field with your datepicker? Want to customize its value anytime a date is selected? Provide a function that manually sets the provided input's value with your own formatting. 25 | * 26 | * NOTE: The formatter function will only run if the datepicker instance is associated with an field. 27 | */ 28 | formatter?( 29 | input: HTMLInputElement, 30 | date: Date, 31 | instance: DatepickerInstance 32 | ): void 33 | 34 | /** 35 | * This option positions the calendar relative to the field it's associated with. This can be 1 of 5 values: 'tr', 'tl', 'br', 'bl', 'c' representing top-right, top-left, bottom-right, bottom-left, and centered respectively. Datepicker will position itself accordingly relative to the element you reference in the 1st argument. For a value of 'c', Datepicker will position itself fixed, smack in the middle of the screen. This can be desirable for mobile devices. 36 | * 37 | * Default - 'bl' 38 | */ 39 | position?: 'tr' | 'tl' | 'br' | 'bl' | 'c' 40 | 41 | /** 42 | * Specify the day of the week your calendar starts on. 0 = Sunday, 1 = Monday, etc. Plays nice with the `customDays` option. 43 | * 44 | * Default - 0 45 | */ 46 | startDay?: 0 | 1 | 2 | 3 | 4 | 5 | 6 47 | 48 | /** 49 | * You can customize the display of days on the calendar by providing an array of 7 values. This can be used with the `startDay` option if your week starts on a day other than Sunday. 50 | * 51 | * Default - ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] 52 | */ 53 | customDays?: [string, string, string, string, string, string, string] 54 | 55 | /** 56 | * You can customize the display of the month name at the top of the calendar by providing an array of 12 strings. 57 | * 58 | * Default - ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] 59 | */ 60 | customMonths?: [ 61 | string, 62 | string, 63 | string, 64 | string, 65 | string, 66 | string, 67 | string, 68 | string, 69 | string, 70 | string, 71 | string, 72 | string 73 | ] 74 | 75 | /** 76 | * You can customize the display of the month names in the overlay view by providing an array of 12 strings. Keep in mind that if the values are too long, it could produce undesired results in the UI. 77 | * 78 | * Default - The first 3 characters of each item in `customMonths`. 79 | */ 80 | customOverlayMonths?: [ 81 | string, 82 | string, 83 | string, 84 | string, 85 | string, 86 | string, 87 | string, 88 | string, 89 | string, 90 | string, 91 | string, 92 | string 93 | ] 94 | 95 | /** 96 | * Want the overlay to be the default view when opening the calendar? This property is for you. Simply set this property to 'overlay' and you're done. This is helpful if you want a month picker to be front and center. 97 | * 98 | * Default - 'calendar' 99 | */ 100 | defaultView?: 'calendar' | 'overlay' 101 | 102 | /** 103 | * Custom text for the year overlay submit button. 104 | * 105 | * Default - 'Submit' 106 | */ 107 | overlayButton?: string 108 | 109 | /** 110 | * Custom placeholder text for the year overlay. 111 | * 112 | * Default - '4-digit year' 113 | */ 114 | overlayPlaceholder?: string 115 | 116 | /** 117 | * An array of dates which indicate something is happening - a meeting, birthday, etc. I.e. an event. 118 | */ 119 | events?: Date[] 120 | 121 | /** 122 | * By default, the datepicker will hide & show itself automatically depending on where you click or focus on the page. If you want the calendar to always be on the screen, use this option. 123 | * 124 | * Default - false 125 | */ 126 | alwaysShow?: boolean 127 | 128 | /** 129 | * This will start the calendar with a date already selected. If Datepicker is used with an element, that field will be populated with this date as well. 130 | */ 131 | dateSelected?: Date 132 | 133 | /** 134 | * This will be the maximum threshold of selectable dates. Anything after it will be unselectable. 135 | * 136 | * NOTE: When using a daterange pair, if you set `maxDate` on the first instance options it will be ignored on the 2nd instance options. 137 | */ 138 | maxDate?: Date 139 | 140 | /** 141 | * This will be the minumum threshold of selectable dates. Anything prior will be unselectable. 142 | * 143 | * NOTE: When using a daterange pair, if you set `minDate` on the first instance options it will be ignored on the 2nd instance options. 144 | */ 145 | minDate?: Date 146 | 147 | /** 148 | * The date you provide will determine the month that the calendar starts off at. 149 | * 150 | * Default - today's month 151 | */ 152 | startDate?: Date 153 | 154 | /** 155 | * By default, the datepicker will not put date numbers on calendar days that fall outside the current month. They will be empty squares. Sometimes you want to see those preceding and trailing days. This is the option for you. 156 | * 157 | * Default - false 158 | */ 159 | showAllDates?: boolean 160 | 161 | /** 162 | * 's can have a `disabled` or `readonly` attribute applied to them. In those cases, you might want to prevent Datepicker from selecting a date and changing the input's value. Set this option to `true` if that's the case. The calendar will still be functional in that you can change months and enter a year, but dates will not be selectable (or deselectable). 163 | * 164 | * Default - false 165 | */ 166 | respectDisabledReadOnly?: boolean 167 | 168 | /** 169 | * Provide `true` to disable selecting weekends. Weekends are Saturday & Sunday. If your weekends are a set of different days or you need more control over disabled dates, check out the `disabler` option. 170 | * 171 | * Default - false 172 | */ 173 | noWeekends?: boolean 174 | 175 | /** 176 | * Sometimes you need more control over which dates to disable. The `disabledDates` option is limited to an explicit array of dates and the `noWeekends` option is limited to Saturdays & Sundays. Provide a function that takes a JavaScript date as it's only argument and returns `true` if the date should be disabled. When the calendar builds, each date will be run through this function to determine whether or not it should be disabled. 177 | */ 178 | disabler?(date: Date): boolean 179 | 180 | /** 181 | * Provide an array of JS date objects that will be disabled on the calendar. This array cannot include the same date as `dateSelected`. If you need more control over which dates are disabled, see the `disabler` option. 182 | */ 183 | disabledDates?: Date[] 184 | 185 | /** 186 | * Optionally disable Datepicker on mobile devices. This is handy if you'd like to trigger the mobile device's native date picker instead. If that's the case, make sure to use an input with a type of "date" - 187 | * 188 | * Default - false 189 | */ 190 | disableMobile?: boolean 191 | 192 | /** 193 | * Clicking the year or month name on the calendar triggers an overlay to show, allowing you to enter a year manually. If you want to disable this feature, set this option to `true`. 194 | * 195 | * Default - false 196 | */ 197 | disableYearOverlay?: boolean 198 | } 199 | 200 | export type DaterangePickerOptions = DatepickerOptions & { 201 | /** 202 | * Now we're getting fancy! If you want to link two instances together to help form a daterange picker, this is your option. Only two picker instances can share an `id`. The datepicker instance declared first will be considered the "start" picker in the range. There's a fancy `getRange` method for you to use as well. 203 | */ 204 | id: any 205 | } 206 | 207 | export type DatepickerInstance = { 208 | /** 209 | * TODO - move this into the initialize options. 210 | * Manually set this property to `true` to fully disable the calendar. 211 | */ 212 | disabled: boolean 213 | 214 | /** 215 | * Performs cleanup. This will remove the current instance from the DOM, leaving all others in tact. If this is the only instance left, it will also remove the event listeners that Datepicker previously set up. 216 | */ 217 | remove(): void 218 | 219 | /** 220 | * Programmatically navigates the calendar to the date you provide. This doesn't select a date, it's literally just for navigation. You can optionally trigger the `onMonthChange` callback with the 2nd argument. 221 | */ 222 | navigate(date: Date, triggerOnMonthChange?: boolean): void 223 | 224 | /** 225 | * Allows you to programmatically select or unselect a date on the calendar. To select a date on the calendar, pass in a JS date object for the 1st argument. If you set a date on a month other than what's currently displaying and you want the calendar to automatically change to it, pass in `true` as the 2nd argument. 226 | * Want to unselect a date? Simply run the function with no arguments. 227 | * 228 | * NOTE: This will not trigger the `onSelect` callback. 229 | */ 230 | setDate(date: Date, changeCalendar?: boolean): void 231 | setDate(): void 232 | 233 | /** 234 | * Allows you to programmatically set the minimum selectable date or unset it. If this instance is part of a daterange instance (see the `id` option) then the other instance will be changed as well. To unset a minimum date, simply run the function with no arguments. 235 | */ 236 | setMin(date: Date): void 237 | setMin(): void 238 | 239 | /** 240 | * Allows you to programmatically set the maximum selectable date or unset it. If this instance is part of a daterange instance (see the `id` option) then the other instance will be changed as well. To unset a maximum date, simply run the function with no arguments. 241 | */ 242 | setMax(date: Date): void 243 | setMax(): void 244 | 245 | /** 246 | * Allows you to programmatically show the calendar. Using this method will trigger the `onShow` callback if your instance has one. 247 | * 248 | * NOTE: Want to show / hide the calendar programmatically with a button or by clicking some element? Make sure to use `stopPropagation` in your event callback! If you don't, any click event in the DOM will bubble up to Datepicker's internal oneHandler event listener, triggering logic to close the calendar since it "sees" the click event outside the calendar. 249 | */ 250 | show(): void 251 | 252 | /** 253 | * Allows you to programmatically hide the calendar. If the `alwaysShow` property was set on the instance then this method will have no effect. Using this method will trigger the `onHide` callback if your instance has one. 254 | * 255 | * NOTE: Want to show / hide the calendar programmatically with a button or by clicking some element? Make sure to use `stopPropagation` in your event callback! If you don't, any click event in the DOM will bubble up to Datepicker's internal oneHandler event listener, triggering logic to close the calendar since it "sees" the click event outside the calendar. 256 | */ 257 | hide(): void 258 | 259 | /** 260 | * Call this method on the picker to programmatically toggle the overlay. This will only work if the calendar is showing! 261 | */ 262 | toggleOverlay(): void 263 | 264 | /** 265 | * The calendar element. 266 | */ 267 | calendar: HTMLElement 268 | 269 | /** 270 | * The container element that houses the calendar. Use it to size the calendar or programmatically check if the calendar is showing. 271 | */ 272 | calendarContainer: HTMLElement 273 | 274 | /** 275 | * A 0-index number representing the current month. For example, 0 represents January. 276 | */ 277 | currentMonth: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 278 | 279 | /** 280 | * Calendar month in plain english. E.x. January 281 | */ 282 | currentMonthName: string 283 | 284 | /** 285 | * The current year. E.x. 2099 286 | */ 287 | currentYear: number 288 | 289 | /** 290 | * The value of the selected date. This will be `undefined` if no date has been selected yet. 291 | */ 292 | dateSelected: Date | undefined 293 | 294 | /** 295 | * The element datepicker is relatively positioned against (unless centered). 296 | */ 297 | el: Element 298 | 299 | /** 300 | * The minimum selectable date. 301 | */ 302 | minDate: Date | undefined 303 | 304 | /** 305 | * The maximum selectable date. 306 | */ 307 | maxDate: Date | undefined 308 | } 309 | 310 | export type DaterangePickerInstance = DatepickerInstance & { 311 | /** 312 | * This method is only available on daterange pickers. It will return an object with `start` and `end` properties whose values are JavaScript date objects representing what the user selected on both calendars. 313 | */ 314 | getRange(): {start: Date | undefined; end: Date | undefined} 315 | 316 | /** 317 | * If two datepickers have the same `id` option then this property will be available and refer to the other instance. 318 | */ 319 | sibling: DaterangePickerInstance 320 | } 321 | 322 | export type Selector = string | HTMLElement 323 | 324 | function datepicker( 325 | selector: Selector, 326 | options?: DatepickerOptions 327 | ): DatepickerInstance 328 | function datepicker( 329 | selector: Selector, 330 | options: DaterangePickerOptions 331 | ): DaterangePickerInstance 332 | 333 | export default datepicker 334 | } 335 | -------------------------------------------------------------------------------- /dist/datepicker.min.css: -------------------------------------------------------------------------------- 1 | .qs-datepicker-container{font-size:1rem;font-family:sans-serif;color:#000;position:absolute;width:15.625em;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;z-index:9001;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid grey;border-radius:.263921875em;overflow:hidden;background:#fff;-webkit-box-shadow:0 1.25em 1.25em -.9375em rgba(0,0,0,.3);box-shadow:0 1.25em 1.25em -.9375em rgba(0,0,0,.3)}.qs-datepicker-container *{-webkit-box-sizing:border-box;box-sizing:border-box}.qs-centered{position:fixed;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.qs-hidden{display:none}.qs-overlay{position:absolute;top:0;left:0;background:rgba(0,0,0,.75);color:#fff;width:100%;height:100%;padding:.5em;z-index:1;opacity:1;-webkit-transition:opacity .3s;transition:opacity .3s;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.qs-overlay.qs-hidden{opacity:0;z-index:-1}.qs-overlay .qs-overlay-year{background:rgba(0,0,0,0);border:none;border-bottom:1px solid #fff;border-radius:0;color:#fff;font-size:.875em;padding:.25em 0;width:80%;text-align:center;margin:0 auto;display:block}.qs-overlay .qs-overlay-year::-webkit-inner-spin-button{-webkit-appearance:none}.qs-overlay .qs-close{padding:.5em;cursor:pointer;position:absolute;top:0;right:0}.qs-overlay .qs-submit{border:1px solid #fff;border-radius:.263921875em;padding:.5em;margin:0 auto auto;cursor:pointer;background:hsla(0,0%,50.2%,.4)}.qs-overlay .qs-submit.qs-disabled{color:grey;border-color:grey;cursor:not-allowed}.qs-overlay .qs-overlay-month-container{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.qs-overlay .qs-overlay-month{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:calc(100% / 3);cursor:pointer;opacity:.5;-webkit-transition:opacity .15s;transition:opacity .15s}.qs-overlay .qs-overlay-month.active,.qs-overlay .qs-overlay-month:hover{opacity:1}.qs-controls{width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-negative:0;flex-shrink:0;background:#d3d3d3;-webkit-filter:blur(0);filter:blur(0);-webkit-transition:-webkit-filter .3s;transition:-webkit-filter .3s;transition:filter .3s;transition:filter .3s, -webkit-filter .3s}.qs-controls.qs-blur{-webkit-filter:blur(5px);filter:blur(5px)}.qs-arrow{height:1.5625em;width:1.5625em;position:relative;cursor:pointer;border-radius:.263921875em;-webkit-transition:background .15s;transition:background .15s}.qs-arrow:hover.qs-left:after{border-right-color:#000}.qs-arrow:hover.qs-right:after{border-left-color:#000}.qs-arrow:hover{background:rgba(0,0,0,.1)}.qs-arrow:after{content:"";border:.390625em solid rgba(0,0,0,0);position:absolute;top:50%;-webkit-transition:border .2s;transition:border .2s}.qs-arrow.qs-left:after{border-right-color:grey;right:50%;-webkit-transform:translate(25%,-50%);-ms-transform:translate(25%,-50%);transform:translate(25%,-50%)}.qs-arrow.qs-right:after{border-left-color:grey;left:50%;-webkit-transform:translate(-25%,-50%);-ms-transform:translate(-25%,-50%);transform:translate(-25%,-50%)}.qs-month-year{font-weight:700;-webkit-transition:border .2s;transition:border .2s;border-bottom:1px solid rgba(0,0,0,0)}.qs-month-year:not(.qs-disabled-year-overlay){cursor:pointer}.qs-month-year:not(.qs-disabled-year-overlay):hover{border-bottom:1px solid grey}.qs-month-year:active:focus,.qs-month-year:focus{outline:none}.qs-month{padding-right:.5ex}.qs-year{padding-left:.5ex}.qs-squares{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.3125em;-webkit-filter:blur(0);filter:blur(0);-webkit-transition:-webkit-filter .3s;transition:-webkit-filter .3s;transition:filter .3s;transition:filter .3s, -webkit-filter .3s}.qs-squares.qs-blur{-webkit-filter:blur(5px);filter:blur(5px)}.qs-square{width:calc(100% / 7);height:1.5625em;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;cursor:pointer;-webkit-transition:background .1s;transition:background .1s;border-radius:.263921875em}.qs-square:not(.qs-empty):not(.qs-disabled):not(.qs-day):not(.qs-active):hover{background:orange}.qs-current{font-weight:700;text-decoration:underline}.qs-active,.qs-range-end,.qs-range-start{background:#add8e6}.qs-range-start:not(.qs-range-6){border-top-right-radius:0;border-bottom-right-radius:0}.qs-range-middle{background:#d4ebf2}.qs-range-middle:not(.qs-range-0):not(.qs-range-6){border-radius:0}.qs-range-middle.qs-range-0{border-top-right-radius:0;border-bottom-right-radius:0}.qs-range-end:not(.qs-range-0),.qs-range-middle.qs-range-6{border-top-left-radius:0;border-bottom-left-radius:0}.qs-disabled,.qs-outside-current-month{opacity:.2}.qs-disabled{cursor:not-allowed}.qs-day,.qs-empty{cursor:default}.qs-day{font-weight:700;color:grey}.qs-event{position:relative}.qs-event:after{content:"";position:absolute;width:.46875em;height:.46875em;border-radius:50%;background:#07f;bottom:0;right:0} 2 | -------------------------------------------------------------------------------- /dist/datepicker.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.datepicker=t():e.datepicker=t()}(window,(function(){return function(e){var t={};function n(a){if(t[a])return t[a].exports;var r=t[a]={i:a,l:!1,exports:{}};return e[a].call(r.exports,r,r.exports,n),r.l=!0,r.exports}return n.m=e,n.c=t,n.d=function(e,t,a){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:a})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var a=Object.create(null);if(n.r(a),Object.defineProperty(a,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(a,r,function(t){return e[t]}.bind(null,r));return a},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";n.r(t);var a=[],r=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],i=["January","February","March","April","May","June","July","August","September","October","November","December"],o={t:"top",r:"right",b:"bottom",l:"left",c:"centered"};function s(){}var l=["click","focusin","keydown","input"];function d(e){l.forEach((function(t){e.addEventListener(t,e===document?L:Y)}))}function c(e){return Array.isArray(e)?e.map(c):"[object Object]"===x(e)?Object.keys(e).reduce((function(t,n){return t[n]=c(e[n]),t}),{}):e}function u(e,t){var n=e.calendar.querySelector(".qs-overlay"),a=n&&!n.classList.contains("qs-hidden");t=t||new Date(e.currentYear,e.currentMonth),e.calendar.innerHTML=[h(t,e,a),f(t,e,a),v(e,a)].join(""),a&&window.requestAnimationFrame((function(){M(!0,e)}))}function h(e,t,n){return['
','
','
',''+t.months[e.getMonth()]+"",''+e.getFullYear()+"","
",'
',"
"].join("")}function f(e,t,n){var a=t.currentMonth,r=t.currentYear,i=t.dateSelected,o=t.maxDate,s=t.minDate,l=t.showAllDates,d=t.days,c=t.disabledDates,u=t.startDay,h=t.weekendIndices,f=t.events,v=t.getRange?t.getRange():{},m=+v.start,y=+v.end,p=g(new Date(e).setDate(1)),w=p.getDay()-u,D=w<0?7:0;p.setMonth(p.getMonth()+1),p.setDate(0);var b=p.getDate(),q=[],S=D+7*((w+b)/7|0);S+=(w+b)%7?7:0;for(var M=1;M<=S;M++){var E=(M-1)%7,x=d[E],C=M-(w>=0?w:7+w),L=new Date(r,a,C),Y=f[+L],j=C<1||C>b,O=j?C<1?-1:1:0,P=j&&!l,k=P?"":L.getDate(),N=+L==+i,_=E===h[0]||E===h[1],I=m!==y,A="qs-square "+x;Y&&!P&&(A+=" qs-event"),j&&(A+=" qs-outside-current-month"),!l&&j||(A+=" qs-num"),N&&(A+=" qs-active"),(c[+L]||t.disabler(L)||_&&t.noWeekends||s&&+L<+s||o&&+L>+o)&&!P&&(A+=" qs-disabled"),+g(new Date)==+L&&(A+=" qs-current"),+L===m&&y&&I&&(A+=" qs-range-start"),+L>m&&+L'+k+"
")}var R=d.map((function(e){return'
'+e+"
"})).concat(q);return R.unshift('
'),R.push("
"),R.join("")}function v(e,t){var n=e.overlayPlaceholder,a=e.overlayButton;return['
',"
",'','
',"
",'
'+e.overlayMonths.map((function(e,t){return'
'+e+"
"})).join("")+"
",'
'+a+"
","
"].join("")}function m(e,t,n){var a=t.el,r=t.calendar.querySelector(".qs-active"),i=e.textContent,o=t.sibling;(a.disabled||a.readOnly)&&t.respectDisabledReadOnly||(t.dateSelected=n?void 0:new Date(t.currentYear,t.currentMonth,i),r&&r.classList.remove("qs-active"),n||e.classList.add("qs-active"),p(a,t,n),n||q(t),o&&(y({instance:t,deselect:n}),t.first&&!o.dateSelected&&(o.currentYear=t.currentYear,o.currentMonth=t.currentMonth,o.currentMonthName=t.currentMonthName),u(t),u(o)),t.onSelect(t,n?void 0:new Date(t.dateSelected)))}function y(e){var t=e.instance.first?e.instance:e.instance.sibling,n=t.sibling;t===e.instance?e.deselect?(t.minDate=t.originalMinDate,n.minDate=n.originalMinDate):n.minDate=t.dateSelected:e.deselect?(n.maxDate=n.originalMaxDate,t.maxDate=t.originalMaxDate):t.maxDate=n.dateSelected}function p(e,t,n){if(!t.nonInput)return n?e.value="":t.formatter!==s?t.formatter(e,t.dateSelected,t):void(e.value=t.dateSelected.toDateString())}function w(e,t,n,a){n||a?(n&&(t.currentYear=+n),a&&(t.currentMonth=+a)):(t.currentMonth+=e.contains("qs-right")?1:-1,12===t.currentMonth?(t.currentMonth=0,t.currentYear++):-1===t.currentMonth&&(t.currentMonth=11,t.currentYear--)),t.currentMonthName=t.months[t.currentMonth],u(t),t.onMonthChange(t)}function D(e){if(!e.noPosition){var t=e.position.top,n=e.position.right;if(e.position.centered)return e.calendarContainer.classList.add("qs-centered");var a=e.positionedEl.getBoundingClientRect(),r=e.el.getBoundingClientRect(),i=e.calendarContainer.getBoundingClientRect(),o=r.top-a.top+(t?-1*i.height:r.height)+"px",s=r.left-a.left+(n?r.width-i.width:0)+"px";e.calendarContainer.style.setProperty("top",o),e.calendarContainer.style.setProperty("left",s)}}function b(e){return"[object Date]"===x(e)&&"Invalid Date"!==e.toString()}function g(e){if(b(e)||"number"==typeof e&&!isNaN(e)){var t=new Date(+e);return new Date(t.getFullYear(),t.getMonth(),t.getDate())}}function q(e){e.disabled||!e.calendarContainer.classList.contains("qs-hidden")&&!e.alwaysShow&&("overlay"!==e.defaultView&&M(!0,e),e.calendarContainer.classList.add("qs-hidden"),e.onHide(e))}function S(e){e.disabled||(e.calendarContainer.classList.remove("qs-hidden"),"overlay"===e.defaultView&&M(!1,e),D(e),e.onShow(e))}function M(e,t){var n=t.calendar,a=n.querySelector(".qs-overlay"),r=a.querySelector(".qs-overlay-year"),i=n.querySelector(".qs-controls"),o=n.querySelector(".qs-squares");e?(a.classList.add("qs-hidden"),i.classList.remove("qs-blur"),o.classList.remove("qs-blur"),r.value=""):(a.classList.remove("qs-hidden"),i.classList.add("qs-blur"),o.classList.add("qs-blur"),r.focus())}function E(e,t,n,a){var r=isNaN(+(new Date).setFullYear(t.value||void 0)),i=r?null:t.value;if(13===e.which||13===e.keyCode||"click"===e.type)a?w(null,n,i,a):r||t.classList.contains("qs-disabled")||w(null,n,i);else if(n.calendar.contains(t)){n.calendar.querySelector(".qs-submit").classList[r?"add":"remove"]("qs-disabled")}}function x(e){return{}.toString.call(e)}function C(e){a.forEach((function(t){t!==e&&q(t)}))}function L(e){if(!e.__qs_shadow_dom){var t=e.which||e.keyCode,n=e.type,r=e.target,o=r.classList,s=a.filter((function(e){return e.calendar.contains(r)||e.el===r}))[0],l=s&&s.calendar.contains(r);if(!(s&&s.isMobile&&s.disableMobile))if("click"===n){if(!s)return a.forEach(q);if(s.disabled)return;var d=s.calendar,c=s.calendarContainer,h=s.disableYearOverlay,f=s.nonInput,v=d.querySelector(".qs-overlay-year"),y=!!d.querySelector(".qs-hidden"),p=d.querySelector(".qs-month-year").contains(r),D=r.dataset.monthNum;if(s.noPosition&&!l)(c.classList.contains("qs-hidden")?S:q)(s);else if(o.contains("qs-arrow"))w(o,s);else if(p||o.contains("qs-close"))h||M(!y,s);else if(D)E(e,v,s,D);else{if(o.contains("qs-disabled"))return;if(o.contains("qs-num")){var b=r.textContent,g=+r.dataset.direction,x=new Date(s.currentYear,s.currentMonth+g,b);if(g){s.currentYear=x.getFullYear(),s.currentMonth=x.getMonth(),s.currentMonthName=i[s.currentMonth],u(s);for(var L,Y=s.calendar.querySelectorAll('[data-direction="0"]'),j=0;!L;){var O=Y[j];O.textContent===b&&(L=O),j++}r=L}return void(+x==+s.dateSelected?m(r,s,!0):r.classList.contains("qs-disabled")||m(r,s))}o.contains("qs-submit")?E(e,v,s):f&&r===s.el&&(S(s),C(s))}}else if("focusin"===n&&s)S(s),C(s);else if("keydown"===n&&9===t&&s)q(s);else if("keydown"===n&&s&&!s.disabled){var P=!s.calendar.querySelector(".qs-overlay").classList.contains("qs-hidden");13===t&&P&&l?E(e,r,s):27===t&&P&&l&&M(!0,s)}else if("input"===n){if(!s||!s.calendar.contains(r))return;var k=s.calendar.querySelector(".qs-submit"),N=r.value.split("").reduce((function(e,t){return e||"0"!==t?e+(t.match(/[0-9]/)?t:""):""}),"").slice(0,4);r.value=N,k.classList[4===N.length?"remove":"add"]("qs-disabled")}}}function Y(e){L(e),e.__qs_shadow_dom=!0}function j(e,t){l.forEach((function(n){e.removeEventListener(n,t)}))}function O(){S(this)}function P(){q(this)}function k(e,t){var n=g(e),a=this.currentYear,r=this.currentMonth,i=this.sibling;if(null==e)return this.dateSelected=void 0,p(this.el,this,!0),i&&(y({instance:this,deselect:!0}),u(i)),u(this),this;if(!b(e))throw new Error("`setDate` needs a JavaScript Date object.");if(this.disabledDates[+n]||nthis.maxDate)throw new Error("You can't manually set a date that's disabled.");this.dateSelected=n,t&&(this.currentYear=n.getFullYear(),this.currentMonth=n.getMonth(),this.currentMonthName=this.months[n.getMonth()]),p(this.el,this),i&&(y({instance:this}),u(i));var o=a===n.getFullYear()&&r===n.getMonth();return o||t?u(this,n):o||u(this,new Date(a,r,1)),this}function N(e){return I(this,e,!0)}function _(e){return I(this,e)}function I(e,t,n){var a=e.dateSelected,r=e.first,i=e.sibling,o=e.minDate,s=e.maxDate,l=g(t),d=n?"Min":"Max";function c(){return"original"+d+"Date"}function h(){return d.toLowerCase()+"Date"}function f(){return"set"+d}function v(){throw new Error("Out-of-range date passed to "+f())}if(null==t)e[c()]=void 0,i?(i[c()]=void 0,n?(r&&!a||!r&&!i.dateSelected)&&(e.minDate=void 0,i.minDate=void 0):(r&&!i.dateSelected||!r&&!a)&&(e.maxDate=void 0,i.maxDate=void 0)):e[h()]=void 0;else{if(!b(t))throw new Error("Invalid date passed to "+f());i?((r&&n&&l>(a||s)||r&&!n&&l<(i.dateSelected||o)||!r&&n&&l>(i.dateSelected||s)||!r&&!n&&l<(a||o))&&v(),e[c()]=l,i[c()]=l,(n&&(r&&!a||!r&&!i.dateSelected)||!n&&(r&&!i.dateSelected||!r&&!a))&&(e[h()]=l,i[h()]=l)):((n&&l>(a||s)||!n&&l<(a||o))&&v(),e[h()]=l)}return i&&u(i),u(e),e}function A(){var e=this.first?this:this.sibling,t=e.sibling;return{start:e.dateSelected,end:t.dateSelected}}function R(){var e=this.shadowDom,t=this.positionedEl,n=this.calendarContainer,r=this.sibling,i=this;this.inlinePosition&&(a.some((function(e){return e!==i&&e.positionedEl===t}))||t.style.setProperty("position",null));n.remove(),a=a.filter((function(e){return e!==i})),r&&delete r.sibling,a.length||j(document,L);var o=a.some((function(t){return t.shadowDom===e}));for(var s in e&&!o&&j(e,Y),this)delete this[s];a.length||l.forEach((function(e){document.removeEventListener(e,L)}))}function F(e,t){var n=new Date(e);if(!b(n))throw new Error("Invalid date passed to `navigate`");this.currentYear=n.getFullYear(),this.currentMonth=n.getMonth(),u(this),t&&this.onMonthChange(this)}function B(){var e=!this.calendarContainer.classList.contains("qs-hidden"),t=!this.calendarContainer.querySelector(".qs-overlay").classList.contains("qs-hidden");e&&M(t,this)}t.default=function(e,t){var n=function(e,t){var n,l,d=function(e){var t=c(e);t.events&&(t.events=t.events.reduce((function(e,t){if(!b(t))throw new Error('"options.events" must only contain valid JavaScript Date objects.');return e[+g(t)]=!0,e}),{}));["startDate","dateSelected","minDate","maxDate"].forEach((function(e){var n=t[e];if(n&&!b(n))throw new Error('"options.'+e+'" needs to be a valid JavaScript Date object.');t[e]=g(n)}));var n=t.position,i=t.maxDate,l=t.minDate,d=t.dateSelected,u=t.overlayPlaceholder,h=t.overlayButton,f=t.startDay,v=t.id;if(t.startDate=g(t.startDate||d||new Date),t.disabledDates=(t.disabledDates||[]).reduce((function(e,t){var n=+g(t);if(!b(t))throw new Error('You supplied an invalid date to "options.disabledDates".');if(n===+g(d))throw new Error('"disabledDates" cannot contain the same date as "dateSelected".');return e[n]=1,e}),{}),t.hasOwnProperty("id")&&null==v)throw new Error("`id` cannot be `null` or `undefined`");if(null!=v){var m=a.filter((function(e){return e.id===v}));if(m.length>1)throw new Error("Only two datepickers can share an id.");m.length?(t.second=!0,t.sibling=m[0]):t.first=!0}var y=["tr","tl","br","bl","c"].some((function(e){return n===e}));if(n&&!y)throw new Error('"options.position" must be one of the following: tl, tr, bl, br, or c.');function p(e){throw new Error('"dateSelected" in options is '+(e?"less":"greater")+' than "'+(e||"max")+'Date".')}if(t.position=function(e){var t=e[0],n=e[1],a={};a[o[t]]=1,n&&(a[o[n]]=1);return a}(n||"bl"),id&&p("min"),i0&&f<7){var w=(t.customDays||r).slice(),D=w.splice(0,f);t.customDays=w.concat(D),t.startDay=+f,t.weekendIndices=[w.length-1,w.length]}else t.startDay=0,t.weekendIndices=[6,0];"string"!=typeof u&&delete t.overlayPlaceholder;"string"!=typeof h&&delete t.overlayButton;var q=t.defaultView;if(q&&"calendar"!==q&&"overlay"!==q)throw new Error('options.defaultView must either be "calendar" or "overlay".');return t.defaultView=q||"calendar",t}(t||{startDate:g(new Date),position:"bl",defaultView:"calendar"}),u=e;if("string"==typeof u)u="#"===u[0]?document.getElementById(u.slice(1)):document.querySelector(u);else{if("[object ShadowRoot]"===x(u))throw new Error("Using a shadow DOM as your selector is not supported.");for(var h,f=u.parentNode;!h;){var v=x(f);"[object HTMLDocument]"===v?h=!0:"[object ShadowRoot]"===v?(h=!0,n=f,l=f.host):f=f.parentNode}}if(!u)throw new Error("No selector / element found.");if(a.some((function(e){return e.el===u})))throw new Error("A datepicker already exists on that element.");var m=u===document.body,y=n?u.parentElement||n:m?document.body:u.parentElement,w=n?u.parentElement||l:y,D=document.createElement("div"),q=document.createElement("div");D.className="qs-datepicker-container qs-hidden",q.className="qs-datepicker";var M={shadowDom:n,customElement:l,positionedEl:w,el:u,parent:y,nonInput:"INPUT"!==u.nodeName,noPosition:m,position:!m&&d.position,startDate:d.startDate,dateSelected:d.dateSelected,disabledDates:d.disabledDates,minDate:d.minDate,maxDate:d.maxDate,noWeekends:!!d.noWeekends,weekendIndices:d.weekendIndices,calendarContainer:D,calendar:q,currentMonth:(d.startDate||d.dateSelected).getMonth(),currentMonthName:(d.months||i)[(d.startDate||d.dateSelected).getMonth()],currentYear:(d.startDate||d.dateSelected).getFullYear(),events:d.events||{},defaultView:d.defaultView,setDate:k,remove:R,setMin:N,setMax:_,show:O,hide:P,navigate:F,toggleOverlay:B,onSelect:d.onSelect,onShow:d.onShow,onHide:d.onHide,onMonthChange:d.onMonthChange,formatter:d.formatter,disabler:d.disabler,months:d.months||i,days:d.customDays||r,startDay:d.startDay,overlayMonths:d.overlayMonths||(d.months||i).map((function(e){return e.slice(0,3)})),overlayPlaceholder:d.overlayPlaceholder||"4-digit year",overlayButton:d.overlayButton||"Submit",disableYearOverlay:!!d.disableYearOverlay,disableMobile:!!d.disableMobile,isMobile:"ontouchstart"in window,alwaysShow:!!d.alwaysShow,id:d.id,showAllDates:!!d.showAllDates,respectDisabledReadOnly:!!d.respectDisabledReadOnly,first:d.first,second:d.second};if(d.sibling){var E=d.sibling,C=M,L=E.minDate||C.minDate,Y=E.maxDate||C.maxDate;C.sibling=E,E.sibling=C,E.minDate=L,E.maxDate=Y,C.minDate=L,C.maxDate=Y,E.originalMinDate=L,E.originalMaxDate=Y,C.originalMinDate=L,C.originalMaxDate=Y,E.getRange=A,C.getRange=A}d.dateSelected&&p(u,M);var j=getComputedStyle(w).position;m||j&&"static"!==j||(M.inlinePosition=!0,w.style.setProperty("position","relative"));var I=a.filter((function(e){return e.positionedEl===M.positionedEl}));I.some((function(e){return e.inlinePosition}))&&(M.inlinePosition=!0,I.forEach((function(e){e.inlinePosition=!0})));D.appendChild(q),y.appendChild(D),M.alwaysShow&&S(M);return M}(e,t);if(a.length||d(document),n.shadowDom&&(a.some((function(e){return e.shadowDom===n.shadowDom}))||d(n.shadowDom)),a.push(n),n.second){var l=n.sibling;y({instance:n,deselect:!n.dateSelected}),y({instance:l,deselect:!l.dateSelected}),u(l)}return u(n,n.startDate||n.dateSelected),n.alwaysShow&&D(n),n}}]).default})); -------------------------------------------------------------------------------- /images/calendar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qodesmith/datepicker/40334bce4a2fdffa734455b86716cbff06228f3b/images/calendar.png -------------------------------------------------------------------------------- /images/chinese-days.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qodesmith/datepicker/40334bce4a2fdffa734455b86716cbff06228f3b/images/chinese-days.png -------------------------------------------------------------------------------- /images/daterange.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qodesmith/datepicker/40334bce4a2fdffa734455b86716cbff06228f3b/images/daterange.gif -------------------------------------------------------------------------------- /images/events.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qodesmith/datepicker/40334bce4a2fdffa734455b86716cbff06228f3b/images/events.png -------------------------------------------------------------------------------- /images/overlay-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qodesmith/datepicker/40334bce4a2fdffa734455b86716cbff06228f3b/images/overlay-button.png -------------------------------------------------------------------------------- /images/overlay-custom-months.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qodesmith/datepicker/40334bce4a2fdffa734455b86716cbff06228f3b/images/overlay-custom-months.png -------------------------------------------------------------------------------- /images/overlay-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qodesmith/datepicker/40334bce4a2fdffa734455b86716cbff06228f3b/images/overlay-default.png -------------------------------------------------------------------------------- /images/overlay-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qodesmith/datepicker/40334bce4a2fdffa734455b86716cbff06228f3b/images/overlay-placeholder.png -------------------------------------------------------------------------------- /images/overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qodesmith/datepicker/40334bce4a2fdffa734455b86716cbff06228f3b/images/overlay.png -------------------------------------------------------------------------------- /images/show-all-dates-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qodesmith/datepicker/40334bce4a2fdffa734455b86716cbff06228f3b/images/show-all-dates-on.png -------------------------------------------------------------------------------- /images/spanish-months.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qodesmith/datepicker/40334bce4a2fdffa734455b86716cbff06228f3b/images/spanish-months.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-datepicker", 3 | "version": "5.18.4", 4 | "description": "Get a date with JavaScript! A datepicker with no dependencies.", 5 | "main": "dist/datepicker.min.js", 6 | "style": "./src/datepicker.scss", 7 | "types": "datepicker.d.ts", 8 | "scripts": { 9 | "build": "NODE_OPTIONS=--openssl-legacy-provider webpack --mode production --env.prod", 10 | "test": "start-server-and-test start http-get://localhost:9001 start-cypress-test", 11 | "start": "NODE_OPTIONS=-'-openssl-legacy-provider' webpack-dev-server --mode development --env.dev --progress", 12 | "start-cypress-test": "cypress open" 13 | }, 14 | "keywords": [ 15 | "datepicker", 16 | "date range picker", 17 | "date range", 18 | "date range calendar", 19 | "date picker", 20 | "date", 21 | "picker", 22 | "calendar", 23 | "javascript", 24 | "vanilla" 25 | ], 26 | "author": "Qodesmith", 27 | "email": "theqodesmith@gmail.com", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/qodesmith/datepicker" 31 | }, 32 | "browserslist": [ 33 | "last 2 versions", 34 | "ie >= 9" 35 | ], 36 | "license": "MIT", 37 | "devDependencies": { 38 | "@babel/core": "^7.12.10", 39 | "@babel/preset-env": "^7.12.11", 40 | "@cypress/webpack-preprocessor": "^5.5.0", 41 | "autoprefixer": "^10.1.0", 42 | "babel-loader": "^8.2.2", 43 | "css-loader": "^5.0.1", 44 | "cssnano": "^4.1.10", 45 | "cypress": "^6.2.0", 46 | "eslint": "^7.16.0", 47 | "html-webpack-plugin": "^4.5.0", 48 | "mini-css-extract-plugin": "^1.3.3", 49 | "postcss-loader": "^4.1.0", 50 | "sass": "^1.30.0", 51 | "sass-loader": "^10.1.0", 52 | "start-server-and-test": "^1.11.6", 53 | "terser-webpack-plugin": "^3.0.6", 54 | "typescript": "^4.9.3", 55 | "webpack": "^4.41.6", 56 | "webpack-cli": "^3.3.12", 57 | "webpack-dev-server": "^3.11.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | cssnano: { 5 | preset: [ 6 | 'default', 7 | { 8 | /* 9 | Why do we need to exclude any calc transformations? 10 | Because we want to preserve any calc declarations as they are 11 | so that the browser is left to implement how to render any 12 | sub-pixel situations. Otherwise, cssnano will transform calc's 13 | into literal percent values and potentially break things for 14 | some users (https://bit.ly/2vVzZvL). 15 | */ 16 | calc: { 17 | exclude: true 18 | } 19 | } 20 | ] 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sandbox/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 |

Single Datepicker

12 | 13 | 14 |
15 | 16 | 17 | 18 |
19 |
20 |

Daterange

21 |
22 |
23 | 24 |
25 |
26 | 27 |
28 |
29 |
30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /sandbox/sandbox.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body { 7 | margin: 0; 8 | padding: 0; 9 | font-family: sans-serif; 10 | height: 100%; 11 | } 12 | 13 | input { 14 | font-size: inherit; 15 | padding: 10px; 16 | width: 350px; 17 | border: 1px solid #ccc; 18 | border-radius: 3px; 19 | outline: none; 20 | } 21 | 22 | section { 23 | height: 50%; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | } 28 | 29 | #daterange-section { 30 | align-items: flex-start; 31 | border-top: 1px solid; 32 | padding-top: 1em; 33 | } 34 | 35 | #daterange-inputs-container { 36 | display: flex; 37 | } 38 | 39 | #daterange-inputs-container > div { 40 | margin: 0 5px; 41 | } 42 | 43 | h2 { 44 | text-align: center; 45 | margin-bottom: 5px; 46 | } 47 | -------------------------------------------------------------------------------- /sandbox/sandbox.js: -------------------------------------------------------------------------------- 1 | import datepicker from '../src/datepicker' 2 | import './sandbox.css' 3 | 4 | // Enable us to play with datepicker in the console. 5 | window.datepicker = datepicker 6 | 7 | window.test = () => { 8 | window.start = datepicker('[data-cy="daterange-input-start"]', { 9 | id: 1, 10 | alwaysShow: 0, 11 | }) 12 | window.end = datepicker('[data-cy="daterange-input-end"]', { 13 | id: 1, 14 | alwaysShow: 0, 15 | }) 16 | 17 | window.single = datepicker('input', { 18 | alwaysShow: 0, 19 | defaultView: 'overlay', 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/datepicker.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | 3 | $width: 15.625em; 4 | $radius: $width * .016891; 5 | $transition: .3s; 6 | $font-size: $width * .056; 7 | $lightblue: lightblue; 8 | 9 | /* 10 | All a user has to do to change the calendar size is 11 | change the font-size on the container and everything 12 | magically resizes accordingly. Relative units ftw! 13 | */ 14 | .qs-datepicker-container { 15 | font-size: 1rem; 16 | font-family: sans-serif; 17 | color: black; 18 | position: absolute; 19 | width: $width; 20 | display: flex; 21 | flex-direction: column; 22 | z-index: 9001; 23 | user-select: none; 24 | border: 1px solid gray; 25 | border-radius: $radius; 26 | overflow: hidden; 27 | background: white; 28 | box-shadow: 0 ($width * .08) ($width * .08) ($width * -.06) rgba(0,0,0,.3); 29 | 30 | * { 31 | box-sizing: border-box; 32 | } 33 | } 34 | 35 | .qs-centered { 36 | position: fixed; 37 | top: 50%; 38 | left: 50%; 39 | transform: translate(-50%, -50%); 40 | } 41 | 42 | .qs-hidden { 43 | display: none; 44 | } 45 | 46 | .qs-overlay { 47 | position: absolute; 48 | top: 0; 49 | left: 0; 50 | background: rgba(0,0,0,.75); 51 | color: white; 52 | width: 100%; 53 | height: 100%; 54 | padding: .5em; 55 | z-index: 1; 56 | opacity: 1; 57 | transition: opacity $transition; 58 | display: flex; 59 | flex-direction: column; 60 | 61 | &.qs-hidden { 62 | opacity: 0; 63 | z-index: -1; 64 | } 65 | 66 | .qs-overlay-year { // Overlay year input element. 67 | border: none; 68 | background: transparent; 69 | border-bottom: 1px solid white; 70 | border-radius: 0; 71 | color: white; 72 | font-size: $font-size; 73 | padding: .25em 0; 74 | width: 80%; 75 | text-align: center; 76 | margin: 0 auto; 77 | display: block; 78 | 79 | // https://goo.gl/oUuGkG 80 | &::-webkit-inner-spin-button { 81 | -webkit-appearance: none; 82 | } 83 | } 84 | 85 | .qs-close { 86 | padding: .5em; 87 | cursor: pointer; 88 | position: absolute; 89 | top: 0; 90 | right: 0; 91 | } 92 | 93 | .qs-submit { 94 | border: 1px solid white; 95 | border-radius: $radius; 96 | padding: .5em; 97 | margin: 0 auto auto; 98 | cursor: pointer; 99 | background: rgba(128,128,128,.4); 100 | 101 | &.qs-disabled { 102 | color: gray; 103 | border-color: gray; 104 | cursor: not-allowed; 105 | } 106 | } 107 | 108 | .qs-overlay-month-container { 109 | display: flex; 110 | flex-wrap: wrap; 111 | flex-grow: 1; 112 | } 113 | 114 | .qs-overlay-month { 115 | display: flex; 116 | justify-content: center; 117 | align-items: center; 118 | width: #{'calc(100% / 3)'}; 119 | cursor: pointer; 120 | opacity: .5; 121 | transition: opacity math.div($transition, 2); 122 | 123 | &.active, &:hover { 124 | opacity: 1; 125 | } 126 | } 127 | } 128 | 129 | .qs-controls { 130 | width: 100%; 131 | display: flex; 132 | justify-content: space-between; 133 | align-items: center; 134 | flex-grow: 1; 135 | flex-shrink: 0; 136 | background: lightgray; 137 | filter: blur(0px); 138 | transition: filter $transition; 139 | 140 | &.qs-blur { 141 | filter: blur(5px); 142 | } 143 | } 144 | 145 | .qs-arrow { 146 | height: math.div($width, 10); 147 | width: math.div($width, 10); 148 | position: relative; 149 | cursor: pointer; 150 | border-radius: $radius; 151 | transition: background .15s; 152 | 153 | &:hover { 154 | &.qs-left:after { 155 | border-right-color: black; 156 | } 157 | 158 | &.qs-right:after { 159 | border-left-color: black; 160 | } 161 | 162 | & { 163 | background: rgba(0,0,0,.1); 164 | } 165 | } 166 | 167 | &:after { 168 | content: ''; 169 | border: math.div($width, 40) solid transparent; 170 | position: absolute; 171 | top: 50%; 172 | transition: border .2s; 173 | } 174 | 175 | &.qs-left:after { 176 | border-right-color: gray; 177 | right: 50%; 178 | transform: translate(25%, -50%); 179 | } 180 | 181 | &.qs-right:after { 182 | border-left-color: gray; 183 | left: 50%; 184 | transform: translate(-25%, -50%); 185 | } 186 | } 187 | 188 | .qs-month-year { 189 | font-weight: bold; 190 | transition: border .2s; 191 | border-bottom: 1px solid transparent; 192 | 193 | &:not(.qs-disabled-year-overlay) { 194 | cursor: pointer; 195 | 196 | &:hover { 197 | border-bottom: 1px solid gray; 198 | } 199 | } 200 | 201 | &:focus, 202 | &:active:focus { 203 | outline: none; 204 | } 205 | } 206 | 207 | .qs-month { 208 | padding-right: .5ex; 209 | } 210 | 211 | .qs-year { 212 | padding-left: .5ex; 213 | } 214 | 215 | .qs-squares { 216 | display: flex; 217 | flex-wrap: wrap; 218 | padding: $width * .02; 219 | filter: blur(0px); 220 | transition: filter $transition; 221 | 222 | &.qs-blur { 223 | filter: blur(5px); 224 | } 225 | } 226 | 227 | .qs-square { 228 | width: #{'calc(100% / 7)'}; 229 | height: math.div($width, 10); 230 | display: flex; 231 | align-items: center; 232 | justify-content: center; 233 | cursor: pointer; 234 | transition: background .1s; 235 | 236 | // Overriden for date-range dates below. 237 | border-radius: $radius; 238 | 239 | &:not(.qs-empty):not(.qs-disabled):not(.qs-day):not(.qs-active) { 240 | &:hover { 241 | background: orange; 242 | } 243 | } 244 | } 245 | 246 | // Today's date 247 | .qs-current { 248 | font-weight: bold; 249 | text-decoration: underline; 250 | } 251 | 252 | /* 253 | 3 possibilities: 254 | 1. Single, active date. 255 | 2. Daterange start selection. 256 | 3. Daterange end selection. 257 | */ 258 | .qs-active, 259 | .qs-range-start, 260 | .qs-range-end { 261 | background: $lightblue; 262 | } 263 | 264 | // Daterange start selection. 265 | .qs-range-start { 266 | &:not(.qs-range-6) { 267 | border-top-right-radius: 0; 268 | border-bottom-right-radius: 0; 269 | } 270 | } 271 | 272 | // Daterange middle selections. 273 | .qs-range-middle { 274 | background: lighten($lightblue, 10%); 275 | 276 | &:not(.qs-range-0):not(.qs-range-6) { 277 | border-radius: 0; 278 | } 279 | 280 | &.qs-range-0 { 281 | border-top-right-radius: 0; 282 | border-bottom-right-radius: 0; 283 | } 284 | 285 | &.qs-range-6 { 286 | border-top-left-radius: 0; 287 | border-bottom-left-radius: 0; 288 | } 289 | } 290 | 291 | // Daterange end selection. 292 | .qs-range-end { 293 | &:not(.qs-range-0) { 294 | border-top-left-radius: 0; 295 | border-bottom-left-radius: 0; 296 | } 297 | } 298 | 299 | .qs-disabled, 300 | .qs-outside-current-month { 301 | opacity: .2; 302 | } 303 | 304 | .qs-disabled { 305 | cursor: not-allowed; 306 | } 307 | 308 | .qs-empty { 309 | cursor: default; 310 | } 311 | 312 | .qs-day { 313 | cursor: default; 314 | font-weight: bold; 315 | color: gray; 316 | } 317 | 318 | .qs-event { 319 | position: relative; 320 | 321 | &:after { 322 | content: ''; 323 | position: absolute; 324 | width: $width * .03; 325 | height: $width * .03; 326 | border-radius: 50%; 327 | background: #07f; 328 | bottom: 0; 329 | right: 0; 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | const TerserPlugin = require('terser-webpack-plugin') 5 | 6 | 7 | module.exports = (env, argv) => ({ 8 | mode: env.prod ? 'production' : 'development', 9 | entry: path.resolve(__dirname, env.prod ? 'src/datepicker.js' : 'sandbox/sandbox.js'), 10 | target: 'web', 11 | output: env.prod ? { 12 | path: path.resolve(__dirname, 'dist'), 13 | library: 'datepicker', // The name of the global variable the library is set to. 14 | libraryExport: 'default', 15 | libraryTarget: 'umd', // "Universal" export - Node, browser, amd, etc. 16 | filename: 'datepicker.min.js' 17 | } : {}, 18 | devServer: { 19 | open: true, 20 | contentBase: path.resolve(__dirname, './sandbox'), 21 | host: '0.0.0.0', // Allow viewing site locally on a phone. 22 | port: 9001, 23 | public: 'http://localhost:9001' 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.(scss|css)$/i, 29 | use: [ 30 | MiniCssExtractPlugin.loader, // https://goo.gl/uUBr8G 31 | 'css-loader', 32 | 'postcss-loader', // https://goo.gl/BCwCzg - needs to be *after* `css-loader`. 33 | 'sass-loader' 34 | ] 35 | } 36 | ] 37 | }, 38 | optimization: { 39 | minimize: !!env.prod, 40 | minimizer: [ 41 | new TerserPlugin({ // https://goo.gl/YgdtKb 42 | parallel: true, //https://goo.gl/hUkvnK 43 | terserOptions: { // https://goo.gl/y3psR1 44 | ecma: 5, 45 | output: { 46 | comments: false 47 | } 48 | } 49 | }) 50 | ] 51 | }, 52 | plugins: [ 53 | new MiniCssExtractPlugin({ 54 | filename: 'datepicker.min.css' 55 | }), 56 | !env.prod && new HtmlWebpackPlugin({ 57 | template: path.resolve(__dirname, 'sandbox/index.ejs'), 58 | title: 'Datepicker Sandbox' 59 | }) 60 | ].filter(Boolean) 61 | }) 62 | --------------------------------------------------------------------------------