`...
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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['