├── .browserslistrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── stale.yml └── workflows │ └── auto-assign.yml ├── .gitignore ├── .npmrc ├── .prettierrc.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.js ├── demo.gif ├── issue_template.md ├── package.json ├── pnpm-lock.yaml ├── public ├── favicon.ico ├── i18n │ ├── en.js │ ├── es.js │ ├── fr.js │ ├── it.js │ └── pt.js └── index.html ├── src ├── App.vue ├── DatePicker │ ├── HotelDatePicker.vue │ └── components │ │ ├── BookingBullet.vue │ │ ├── DateInput.vue │ │ ├── Day.vue │ │ ├── Month.vue │ │ ├── Price.vue │ │ └── WeekRow.vue ├── assets │ ├── images │ │ ├── calendar_icon.regular.svg │ │ ├── ic-arrow-right-datepicker.regular.svg │ │ ├── ic-arrow-right-green.regular.svg │ │ └── menu.svg │ └── scss │ │ ├── _mixins.scss │ │ ├── _variables.scss │ │ └── index.scss ├── helpers.js ├── index.js └── main.js ├── tests └── unit │ ├── datepicker.spec.js │ ├── datepickerDay.spec.js │ └── datepickerHelpers.spec.js ├── travis.yml └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | node_modules/* 3 | build/* 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: ['plugin:vue/essential', '@vue/airbnb', '@vue/prettier'], 7 | parserOptions: { 8 | parser: '@babel/eslint-parser', 9 | }, 10 | plugins: ['prettier'], 11 | rules: { 12 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | 'no-plusplus': 'off', 14 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 15 | 'padding-line-between-statements': [ 16 | 'error', 17 | { blankLine: 'always', prev: '*', next: 'return' }, 18 | { blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' }, 19 | { 20 | blankLine: 'never', 21 | prev: ['const', 'let', 'var'], 22 | next: ['const', 'let', 'var'], 23 | }, 24 | { blankLine: 'always', prev: '*', next: 'block-like' }, 25 | { blankLine: 'always', prev: 'block-like', next: '*' }, 26 | ], 27 | 'vue/order-in-components': [ 28 | 'error', 29 | { 30 | order: [ 31 | 'el', 32 | 'name', 33 | 'parent', 34 | 'functional', 35 | ['delimiters', 'comments'], 36 | ['components', 'directives', 'filters'], 37 | 'extends', 38 | 'mixins', 39 | 'inheritAttrs', 40 | 'model', 41 | ['props', 'propsData'], 42 | 'data', 43 | 'computed', 44 | 'watch', 45 | 'LIFECYCLE_HOOKS', 46 | 'methods', 47 | 'head', 48 | ['template', 'render'], 49 | 'renderError', 50 | ], 51 | }, 52 | ], 53 | }, 54 | overrides: [ 55 | { 56 | files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/unit/**/*.spec.{j,t}s?(x)'], 57 | env: { 58 | jest: true, 59 | }, 60 | }, 61 | ], 62 | } 63 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 20 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 5 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - Feature Request 8 | - bug 9 | - help wanted 10 | # Label to use when marking an issue as stale 11 | staleLabel: stale 12 | # Comment to post when marking an issue as stale. Set to `false` to disable 13 | markComment: > 14 | This issue has been automatically marked as stale because it has not had 15 | recent activity. It will be closed if no further activity occurs. Thank you 16 | for your contributions. 17 | # Comment to post when closing a stale issue. Set to `false` to disable 18 | closeComment: false 19 | -------------------------------------------------------------------------------- /.github/workflows /auto-assign.yml: -------------------------------------------------------------------------------- 1 | name: Auto Assign 2 | on: 3 | issues: 4 | types: [opened] 5 | pull_request: 6 | types: [opened] 7 | jobs: 8 | run: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | pull-requests: write 13 | steps: 14 | - name: 'Auto-assign issue' 15 | uses: pozil/auto-assign-issue@v1 16 | with: 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | assignees: matiasperrone 19 | numOfAssignee: 1 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | # local env files 5 | .env.local 6 | .env.*.local 7 | 8 | # Log files 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Editor directories and files 14 | .idea 15 | .vscode 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | *.sw? 21 | 22 | dist/ 23 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "printWidth": 120, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "useTabs": false, 7 | "semi": false, 8 | "trailingComma": "all", 9 | "insertPragma": false 10 | } 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | # v4.3: 4 | - Improved price styling (fixed #264) 5 | - Added price currency symbol string, for instance: '$', 'EUR'. The new prop is `priceSymbol`, default at empty string 6 | # v4.2: 7 | - Fixed #257: Disabled dates not updating when new dates are added 8 | - New [CHANGELOG.md](CHANGELOG.md) file 9 | 10 | # v4.1 - 2021-01-14 11 | - correct configuration in i18n `fecha` package 12 | - Italian added on demo (dev) page 13 | - new prop `yearBeforeMonth` 14 | 15 | # v4.0 - 2021-01-14 16 | 17 | ## Important Fixes! 18 | * Now is working properly in mobile. 19 | 20 | ## Featured changes 21 | * Language now is available in a folder with different translations available: es, en, pt, fr (ISO lang codes) 22 | * New prop `disabledWeekDays`: An object with the following properties: `sunday`, `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, the value indicates if that day is disabled (true) or enabled (false). 23 | * New event `next-month-rendered` (Beta 11) 24 | * SCSS now in a separated file 25 | * Dependencies updated. 26 | 27 | ## Documentation Improvements 28 | * Props 29 | * Events 30 | 31 | ## Featured changes 32 | * **New Event** `next-month-rendered`, emitted every time the next month button is pressed and a new month is rendered. 33 | * #201 UX improvements related to check-in selection. 34 | 35 | ## Deprecation 36 | * Prop: `disabledDaysOfWeek`: use the new `disabledWeekDays` instead. `disabledWeekDays` and `disabledDaysOfWeek` both work but `disabledWeekDays` take precedence. 37 | * Events: `bookingClicked`, `dayClicked`, `handleCheckIncheckOutHalfDay` and `periodSelected`, now use kebab-case as recommended in Vue documentation (old names still works and will be removed in v5) 38 | 39 | ## Breaking changes 40 | * `showYear` now is true by default 41 | * `value` now is `false` by default 42 | 43 | ## Other changes 44 | * "npm" and "pnpm" lock files with version bump. 45 | * PR #230: Use relative units instead of px 46 | * PR #246: Dependencies fixes. 47 | * PR #241: Add `price` argument to `periodDates` doc. 48 | * PR #259: Dates become disabled when toggling month (`singleDaySelection`) 49 | * Improvements 50 | * value is Boolean as expected. 51 | * Component renaming (src) 52 | * fixed range highlight selection showing on "singleDaySelection" 53 | * fixed `startingDateValue` cleared when open the datepicker. 54 | * minor bug fixes 55 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | 3 | ### Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ### Our Standards 13 | 14 | Examples of behaviour that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behaviour by participants include: 24 | 25 | * The use of sexualised language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ### Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behaviour and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behaviour. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ### Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ### Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behaviour may be 58 | reported by contacting the project team at hello@krystalcampioni.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ### Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | We love pull requests from everyone. By participating in this project, you 3 | agree to abide by the [code of conduct]. 4 | 5 | [code of conduct]: https://github.com/krystalcampioni/vue-hotel-datepicker/blob/master/CODE_OF_CONDUCT.md 6 | 7 | * Fork, then clone the repo: 8 | ``` 9 | git clone git@github.com:your-username/vue-hotel-datepicker.git 10 | ``` 11 | 12 | * Set up your machine: 13 | ``` 14 | npm install 15 | ``` 16 | or 17 | ``` 18 | yarn 19 | ``` 20 | 21 | * Push to your fork and submit a pull request 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Krystal Campioni 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm](https://img.shields.io/npm/dt/vue-hotel-datepicker.svg)](vue-hotel-datepicker) 2 | [![Build Status](https://travis-ci.org/krystalcampioni/vue-hotel-datepicker.svg?branch=main)](https://travis-ci.org/krystalcampioni/vue-hotel-datepicker) 3 | 4 | A responsive date range picker for Vue.js that displays the number of nights selected and allow several useful options like custom check-in/check-out rules, localization support and more. 5 | 6 | 7 | ![demo gif](https://github.com/ZestfulNation/vue-hotel-datepicker/blob/main/demo.gif?raw=true) 8 | 9 | 10 | 11 | ## Demo 12 | [https://ZestfulNation.github.io/vue-hotel-datepicker/](https://ZestfulNation.github.io/vue-hotel-datepicker/) 13 | 14 | ## Installation 15 | 16 | #### NPM 17 | 18 | ```bash 19 | npm install vue-hotel-datepicker 20 | ``` 21 | 22 | #### PNPM 23 | 24 | ```bash 25 | pnpm install vue-hotel-datepicker 26 | ``` 27 | 28 | #### YARN 29 | 30 | ```bash 31 | yarn add vue-hotel-datepicker 32 | ``` 33 | 34 | 35 | ```javascript 36 | import HotelDatePicker from 'vue-hotel-datepicker' 37 | import 'vue-hotel-datepicker/dist/vueHotelDatepicker.css'; 38 | 39 | export default { 40 | components: { 41 | HotelDatePicker, 42 | }, 43 | } 44 | ``` 45 | 46 | ```html 47 | 48 | ``` 49 | 50 | 51 | ## Props/Options 52 | 53 | | Name | Type | Default | Description | 54 | |--|--|--|--| 55 | |**alwaysVisible**|`Boolean`|`false`|If true shows display calendar in the page without an input. 56 | |**bookings**|`Array`|`[]`|If you want to show bookings. 57 | |**closeDatepickerOnClickOutside**|`Boolean`|`true`|Closes the date picker when the user clicks outside the date picker. 58 | |**disableCheckoutOnCheckin**|`Boolean`|`false`|If set to true, disable checkout on the same date has checkin. 59 | |**disabledDates**|`Array`|`[]`|An array of strings in this format: `YYYY-MM-DD`. All the dates passed to the list will be disabled. 60 | |**disabledDaysOfWeek**|`Array`|`[]`| **DEPRECATED**: An array of strings in this format: `['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']`. All the days passed to the list will be disabled. It depends on the translated names. 61 | |**disabledWeekDays**|`Object`|`{}`| An object with the following properties: `sunday`, `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, the value indicates if that day is disabled (true) or enabled (false). 62 | |**displayClearButton**|`Boolean`|`true`|If set to true, displays a clear button on the right side of the input if there are dates set. 63 | |**enableCheckout**|`Boolean`|`false`|If `true`, allows the checkout on a disabled date. 64 | |**endDate**|`[Date, String, Number]`|`Infinity`|The end view date. All the dates after this date will be disabled. 65 | |**endingDateValue**|`Date`|`null`|The initial value of the end date. 66 | |**firstDayOfWeek**|`Number`|`0`|The first day of the week. Where Sun = 0, Mon = 1, ... Sat = 6. You need to set the right order in `i18n.day-names` too. 67 | |**format**|`String`|`'YYYY-MM-DD'`|The date format string. 68 | |**gridStyle**|`Boolean`|`true`|If false hides the grid around the days. 69 | |**halfDay**|`Boolean`|`true`|Allows to have half a day, if you have check in at noon and checkout before noon 70 | |**hoveringTooltip**|`[Boolean, Function]`|`true`|Shows a tooltip with the number of nights when hovering a date. 71 | |**i18n**|`Object`| see below | Holds the traslation of the date picker. 72 | |**lastDateAvailable**|`[Number, Date]`|`Infinity`|Allows to stop calendar pagination after the month of that date 73 | |**maxNights**|`Number`|`null`|Maximum nights required to select a range of dates. `0` or `null` for no limit. 74 | |**minNights**|`Number`|`1`|Minimum nights required to select a range of dates. 75 | |**periodDates**|`Array`| `[]` | If you want to have specific startAt and endAt period with different duration or price or type of period. See below for more information 76 | |**positionRight**|`Boolean`|`false`|If true shows the calendar on the **right** of the input. 77 | |**priceDecimals**|`Number`|`0`|The price decimals for weekly periods (see `periodDates`). 78 | |**priceSymbol**|`String`|`''`|The price symbol added before the price when `showPrice` is true and a `price` has been set in one of the `periodDates` array items (period). 79 | |**showPrice**|`Boolean`|`false`|If set to true, displays a price contains on your `periodDates`. 80 | |**showSingleMonth**|`Boolean`|`false`|If set to true, display one month only. 81 | |**showWeekNumbers**|`Boolean`|`false`|If set to true, displays the week numbers. 82 | |**showYear**|`Boolean`|`true`|Shows the year next to the month. 83 | |**singleDaySelection**|`Boolean`|`false`|When true only one day can be selected instead of a range. 84 | |**startDate**|`[Date, String]`|`new Date()`|The start view date. All the dates before this date will be disabled. 85 | |**startingDateValue**|`Date`|`null`|The initial value of the start date. 86 | |**tooltipMessage**|`String`|`null`|If provided, it will override the default tooltip "X nights" with the text provided. You can use HTML in the string. 87 | |**value**|`Boolean`|`false`| The v-model prop, controls the visibility of the date picker. 88 | |**yearBeforeMonth**|`Boolean`|`false`| Show the year before the month, only when showYear is true. 89 | 90 | ## i18n Defaults: 91 | 92 | ```js 93 | i18n: { 94 | "night": "Night", 95 | "nights": "Nights", 96 | "week": "week", 97 | "weeks": "weeks", 98 | "day-names": ["Sun", "Mon", "Tue", "Wed", "Thur", "Fri", "Sat"], 99 | "check-in": "Check-in", 100 | "check-out": "Check-out", 101 | "month-names": [ 102 | "January", 103 | "February", 104 | "March", 105 | "April", 106 | "May", 107 | "June", 108 | "July", 109 | "August", 110 | "September", 111 | "October", 112 | "November", 113 | "December", 114 | ], 115 | "tooltip": { 116 | "halfDayCheckIn": "Available CheckIn", 117 | "halfDayCheckOut": "Available CheckOut", 118 | "saturdayToSaturday": "Only Saturday to Saturday", 119 | "sundayToSunday": "Only Sunday to Sunday", 120 | "minimumRequiredPeriod": "%{minNightInPeriod} %{night} minimum.", 121 | }, 122 | } 123 | ``` 124 | 125 | ## periodDates 126 | - Type: `Array` 127 | - Default: `[]` 128 | If you want to have specific startAt and endAt period with different duration or price or type of period- 129 | 130 | Key | Type | Description 131 | -------------------------------------|------------|------------------------- 132 | endAt | String | YYYY-MM-DD 133 | startAt | String | YYYY-MM-DD 134 | minimumDuration | Number | Minimum stay (Type: weekly => per_week \| Type: nightly => per night) 135 | periodType | String | *nightly*, *weekly_by_saturday*, *weekly_by_sunday* 136 | price | Float | Price displayed on each day for this period 137 | 138 | 139 | **Example:** 140 | ```js 141 | periodDates: [ 142 | { 143 | startAt: "2020-06-09", 144 | endAt: "2020-07-26", 145 | minimumDuration: 4, 146 | periodType: "nightly" 147 | }, 148 | { 149 | startAt: "2020-07-26", 150 | endAt: "2020-09-30", 151 | minimumDuration: 1, 152 | periodType: "weekly_by_saturday" 153 | }, 154 | { 155 | startAt: "2020-09-30", 156 | endAt: "2020-11-30", 157 | minimumDuration: 2, 158 | periodType: "weekly_by_sunday", 159 | price: 4000.0 160 | } 161 | ], 162 | ``` 163 | 164 | #### `MinimumDuration` with a periodType `weekly-~` equals to a week 165 | 166 | ## bookings 167 | If you want to show bookings 168 | - Type: `Array` 169 | - Default: `[]` 170 | 171 | Key | Type | Description 172 | -----------------|-------------|------------------------- 173 | `checkInDate` | `String` | `'YYYY-MM-DD'` 174 | `checkOutDate` | `String` | `'YYYY-MM-DD'` 175 | `style` | `Object` | Style, (see the example) 176 | 177 | **Example:** 178 | ```js 179 | bookings: [ 180 | { 181 | event: true, 182 | checkInDate: "2020-08-26", 183 | checkOutDate: "2020-08-29", 184 | style: { 185 | backgroundColor: "#399694" 186 | } 187 | }, 188 | { 189 | event: false, 190 | checkInDate: "2020-07-01", 191 | checkOutDate: "2020-07-08", 192 | style: { 193 | backgroundColor: "#9DC1C9" 194 | } 195 | } 196 | ], 197 | ``` 198 | 199 | 200 | ## Methods 201 | ⚠️ In order to open/close the datepicker from an external element, such as a button make sure to set `closeDatepickerOnClickOutside` to `false` 202 | 203 | | Name | Description | 204 | |--|--| 205 | |`hideDatepicker` | Hides the datepicker 206 | |`showDatepicker` | Shows the datepicker 207 | |`toggleDatepicker`| Toggles (shows or hides) the datepicker 208 | 209 | ## Events 210 | 211 | | Name | Params enum | Description | 212 | |--|--|--| 213 | |`booking-clicked`|`MouseEvent`, `Date`, `Object`|Emitted every time a booking is clicked. The first param is the mouse javascript event, the second is the clicked Date and the third is the clicked booking. 214 | |`check-in-changed`| | Emitted every time a new check in date is selected with the new date as payload. 215 | |`check-out-changed`| | Emitted every time a new check out date is selected with the new date as payload. 216 | |`clear-selection`| | Emitted every time you clicked on clear Date button. 217 | |`day-clicked`| `Date`, `String`, `Date\|Number\|String` | Emitted every time when day is clicked. The params are clicked: date, format and next disabled date. 218 | |`handle-checkin-checkout-half-day`| `Object` | Emitted on [`beforeMount`, `clear-selection`, `checkout`]. Param: Object of checkin-checkout date. 219 | |`next-month-rendered`| | Emitted every time the next month is rendered. 220 | |`period-selected`| `Event`, `Date`, `Date` | Emitted every time when a checkOut is clicked. Params: Mouse Event, checkIn, checkOut 221 | 222 | ### `booking-clicked` examples 223 | ```js 224 | { 225 | checkInDate: "YYYY-MM-DD", 226 | checkOutDate: "YYYY-MM-DD", 227 | style: { 228 | backgroundColor: "#399694", 229 | } 230 | } 231 | ``` 232 | 233 | ## Credits 234 | This component was originally built as a Vue wrapper component for the [Hotel Datepicker](https://github.com/benitolopez/hotel-datepicker) by @benitolopez. Version 2.0.0 was completely rewritten with Vue, removing the original library, removing some features and introducing others. 235 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'], 3 | } 4 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZestfulNation/vue-hotel-datepicker/3e92a7e6ebd16078329312973e7cb8d6808507ac/demo.gif -------------------------------------------------------------------------------- /issue_template.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | [Description of the bug or feature] 4 | 5 | ### Code sample 6 | [ If you are submitting a bug issue, paste your code so I can reproduce it, or a link to a Codepen with it ] 7 | 8 | ### Steps to Reproduce 9 | 10 | 1. [First Step] 11 | 2. [Second Step] 12 | 3. [and so on...] 13 | 14 | **Expected behavior:** [What you expected to happen] 15 | 16 | **Actual behavior:** [What actually happened] 17 | 18 | ### Datepicker Version 19 | 20 | [ The version of the datepicker you have installed in your project ] 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-hotel-datepicker", 3 | "version": "4.6.0", 4 | "description": "A responsive date range picker for Vue.js that displays the number of nights selected and allow several useful options like custom check-in/check-out rules, localization support and more", 5 | "author": "krystalcampioni ", 6 | "main": "dist/vueHotelDatepicker.common.js", 7 | "repository": { 8 | "url": "git@github.com:ZestfulNation/vue-hotel-datepicker.git", 9 | "type": "git" 10 | }, 11 | "keywords": [ 12 | "vue", 13 | "vuejs", 14 | "date", 15 | "dates", 16 | "input", 17 | "date input", 18 | "datepicker", 19 | "date picker", 20 | "range", 21 | "date range picker", 22 | "vue-datepicker", 23 | "vue-hotel-datepicker", 24 | "vue-date-range" 25 | ], 26 | "license": "MIT", 27 | "scripts": { 28 | "serve": "vue-cli-service serve", 29 | "build": "vue-cli-service build --target lib --name vueHotelDatepicker ./src/index.js", 30 | "build-package": "vue-cli-service build --target lib --name vueHotelDatepicker ./src/index.js", 31 | "test:unit": "vue-cli-service test:unit", 32 | "lint": "vue-cli-service lint" 33 | }, 34 | "files": [ 35 | "dist/*", 36 | "src/*", 37 | "*.json" 38 | ], 39 | "dependencies": { 40 | "deasync": "^0.1.28", 41 | "fecha": "^4.2.1", 42 | "lodash.throttle": "^4.1.1", 43 | "vue": "^2.6.14" 44 | }, 45 | "devDependencies": { 46 | "@babel/core": "^7.14.6", 47 | "@babel/eslint-parser": "^7.14.7", 48 | "@vue/cli-plugin-babel": "~4.5.13", 49 | "@vue/cli-plugin-eslint": "~4.5.13", 50 | "@vue/cli-plugin-unit-jest": "~4.5.13", 51 | "@vue/cli-service": "~4.5.13", 52 | "@vue/eslint-config-airbnb": "^5.3.0", 53 | "@vue/eslint-config-prettier": "^6.0.0", 54 | "@vue/test-utils": "^1.2.1", 55 | "chai": "^4.3.4", 56 | "copy-webpack-plugin": "^9.0.1", 57 | "core-js": "^3.15.2", 58 | "eslint": "^7.30.0", 59 | "eslint-plugin-prettier": "^3.4.0", 60 | "eslint-plugin-vue": "^7.13.0", 61 | "prettier": "^2.3.2", 62 | "sass": "^1.35.2", 63 | "sass-loader": "^8.0.2", 64 | "vue-template-compiler": "^2.6.14" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZestfulNation/vue-hotel-datepicker/3e92a7e6ebd16078329312973e7cb8d6808507ac/public/favicon.ico -------------------------------------------------------------------------------- /public/i18n/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | night: 'Night', 3 | nights: 'Nights', 4 | week: 'Week', 5 | weeks: 'Weeks', 6 | 'day-names': ['Sun', 'Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat'], 7 | 'check-in': 'Check-in', 8 | 'check-out': 'Check-out', 9 | 'month-names': [ 10 | 'January', 11 | 'February', 12 | 'March', 13 | 'April', 14 | 'May', 15 | 'June', 16 | 'July', 17 | 'August', 18 | 'September', 19 | 'October', 20 | 'November', 21 | 'December', 22 | ], 23 | tooltip: { 24 | halfDayCheckIn: 'Available CheckIn', 25 | halfDayCheckOut: 'Available CheckOut', 26 | saturdayToSaturday: 'Only Saturday to Saturday', 27 | sundayToSunday: 'Only Sunday to Sunday', 28 | minimumRequiredPeriod: '%{minNightInPeriod} %{night} minimum.', 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /public/i18n/es.js: -------------------------------------------------------------------------------- 1 | export default { 2 | night: 'Noche', 3 | nights: 'Noches', 4 | week: 'Semana', 5 | weeks: 'Semanas', 6 | 'day-names': ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'], 7 | 'check-in': 'Arribo', 8 | 'check-out': 'Partida', 9 | 'month-names': [ 10 | 'Enero', 11 | 'Febrero', 12 | 'Marzo', 13 | 'Abril', 14 | 'Mayo', 15 | 'Junio', 16 | 'Julio', 17 | 'Agosto', 18 | 'Septiembre', 19 | 'Octubre', 20 | 'Noviembre', 21 | 'Diciembre', 22 | ], 23 | tooltip: { 24 | halfDayCheckIn: 'Arribo Disponible', 25 | halfDayCheckOut: 'Partida Disponible', 26 | saturdayToSaturday: 'Sólo Sábados a Sábados', 27 | sundayToSunday: 'Sólo Domingo a Domingo', 28 | minimumRequiredPeriod: '%{minNightInPeriod} %{night} mínimo.', 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /public/i18n/fr.js: -------------------------------------------------------------------------------- 1 | export default { 2 | night: 'Nuit', 3 | nights: 'Nuits', 4 | week: 'Semaine', 5 | weeks: 'Semaines', 6 | 'check-in': 'Départ', 7 | 'check-out': 'Arrivée', 8 | 'day-names': ['Lu', 'Ma', 'Me', 'Je', 'Ve', 'Sa', 'Di'], 9 | 'month-names': [ 10 | 'Janvier', 11 | 'Février', 12 | 'Mars', 13 | 'Avril', 14 | 'Mai', 15 | 'Juin', 16 | 'Juillet', 17 | 'Août', 18 | 'Septembre', 19 | 'Octobre', 20 | 'Novembre', 21 | 'Décembre', 22 | ], 23 | tooltip: { 24 | halfDayCheckIn: 'Réservation possible', 25 | halfDayCheckOut: 'Réservation possible', 26 | saturdayToSaturday: 'Du samedi au samedi uniquement', 27 | sundayToSunday: 'Du dimanche au dimanche uniquement', 28 | minimumRequiredPeriod: '%{minNightInPeriod} %{night} minimum', 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /public/i18n/it.js: -------------------------------------------------------------------------------- 1 | export default { 2 | night: 'Notte', 3 | nights: 'Notti', 4 | week: 'Settimana', 5 | weeks: 'Settimane', 6 | 'day-names': ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'], 7 | 'check-in': 'Check-in', 8 | 'check-out': 'Check-out', 9 | 'month-names': [ 10 | 'Gennaio', 11 | 'Febbraio', 12 | 'Marzo', 13 | 'Aprile', 14 | 'Maggio', 15 | 'Giugno', 16 | 'Luglio', 17 | 'Agosto', 18 | 'Settembre', 19 | 'Ottobre', 20 | 'Novembre', 21 | 'Dicembre', 22 | ], 23 | tooltip: { 24 | halfDayCheckIn: 'Check-in Disponibile', 25 | halfDayCheckOut: 'Check-out Disponibile', 26 | saturdayToSaturday: 'Solo da Sabato a Sabato', 27 | sundayToSunday: 'Solo da Domenica a Domenica', 28 | minimumRequiredPeriod: '%{minNightInPeriod} %{night} minimo.', 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /public/i18n/pt.js: -------------------------------------------------------------------------------- 1 | export default { 2 | night: 'Noite', 3 | nights: 'Noites', 4 | week: 'Semana', 5 | weeks: 'Semanas', 6 | 'day-names': ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sab'], 7 | 'check-in': 'Chegada', 8 | 'check-out': 'Partida', 9 | 'month-names': [ 10 | 'Janeiro', 11 | 'Fevereiro', 12 | 'Março', 13 | 'Abril', 14 | 'Maio', 15 | 'Junho', 16 | 'Julho', 17 | 'Agosto', 18 | 'Setembro', 19 | 'Outubro', 20 | 'Novembro', 21 | 'Dezembro', 22 | ], 23 | tooltip: { 24 | halfDayCheckIn: 'Chegada possíveis', 25 | halfDayCheckOut: 'Partida possíveis', 26 | saturdayToSaturday: 'Sábado a Sábado apenas', 27 | sundayToSunday: 'Domingo a domingo apenas', 28 | minimumRequiredPeriod: '%{minNightInPeriod} %{night} mínimo.', 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | vue-hotel-datepicker v4.0 10 | 11 | 12 | 13 | 14 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 261 | 262 | 516 | 517 | 665 | -------------------------------------------------------------------------------- /src/DatePicker/HotelDatePicker.vue: -------------------------------------------------------------------------------- 1 | 210 | 211 | 1374 | -------------------------------------------------------------------------------- /src/DatePicker/components/BookingBullet.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 66 | -------------------------------------------------------------------------------- /src/DatePicker/components/DateInput.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 55 | -------------------------------------------------------------------------------- /src/DatePicker/components/Day.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 707 | -------------------------------------------------------------------------------- /src/DatePicker/components/Month.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /src/DatePicker/components/Price.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/DatePicker/components/WeekRow.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 36 | -------------------------------------------------------------------------------- /src/assets/images/calendar_icon.regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/images/ic-arrow-right-datepicker.regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic-arrow-right-green.regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin focusStyle() { 2 | &:focus { 3 | outline: 1px dashed darken($primary-color, 10%); 4 | outline-offset: -10px; 5 | } 6 | } 7 | 8 | @mixin device($widths) { 9 | @media screen and #{$widths} { 10 | @content; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | /* ============================================================= 2 | * VARIABLES 3 | * ============================================================*/ 4 | 5 | $vhd__white: #fff !default; 6 | $vhd__black: #000 !default; 7 | $vhd__gray: #424b53 !default; 8 | $vhd__primary-text-color: #35343d !default; 9 | $vhd__lightest-gray: #f3f5f8 !default; 10 | $vhd__primary-color: #00ca9d !default; 11 | $vhd__medium-gray: #999999 !default; 12 | $vhd__light-gray: #d7d9e2 !default; 13 | $vhd__dark-gray: #2d3047 !default; 14 | $vhd__font-base: 16px !default; 15 | $vhd__font-small: 14px !default; 16 | $vhd__font-smaller: 12px !default; 17 | /* ============================================================= 18 | * RESPONSIVE LAYOUT HELPERS 19 | * ============================================================*/ 20 | 21 | $vhd__tablet: "(min-width: 480px) and (max-width: 767px)" !default; 22 | $vhd__phone: "(max-width: 479px)" !default; 23 | $vhd__desktop: "(min-width: 768px)" !default; 24 | $vhd__up-to-tablet: "(max-width: 767px)" !default; 25 | $extra-small-screen: "(max-width: 23em)" !default; 26 | -------------------------------------------------------------------------------- /src/assets/scss/index.scss: -------------------------------------------------------------------------------- 1 | /* ============================================================= 2 | * VARIABLES 3 | * ============================================================*/ 4 | $prefix: 'vhd__datepicker'; 5 | $white: #fff; 6 | $black: #000; 7 | $gray: #424b53; 8 | $primary-text-color: #35343d; 9 | $primary-text-inverse-color: #fff; 10 | $lightest-gray: #f3f5f8; 11 | $primary-color: #0fb8ad; 12 | $medium-gray: #424b53; 13 | $light-gray: #eaeaea; 14 | $disabledBg: #f5f7f8; 15 | $disabled-color: #d8d8d8; 16 | $dark-gray: #2d3047; 17 | $box-shadow: 0 0 10px 3px rgba(red, 0.4); 18 | $background-color: #fff; 19 | 20 | // Bg Date when hover it 21 | $bgValidHoverDate: $primary-color; 22 | $colorValidHoverDate: $white; 23 | 24 | // Bg Date when range or valid it 25 | $bgRollActiveDage: $primary-color; 26 | $colorRollActiveDage: $white; 27 | 28 | $font-size: 16px; 29 | $font-small: #{$font-size * 0.875}; 30 | $font-family: 'Source Sans Pro', sans-serif, verdana, arial; 31 | $font-regular: 400; 32 | $font-bold: 700; 33 | $tooltip-font-size: 11px; 34 | 35 | $width-half-day: 120px; 36 | $bullet-size: 4px; 37 | 38 | $tooltip-border-width: 4px; 39 | $tooltip-border-radius: 2px; 40 | $border-generic-width: 1px; 41 | 42 | /* ============================================================= 43 | * RESPONSIVE LAYOUT HELPERS 44 | * ============================================================*/ 45 | $tablet: '(min-width: 480px) and (max-width: 767px)'; 46 | $phone: '(max-width: 479px)'; 47 | $desktop: '(min-width: 768px)'; 48 | $up-to-tablet: '(max-width: 767px)'; 49 | $extra-small-screen: '(max-width: 23em)'; 50 | 51 | @mixin focusStyle() { 52 | &:focus { 53 | outline: none; 54 | } 55 | } 56 | 57 | @mixin device($device-widths) { 58 | @media screen and #{$device-widths} { 59 | @content; 60 | } 61 | } 62 | 63 | /* ============================================================= 64 | * BASE STYLES 65 | * ============================================================*/ 66 | 67 | .#{$prefix} { 68 | position: absolute; 69 | top: 3em; 70 | z-index: 999; 71 | transition: all 0.2s ease-in-out; 72 | background-color: $background-color; 73 | font-size: $font-size; 74 | font-family: $font-family; 75 | line-height: 0.875em; 76 | overflow: hidden; 77 | 78 | &--right { 79 | right: 0; 80 | } 81 | 82 | & .vhd__square { 83 | position: relative; 84 | width: calc(100% / 7); 85 | float: left; 86 | &:last-child { 87 | margin-bottom: 1.5em; 88 | } 89 | } 90 | 91 | button.next--mobile { 92 | border: $border-generic-width solid $light-gray; 93 | float: none; 94 | height: 3.125em; 95 | width: 100%; 96 | position: relative; 97 | appearance: none; 98 | overflow: hidden; 99 | position: fixed; 100 | bottom: 0; 101 | left: 0; 102 | outline: none; 103 | box-shadow: 0 5px 30px 10px rgba($black, 0.08); 104 | background: white; 105 | 106 | &:after { 107 | background: transparent url('../images/ic-arrow-right-green.regular.svg') no-repeat center / 8px; 108 | transform: rotate(90deg); 109 | content: ''; 110 | position: absolute; 111 | width: 200%; 112 | height: 200%; 113 | top: -50%; 114 | left: -50%; 115 | } 116 | } 117 | 118 | &--closed { 119 | box-shadow: none; 120 | max-height: 0; 121 | } 122 | 123 | &--open { 124 | box-shadow: 0 15px 30px 10px rgba($black, 0.08); 125 | max-height: 56.25em; 126 | 127 | @include device($up-to-tablet) { 128 | box-shadow: none; 129 | height: 100%; 130 | left: 0; 131 | right: 0; 132 | bottom: 0; 133 | -webkit-overflow-scrolling: touch !important; 134 | position: fixed; 135 | top: 0; 136 | width: 100%; 137 | } 138 | } 139 | 140 | &__header { 141 | text-align: left; 142 | position: absolute; 143 | top: 0; 144 | left: 0; 145 | right: 0; 146 | padding: 0.5em; 147 | } 148 | 149 | &__header-mobile { 150 | text-align: left; 151 | position: absolute; 152 | width: 100%; 153 | z-index: 1; 154 | } 155 | 156 | &__wrapper { 157 | position: relative; 158 | display: inline-block; 159 | width: 100%; 160 | height: 3em; 161 | background: $white url('../images/calendar_icon.regular.svg') no-repeat 1em center / 1em; 162 | 163 | & *, 164 | & *::before, 165 | & *::after { 166 | box-sizing: border-box; 167 | } 168 | 169 | &--grid .vhd__square .#{$prefix}__month-day { 170 | border: $border-generic-width solid $light-gray; 171 | margin: -1px 0 0 -1px; 172 | } 173 | 174 | &--booking { 175 | .#{$prefix}__month-day-wrapper { 176 | & .day { 177 | display: inline; 178 | text-align: right; 179 | padding-top: 0.75em; 180 | padding-right: 0.75em; 181 | right: 0; 182 | top: 0; 183 | transform: none; 184 | } 185 | } 186 | .#{$prefix}__month-day:before { 187 | display: none; 188 | } 189 | } 190 | } 191 | 192 | &__fullview { 193 | background: none; 194 | height: auto; 195 | 196 | .#{$prefix}__close-button, 197 | .#{$prefix}__dummy-wrapper, 198 | .#{$prefix}__clear-button, 199 | .vhd__hide-on-desktop { 200 | display: none; 201 | } 202 | .#{$prefix} { 203 | position: relative; 204 | top: 0; 205 | } 206 | 207 | .vhd__hide-up-to-tablet { 208 | display: block; 209 | } 210 | 211 | .#{$prefix}__month-button { 212 | display: inline-block; 213 | } 214 | 215 | .#{$prefix}__months { 216 | position: static; 217 | margin: 0; 218 | width: auto; 219 | 220 | &::before { 221 | display: none; 222 | } 223 | 224 | &.#{$prefix}__months--full { 225 | width: 100% !important; 226 | } 227 | } 228 | } 229 | 230 | &__dummy-wrapper { 231 | border: $border-generic-width solid $light-gray; 232 | cursor: pointer; 233 | display: flex; 234 | flex-wrap: wrap; 235 | justify-content: space-between; 236 | width: 100%; 237 | height: 100%; 238 | 239 | &--no-border.#{$prefix}__dummy-wrapper { 240 | border: 0; 241 | } 242 | 243 | &--is-active { 244 | border: $border-generic-width solid $primary-color; 245 | } 246 | } 247 | 248 | &__input { 249 | background: transparent; 250 | border: 0; 251 | color: $primary-text-color; 252 | font-size: $font-small; 253 | height: 3.43em; 254 | line-height: 3.43em; 255 | outline: none; 256 | padding: 0 1.875em 0.125em; 257 | text-align: center; 258 | width: 50%; 259 | word-spacing: 0.3125em; 260 | 261 | @include focusStyle(); 262 | 263 | &::-webkit-input-placeholder, 264 | &::-moz-placeholder, 265 | &:-ms-input-placeholder, 266 | &:-moz-placeholder { 267 | color: $primary-text-color; 268 | } 269 | 270 | @include device($phone) { 271 | text-indent: 0; 272 | text-align: center; 273 | } 274 | 275 | &:first-child { 276 | background: transparent url('../images/ic-arrow-right-datepicker.regular.svg') no-repeat right center / 8px; 277 | width: 50%; 278 | } 279 | 280 | &--is-active { 281 | color: $primary-text-color; 282 | } 283 | 284 | &--is-active::placeholder { 285 | color: $primary-text-color; 286 | } 287 | 288 | &--is-active::-moz-placeholder { 289 | color: $primary-text-color; 290 | } 291 | 292 | &--is-active:-ms-input-placeholder { 293 | color: $primary-text-color; 294 | } 295 | 296 | &--is-active:-moz-placeholder { 297 | color: $primary-text-color; 298 | } 299 | 300 | &--single-date:first-child { 301 | width: 100%; 302 | background: none; 303 | text-align: center; 304 | } 305 | } 306 | 307 | &__month-day-wrapper { 308 | height: 0; 309 | padding-top: calc(100% - 1px); //fix for safari 310 | span.day { 311 | z-index: 1; 312 | position: absolute; 313 | top: 50%; 314 | left: 50%; 315 | transform: translate(-50%, -50%); 316 | } 317 | 318 | .price { 319 | position: absolute; 320 | top: 0; 321 | width: 100%; 322 | text-align: center; 323 | font-weight: bold; 324 | font-size: 0.75em; 325 | } 326 | } 327 | 328 | &__month-day { 329 | visibility: visible; 330 | text-align: center; 331 | color: $primary-text-color; 332 | cursor: pointer; 333 | 334 | @include focusStyle(); 335 | 336 | &--today { 337 | border: 0; 338 | 339 | .#{$prefix}__month-day-wrapper { 340 | border: 2px solid $primary-color; 341 | padding-top: calc(100% - 5px); 342 | } 343 | } 344 | 345 | &--invalid-range { 346 | background-color: rgba($primary-color, 0.3); 347 | color: $lightest-gray; 348 | cursor: not-allowed; 349 | position: relative; 350 | } 351 | 352 | &--invalid { 353 | cursor: not-allowed; 354 | pointer-events: none; 355 | } 356 | 357 | &--valid:hover, 358 | &--allowed-checkout:hover { 359 | background-color: $primary-color; 360 | color: $colorValidHoverDate; 361 | } 362 | 363 | &--disabled { 364 | opacity: 1; 365 | background: $disabledBg; 366 | color: $disabled-color; 367 | cursor: not-allowed; 368 | pointer-events: none; 369 | font-weight: $font-regular; 370 | span { 371 | text-decoration: line-through; 372 | } 373 | } 374 | 375 | &--valid#{&}--not-allowed, 376 | &--not-allowed.vhd__currentDay, 377 | &--valid#{&}--not-allowed:hover { 378 | color: $primary-text-color; 379 | font-weight: $font-regular; 380 | cursor: default; 381 | background: transparent; 382 | span { 383 | text-decoration: none; 384 | } 385 | } 386 | 387 | &--hovering#{&}--not-allowed:hover { 388 | cursor: pointer; 389 | } 390 | 391 | &--halfCheckIn, 392 | &--halfCheckOut { 393 | position: relative; 394 | overflow: hidden; 395 | &:before { 396 | content: ''; 397 | position: absolute; 398 | top: 50%; 399 | left: 50%; 400 | -webkit-transform: translate(-50%, -50%); 401 | transform: translate(-50%, -50%); 402 | content: ''; 403 | z-index: -1; 404 | height: 0; 405 | width: 0; 406 | border-bottom: $width-half-day solid $disabledBg; 407 | border-left: $width-half-day solid transparent; 408 | } 409 | } 410 | 411 | &--halfCheckOut { 412 | &:before { 413 | border-top: $width-half-day solid $disabledBg; 414 | border-bottom: 0; 415 | border-left: 0; 416 | border-right: $width-half-day solid transparent; 417 | } 418 | } 419 | 420 | &--selected { 421 | background-color: rgba($primary-color, 0.7); 422 | color: $primary-text-inverse-color; 423 | span { 424 | text-decoration: none; 425 | } 426 | &:hover { 427 | font-weight: $font-bold; 428 | background-color: $bgRollActiveDage; 429 | color: $colorRollActiveDage; 430 | z-index: 1; 431 | } 432 | } 433 | 434 | &--hovering { 435 | background-color: rgba($primary-color, 0.7); 436 | color: $primary-text-inverse-color; 437 | font-weight: $font-bold; 438 | cursor: pointer; 439 | span { 440 | text-decoration: none; 441 | } 442 | } 443 | 444 | &--first-day-selected, 445 | &--last-day-selected { 446 | background: $primary-color; 447 | color: $primary-text-inverse-color; 448 | cursor: pointer; 449 | font-weight: $font-bold; 450 | pointer-events: auto; 451 | span { 452 | text-decoration: none; 453 | } 454 | } 455 | 456 | &--allowed-checkout { 457 | color: $medium-gray; 458 | } 459 | 460 | &--out-of-range { 461 | color: $lightest-gray; 462 | cursor: not-allowed; 463 | font-weight: $font-regular; 464 | position: relative; 465 | pointer-events: none; 466 | span { 467 | text-decoration: none; 468 | } 469 | } 470 | 471 | &--valid { 472 | cursor: pointer; 473 | font-weight: $font-bold; 474 | } 475 | 476 | &--valid#{&}--halfCheckIn#{&}--last-day-selected { 477 | color: white; 478 | } 479 | 480 | &--hidden { 481 | opacity: 0; 482 | pointer-events: none; 483 | } 484 | } 485 | 486 | &__month-button { 487 | background: transparent url('../images/ic-arrow-right-green.regular.svg') no-repeat center center / 8px; 488 | width: 2.5em; 489 | height: 2.5em; 490 | border: $border-generic-width solid #00ca9d; 491 | outline: none; 492 | text-align: center; 493 | cursor: pointer; 494 | opacity: 1; 495 | transition: opacity ease 0.5s; 496 | 497 | &:hover { 498 | opacity: 0.65; 499 | } 500 | 501 | @include focusStyle(); 502 | 503 | &--prev { 504 | transform: rotateY(180deg); 505 | } 506 | 507 | &--next { 508 | float: right; 509 | } 510 | 511 | &[disabled] { 512 | opacity: 0.2; 513 | cursor: not-allowed; 514 | pointer-events: none; 515 | } 516 | } 517 | &__inner { 518 | padding: 0 2.5rem; 519 | position: relative; 520 | height: calc(100% - 3em); 521 | 522 | @include device($up-to-tablet) { 523 | padding: 0; 524 | } 525 | } 526 | 527 | &__months-wrapper { 528 | height: 100%; 529 | .#{$prefix}__months { 530 | margin-top: 0; 531 | height: 100%; 532 | .#{$prefix}__month .#{$prefix}week-name { 533 | font-size: 1.25em; 534 | } 535 | } 536 | } 537 | 538 | .vhd__show-tooltip { 539 | .#{$prefix}__months { 540 | margin-top: 10em; 541 | height: calc(100% - 10em); 542 | } 543 | .#{$prefix}__tooltip--mobile { 544 | height: auto; 545 | opacity: 1; 546 | padding: 1em; 547 | visibility: visible; 548 | } 549 | } 550 | 551 | &__months { 552 | @include device($desktop) { 553 | display: flex; 554 | flex-wrap: wrap; 555 | width: 40.625em; 556 | justify-content: space-between; 557 | 558 | &.#{$prefix}__months--full { 559 | width: 20.3125em !important; 560 | } 561 | } 562 | 563 | @include device($up-to-tablet) { 564 | margin-top: 5.625em; 565 | height: calc(100% - 5.625em); 566 | position: absolute; 567 | left: 0; 568 | top: 0; 569 | overflow-y: scroll; 570 | right: 0; 571 | bottom: 0; 572 | transition: all ease 0.2s; 573 | } 574 | 575 | &::before { 576 | content: ''; 577 | background: $light-gray; 578 | bottom: 0; 579 | display: block; 580 | left: 50%; 581 | position: absolute; 582 | top: 0; 583 | width: 1px; 584 | 585 | @include device($up-to-tablet) { 586 | display: none; 587 | } 588 | } 589 | 590 | &--full { 591 | .#{$prefix}__month { 592 | width: 100% !important; 593 | padding: 0; 594 | } 595 | 596 | &::before { 597 | display: none; 598 | } 599 | } 600 | } 601 | 602 | &__month { 603 | font-size: 0.75em; 604 | width: 50%; 605 | padding-right: 0.83334em; 606 | 607 | @include device($up-to-tablet) { 608 | width: 100%; 609 | padding-right: 0; 610 | padding-top: 5em; 611 | height: 30em; 612 | 613 | &:last-of-type { 614 | margin-bottom: 5.416667em; 615 | } 616 | } 617 | 618 | @include device($desktop) { 619 | &:last-of-type { 620 | padding-right: 0; 621 | padding-left: 0.83334em; 622 | } 623 | } 624 | 625 | &--with-week-numbers { 626 | position: relative; 627 | 628 | .#{$prefix}__weeknumbers { 629 | position: absolute; 630 | top: 4.5rem; 631 | bottom: 0.875rem; 632 | display: flex; 633 | flex-direction: column; 634 | 635 | &__number { 636 | display: flex; 637 | align-items: center; 638 | width: 2rem; 639 | height: calc(100% / 6); 640 | margin: -0.5px 0 0; 641 | } 642 | } 643 | 644 | &:first-child { 645 | padding-left: 2rem; 646 | 647 | .#{$prefix}__weeknumbers { 648 | left: 0; 649 | } 650 | } 651 | 652 | &:last-child { 653 | padding-right: 2rem; 654 | 655 | .#{$prefix}__weeknumbers { 656 | right: 0; 657 | 658 | &__number { 659 | justify-content: flex-end; 660 | } 661 | } 662 | } 663 | } 664 | } 665 | 666 | &__month-caption { 667 | height: 2.5em; 668 | vertical-align: middle; 669 | } 670 | 671 | &__month-name { 672 | font-size: $font-size; 673 | font-weight: $font-bold; 674 | margin: 0; 675 | padding: 0 0 1.625em; 676 | pointer-events: none; 677 | text-align: center; 678 | line-height: 2em; 679 | height: 2.5em; 680 | padding-top: 0.5em; 681 | 682 | @include device($up-to-tablet) { 683 | margin-top: -3.125em; 684 | margin-bottom: 0; 685 | position: absolute; 686 | width: 100%; 687 | } 688 | } 689 | 690 | &__week-days { 691 | height: 2em; 692 | vertical-align: middle; 693 | } 694 | 695 | &__week-row { 696 | height: 2.5em; 697 | line-height: 2.5em; 698 | 699 | @include device($up-to-tablet) { 700 | box-shadow: 0px 8px 12px 0px rgba($black, 0.1); 701 | } 702 | } 703 | 704 | &__week-name { 705 | width: calc(100% / 7); 706 | float: left; 707 | font-size: 1em; 708 | font-weight: $font-regular; 709 | color: $medium-gray; 710 | text-align: center; 711 | } 712 | 713 | &__close-button { 714 | appearance: none; 715 | background: transparent; 716 | border: 0; 717 | color: $primary-text-color; 718 | cursor: pointer; 719 | font-size: 1.3125em; 720 | font-weight: $font-bold; 721 | margin-top: 0; 722 | outline: 0; 723 | z-index: 10000; 724 | position: fixed; 725 | right: 0.7143em; 726 | top: 0; 727 | height: 2.286em; 728 | line-height: 2.286em; 729 | 730 | i { 731 | display: block; 732 | font-style: inherit; 733 | transform: rotate(45deg); 734 | } 735 | } 736 | 737 | &__clear-button { 738 | appearance: none; 739 | background: transparent; 740 | border: 0; 741 | cursor: pointer; 742 | font-size: 1.5625em; 743 | font-weight: $font-bold; 744 | height: 100%; 745 | margin: 0; 746 | padding: 0; 747 | position: absolute; 748 | right: 0; 749 | top: 0; 750 | width: 1.6em; 751 | 752 | svg { 753 | fill: none; 754 | stroke-linecap: round; 755 | stroke-width: 0.32em; 756 | stroke: $medium-gray; 757 | width: 0.56em; 758 | position: absolute; 759 | top: 50%; 760 | left: 50%; 761 | transform: translate(-50%, -50%); 762 | } 763 | 764 | @include focusStyle(); 765 | } 766 | 767 | &__tooltip { 768 | background-color: $dark-gray; 769 | border-radius: $tooltip-border-radius; 770 | color: $primary-text-inverse-color; 771 | font-size: $tooltip-font-size; 772 | padding: 0, 45em 0, 91em; 773 | position: absolute; 774 | z-index: 50; 775 | left: 50%; 776 | bottom: 100%; 777 | white-space: nowrap; 778 | transform: translateX(-50%); 779 | text-align: center; 780 | 781 | &--mobile { 782 | height: 0; 783 | opacity: 0; 784 | visibility: hidden; 785 | padding: 0 1.1em; 786 | border: $border-generic-width solid #d7d9e2; 787 | font-size: $font-small; 788 | line-height: 1.4; 789 | transition: all ease 0.2s; 790 | } 791 | 792 | &:after { 793 | border-left: $tooltip-border-width solid transparent; 794 | border-right: $tooltip-border-width solid transparent; 795 | border-top: $tooltip-border-width solid $dark-gray; 796 | bottom: -0.364em; 797 | content: ''; 798 | left: 50%; 799 | margin-left: -0.364em; 800 | position: absolute; 801 | } 802 | } 803 | } 804 | 805 | .-vhd__is-hidden { 806 | display: none; 807 | } 808 | 809 | .vhd__hide-up-to-tablet { 810 | @include device($up-to-tablet) { 811 | display: none; 812 | } 813 | } 814 | 815 | .vhd__hide-on-desktop { 816 | @include device($desktop) { 817 | display: none; 818 | } 819 | } 820 | 821 | .vhd__parent-bullet { 822 | position: absolute; 823 | top: 50%; 824 | left: 50%; 825 | transform: translate(-50%, -50%); 826 | width: 100%; 827 | height: 100%; 828 | display: block; 829 | z-index: -1; 830 | 831 | .vhd__bullet { 832 | position: absolute; 833 | top: 60%; 834 | left: 50%; 835 | transform: translate(-50%, -50%); 836 | width: 100%; 837 | height: $bullet-size; 838 | transition: opacity ease 0.3s; 839 | @include device($desktop) { 840 | top: 50%; 841 | } 842 | 843 | &.vhd__checkInCheckOut, 844 | &.vhd__checkIn, 845 | &.vhd__checkOut { 846 | width: 0.5em; 847 | height: 1.125em; 848 | border-radius: 0.625em; 849 | 850 | &.vhd__bullet--small { 851 | height: 0.375em; 852 | width: 0.875em; 853 | } 854 | } 855 | 856 | &.vhd__checkInCheckOut { 857 | left: calc(50% - 1em); 858 | } 859 | } 860 | 861 | .vhd__pipe { 862 | display: block; 863 | width: 100%; 864 | height: $bullet-size; 865 | position: absolute; 866 | top: 60%; 867 | transform: translateY(-50%); 868 | transition: opacity ease 0.3s; 869 | @include device($desktop) { 870 | top: 50%; 871 | } 872 | 873 | &.pipe--small { 874 | height: calc(#{$bullet-size} * 0.75); 875 | } 876 | 877 | &.vhd__checkIn { 878 | left: calc(50% + #{$bullet-size}); 879 | width: calc(50% - #{$bullet-size}); 880 | } 881 | 882 | &.vhd__checkOut { 883 | left: 0px; 884 | width: calc(50% - #{$bullet-size}); 885 | } 886 | 887 | &.vhd__checkInCheckOut { 888 | width: calc(50% - 1.1875em); 889 | } 890 | } 891 | } 892 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable vars-on-top */ 2 | import fecha from 'fecha' 3 | 4 | const helpers = { 5 | getNextDate(datesArray, referenceDate) { 6 | const now = new Date(referenceDate) 7 | let closest = Infinity 8 | 9 | datesArray.forEach((d) => { 10 | const date = new Date(d) 11 | 12 | if (date >= now && date < closest) { 13 | closest = d 14 | } 15 | }) 16 | 17 | if (closest === Infinity) { 18 | return null 19 | } 20 | 21 | return closest 22 | }, 23 | nextDateByDayOfWeek(weekDay, referenceDate, i18n) { 24 | const newReferenceDate = new Date(referenceDate) 25 | let newWeekDay = weekDay.toLowerCase() 26 | const daysDefault = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'] 27 | const days = i18n ? i18n['day-names'] : daysDefault 28 | const referenceDateDay = newReferenceDate.getDay() 29 | 30 | for (let i = 7; ; i--) { 31 | if (newWeekDay === days[i]) { 32 | newWeekDay = i <= referenceDateDay ? i + 7 : i 33 | break 34 | } 35 | } 36 | 37 | const daysUntilNext = newWeekDay - referenceDateDay 38 | 39 | return newReferenceDate.setDate(newReferenceDate.getDate() + daysUntilNext) 40 | }, 41 | nextDateByDayOfWeekArray(daysArray, referenceDate, i18n) { 42 | const tempArray = [] 43 | 44 | for (let i = 0; i < daysArray.length; i++) { 45 | tempArray.push(new Date(this.nextDateByDayOfWeek(daysArray[i], referenceDate, i18n))) 46 | } 47 | 48 | return this.getNextDate(tempArray, referenceDate) 49 | }, 50 | nextDateByDayOfWeekObject(days, referenceDate, i18n) { 51 | const daysArray = Object.entries(days) 52 | .map((e) => (e[1] ? e[0] : false)) 53 | .filter((v) => v) 54 | 55 | return this.nextDateByDayOfWeekArray(daysArray, referenceDate, i18n) 56 | }, 57 | countDays(start, end) { 58 | const oneDay = 24 * 60 * 60 * 1000 59 | const firstDate = new Date(start) 60 | const secondDate = new Date(end) 61 | 62 | return Math.round(Math.abs((firstDate.getTime() - secondDate.getTime()) / oneDay)) 63 | }, 64 | addDays(date, quantity) { 65 | const result = new Date(date) 66 | 67 | result.setDate(result.getDate() + quantity) 68 | 69 | return result 70 | }, 71 | getDayDiff(d1, d2) { 72 | const t2 = new Date(d2).getTime() 73 | const t1 = new Date(d1).getTime() 74 | 75 | return parseInt((t2 - t1) / (24 * 3600 * 1000), 10) 76 | }, 77 | getFirstDay(date, firstDayOfWeek) { 78 | const firstDay = this.getFirstDayOfMonth(date) 79 | const day = firstDay.getDay() 80 | let offset = 0 81 | 82 | if (firstDayOfWeek > 0) { 83 | offset = !day ? -6 : firstDayOfWeek 84 | } 85 | 86 | return new Date(firstDay.setDate(firstDay.getDate() - (day - offset))) 87 | }, 88 | getFirstDayOfMonth(date) { 89 | return new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0) 90 | }, 91 | getNextMonth(date) { 92 | let nextMonth 93 | 94 | if (date.getMonth() === 11) { 95 | nextMonth = new Date(date.getFullYear() + 1, 0, 1) 96 | } else { 97 | nextMonth = new Date(date.getFullYear(), date.getMonth() + 1, 1) 98 | } 99 | 100 | return nextMonth 101 | }, 102 | getPreviousMonth(date) { 103 | let prevMonth 104 | 105 | if (date.getMonth() === 0) { 106 | prevMonth = new Date(date.getFullYear() - 1, 11, 1) 107 | } else { 108 | prevMonth = new Date(date.getFullYear(), date.getMonth() - 1, 1) 109 | } 110 | 111 | return prevMonth 112 | }, 113 | handleTouchStart(evt) { 114 | this.isTouchMove = false 115 | 116 | if (this.isOpen) { 117 | this.xDown = evt.touches[0].clientX 118 | this.yDown = evt.touches[0].clientY 119 | } 120 | }, 121 | handleTouchMove(evt) { 122 | if (!this.xDown || !this.yDown) { 123 | this.isTouchMove = false 124 | 125 | return 126 | } 127 | 128 | this.isTouchMove = true 129 | this.xUp = evt.touches[0].clientX 130 | this.yUp = evt.touches[0].clientY 131 | }, 132 | handleTouchEnd() { 133 | if (!this.isTouchMove) { 134 | return 135 | } 136 | 137 | if (!this.xDown || !this.yDown) { 138 | return 139 | } 140 | 141 | const xDiff = this.xDown - this.xUp 142 | const yDiff = this.yDown - this.yUp 143 | 144 | if (Math.abs(xDiff) < Math.abs(yDiff) && yDiff > 0 && !this.isPreventedMaxMonth) { 145 | this.renderNextMonth() 146 | } else { 147 | this.renderPreviousMonth() 148 | } 149 | 150 | this.xDown = null 151 | this.yDown = null 152 | }, 153 | validateDateBetweenTwoDates(fromDate, toDate, givenDate) { 154 | const getvalidDate = (d) => { 155 | const formatDateAt00 = new Date(d).setHours(0, 0, 0, 0) 156 | 157 | return new Date(formatDateAt00) 158 | } 159 | 160 | return getvalidDate(givenDate) <= getvalidDate(toDate) && getvalidDate(givenDate) >= getvalidDate(fromDate) 161 | }, 162 | validateDateBetweenDate(fromDate, givenDate) { 163 | const getvalidDate = (d) => { 164 | return new Date(d) 165 | } 166 | 167 | return getvalidDate(givenDate) <= getvalidDate(fromDate) 168 | }, 169 | getMonthDiff(d1, d2) { 170 | const newD1 = new Date(d1) 171 | const newD2 = new Date(d2) 172 | const d1Y = newD1.getFullYear() 173 | const d2Y = newD2.getFullYear() 174 | const d1M = newD1.getMonth() 175 | const d2M = newD2.getMonth() 176 | 177 | return d2M + 12 * d2Y - (d1M + 12 * d1Y) 178 | }, 179 | shortenString(arr, sLen) { 180 | const newArr = [] 181 | 182 | for (let i = 0, len = arr.length; i < len; i++) { 183 | newArr.push(arr[i].substr(0, sLen)) 184 | } 185 | 186 | return newArr 187 | }, 188 | getDaysArray(start, end) { 189 | for ( 190 | // eslint-disable-next-line no-var 191 | var arr = [], dt = new Date(start); 192 | dt <= end; 193 | dt.setDate(dt.getDate() + 1) 194 | ) { 195 | arr.push(new Date(dt)) 196 | } 197 | 198 | // eslint-disable-next-line block-scoped-var 199 | return arr 200 | }, 201 | dateFormater(date, format) { 202 | const f = format || 'YYYY-MM-DD' 203 | 204 | if (date) { 205 | return fecha.format(date, f) 206 | } 207 | 208 | return '' 209 | }, 210 | pluralize(countOfDays, periodType = 'night') { 211 | if (periodType === 'week') { 212 | return countOfDays > 7 ? this.i18n.weeks : this.i18n.week 213 | } 214 | 215 | return countOfDays !== 1 ? this.i18n.nights : this.i18n.night 216 | }, 217 | isDateLessOrEquals(time1, time2) { 218 | return new Date(time1) < new Date(time2) 219 | }, 220 | compareDay(day1, day2) { 221 | const date1 = fecha.format(new Date(day1), 'YYYYMMDD') 222 | const date2 = fecha.format(new Date(day2), 'YYYYMMDD') 223 | 224 | if (date1 > date2) { 225 | return 1 226 | } 227 | 228 | if (date1 === date2) { 229 | return 0 230 | } 231 | 232 | if (date1 < date2) { 233 | return -1 234 | } 235 | 236 | return null 237 | }, 238 | getIsoWeek(testDate) { 239 | const date = new Date(testDate) 240 | 241 | date.setHours(0, 0, 0, 0) 242 | 243 | // Thursday in current week decides the year. 244 | date.setDate(date.getDate() + 3 - ((date.getDay() + 6) % 7)) 245 | 246 | // January 4 is always in week 1. 247 | const week1 = new Date(date.getFullYear(), 0, 4) 248 | 249 | // Adjust to Thursday in week 1 and count number of weeks from date to week1. 250 | return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 - 3 + ((week1.getDay() + 6) % 7)) / 7) 251 | }, 252 | } 253 | 254 | export default helpers 255 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import HotelDatePicker from './DatePicker/HotelDatePicker.vue' 2 | import css from './assets/scss/index.scss' 3 | 4 | export default HotelDatePicker 5 | export { css } 6 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | Vue.config.productionTip = false 5 | 6 | new Vue({ 7 | render: (h) => h(App), 8 | }).$mount('#app') 9 | -------------------------------------------------------------------------------- /tests/unit/datepicker.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { mount } from '@vue/test-utils' 3 | 4 | import Datepicker from '@/DatePicker/HotelDatePicker.vue' 5 | 6 | describe('Datepicker Calendar', () => { 7 | const wrapper = mount(Datepicker) 8 | 9 | it('should correctly re-render the calendar', () => { 10 | expect(wrapper.vm.value).to.equal(true) 11 | wrapper.vm.reRender() 12 | expect(wrapper.vm.isOpen).to.equal(false) 13 | expect(wrapper.vm.value).to.equal(true) 14 | 15 | setTimeout(() => { 16 | expect(wrapper.vm.isOpen).to.equal(true) 17 | expect(wrapper.vm.value).to.equal(true) 18 | }, 200) 19 | }) 20 | }) 21 | 22 | describe('Datepicker Component', () => { 23 | let wrapper 24 | 25 | beforeEach(() => { 26 | wrapper = mount(Datepicker, { 27 | attachToDocument: true, 28 | propsData: { 29 | minNights: 3, 30 | disabledDates: ['2020-05-28', '2020-05-10', '2020-05-01', '2020-05-22'], 31 | }, 32 | }) 33 | }) 34 | 35 | it('should toggle the calendar visibility on input click', () => { 36 | expect(wrapper.vm.isOpen).to.equal(false) 37 | 38 | const datepickerInput = wrapper.find('[data-qa="vhd__datepickerInput"]') 39 | 40 | datepickerInput.trigger('click') 41 | 42 | expect(wrapper.vm.isOpen).to.equal(true) 43 | }) 44 | 45 | it('should correctly render the next and previous months', () => { 46 | const { activeMonthIndex } = wrapper.vm 47 | 48 | wrapper.vm.renderNextMonth() 49 | expect(wrapper.vm.activeMonthIndex).to.equal(activeMonthIndex + 1) 50 | 51 | wrapper.vm.renderPreviousMonth() 52 | expect(wrapper.vm.activeMonthIndex).to.equal(activeMonthIndex) 53 | }) 54 | 55 | // it('should correctly parse and sort the disabled dates', () => { 56 | // // wrapper.vm.parseDisabledDates() 57 | // wrapper.vm.createHalfDayDates(wrapper.vm.baseHalfDayDates) 58 | // expect(wrapper.vm.sortedDisabledDates).to.eql([ 59 | // new Date('2020-05-01'), 60 | // new Date('2020-05-10'), 61 | // new Date('2020-05-22'), 62 | // new Date('2020-05-28'), 63 | // ]) 64 | // }) 65 | }) 66 | -------------------------------------------------------------------------------- /tests/unit/datepickerDay.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import { expect } from 'chai' 3 | 4 | import Day from '@/DatePicker/components/Day.vue' 5 | 6 | describe('Datepicker Day', () => { 7 | let wrapper 8 | 9 | beforeEach(() => { 10 | wrapper = shallowMount(Day, { 11 | propsData: { 12 | activeMonthIndex: 0, 13 | belongsToThisMonth: true, 14 | checkIn: null, 15 | checkOut: null, 16 | date: new Date(), 17 | dayNumber: '1', 18 | hoveringDate: null, 19 | hoveringTooltip: true, 20 | isOpen: true, 21 | nextDisabledDate: null, 22 | options: { 23 | disabledDates: [], 24 | disabledDaysOfWeek: [], 25 | endDate: '2017-12-30T23:00:00.000Z', 26 | format: 'YYYY-MM-DD', 27 | hoveringTooltip: true, 28 | maxNights: null, 29 | minNights: 3, 30 | startDate: '2017-10-05T15:16:50.281Z', 31 | value: undefined, 32 | }, 33 | }, 34 | }) 35 | }) 36 | 37 | describe('isDateLessOrEquals', () => { 38 | it('should return a boolean when comparing two dates', () => { 39 | expect(wrapper.vm.isDateLessOrEquals(new Date('12-10-2017'), new Date('10-10-2017'))).to.equal(false) 40 | expect(wrapper.vm.isDateLessOrEquals(new Date('12-10-2017'), new Date('12-15-2017'))).to.equal(true) 41 | }) 42 | }) 43 | 44 | describe('compareDay', () => { 45 | it('should return return -1 if the first day is before the second day', () => { 46 | expect(wrapper.vm.compareDay('10-10-2017', '10-12-2017')).to.equal(-1) 47 | }) 48 | it('should return return 1 if the first day is after the second day', () => { 49 | expect(wrapper.vm.compareDay('10-12-2017', '10-10-2017')).to.equal(1) 50 | }) 51 | it('should return return 0 if the days are the same', () => { 52 | expect(wrapper.vm.compareDay('10-12-2017', '10-12-2017')).to.equal(0) 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /tests/unit/datepickerHelpers.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import DatepickerHelpers from '../../src/helpers' 3 | 4 | describe('Datepicker Helpers', () => { 5 | describe('nextDateByDayOfWeek', () => { 6 | it('should return the next given day of the week when comparing a date to a date', () => { 7 | expect(new Date(DatepickerHelpers.nextDateByDayOfWeek('Saturday', '10-11-2017'))).to.eql(new Date('10-14-2017')) 8 | }) 9 | }) 10 | 11 | describe('nextDateByDayOfWeekArray', () => { 12 | it('should return the next date when comparing to an array days of the week', () => { 13 | expect(DatepickerHelpers.nextDateByDayOfWeekArray(['Saturday', 'Tuesday'], '11-08-2017')).to.eql( 14 | new Date('11-11-2017'), 15 | ) 16 | }) 17 | }) 18 | 19 | describe('getNextDate', () => { 20 | it('should return the next day when comparing a date to a dates array', () => { 21 | expect(DatepickerHelpers.getNextDate(['10-10-2017', '10-15-2017', '10-20-2017'], '10-12-2017')).to.equal( 22 | '10-15-2017', 23 | ) 24 | }) 25 | }) 26 | 27 | describe('countDays', () => { 28 | it('should correctly count the number of days between two given dates', () => { 29 | expect(DatepickerHelpers.countDays('10-10-2017', '10-15-2017')).to.equal(5) 30 | }) 31 | }) 32 | 33 | describe('addDays', () => { 34 | it('should return the correct date when given a date and the amount of days to add', () => { 35 | expect(DatepickerHelpers.addDays('10-10-2017', 5)).to.eql(new Date('10-15-2017')) 36 | }) 37 | }) 38 | 39 | describe('getFirstDaySunday', () => { 40 | it('should return the first sunday of a given month', () => { 41 | expect(DatepickerHelpers.getFirstDay(new Date('10-10-2017'), 0)).to.eql(new Date('10-01-2017')) 42 | }) 43 | }) 44 | 45 | describe('getFirstDayMonday', () => { 46 | it('should return the first monday of a given month', () => { 47 | expect(DatepickerHelpers.getFirstDay(new Date('10-10-2017'), 1)).to.eql(new Date('09-25-2017')) 48 | }) 49 | }) 50 | 51 | describe('getFirstDayOfMonth', () => { 52 | it('should return the first sunday of a given month', () => { 53 | expect(DatepickerHelpers.getFirstDayOfMonth(new Date('12-10-2017'))).to.eql(new Date('12-01-2017')) 54 | }) 55 | }) 56 | 57 | describe('getNextMonth', () => { 58 | it('should return the next month of a given date', () => { 59 | expect(DatepickerHelpers.getNextMonth(new Date('12-10-2017'))).to.eql(new Date('01-01-2018')) 60 | }) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" 4 | cache: 5 | directories: 6 | - node_modules 7 | jobs: 8 | include: 9 | - stage: tdd 10 | script: 11 | - yarn build 12 | - yarn test:unit 13 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | css: { extract: true }, 3 | assetsDir: 'assets', 4 | } 5 | --------------------------------------------------------------------------------