├── .babelrc ├── .circleci └── config.yml ├── .eslintrc ├── .gitignore ├── .npmrc ├── .sasslintrc ├── LICENSE ├── README.md ├── components ├── vl-calendar-month.vue ├── vl-calendar.vue ├── vl-day-selector.vue └── vl-range-selector.vue ├── constants └── days.js ├── package.json ├── scss ├── _base.scss ├── _vl-calendar-month.scss ├── _vl-calendar.scss ├── variables.scss └── vuelendar.scss ├── test ├── e2e │ ├── mock-example │ │ ├── .npmrc │ │ ├── application.scss │ │ └── package.json │ └── test-install.js └── unit │ ├── components │ ├── vl-calendar-month.spec.js │ ├── vl-calendar.spec.js │ ├── vl-day-selector.spec.js │ └── vl-range-selector.spec.js │ └── utils │ ├── CollectionUtils.spec.js │ ├── DatesUtils.spec.js │ └── NumberUtils.spec.js └── utils ├── CollectionUtils.js ├── DatesUtils.js └── NumberUtils.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "modules": false }] 4 | ], 5 | "env": { 6 | "test": { 7 | "presets": [ 8 | ["env", { "targets": { "node": "current" }}] 9 | ] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | build: 3 | working_directory: ~/vuelendar 4 | docker: 5 | - image: circleci/node:lts 6 | steps: 7 | - checkout 8 | - run: 9 | name: update-npm 10 | command: 'sudo npm install -g npm@latest' 11 | - restore_cache: 12 | key: dependency-cache-{{ checksum "package.json" }} 13 | - run: 14 | name: install-npm 15 | command: npm install 16 | - run: 17 | name: install-peer-deps 18 | command: npm install vue@2.6.10 19 | - save_cache: 20 | key: dependency-cache-{{ checksum "package.json" }} 21 | paths: 22 | - ./node_modules 23 | - run: 24 | name: lint-js 25 | command: npm run lint-js 26 | - run: 27 | name: lint-scss 28 | command: npm run lint-scss 29 | - run: 30 | name: test-unit 31 | command: npm run test:unit 32 | - run: 33 | name: test-e2e 34 | command: npm run test:unit 35 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "parser": "babel-eslint" 4 | }, 5 | "rules": { 6 | "indent": [2, 2], 7 | "quotes": [2, "single"], 8 | "linebreak-style": [2, "unix"], 9 | "semi": [2, "never"], 10 | "camelcase": 0, 11 | "max-statements-per-line": [2, { "max": 1 }], 12 | "object-shorthand": ["error", "always"], 13 | "object-curly-spacing": ["error", "always"], 14 | "vue/require-default-prop": false, 15 | "arrow-body-style": ["error", "as-needed"], 16 | "mocha/no-exclusive-tests": "error", 17 | "arrow-parens": ["error", "as-needed"], 18 | "keyword-spacing": "error", 19 | "space-before-function-paren": ["error", "always"], 20 | "vue/order-in-components": ["error", { 21 | "order": [ 22 | "extends", 23 | "name", 24 | "model", 25 | "mixins", 26 | "components", 27 | "props", 28 | "data", 29 | "computed", 30 | "methods", 31 | "watch", 32 | "LIFECYCLE_HOOKS" 33 | ] 34 | }] 35 | }, 36 | "plugins": ["mocha"], 37 | "extends": ["eslint:recommended", "plugin:vue/strongly-recommended"], 38 | "env": { 39 | "node": true, 40 | "browser": true, 41 | "mocha": true 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.sasslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "include": "scss/**/*.scss" 4 | }, 5 | "rules": { 6 | "extends-before-mixins": 2, 7 | "extends-before-declarations": 2, 8 | "placeholder-in-extend": 2, 9 | "mixins-before-declarations": 2, 10 | "one-declaration-per-line": 2, 11 | "single-line-per-selector": 2, 12 | "no-attribute-selectors": 2, 13 | "no-color-hex": 0, 14 | "no-color-keywords": 2, 15 | "no-color-literals": [2, { 16 | "allow-rgba": true 17 | }], 18 | "no-combinators": 2, 19 | "no-css-comments": 2, 20 | "no-debug": 2, 21 | "no-disallowed-properties": 2, 22 | "no-duplicate-properties": 2, 23 | "no-empty-rulesets": 2, 24 | "no-extends": 2, 25 | "no-ids": 2, 26 | "no-important": 2, 27 | "no-invalid-hex": 2, 28 | "no-mergeable-selectors": 2, 29 | "no-misspelled-properties": 2, 30 | "no-qualifying-elements": 2, 31 | "no-trailing-whitespace": 2, 32 | "no-trailing-zero": 2, 33 | "no-transition-all": 2, 34 | "no-universal-selectors": 2, 35 | "no-url-domains": 2, 36 | "no-url-protocols": 2, 37 | "no-vendor-prefixes": 0, 38 | "no-warn": 2, 39 | "property-units": 2, 40 | "declarations-before-nesting": 2, 41 | "force-attribute-nesting": 2, 42 | "force-element-nesting": 2, 43 | "force-pseudo-nesting": 2, 44 | "class-name-format": [2, { 45 | "convention": "hyphenatedbem" 46 | }], 47 | "function-name-format": 2, 48 | "id-name-format": 2, 49 | "mixin-name-format": 2, 50 | "placeholder-name-format": 2, 51 | "variable-name-format": 2, 52 | "attribute-quotes": 2, 53 | "bem-depth": 2, 54 | "border-zero": 2, 55 | "brace-style": 2, 56 | "clean-import-paths": 2, 57 | "empty-args": 2, 58 | "hex-length": 2, 59 | "hex-notation": [2, { 60 | "style": "uppercase" 61 | }], 62 | "indentation": 2, 63 | "leading-zero": 2, 64 | "max-line-length": 2, 65 | "max-file-line-count": 2, 66 | "nesting-depth": 2, 67 | "property-sort-order": [2, { 68 | "order": "concentric" 69 | }], 70 | "pseudo-element": 2, 71 | "quotes": 2, 72 | "shorthand-values": 2, 73 | "url-quotes": 2, 74 | "variable-for-property": 2, 75 | "zero-unit": 2, 76 | "space-after-comma": 2, 77 | "space-before-colon": 2, 78 | "space-after-colon": 2, 79 | "space-before-brace": 2, 80 | "space-before-bang": 2, 81 | "space-after-bang": 2, 82 | "space-between-parens": 2, 83 | "space-around-operator": 2, 84 | "trailing-semicolon": 2, 85 | "final-newline": 1 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 CODEST 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 | # Vuelendar 2 | Simple and clean calendar written in Vue.js. Check out full Vuelendar's documentation [here](https://codesthq.github.io/vuelendar-lp/). 3 | 4 | [](https://circleci.com/gh/codesthq/vuelendar/tree/master) 5 | 6 | ## Features 7 | ### Select single date 8 |  9 | 10 | ### Select range of dates 11 |  12 | 13 | 14 | ## Installation 15 | npm install vuelendar@1.0.0 16 | 17 | ## Usage 18 | Import styles in your .vue file: 19 | 20 | 21 | 22 | Register components: 23 | 24 | import VRangeSelector from 'vuelendar/components/vl-range-selector'; 25 | import VDaySelector from 'vuelendar/components/vl-day-selector'; 26 | 27 | export default { 28 | components: { 29 | VRangeSelector, 30 | VDaySelector 31 | }, 32 | data () { 33 | return { 34 | range: {}, 35 | date: null 36 | } 37 | } 38 | // ... 39 | } 40 | 41 | Use in template: 42 | 43 | 47 | 48 | 51 | 52 | ## Disabling dates 53 | 54 | Vuelendar allows two ways for disabling dates. 55 | 56 | Using an array: 57 | 58 | 59 | 74 | Will disable all dates from 21st April 2019 and 25th April 2019 75 | 76 | Specifying only 'from' attribute will disable all dates past that date. 77 | 78 | 79 | 85 | Will disable all dates from 21st April 2019 86 | 87 | Specifying only 'to' attribute will disable all dates before that date. 88 | 89 | 90 | 96 | Will disable all dates before 21st April 2019 97 | -------------------------------------------------------------------------------- /components/vl-calendar-month.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ monthName }} {{ year }} 5 | 6 | 7 | 8 | 12 | 17 | {{ number }} 18 | 19 | 20 | 21 | 22 | 23 | {{ name }} 28 | 29 | 30 | 31 | 41 | {{ day }} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 143 | -------------------------------------------------------------------------------- /components/vl-calendar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 12 | 13 | $emit('input', date)" 23 | /> 24 | 25 | $emit('input', date)" 36 | /> 37 | 38 | 39 | 40 | 104 | -------------------------------------------------------------------------------- /components/vl-day-selector.vue: -------------------------------------------------------------------------------- 1 | 2 | emitDate(date)" 4 | :is-selected="date => date === selectedDate" 5 | :is-disabled="isDisabled" 6 | :custom-classes="customClasses" 7 | :show-weeks-number="showWeeksNumber" 8 | :default-date="defaultDate" 9 | :single-month="singleMonth" 10 | :first-day-of-week="firstDayOfWeek" 11 | ref="calendar" 12 | /> 13 | 14 | 15 | 72 | -------------------------------------------------------------------------------- /components/vl-range-selector.vue: -------------------------------------------------------------------------------- 1 | 2 | emitDate(date)" 4 | :is-selected="isSelected" 5 | :is-disabled="calculateDisabled" 6 | :custom-classes="customClasses" 7 | :show-weeks-number="showWeeksNumber" 8 | :default-date="defaultDate" 9 | :single-month="singleMonth" 10 | :first-day-of-week="firstDayOfWeek" 11 | ref="calendar" 12 | /> 13 | 14 | 15 | 81 | -------------------------------------------------------------------------------- /constants/days.js: -------------------------------------------------------------------------------- 1 | export const DAYS_SHORTCUTS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] 2 | export const DAYS_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] 3 | 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuelendar", 3 | "version": "1.0.16", 4 | "description": "Simple and clean calendar written in Vue.js", 5 | "scripts": { 6 | "test:unit": "jest", 7 | "test:e2e": "mocha ./test/e2e/test-install.js --timeout 15000", 8 | "lint-js": "eslint . --ext js,vue", 9 | "lint-scss": "sass-lint -v -f compact" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/codesthq/vuelendar.git" 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/codesthq/vuelendar/issues" 20 | }, 21 | "files": [ 22 | "components/**/*.*", 23 | "scss/**/*.*", 24 | "utils/**/*.*", 25 | "constants/**/*.*" 26 | ], 27 | "homepage": "https://github.com/codesthq/vuelendar#readme", 28 | "devDependencies": { 29 | "@vue/test-utils": "^1.0.0-beta.28", 30 | "babel-core": "^6.26.0", 31 | "babel-eslint": "^10.0.1", 32 | "babel-jest": "^23.6.0", 33 | "babel-loader": "^7.1.2", 34 | "babel-preset-env": "^1.6.0", 35 | "chai": "^4.2.0", 36 | "eslint": "^5.12.1", 37 | "eslint-plugin-mocha": "^5.2.1", 38 | "eslint-plugin-vue": "^5.1.0", 39 | "execa": "^1.0.0", 40 | "jest": "^23.6.0", 41 | "mocha": "^5.2.0", 42 | "rimraf": "^2.6.3", 43 | "sass-lint": "^1.12.1", 44 | "sinon": "^7.2.3", 45 | "vue-jest": "^3.0.2", 46 | "vue-template-compiler": "^2.5.22" 47 | }, 48 | "jest": { 49 | "moduleFileExtensions": [ 50 | "js", 51 | "vue" 52 | ], 53 | "moduleNameMapper": { 54 | "^@/(.*)$": "/lib/$1" 55 | }, 56 | "transform": { 57 | "^.+\\.js$": "/node_modules/babel-jest", 58 | ".*\\.(vue)$": "/node_modules/vue-jest" 59 | } 60 | }, 61 | "peerDependencies": { 62 | "vue": "^2.6.10 || ^3.0.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /scss/_base.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .vl-flex { 4 | display: flex; 5 | } 6 | 7 | .vl-flex-wrap { 8 | flex-wrap: wrap; 9 | } 10 | 11 | .vl-flex-column { 12 | flex-direction: column; 13 | } 14 | -------------------------------------------------------------------------------- /scss/_vl-calendar-month.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .vl-calendar-month { 4 | padding: $vl-calendar-month-padding; 5 | 6 | &__title { 7 | margin-bottom: 20px; 8 | text-align: center; 9 | font-weight: 600; 10 | } 11 | 12 | &__week-numbers-column { 13 | padding-top: 24px; 14 | } 15 | 16 | &__week-number { 17 | display: inline-flex; 18 | align-items: center; 19 | justify-content: center; 20 | margin: 5px 0; 21 | width: 100%; 22 | height: 24px; 23 | color: $vl-gray-0; 24 | font-size: 10px; 25 | } 26 | 27 | &__week-day { 28 | display: inline-block; 29 | margin-bottom: 10px; 30 | width: 14%; 31 | text-align: center; 32 | color: $vl-weekday-text-color; 33 | font-size: 12px; 34 | } 35 | 36 | &__day { 37 | display: inline-flex; 38 | align-items: center; 39 | justify-content: center; 40 | margin: 5px 0; 41 | cursor: pointer; 42 | width: 14%; 43 | height: 24px; 44 | 45 | @for $i from 1 through 6 { 46 | &--offset-#{$i} { 47 | margin-left: calc(#{$i} * 14%); 48 | } 49 | } 50 | 51 | &.disabled { 52 | color: $vl-gray-1; 53 | pointer-events: none; 54 | 55 | &--first { 56 | border-top-left-radius: 14px; 57 | border-bottom-left-radius: 14px; 58 | } 59 | 60 | &--last { 61 | border-top-right-radius: 14px; 62 | border-bottom-right-radius: 14px; 63 | } 64 | } 65 | 66 | &.selected { 67 | background: $vl-selected-day-bg-color; 68 | color: $vl-selected-day-text-color; 69 | font-weight: 800; 70 | 71 | &.disabled { 72 | border: $vl-selected-disabled-border; 73 | background: $vl-selected-disabled-day-bg-color; 74 | color: $vl-selected-disabled-day-text-color; 75 | } 76 | 77 | &--first { 78 | border-top-left-radius: 14px; 79 | border-bottom-left-radius: 14px; 80 | } 81 | 82 | &--last { 83 | border-top-right-radius: 14px; 84 | border-bottom-right-radius: 14px; 85 | } 86 | } 87 | 88 | &:hover { 89 | &:not(.selected) { 90 | border-radius: 14px; 91 | background: $vl-selected-day-bg-color; 92 | color: $vl-selected-day-text-color; 93 | font-weight: 800; 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /scss/_vl-calendar.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .vl-calendar { 4 | display: inline-block; 5 | position: relative; 6 | background: $vl-calendar-bg-color; 7 | padding: $vl-calendar-padding; 8 | text-align: center; 9 | color: $vl-calendar-text-color; 10 | font-size: 14px; 11 | user-select: none; 12 | 13 | &__month { 14 | display: inline-block; 15 | margin-bottom: 30px; 16 | width: $vl-calendar-month-width; 17 | max-width: 80vw; 18 | } 19 | 20 | &__arrow { 21 | position: absolute; 22 | top: 15px; 23 | background-repeat: no-repeat; 24 | background-position: center center; 25 | background-size: 75%; 26 | cursor: pointer; 27 | width: 24px; 28 | height: 24px; 29 | 30 | &--back { 31 | left: 15px; 32 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAMAAAAM7l6QAAAA0lBMVEUZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjSTOY05AAAARnRSTlMAAQIDBAUGBwgJCgsMDQ8QERITFBUXGBkaGx0eHyAiIyQnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8Pj9AQUJDREZHSElKS0xNUWPYUAAAAU1JREFUeNptk+lygjAURq8gFm2h1q3uWsVqtZUWXAGtIN/7v1KHSGxYzp9kcnKXJBMSUfRGXZMpl+rcRUS4f6tk5RYINovpdLkPEa7KCVmYhLBbUlyi78DvCrZowmuIuwc+DOJINmyFEjx5mPL5HJZEKVQPHWI04BQpgxZcHlihI6qUwwiLaGjh697iTP/X0ukahZv3YHmLcSJ8SCQFJ243WIstlmET6VjGySxuOa5P1MboZn/Slkyo1EePWRM4eRyL6SU0pm+Fcbmz5TpKztv+oGxyna8qu7R3g+hgHsV+n/QqNkT0jWfuDxgKesgu6RWfFFNa1YRHd0P1NuiUwyC+ryYOctY++r5K8fnMQtqWHPT4W+xgpuLLR8yJo1g4VkXb+YUhJCy8I1zzrqX2Dtdu9huc15Px2LB89g3S1FdnMJyZRrlUaq3mS4kE/gAULzTK6Ml9VwAAAABJRU5ErkJggg=='); // sass-lint:disable-line max-line-length 33 | } 34 | 35 | &--forward { 36 | right: 15px; 37 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAMAAAAM7l6QAAAAz1BMVEUZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjQZJjTQKaQcAAAARXRSTlMAAQIDBAUGBwgJCgsMDg8QERITFBUWGBkaGx0eHyAhIiMkJSYnKCkqLC0uLzAxMjQ1Nzg5Ojs/QEFCQ0RFRkdISUpLTE22Gs8EAAABVUlEQVR4AW3T4XaaMBwF8IvKRp2dU5F266qwMWXT1bWjqbMUanLf/5nWcIzByO9L/udcJRcIaHo3DMeDDlqF64KafIzfwzUVZPWQLZZ3gpSZj6beb6rNxEMtiAu+TGEFgmIIq5sqNYPhC256ODEqeWvme/6C63KvRqh9Ze7hTMRdt65V7vto8ZPf9DLnAtbtzfEZvZYdAEIFsJ44t3+PgIA5GgYvx8pjroArfoeTH67fkQJIeAU3/3LYSAILTvQ43O6MiuoztJzdt3isx09FZezJ2TFOeI0TFztTXkhbrZEmptoTcMEHJzU/H3Gl+6nm4RD8Ycas3jZmCitNzORXZadeXgO0WB52mfEe50L13IPm/eUSrg+VCs2r+8e1c7o/Fozt3Wz5OIDlzSUTWP4fytWlOdU3W1bX55/B812aJFkuqdYBHF60qagpkfbRxutPounQR8N/jIU0mXzMHWoAAAAASUVORK5CYII='); // sass-lint:disable-line max-line-length 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /scss/variables.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Base colors variables 3 | // 4 | $vl-primary: #C83F3A !default; 5 | $vl-white: #FFF !default; 6 | $vl-black: #3E3A4E !default; 7 | $vl-gray-0: #A0A0A0 !default; 8 | $vl-gray-1: #B5B5B5 !default; 9 | $vl-gray-2: #F6F6F6 !default; 10 | 11 | // 12 | // Specific colors for stuff 13 | // 14 | $vl-calendar-text-color: $vl-black !default; 15 | $vl-calendar-bg-color: $vl-gray-2 !default; 16 | 17 | $vl-weekday-text-color: $vl-primary !default; 18 | 19 | $vl-selected-day-bg-color: $vl-primary !default; 20 | $vl-selected-day-text-color: $vl-white !default; 21 | $vl-selected-disabled-day-bg-color: darken($vl-primary, 15%) !default; 22 | $vl-selected-disabled-day-text-color: $vl-black !default; 23 | $vl-selected-disabled-border: 1px solid $vl-black !default; 24 | 25 | // 26 | // Sizes 27 | // 28 | $vl-calendar-month-width: 300px !default; 29 | 30 | // 31 | // Paddings 32 | // 33 | $vl-calendar-month-padding: 0 20px !default; 34 | $vl-calendar-padding: 20px 0 0 !default; 35 | -------------------------------------------------------------------------------- /scss/vuelendar.scss: -------------------------------------------------------------------------------- 1 | @import 'base'; 2 | @import 'vl-calendar'; 3 | @import 'vl-calendar-month'; 4 | -------------------------------------------------------------------------------- /test/e2e/mock-example/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /test/e2e/mock-example/application.scss: -------------------------------------------------------------------------------- 1 | @import './node_modules/vuelendar/scss/vuelendar.scss'; 2 | 3 | .body { 4 | background: red; 5 | } 6 | -------------------------------------------------------------------------------- /test/e2e/mock-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mock-example", 3 | "version": "1.0.0", 4 | "homepage": "https://github.com/codesthq/vuelendar#readme", 5 | "dependencies": { 6 | "vuelendar": "git@github.com:codesthq/vuelendar.git" 7 | }, 8 | "devDependencies": { 9 | "node-sass": "^4.11.0" 10 | }, 11 | "scripts": { 12 | "build": "node-sass application.scss dist/application.css" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/e2e/test-install.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const path = require('path') 3 | const rm = require('rimraf').sync 4 | const execa = require('execa') 5 | const fs = require('fs') 6 | 7 | describe('Test install', () => { 8 | let originalCwd 9 | 10 | before (async () => { 11 | originalCwd = process.cwd() 12 | process.chdir(path.join(__dirname, 'mock-example')) 13 | await execa('npm', ['install', '--only=production']) 14 | }) 15 | 16 | after (() => { 17 | rm('node_modules') 18 | rm('dist') 19 | process.chdir(originalCwd) 20 | }) 21 | 22 | 23 | it('install properly', async () => { 24 | await execa('npx', ['node-sass', 'application.scss', 'dist/application.css']) 25 | const css = fs.readFileSync('./dist/application.css').toString() 26 | expect(css).to.include('.vl-calendar') 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /test/unit/components/vl-calendar-month.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import sinon from 'sinon' 3 | import { shallowMount } from '@vue/test-utils' 4 | import VlCalendarMonth from '../../../components/vl-calendar-month' 5 | import { createRange } from '../../../utils/CollectionUtils' 6 | import * as DatesUtils from '../../../utils/DatesUtils' 7 | 8 | describe('vl-calendar-month', () => { 9 | let wrapper 10 | 11 | function mountComponent (propsData = {}) { 12 | sinon.restore() 13 | sinon.stub(DatesUtils, 'getWeekNumbers').returns([5, 6, 7, 8, 9]) 14 | wrapper = shallowMount(VlCalendarMonth, { propsData }) 15 | return wrapper 16 | } 17 | 18 | // beforeEach(() => mountComponent({ month: new Date().getMonth(), year: new Date().getFullYear() })); 19 | afterEach(() => sinon.restore()) 20 | 21 | it('title contains month name and year', () => { 22 | expect(mountComponent({ month: 0, year: 2019 }).find('.vl-calendar-month__title').text()).to.equal('January 2019') 23 | expect(mountComponent({ month: 1, year: 2019 }).find('.vl-calendar-month__title').text()).to.equal('February 2019') 24 | expect(mountComponent({ month: 2, year: 2019 }).find('.vl-calendar-month__title').text()).to.equal('March 2019') 25 | expect(mountComponent({ month: 3, year: 2019 }).find('.vl-calendar-month__title').text()).to.equal('April 2019') 26 | expect(mountComponent({ month: 4, year: 2019 }).find('.vl-calendar-month__title').text()).to.equal('May 2019') 27 | expect(mountComponent({ month: 5, year: 2019 }).find('.vl-calendar-month__title').text()).to.equal('June 2019') 28 | expect(mountComponent({ month: 6, year: 2019 }).find('.vl-calendar-month__title').text()).to.equal('July 2019') 29 | expect(mountComponent({ month: 7, year: 2019 }).find('.vl-calendar-month__title').text()).to.equal('August 2019') 30 | expect(mountComponent({ month: 8, year: 2019 }).find('.vl-calendar-month__title').text()).to.equal('September 2019') 31 | expect(mountComponent({ month: 9, year: 2019 }).find('.vl-calendar-month__title').text()).to.equal('October 2019') 32 | expect(mountComponent({ month: 10, year: 2019 }).find('.vl-calendar-month__title').text()).to.equal('November 2019') 33 | expect(mountComponent({ month: 11, year: 2019 }).find('.vl-calendar-month__title').text()).to.equal('December 2019') 34 | }) 35 | 36 | it('week day names are displayed', () => { 37 | expect(wrapper.findAll('.vl-calendar-month__week-day').wrappers.map(w => w.text())) 38 | .to.deep.equal(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']) 39 | }) 40 | 41 | it('every day in month is displayed', () => { 42 | expect(mountComponent({ month: 1, year: 2019 }).findAll('.vl-calendar-month__day').wrappers.map(w => +w.text())) 43 | .to.deep.equal(createRange(1, 28)) 44 | 45 | expect(mountComponent({ month: 1, year: 2020 }).findAll('.vl-calendar-month__day').wrappers.map(w => +w.text())) 46 | .to.deep.equal(createRange(1, 29)) 47 | 48 | expect(mountComponent({ month: 3, year: 2019 }).findAll('.vl-calendar-month__day').wrappers.map(w => +w.text())) 49 | .to.deep.equal(createRange(1, 30)) 50 | 51 | expect(mountComponent({ month: 6, year: 2019 }).findAll('.vl-calendar-month__day').wrappers.map(w => +w.text())) 52 | .to.deep.equal(createRange(1, 31)) 53 | }) 54 | 55 | it('first day of month is displayed below corresponding week day', () => { 56 | mountComponent({ month: 8, year: 2019 }) // First day is Sunday 57 | expect(wrapper.find('.vl-calendar-month__day').classes()).to.include('vl-calendar-month__day--offset-6') 58 | 59 | mountComponent({ month: 8, year: 2019, firstDayOfWeek: 'sun' }) // First day is Sunday 60 | expect(wrapper.find('.vl-calendar-month__day').classes()).to.deep.equal(['vl-calendar-month__day']) 61 | 62 | mountComponent({ month: 8, year: 2019, firstDayOfWeek: 'sat' }) // First day is Sunday 63 | expect(wrapper.find('.vl-calendar-month__day').classes()).to.include('vl-calendar-month__day--offset-1') 64 | 65 | mountComponent({ month: 1, year: 2019 }) // First day is Friday 66 | expect(wrapper.find('.vl-calendar-month__day').classes()).to.include('vl-calendar-month__day--offset-4') 67 | 68 | mountComponent({ month: 1, year: 2019, firstDayOfWeek: 'sun' }) // First day is Friday 69 | expect(wrapper.find('.vl-calendar-month__day').classes()).to.include('vl-calendar-month__day--offset-5') 70 | 71 | mountComponent({ month: 0, year: 2019 }) // First day is Tuesday 72 | expect(wrapper.find('.vl-calendar-month__day').classes()).to.include('vl-calendar-month__day--offset-1') 73 | 74 | mountComponent({ month: 0, year: 2019, firstDayOfWeek: 'sun' }) // First day is Tuesday 75 | expect(wrapper.find('.vl-calendar-month__day').classes()).to.include('vl-calendar-month__day--offset-2') 76 | 77 | mountComponent({ month: 3, year: 2019 }) // First day is Monday 78 | expect(wrapper.find('.vl-calendar-month__day').classes()).to.deep.equal(['vl-calendar-month__day']) 79 | 80 | mountComponent({ month: 3, year: 2019, firstDayOfWeek: 'sun' }) // First day is Monday 81 | expect(wrapper.find('.vl-calendar-month__day').classes()).to.include('vl-calendar-month__day--offset-1') 82 | }) 83 | 84 | it('date string is emitted after click on day', () => { 85 | mountComponent({ month: 1, year: 2019 }) 86 | wrapper.findAll('.vl-calendar-month__day').at(7).trigger('click') 87 | 88 | expect(wrapper.emitted('input')).to.deep.equal([['2019-02-08']]) 89 | }) 90 | 91 | it('day may be selected base on passed isSelected callback', () => { 92 | mountComponent({ 93 | month: 1, 94 | year: 2019, 95 | isSelected: date => date < '2019-02-20' && date > '2019-01-31' 96 | }) 97 | 98 | const days = wrapper.findAll('.vl-calendar-month__day').wrappers 99 | days.slice(0, 19).forEach(w => { 100 | expect(w.classes()).to.include('selected') 101 | }) 102 | 103 | expect(days[0].classes()).to.include('selected--first') 104 | expect(days[18].classes()).to.include('selected--last') 105 | 106 | wrapper.findAll('.vl-calendar-month__day').wrappers.slice(20).forEach(w => { 107 | expect(w.classes()).to.not.include('selected') 108 | }) 109 | }) 110 | 111 | it('day may be disabled base on passed isDisabled callback', () => { 112 | mountComponent({ 113 | month: 1, 114 | year: 2019, 115 | isDisabled: date => date < '2019-02-21' && date > '2019-01-31' 116 | }) 117 | 118 | const days = wrapper.findAll('.vl-calendar-month__day').wrappers 119 | 120 | days.slice(0, 20).forEach(w => { 121 | expect(w.classes()).to.include('disabled') 122 | }) 123 | 124 | expect(days[0].classes()).to.include('disabled--first') 125 | expect(days[19].classes()).to.include('disabled--last') 126 | 127 | days.slice(21).forEach(w => { 128 | expect(w.classes()).to.not.include('disabled') 129 | }) 130 | }) 131 | 132 | it('weeks numbers are displayed on demand', () => { 133 | mountComponent({ showWeeksNumber: true }) 134 | 135 | expect(wrapper.findAll('.vl-calendar-month__week-numbers-column .vl-calendar-month__week-number').wrappers.map(w => w.text())) 136 | .to.deep.equal(['5', '6', '7', '8', '9']) 137 | }) 138 | 139 | it('it is possible to setup custom class', () => { 140 | mountComponent({ 141 | month: 1, 142 | year: 2019, 143 | customClasses: { 'is-processing': date => date < '2019-02-21' && date > '2019-01-31' } 144 | }) 145 | 146 | const days = wrapper.findAll('.vl-calendar-month__day').wrappers 147 | 148 | days.slice(0, 20).forEach(w => { 149 | expect(w.classes()).to.include('is-processing') 150 | }) 151 | 152 | days.slice(21).forEach(w => { 153 | expect(w.classes()).to.not.include('is-processing') 154 | }) 155 | }) 156 | }) 157 | -------------------------------------------------------------------------------- /test/unit/components/vl-calendar.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import sinon from 'sinon' 3 | import { shallowMount } from '@vue/test-utils' 4 | import VlCalendar from '../../../components/vl-calendar' 5 | import VlCalendarMonth from '../../../components/vl-calendar-month' 6 | import * as DatesUtils from '../../../utils/DatesUtils' 7 | 8 | describe('vl-calendar', () => { 9 | let wrapper 10 | 11 | function mountComponent (propsData = {}) { 12 | wrapper = shallowMount(VlCalendar, { propsData }) 13 | } 14 | 15 | beforeEach(() => mountComponent()) 16 | afterEach(() => sinon.restore()) 17 | 18 | it('December 2019 and January 2020 are visible in 2019-12-31', () => { 19 | sinon.stub(DatesUtils, 'getToday').returns(new Date('2019-12-31')) 20 | mountComponent() 21 | 22 | expect(wrapper.findAll(VlCalendarMonth).wrappers.map(w => w.props())).to.deep.equal([{ 23 | month: 11, 24 | year: 2019, 25 | isSelected: undefined, 26 | isDisabled: undefined, 27 | customClasses: undefined, 28 | showWeeksNumber: false, 29 | firstDayOfWeek: 'mon' 30 | }, { 31 | month: 0, 32 | year: 2020, 33 | isSelected: undefined, 34 | isDisabled: undefined, 35 | customClasses: undefined, 36 | showWeeksNumber: false, 37 | firstDayOfWeek: 'mon' 38 | }]) 39 | }) 40 | 41 | it('when default date is passed, it is displayed on first calendar', () => { 42 | sinon.stub(DatesUtils, 'getToday').returns(new Date('2019-12-31')) 43 | mountComponent({ defaultDate: '2018-04-03' }) 44 | 45 | const props = wrapper.findAll(VlCalendarMonth).wrappers.map(w => w.props()) 46 | expect(props[0]).to.deep.include({ month: 3, year: 2018 }) 47 | expect(props[1]).to.deep.include({ month: 4, year: 2018 }) 48 | }) 49 | 50 | it('January and February 2019 are visible in 2019-01-31', () => { 51 | sinon.stub(DatesUtils, 'getToday').returns(new Date('2019-01-31')) 52 | mountComponent() 53 | 54 | expect(wrapper.findAll(VlCalendarMonth).wrappers.map(w => w.props())).to.deep.equal([{ 55 | month: 0, 56 | year: 2019, 57 | isSelected: undefined, 58 | isDisabled: undefined, 59 | customClasses: undefined, 60 | showWeeksNumber: false, 61 | firstDayOfWeek: 'mon' 62 | }, { 63 | month: 1, 64 | year: 2019, 65 | isSelected: undefined, 66 | isDisabled: undefined, 67 | customClasses: undefined, 68 | showWeeksNumber: false, 69 | firstDayOfWeek: 'mon' 70 | }]) 71 | }) 72 | 73 | it('it is possible to move visible months forward', () => { 74 | sinon.stub(DatesUtils, 'getToday').returns(new Date('2018-12-20')) 75 | mountComponent() 76 | 77 | wrapper.find('.vl-calendar__arrow--forward').trigger('click') 78 | 79 | expect(wrapper.findAll(VlCalendarMonth).wrappers.map(w => w.props())).to.deep.equal([{ 80 | month: 0, 81 | year: 2019, 82 | isSelected: undefined, 83 | isDisabled: undefined, 84 | customClasses: undefined, 85 | showWeeksNumber: false, 86 | firstDayOfWeek: 'mon' 87 | }, { 88 | month: 1, 89 | year: 2019, 90 | isSelected: undefined, 91 | isDisabled: undefined, 92 | customClasses: undefined, 93 | showWeeksNumber: false, 94 | firstDayOfWeek: 'mon' 95 | }]) 96 | }) 97 | 98 | it('it is possible to move visible months backward', () => { 99 | sinon.stub(DatesUtils, 'getToday').returns(new Date('2018-12-20')) 100 | mountComponent() 101 | 102 | wrapper.find('.vl-calendar__arrow--back').trigger('click') 103 | 104 | expect(wrapper.findAll(VlCalendarMonth).wrappers.map(w => w.props())).to.deep.equal([{ 105 | month: 10, 106 | year: 2018, 107 | isSelected: undefined, 108 | isDisabled: undefined, 109 | customClasses: undefined, 110 | showWeeksNumber: false, 111 | firstDayOfWeek: 'mon' 112 | }, { 113 | month: 11, 114 | year: 2018, 115 | isSelected: undefined, 116 | isDisabled: undefined, 117 | customClasses: undefined, 118 | showWeeksNumber: false, 119 | firstDayOfWeek: 'mon' 120 | }]) 121 | }) 122 | 123 | it('functions to calculate classes are propagated down', () => { 124 | sinon.stub(DatesUtils, 'getToday').returns(new Date('2019-01-31')) 125 | 126 | const isSelected = () => {} 127 | const isDisabled = () => {} 128 | const customClasses = { 'is-processing': () => true } 129 | mountComponent({ isSelected, isDisabled, customClasses }) 130 | 131 | expect(wrapper.findAll(VlCalendarMonth).wrappers.map(w => w.props())).to.deep.equal([{ 132 | month: 0, 133 | year: 2019, 134 | isDisabled, 135 | isSelected, 136 | customClasses, 137 | showWeeksNumber: false, 138 | firstDayOfWeek: 'mon' 139 | }, { 140 | month: 1, 141 | year: 2019, 142 | isDisabled, 143 | isSelected, 144 | customClasses, 145 | showWeeksNumber: false, 146 | firstDayOfWeek: 'mon' 147 | }]) 148 | }) 149 | 150 | it('propagate input event from children', () => { 151 | mountComponent() 152 | 153 | wrapper.findAll(VlCalendarMonth).wrappers.forEach(w => { 154 | w.vm.$emit('input', '2019-02-13') 155 | }) 156 | 157 | expect(wrapper.emitted('input')).to.deep.equal([['2019-02-13'], ['2019-02-13']]) 158 | }) 159 | 160 | it('showWeeksNumber property is propagated down', () => { 161 | mountComponent() 162 | expect(wrapper.find(VlCalendarMonth).props('showWeeksNumber')).to.be.false 163 | 164 | mountComponent({ showWeeksNumber: true }) 165 | expect(wrapper.find(VlCalendarMonth).props('showWeeksNumber')).to.be.true 166 | }) 167 | 168 | it('firstDayOfWeek property is propagated down', () => { 169 | mountComponent({ firstDayOfWeek: 'mon' }) 170 | expect(wrapper.find(VlCalendarMonth).props('firstDayOfWeek')).to.equal('mon') 171 | }) 172 | 173 | it('single month is rendered on demand', () => { 174 | mountComponent({ singleMonth: true }) 175 | expect(wrapper.findAll(VlCalendarMonth)).to.have.lengthOf(1) 176 | }) 177 | }) 178 | -------------------------------------------------------------------------------- /test/unit/components/vl-day-selector.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { shallowMount } from '@vue/test-utils' 3 | import VLSingleDateSelector from '../../../components/vl-day-selector' 4 | 5 | describe('vl-day-selector', () => { 6 | let wrapper 7 | 8 | function mountComponent (config = {}) { 9 | wrapper = shallowMount(VLSingleDateSelector, { 10 | propsData: { 11 | selectedDate: config.selectedDate, 12 | disabledDates: config.disabledDates, 13 | customClasses: config.customClasses, 14 | showWeeksNumber: config.showWeeksNumber, 15 | defaultDate: config.defaultDate, 16 | isDisabled: config.isDisabled, 17 | singleMonth: config.singleMonth, 18 | firstDayOfWeek: config.firstDayOfWeek 19 | } 20 | }) 21 | return wrapper 22 | } 23 | 24 | it('contains calendar', () => { 25 | mountComponent() 26 | expect(wrapper.find({ ref: 'calendar' }).exists()).to.be.true 27 | }) 28 | 29 | it('emits appropriate events', () => { 30 | mountComponent() 31 | 32 | wrapper.find({ ref: 'calendar' }).vm.$emit('input', '2018-02-05') 33 | wrapper.find({ ref: 'calendar' }).vm.$emit('input', '2018-03-05') 34 | wrapper.find({ ref: 'calendar' }).vm.$emit('input', '2018-01-05') 35 | wrapper.find({ ref: 'calendar' }).vm.$emit('input', '2018-05-05') 36 | 37 | expect(wrapper.emitted('input')).to.deep.equal([['2018-02-05'], ['2018-03-05'], ['2018-01-05'], ['2018-05-05']]) 38 | expect(wrapper.emitted().focus).to.have.lengthOf(4) 39 | }) 40 | 41 | it('selected date is marked', () => { 42 | mountComponent({ selectedDate: '2018-02-15' }) 43 | 44 | const isSelected = wrapper.find({ ref: 'calendar' }).props().isSelected 45 | expect(isSelected('2018-02-15')).to.be.true 46 | expect(isSelected('2018-02-14')).to.be.false 47 | expect(isSelected('2018-02-16')).to.be.false 48 | }) 49 | 50 | it('disable dates when giving a full object of disabled dates', () => { 51 | mountComponent({ isDisabled: date => date >= '2018-02-15' && date <= '2018-02-17' }) 52 | 53 | const isDisabled = wrapper.find({ ref: 'calendar' }).props().isDisabled 54 | expect(isDisabled('2018-02-13')).to.be.false 55 | expect(isDisabled('2018-02-14')).to.be.false 56 | expect(isDisabled('2018-02-15')).to.be.true 57 | expect(isDisabled('2018-02-16')).to.be.true 58 | expect(isDisabled('2018-02-17')).to.be.true 59 | expect(isDisabled('2018-02-18')).to.be.false 60 | expect(isDisabled('2018-02-19')).to.be.false 61 | }) 62 | 63 | it('"customClasses" property is propagated down', () => { 64 | const customClasses = { 'is-processing': () => true } 65 | mountComponent({ customClasses }) 66 | 67 | expect(wrapper.find({ ref: 'calendar' }).props().customClasses).to.equal(customClasses) 68 | }) 69 | 70 | it('appropriate properties are propagated down', () => { 71 | mountComponent() 72 | expect(wrapper.find({ ref: 'calendar' }).props().showWeeksNumber).to.be.undefined 73 | expect(wrapper.find({ ref: 'calendar' }).props().defaultDate).to.be.undefined 74 | expect(wrapper.find({ ref: 'calendar' }).props().isDisabled).to.be.undefined 75 | expect(wrapper.find({ ref: 'calendar' }).props().singleMonth).to.be.undefined 76 | expect(wrapper.find({ ref: 'calendar' }).props().firstDayOfWeek).to.equal('mon') 77 | 78 | const isDisabled = () => true 79 | mountComponent({ showWeeksNumber: true, defaultDate: '2019-01-03', singleMonth: true, isDisabled, firstDayOfWeek: 'tue' }) 80 | expect(wrapper.find({ ref: 'calendar' }).props().showWeeksNumber).to.be.true 81 | expect(wrapper.find({ ref: 'calendar' }).props().defaultDate).to.equal('2019-01-03') 82 | expect(wrapper.find({ ref: 'calendar' }).props().isDisabled).to.equal(isDisabled) 83 | expect(wrapper.find({ ref: 'calendar' }).props().singleMonth).to.equal(true) 84 | expect(wrapper.find({ ref: 'calendar' }).props().firstDayOfWeek).to.equal('tue') 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /test/unit/components/vl-range-selector.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { shallowMount } from '@vue/test-utils' 3 | import VLRangeSelector from '../../../components/vl-range-selector' 4 | 5 | describe('vl-range-selector', () => { 6 | let wrapper 7 | 8 | function mountComponent (propsData = {}) { 9 | wrapper = shallowMount(VLRangeSelector, { propsData }) 10 | } 11 | 12 | it('contains calendar', () => { 13 | mountComponent() 14 | expect(wrapper.find({ ref: 'calendar' }).props()).to.deep.include({}) 15 | }) 16 | 17 | it('emits appropriate events', () => { 18 | mountComponent() 19 | wrapper.find({ ref: 'calendar' }).vm.$emit('input', '2018-02-05') 20 | expect(wrapper.emitted().focus).to.have.lengthOf(1) 21 | expect(wrapper.emitted('update:startDate')).to.deep.equal([['2018-02-05']]) 22 | expect(wrapper.emitted('update:endDate')).to.deep.equal(undefined) 23 | 24 | mountComponent({ startDate: '2018-02-01', endDate: '2018-02-06' }) 25 | wrapper.find({ ref: 'calendar' }).vm.$emit('input', '2018-02-05') 26 | expect(wrapper.emitted().focus).to.have.lengthOf(1) 27 | expect(wrapper.emitted('update:startDate')).to.deep.equal([['2018-02-05']]) 28 | expect(wrapper.emitted('update:endDate')).to.deep.equal([[null]]) 29 | 30 | mountComponent({ startDate: '2018-02-01' }) 31 | wrapper.find({ ref: 'calendar' }).vm.$emit('input', '2018-02-05') 32 | expect(wrapper.emitted().focus).to.have.lengthOf(1) 33 | expect(wrapper.emitted('update:startDate')).to.deep.equal(undefined) 34 | expect(wrapper.emitted('update:endDate')).to.deep.equal([['2018-02-05']]) 35 | }) 36 | 37 | it('it is possible to block start date', () => { 38 | mountComponent({ startDate: '2019-01-28', endDate: '2019-02-04', blockStartDate: true }) 39 | wrapper.find({ ref: 'calendar' }).vm.$emit('input', '2019-02-08') 40 | wrapper.find({ ref: 'calendar' }).vm.$emit('input', '2019-02-09') 41 | 42 | expect(wrapper.emitted('update:startDate')).to.deep.equal(undefined) 43 | expect(wrapper.emitted('update:endDate')).to.deep.equal([['2019-02-08'], ['2019-02-09']]) 44 | }) 45 | 46 | it('it is possible to block whole calendar', () => { 47 | mountComponent({ disabled: true }) 48 | 49 | const isDisabled = wrapper.find({ ref: 'calendar' }).props().isDisabled 50 | expect(isDisabled('2019-01-15')).to.be.true 51 | expect(isDisabled('2019-01-27')).to.be.true 52 | expect(isDisabled('2019-01-28')).to.be.true 53 | }) 54 | 55 | it('it is possible to limit available dates on calendar', () => { 56 | mountComponent({ isDisabled: date => date >= '2019-01-15' }) 57 | let isDisabled = wrapper.find({ ref: 'calendar' }).props().isDisabled 58 | expect(isDisabled('2019-01-14')).to.be.false 59 | expect(isDisabled('2019-01-15')).to.be.true 60 | expect(isDisabled('2019-01-16')).to.be.true 61 | 62 | mountComponent({ isDisabled: date => date <= '2019-01-15' }) 63 | isDisabled = wrapper.find({ ref: 'calendar' }).props().isDisabled 64 | expect(isDisabled('2019-01-14')).to.be.true 65 | expect(isDisabled('2019-01-15')).to.be.true 66 | expect(isDisabled('2019-01-16')).to.be.false 67 | }) 68 | 69 | it('when "enableSingleDate" flag is passed, endDate can be same as startDate', () => { 70 | mountComponent({ startDate: '2019-02-08' }) 71 | expect(wrapper.find({ ref: 'calendar' }).props().isDisabled('2019-02-08')).to.be.true 72 | 73 | mountComponent({ startDate: '2019-02-08', enableSingleDate: true }) 74 | expect(wrapper.find({ ref: 'calendar' }).props().isDisabled('2019-02-08')).to.be.false 75 | }) 76 | 77 | it('when only start date is selected, it is marked', () => { 78 | mountComponent({ startDate: '2018-02-15' }) 79 | 80 | const isSelected = wrapper.find({ ref: 'calendar' }).props().isSelected 81 | 82 | expect(isSelected('2018-02-15')).to.be.true 83 | expect(isSelected('2018-02-14')).to.be.false 84 | expect(isSelected('2018-02-16')).to.be.false 85 | }) 86 | 87 | it('dates in range are marked', () => { 88 | mountComponent({ startDate: '2019-01-28', endDate: '2019-02-04' }) 89 | 90 | const isSelected = wrapper.find({ ref: 'calendar' }).props().isSelected 91 | 92 | expect(isSelected('2019-01-15')).to.be.false 93 | expect(isSelected('2019-01-28')).to.be.true 94 | expect(isSelected('2019-01-29')).to.be.true 95 | expect(isSelected('2019-02-03')).to.be.true 96 | expect(isSelected('2019-02-04')).to.be.true 97 | expect(isSelected('2019-02-05')).to.be.false 98 | expect(isSelected('2018-01-30')).to.be.false 99 | }) 100 | 101 | it('when only start date is selected, click on previous date cause change on start date', () => { 102 | mountComponent({ startDate: '2019-01-28' }) 103 | 104 | const isDisabled = wrapper.find({ ref: 'calendar' }).props().isDisabled 105 | expect(isDisabled('2019-01-15')).to.be.false 106 | expect(isDisabled('2019-01-27')).to.be.false 107 | expect(isDisabled('2019-01-28')).to.be.true 108 | expect(isDisabled('2019-01-29')).to.be.false 109 | expect(isDisabled('2020-01-26')).to.be.false 110 | 111 | wrapper.find({ ref: 'calendar' }).vm.$emit('input', '2019-01-15') 112 | 113 | expect(wrapper.emitted()).to.deep.equal({ 'update:startDate': [['2019-01-15']], 'focus': [[]] }) 114 | }) 115 | 116 | it('"customClasses" property is propagated down', () => { 117 | const customClasses = { 'is-processing': () => true } 118 | mountComponent({ customClasses }) 119 | 120 | expect(wrapper.find({ ref: 'calendar' }).props().customClasses).to.equal(customClasses) 121 | }) 122 | 123 | it('appropriate properties are propagated down', () => { 124 | mountComponent() 125 | expect(wrapper.find({ ref: 'calendar' }).props().showWeeksNumber).to.be.false 126 | expect(wrapper.find({ ref: 'calendar' }).props().defaultDate).to.be.undefined 127 | expect(wrapper.find({ ref: 'calendar' }).props().singleMonth).to.be.false 128 | expect(wrapper.find({ ref: 'calendar' }).props().firstDayOfWeek).to.equal('mon') 129 | 130 | mountComponent({ showWeeksNumber: true, defaultDate: '2019-01-03', singleMonth: true, firstDayOfWeek: 'tue' }) 131 | expect(wrapper.find({ ref: 'calendar' }).props().showWeeksNumber).to.be.true 132 | expect(wrapper.find({ ref: 'calendar' }).props().defaultDate).to.equal('2019-01-03') 133 | expect(wrapper.find({ ref: 'calendar' }).props().singleMonth).to.be.true 134 | expect(wrapper.find({ ref: 'calendar' }).props().firstDayOfWeek).to.equal('tue') 135 | }) 136 | }) 137 | -------------------------------------------------------------------------------- /test/unit/utils/CollectionUtils.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import * as CollectionUtils from '../../../utils/CollectionUtils' 3 | 4 | describe('CollectionUtils.createRange', () => { 5 | it('return array of numbers including start and end', () => { 6 | expect(CollectionUtils.createRange(1, 3)).to.deep.equal([1, 2, 3]) 7 | }) 8 | 9 | it('return single item when start is same as end', () => { 10 | expect(CollectionUtils.createRange(1, 1)).to.deep.equal([1]) 11 | }) 12 | }) 13 | 14 | describe('CollectionUtils.transpose', () => { 15 | it('returns transposed array', () => { 16 | expect(CollectionUtils.transpose([1, 2, 3, 4, 5, 6, 7], 2)).to.deep.equal([3, 4, 5, 6, 7, 1, 2]) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /test/unit/utils/DatesUtils.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import * as DatesUtils from '../../../utils/DatesUtils' 3 | 4 | describe('DatesUtils.countDays', () => { 5 | it('return 28 for February 2019', () => { 6 | expect(DatesUtils.countDays(1, 2019)).to.equal(28) 7 | }) 8 | 9 | it('return 29 for February 2020', () => { 10 | expect(DatesUtils.countDays(1, 2020)).to.equal(29) 11 | }) 12 | 13 | it('return 30 for June 2019', () => { 14 | expect(DatesUtils.countDays(5, 2019)).to.equal(30) 15 | }) 16 | }) 17 | 18 | describe('DatesUtils.parseDate', () => { 19 | it('return Date object from string', () => { 20 | const date = DatesUtils.parseDate('2019-01-03') 21 | 22 | expect(date instanceof Date).to.be.true 23 | expect(date.getFullYear()).to.equal(2019) 24 | expect(date.getMonth()).to.equal(0) 25 | expect(date.getDate()).to.equal(3) 26 | }) 27 | }) 28 | 29 | describe('DatesUtils.formatDate', () => { 30 | it('return appropriate formatted date', () => { 31 | expect(DatesUtils.formatDate(2, 0, 2018)).to.equal('2018-01-02') 32 | }) 33 | 34 | it('return last day of previous month formatted date when day is 0', () => { 35 | expect(DatesUtils.formatDate(0, 1, 2018)).to.equal('2018-01-31') 36 | }) 37 | 38 | it('return first day of next month formatted date when day is one bigger than number of days in \ 39 | month', () => { 40 | expect(DatesUtils.formatDate(32, 0, 2018)).to.equal('2018-02-01') 41 | }) 42 | }) 43 | 44 | describe('DatesUtils.getMonthName', () => { 45 | it('return appropriate month name', () => { 46 | expect(DatesUtils.getMonthName(0)).to.equal('January') 47 | expect(DatesUtils.getMonthName(1)).to.equal('February') 48 | expect(DatesUtils.getMonthName(2)).to.equal('March') 49 | expect(DatesUtils.getMonthName(3)).to.equal('April') 50 | expect(DatesUtils.getMonthName(4)).to.equal('May') 51 | expect(DatesUtils.getMonthName(5)).to.equal('June') 52 | expect(DatesUtils.getMonthName(6)).to.equal('July') 53 | expect(DatesUtils.getMonthName(7)).to.equal('August') 54 | expect(DatesUtils.getMonthName(8)).to.equal('September') 55 | expect(DatesUtils.getMonthName(9)).to.equal('October') 56 | expect(DatesUtils.getMonthName(10)).to.equal('November') 57 | expect(DatesUtils.getMonthName(11)).to.equal('December') 58 | }) 59 | }) 60 | 61 | describe('DatesUtils.getWeekNumbers', () => { 62 | it('return appropriate weeks number', () => { 63 | expect(DatesUtils.getWeekNumbers(4, 2019)).to.deep.equal([18, 19, 20, 21, 22]) 64 | expect(DatesUtils.getWeekNumbers(11, 2019)).to.deep.equal([48, 49, 50, 51, 52, 53]) 65 | expect(DatesUtils.getWeekNumbers(0, 2020)).to.deep.equal([1, 2, 3, 4, 5]) 66 | }) 67 | }) 68 | 69 | describe('DatesUtils.getWeekNumber', () => { 70 | it('return week number', () => { 71 | expect(DatesUtils.getWeekNumber(new Date(2019, 0, 1))).to.equal(1) 72 | expect(DatesUtils.getWeekNumber(new Date(2019, 0, 16))).to.equal(3) 73 | expect(DatesUtils.getWeekNumber(new Date(2019, 4, 6))).to.equal(19) 74 | expect(DatesUtils.getWeekNumber(new Date(2019, 11, 31))).to.equal(1) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /test/unit/utils/NumberUtils.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import * as NumberUtils from '../../../utils/NumberUtils' 3 | 4 | describe('NumberUtils.twoDigits', () => { 5 | it('return "10" for 10', () => { 6 | expect(NumberUtils.twoDigits(10)).to.equal('10') 7 | }) 8 | 9 | it('return "09" for 9', () => { 10 | expect(NumberUtils.twoDigits(9)).to.equal('09') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /utils/CollectionUtils.js: -------------------------------------------------------------------------------- 1 | export function createRange (start, end) { 2 | return Array.from({ length: end - start + 1 }, (x, i) => i + start) 3 | } 4 | 5 | export function transpose (array, offset) { 6 | const table = [...array] 7 | for (let i = 0; i < offset; i++) { 8 | table.push(table.shift()) 9 | } 10 | return table 11 | } 12 | -------------------------------------------------------------------------------- /utils/DatesUtils.js: -------------------------------------------------------------------------------- 1 | import { twoDigits } from './NumberUtils' 2 | 3 | export function getToday () { 4 | return new Date() 5 | } 6 | 7 | export function countDays (month, year) { 8 | return new Date(year, month + 1, 0).getDate() 9 | } 10 | 11 | export function parseDate (string) { 12 | return new Date(string) 13 | } 14 | 15 | export function formatDate (day, month, year) { 16 | const date = new Date(year, month, day) 17 | return `${date.getFullYear()}-${twoDigits(date.getMonth() + 1)}-${twoDigits(date.getDate())}` 18 | } 19 | 20 | export function getWeekNumbers (month, year) { 21 | let weekNumbers = [] 22 | for (let i = 1; i <= countDays(month, year); i++) { 23 | let weekNumber = getWeekNumber(new Date(year, month, i)) 24 | if (month === 11 && weekNumber === 1) { 25 | weekNumber = weekNumbers[weekNumbers.length - 1] + 1 26 | weekNumbers.push(weekNumber) 27 | break 28 | } 29 | weekNumbers.push(weekNumber) 30 | } 31 | return Array.from(new Set(weekNumbers)) 32 | } 33 | 34 | export function getWeekNumber (date) { 35 | date = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())) 36 | // Set to nearest Thursday: current date + 4 - current day number 37 | // Make Sunday's day number 7 38 | date.setUTCDate(date.getUTCDate() + 4 - (date.getUTCDay() || 7)) 39 | 40 | // Calculate full weeks to nearest Thursday 41 | let yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1)) 42 | const oneDayInMs = 86400000 43 | return Math.ceil(((date - yearStart) / oneDayInMs + 1) / 7) 44 | } 45 | 46 | export function getMonthName (month) { 47 | return [ 48 | 'January', 49 | 'February', 50 | 'March', 51 | 'April', 52 | 'May', 53 | 'June', 54 | 'July', 55 | 'August', 56 | 'September', 57 | 'October', 58 | 'November', 59 | 'December' 60 | ][month] 61 | } 62 | -------------------------------------------------------------------------------- /utils/NumberUtils.js: -------------------------------------------------------------------------------- 1 | export function twoDigits (number) { 2 | return number > 9 ? '' + number : '0' + number 3 | } 4 | --------------------------------------------------------------------------------