├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .stylelintrc ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build ├── build.js ├── utils │ ├── index.js │ ├── log.js │ ├── style.js │ └── write.js ├── webpack.config.base.js ├── webpack.config.demo.js ├── webpack.config.dev.js └── webpack.config.dll.js ├── demo ├── demo.gif ├── index.html └── src │ ├── app.css │ └── app.js ├── package.json ├── src ├── Datetime.vue ├── DatetimeCalendar.vue ├── DatetimeMonthPicker.vue ├── DatetimePopup.vue ├── DatetimeTimePicker.vue ├── DatetimeYearPicker.vue ├── FlowManager.js ├── index.js └── util.js ├── test ├── .eslintrc ├── helpers │ ├── Test.vue │ ├── index.js │ ├── utils.js │ └── wait-for-update.js ├── index.js ├── karma.conf.js ├── setup.js ├── specs │ ├── Datetime.spec.js │ ├── DatetimeCalendar.spec.js │ ├── DatetimeMonthsPicker.spec.js │ ├── DatetimePopup.spec.js │ ├── DatetimeTimePicker.spec.js │ └── DatetimeYearPicker.spec.js └── visual.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "browsers": [ 8 | "last 2 versions" 9 | ] 10 | } 11 | } 12 | ] 13 | ], 14 | "plugins": [ 15 | "transform-object-rest-spread" 16 | ], 17 | "env": { 18 | "test": { 19 | "plugins": [ 20 | "istanbul" 21 | ] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/*.js 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | extends: 'vue', 8 | // add your custom rules here 9 | 'rules': { 10 | // allow async-await 11 | 'generator-star-spacing': 0, 12 | // allow debugger during development 13 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 14 | }, 15 | globals: { 16 | requestAnimationFrame: true, 17 | performance: true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Versions 2 | - vue: 3 | - vue-datetime: 4 | - luxon: 5 | 6 | ### Description: 7 | 8 | ### Steps To Reproduce: 9 | 10 | Including a reproduction would be great. You can fork this fiddle: https://jsfiddle.net/ofc30uv3/ 11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | test/coverage 5 | dist 6 | yarn-error.log 7 | reports 8 | demo/dist 9 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "processors": ["stylelint-processor-html"], 3 | "extends": "stylelint-config-standard", 4 | "rules": { 5 | "no-empty-source": null, 6 | "number-leading-zero": null 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: node_js 3 | node_js: 4 | - '8' 5 | dist: trusty 6 | cache: yarn 7 | before_script: 8 | - "export DISPLAY=:99.0" 9 | - "sh -e /etc/init.d/xvfb start" 10 | - sleep 3 # give xvfb some time to start 11 | - yarn build:dll 12 | after_success: 13 | - bash <(curl -s https://codecov.io/bash) 14 | addons: 15 | chrome: stable 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). 5 | 6 | #### [v1.0.0-beta.13](https://github.com/mariomka/vue-datetime/compare/v1.0.0-beta.12...v1.0.0-beta.13) 7 | > 1 May 2020 8 | - Revert `rollup-plugin-vue` update. It doesn't output the same files. We should work on it deeper. [`91818e5`](https://github.com/mariomka/vue-datetime/commit/91818e5650cd9377976e1f56c93b6ae810cf6601) 9 | 10 | #### [v1.0.0-beta.12](https://github.com/mariomka/vue-datetime/compare/v1.0.0-beta.11...v1.0.0-beta.12) 11 | > 1 May 2020 12 | - Readme - register component when using CDN [`#193`](https://github.com/mariomka/vue-datetime/pull/193) 13 | - Update `yarn.lock` [`ddde993`](https://github.com/mariomka/vue-datetime/commit/ddde9938ee0d9cea4fc8075ae23f40ee29ec2e6f) 14 | - Fix max-datetime when time is midnight. Fixes: #215. [`599faf8`](https://github.com/mariomka/vue-datetime/commit/599faf8a3f603aebf245452bd965710908cf32f7) 15 | - Don't add the `id` attribute when it is empty. Fixes: #202. [`d9f45b7`](https://github.com/mariomka/vue-datetime/commit/d9f45b7261540ee4ab0302cf19330a9a7049d339) 16 | - Fix travis config [`b8084aa`](https://github.com/mariomka/vue-datetime/commit/b8084aab6872d4b638f8a3788af7288ea4e030c7) 17 | - Update readme [`de01b03`](https://github.com/mariomka/vue-datetime/commit/de01b0370c97f009873a2f2b935c0d18943eb7b3) 18 | - FIX: Update dependency version of the rollup-plugin-vue since the latest 2.x is not compatible with newer vue stack (see https://github.com/vuejs/rollup-plugin-vue/issues/273) [`77f1826`](https://github.com/mariomka/vue-datetime/commit/77f1826522bfad27cbb943b9255d7ba8fca3dcbc) 19 | 20 | #### [v1.0.0-beta.11](https://github.com/mariomka/vue-datetime/compare/v1.0.0-beta.10...v1.0.0-beta.11) 21 | > 13 November 2019 22 | - add curly brackets export name [`#178`](https://github.com/mariomka/vue-datetime/pull/178) 23 | - Add type=time to the time example [`#172`](https://github.com/mariomka/vue-datetime/pull/172) 24 | - Update util.js [`#164`](https://github.com/mariomka/vue-datetime/pull/164) 25 | - improve pickers scrolling experience on mobile devices [`#162`](https://github.com/mariomka/vue-datetime/pull/162) 26 | - added a check to see if yearList ref exists [`#131`](https://github.com/mariomka/vue-datetime/pull/131) 27 | - replaced usage of Array.fill to work on old versions on Chrome [`#129`](https://github.com/mariomka/vue-datetime/pull/129) 28 | - Fix min/max dates when picker zone isn't UTC [`0576c01`](https://github.com/mariomka/vue-datetime/commit/0576c015c4b0906081ea1218a278f8af34fec13c) 29 | - Expose the popup. [`ad7d42f`](https://github.com/mariomka/vue-datetime/commit/ad7d42fb2f64d8725465cfd2b91eba22a534000d) 30 | - Added height to month selection arrows. [`55d3ac4`](https://github.com/mariomka/vue-datetime/commit/55d3ac432ece6a965a52567eabedef65e9dc2038) 31 | 32 | #### [v1.0.0-beta.10](https://github.com/mariomka/vue-datetime/compare/v1.0.0-beta.9...v1.0.0-beta.10) 33 | > 13 January 2019 34 | - Add title property. Based on #118 [`0da317c`](https://github.com/mariomka/vue-datetime/commit/0da317c4be5ef8c3c21642d0db5bcd17f9d27840) 35 | - Set `line-height` for popup [`04db82b`](https://github.com/mariomka/vue-datetime/commit/04db82bc12753b9a60ce3b8a2ff36bc31c6e9c43) 36 | - Remove 0.x notice [`3ba3ea8`](https://github.com/mariomka/vue-datetime/commit/3ba3ea8df943e239c927a4fb2ea38b39f2605c89) 37 | 38 | #### [v1.0.0-beta.9](https://github.com/mariomka/vue-datetime/compare/v1.0.0-beta.8...v1.0.0-beta.9) 39 | > 12 January 2019 40 | - Added new custom slots for Buttons [`#97`](https://github.com/mariomka/vue-datetime/pull/97) 41 | - fix: docs typo [`#99`](https://github.com/mariomka/vue-datetime/pull/99) 42 | - Update `yarn.lock` [`24830ab`](https://github.com/mariomka/vue-datetime/commit/24830ab587044fe34162054947af796cb5eaeed0) 43 | - months selector [`5e093d5`](https://github.com/mariomka/vue-datetime/commit/5e093d5f6539f64914894498208d1cc202fd18db) 44 | - specs for DatetimeMonthsPicker [`38f96d7`](https://github.com/mariomka/vue-datetime/commit/38f96d74f90feaa7368326f010d8b4a34ef04928) 45 | - Add property to customize the flow [`408720a`](https://github.com/mariomka/vue-datetime/commit/408720a400956d8ea4fd1e5df63f61b599368160) 46 | - Update readme [`1f5accd`](https://github.com/mariomka/vue-datetime/commit/1f5accd58a19cbab64bc0adceaf4aa28ccb3ea2b) 47 | - Revert split date for being compatible with all locales. So, month picker will be shown when clicking on the date. [`7892362`](https://github.com/mariomka/vue-datetime/commit/78923624c1e1c97a07aae0e201b1e32a38f75206) 48 | - Add `input-style` property and accept `Object` and `Array` besides to `String` for the property `input-class`. [`e5653b5`](https://github.com/mariomka/vue-datetime/commit/e5653b5cf7715b44530a9171880ebd53b4247b15) 49 | - Update readme [`0b79ce9`](https://github.com/mariomka/vue-datetime/commit/0b79ce95f21d99ff7c8c684b7f1d3a7bf92332d6) 50 | - Add `month` to flow [`84589b8`](https://github.com/mariomka/vue-datetime/commit/84589b864c185e904458c8597f64cf10ceb22df3) 51 | - Fix disabled months. Month could be enabled even if it first and last day is disabled. [`26493f4`](https://github.com/mariomka/vue-datetime/commit/26493f44b323935690556c225b6e6ddd30418f53) 52 | - Code style [`8a2a0bb`](https://github.com/mariomka/vue-datetime/commit/8a2a0bbaf11cd25783305595d085ece5b6773b76) 53 | - Update DatetimePopup.vue [`aa89e7f`](https://github.com/mariomka/vue-datetime/commit/aa89e7fa3589470d098366b218effe0b9bcca02d) 54 | - Set cursor pointer to header date [`1eb7125`](https://github.com/mariomka/vue-datetime/commit/1eb7125373d1fa7dfdf2775631db911d5440120c) 55 | - Set `inheritAttrs` to prevent apply not recognized props to the root element. [`a1ff9ee`](https://github.com/mariomka/vue-datetime/commit/a1ff9ee8e0f64409865856b51baa378c892f0f2e) 56 | - fix disabling [`dbefc2f`](https://github.com/mariomka/vue-datetime/commit/dbefc2f009280f15d9c2a096208fd0f689aa817d) 57 | - fix comma-spacing [`9a330e0`](https://github.com/mariomka/vue-datetime/commit/9a330e03af02308e264be739151b94d127c3d474) 58 | - fix comma-dangle [`68c70c2`](https://github.com/mariomka/vue-datetime/commit/68c70c229e18dabe818bbe75f544ca2274496b48) 59 | - fix comma-spacing [`44c69b8`](https://github.com/mariomka/vue-datetime/commit/44c69b83a204296fb1dd30417cfdd224d61caf1a) 60 | - Fix demo [`395f9db`](https://github.com/mariomka/vue-datetime/commit/395f9dbdad4f4b8180e44eaf358c52aef5a3a899) 61 | - Update DatetimePopup.vue [`b80f55b`](https://github.com/mariomka/vue-datetime/commit/b80f55bf5c49918d821c9349738d3a7d103c5222) 62 | 63 | #### [v1.0.0-beta.8](https://github.com/mariomka/vue-datetime/compare/v1.0.0-beta.7...v1.0.0-beta.8) 64 | > 14 October 2018 65 | - Fix min/max [`0466724`](https://github.com/mariomka/vue-datetime/commit/0466724360d90083cede24c940711691d7a4fd68) 66 | - Fix code style [`caa4cd2`](https://github.com/mariomka/vue-datetime/commit/caa4cd2d7d56a9ab2e2634cede7d14635be20d6d) 67 | - Fix min/max + [`bc28704`](https://github.com/mariomka/vue-datetime/commit/bc28704958199177cc63864d669029a8a83d04e8) 68 | 69 | #### [v1.0.0-beta.7](https://github.com/mariomka/vue-datetime/compare/v1.0.0-beta.6...v1.0.0-beta.7) 70 | > 14 October 2018 71 | - Tweak time picker [`3068f46`](https://github.com/mariomka/vue-datetime/commit/3068f46c3ab3dfb42e312b0200d26916a6429129) 72 | - time only spec [`1d2454c`](https://github.com/mariomka/vue-datetime/commit/1d2454c553375e7c3193bfa9cfa6639c2e2feb03) 73 | - Add tests [`00a2603`](https://github.com/mariomka/vue-datetime/commit/00a2603d9695cc1b85588618b2b6c324c5af42a6) 74 | - Update tests [`ae5ef11`](https://github.com/mariomka/vue-datetime/commit/ae5ef118cc408c9788a6d9d3ddef35f51a7c5776) 75 | - Added tests [`84f2b04`](https://github.com/mariomka/vue-datetime/commit/84f2b041d578b3a212f8314898f625cfcdb8ffd4) 76 | - Disable invalid year in yearpicker [`76fd1d9`](https://github.com/mariomka/vue-datetime/commit/76fd1d99f747032b150e76a5eff67d9807a65d6a) 77 | - Respect min date on default selection [`f8e40ea`](https://github.com/mariomka/vue-datetime/commit/f8e40ead674b7d52a6e9dc7c75b221bd27daf23f) 78 | - Added support for named slots [`13ad825`](https://github.com/mariomka/vue-datetime/commit/13ad825dd980dfe70abc723e740ca1dde09d0552) 79 | - time support [`35a0a13`](https://github.com/mariomka/vue-datetime/commit/35a0a1377cea97e3eeb2e5477bb25b14b244941c) 80 | - added setting value via hidden input [`56eb53e`](https://github.com/mariomka/vue-datetime/commit/56eb53eeac48d2df579ee07f622e94af3a942985) 81 | - Fix code style [`87f650f`](https://github.com/mariomka/vue-datetime/commit/87f650f72130d4f4c6ca2b51342a49010de2dbcd) 82 | - fix specs and dupe headers [`077e82a`](https://github.com/mariomka/vue-datetime/commit/077e82af109755da8f204bd741c1ae9ebe385aa4) 83 | - Simplify test [`93f08e2`](https://github.com/mariomka/vue-datetime/commit/93f08e2796a4563009dbdf32a06d730efe5d3d87) 84 | - Fix datetime popup display bug when using UTC zone [`598253e`](https://github.com/mariomka/vue-datetime/commit/598253e48c8f7e0b8ae96a7d7fe96c102ae5cc4a) 85 | - Fix min/max calendar test [`fb48c1c`](https://github.com/mariomka/vue-datetime/commit/fb48c1c6226d6ecabccb9974bad9aa495f5b6cd7) 86 | - Fix case [`fc4a5fb`](https://github.com/mariomka/vue-datetime/commit/fc4a5fb76b50c6b802c92b113d763794d43261db) 87 | - tweaks [`2b8ebed`](https://github.com/mariomka/vue-datetime/commit/2b8ebed08764ba39099212b63b4a532f2785acd6) 88 | - Removed forgotten comments :( [`bc583bf`](https://github.com/mariomka/vue-datetime/commit/bc583bf2556feac6f8e0da80ab73f1ba4bc95b07) 89 | 90 | #### [v1.0.0-beta.6](https://github.com/mariomka/vue-datetime/compare/v1.0.0-beta.5...v1.0.0-beta.6) 91 | > 9 July 2018 92 | - Add empty days at the end of the month to complete the week [`#72`](https://github.com/mariomka/vue-datetime/pull/72) 93 | - Merge branch 'v1.x' of https://github.com/coreycoburn/vue-datetime into coreycoburn-v1.x [`#56`](https://github.com/mariomka/vue-datetime/issues/56) 94 | - Add test `should pass datetime to popup` [`9a22b1e`](https://github.com/mariomka/vue-datetime/commit/9a22b1e4c18175d24ea9961994a877bbd9e1dafb) 95 | - Add `input-id` parameter to avoid id collisions [`d3d9797`](https://github.com/mariomka/vue-datetime/commit/d3d979771b5d51b5c0f961a758bb7eef04597107) 96 | - Simplify roundMinute code [`3e28c20`](https://github.com/mariomka/vue-datetime/commit/3e28c2064d1cbe01dbc5ee1986bdacf4eebac6ed) 97 | - Remove seconds and milliseconds when new date is created [`4895a21`](https://github.com/mariomka/vue-datetime/commit/4895a2189717aa526a33c0b99d7d779a4212e8ff) 98 | 99 | #### [v1.0.0-beta.5](https://github.com/mariomka/vue-datetime/compare/v1.0.0-beta.4...v1.0.0-beta.5) 100 | > 9 July 2018 101 | - Add close event [`5fe6a20`](https://github.com/mariomka/vue-datetime/commit/5fe6a20f31d5c29190e0c4ef998a9e50f0d2e7b6) 102 | - Update luxon to v1.x [`06783cf`](https://github.com/mariomka/vue-datetime/commit/06783cf1716d3eda92c820f0da4d880389199aa7) 103 | - Fix test [`2d93a6e`](https://github.com/mariomka/vue-datetime/commit/2d93a6e129847b652c20557e2950a539f1a37f6b) 104 | 105 | #### [v1.0.0-beta.4](https://github.com/mariomka/vue-datetime/compare/v1.0.0-beta.3...v1.0.0-beta.4) 106 | > 8 July 2018 107 | - apply default rounded time when using minute step - resolves #56 [`#56`](https://github.com/mariomka/vue-datetime/issues/56) 108 | - Generate hidden input with raw value when name is passed [`32f8744`](https://github.com/mariomka/vue-datetime/commit/32f87444285396152f04fc2cc4b10dce31c437a3) 109 | - macro tokens are supported. [`5db1a70`](https://github.com/mariomka/vue-datetime/commit/5db1a70b532476e1481123a792bf3dd4ffd39844) 110 | - Add test for macro tokens format [`d995e02`](https://github.com/mariomka/vue-datetime/commit/d995e02a9a9c86915a25ed83993f286b3fdc1a1c) 111 | - Fix value formatting with `type=date` and specified time zone [`522b19d`](https://github.com/mariomka/vue-datetime/commit/522b19d5c3780b26a1158dd794c249eb51a75dd8) 112 | - Rename `name` attribute by `hidden-name` [`f06fc41`](https://github.com/mariomka/vue-datetime/commit/f06fc41ff4b4377793f6541c9ca0af63e2359e91) 113 | - Update readme [`4bcbeef`](https://github.com/mariomka/vue-datetime/commit/4bcbeef72201ea899903ec4eb9d59ec75d0bad1b) 114 | - Fix time zone for macro tokens format [`f868eb6`](https://github.com/mariomka/vue-datetime/commit/f868eb636472bdf059d832b95cf71dc7a9559f66) 115 | - fix format describe [`c2a3456`](https://github.com/mariomka/vue-datetime/commit/c2a34563d7b8b3015dd4437d5f76d1473f87ee89) 116 | - fix format describe [`5f44166`](https://github.com/mariomka/vue-datetime/commit/5f441666159a1c98cf65b59ec1fe749e14af173c) 117 | - Macro tokens are supported. [`74deb7d`](https://github.com/mariomka/vue-datetime/commit/74deb7dc264ff7342e6595ebc1d02159208d8b72) 118 | 119 | #### [v1.0.0-beta.3](https://github.com/mariomka/vue-datetime/compare/v1.0.0-beta.2...v1.0.0-beta.3) 120 | > 3 February 2018 121 | - Add 12/24 hour format option to the time picker. [`#39`](https://github.com/mariomka/vue-datetime/pull/39) 122 | - Update issue template [`4c5de6d`](https://github.com/mariomka/vue-datetime/commit/4c5de6d79a8d56f9ee41fef5001035bcf7ce446e) 123 | - Fix demo example code [`2a5944f`](https://github.com/mariomka/vue-datetime/commit/2a5944f4fd1f0393147367ef9e80477529e7d2bd) 124 | 125 | #### [v1.0.0-beta.2](https://github.com/mariomka/vue-datetime/compare/v1.0.0-beta.1...v1.0.0-beta.2) 126 | > 23 January 2018 127 | - Add `week-start` parameter [`6cb9279`](https://github.com/mariomka/vue-datetime/commit/6cb92797ec9140b02adb5566bdd9dfd5b4869b0c) 128 | - Update demo [`057f87a`](https://github.com/mariomka/vue-datetime/commit/057f87a13d66890df4cc967134f193147acd53ea) 129 | - Fix issue with value time zone [`035d4da`](https://github.com/mariomka/vue-datetime/commit/035d4daeb7d6bd8d051f97458c68707941c9870b) 130 | - Refactoring calendar new date [`d82d09c`](https://github.com/mariomka/vue-datetime/commit/d82d09c82cabb90dda2d7fc876f106edc8c2af5f) 131 | - Rename `date` by `datetime` in `Datetime.vue` [`296f2c0`](https://github.com/mariomka/vue-datetime/commit/296f2c01b92e14dd628e5d667963cd79d1f63705) 132 | - Refactoring [`7d40f6f`](https://github.com/mariomka/vue-datetime/commit/7d40f6fd4d55e1b37b9fa2f4332b758b464f93f5) 133 | - Rename and improve `clearTime` util [`ccd80f5`](https://github.com/mariomka/vue-datetime/commit/ccd80f5ed1f7453993fc11e441b941c13d3c312d) 134 | - Fix demo webpack [`579096d`](https://github.com/mariomka/vue-datetime/commit/579096ddb528e6a7db4c2ad157a13995f5c9a775) 135 | - Add `luxon` in rollup globals config [`46551bf`](https://github.com/mariomka/vue-datetime/commit/46551bfb6680605c24f9731896224977af378801) 136 | - Add demo link to readme [`44c667f`](https://github.com/mariomka/vue-datetime/commit/44c667fcd9ee5927581f886902ae438488567296) 137 | - Remove auto-changelog limit [`f972692`](https://github.com/mariomka/vue-datetime/commit/f9726929820e3666a9d968c080277c0e0673c0a1) 138 | 139 | #### [v1.0.0-beta.1](https://github.com/mariomka/vue-datetime/compare/v1.0.0-alpha.8...v1.0.0-beta.1) 140 | > 19 January 2018 141 | - Cancel popup on key down ESC or TAB [`47c8fca`](https://github.com/mariomka/vue-datetime/commit/47c8fcaf21bf53779f6549bf955ba27baddefd0a) 142 | - Close popup on press ESC key [`072063f`](https://github.com/mariomka/vue-datetime/commit/072063fbab008a1d9c0081b5418a385b40de3623) 143 | - Continue or confirm popup on key down ENTER [`ff281ca`](https://github.com/mariomka/vue-datetime/commit/ff281cab340bc34e2bd7098f72a3ac9ef912aace) 144 | - Fix auto on select time [`69edaaa`](https://github.com/mariomka/vue-datetime/commit/69edaaa3403a664779c026b72a7b1beec06e115c) 145 | - On open popup blur input [`5155ac7`](https://github.com/mariomka/vue-datetime/commit/5155ac7651922bab7a245886df1cafc81d4d1f3a) 146 | 147 | #### [v1.0.0-alpha.8](https://github.com/mariomka/vue-datetime/compare/v1.0.0-alpha.7...v1.0.0-alpha.8) 148 | > 18 January 2018 149 | - Value will be an empty string instead of null when the date is empty or invalid because value type is a string. [`3fa3463`](https://github.com/mariomka/vue-datetime/commit/3fa346337cc6266b532c4373142429431fe9972e) 150 | - Update luxon peer dependency version [`2dd13b2`](https://github.com/mariomka/vue-datetime/commit/2dd13b2f8f445c1994d1e4afddedb31006c4417b) 151 | 152 | #### [v1.0.0-alpha.7](https://github.com/mariomka/vue-datetime/compare/v1.0.0-alpha.6...v1.0.0-alpha.7) 153 | > 18 January 2018 154 | - Watch value changes [`d58b910`](https://github.com/mariomka/vue-datetime/commit/d58b9109197fcef0334c783130df002760270bd7) 155 | - Update readme and issue template [`8adff3d`](https://github.com/mariomka/vue-datetime/commit/8adff3df24f009048ae0d8beb07b78ef14d1a5f2) 156 | - Ignore all vue devtools globals in mocha [`7daee98`](https://github.com/mariomka/vue-datetime/commit/7daee98d505f683ae7d8cd31035676ab5b03097b) 157 | - Update issue template [`b525ff6`](https://github.com/mariomka/vue-datetime/commit/b525ff62f2ae855eb4a9e5ccda96972821b06b59) 158 | 159 | #### [v1.0.0-alpha.6](https://github.com/mariomka/vue-datetime/compare/v1.0.0-alpha.5...v1.0.0-alpha.6) 160 | > 16 January 2018 161 | - Add `auto` property [`8d6cec3`](https://github.com/mariomka/vue-datetime/commit/8d6cec3a45e8ce26e7eb0a5ffef7a410167b3809) 162 | - Update readme [`cef7afe`](https://github.com/mariomka/vue-datetime/commit/cef7afe4a4ef7db80209bf48f5dff4bfddbc8254) 163 | - Update luxon to v0.3.1 [`635be46`](https://github.com/mariomka/vue-datetime/commit/635be467a24f760a76e46414660d306f328ea671) 164 | - Fix local time zone test [`e35cec1`](https://github.com/mariomka/vue-datetime/commit/e35cec180762fd15adf359a31eb47eac07a13970) 165 | - Add travis config [`f0289e9`](https://github.com/mariomka/vue-datetime/commit/f0289e96278ddfa2997be8f2bd7812a6784b77f5) 166 | - Fix Datetime types tests [`0e6663c`](https://github.com/mariomka/vue-datetime/commit/0e6663c07c9b40e21646e32dee16982a9a8cd8d5) 167 | - Add shields to readme [`a0af5e3`](https://github.com/mariomka/vue-datetime/commit/a0af5e3ebe242c217bd35b313f3af0839ddd7444) 168 | - Fix license and coverage link [`109a5be`](https://github.com/mariomka/vue-datetime/commit/109a5be52020d7d49dd376bd6a6b36af53470d9e) 169 | - Add `codecov` to travis configuration [`122a301`](https://github.com/mariomka/vue-datetime/commit/122a3010b86cfec4b9abd243b40297c63e229dfd) 170 | - Add demo gif [`e528f9e`](https://github.com/mariomka/vue-datetime/commit/e528f9e3f9c90a0627326cec293e63fa78e01b16) 171 | - Fix shields [`9356b3f`](https://github.com/mariomka/vue-datetime/commit/9356b3f1b29f14310dd739d12fa9bf181b0339af) 172 | 173 | #### [v1.0.0-alpha.5](https://github.com/mariomka/vue-datetime/compare/v1.0.0-alpha.4...v1.0.0-alpha.5) 174 | > 15 January 2018 175 | - Fix popup time zone [`787752f`](https://github.com/mariomka/vue-datetime/commit/787752f1f48f5a52a55e6440ed86a844d0a49870) 176 | - Add `input-class` property [`b94c355`](https://github.com/mariomka/vue-datetime/commit/b94c3554731b2323a4b3050b873737a02773b71b) 177 | - Add issue template [`c0c6057`](https://github.com/mariomka/vue-datetime/commit/c0c605734295413c5135d194f8dd3eb8c1985168) 178 | 179 | #### [v1.0.0-alpha.4](https://github.com/mariomka/vue-datetime/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) 180 | > 15 January 2018 181 | - Reformat code [`6391d30`](https://github.com/mariomka/vue-datetime/commit/6391d30f3d778bd55d9e72fd4ad89c60190a8660) 182 | - Add minimum and maximum datetimes. [`5467eec`](https://github.com/mariomka/vue-datetime/commit/5467eec5125b8f3eea605bca3fc57fafb9d473e2) 183 | - Clear time when type is date [`317d0b4`](https://github.com/mariomka/vue-datetime/commit/317d0b4f2b1a3f53daa35fcc1f1589ed80c89f7d) 184 | - Add notice and todo to readme [`6a8855b`](https://github.com/mariomka/vue-datetime/commit/6a8855b0a970972b0992c8e840efcb13637f2ca8) 185 | - Refactor utils [`45a82a3`](https://github.com/mariomka/vue-datetime/commit/45a82a3db8cdb61cd89aa354e1cfd8cc1d899851) 186 | - Update readme parameters section [`4461ab5`](https://github.com/mariomka/vue-datetime/commit/4461ab5340d69b4c959fa4b7007659ef7e0f81e3) 187 | - Add `__VUE_DEVTOOLS_TOAST__` to accepted global variables names in mocha. [`1b40d2f`](https://github.com/mariomka/vue-datetime/commit/1b40d2f0f515c65ee5bc49e6ab9c676ea35a7d96) 188 | 189 | #### [v1.0.0-alpha.3](https://github.com/mariomka/vue-datetime/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) 190 | > 25 December 2017 191 | 192 | #### [v1.0.0-alpha.2](https://github.com/mariomka/vue-datetime/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) 193 | > 25 December 2017 194 | - Add year picker [`c7766c8`](https://github.com/mariomka/vue-datetime/commit/c7766c837bb54ccadbfa6007a7891c7ddea6426d) 195 | - Update `phrases` test [`c056826`](https://github.com/mariomka/vue-datetime/commit/c056826b5876f5355d02830600b5670b68628614) 196 | - Pass time steps from date picker to time picker through the popup. [`fd2d338`](https://github.com/mariomka/vue-datetime/commit/fd2d338a731bb3a659fb5ed754b4f87c053d9b7d) 197 | - Fix tests [`539a5cf`](https://github.com/mariomka/vue-datetime/commit/539a5cfc094b0c980e3d40844d2f5a8f6c7c1f0f) 198 | - Refactor flow [`7b19f2f`](https://github.com/mariomka/vue-datetime/commit/7b19f2ffaa2106eb81a84c54e443c9405c3ed679) 199 | - Rename `i18n` prop by `phrases` and add it to `Datetime` component [`46e10ff`](https://github.com/mariomka/vue-datetime/commit/46e10ffff0bded97554a99836c512f8da99546a1) 200 | - Add `auto-changelog` package to auto generate changelog on every version [`db2b5ea`](https://github.com/mariomka/vue-datetime/commit/db2b5eab7678fee07c0fa97f4084d9b3468fbac2) 201 | - Update readme [`d882209`](https://github.com/mariomka/vue-datetime/commit/d8822092c9cd99f1748f0faf5eb0ed758ab19da1) 202 | - Remove `wrapper-class` and `input-class` props. Use `class` attribute instead. [`5017b63`](https://github.com/mariomka/vue-datetime/commit/5017b6374486f0cd78f3b508fc495fbdc6185390) 203 | - Refactor how years, hours and minutes are generated. [`e3e5429`](https://github.com/mariomka/vue-datetime/commit/e3e54296b0e9ef7dceed7bba4661d8b6c9f2e532) 204 | - Update readme [`5d60ed3`](https://github.com/mariomka/vue-datetime/commit/5d60ed3bc0f937dc6e380c0dc25d8b9b4677812e) 205 | - Update readme [`63c1c43`](https://github.com/mariomka/vue-datetime/commit/63c1c43d0d91edf8c4f437317a1fb6e06a1ec194) 206 | - Fix transition z-index issue [`9ac6aa9`](https://github.com/mariomka/vue-datetime/commit/9ac6aa9d9b6ab23d826e913a998ae3cac466bccc) 207 | - Remove unused prop [`c709478`](https://github.com/mariomka/vue-datetime/commit/c7094782a9b24702f2ae12f8c99441336837e746) 208 | - Remove duplicated year from header [`f2ba651`](https://github.com/mariomka/vue-datetime/commit/f2ba651edad9392759883482f8d53d84bebe252c) 209 | - Update readme [`d04236a`](https://github.com/mariomka/vue-datetime/commit/d04236ac20954901eb9808c07d754514d53f97f4) 210 | 211 | #### v1.0.0-alpha.1 212 | > 20 December 2017 213 | 214 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/mariomka/vue-datetime). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **Keep the same style** - eslint will automatically be ran before committing 11 | 12 | - **Tip** to pass lint tests easier use the `yarn lint` command 13 | 14 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 15 | 16 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 17 | 18 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 19 | 20 | - **Create feature branches** - Don't ask us to pull from your master branch. 21 | 22 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 23 | 24 | - **Send coherent history** - Make sure your commits message means something 25 | 26 | 27 | ## Running Tests 28 | 29 | Launch tests 30 | 31 | ``` bash 32 | $ yarn test 33 | ``` 34 | 35 | Launch visual tests and watch the components at the same time 36 | 37 | ``` bash 38 | $ yarn dev 39 | ``` 40 | 41 | 42 | **Happy coding**! 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present Mario Juárez (http://www.mjp.one) 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 | **🚨 THIS PROJECT IS NO LONGER MAINTAINED 🚨** [read more](https://github.com/mariomka/vue-datetime/issues/276) 2 | 3 | # vue-datetime 4 | 5 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 6 | [![Latest Version on NPM](https://img.shields.io/npm/v/vue-datetime.svg?style=flat-square)](https://npmjs.com/package/vue-datetime) 7 | [![npm](https://img.shields.io/npm/dt/vue-datetime.svg?style=flat-square)](https://www.npmjs.com/package/vue-datetime) 8 | [![Vue 2.x](https://img.shields.io/badge/vue-2.x-brightgreen.svg?style=flat-square)](https://vuejs.org) 9 | [![Build](https://img.shields.io/travis/mariomka/vue-datetime/v1.x.svg?style=flat-square)](https://travis-ci.org/mariomka/vue-datetime) 10 | [![Coverage](https://img.shields.io/codecov/c/github/mariomka/vue-datetime/v1.x.svg?style=flat-square)](https://codecov.io/gh/mariomka/vue-datetime) 11 | 12 | > Mobile friendly datetime picker for Vue. Supports date, datetime and time modes, i18n and more. 13 | 14 | ## Demo 15 | 16 | **[Go to demo](http://mariomka.github.io/vue-datetime)**. 17 | 18 | [![demo](https://raw.githubusercontent.com/mariomka/vue-datetime/v1.x/demo/demo.gif)](http://mariomka.github.io/vue-datetime) 19 | 20 | ## Installation 21 | 22 | ### Bundler (Webpack, Rollup...) 23 | 24 | ```bash 25 | yarn add luxon vue-datetime weekstart 26 | ``` 27 | 28 | Or 29 | 30 | ```bash 31 | npm install --save luxon vue-datetime weekstart 32 | ``` 33 | 34 | **weekstart** is optional, is used to get the first day of the week. 35 | 36 | #### Register 37 | 38 | ```js 39 | import Vue from 'vue' 40 | import { Datetime } from 'vue-datetime' 41 | // You need a specific loader for CSS files 42 | import 'vue-datetime/dist/vue-datetime.css' 43 | 44 | Vue.use(Datetime) 45 | ``` 46 | 47 | #### Register manually 48 | 49 | ##### Global 50 | 51 | ```js 52 | import { Datetime } from 'vue-datetime'; 53 | 54 | Vue.component('datetime', Datetime); 55 | ``` 56 | 57 | ##### Local 58 | 59 | ```js 60 | import { Datetime } from 'vue-datetime'; 61 | 62 | Vue.extend({ 63 | template: '...', 64 | components: { 65 | datetime: Datetime 66 | } 67 | }); 68 | ``` 69 | 70 | ### Browser 71 | 72 | Download vue, luxon, weekstart and vue-datetime or use a CDN like unpkg. 73 | 74 | ```html 75 | 76 | 77 | 78 | 79 | 80 | ``` 81 | 82 | The component registers itself automatically as ``. If you want to use a different name then register it explicitly: 83 | 84 | ```js 85 | Vue.component('vue-datetime', window.VueDatetime.Datetime); 86 | ``` 87 | 88 | 89 | **weekstart** is optional, is used to get the first day of the week. 90 | 91 | ## Usage 92 | 93 | ### Minimal 94 | 95 | ```html 96 | 97 | ``` 98 | 99 | ## Setup 100 | 101 | ### Parameters 102 | 103 | Parameter | Type | Default | Description 104 | --------- | ---- | ------- | ----------- 105 | v-model (*required*) | ISO 8601 `String` | - | Datetime. 106 | type | `String` | `date` | Picker type: date, datetime or time. 107 | input-id | `String` | `''` | Id for the input. 108 | input-class | `String`, `Array` or `Object` | `''` | Class for the input. 109 | input-style | `String`, `Array` or `Object` | `''` | Style for the input. 110 | hidden-name | `String` | `null` | Name for hidden input with raw value. See #51. 111 | value-zone | `String` | `UTC` | Time zone for the value. 112 | zone | `String` | `local` | Time zone for the picker. 113 | format | `Object` or `String` | `DateTime.DATE_MED`, `DateTime.DATETIME_MED` or `DateTime.TIME_24_SIMPLE` | Input date format. Luxon [presets](https://moment.github.io/luxon/docs/manual/formatting.html#tolocalestring--strings-for-humans-) or [tokens](https://moment.github.io/luxon/docs/manual/formatting.html#formatting-with-tokens--strings-for-cthulhu-). 114 | phrases | `Object` | `{ok: 'Ok', cancel: 'Cancel'}` | Phrases. 115 | use12-hour | `Boolean` | `false` | Display 12 hour (AM/PM) mode 116 | hour-step | `Number` | `1` | Hour step. 117 | minute-step | `Number` | `1` | Minute step. 118 | min-datetime | ISO 8601 `String` | `null` | Minimum datetime. 119 | max-datetime | ISO 8601 `String` | `null` | Maximum datetime. 120 | auto | `Boolean` | `false` | Auto continue/close on select. 121 | week-start | `Number` | auto from locale if _weekstart_ is available or `1` | First day of the week. 1 is Monday and 7 is Sunday. 122 | flow | `Array` | Depends of *type* | Customize steps flow, steps available: time, date, month, year. Example: ['year', 'date', 'time'] 123 | title | `String` | `''` | Popup title. 124 | hide-backdrop | `Boolean` | `false` | Show/Hide backdrop. 125 | backdrop-click | `Boolean` | `true` | Enable/Disable backdrop click to cancel (outside click). 126 | 127 | Input inherits all props not defined above but `style` and `class` will be inherited by root element. [See inheritAttrs option](https://vuejs.org/v2/api/#inheritAttrs) 128 | 129 | The component is based on [Luxon](https://github.com/moment/luxon), check out [documentation](https://moment.github.io/luxon/docs/index.html) to set [time zones](https://moment.github.io/luxon/docs/manual/zones.html) and [format](https://moment.github.io/luxon/docs/manual/formatting.html). 130 | 131 | ### Internationalization 132 | 133 | Date internationalization depends on luxon. [Set the default locale](https://moment.github.io/luxon/docs/manual/intl.html#setting-the-default). 134 | 135 | ```js 136 | import { Settings } from 'luxon' 137 | 138 | Settings.defaultLocale = 'es' 139 | ``` 140 | 141 | ### Events 142 | 143 | Component emits the `input` event to work with `v-model`. [More info](https://vuejs.org/v2/guide/components.html#Form-Input-Components-using-Custom-Events). 144 | 145 | `close` event is emitted when the popup closes. 146 | 147 | Also, input text inherits all component events. 148 | 149 | ### Slots 150 | 151 | You can customize the component using named slots. 152 | 153 | Available slots: `before`, `after`, `button-cancel` and `button-confirm` 154 | 155 | #### Button customization example: 156 | 157 | ```html 158 | 159 | 160 | The field description 161 | 165 | 169 | 170 | ``` 171 | 172 | You can also use `slot-scope` to determine which view is currently active: 173 | 174 | ```html 175 | 179 | ``` 180 | 181 | ## Theming 182 | 183 | Theming is supported by overwriting CSS classes. 184 | 185 | ## Development 186 | 187 | ### Launch lint and tests 188 | 189 | ```bash 190 | yarn test 191 | ``` 192 | 193 | ### Launch visual tests 194 | 195 | ```bash 196 | yarn dev 197 | ``` 198 | 199 | ### Build 200 | 201 | Bundle the js and css to the `dist` folder: 202 | 203 | ```bash 204 | yarn build 205 | ``` 206 | 207 | ## License 208 | 209 | [The MIT License](http://opensource.org/licenses/MIT) 210 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | const mkdirp = require('mkdirp') 2 | const rollup = require('rollup').rollup 3 | const vue = require('rollup-plugin-vue') 4 | const buble = require('rollup-plugin-buble') 5 | const replace = require('rollup-plugin-replace') 6 | const cjs = require('rollup-plugin-commonjs') 7 | const node = require('rollup-plugin-node-resolve') 8 | const uglify = require('uglify-js') 9 | const CleanCSS = require('clean-css') 10 | 11 | // Make sure dist dir exists 12 | mkdirp('dist') 13 | 14 | const { 15 | logError, 16 | write, 17 | banner, 18 | name, 19 | moduleName, 20 | version, 21 | processStyle 22 | } = require('./utils') 23 | 24 | function rollupBundle ({ env }) { 25 | return rollup({ 26 | entry: 'src/index.js', 27 | external: ['luxon'], 28 | plugins: [ 29 | node({ 30 | extensions: ['.js', '.vue'] 31 | }), 32 | cjs(), 33 | vue({ 34 | compileTemplate: true, 35 | css (styles, stylesNodes) { 36 | // Only generate the styles once 37 | if (env['process.env.NODE_ENV'] === '"production"') { 38 | Promise.all( 39 | stylesNodes.map(processStyle) 40 | ).then(css => { 41 | const result = css.map(c => c.css).join('') 42 | // write the css for every component 43 | // TODO add it back if we extract all components to individual js 44 | // files too 45 | // css.forEach(writeCss) 46 | write(`dist/${name}.css`, result) 47 | write(`dist/${name}.min.css`, new CleanCSS().minify(result).styles) 48 | }).catch(logError) 49 | } 50 | } 51 | }), 52 | replace(Object.assign({ 53 | __VERSION__: version 54 | }, env)), 55 | buble({ 56 | objectAssign: 'Object.assign' 57 | }) 58 | ] 59 | }) 60 | } 61 | 62 | const bundleOptions = { 63 | banner, 64 | exports: 'named', 65 | format: 'umd', 66 | globals: { 67 | luxon: 'luxon' 68 | }, 69 | moduleName 70 | } 71 | 72 | function createBundle ({ name, env, format }) { 73 | return rollupBundle({ 74 | env 75 | }).then(function (bundle) { 76 | const options = Object.assign({}, bundleOptions) 77 | if (format) { 78 | options.format = format 79 | } 80 | 81 | const code = bundle.generate(options).code 82 | if (/min$/.test(name)) { 83 | const minified = uglify.minify(code, { 84 | output: { 85 | preamble: banner, 86 | ascii_only: true // eslint-disable-line camelcase 87 | } 88 | }).code 89 | return write(`dist/${name}.js`, minified) 90 | } else { 91 | return write(`dist/${name}.js`, code) 92 | } 93 | }).catch(logError) 94 | } 95 | 96 | // Browser bundle (can be used with script) 97 | createBundle({ 98 | name: `${name}`, 99 | env: { 100 | 'process.env.NODE_ENV': '"development"' 101 | } 102 | }) 103 | 104 | // Commonjs bundle (preserves process.env.NODE_ENV) so 105 | // the user can replace it in dev and prod mode 106 | createBundle({ 107 | name: `${name}.common`, 108 | env: {}, 109 | format: 'cjs' 110 | }) 111 | 112 | // uses export and import syntax. Should be used with modern bundlers 113 | // like rollup and webpack 2 114 | createBundle({ 115 | name: `${name}.esm`, 116 | env: {}, 117 | format: 'es' 118 | }) 119 | 120 | // Minified version for browser 121 | createBundle({ 122 | name: `${name}.min`, 123 | env: { 124 | 'process.env.NODE_ENV': '"production"' 125 | } 126 | }) 127 | -------------------------------------------------------------------------------- /build/utils/index.js: -------------------------------------------------------------------------------- 1 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 2 | const { join } = require('path') 3 | 4 | const { 5 | red, 6 | logError 7 | } = require('./log') 8 | 9 | const { 10 | processStyle 11 | } = require('./style') 12 | 13 | const uppercamelcase = require('uppercamelcase') 14 | 15 | exports.write = require('./write') 16 | 17 | const { 18 | author, 19 | name, 20 | version, 21 | dllPlugin 22 | } = require('../../package.json') 23 | 24 | const authorName = author.replace(/\s+<.*/, '') 25 | const minExt = process.env.NODE_ENV === 'production' ? '.min' : '' 26 | 27 | exports.author = authorName 28 | exports.version = version 29 | exports.dllName = dllPlugin.name 30 | exports.moduleName = uppercamelcase(name) 31 | exports.name = name 32 | exports.filename = name + minExt 33 | exports.banner = `/*! 34 | * ${name} v${version} 35 | * (c) ${new Date().getFullYear()} ${authorName} 36 | * Released under the MIT License. 37 | */ 38 | ` 39 | 40 | // log.js 41 | exports.red = red 42 | exports.logError = logError 43 | 44 | // It'd be better to add a sass property to the vue-loader options 45 | // but it simply don't work 46 | const sassOptions = { 47 | includePaths: [ 48 | join(__dirname, '../../node_modules') 49 | ] 50 | } 51 | 52 | // don't extract css in test mode 53 | const nullLoader = process.env.NODE_ENV === 'common' ? 'null-loader!' : '' 54 | exports.vueLoaders = 55 | process.env.BABEL_ENV === 'test' ? { 56 | css: 'css-loader', 57 | scss: `css-loader!sass-loader?${JSON.stringify(sassOptions)}` 58 | } : { 59 | css: ExtractTextPlugin.extract(`${nullLoader}css-loader`), 60 | scss: ExtractTextPlugin.extract( 61 | `${nullLoader}css-loader!sass-loader?${JSON.stringify(sassOptions)}` 62 | ) 63 | } 64 | 65 | // style.js 66 | exports.processStyle = processStyle 67 | -------------------------------------------------------------------------------- /build/utils/log.js: -------------------------------------------------------------------------------- 1 | function logError (e) { 2 | console.log(e) 3 | } 4 | 5 | function blue (str) { 6 | return `\x1b[1m\x1b[34m${str}\x1b[39m\x1b[22m` 7 | } 8 | 9 | function green (str) { 10 | return `\x1b[1m\x1b[32m${str}\x1b[39m\x1b[22m` 11 | } 12 | 13 | function red (str) { 14 | return `\x1b[1m\x1b[31m${str}\x1b[39m\x1b[22m` 15 | } 16 | 17 | function yellow (str) { 18 | return `\x1b[1m\x1b[33m${str}\x1b[39m\x1b[22m` 19 | } 20 | 21 | module.exports = { 22 | blue, 23 | green, 24 | red, 25 | yellow, 26 | logError 27 | } 28 | -------------------------------------------------------------------------------- /build/utils/style.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const postcss = require('postcss') 3 | const cssnext = require('postcss-cssnext') 4 | const CleanCSS = require('clean-css') 5 | const write = require('./write.js') 6 | 7 | function processCss (style) { 8 | const componentName = path.basename(style.id, '.vue') 9 | return postcss([cssnext()]) 10 | .process(style.code, {}) 11 | .then(result => { 12 | return { 13 | name: componentName, 14 | css: result.css, 15 | map: result.map 16 | } 17 | }) 18 | } 19 | 20 | function processStyle (style) { 21 | if (style.lang === 'css') { 22 | return processCss(style) 23 | } else { 24 | throw new Error(`Unknown style language '${style.lang}'`) 25 | } 26 | } 27 | 28 | function writeCss (style) { 29 | write(`dist/${style.name}.css`, style.css) 30 | 31 | if (style.original) { 32 | write(`dist/${style.name}.${style.original.ext}`, style.original.code) 33 | } 34 | 35 | if (style.map) { 36 | write(`dist/${style.name}.css.map`, style.map) 37 | } 38 | 39 | write(`dist/${style.name}.min.css`, new CleanCSS().minify(style.css).styles) 40 | } 41 | 42 | module.exports = { 43 | writeCss, 44 | processStyle 45 | } 46 | -------------------------------------------------------------------------------- /build/utils/write.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | const { blue } = require('./log.js') 4 | 5 | function write (dest, code) { 6 | return new Promise(function (resolve, reject) { 7 | fs.writeFile(dest, code, function (err) { 8 | if (err) { 9 | return reject(err) 10 | } 11 | 12 | console.log(blue(dest) + ' ' + getSize(code)) 13 | resolve(code) 14 | }) 15 | }) 16 | } 17 | 18 | function getSize (code) { 19 | return (code.length / 1024).toFixed(2) + 'kb' 20 | } 21 | 22 | module.exports = write 23 | -------------------------------------------------------------------------------- /build/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 3 | const { resolve } = require('path') 4 | 5 | const { 6 | banner, 7 | filename, 8 | version, 9 | vueLoaders 10 | } = require('./utils') 11 | 12 | const plugins = [ 13 | new webpack.DefinePlugin({ 14 | '__VERSION__': JSON.stringify(version), 15 | 'process.env.NODE_ENV': '"test"' 16 | }), 17 | new webpack.BannerPlugin({ banner, raw: true, entryOnly: true }), 18 | new ExtractTextPlugin({ 19 | filename: `${filename}.css`, 20 | // Don't extract css in test mode 21 | disable: /^(common|test)$/.test(process.env.NODE_ENV) 22 | }) 23 | ] 24 | 25 | module.exports = { 26 | output: { 27 | path: resolve(__dirname, '../dist'), 28 | filename: `${filename}.common.js` 29 | }, 30 | entry: './src/index.js', 31 | resolve: { 32 | extensions: ['.js', '.vue', 'css'], 33 | alias: { 34 | 'src': resolve(__dirname, '../src') 35 | } 36 | }, 37 | module: { 38 | rules: [ 39 | { 40 | test: /.js$/, 41 | use: 'babel-loader', 42 | include: [ 43 | resolve(__dirname, '../node_modules/@material'), 44 | resolve(__dirname, '../src'), 45 | resolve(__dirname, '../test') 46 | ] 47 | }, 48 | { 49 | test: /\.vue$/, 50 | loader: 'vue-loader', 51 | options: { 52 | loaders: vueLoaders, 53 | postcss: [require('postcss-cssnext')()] 54 | } 55 | } 56 | ] 57 | }, 58 | plugins 59 | } 60 | -------------------------------------------------------------------------------- /build/webpack.config.demo.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | entry: path.resolve(__dirname, '../demo/src/app.js'), 5 | 6 | output: { 7 | path: path.resolve(__dirname, '../demo/dist'), 8 | filename: 'app.js', 9 | publicPath: '/dist/' 10 | }, 11 | 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.vue$/, 16 | loader: 'vue-loader', 17 | exclude: /node_modules/ 18 | }, 19 | { 20 | test: /\.js$/, 21 | loader: 'babel-loader', 22 | exclude: /node_modules/ 23 | }, 24 | { 25 | test: /\.css$/, 26 | use: [ 27 | 'style-loader', 28 | 'css-loader', 29 | { 30 | loader: 'postcss-loader', 31 | options: { 32 | plugins: () => [ 33 | require('postcss-cssnext')() 34 | ] 35 | } 36 | } 37 | ] 38 | } 39 | ] 40 | }, 41 | resolve: { 42 | alias: { 43 | vue: 'vue/dist/vue.js' 44 | }, 45 | extensions: ['.js', '.vue'] 46 | }, 47 | devServer: { 48 | contentBase: path.resolve(__dirname, '../demo'), 49 | watchContentBase: true 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /build/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const merge = require('webpack-merge') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin') 5 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 6 | const DashboardPlugin = require('webpack-dashboard/plugin') 7 | const base = require('./webpack.config.base') 8 | const { resolve, join } = require('path') 9 | const { existsSync } = require('fs') 10 | const { 11 | dllName, 12 | logError, 13 | red, 14 | vueLoaders 15 | } = require('./utils') 16 | 17 | const rootDir = resolve(__dirname, '../test') 18 | const buildPath = resolve(rootDir, 'dist') 19 | 20 | if (!existsSync(join(buildPath, dllName) + '.dll.js')) { 21 | logError(red('The DLL manifest is missing. Please run `npm run build:dll` (Quit this with `q`)')) 22 | process.exit(1) 23 | } 24 | 25 | const dllManifest = require( 26 | join(buildPath, dllName) + '.json' 27 | ) 28 | 29 | module.exports = merge(base, { 30 | entry: { 31 | tests: resolve(rootDir, 'visual.js') 32 | }, 33 | output: { 34 | path: buildPath, 35 | filename: '[name].js', 36 | chunkFilename: '[id].js' 37 | }, 38 | module: { 39 | rules: [ 40 | { 41 | test: /.scss$/, 42 | loader: vueLoaders.scss, 43 | include: [ 44 | resolve(__dirname, '../node_modules/@material'), 45 | resolve(__dirname, '../src') 46 | ] 47 | } 48 | ] 49 | }, 50 | plugins: [ 51 | new webpack.DllReferencePlugin({ 52 | context: join(__dirname, '..'), 53 | manifest: dllManifest 54 | }), 55 | new HtmlWebpackPlugin({ 56 | chunkSortMode: 'dependency' 57 | }), 58 | new AddAssetHtmlPlugin({ 59 | filepath: require.resolve( 60 | join(buildPath, dllName) + '.dll.js' 61 | ) 62 | }), 63 | new webpack.optimize.CommonsChunkPlugin({ 64 | name: 'vendor', 65 | minChunks (module, count) { 66 | return ( 67 | module.resource && 68 | /\.js$/.test(module.resource) && 69 | module.resource.indexOf(join(__dirname, '../node_modules/')) === 0 70 | ) 71 | } 72 | }), 73 | new webpack.optimize.CommonsChunkPlugin({ 74 | name: 'manifest', 75 | chunks: ['vendor'] 76 | }), 77 | new DashboardPlugin(), 78 | new BundleAnalyzerPlugin({ 79 | analyzerMode: 'static', 80 | openAnalyzer: false, 81 | reportFilename: resolve(__dirname, `../reports/report.html`) 82 | }) 83 | ], 84 | devtool: '#eval-source-map', 85 | devServer: { 86 | inline: true, 87 | stats: { 88 | colors: true, 89 | chunks: false, 90 | cached: false 91 | }, 92 | contentBase: buildPath 93 | }, 94 | performance: { 95 | hints: false 96 | } 97 | }) 98 | -------------------------------------------------------------------------------- /build/webpack.config.dll.js: -------------------------------------------------------------------------------- 1 | const { resolve, join } = require('path') 2 | const webpack = require('webpack') 3 | const pkg = require('../package.json') 4 | 5 | const rootDir = resolve(__dirname, '../test') 6 | const buildPath = resolve(rootDir, 'dist') 7 | 8 | const entry = {} 9 | entry[pkg.dllPlugin.name] = pkg.dllPlugin.include 10 | 11 | module.exports = { 12 | devtool: '#source-map', 13 | entry, 14 | output: { 15 | path: buildPath, 16 | filename: '[name].dll.js', 17 | library: '[name]' 18 | }, 19 | plugins: [ 20 | new webpack.DllPlugin({ 21 | name: '[name]', 22 | path: join(buildPath, '[name].json') 23 | }) 24 | ], 25 | performance: { 26 | hints: false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /demo/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariomka/vue-datetime/1286b6c9d3b4dc37d64b529dcc9f5adda050cace/demo/demo.gif -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-datetime demo 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |

vue-datetime demo

17 | 18 |

Date

19 | 20 |
21 |
22 | 23 | 24 |
25 |

26 | Value: {{ date }} 27 |

28 |
29 |
30 |
31 |
<datetime v-model="date"></datetime>
32 |
33 |
34 | 35 |

Datetime

36 | 37 |
38 |
39 | 40 | 41 |
42 |

43 | Value: {{ datetime }} 44 |

45 |
46 |
47 |
48 |
<datetime type="datetime" v-model="datetime"></datetime>
49 |
50 |
51 | 52 |

12h datetime

53 | 54 |
55 |
56 | 57 | 58 |
59 |

60 | Value: {{ datetime12 }} 61 |

62 |
63 |
64 |
65 |
<datetime type="datetime" v-model="datetime12" use12-hour></datetime>
66 |
67 |
68 | 69 |

Time

70 |
71 |
72 | 73 |
74 |

75 | Value: {{ time }} 76 |

77 |
78 |
79 |
80 |
<datetime type="time" v-model="time"></datetime>
81 |
82 |
83 | 84 |

Macro tokens

85 |
86 |
87 | 88 | 89 |
90 |

91 | Value: {{ datetime13 }} 92 |

93 |
94 |
95 |
96 |
<datetime type="datetime" v-model="datetime13" format="yyyy-MM-dd HH:mm:ss"></datetime>
97 |
98 |
99 | 100 |

Complete demo

101 | 102 |
103 |
104 | 122 | 123 |
124 |

125 | Value: {{ datetimeEmpty }} 126 |

127 |
128 |
129 |
130 |
<datetime
131 |   type="datetime"
132 |   v-model="datetimeEmpty"
133 |   input-class="my-class"
134 |   value-zone="America/New_York"
135 |   zone="Asia/Shanghai"
136 |   :format="{ year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: '2-digit', timeZoneName: 'short' }"
137 |   :phrases="{ok: 'Continue', cancel: 'Exit'}"
138 |   :hour-step="2"
139 |   :minute-step="15"
140 |   :min-datetime="minDatetime"
141 |   :max-datetime="maxDatetime"
142 |   :week-start="7"
143 |   use12-hour
144 |   auto
145 |   ></datetime>
146 |
147 |
148 | 149 |

Theming

150 | 151 |
152 |
153 | 154 | 155 |
156 |

157 | Value: {{ datetimeTheming }} 158 |

159 |
160 |
161 |
162 |
<datetime type="datetime" v-model="datetimeTheming" class="theme-orange"></datetime>
163 | 164 |
.theme-orange .vdatetime-popup__header,
165 | .theme-orange .vdatetime-calendar__month__day--selected > span > span,
166 | .theme-orange .vdatetime-calendar__month__day--selected:hover > span > span {
167 |   background: #FF9800;
168 | }
169 | 
170 | .theme-orange .vdatetime-year-picker__item--selected,
171 | .theme-orange .vdatetime-time-picker__item--selected,
172 | .theme-orange .vdatetime-popup__actions__button {
173 |   color: #ff9800;
174 | }
175 |
176 |
177 | 178 | 182 |
183 | 184 | 185 | 186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /demo/src/app.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::after, 3 | *::before { 4 | box-sizing: border-box; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | body { 10 | background-color: #eee; 11 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 12 | font-size: 16px; 13 | padding: 1em; 14 | color: #444; 15 | } 16 | 17 | .page { 18 | background-color: #fff; 19 | margin: 0 auto; 20 | padding: 2em 3em; 21 | width: 100%; 22 | max-width: 900px; 23 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); 24 | } 25 | 26 | h1 { 27 | margin: 0 0 1em; 28 | font-weight: 200; 29 | font-size: 52px; 30 | 31 | & a { 32 | color: #333; 33 | text-decoration: none; 34 | } 35 | } 36 | 37 | h2 { 38 | margin: 0 0 1em; 39 | border-bottom: solid 1px #ddd; 40 | font-weight: 200; 41 | font-size: 26px; 42 | color: #666; 43 | } 44 | 45 | p.description { 46 | margin: 0 0 1em; 47 | font-style: italic; 48 | font-size: 14px; 49 | color: #aaa; 50 | } 51 | 52 | .example { 53 | margin: 0 0 2em; 54 | 55 | & .example-inputs input { 56 | padding: 8px 10px; 57 | font-size: 16px; 58 | border: solid 1px #ddd; 59 | color: #444; 60 | } 61 | 62 | & .example-code { 63 | & pre { 64 | margin-bottom: 15px; 65 | width: 100%; 66 | font-size: 14px; 67 | overflow: scroll; 68 | 69 | & code { 70 | padding: 1.5em; 71 | } 72 | } 73 | } 74 | 75 | @media screen and (min-width: 900px) { 76 | display: flex; 77 | 78 | & .example-inputs { 79 | width: 32%; 80 | } 81 | 82 | & .example-code { 83 | margin-top: 0; 84 | width: 68%; 85 | } 86 | } 87 | } 88 | 89 | .values { 90 | margin-top: 20px; 91 | font-size: 14px; 92 | } 93 | 94 | footer { 95 | margin: 50px 0 0; 96 | font-size: 14px; 97 | font-weight: lighter; 98 | color: #aaa; 99 | 100 | & a { 101 | color: #aaa; 102 | text-decoration: none; 103 | 104 | &:hover { 105 | text-decoration: underline; 106 | } 107 | } 108 | } 109 | 110 | .theme-orange { 111 | & .vdatetime-popup__header, 112 | & .vdatetime-calendar__month__day--selected > span > span, 113 | & .vdatetime-calendar__month__day--selected:hover > span > span { 114 | background: #ff9800; 115 | } 116 | 117 | & .vdatetime-year-picker__item--selected, 118 | & .vdatetime-time-picker__item--selected, 119 | & .vdatetime-popup__actions__button { 120 | color: #ff9800; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /demo/src/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { DateTime as LuxonDateTime } from 'luxon' 3 | import Datetime from '../../dist/vue-datetime' 4 | import '../../dist/vue-datetime.css' 5 | import './app.css' 6 | 7 | Vue.use(Datetime) 8 | 9 | new Vue({ 10 | el: '#app', 11 | 12 | data () { 13 | return { 14 | time: '19:06', 15 | date: '2018-05-12T00:00:00.000Z', 16 | datetime: '2018-05-12T17:19:06.151Z', 17 | datetime12: '2018-05-12T17:19:06.151Z', 18 | datetime13: '2018-05-12T17:19:06.151Z', 19 | datetimeEmpty: '', 20 | minDatetime: LuxonDateTime.local().minus({ month: 1, days: 3 }).toISO(), 21 | maxDatetime: LuxonDateTime.local().plus({ days: 3 }).toISO(), 22 | datetimeTheming: LuxonDateTime.local().toISO() 23 | } 24 | } 25 | }) 26 | 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-datetime", 3 | "version": "1.0.0-beta.13", 4 | "description": "Mobile friendly datetime picker for Vue. Supports date, datetime and time modes, i18n and disabling dates.", 5 | "keywords": [ 6 | "datetime", 7 | "datetime-picker", 8 | "picker", 9 | "date", 10 | "vue" 11 | ], 12 | "author": "Mario Juárez ", 13 | "main": "dist/vue-datetime.common.js", 14 | "module": "dist/vue-datetime.esm.js", 15 | "browser": "dist/vue-datetime.js", 16 | "unpkg": "dist/vue-datetime.js", 17 | "style": "dist/vue-datetime.css", 18 | "files": [ 19 | "dist", 20 | "src" 21 | ], 22 | "scripts": { 23 | "clean": "rimraf dist && rimraf build/dist", 24 | "build": "node build/build.js", 25 | "build:dll": "webpack --progress --config build/webpack.config.dll.js", 26 | "lint": "yon run lint:js && yon run lint:css", 27 | "lint:js": "eslint --ext js --ext vue src test/**/*.spec.js test/*.js build", 28 | "lint:js:fix": "yon run lint:js -- --fix", 29 | "lint:css": "stylelint src/**/*.{vue,css}", 30 | "lint:staged": "lint-staged", 31 | "pretest": "yon run lint", 32 | "test": "cross-env BABEL_ENV=test karma start test/karma.conf.js --single-run", 33 | "dev": "webpack-dashboard -- webpack-dev-server --config build/webpack.config.dev.js --open", 34 | "dev:coverage": "cross-env BABEL_ENV=test karma start test/karma.conf.js", 35 | "demo": "webpack-dev-server --config build/webpack.config.demo.js --open", 36 | "demo:build": "webpack --config build/webpack.config.demo.js", 37 | "demo:deploy": "gh-pages -d demo", 38 | "version": "auto-changelog -p --starting-commit 975d478b80b8e1ed3663f55b34c3a35521a43bdb --commit-limit false && git add CHANGELOG.md", 39 | "prepublish": "yon run build && yon run demo:build", 40 | "postpublish": "yon run demo:deploy" 41 | }, 42 | "lint-staged": { 43 | "*.{vue,js}": [ 44 | "eslint --fix" 45 | ], 46 | "*.{vue,css}": [ 47 | "stylefmt", 48 | "stylelint" 49 | ] 50 | }, 51 | "pre-commit": "lint:staged", 52 | "devDependencies": { 53 | "add-asset-html-webpack-plugin": "^2.0.0", 54 | "auto-changelog": "^1.3.0", 55 | "babel-core": "^6.24.0", 56 | "babel-eslint": "^7.2.0", 57 | "babel-loader": "^7.0.0", 58 | "babel-plugin-istanbul": "^4.1.0", 59 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 60 | "babel-plugin-transform-runtime": "^6.23.0", 61 | "babel-preset-env": "^1.4.0", 62 | "buble": "^0.15.2", 63 | "chai": "^3.5.0", 64 | "chai-dom": "^1.4.0", 65 | "clean-css": "^4.0.0", 66 | "cross-env": "^4.0.0", 67 | "css-loader": "^0.28.0", 68 | "eslint": "^3.19.0", 69 | "eslint-config-vue": "^2.0.0", 70 | "eslint-plugin-vue": "^2.0.0", 71 | "extract-text-webpack-plugin": "^2.1.0", 72 | "gh-pages": "^1.1.0", 73 | "html-webpack-plugin": "^2.28.0", 74 | "karma": "^1.7.0", 75 | "karma-chai-dom": "^1.1.0", 76 | "karma-chrome-launcher": "^2.1.0", 77 | "karma-coverage": "^1.1.0", 78 | "karma-mocha": "^1.3.0", 79 | "karma-sinon-chai": "^1.3.0", 80 | "karma-sourcemap-loader": "^0.3.7", 81 | "karma-spec-reporter": "^0.0.31", 82 | "karma-webpack": "^2.0.0", 83 | "lint-staged": "^3.4.0", 84 | "luxon": "^1.0.0", 85 | "mkdirp": "^0.5.1", 86 | "mocha": "^3.3.0", 87 | "mocha-css": "^1.0.1", 88 | "postcss": "^6.0.0", 89 | "postcss-cssnext": "^2.10.0", 90 | "postcss-loader": "^2.0.9", 91 | "pre-commit": "^1.2.0", 92 | "rimraf": "^2.6.0", 93 | "rollup": "^0.41.6", 94 | "rollup-plugin-buble": "^0.15.0", 95 | "rollup-plugin-commonjs": "^8.0.0", 96 | "rollup-plugin-node-resolve": "^3.0.0", 97 | "rollup-plugin-postcss": "^0.4.1", 98 | "rollup-plugin-replace": "^1.1.0", 99 | "rollup-plugin-vue": "^2.3.0", 100 | "sinon": "2.2.0", 101 | "sinon-chai": "^2.10.0", 102 | "style-loader": "^0.17.0", 103 | "stylefmt": "^5.3.0", 104 | "stylelint": "^7.10.0", 105 | "stylelint-config-standard": "^16.0.0", 106 | "stylelint-processor-html": "^1.0.0", 107 | "uglify-js": "^3.0.0", 108 | "uppercamelcase": "^3.0.0", 109 | "vue": "^2.3.0", 110 | "vue-loader": "^12.0.0", 111 | "vue-template-compiler": "^2.3.0", 112 | "webpack": "^2.5.0", 113 | "webpack-bundle-analyzer": "^2.4.0", 114 | "webpack-dashboard": "^0.4.0", 115 | "webpack-dev-server": "^2.4.0", 116 | "webpack-merge": "^4.0.0", 117 | "weekstart": "^1.0.0", 118 | "yarn-or-npm": "^2.0.0" 119 | }, 120 | "peerDependencies": { 121 | "luxon": "^1.0.0", 122 | "vue": "^2.3.0", 123 | "weekstart": "^1.0.0" 124 | }, 125 | "dllPlugin": { 126 | "name": "vuePluginTemplateDeps", 127 | "include": [ 128 | "mocha/mocha.js", 129 | "style-loader!css-loader!mocha-css", 130 | "html-entities", 131 | "vue/dist/vue.js", 132 | "chai", 133 | "core-js/library", 134 | "url", 135 | "sockjs-client", 136 | "vue-style-loader/lib/addStylesClient.js", 137 | "events", 138 | "ansi-html", 139 | "style-loader/addStyles.js" 140 | ] 141 | }, 142 | "repository": { 143 | "type": "git", 144 | "url": "git+https://github.com/mariomka/vue-datetime.git" 145 | }, 146 | "bugs": { 147 | "url": "https://github.com/mariomka/vue-datetime/issues" 148 | }, 149 | "homepage": "https://github.com/mariomka/vue-datetime#readme", 150 | "license": { 151 | "type": "MIT", 152 | "url": "http://www.opensource.org/licenses/mit-license.php" 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Datetime.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 261 | 262 | 284 | -------------------------------------------------------------------------------- /src/DatetimeCalendar.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 104 | 105 | 219 | -------------------------------------------------------------------------------- /src/DatetimeMonthPicker.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 69 | 70 | 132 | -------------------------------------------------------------------------------- /src/DatetimePopup.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 272 | 273 | 345 | -------------------------------------------------------------------------------- /src/DatetimeTimePicker.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 140 | 141 | 203 | -------------------------------------------------------------------------------- /src/DatetimeYearPicker.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 66 | 67 | 129 | -------------------------------------------------------------------------------- /src/FlowManager.js: -------------------------------------------------------------------------------- 1 | export default class FlowManager { 2 | constructor (flow = [], endStatus = null) { 3 | this.flow = flow 4 | this.endStatus = endStatus 5 | this.diversionNext = null 6 | } 7 | 8 | step (index) { 9 | return this.flow.length > index ? this.flow[index] : this.endStatus 10 | } 11 | 12 | first () { 13 | return this.step(0) 14 | } 15 | 16 | next (current) { 17 | if (this.diversionNext) { 18 | const next = this.diversionNext 19 | this.diversionNext = null 20 | 21 | return next 22 | } 23 | 24 | return this.step(this.flow.indexOf(current) + 1) 25 | } 26 | 27 | diversion (next) { 28 | this.diversionNext = next 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Datetime from './Datetime.vue' 2 | import DatetimePopup from './DatetimePopup.vue' 3 | 4 | function plugin (Vue) { 5 | Vue.component('datetime', Datetime) 6 | Vue.component('datetime-popup', DatetimePopup) 7 | } 8 | 9 | // Install by default if using the script tag 10 | if (typeof window !== 'undefined' && window.Vue) { 11 | window.Vue.use(plugin) 12 | } 13 | 14 | export default plugin 15 | const version = '__VERSION__' 16 | 17 | // Export all components too 18 | export { 19 | Datetime, 20 | DatetimePopup, 21 | version 22 | } 23 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | import { DateTime, Info, Settings } from 'luxon' 2 | import FlowManager from './FlowManager' 3 | 4 | export function capitalize (string) { 5 | return string.charAt(0).toUpperCase() + string.slice(1) 6 | } 7 | 8 | export function datetimeFromISO (string) { 9 | const datetime = DateTime.fromISO(string).toUTC() 10 | 11 | return datetime.isValid ? datetime : null 12 | } 13 | 14 | export function monthDays (year, month, weekStart) { 15 | const monthDate = DateTime.local(year, month, 1) 16 | let firstDay = monthDate.weekday - weekStart 17 | 18 | if (firstDay < 0) { 19 | firstDay += 7 20 | } 21 | let lastDay = (weekStart - monthDate.weekday - monthDate.daysInMonth) % 7 22 | if (lastDay < 0) { 23 | lastDay += 7 24 | } 25 | 26 | return Array.apply(null, Array(monthDate.daysInMonth + firstDay + lastDay)) 27 | .map((value, index) => 28 | (index + 1 <= firstDay || index >= firstDay + monthDate.daysInMonth) ? null : (index + 1 - firstDay) 29 | ) 30 | } 31 | 32 | export function monthDayIsDisabled (minDate, maxDate, year, month, day) { 33 | const date = DateTime.fromObject({ year, month, day, zone: 'UTC' }) 34 | 35 | minDate = minDate ? startOfDay(minDate.setZone('UTC', { keepLocalTime: true })) : null 36 | maxDate = maxDate ? startOfDay(maxDate.setZone('UTC', { keepLocalTime: true })) : null 37 | 38 | return (minDate && date < minDate) || 39 | (maxDate && date > maxDate) 40 | } 41 | 42 | export function monthIsDisabled (minDate, maxDate, year, month) { 43 | return (minDate && minDate > DateTime.utc(year, month, DateTime.utc(year, month).daysInMonth)) || 44 | (maxDate && maxDate < DateTime.utc(year, month, 1)) 45 | } 46 | 47 | export function yearIsDisabled (minDate, maxDate, year) { 48 | const minYear = minDate ? minDate.year : null 49 | const maxYear = maxDate ? maxDate.year : null 50 | 51 | return (minYear && year < minYear) || 52 | (maxYear && year > maxYear) 53 | } 54 | 55 | export function timeComponentIsDisabled (min, max, component) { 56 | return (min !== null && component < min) || 57 | (max !== null && component > max) 58 | } 59 | 60 | export function weekdays (weekStart) { 61 | if (--weekStart < 0) { 62 | weekStart = 6 63 | } 64 | 65 | let weekDays = Info.weekdays('short').map(weekday => capitalize(weekday)) 66 | 67 | weekDays = weekDays.concat(weekDays.splice(0, weekStart)) 68 | 69 | return weekDays 70 | } 71 | 72 | export function months () { 73 | return Info.months().map(month => capitalize(month)) 74 | } 75 | 76 | export function hours (step) { 77 | return Array.apply(null, Array(Math.ceil(24 / step))).map((item, index) => index * step) 78 | } 79 | 80 | export function minutes (step) { 81 | return Array.apply(null, Array(Math.ceil(60 / step))).map((item, index) => index * step) 82 | } 83 | 84 | export function years (current) { 85 | return Array.apply(null, Array(201)).map((item, index) => current - 100 + index) 86 | } 87 | 88 | export function pad (number) { 89 | return number < 10 ? '0' + number : number 90 | } 91 | 92 | export function startOfDay (datetime) { 93 | return datetime.startOf('day') 94 | } 95 | 96 | export function createFlowManager (flow) { 97 | return new FlowManager(flow, 'end') 98 | } 99 | 100 | export function createFlowManagerFromType (type) { 101 | let flow = [] 102 | 103 | switch (type) { 104 | case 'datetime': 105 | flow = ['date', 'time'] 106 | break 107 | case 'time': 108 | flow = ['time'] 109 | break 110 | default: 111 | flow = ['date'] 112 | } 113 | 114 | return new FlowManager(flow, 'end') 115 | } 116 | 117 | export function weekStart () { 118 | let weekstart 119 | 120 | try { 121 | weekstart = require('weekstart/package.json').version ? require('weekstart') : null 122 | } catch (e) { 123 | weekstart = window.weekstart 124 | } 125 | 126 | const firstDay = weekstart ? weekstart.getWeekStartByLocale(Settings.defaultLocale) : 1 127 | 128 | return firstDay === 0 ? 7 : firstDay 129 | } 130 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/helpers/Test.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 39 | 40 | 108 | -------------------------------------------------------------------------------- /test/helpers/index.js: -------------------------------------------------------------------------------- 1 | import { createVM, Vue } from './utils' 2 | import { nextTick } from './wait-for-update' 3 | 4 | export { 5 | createVM, 6 | Vue, 7 | nextTick 8 | } 9 | -------------------------------------------------------------------------------- /test/helpers/utils.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.js' 2 | import Test from './Test.vue' 3 | 4 | Vue.config.productionTip = false 5 | const isKarma = !!window.__karma__ 6 | 7 | export function createVM (context, template, opts = {}) { 8 | // Remove transitions 9 | const transition = { 10 | functional: true, 11 | render: (h, { data, children }) => h('div', data, children) 12 | } 13 | 14 | Vue.component('transition', transition) 15 | Vue.component('transition-group', transition) 16 | 17 | return isKarma 18 | ? createKarmaTest(context, template, opts) 19 | : createVisualTest(context, template, opts) 20 | } 21 | 22 | const emptyNodes = document.querySelectorAll('nonexistant') 23 | 24 | Vue.prototype.$$ = function $$ (selector) { 25 | const els = document.querySelectorAll(selector) 26 | const vmEls = this.$el.querySelectorAll(selector) 27 | const fn = vmEls.length 28 | ? el => Array.from(vmEls).find(vmEl => el === vmEl) 29 | : el => this.$el === el 30 | const found = Array.from(els).filter(fn) 31 | return found.length 32 | ? found 33 | : emptyNodes 34 | } 35 | 36 | Vue.prototype.$ = function $ (selector) { 37 | const els = document.querySelectorAll(selector) 38 | const vmEl = this.$el.querySelector(selector) 39 | const fn = vmEl 40 | ? el => el === vmEl 41 | : el => el === this.$el 42 | // Allow should chaining for tests 43 | return Array.from(els).find(fn) || emptyNodes 44 | } 45 | 46 | Vue.prototype.$findChild = function $ (selector) { 47 | return this.$children.find(child => child.$el.matches(selector)) || 48 | this.$children.reduce((found, child) => found || child.$findChild(selector), null) 49 | } 50 | 51 | export function createKarmaTest (context, template, opts) { 52 | const el = document.createElement('div') 53 | document.getElementById('tests').appendChild(el) 54 | const render = typeof template === 'string' 55 | ? { template: `
${template}
` } 56 | : { render: template } 57 | return new Vue({ 58 | el, 59 | name: 'Test', 60 | ...render, 61 | ...opts 62 | }) 63 | } 64 | 65 | export function createVisualTest (context, template, opts) { 66 | let vm 67 | if (typeof template === 'string') { 68 | opts.components = opts.components || {} 69 | // Let the user define a test component 70 | if (!opts.components.Test) { 71 | opts.components.Test = Test 72 | } 73 | vm = new Vue({ 74 | name: 'TestContainer', 75 | el: context.DOMElement, 76 | template: `${template}`, 77 | ...opts 78 | }) 79 | } else { 80 | // TODO allow redefinition of Test component 81 | vm = new Vue({ 82 | name: 'TestContainer', 83 | el: context.DOMElement, 84 | render (h) { 85 | return h(Test, { 86 | attrs: { 87 | id: context.DOMElement.id 88 | } 89 | // render the passed component with this scope 90 | }, [template.call(this, h)]) 91 | }, 92 | ...opts 93 | }) 94 | } 95 | 96 | context.DOMElement.vm = vm 97 | return vm 98 | } 99 | 100 | export { isKarma, Vue } 101 | -------------------------------------------------------------------------------- /test/helpers/wait-for-update.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.js' 2 | 3 | // Testing helper 4 | // nextTick().then(() => { 5 | // 6 | // Automatically waits for nextTick 7 | // }).then(() => { 8 | // return a promise or value to skip the wait 9 | // }) 10 | function nextTick () { 11 | const jobs = [] 12 | let done 13 | 14 | const chainer = { 15 | then (cb) { 16 | jobs.push(cb) 17 | return chainer 18 | } 19 | } 20 | 21 | function shift (...args) { 22 | const job = jobs.shift() 23 | let result 24 | try { 25 | result = job(...args) 26 | } catch (e) { 27 | jobs.length = 0 28 | done(e) 29 | } 30 | 31 | // wait for nextTick 32 | if (result !== undefined) { 33 | if (result.then) { 34 | result.then(shift) 35 | } else { 36 | shift(result) 37 | } 38 | } else if (jobs.length) { 39 | requestAnimationFrame(() => Vue.nextTick(shift)) 40 | } 41 | } 42 | 43 | // First time 44 | Vue.nextTick(() => { 45 | done = jobs[jobs.length - 1] 46 | if (done.toString().slice(0, 14) !== 'function (err)') { 47 | throw new Error('waitForUpdate chain is missing .then(done)') 48 | } 49 | shift() 50 | }) 51 | 52 | return chainer 53 | } 54 | 55 | exports.nextTick = nextTick 56 | exports.delay = time => new Promise(resolve => setTimeout(resolve, time)) 57 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // Polyfill fn.bind() for PhantomJS 2 | import bind from 'function-bind' 3 | // Polyfill Object.assign for PhantomJS 4 | import objectAssign from 'object-assign' 5 | 6 | /* eslint-disable no-extend-native */ 7 | Function.prototype.bind = bind 8 | 9 | Object.assign = objectAssign 10 | 11 | // require all src files for coverage. 12 | // you can also change this to match only the subset of files that 13 | // you want coverage for. 14 | const srcContext = require.context('../src', true, /^\.\/(?!index(\.js)?$)/) 15 | srcContext.keys().forEach(srcContext) 16 | 17 | // Set up 18 | require('./setup') 19 | 20 | // Use a div to insert elements 21 | before(function () { 22 | const el = document.createElement('DIV') 23 | el.id = 'tests' 24 | document.body.appendChild(el) 25 | }) 26 | 27 | // Remove every test html scenario 28 | afterEach(function () { 29 | const el = document.getElementById('tests') 30 | for (let i = 0; i < el.children.length; ++i) { 31 | el.removeChild(el.children[i]) 32 | } 33 | }) 34 | 35 | const specsContext = require.context('./specs', true) 36 | specsContext.keys().forEach(specsContext) 37 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | const baseConfig = require('../build/webpack.config.dev.js') 3 | 4 | const webpackConfig = merge(baseConfig, { 5 | // use inline sourcemap for karma-sourcemap-loader 6 | devtool: '#inline-source-map' 7 | }) 8 | 9 | webpackConfig.plugins = [] 10 | 11 | const vueRule = webpackConfig.module.rules.find(rule => rule.loader === 'vue-loader') 12 | vueRule.options = vueRule.options || {} 13 | vueRule.options.loaders = vueRule.options.loaders || {} 14 | vueRule.options.loaders.js = 'babel-loader' 15 | 16 | // no need for app entry during tests 17 | delete webpackConfig.entry 18 | 19 | module.exports = function (config) { 20 | config.set({ 21 | // to run in additional browsers: 22 | // 1. install corresponding karma launcher 23 | // http://karma-runner.github.io/0.13/config/browsers.html 24 | // 2. add it to the `browsers` array below. 25 | browsers: ['Chrome'], 26 | frameworks: ['mocha', 'chai-dom', 'sinon-chai'], 27 | reporters: ['spec', 'coverage'], 28 | files: ['./index.js'], 29 | preprocessors: { 30 | './index.js': ['webpack', 'sourcemap'] 31 | }, 32 | webpack: webpackConfig, 33 | webpackMiddleware: { 34 | noInfo: true 35 | }, 36 | coverageReporter: { 37 | dir: './coverage', 38 | reporters: [ 39 | { type: 'lcov', subdir: '.' }, 40 | { type: 'text-summary' } 41 | ] 42 | } 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import { Settings as LuxonSettings } from 'luxon' 2 | 3 | beforeEach(function () { 4 | LuxonSettings.defaultLocale = 'en' 5 | }) 6 | -------------------------------------------------------------------------------- /test/specs/Datetime.spec.js: -------------------------------------------------------------------------------- 1 | import { DateTime as LuxonDateTime, Settings as LuxonSettings } from 'luxon' 2 | import Datetime from 'src/Datetime.vue' 3 | import { createVM } from '../helpers/utils.js' 4 | 5 | describe('Datetime.vue', function () { 6 | describe('render', function () { 7 | it('should add class to wrapper', function () { 8 | const vm = createVM(this, 9 | ``, 10 | { 11 | components: { Datetime } 12 | }) 13 | 14 | expect(vm.$('.vdatetime')).to.have.class('class-name') 15 | }) 16 | 17 | it('should print an input text', function () { 18 | const vm = createVM(this, 19 | ``, 20 | { 21 | components: { Datetime } 22 | }) 23 | 24 | expect(vm.$('.vdatetime-input')).to.match('input') 25 | }) 26 | 27 | it('should add class to input', function () { 28 | const vm = createVM(this, 29 | ``, 30 | { 31 | components: { Datetime } 32 | }) 33 | 34 | expect(vm.$('.vdatetime-input')).to.have.class('class-name') 35 | }) 36 | 37 | it('should add id to input', function () { 38 | const vm = createVM(this, 39 | ``, 40 | { 41 | components: { Datetime } 42 | }) 43 | 44 | expect(vm.$('.vdatetime-input')).to.have.id('id-name') 45 | }) 46 | 47 | it('should not add empty id to input', function () { 48 | const vm = createVM(this, 49 | ``, 50 | { 51 | components: { Datetime } 52 | }) 53 | 54 | expect(vm.$('.vdatetime-input')).to.not.have.attribute('id') 55 | }) 56 | 57 | it('should add style to input', function () { 58 | const vm = createVM(this, 59 | ``, 60 | { 61 | components: { Datetime } 62 | }) 63 | 64 | expect(vm.$('.vdatetime-input')).to.have.attr('style', 'background-color: cyan;') 65 | }) 66 | 67 | it('input should inherit attributes', function () { 68 | const vm = createVM(this, 69 | ``, 70 | { 71 | components: { Datetime } 72 | }) 73 | 74 | expect(vm.$('.vdatetime-input')).to.have.attr('placeholder', 'Select date...') 75 | }) 76 | 77 | it('input should inherit events', function () { 78 | const vm = createVM(this, 79 | ``, 80 | { 81 | components: { Datetime }, 82 | data () { 83 | return { 84 | spy: sinon.spy() 85 | } 86 | } 87 | }) 88 | 89 | expect(vm.spy).to.have.not.been.called 90 | vm.$('.vdatetime-input').click() 91 | expect(vm.spy).to.have.been.calledOnce 92 | }) 93 | 94 | it('should not create hidden input by default', function () { 95 | const vm = createVM(this, 96 | ``, 97 | { 98 | components: { Datetime } 99 | }) 100 | 101 | expect(vm.$('.vdatetime input[type=hidden]')).to.not.exist 102 | }) 103 | 104 | it('should create hidden input if name is passed', function () { 105 | const vm = createVM(this, 106 | ``, 107 | { 108 | components: { Datetime } 109 | }) 110 | 111 | expect(vm.$('.vdatetime input[type=hidden]')).to.have.attr('name', 'dt') 112 | }) 113 | 114 | it('should support named slots', function () { 115 | const vm = createVM(this, 116 | ` 117 | 118 | Invalid date 119 | `, 120 | { 121 | components: { Datetime } 122 | }) 123 | 124 | const children = vm.$('.vdatetime').children 125 | expect(children[0].nodeName).to.equal('LABEL') 126 | expect(children[1].nodeName).to.equal('INPUT') 127 | expect(children[2].nodeName).to.equal('SPAN') 128 | }) 129 | }) 130 | 131 | describe('pass props', function () { 132 | it('should pass phrases to popup', function (done) { 133 | const vm = createVM(this, 134 | ``, 135 | { 136 | components: { Datetime }, 137 | data () { 138 | return { 139 | phrases: { 140 | cancel: 'Cancelar', 141 | ok: 'Confirmar' 142 | } 143 | } 144 | } 145 | }) 146 | 147 | vm.$('.vdatetime-input').click() 148 | 149 | vm.$nextTick(() => { 150 | expect(vm.$findChild('.vdatetime-popup').phrases).to.be.eql({ 151 | cancel: 'Cancelar', 152 | ok: 'Confirmar' 153 | }) 154 | done() 155 | }) 156 | }) 157 | 158 | it('should render the action buttons with custom slots', function (done) { 159 | const vm = createVM(this, 160 | ` 161 | 162 | 163 | `, 164 | { 165 | components: { Datetime }, 166 | data () { 167 | return {} 168 | } 169 | }) 170 | 171 | vm.$('.vdatetime-input').click() 172 | 173 | vm.$nextTick(() => { 174 | expect(vm.$('.vdatetime-popup__actions')).to.exist 175 | expect(vm.$$('.vdatetime-popup__actions__button')[0]).to.have.html('Abort') 176 | expect(vm.$$('.vdatetime-popup__actions__button')[1]).to.have.html('Confirm') 177 | done() 178 | }) 179 | }) 180 | 181 | it('should render the action buttons with custom slots and scoped slot for using the local scope', function (done) { 182 | const vm = createVM(this, 183 | ` 184 | 185 | 189 | `, 190 | { 191 | components: { Datetime }, 192 | data () { 193 | return {} 194 | } 195 | }) 196 | 197 | vm.$('.vdatetime-input').click() 198 | 199 | vm.$nextTick(() => { 200 | const btnCancel = vm.$$('.vdatetime-popup__actions__button')[0] 201 | const btnConfirm = vm.$$('.vdatetime-popup__actions__button')[1] 202 | 203 | expect(vm.$('.vdatetime-popup__actions')).to.exist 204 | expect(btnCancel).to.exist 205 | expect(btnCancel).to.have.html('Abort') 206 | expect(btnConfirm).to.exist 207 | expect(btnConfirm).to.have.text('Next') 208 | 209 | btnConfirm.click() 210 | 211 | vm.$nextTick(() => { 212 | expect(btnConfirm).to.have.text('Publish') 213 | done() 214 | }) 215 | }) 216 | }) 217 | 218 | it('should pass use 12 hour to popup', function (done) { 219 | const vm = createVM(this, 220 | ``, 221 | { 222 | components: { Datetime } 223 | }) 224 | 225 | vm.$('.vdatetime-input').click() 226 | 227 | vm.$nextTick(() => { 228 | expect(vm.$findChild('.vdatetime-popup').use12Hour).to.be.equal(true) 229 | done() 230 | }) 231 | }) 232 | 233 | it('should pass time steps to popup', function (done) { 234 | const vm = createVM(this, 235 | ``, 236 | { 237 | components: { Datetime } 238 | }) 239 | 240 | vm.$('.vdatetime-input').click() 241 | 242 | vm.$nextTick(() => { 243 | expect(vm.$findChild('.vdatetime-popup').hourStep).to.be.equal(2) 244 | expect(vm.$findChild('.vdatetime-popup').minuteStep).to.be.equal(15) 245 | done() 246 | }) 247 | }) 248 | 249 | it('should pass min and max datetimes to popup', function (done) { 250 | const vm = createVM(this, 251 | ``, 252 | { 253 | components: { Datetime } 254 | }) 255 | 256 | vm.$('.vdatetime-input').click() 257 | 258 | vm.$nextTick(() => { 259 | expect(vm.$findChild('.vdatetime-popup').minDatetime.toUTC().toISO()).to.be.equal('2018-01-01T12:35:22.000Z') 260 | expect(vm.$findChild('.vdatetime-popup').maxDatetime.toUTC().toISO()).to.be.equal('2018-01-03T20:43:13.000Z') 261 | done() 262 | }) 263 | }) 264 | 265 | it('should pass auto to popup', function (done) { 266 | const vm = createVM(this, 267 | ``, 268 | { 269 | components: { Datetime } 270 | }) 271 | 272 | vm.$('.vdatetime-input').click() 273 | 274 | vm.$nextTick(() => { 275 | expect(vm.$findChild('.vdatetime-popup').auto).to.be.equal(true) 276 | done() 277 | }) 278 | }) 279 | 280 | it('should pass week start to popup', function (done) { 281 | const vm = createVM(this, 282 | ``, 283 | { 284 | components: { Datetime } 285 | }) 286 | 287 | vm.$('.vdatetime-input').click() 288 | 289 | vm.$nextTick(() => { 290 | expect(vm.$findChild('.vdatetime-popup').weekStart).to.be.equal(3) 291 | done() 292 | }) 293 | }) 294 | 295 | it('should pass datetime to popup', function (done) { 296 | const vm = createVM(this, 297 | ``, 298 | { 299 | components: { Datetime }, 300 | data () { 301 | return { 302 | datetime: '2017-12-07T19:34:54.078+03:00' 303 | } 304 | } 305 | }) 306 | 307 | vm.$('.vdatetime-input').click() 308 | 309 | vm.$nextTick(() => { 310 | expect(vm.$findChild('.vdatetime-popup').datetime.toISO()).to.be.equal('2017-12-07T19:34:54.078+03:00') 311 | done() 312 | }) 313 | }) 314 | 315 | it('should pass flow to popup', function (done) { 316 | const vm = createVM(this, 317 | ``, 318 | { 319 | components: { Datetime } 320 | }) 321 | 322 | vm.$('.vdatetime-input').click() 323 | 324 | vm.$nextTick(() => { 325 | expect(vm.$findChild('.vdatetime-popup').flow).to.be.deep.equal(['year', 'month', 'date', 'time']) 326 | done() 327 | }) 328 | }) 329 | 330 | it('should pass title to popup', function (done) { 331 | const vm = createVM(this, 332 | ``, 333 | { 334 | components: { Datetime } 335 | }) 336 | 337 | vm.$('.vdatetime-input').click() 338 | 339 | vm.$nextTick(() => { 340 | expect(vm.$findChild('.vdatetime-popup').title).to.be.equal('Select your birthday') 341 | done() 342 | }) 343 | }) 344 | }) 345 | 346 | describe('types', function () { 347 | it('should be date type by default', function (done) { 348 | const vm = createVM(this, 349 | ``, 350 | { 351 | components: { Datetime } 352 | }) 353 | 354 | vm.$('.vdatetime-input').click() 355 | 356 | vm.$nextTick(() => { 357 | expect(vm.$('.vdatetime-calendar')).to.exist 358 | done() 359 | }) 360 | }) 361 | 362 | it('should be date type', function (done) { 363 | const vm = createVM(this, 364 | ``, 365 | { 366 | components: { Datetime } 367 | }) 368 | 369 | vm.$('.vdatetime-input').click() 370 | 371 | vm.$nextTick(() => { 372 | expect(vm.$('.vdatetime-calendar')).to.exist 373 | done() 374 | }) 375 | }) 376 | 377 | it('should be datetime type', function (done) { 378 | const vm = createVM(this, 379 | ``, 380 | { 381 | components: { Datetime } 382 | }) 383 | 384 | vm.$('.vdatetime-input').click() 385 | 386 | vm.$nextTick(() => { 387 | vm.$('.vdatetime-popup__actions__button--confirm').click() 388 | vm.$nextTick(() => { 389 | expect(vm.$('.vdatetime-time-picker')).to.exist 390 | done() 391 | }) 392 | }) 393 | }) 394 | 395 | it('should be a time type', function (done) { 396 | const vm = createVM(this, 397 | ``, 398 | { 399 | components: { Datetime } 400 | }) 401 | 402 | vm.$('.vdatetime-input').click() 403 | 404 | vm.$nextTick(() => { 405 | expect(vm.$('.vdatetime-time-picker')).to.exist 406 | done() 407 | }) 408 | }) 409 | }) 410 | 411 | describe('value', function () { 412 | it('should be empty string when value is empty', function () { 413 | const vm = createVM(this, 414 | ``, 415 | { 416 | components: { Datetime }, 417 | data () { 418 | return { 419 | datetime: '' 420 | } 421 | } 422 | }) 423 | 424 | expect(vm.datetime).to.be.equal('') 425 | }) 426 | 427 | it('should be empty string when value is not valid', function () { 428 | const vm = createVM(this, 429 | ``, 430 | { 431 | components: { Datetime }, 432 | data () { 433 | return { 434 | datetime: '2017-32-32T19:34:54.078Z' 435 | } 436 | } 437 | }) 438 | 439 | expect(vm.datetime).to.be.equal('') 440 | }) 441 | 442 | it('should be a UTC date by default', function () { 443 | const vm = createVM(this, 444 | ``, 445 | { 446 | components: { Datetime }, 447 | data () { 448 | return { 449 | datetime: '2017-12-07T19:34:54.078+03:00' 450 | } 451 | } 452 | }) 453 | 454 | expect(vm.datetime).to.be.equal('2017-12-07T16:34:54.078Z') 455 | }) 456 | 457 | it('should be a date in the specified time zone', function () { 458 | const vm = createVM(this, 459 | ``, 460 | { 461 | components: { Datetime }, 462 | data () { 463 | return { 464 | datetime: '2017-12-07T19:34:54.078+03:00' 465 | } 466 | } 467 | }) 468 | 469 | expect(vm.datetime).to.be.equal('2017-12-07T12:34:54.078-04:00') 470 | }) 471 | 472 | it('should be a date with cleared time when type is date', function () { 473 | const vm = createVM(this, 474 | ``, 475 | { 476 | components: { Datetime }, 477 | data () { 478 | return { 479 | datetime: '2017-12-07T19:34:54.078Z' 480 | } 481 | } 482 | }) 483 | 484 | expect(vm.datetime).to.be.equal('2017-12-07T00:00:00.000Z') 485 | }) 486 | 487 | it('should be a date with cleared time in the specified time zone when type is date', function () { 488 | const vm = createVM(this, 489 | ``, 490 | { 491 | components: { Datetime }, 492 | data () { 493 | return { 494 | datetime: '2017-12-07T22:34:54.078Z' 495 | } 496 | } 497 | }) 498 | 499 | expect(vm.datetime).to.be.equal('2017-12-08T00:00:00.000+03:00') 500 | }) 501 | 502 | it('should be a time with the specified time zone', function (done) { 503 | const vm = createVM(this, 504 | ``, 505 | { 506 | components: { Datetime }, 507 | data () { 508 | return { 509 | datetime: '2017-12-07T09:00:00.000Z' 510 | } 511 | } 512 | }) 513 | 514 | vm.$nextTick(() => { 515 | expect(vm.$('.vdatetime-input').value).to.be.equal('06:00') 516 | done() 517 | }) 518 | }) 519 | 520 | it('should be a time in the local time zone on default', function (done) { 521 | const vm = createVM(this, 522 | ``, 523 | { 524 | components: { Datetime }, 525 | data () { 526 | return { 527 | datetime: '2017-12-07T09:00:00.000Z' 528 | } 529 | } 530 | }) 531 | 532 | vm.$nextTick(() => { 533 | const time = LuxonDateTime.fromISO('2017-12-07T09:00:00.000Z').toUTC().setZone('local').toLocaleString(LuxonDateTime.TIME_24_SIMPLE) 534 | 535 | expect(vm.$('.vdatetime-input').value).to.be.equal(time) 536 | done() 537 | }) 538 | }) 539 | 540 | it('should be a time converted to utc', function () { 541 | const vm = createVM(this, 542 | ``, 543 | { 544 | components: { Datetime }, 545 | data () { 546 | return { 547 | datetime: '2017-12-05T00:00:00.000Z' 548 | } 549 | } 550 | }) 551 | 552 | expect(vm.datetime).to.be.equal('2017-12-04T19:00:00.000-05:00') 553 | }) 554 | }) 555 | 556 | describe('input value', function () { 557 | it('should be empty when value is empty', function () { 558 | const vm = createVM(this, 559 | ``, 560 | { 561 | components: { Datetime }, 562 | data () { 563 | return { 564 | datetime: '' 565 | } 566 | } 567 | }) 568 | 569 | expect(vm.$('.vdatetime-input').value).to.be.empty 570 | }) 571 | 572 | it('should be empty when value is not valid', function () { 573 | const vm = createVM(this, 574 | ``, 575 | { 576 | components: { Datetime }, 577 | data () { 578 | return { 579 | datetime: '2017-12-32T19:34:54.078Z' 580 | } 581 | } 582 | }) 583 | 584 | expect(vm.$('.vdatetime-input').value).to.be.empty 585 | }) 586 | 587 | it('should be a date in the local time zone by default', function () { 588 | const vm = createVM(this, 589 | ``, 590 | { 591 | components: { Datetime }, 592 | data () { 593 | return { 594 | datetime: '2017-12-07T19:34:54.078+03:00', 595 | format: LuxonDateTime.DATETIME_MED 596 | } 597 | } 598 | }) 599 | 600 | const localDateString = LuxonDateTime.fromISO('2017-12-07T19:34:54.078+03:00').toLocal().toLocaleString(LuxonDateTime.DATETIME_MED) 601 | 602 | expect(vm.$('.vdatetime-input').value).to.be.equal(localDateString) 603 | }) 604 | 605 | it('should be a date in the specified time zone', function () { 606 | const vm = createVM(this, 607 | ``, 608 | { 609 | components: { Datetime }, 610 | data () { 611 | return { 612 | datetime: '2017-12-07T19:34:54.078+03:00' 613 | } 614 | } 615 | }) 616 | 617 | expect(vm.$('.vdatetime-input').value).to.be.equal('Dec 7, 2017, 8:34 PM') 618 | }) 619 | 620 | it('should be formatted in the specified format', function () { 621 | const vm = createVM(this, 622 | ``, 623 | { 624 | components: { Datetime }, 625 | data () { 626 | return { 627 | datetime: '2017-12-07T19:34:54.078+03:00', 628 | format: LuxonDateTime.DATE_HUGE 629 | } 630 | } 631 | }) 632 | 633 | expect(vm.$('.vdatetime-input').value).to.be.equal('Thursday, December 7, 2017') 634 | }) 635 | 636 | it('should be formatted in the specified macro format', function () { 637 | const vm = createVM(this, 638 | ``, 639 | { 640 | components: { Datetime }, 641 | data () { 642 | return { 643 | datetime: '2017-12-07T19:34:54.078+03:00', 644 | format: 'yyyy-MM-dd HH:mm:ss' 645 | } 646 | } 647 | }) 648 | 649 | expect(vm.$('.vdatetime-input').value).to.be.equal('2017-12-07 19:34:54') 650 | }) 651 | 652 | it('should be formatted in the specified format (time)', function () { 653 | const vm = createVM(this, 654 | ``, 655 | { 656 | components: { Datetime }, 657 | data () { 658 | return { 659 | datetime: '2017-12-07T19:34:54.078+03:00', 660 | format: LuxonDateTime.TIME_24_WITH_SECONDS 661 | } 662 | } 663 | }) 664 | 665 | expect(vm.$('.vdatetime-input').value).to.be.equal('19:34:54') 666 | }) 667 | 668 | it('should be formatted in the specified macro format (time)', function () { 669 | const vm = createVM(this, 670 | ``, 671 | { 672 | components: { Datetime }, 673 | data () { 674 | return { 675 | datetime: '2017-12-07T19:34:54.078+03:00', 676 | format: 'HH:mm:ss' 677 | } 678 | } 679 | }) 680 | 681 | expect(vm.$('.vdatetime-input').value).to.be.equal('19:34:54') 682 | }) 683 | 684 | it('should be updated if value is updated', function (done) { 685 | const vm = createVM(this, 686 | ``, 687 | { 688 | components: { Datetime }, 689 | data () { 690 | return { 691 | datetime: '2017-12-05T00:00:00.000Z' 692 | } 693 | }, 694 | mounted () { 695 | setTimeout(() => { 696 | this.datetime = '2017-12-07T00:00:00.000Z' 697 | }, 50) 698 | } 699 | }) 700 | 701 | setTimeout(() => { 702 | expect(vm.$('.vdatetime-input').value).to.be.equal('Dec 7, 2017') 703 | done() 704 | }, 50) 705 | }) 706 | }) 707 | 708 | describe('hidden input value', function () { 709 | it('should be empty when value is empty', function () { 710 | const vm = createVM(this, 711 | ``, 712 | { 713 | components: { Datetime }, 714 | data () { 715 | return { 716 | datetime: '' 717 | } 718 | } 719 | }) 720 | 721 | expect(vm.$('.vdatetime input[name=dt]').value).to.be.empty 722 | }) 723 | 724 | it('should be equal to value', function () { 725 | const datetime = '2017-12-31T19:34:54.078Z' 726 | const vm = createVM(this, 727 | ``, 728 | { 729 | components: { Datetime }, 730 | data () { 731 | return { datetime } 732 | } 733 | }) 734 | 735 | expect(vm.$('.vdatetime input[name=dt]').value).to.equal(datetime) 736 | }) 737 | 738 | it('should be equal to value even when value is not valid', function () { 739 | const datetime = '2017-12-32T19:34:54.078Z' 740 | const vm = createVM(this, 741 | ``, 742 | { 743 | components: { Datetime }, 744 | data () { 745 | return { datetime } 746 | } 747 | }) 748 | 749 | expect(vm.$('.vdatetime input[name=dt]').value).to.equal(datetime) 750 | }) 751 | 752 | it('should be updated if value is updated', function (done) { 753 | const datetime1 = '2017-12-05T00:00:00.000Z' 754 | const datetime2 = '2017-12-07T00:00:00.000Z' 755 | const vm = createVM(this, 756 | ``, 757 | { 758 | components: { Datetime }, 759 | data () { 760 | return { 761 | datetime: datetime1 762 | } 763 | }, 764 | mounted () { 765 | setTimeout(() => { 766 | this.datetime = datetime2 767 | }, 50) 768 | } 769 | }) 770 | 771 | setTimeout(() => { 772 | expect(vm.$('.vdatetime input[type=hidden]').value).to.be.equal(datetime2) 773 | done() 774 | }, 50) 775 | }) 776 | }) 777 | 778 | describe('week start', function () { 779 | it('should be set number', function () { 780 | const vm = createVM(this, 781 | ``, 782 | { 783 | components: { Datetime }, 784 | data () { 785 | return { 786 | datetime: '' 787 | } 788 | } 789 | }) 790 | 791 | expect(vm.$refs['datetime'].weekStart).to.be.equal(4) 792 | }) 793 | 794 | it('should be auto from locale', function () { 795 | LuxonSettings.defaultLocale = 'es-ES' 796 | 797 | const vm = createVM(this, 798 | ``, 799 | { 800 | components: { Datetime }, 801 | data () { 802 | return { 803 | datetime: '' 804 | } 805 | } 806 | }) 807 | 808 | expect(vm.$refs['datetime'].weekStart).to.be.equal(1) 809 | }) 810 | }) 811 | 812 | describe('popup', function () { 813 | it('should open when clicking the input', function (done) { 814 | const vm = createVM(this, 815 | ``, 816 | { 817 | components: { Datetime } 818 | }) 819 | 820 | vm.$('.vdatetime-input').click() 821 | 822 | vm.$nextTick(() => { 823 | expect(vm.$('.vdatetime-overlay')).to.exist 824 | expect(vm.$('.vdatetime-popup')).to.exist 825 | done() 826 | }) 827 | }) 828 | 829 | it('should open when focusing the input', function (done) { 830 | const vm = createVM(this, 831 | ``, 832 | { 833 | components: { Datetime } 834 | }) 835 | 836 | vm.$('.vdatetime-input').focus() 837 | 838 | vm.$nextTick(() => { 839 | expect(vm.$('.vdatetime-overlay')).to.exist 840 | expect(vm.$('.vdatetime-popup')).to.exist 841 | done() 842 | }) 843 | }) 844 | 845 | it('should blur input when open popup', function (done) { 846 | const vm = createVM(this, 847 | ``, 848 | { 849 | components: { Datetime } 850 | }) 851 | 852 | vm.$('.vdatetime-input').focus() 853 | 854 | setTimeout(() => { 855 | expect(document.activeElement.isEqualNode(vm.$('.vdatetime-input'))).to.be.false 856 | done() 857 | }, 50) 858 | }) 859 | 860 | it('should close when clicking the overlay', function (done) { 861 | const vm = createVM(this, 862 | ``, 863 | { 864 | components: { Datetime } 865 | }) 866 | 867 | vm.$('.vdatetime-input').click() 868 | 869 | vm.$nextTick(() => { 870 | vm.$('.vdatetime-overlay').click() 871 | vm.$nextTick(() => { 872 | expect(vm.$('.vdatetime-overlay')).to.not.exist 873 | expect(vm.$('.vdatetime-popup')).to.not.exist 874 | done() 875 | }) 876 | }) 877 | }) 878 | 879 | it('should not close when clicking the overlay', function (done) { 880 | const vm = createVM(this, 881 | ``, 882 | { 883 | components: { Datetime } 884 | }) 885 | 886 | vm.$('.vdatetime-input').click() 887 | 888 | vm.$nextTick(() => { 889 | vm.$('.vdatetime-overlay').click() 890 | vm.$nextTick(() => { 891 | expect(vm.$('.vdatetime-overlay')).to.exist 892 | expect(vm.$('.vdatetime-popup')).to.exist 893 | done() 894 | }) 895 | }) 896 | }) 897 | 898 | it('should not render overlay', function (done) { 899 | const vm = createVM(this, 900 | ``, 901 | { 902 | components: { Datetime } 903 | }) 904 | 905 | vm.$('.vdatetime-input').click() 906 | 907 | vm.$nextTick(() => { 908 | expect(vm.$('.vdatetime-overlay')).to.not.exist 909 | done() 910 | }) 911 | }) 912 | }) 913 | 914 | describe('events', function () { 915 | it('should update the date and close popup on confirm', function (done) { 916 | const vm = createVM(this, 917 | ``, 918 | { 919 | components: { Datetime }, 920 | data () { 921 | return { 922 | datetime: '2020-05-07T14:32:00.000Z' 923 | } 924 | } 925 | }) 926 | 927 | vm.$('.vdatetime-input').click() 928 | 929 | vm.$nextTick(() => { 930 | vm.$$('.vdatetime-calendar__month__day')[24].click() 931 | vm.$('.vdatetime-popup__actions__button--confirm').click() 932 | 933 | vm.$nextTick(() => { 934 | expect(vm.datetime).to.be.equal('2020-05-20T00:00:00.000Z') 935 | expect(vm.$('.vdatetime-input').value).to.be.equal('May 20, 2020') 936 | done() 937 | }) 938 | }) 939 | }) 940 | 941 | it('should update the datetime and close popup on confirm', function (done) { 942 | const vm = createVM(this, 943 | ``, 944 | { 945 | components: { Datetime }, 946 | data () { 947 | return { 948 | datetime: '2020-05-07T14:32:00.000Z' 949 | } 950 | } 951 | }) 952 | 953 | vm.$('.vdatetime-input').click() 954 | 955 | vm.$nextTick(() => { 956 | vm.$$('.vdatetime-calendar__month__day')[24].click() 957 | vm.$('.vdatetime-popup__actions__button--confirm').click() 958 | 959 | vm.$nextTick(() => { 960 | vm.$$('.vdatetime-time-picker__list--hours .vdatetime-time-picker__item')[12].click() 961 | vm.$('.vdatetime-popup__actions__button--confirm').click() 962 | 963 | vm.$nextTick(() => { 964 | expect(vm.datetime).to.be.equal('2020-05-20T08:32:00.000-02:00') 965 | expect(vm.$('.vdatetime-input').value).to.be.equal('May 20, 2020, 12:32 PM') 966 | expect(vm.$('.vdatetime-overlay')).to.not.exist 967 | expect(vm.$('.vdatetime-popup')).to.not.exist 968 | done() 969 | }) 970 | }) 971 | }) 972 | }) 973 | 974 | it('should not update value and close popup on cancel', function (done) { 975 | const vm = createVM(this, 976 | ``, 977 | { 978 | components: { Datetime }, 979 | data () { 980 | return { 981 | datetime: '2020-05-07T00:00:00.000Z' 982 | } 983 | } 984 | }) 985 | 986 | vm.$('.vdatetime-input').click() 987 | 988 | vm.$nextTick(() => { 989 | vm.$$('.vdatetime-calendar__month__day')[21].click() 990 | vm.$('.vdatetime-popup__actions__button--cancel').click() 991 | vm.$nextTick(() => { 992 | expect(vm.$('.vdatetime-input').value).to.be.equal('May 7, 2020') 993 | expect(vm.datetime).to.be.equal('2020-05-07T00:00:00.000Z') 994 | expect(vm.$('.vdatetime-overlay')).to.not.exist 995 | expect(vm.$('.vdatetime-popup')).to.not.exist 996 | done() 997 | }) 998 | }) 999 | }) 1000 | 1001 | it('should emit close when popup closes', function (done) { 1002 | const vm = createVM(this, 1003 | ``, 1004 | { 1005 | components: { Datetime }, 1006 | data () { 1007 | return { 1008 | spy: sinon.spy() 1009 | } 1010 | } 1011 | }) 1012 | 1013 | expect(vm.spy).to.have.not.been.called 1014 | vm.$('.vdatetime-input').click() 1015 | 1016 | vm.$nextTick(() => { 1017 | vm.$('.vdatetime-popup__actions__button--cancel').click() 1018 | 1019 | vm.$nextTick(() => { 1020 | expect(vm.spy).to.have.been.calledOnce 1021 | done() 1022 | }) 1023 | }) 1024 | }) 1025 | 1026 | it('should update datetime when hidden input changes', function (done) { 1027 | const vm = createVM(this, 1028 | ``, 1029 | { 1030 | components: { Datetime }, 1031 | data () { 1032 | return { 1033 | datetime: '2020-05-07T00:00:00.000Z' 1034 | } 1035 | } 1036 | }) 1037 | 1038 | const newDate = '2020-06-08T00:00:00.000Z' 1039 | const hiddenInput = vm.$('.vdatetime input[name=dt]') 1040 | 1041 | hiddenInput.value = newDate 1042 | hiddenInput.dispatchEvent(new window.Event('input')) 1043 | 1044 | vm.$nextTick(() => { 1045 | expect(vm.datetime).to.be.equal(newDate) 1046 | expect(hiddenInput.value).to.be.equal(newDate) 1047 | done() 1048 | }) 1049 | }) 1050 | 1051 | it('should select min date if clicked through', function (done) { 1052 | const minDatetime = LuxonDateTime.utc().plus({ days: 3 }).set({ seconds: 0, millisecond: 0 }).toISO() 1053 | 1054 | const vm = createVM(this, 1055 | ``, 1056 | { 1057 | components: { Datetime }, 1058 | data () { 1059 | return { 1060 | datetime: null, 1061 | minDatetime: minDatetime 1062 | } 1063 | } 1064 | }) 1065 | 1066 | vm.$('.vdatetime-input').click() 1067 | 1068 | vm.$nextTick(() => { 1069 | vm.$('.vdatetime-popup__actions__button--confirm').click() 1070 | 1071 | vm.$nextTick(() => { 1072 | vm.$('.vdatetime-popup__actions__button--confirm').click() 1073 | 1074 | vm.$nextTick(() => { 1075 | expect(vm.datetime).to.be.equal(minDatetime) 1076 | done() 1077 | }) 1078 | }) 1079 | }) 1080 | }) 1081 | 1082 | it('should select max date if clicked through', function (done) { 1083 | const maxDatetime = LuxonDateTime.utc().minus({ days: 3 }).set({ seconds: 0, millisecond: 0 }).toISO() 1084 | 1085 | const vm = createVM(this, 1086 | ``, 1087 | { 1088 | components: { Datetime }, 1089 | data () { 1090 | return { 1091 | datetime: null, 1092 | maxDatetime: maxDatetime 1093 | } 1094 | } 1095 | }) 1096 | 1097 | vm.$('.vdatetime-input').click() 1098 | 1099 | vm.$nextTick(() => { 1100 | vm.$('.vdatetime-popup__actions__button--confirm').click() 1101 | 1102 | vm.$nextTick(() => { 1103 | vm.$('.vdatetime-popup__actions__button--confirm').click() 1104 | 1105 | vm.$nextTick(() => { 1106 | expect(vm.datetime).to.be.equal(maxDatetime) 1107 | done() 1108 | }) 1109 | }) 1110 | }) 1111 | }) 1112 | }) 1113 | }) 1114 | -------------------------------------------------------------------------------- /test/specs/DatetimeCalendar.spec.js: -------------------------------------------------------------------------------- 1 | import { DateTime as LuxonDatetime, Settings as LuxonSettings } from 'luxon' 2 | import { createVM } from '../helpers/utils.js' 3 | import DatetimeCalendar from 'src/DatetimeCalendar.vue' 4 | 5 | describe('DatetimeCalendar.vue', function () { 6 | describe('render', function () { 7 | it('should render a calendar', function () { 8 | LuxonSettings.defaultLocale = 'en' 9 | const vm = createVM(this, 10 | ``, 11 | { 12 | components: { DatetimeCalendar } 13 | }) 14 | 15 | expect(vm.$('.vdatetime-calendar')).to.exist 16 | expect(vm.$('.vdatetime-calendar__current--month')).to.have.text('April 2017') 17 | 18 | const weekdays = vm.$$('.vdatetime-calendar__month__weekday').map(el => el.textContent) 19 | expect(weekdays).deep.equal(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']) 20 | 21 | const days = vm.$$('.vdatetime-calendar__month__day').map(el => el.textContent) 22 | expect(days).deep.equal(['', '', '', '', '', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30']) 23 | }) 24 | 25 | it('should render a calendar in default locale', function () { 26 | LuxonSettings.defaultLocale = 'es' 27 | const vm = createVM(this, 28 | ``, 29 | { 30 | components: { DatetimeCalendar } 31 | }) 32 | 33 | expect(vm.$('.vdatetime-calendar__current--month')).to.have.text('Julio 2018') 34 | 35 | const weekdays = vm.$$('.vdatetime-calendar__month__weekday').map(el => el.textContent) 36 | expect(weekdays).deep.equal(['Lun.', 'Mar.', 'Mié.', 'Jue.', 'Vie.', 'Sáb.', 'Dom.']) 37 | }) 38 | 39 | it('should render a calendar with other week start', function () { 40 | LuxonSettings.defaultLocale = 'en' 41 | const vm = createVM(this, 42 | ``, 43 | { 44 | components: { DatetimeCalendar } 45 | }) 46 | 47 | expect(vm.$('.vdatetime-calendar')).to.exist 48 | 49 | const weekdays = vm.$$('.vdatetime-calendar__month__weekday').map(el => el.textContent) 50 | expect(weekdays).deep.equal(['Thu', 'Fri', 'Sat', 'Sun', 'Mon', 'Tue', 'Wed']) 51 | 52 | const days = vm.$$('.vdatetime-calendar__month__day').map(el => el.textContent) 53 | expect(days).deep.equal(['', '', '', '', '', '', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '', '', '', '', '']) 54 | }) 55 | 56 | it('should select the current day', function () { 57 | const vm = createVM(this, 58 | ``, 59 | { 60 | components: { DatetimeCalendar } 61 | }) 62 | 63 | expect(vm.$('.vdatetime-calendar__month__day--selected')).to.have.text('10') 64 | }) 65 | 66 | it('should disable days before min date', function () { 67 | const vm = createVM(this, 68 | ``, 69 | { 70 | components: { DatetimeCalendar }, 71 | data () { 72 | return { 73 | minDate: LuxonDatetime.fromISO('2018-07-06T12:00:00.000Z').toUTC() 74 | } 75 | } 76 | }) 77 | 78 | const monthDays = vm.$$('.vdatetime-calendar__month__day') 79 | 80 | monthDays.forEach(monthDay => { 81 | const dayNumber = parseInt(monthDay.textContent) 82 | 83 | if (isNaN(dayNumber) || dayNumber < 6) { 84 | expect(monthDay).to.have.class('vdatetime-calendar__month__day--disabled') 85 | } else { 86 | expect(monthDay).to.have.not.class('vdatetime-calendar__month__day--disabled') 87 | } 88 | }) 89 | }) 90 | 91 | it('should disable days after max date', function () { 92 | const vm = createVM(this, 93 | ``, 94 | { 95 | components: { DatetimeCalendar }, 96 | data () { 97 | return { 98 | maxDate: LuxonDatetime.fromISO('2018-07-12T00:00:00.000Z').toUTC() 99 | } 100 | } 101 | }) 102 | 103 | const monthDays = vm.$$('.vdatetime-calendar__month__day') 104 | 105 | monthDays.forEach(monthDay => { 106 | const dayNumber = parseInt(monthDay.textContent) 107 | 108 | if (isNaN(dayNumber) || dayNumber > 12) { 109 | expect(monthDay).to.have.class('vdatetime-calendar__month__day--disabled') 110 | } else { 111 | expect(monthDay).to.have.not.class('vdatetime-calendar__month__day--disabled') 112 | } 113 | }) 114 | }) 115 | }) 116 | 117 | describe('navigation', function () { 118 | it('should render a next month when clicking next', function (done) { 119 | const vm = createVM(this, 120 | ``, 121 | { 122 | components: { DatetimeCalendar } 123 | }) 124 | 125 | vm.$('.vdatetime-calendar__navigation--next').click() 126 | 127 | vm.$nextTick(() => { 128 | expect(vm.$('.vdatetime-calendar__current--month')).to.have.text('May 2017') 129 | const days = vm.$$('.vdatetime-calendar__month__day').map(el => el.textContent) 130 | expect(days).deep.equal(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '', '', '', '']) 131 | done() 132 | }) 133 | }) 134 | 135 | it('should render a next month when clicking next even at end of the year', function (done) { 136 | const vm = createVM(this, 137 | ``, 138 | { 139 | components: { DatetimeCalendar } 140 | }) 141 | 142 | vm.$('.vdatetime-calendar__navigation--next').click() 143 | 144 | vm.$nextTick(() => { 145 | expect(vm.$('.vdatetime-calendar__current--month')).to.have.text('January 2018') 146 | const days = vm.$$('.vdatetime-calendar__month__day').map(el => el.textContent) 147 | expect(days).deep.equal(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '', '', '', '']) 148 | done() 149 | }) 150 | }) 151 | 152 | it('should render a previous month when clicking previous', function (done) { 153 | const vm = createVM(this, 154 | ``, 155 | { 156 | components: { DatetimeCalendar } 157 | }) 158 | 159 | vm.$('.vdatetime-calendar__navigation--previous').click() 160 | 161 | vm.$nextTick(() => { 162 | expect(vm.$('.vdatetime-calendar__current--month')).to.have.text('March 2017') 163 | const days = vm.$$('.vdatetime-calendar__month__day').map(el => el.textContent) 164 | expect(days).deep.equal(['', '', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '', '']) 165 | done() 166 | }) 167 | }) 168 | 169 | it('should render a previous month when clicking previous even at beginning of year', function (done) { 170 | const vm = createVM(this, 171 | ``, 172 | { 173 | components: { DatetimeCalendar } 174 | }) 175 | 176 | vm.$('.vdatetime-calendar__navigation--previous').click() 177 | 178 | vm.$nextTick(() => { 179 | expect(vm.$('.vdatetime-calendar__current--month')).to.have.text('December 2016') 180 | const days = vm.$$('.vdatetime-calendar__month__day').map(el => el.textContent) 181 | expect(days).deep.equal(['', '', '', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '']) 182 | done() 183 | }) 184 | }) 185 | }) 186 | 187 | describe('events', function () { 188 | it('should emit change event on select a day', function () { 189 | const vm = createVM(this, 190 | ``, 191 | { 192 | components: { DatetimeCalendar }, 193 | data () { 194 | return { 195 | year: null, 196 | month: null, 197 | day: null 198 | } 199 | }, 200 | methods: { 201 | onChange (year, month, day) { 202 | this.year = year 203 | this.month = month 204 | this.day = day 205 | } 206 | } 207 | }) 208 | 209 | vm.$$('.vdatetime-calendar__month__day')[10].click() 210 | expect(vm.year).to.be.equal(2017) 211 | expect(vm.month).to.be.equal(4) 212 | expect(vm.day).to.be.equal(6) 213 | }) 214 | 215 | it('should not emit change event on select a disabled day', function () { 216 | const vm = createVM(this, 217 | ``, 218 | { 219 | components: { DatetimeCalendar }, 220 | data () { 221 | return { 222 | maxDate: LuxonDatetime.fromISO('2017-04-04T04:04:04.000Z'), 223 | spy: sinon.spy() 224 | } 225 | } 226 | }) 227 | 228 | vm.$$('.vdatetime-calendar__month__day')[10].click() 229 | expect(vm.spy).to.have.not.been.called 230 | }) 231 | }) 232 | }) 233 | -------------------------------------------------------------------------------- /test/specs/DatetimeMonthsPicker.spec.js: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import { createVM } from '../helpers/utils.js' 3 | import DatetimeMonthPicker from 'src/DatetimeMonthPicker.vue' 4 | import { months as utilsMonths } from 'src/util.js' 5 | 6 | describe('DatetimeMonthPicker.vue', function () { 7 | describe('render', function () { 8 | it('should render the months picker', function () { 9 | const vm = createVM(this, 10 | ``, 11 | { 12 | components: { DatetimeMonthPicker } 13 | }) 14 | 15 | expect(vm.$('.vdatetime-month-picker')).to.exist 16 | 17 | const months = vm.$$('.vdatetime-month-picker__list .vdatetime-month-picker__item') 18 | .map(el => el.textContent.trim()) 19 | 20 | expect(months).eql(utilsMonths()) 21 | }) 22 | 23 | it('should disable months', function () { 24 | const vm = createVM(this, 25 | ``, 26 | { 27 | components: { DatetimeMonthPicker }, 28 | data () { 29 | return { 30 | minDate: DateTime.fromISO('2018-07-01T00:00:00.000Z').toUTC(), 31 | maxDate: DateTime.fromISO('2018-12-01T00:00:00.000Z').toUTC() 32 | } 33 | } 34 | }) 35 | 36 | const months = vm.$$('.vdatetime-month-picker__list .vdatetime-month-picker__item') 37 | 38 | months.forEach(month => { 39 | const monthName = month.textContent.trim() 40 | 41 | if (!(['July', 'August', 'September', 'October', 'November', 'December'].indexOf(monthName) === -1)) { 42 | expect(month).to.have.not.class('vdatetime-month-picker__item--disabled') 43 | } else { 44 | expect(month).to.have.class('vdatetime-month-picker__item--disabled') 45 | } 46 | }) 47 | }) 48 | }) 49 | 50 | describe('events', function () { 51 | it('should emit change event on select a month', function () { 52 | const vm = createVM(this, 53 | ``, 54 | { 55 | components: { DatetimeMonthPicker }, 56 | data () { 57 | return { 58 | year: null, 59 | month: null 60 | } 61 | }, 62 | methods: { 63 | onChange (month) { 64 | this.month = month 65 | } 66 | } 67 | }) 68 | 69 | vm.$$('.vdatetime-month-picker__list .vdatetime-month-picker__item')[2].click() 70 | expect(vm.month).to.be.equal(3) 71 | }) 72 | 73 | it('should disable change event when month is disabled', function () { 74 | const vm = createVM(this, 75 | ``, 76 | { 77 | components: { DatetimeMonthPicker }, 78 | data () { 79 | return { 80 | minDate: DateTime.fromISO('2018-07-01T00:00:00.000Z').toUTC(), 81 | maxDate: DateTime.fromISO('2018-12-01T00:00:00.000Z').toUTC() 82 | } 83 | } 84 | }) 85 | vm.$$('.vdatetime-month-picker__list .vdatetime-month-picker__item')[3].click() 86 | expect(vm.month).not.to.be.equal(3) 87 | }) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /test/specs/DatetimePopup.spec.js: -------------------------------------------------------------------------------- 1 | import { DateTime as LuxonDatetime, Settings as LuxonSettings } from 'luxon' 2 | import { createVM } from '../helpers/utils.js' 3 | import DatetimePopup from 'src/DatetimePopup.vue' 4 | 5 | describe('DatetimePopup.vue', function () { 6 | describe('render', function () { 7 | it('should render the popup', function () { 8 | const vm = createVM(this, 9 | ``, 10 | { 11 | components: { DatetimePopup }, 12 | data () { 13 | return { 14 | datetime: LuxonDatetime.local() 15 | } 16 | } 17 | }) 18 | 19 | expect(vm.$('.vdatetime-popup')).to.exist 20 | }) 21 | 22 | it('should render the header', function () { 23 | LuxonSettings.defaultLocale = 'es-ES' 24 | const vm = createVM(this, 25 | ``, 26 | { 27 | components: { DatetimePopup }, 28 | data () { 29 | return { 30 | datetime: LuxonDatetime.fromObject({ 31 | year: 2017, 32 | month: 5, 33 | day: 12 34 | }) 35 | } 36 | } 37 | }) 38 | 39 | expect(vm.$('.vdatetime-popup__year').textContent).to.be.equal('2017') 40 | expect(vm.$('.vdatetime-popup__date').textContent).to.be.equal('12 de mayo') 41 | }) 42 | 43 | it('should render the body', function () { 44 | const vm = createVM(this, 45 | ``, 46 | { 47 | components: { DatetimePopup }, 48 | data () { 49 | return { 50 | datetime: LuxonDatetime.local() 51 | } 52 | } 53 | }) 54 | 55 | expect(vm.$('.vdatetime-popup__body')).to.exist 56 | }) 57 | 58 | it('should render the action buttons', function () { 59 | const vm = createVM(this, 60 | ``, 61 | { 62 | components: { DatetimePopup }, 63 | data () { 64 | return { 65 | datetime: LuxonDatetime.local() 66 | } 67 | } 68 | }) 69 | 70 | expect(vm.$('.vdatetime-popup__actions')).to.exist 71 | expect(vm.$$('.vdatetime-popup__actions__button')[0]).to.have.text('Cancel') 72 | expect(vm.$$('.vdatetime-popup__actions__button')[1]).to.have.text('Ok') 73 | }) 74 | 75 | it('should render the action buttons with custom slots', function () { 76 | const vm = createVM(this, 77 | ` 78 | 79 | 80 | `, 81 | { 82 | components: { DatetimePopup }, 83 | data () { 84 | return { 85 | datetime: LuxonDatetime.local() 86 | } 87 | } 88 | }) 89 | 90 | expect(vm.$('.vdatetime-popup__actions')).to.exist 91 | expect(vm.$$('.vdatetime-popup__actions__button')[0]).to.have.html('Abort') 92 | expect(vm.$$('.vdatetime-popup__actions__button')[1]).to.have.html('Confirm') 93 | }) 94 | 95 | it('should render the calendar by default', function () { 96 | const vm = createVM(this, 97 | ``, 98 | { 99 | components: { DatetimePopup }, 100 | data () { 101 | return { 102 | datetime: LuxonDatetime.local() 103 | } 104 | } 105 | }) 106 | 107 | expect(vm.$('.vdatetime-popup__body .vdatetime-calendar')).to.exist 108 | }) 109 | 110 | it('should render the calendar when type is date', function () { 111 | const vm = createVM(this, 112 | ``, 113 | { 114 | components: { DatetimePopup }, 115 | data () { 116 | return { 117 | datetime: LuxonDatetime.local() 118 | } 119 | } 120 | }) 121 | 122 | expect(vm.$('.vdatetime-popup__body .vdatetime-calendar')).to.exist 123 | }) 124 | 125 | it('should render the time picker when type is datetime', function (done) { 126 | const vm = createVM(this, 127 | ``, 128 | { 129 | components: { DatetimePopup }, 130 | data () { 131 | return { 132 | datetime: LuxonDatetime.local() 133 | } 134 | } 135 | }) 136 | 137 | vm.$('.vdatetime-popup__actions__button--confirm').click() 138 | vm.$nextTick(() => { 139 | expect(vm.$('.vdatetime-popup__body .vdatetime-time-picker')).to.exist 140 | done() 141 | }) 142 | }) 143 | 144 | it('should render the year picker', function (done) { 145 | const vm = createVM(this, 146 | ``, 147 | { 148 | components: { DatetimePopup }, 149 | data () { 150 | return { 151 | datetime: LuxonDatetime.local() 152 | } 153 | } 154 | }) 155 | 156 | vm.$('.vdatetime-popup__year').click() 157 | vm.$nextTick(() => { 158 | expect(vm.$('.vdatetime-popup__body .vdatetime-year-picker')).to.exist 159 | done() 160 | }) 161 | }) 162 | 163 | it('should render the month picker', function (done) { 164 | const vm = createVM(this, 165 | ``, 166 | { 167 | components: { DatetimePopup }, 168 | data () { 169 | return { 170 | datetime: LuxonDatetime.local() 171 | } 172 | } 173 | }) 174 | 175 | vm.$('.vdatetime-popup__date').click() 176 | vm.$nextTick(() => { 177 | expect(vm.$('.vdatetime-popup__body .vdatetime-month-picker')).to.exist 178 | done() 179 | }) 180 | }) 181 | 182 | it('should render the calendar on confirm in year picker', function (done) { 183 | const vm = createVM(this, 184 | ``, 185 | { 186 | components: { DatetimePopup }, 187 | data () { 188 | return { 189 | datetime: LuxonDatetime.local() 190 | } 191 | } 192 | }) 193 | 194 | vm.$('.vdatetime-popup__year').click() 195 | vm.$nextTick(() => { 196 | vm.$('.vdatetime-popup__actions__button--confirm').click() 197 | vm.$nextTick(() => { 198 | expect(vm.$('.vdatetime-popup__body .vdatetime-calendar')).to.exist 199 | done() 200 | }) 201 | }) 202 | }) 203 | 204 | it('should render custom phrases', function () { 205 | const vm = createVM(this, 206 | ``, 207 | { 208 | components: { DatetimePopup }, 209 | data () { 210 | return { 211 | datetime: LuxonDatetime.local(), 212 | phrases: { 213 | cancel: 'Cancelar', 214 | ok: 'Confirmar' 215 | } 216 | } 217 | } 218 | }) 219 | 220 | expect(vm.$('.vdatetime-popup__actions__button--confirm').innerText).to.be.equal('Confirmar') 221 | expect(vm.$('.vdatetime-popup__actions__button--cancel').innerText).to.be.equal('Cancelar') 222 | }) 223 | 224 | it('should render custom flow', function (done) { 225 | const vm = createVM(this, 226 | ``, 227 | { 228 | components: { DatetimePopup }, 229 | data () { 230 | return { 231 | datetime: LuxonDatetime.local() 232 | } 233 | } 234 | }) 235 | 236 | expect(vm.$('.vdatetime-popup__body .vdatetime-year-picker')).to.exist 237 | vm.$('.vdatetime-popup__actions__button--confirm').click() 238 | 239 | vm.$nextTick(() => { 240 | expect(vm.$('.vdatetime-popup__body .vdatetime-calendar')).to.exist 241 | vm.$('.vdatetime-popup__actions__button--confirm').click() 242 | 243 | vm.$nextTick(() => { 244 | expect(vm.$('.vdatetime-popup__body .vdatetime-time-picker')).to.exist 245 | done() 246 | }) 247 | }) 248 | }) 249 | 250 | it('should render the title', function () { 251 | const vm = createVM(this, 252 | ``, 253 | { 254 | components: { DatetimePopup }, 255 | data () { 256 | return { 257 | datetime: LuxonDatetime.local() 258 | } 259 | } 260 | }) 261 | 262 | expect(vm.$('.vdatetime-popup__title')).to.have.text('Select your birthday') 263 | }) 264 | }) 265 | 266 | describe('pass props', function () { 267 | it('should pass use 12 hour to time picker', function (done) { 268 | const vm = createVM(this, 269 | ``, 270 | { 271 | components: { DatetimePopup }, 272 | data () { 273 | return { 274 | datetime: LuxonDatetime.local() 275 | } 276 | } 277 | }) 278 | 279 | vm.$('.vdatetime-popup__actions__button--confirm').click() 280 | 281 | vm.$nextTick(() => { 282 | expect(vm.$findChild('.vdatetime-time-picker').use12Hour).to.be.equal(true) 283 | done() 284 | }) 285 | }) 286 | 287 | it('should pass time steps to time picker', function (done) { 288 | const vm = createVM(this, 289 | ``, 290 | { 291 | components: { DatetimePopup }, 292 | data () { 293 | return { 294 | datetime: LuxonDatetime.local() 295 | } 296 | } 297 | }) 298 | 299 | vm.$('.vdatetime-popup__actions__button--confirm').click() 300 | 301 | vm.$nextTick(() => { 302 | expect(vm.$findChild('.vdatetime-time-picker').hourStep).to.be.equal(2) 303 | expect(vm.$findChild('.vdatetime-time-picker').minuteStep).to.be.equal(15) 304 | done() 305 | }) 306 | }) 307 | 308 | it('should pass min and max date to calendar', function () { 309 | const vm = createVM(this, 310 | ``, 311 | { 312 | components: { DatetimePopup }, 313 | data () { 314 | return { 315 | datetime: LuxonDatetime.local(), 316 | minDatetime: LuxonDatetime.fromISO('2018-01-01T12:35:22.000Z'), 317 | maxDatetime: LuxonDatetime.fromISO('2018-01-03T20:43:13.000Z') 318 | } 319 | } 320 | }) 321 | 322 | expect(vm.$findChild('.vdatetime-calendar').minDate.toISODate()).to.be.equal('2018-01-01') 323 | expect(vm.$findChild('.vdatetime-calendar').maxDate.toISODate()).to.be.equal('2018-01-03') 324 | }) 325 | 326 | it('should pass min and max time to time picker when date are equals', function (done) { 327 | const minDatetime = LuxonDatetime.fromISO('2018-01-01T12:35:22.000Z') 328 | const maxDatetime = LuxonDatetime.fromISO('2018-01-01T20:43:13.000Z') 329 | 330 | const vm = createVM(this, 331 | ``, 332 | { 333 | components: { DatetimePopup }, 334 | data () { 335 | return { 336 | datetime: LuxonDatetime.fromISO('2018-01-01T17:42:11.000Z'), 337 | minDatetime: minDatetime, 338 | maxDatetime: maxDatetime 339 | } 340 | } 341 | }) 342 | 343 | vm.$('.vdatetime-popup__actions__button--confirm').click() 344 | 345 | vm.$nextTick(() => { 346 | expect(vm.$findChild('.vdatetime-time-picker').minTime).to.be.equal(minDatetime.toFormat('HH:mm')) 347 | expect(vm.$findChild('.vdatetime-time-picker').maxTime).to.be.equal(maxDatetime.toFormat('HH:mm')) 348 | done() 349 | }) 350 | }) 351 | 352 | it('should pass week start to calendar', function () { 353 | const vm = createVM(this, 354 | ``, 355 | { 356 | components: { DatetimePopup }, 357 | data () { 358 | return { 359 | datetime: LuxonDatetime.local() 360 | } 361 | } 362 | }) 363 | 364 | expect(vm.$findChild('.vdatetime-calendar').weekStart).to.be.equal(3) 365 | }) 366 | }) 367 | 368 | describe('events', function () { 369 | it('should emit confirm event on confirm', function () { 370 | const vm = createVM(this, 371 | ``, 372 | { 373 | components: { DatetimePopup }, 374 | data () { 375 | return { 376 | datetime: LuxonDatetime.fromObject({ 377 | year: 2017, 378 | month: 7, 379 | day: 23, 380 | zone: 'UTC+02:00' 381 | }), 382 | newDatetime: '' 383 | } 384 | }, 385 | methods: { 386 | onConfirm (datetime) { 387 | this.newDatetime = datetime.toISO() 388 | } 389 | } 390 | }) 391 | 392 | vm.$$('.vdatetime-calendar__month__day')[22].click() 393 | vm.$('.vdatetime-popup__actions__button--confirm').click() 394 | expect(vm.newDatetime).to.be.equal('2017-07-18T00:00:00.000+02:00') 395 | }) 396 | 397 | it('should emit confirm event on confirm when type is datetime', function (done) { 398 | const vm = createVM(this, 399 | ``, 400 | { 401 | components: { DatetimePopup }, 402 | data () { 403 | return { 404 | datetime: LuxonDatetime.fromObject({ 405 | year: 2017, 406 | month: 7, 407 | day: 23, 408 | zone: 'UTC+02:00' 409 | }), 410 | newDatetime: '' 411 | } 412 | }, 413 | methods: { 414 | onConfirm (datetime) { 415 | this.newDatetime = datetime.toISO() 416 | } 417 | } 418 | }) 419 | 420 | vm.$$('.vdatetime-calendar__month__day')[15].click() 421 | vm.$('.vdatetime-popup__actions__button--confirm').click() 422 | vm.$nextTick(() => { 423 | vm.$$('.vdatetime-time-picker__list--hours .vdatetime-time-picker__item')[4].click() 424 | vm.$nextTick(() => { 425 | vm.$$('.vdatetime-time-picker__list--minutes .vdatetime-time-picker__item')[15].click() 426 | vm.$('.vdatetime-popup__actions__button--confirm').click() 427 | expect(vm.newDatetime).to.be.equal('2017-07-11T04:15:00.000+02:00') 428 | done() 429 | }) 430 | }) 431 | }) 432 | 433 | it('should emit confirm event on key down ENTER', function (done) { 434 | const vm = createVM(this, 435 | ``, 436 | { 437 | components: { DatetimePopup }, 438 | data () { 439 | return { 440 | datetime: LuxonDatetime.local(), 441 | spy: sinon.spy() 442 | } 443 | } 444 | }) 445 | 446 | expect(vm.spy).to.have.not.been.called 447 | 448 | const event = document.createEvent('Event') 449 | event.keyCode = 13 450 | event.initEvent('keydown') 451 | document.dispatchEvent(event) 452 | 453 | vm.$nextTick(() => { 454 | expect(vm.spy).to.have.been.calledOnce 455 | done() 456 | }) 457 | }) 458 | 459 | it('should emit cancel event on cancel', function () { 460 | const vm = createVM(this, 461 | ``, 462 | { 463 | components: { DatetimePopup }, 464 | data () { 465 | return { 466 | datetime: LuxonDatetime.local(), 467 | spy: sinon.spy() 468 | } 469 | } 470 | }) 471 | 472 | expect(vm.spy).to.have.not.been.called 473 | vm.$('.vdatetime-popup__actions__button--cancel').click() 474 | expect(vm.spy).to.have.been.calledOnce 475 | }) 476 | 477 | it('should emit cancel event on key down ESC', function (done) { 478 | const vm = createVM(this, 479 | ``, 480 | { 481 | components: { DatetimePopup }, 482 | data () { 483 | return { 484 | datetime: LuxonDatetime.local(), 485 | spy: sinon.spy() 486 | } 487 | } 488 | }) 489 | 490 | expect(vm.spy).to.have.not.been.called 491 | 492 | const event = document.createEvent('Event') 493 | event.keyCode = 27 494 | event.initEvent('keydown') 495 | document.dispatchEvent(event) 496 | 497 | vm.$nextTick(() => { 498 | expect(vm.spy).to.have.been.calledOnce 499 | done() 500 | }) 501 | }) 502 | 503 | it('should emit cancel event on key down TAB', function (done) { 504 | const vm = createVM(this, 505 | ``, 506 | { 507 | components: { DatetimePopup }, 508 | data () { 509 | return { 510 | datetime: LuxonDatetime.local(), 511 | spy: sinon.spy() 512 | } 513 | } 514 | }) 515 | 516 | expect(vm.spy).to.have.not.been.called 517 | 518 | const event = document.createEvent('Event') 519 | event.keyCode = 9 520 | event.initEvent('keydown') 521 | document.dispatchEvent(event) 522 | 523 | vm.$nextTick(() => { 524 | expect(vm.spy).to.have.been.calledOnce 525 | done() 526 | }) 527 | }) 528 | }) 529 | 530 | describe('auto', function () { 531 | it('should close on change date when auto is active', function () { 532 | const vm = createVM(this, 533 | ``, 534 | { 535 | components: { DatetimePopup }, 536 | data () { 537 | return { 538 | datetime: LuxonDatetime.local(), 539 | spy: sinon.spy() 540 | } 541 | } 542 | }) 543 | 544 | expect(vm.spy).to.have.not.been.called 545 | vm.$findChild('.vdatetime-popup').onChangeDate(2017, 5, 10) 546 | expect(vm.spy).to.have.been.calledOnce 547 | }) 548 | 549 | it('should close year picker on change year when auto is active', function (done) { 550 | const vm = createVM(this, 551 | ``, 552 | { 553 | components: { DatetimePopup }, 554 | data () { 555 | return { 556 | datetime: LuxonDatetime.local() 557 | } 558 | } 559 | }) 560 | 561 | vm.$('.vdatetime-popup__year').click() 562 | vm.$nextTick(() => { 563 | vm.$findChild('.vdatetime-popup').onChangeYear(2017) 564 | vm.$nextTick(() => { 565 | expect(vm.$('.vdatetime-popup__body .vdatetime-calendar')).to.exist 566 | done() 567 | }) 568 | }) 569 | }) 570 | 571 | it('should close time picker on change time when auto is active', function (done) { 572 | const vm = createVM(this, 573 | ``, 574 | { 575 | components: { DatetimePopup }, 576 | data () { 577 | return { 578 | datetime: LuxonDatetime.local() 579 | } 580 | } 581 | }) 582 | 583 | vm.$('.vdatetime-popup__year').click() 584 | vm.$nextTick(() => { 585 | vm.$findChild('.vdatetime-popup').onChangeTime({ hour: 14 }) // First select hour 586 | vm.$findChild('.vdatetime-popup').onChangeTime({ minute: 30 }) // then minute 587 | vm.$nextTick(() => { 588 | expect(vm.$('.vdatetime-popup__body .vdatetime-calendar')).to.exist 589 | done() 590 | }) 591 | }) 592 | }) 593 | 594 | it('should close time picker on change time when auto is active (12 hour)', function (done) { 595 | const vm = createVM(this, 596 | ``, 597 | { 598 | components: { DatetimePopup }, 599 | data () { 600 | return { 601 | datetime: LuxonDatetime.local() 602 | } 603 | } 604 | }) 605 | 606 | vm.$('.vdatetime-popup__year').click() 607 | vm.$nextTick(() => { 608 | vm.$findChild('.vdatetime-popup').onChangeTime({ hour: 14 }) // First select hour 609 | vm.$findChild('.vdatetime-popup').onChangeTime({ minute: 30 }) // then minute 610 | vm.$findChild('.vdatetime-popup').onChangeTime({ suffixTouched: true }) // then minute 611 | vm.$nextTick(() => { 612 | expect(vm.$('.vdatetime-popup__body .vdatetime-calendar')).to.exist 613 | done() 614 | }) 615 | }) 616 | }) 617 | }) 618 | }) 619 | -------------------------------------------------------------------------------- /test/specs/DatetimeTimePicker.spec.js: -------------------------------------------------------------------------------- 1 | import { createVM } from '../helpers/utils.js' 2 | import DatetimeTimePicker from 'src/DatetimeTimePicker.vue' 3 | 4 | describe('DatetimeTimePicker.vue', function () { 5 | describe('render', function () { 6 | it('should render the time picker', function () { 7 | const vm = createVM(this, 8 | ``, 9 | { 10 | components: { DatetimeTimePicker } 11 | }) 12 | 13 | expect(vm.$('.vdatetime-time-picker')).to.exist 14 | 15 | const hours = vm.$$('.vdatetime-time-picker__list--hours .vdatetime-time-picker__item').map(el => el.textContent) 16 | expect(hours).eql(['00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23']) 17 | 18 | const minutes = vm.$$('.vdatetime-time-picker__list--minutes .vdatetime-time-picker__item').map(el => el.textContent) 19 | expect(minutes).eql(['00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '50', '51', '52', '53', '54', '55', '56', '57', '58', '59']) 20 | 21 | const selected = vm.$$('.vdatetime-time-picker__item--selected').map(el => el.textContent) 22 | expect(selected).eql(['03', '45']) 23 | }) 24 | 25 | it('should render the time picker in 12 hour format', function () { 26 | const vm = createVM(this, 27 | ``, 28 | { 29 | components: { DatetimeTimePicker } 30 | }) 31 | 32 | expect(vm.$('.vdatetime-time-picker')).to.exist 33 | 34 | const hours = vm.$$('.vdatetime-time-picker__list--hours .vdatetime-time-picker__item').map(el => el.textContent) 35 | expect(hours).eql(['12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']) 36 | 37 | const minutes = vm.$$('.vdatetime-time-picker__list--minutes .vdatetime-time-picker__item').map(el => el.textContent) 38 | expect(minutes).eql(['00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '50', '51', '52', '53', '54', '55', '56', '57', '58', '59']) 39 | 40 | const selected = vm.$$('.vdatetime-time-picker__item--selected').map(el => el.textContent) 41 | expect(selected).eql(['3', '45', 'am']) 42 | }) 43 | 44 | it('should render the time picker in 12 hour format (pm)', function () { 45 | const vm = createVM(this, 46 | ``, 47 | { 48 | components: { DatetimeTimePicker } 49 | }) 50 | 51 | expect(vm.$('.vdatetime-time-picker')).to.exist 52 | 53 | const hours = vm.$$('.vdatetime-time-picker__list--hours .vdatetime-time-picker__item').map(el => el.textContent) 54 | expect(hours).eql(['12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']) 55 | 56 | const minutes = vm.$$('.vdatetime-time-picker__list--minutes .vdatetime-time-picker__item').map(el => el.textContent) 57 | expect(minutes).eql(['00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '50', '51', '52', '53', '54', '55', '56', '57', '58', '59']) 58 | 59 | const selected = vm.$$('.vdatetime-time-picker__item--selected').map(el => el.textContent) 60 | expect(selected).eql(['1', '45', 'pm']) 61 | }) 62 | 63 | it('should render the time picker with custom steps', function () { 64 | const vm = createVM(this, 65 | ``, 66 | { 67 | components: { DatetimeTimePicker } 68 | }) 69 | 70 | const hours = vm.$$('.vdatetime-time-picker__list--hours .vdatetime-time-picker__item').map(el => el.textContent) 71 | expect(hours).eql(['00', '02', '04', '06', '08', '10', '12', '14', '16', '18', '20', '22']) 72 | 73 | const minutes = vm.$$('.vdatetime-time-picker__list--minutes .vdatetime-time-picker__item').map(el => el.textContent) 74 | expect(minutes).eql(['00', '15', '30', '45']) 75 | }) 76 | 77 | it('should disable time before min time', function () { 78 | const vm = createVM(this, 79 | ``, 80 | { 81 | components: { DatetimeTimePicker } 82 | }) 83 | 84 | const hours = vm.$$('.vdatetime-time-picker__list--hours .vdatetime-time-picker__item') 85 | 86 | hours.forEach(hour => { 87 | const hourNumber = parseInt(hour.textContent) 88 | 89 | if (hourNumber < 8) { 90 | expect(hour).to.have.class('vdatetime-time-picker__item--disabled') 91 | } else { 92 | expect(hour).to.have.not.class('vdatetime-time-picker__item--disabled') 93 | } 94 | }) 95 | 96 | const minutes = vm.$$('.vdatetime-time-picker__list--minutes .vdatetime-time-picker__item') 97 | 98 | minutes.forEach(minute => { 99 | const minuteNumber = parseInt(minute.textContent) 100 | 101 | if (minuteNumber < 32) { 102 | expect(minute).to.have.class('vdatetime-time-picker__item--disabled') 103 | } else { 104 | expect(minute).to.have.not.class('vdatetime-time-picker__item--disabled') 105 | } 106 | }) 107 | }) 108 | 109 | it('should disable time after max time', function () { 110 | const vm = createVM(this, 111 | ``, 112 | { 113 | components: { DatetimeTimePicker } 114 | }) 115 | 116 | const hours = vm.$$('.vdatetime-time-picker__list--hours .vdatetime-time-picker__item') 117 | 118 | hours.forEach(hour => { 119 | const hourNumber = parseInt(hour.textContent) 120 | 121 | if (hourNumber > 15) { 122 | expect(hour).to.have.class('vdatetime-time-picker__item--disabled') 123 | } else { 124 | expect(hour).to.have.not.class('vdatetime-time-picker__item--disabled') 125 | } 126 | }) 127 | 128 | const minutes = vm.$$('.vdatetime-time-picker__list--minutes .vdatetime-time-picker__item') 129 | 130 | minutes.forEach(minute => { 131 | const minuteNumber = parseInt(minute.textContent) 132 | 133 | if (minuteNumber > 21) { 134 | expect(minute).to.have.class('vdatetime-time-picker__item--disabled') 135 | } else { 136 | expect(minute).to.have.not.class('vdatetime-time-picker__item--disabled') 137 | } 138 | }) 139 | }) 140 | 141 | it('should disable time after max time (midnight)', function () { 142 | const vm = createVM(this, 143 | ``, 144 | { 145 | components: { DatetimeTimePicker } 146 | }) 147 | 148 | const hours = vm.$$('.vdatetime-time-picker__list--hours .vdatetime-time-picker__item') 149 | 150 | hours.forEach(hour => { 151 | const hourNumber = parseInt(hour.textContent) 152 | 153 | if (hourNumber > 0) { 154 | expect(hour).to.have.class('vdatetime-time-picker__item--disabled') 155 | } else { 156 | expect(hour).to.have.not.class('vdatetime-time-picker__item--disabled') 157 | } 158 | }) 159 | 160 | const minutes = vm.$$('.vdatetime-time-picker__list--minutes .vdatetime-time-picker__item') 161 | 162 | minutes.forEach(minute => { 163 | const minuteNumber = parseInt(minute.textContent) 164 | 165 | if (minuteNumber > 0) { 166 | expect(minute).to.have.class('vdatetime-time-picker__item--disabled') 167 | } else { 168 | expect(minute).to.have.not.class('vdatetime-time-picker__item--disabled') 169 | } 170 | }) 171 | }) 172 | 173 | it('should render the time picker with am/pm suffixes', function () { 174 | const vm = createVM(this, 175 | ``, 176 | { 177 | components: { DatetimeTimePicker } 178 | }) 179 | 180 | const suffixes = vm.$$('.vdatetime-time-picker__list--suffix .vdatetime-time-picker__item').map(el => el.textContent) 181 | expect(suffixes).eql(['am', 'pm']) 182 | }) 183 | }) 184 | 185 | describe('events', function () { 186 | it('should emit change event on select a hour', function () { 187 | const vm = createVM(this, 188 | ``, 189 | { 190 | components: { DatetimeTimePicker }, 191 | data () { 192 | return { 193 | hour: null 194 | } 195 | }, 196 | methods: { 197 | onChange ({ hour }) { 198 | this.hour = hour 199 | } 200 | } 201 | }) 202 | 203 | vm.$$('.vdatetime-time-picker__list--hours .vdatetime-time-picker__item')[10].click() 204 | expect(vm.hour).to.be.equal(10) 205 | }) 206 | 207 | it('should not emit change event on select a disabled hour', function () { 208 | const vm = createVM(this, 209 | ``, 210 | { 211 | components: { DatetimeTimePicker }, 212 | data () { 213 | return { 214 | spy: sinon.spy() 215 | } 216 | } 217 | }) 218 | 219 | vm.$$('.vdatetime-time-picker__list--hours .vdatetime-time-picker__item')[10].click() 220 | expect(vm.spy).to.have.not.been.called 221 | }) 222 | 223 | it('should emit change event on select a minute', function () { 224 | const vm = createVM(this, 225 | ``, 226 | { 227 | components: { DatetimeTimePicker }, 228 | data () { 229 | return { 230 | minute: null 231 | } 232 | }, 233 | methods: { 234 | onChange ({ minute }) { 235 | this.minute = minute 236 | } 237 | } 238 | }) 239 | 240 | vm.$$('.vdatetime-time-picker__list--minutes .vdatetime-time-picker__item')[30].click() 241 | expect(vm.minute).to.be.equal(30) 242 | }) 243 | 244 | it('should not emit change event on select a disabled minute', function () { 245 | const vm = createVM(this, 246 | ``, 247 | { 248 | components: { DatetimeTimePicker }, 249 | data () { 250 | return { 251 | spy: sinon.spy() 252 | } 253 | } 254 | }) 255 | 256 | vm.$$('.vdatetime-time-picker__list--minutes .vdatetime-time-picker__item')[30].click() 257 | expect(vm.spy).to.have.not.been.called 258 | }) 259 | 260 | it('should emit change event on suffix change am -> pm', function () { 261 | const vm = createVM(this, 262 | ``, 263 | { 264 | components: { DatetimeTimePicker }, 265 | data () { 266 | return { 267 | hour: null, 268 | suffixTouched: false 269 | } 270 | }, 271 | methods: { 272 | onChange ({ hour, suffixTouched }) { 273 | this.hour = hour 274 | this.suffixTouched = suffixTouched 275 | } 276 | } 277 | }) 278 | 279 | vm.$$('.vdatetime-time-picker__list--suffix .vdatetime-time-picker__item')[1].click() 280 | expect(vm.hour).to.be.equal(15) 281 | expect(vm.suffixTouched).to.be.equal(true) 282 | }) 283 | 284 | it('should emit change event on suffix change pm -> am', function () { 285 | const vm = createVM(this, 286 | ``, 287 | { 288 | components: { DatetimeTimePicker }, 289 | data () { 290 | return { 291 | hour: null, 292 | suffixTouched: false 293 | } 294 | }, 295 | methods: { 296 | onChange ({ hour, suffixTouched }) { 297 | this.hour = hour 298 | this.suffixTouched = suffixTouched 299 | } 300 | } 301 | }) 302 | 303 | vm.$$('.vdatetime-time-picker__list--suffix .vdatetime-time-picker__item')[0].click() 304 | expect(vm.hour).to.be.equal(1) 305 | expect(vm.suffixTouched).to.be.equal(true) 306 | }) 307 | }) 308 | }) 309 | -------------------------------------------------------------------------------- /test/specs/DatetimeYearPicker.spec.js: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import { createVM } from '../helpers/utils.js' 3 | import DatetimeYearPicker from 'src/DatetimeYearPicker.vue' 4 | 5 | describe('DatetimeYearPicker.vue', function () { 6 | describe('render', function () { 7 | it('should render the year picker', function () { 8 | const vm = createVM(this, 9 | ``, 10 | { 11 | components: { DatetimeYearPicker } 12 | }) 13 | 14 | expect(vm.$('.vdatetime-year-picker')).to.exist 15 | 16 | const years = vm.$$('.vdatetime-year-picker__list .vdatetime-year-picker__item') 17 | .map(el => parseInt(el.textContent)) 18 | const yearList = new Array(201).fill(null).map((item, index) => 1920 + index) 19 | 20 | expect(years).eql(yearList) 21 | 22 | const selected = parseInt(vm.$('.vdatetime-year-picker__item--selected').textContent) 23 | expect(selected).eql(2020) 24 | }) 25 | 26 | it('should disable years', function () { 27 | const vm = createVM(this, 28 | ``, 29 | { 30 | components: { DatetimeYearPicker }, 31 | data () { 32 | return { 33 | minDate: DateTime.fromISO('2018-01-01T00:00:00.000Z').toUTC(), 34 | maxDate: DateTime.fromISO('2018-12-01T00:00:00.000Z').toUTC() 35 | } 36 | } 37 | }) 38 | 39 | const years = vm.$$('.vdatetime-year-picker__list .vdatetime-year-picker__item') 40 | 41 | years.forEach(year => { 42 | const yearNumber = parseInt(year.textContent) 43 | 44 | if (yearNumber === 2018) { 45 | expect(year).to.have.not.class('vdatetime-year-picker__item--disabled') 46 | } else { 47 | expect(year).to.have.class('vdatetime-year-picker__item--disabled') 48 | } 49 | }) 50 | }) 51 | }) 52 | 53 | describe('events', function () { 54 | it('should emit change event on select a year', function () { 55 | const vm = createVM(this, 56 | ``, 57 | { 58 | components: { DatetimeYearPicker }, 59 | data () { 60 | return { 61 | year: null 62 | } 63 | }, 64 | methods: { 65 | onChange (year) { 66 | this.year = year 67 | } 68 | } 69 | }) 70 | 71 | vm.$$('.vdatetime-year-picker__list .vdatetime-year-picker__item')[110].click() 72 | expect(vm.year).to.be.equal(2030) 73 | }) 74 | 75 | it('should disable change event when year is disabled', function () { 76 | const vm = createVM(this, 77 | ``, 78 | { 79 | components: { DatetimeYearPicker }, 80 | data () { 81 | return { 82 | minDate: DateTime.fromISO('1920-01-01'), 83 | maxDate: DateTime.fromISO('2000-12-01') 84 | } 85 | } 86 | }) 87 | vm.$$('.vdatetime-year-picker__list .vdatetime-year-picker__item')[0].click() 88 | expect(vm.year).not.to.be.equal(1918) 89 | }) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /test/visual.js: -------------------------------------------------------------------------------- 1 | import 'style-loader!css-loader!mocha-css' 2 | import 'mocha/mocha.js' 3 | import sinon from 'sinon' 4 | import chai from 'chai' 5 | 6 | // create a div where mocha can add its stuff 7 | const mochaDiv = document.createElement('DIV') 8 | mochaDiv.id = 'mocha' 9 | document.body.appendChild(mochaDiv) 10 | 11 | window.mocha.setup({ 12 | ui: 'bdd', 13 | slow: 750, 14 | timeout: 5000, 15 | globals: [ 16 | '__VUE_DEVTOOLS*', 17 | 'script', 18 | 'inject', 19 | 'originalOpenFunction' 20 | ] 21 | }) 22 | window.expect = chai.expect 23 | window.sinon = sinon 24 | chai.use(require('chai-dom')) 25 | chai.use(require('sinon-chai')) 26 | 27 | let vms = [] 28 | let testId = 0 29 | 30 | beforeEach(function () { 31 | this.DOMElement = document.createElement('DIV') 32 | this.DOMElement.id = `test-${++testId}` 33 | document.body.appendChild(this.DOMElement) 34 | }) 35 | 36 | afterEach(function () { 37 | const testReportElements = document.getElementsByClassName('test') 38 | const lastReportElement = testReportElements[testReportElements.length - 1] 39 | 40 | if (!lastReportElement) { 41 | return 42 | } 43 | 44 | const el = document.getElementById(this.DOMElement.id) 45 | if (el) { 46 | lastReportElement.appendChild(el) 47 | } 48 | 49 | // Save the vm to hide it later 50 | if (this.DOMElement.vm) { 51 | vms.push(this.DOMElement.vm) 52 | } 53 | }) 54 | 55 | // Set up 56 | require('./setup') 57 | 58 | // Hide all tests at the end to prevent some weird bugs 59 | before(function () { 60 | vms = [] 61 | testId = 0 62 | }) 63 | 64 | after(function () { 65 | requestAnimationFrame(function () { 66 | setTimeout(function () { 67 | vms.forEach(vm => { 68 | // Hide if test passed 69 | if (!vm.$el.parentElement.classList.contains('fail')) { 70 | vm.$children[0].visible = false 71 | } 72 | }) 73 | }, 100) 74 | }) 75 | }) 76 | 77 | const specsContext = require.context('./specs', true) 78 | specsContext.keys().forEach(specsContext) 79 | 80 | window.mocha.checkLeaks() 81 | window.mocha.run() 82 | --------------------------------------------------------------------------------