├── .commitlintrc.js
├── .eslintrc.json
├── .flowconfig
├── .gitignore
├── .snyk
├── .tool-versions
├── .travis.yml
├── LICENSE
├── README.md
├── cypress.json
├── cypress
├── plugins
│ ├── index.js
│ └── main.js
├── rollup.config.js
└── support
│ ├── commands.js
│ ├── commands
│ ├── mount.js
│ └── standalone.html
│ └── index.js
├── deploy.sh
├── docs
├── .vuepress
│ ├── components
│ │ ├── attributes-event-listeners.vue
│ │ ├── base.js
│ │ ├── basic-example.vue
│ │ ├── manual-trigger.vue
│ │ └── pikaday-options.vue
│ ├── config.js
│ ├── public
│ │ └── vue-pikaday.png
│ └── styles
│ │ └── index.styl
├── README.md
├── config
│ └── README.md
└── guide
│ ├── README.md
│ ├── installation.md
│ ├── usage.md
│ └── usage
│ ├── attributes-event-listeners.md
│ ├── basic-example.md
│ ├── manual-trigger.md
│ ├── pikaday-options.md
│ └── registration.md
├── package.json
├── rollup.config.js
├── src
├── component.js
├── directives.js
└── index.js
├── test
└── specs
│ ├── attributes-event-listeners.js
│ ├── basic.js
│ ├── custom-formating.js
│ ├── external-date-change.js
│ ├── manual-trigger.js
│ └── pikaday-options.js
└── yarn.lock
/.commitlintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional']
3 | };
4 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "node": true,
6 | "mocha": true
7 | },
8 | "extends": [
9 | "plugin:flowtype/recommended",
10 | "eslint:recommended"
11 | ],
12 | "plugins": [
13 | "flowtype"
14 | ],
15 | "parserOptions": {
16 | "sourceType": "module"
17 | },
18 | "rules": {
19 | "indent": [
20 | "error",
21 | 2
22 | ],
23 | "linebreak-style": [
24 | "error",
25 | "unix"
26 | ],
27 | "quotes": [
28 | "error",
29 | "single"
30 | ],
31 | "semi": [
32 | "error",
33 | "always"
34 | ]
35 | },
36 | "globals": {
37 | "expect": true,
38 | "Cypress": true,
39 | "cy": true
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 |
3 | [include]
4 |
5 | [libs]
6 |
7 | [lints]
8 |
9 | [options]
10 |
11 | [strict]
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
3 | .coverage
4 | .envrc
5 | *.log
6 | cypress/screenshots
7 | cypress/videos
8 | dist
9 | docs/.vuepress/dist
10 | .DS_Store
11 |
--------------------------------------------------------------------------------
/.snyk:
--------------------------------------------------------------------------------
1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
2 | version: v1.14.1
3 | ignore: {}
4 | # patches apply the minimum changes required to fix a vulnerability
5 | patch:
6 | SNYK-JS-LODASH-450202:
7 | - lodash:
8 | patched: '2019-07-03T23:00:54.035Z'
9 | SNYK-JS-LODASH-567746:
10 | - lodash:
11 | patched: '2020-04-30T22:28:49.705Z'
12 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | nodejs 13.3.0
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | cache:
4 | yarn: true
5 | directories:
6 | - ~/.cache
7 | - node_modules
8 |
9 | addons:
10 | apt:
11 | packages:
12 | # Ubuntu 16+ does not install this dependency by default, so we need to install it ourselves
13 | - libgconf-2-4
14 |
15 | notifications:
16 | email: false
17 |
18 | sudo: false
19 |
20 | jobs:
21 | include:
22 | - stage: lint
23 | node_js: '13'
24 | script: commitlint-travis
25 |
26 | - &test
27 | node_js: '13'
28 | stage: test
29 | install: yarn
30 | script: yarn test
31 | - <<: *test
32 | node_js: '12'
33 | - <<: *test
34 | node_js: '11'
35 | - <<: *test
36 | node_js: '10'
37 |
38 | - stage: npm release
39 | node_js: '13'
40 | before_install: yarn global add greenkeeper-lockfile@1
41 | before_script: greenkeeper-lockfile-update
42 | script:
43 | - yarn build
44 | - yarn semantic-release
45 | after_script: greenkeeper-lockfile-upload
46 |
47 | branches:
48 | except:
49 | - /^v\d+\.\d+\.\d+$/
50 |
51 | stages:
52 | - lint
53 | - test
54 | - name: npm release
55 | if: branch = master
56 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 - present Enrian Partners a.s.
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 | # Vue Pikaday
2 |
3 | [](https://www.npmjs.com/package/@netcz/vue-pikaday)
4 | [](https://www.npmjs.com/package/@netcz/vue-pikaday)
5 | [](https://travis-ci.com/NetCZ/vue-pikaday)
6 | [](https://snyk.io/test/github/netcz/vue-pikaday)
7 |
8 | VueJS wrapper component for Pikaday datepicker
9 |
10 | ## Install
11 | ```bash
12 | npm i -D @netcz/vue-pikaday
13 | ```
14 | or
15 | ```bash
16 | yarn add -D @netcz/vue-pikaday
17 | ```
18 |
19 | ## Usage
20 |
21 | Please consult [documentation](https://netcz.github.io/vue-pikaday).
22 |
23 | ## Contribution
24 |
25 | Feel free to create issue for bugs or pull request with fixes / enhancements.
26 |
27 | ## License
28 |
29 | [MIT](https://opensource.org/licenses/MIT)
30 |
31 | Copyright (c) 2018 - present Michal Szajter
32 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "fixturesFolder": false,
3 | "integrationFolder": "test/specs",
4 | "projectId": "o8hy27"
5 | }
6 |
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example plugins/index.js can be used to load plugins
3 | //
4 | // You can change the location of this file or turn off loading
5 | // the plugins file with the 'pluginsFile' configuration option.
6 | //
7 | // You can read more here:
8 | // https://on.cypress.io/plugins-guide
9 | // ***********************************************************
10 |
11 | // This function is called when a project is opened or re-opened (e.g. due to
12 | // the project's config changing)
13 |
14 | require('@babel/register')({
15 | presets: ['@babel/preset-env']
16 | });
17 | module.exports = require('./main').default;
18 |
--------------------------------------------------------------------------------
/cypress/plugins/main.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import rollup from '@cypress/rollup-preprocessor';
3 |
4 | export default (on) => {
5 | on('file:preprocessor', rollup(path.resolve('cypress/rollup.config')));
6 | };
7 |
--------------------------------------------------------------------------------
/cypress/rollup.config.js:
--------------------------------------------------------------------------------
1 | import camelCase from 'lodash/camelCase';
2 |
3 | import babel from 'rollup-plugin-babel';
4 | import resolve from 'rollup-plugin-node-resolve';
5 | import commonjs from 'rollup-plugin-commonjs';
6 | import { terser } from 'rollup-plugin-terser';
7 | import html from 'rollup-plugin-html';
8 |
9 | import { scope, name as nameWithScope } from '../package.json';
10 |
11 | const name = nameWithScope.replace(`@${scope}/`, '');
12 |
13 | const debug = process.env.DEBUG === 'true';
14 |
15 | export default {
16 | plugins: [
17 | resolve({ external: ['vue'] }),
18 | html({
19 | include: '**/*.html'
20 | }),
21 | babel({
22 | presets: [
23 | ['@babel/preset-env', { 'modules': false }],
24 | '@babel/preset-flow'
25 | ],
26 | exclude: 'node_modules/**'
27 | }),
28 | commonjs({
29 | include: 'node_modules/**'
30 | }),
31 | terser({
32 | compress: {
33 | drop_console: !debug,
34 | drop_debugger: !debug
35 | }
36 | })
37 | ],
38 | output: {
39 | format: 'umd',
40 | name: camelCase(name),
41 | sourcemap: 'inline'
42 | }
43 | };
44 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 |
11 | import './commands/mount';
12 |
--------------------------------------------------------------------------------
/cypress/support/commands/mount.js:
--------------------------------------------------------------------------------
1 | import merge from 'lodash/merge';
2 | import isFunction from 'lodash/isFunction';
3 |
4 | import mountVue from 'cypress-vue-unit-test';
5 |
6 | import VuePikaday from '../../../dist/vue-pikaday.esm';
7 |
8 | import standaloneHTML from './standalone.html';
9 |
10 | const mode = `${ Cypress.env('mode') || 'esm' }`;
11 |
12 | const options = {
13 | extensions: {
14 | plugins: [VuePikaday]
15 | },
16 | html: '
'
17 | };
18 |
19 | Cypress.Commands.add('mount', (component) => {
20 | const appIframe = window.parent.document.querySelector('.aut-iframe');
21 | const iframeDocument = appIframe.contentDocument || appIframe.contentWindow.document;
22 | const componentData = (isFunction(component.data) ? component.data() : component.data) || {};
23 |
24 | component.data = function () {
25 | return merge({
26 | date: null,
27 | }, componentData, {
28 | options: {
29 | // we have to specify container to put picker to, otherwise it would be rendered in wrong frame
30 | container: iframeDocument.querySelector('body')
31 | }
32 | });
33 | };
34 |
35 | if (mode === 'standalone') {
36 | component.el = '#app';
37 |
38 | // inject component specification into appIframe so it can be used to instantiate Vue component
39 | appIframe.contentWindow._component = component;
40 | // inject Cypress object to appIframe for tests consistency
41 | appIframe.contentWindow.Cypress = Cypress;
42 |
43 | const componentHTML = '';
44 |
45 | iframeDocument.write(standaloneHTML);
46 | iframeDocument.write(componentHTML);
47 | iframeDocument.close();
48 | } else {
49 | mountVue(component, options)();
50 | }
51 | });
52 |
--------------------------------------------------------------------------------
/cypress/support/commands/standalone.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands';
18 |
19 | Cypress.config('mode', `${Cypress.env('mode') || 'esm'}`);
20 |
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | # abort on errors
4 | set -e
5 |
6 | yarn docs:build
7 |
8 | cd docs/.vuepress/dist
9 |
10 | git init
11 | git add -A
12 | git commit -m 'deploy'
13 |
14 | git push -f git@github.com:NetCZ/vue-pikaday.git master:gh-pages
15 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/attributes-event-listeners.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
46 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/base.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import moment from 'moment';
3 | import _ from 'lodash';
4 |
5 | import 'pikaday/css/pikaday.css';
6 |
7 | import VuePikaday from '../../../src';
8 |
9 | Vue.use(VuePikaday);
10 |
11 | Vue.prototype.$moment = moment;
12 | Vue.prototype.$_ = _;
13 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/basic-example.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
41 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/manual-trigger.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
48 |
49 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/pikaday-options.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
48 |
49 |
--------------------------------------------------------------------------------
/docs/.vuepress/config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | base: '/vue-pikaday/',
3 | title: 'VuePikaday',
4 | head: [
5 | ['link', {
6 | rel: 'stylesheet',
7 | href: 'https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600|Roboto Mono'
8 | }]
9 | ],
10 | extendMarkdown: md => {
11 | md.use(require('markdown-it-include'), './docs/guide');
12 | md.use(require('markdown-it-attrs'));
13 | },
14 | themeConfig: {
15 | repo: 'netcz/vue-pikaday',
16 | logo: '/vue-pikaday.png',
17 | nav: [
18 | {
19 | text: 'Guide',
20 | link: '/guide/'
21 | },
22 | {
23 | text: 'Config Reference',
24 | link: '/config/#props'
25 | }
26 | ],
27 | sidebar: {
28 | '/guide/': [
29 | {
30 | title: 'Guide',
31 | collapsable: false,
32 | children: [
33 | '',
34 | 'installation',
35 | ['usage', 'Usage']
36 | ]
37 | }
38 | ],
39 | '/config/': [
40 | {
41 | title: 'Config Reference',
42 | collapsable: false,
43 | sidebarDepth: 0,
44 | children: [
45 | ['#props', 'Props'],
46 | ['#directives', 'Directives']
47 | ]
48 | }
49 | ]
50 | },
51 | smoothScroll: true,
52 | lastUpdated: 'Last Updated',
53 | displayAllHeaders: true
54 | },
55 | chainWebpack: config => {
56 | config.module
57 | .rule('js')
58 | .use('babel-loader')
59 | .tap(options => {
60 | return Object.assign(options, {
61 | presets: [].concat(options.presets, [
62 | ['@babel/preset-env', { 'modules': false }],
63 | '@babel/preset-flow'
64 | ])
65 | })
66 | })
67 | }
68 | };
69 |
--------------------------------------------------------------------------------
/docs/.vuepress/public/vue-pikaday.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NetCZ/vue-pikaday/c79576355c59add84988052b95a6e42c021b8cff/docs/.vuepress/public/vue-pikaday.png
--------------------------------------------------------------------------------
/docs/.vuepress/styles/index.styl:
--------------------------------------------------------------------------------
1 | body
2 | font-family "Source Sans Pro", "Helvetica Neue", Arial, sans-serif !important
3 |
4 | .hero
5 | img
6 | max-width 100%
7 |
8 | .clearfix
9 | &:after
10 | content ""
11 | display table
12 | clear both
13 |
14 | .pika-single
15 | table, thead, tbody, tr, td, th, abbr
16 | margin 0 !important
17 | padding 0 !important
18 | border 0 !important
19 | font-size 100% !important
20 | vertical-align baseline !important
21 |
22 | table
23 | border-collapse collapse !important
24 | border-spacing 0 !important
25 | display table !important
26 |
27 | abbr
28 | cursor pointer !important
29 | font-weight 400
30 | text-decoration none
31 |
32 | .theme-default-content
33 | input
34 | cursor text
35 | width 10rem
36 | color #4e6e8e
37 | display inline-block
38 | border 1px solid #cfd4db
39 | border-radius 2rem
40 | font-size 0.9rem
41 | line-height 2rem
42 | padding 0 1rem
43 | outline none
44 | transition all 0.2s ease
45 |
46 | &:focus
47 | cursor auto
48 | border-color #3eaf7c
49 |
50 | button
51 | cursor pointer
52 | width 5rem
53 | display inline-block
54 | border 1px solid #cfd4db
55 | border-radius 2rem
56 | font-size 0.9rem
57 | line-height 2rem
58 | padding 0 1rem
59 | margin 0 1rem
60 | outline none
61 | transition all 0.2s ease
62 |
63 | &:hover
64 | border-color #3eaf7c
65 |
66 | .example
67 | padding 2rem 0
68 |
69 |
70 | pre
71 | margin 2rem 0 0
72 | padding 1rem
73 | background-color rgba(27,31,35,0.05)
74 | border-radius 6px
75 |
76 | code
77 | color #476582
78 | padding 0.25rem 0.5rem
79 | margin 0
80 | font-size 0.85em
81 |
82 | .mw-100
83 | min-width 100px
84 |
85 | .sidebar-heading
86 | display none
87 |
88 | .sidebar-group a.sidebar-link
89 | padding-left 1rem
90 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | home: true
3 | heroImage: /vue-pikaday.png
4 | description: Pikaday datepicker wrapper for Vue
5 | actionText: Get Started →
6 | actionLink: /guide/
7 | footer: 'MIT Licensed | Copyright © 2018 - present Michal Szajter'
8 | ---
9 |
--------------------------------------------------------------------------------
/docs/config/README.md:
--------------------------------------------------------------------------------
1 | # Config Reference
2 |
3 | [[toc]]
4 |
5 | ## Props
6 |
7 | | Name{.mw-100} | Required | Default | Datatype | Description |
8 | | --- | --- | --- | --- | --- |
9 | | v-model | yes | | Date, null, undefined | component model binding, for more info see [VueJS docs](https://vuejs.org/v2/guide/forms.html#Value-Bindings) |
10 | | options | no | {} | Object | **reactive** options passed to underlaying Pikaday instance, for all available options see Pikaday [documentation](https://github.com/dbushell/Pikaday#configuration)
11 | | autoDefault | no | false | Boolean | if `true`, component will set actual date as its default value |
12 |
13 | ::: warning NOTE
14 | `field` option is internally overridden and not available to use.\
15 | `trigger` is not usable either as DOM element do not exist yet when component is registered
16 | ::: tip
17 | use `v-p-visible` directive instead
18 | :::
19 |
20 | ## Directives
21 |
22 | | Name{.mw-100} | Required | Default | Datatype | Description |
23 | | --- | --- | --- | --- | --- |
24 | | v-p-visible | no | false | Boolean | two-way binding directive to control datepicker visibility |
25 |
26 |
--------------------------------------------------------------------------------
/docs/guide/README.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | VuePikaday is a Vue component (wrapper) around [Pikaday](https://github.com/dbushell/Pikaday) datepicker with
4 | simple purpose - to make it easy to integrate with Vue applications.
5 |
--------------------------------------------------------------------------------
/docs/guide/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | [[toc]]
4 |
5 | You can either add VuePikaday to your `package.json` dependencies or HTML as script tag.
6 | ::: warning NOTE
7 | VuePikaday has also [Pikaday](https://github.com/dbushell/Pikaday) and [moment.js](http://momentjs.com)
8 | as `peerDependencies` and requires them to be installed / included in order to work.
9 | :::
10 |
11 | ## NPM
12 |
13 | ```bash
14 | npm install --save @netcz/vue-pikaday
15 | ```
16 |
17 | or
18 |
19 | ```bash
20 | yarn add @netcz/vue-pikaday
21 | ```
22 |
23 | ## Standalone
24 |
25 | 1. Download latest [release](https://github.com/netcz/vue-pikaday/releases) of VuePikaday or use latest [jsDelivr](https://www.jsdelivr.com/package/npm/@netcz/vue-pikaday) links
26 | 2. Add to your HTML head section (after Vue) using script tag
27 |
28 | ```html
29 |
30 |
31 |
32 |
33 |
34 | or
35 |
36 | ```
37 |
38 | ::: warning
39 | **DO NOT** use jsDelivr's "combine" method (even its tempting) as it uglifies code and [Pikaday](https://github.com/dbushell/Pikaday) relies on "moment" variable in scope in order to use it.
40 | :::
41 |
--------------------------------------------------------------------------------
/docs/guide/usage.md:
--------------------------------------------------------------------------------
1 | # Usage
2 |
3 | [[toc]]
4 |
5 | !!!include(usage/registration.md)!!!
6 | !!!include(usage/basic-example.md)!!!
7 | !!!include(usage/attributes-event-listeners.md)!!!
8 | !!!include(usage/pikaday-options.md)!!!
9 | !!!include(usage/manual-trigger.md)!!!
10 |
--------------------------------------------------------------------------------
/docs/guide/usage/attributes-event-listeners.md:
--------------------------------------------------------------------------------
1 | ## Attributes and event listeners
2 |
3 | Component passes attributes and binds event listeners down to underlying `input`.
4 | All attributes and/or event listeners you are used to use will work.
5 |
6 | ::: tip
7 | Open browser console to see event listeners working.
8 | :::
9 |
10 |
11 |
12 |
13 |
14 | ```vue
15 |
16 |
22 |
23 |
24 |
43 | ```
44 |
--------------------------------------------------------------------------------
/docs/guide/usage/basic-example.md:
--------------------------------------------------------------------------------
1 | ## Basic example
2 |
3 | Simply include `vue-pikaday` component with `v-model` defined.
4 |
5 |
6 |
7 |
8 |
9 | ```vue
10 |
11 |
14 |
15 |
16 |
17 |
33 | ```
34 |
--------------------------------------------------------------------------------
/docs/guide/usage/manual-trigger.md:
--------------------------------------------------------------------------------
1 | ## Manually toggle datepicker
2 |
3 | To delegate datepicker opening to other element than underlying input, use two-way `v-p-visible` directive.
4 |
5 |
6 |
7 |
8 |
9 | ```vue
10 |
11 |
15 |
16 |
17 |
18 |
40 | ```
41 |
--------------------------------------------------------------------------------
/docs/guide/usage/pikaday-options.md:
--------------------------------------------------------------------------------
1 | ## Passing options to underlying Pikaday
2 |
3 | Options can be passed using `options` prop on component.
4 | To see all possible options, consult official Pikaday [documentation](https://github.com/dbushell/Pikaday#configuration).
5 |
6 | ::: tip
7 | see [Config Reference](/config/#props) for more info
8 | :::
9 |
10 |
11 |
12 |
13 |
14 | ```vue
15 |
16 |
20 |
21 |
22 |
38 | ```
39 |
--------------------------------------------------------------------------------
/docs/guide/usage/registration.md:
--------------------------------------------------------------------------------
1 | ## Component registration
2 |
3 | ::: warning NOTE
4 | Skip this step when using standalone version.
5 | :::
6 |
7 | In your main project file add
8 |
9 | ```javascript
10 | import VuePikaday from '@netcz/vue-pikaday';
11 | Vue.use(VuePikaday);
12 | ```
13 |
14 | and from now on you'll be able to use `vue-pikaday` component in your application.
15 |
16 | ::: tip
17 | If you wish to use Pikaday default styles, include them through VuePikaday
18 |
19 | ```javascript
20 | import '@netcz/vue-pikaday/dist/vue-pikaday.min.css';
21 | ```
22 | :::
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@netcz/vue-pikaday",
3 | "scope": "netcz",
4 | "private": false,
5 | "publishConfig": {
6 | "access": "public"
7 | },
8 | "version": "0.0.0-development",
9 | "description": "VueJS wrapper component for Pikaday datepicker",
10 | "main": "dist/vue-pikaday.common.js",
11 | "browser": "dist/vue-pikaday.js",
12 | "module": "dist/vue-pikaday.esm.js",
13 | "style": "dist/vue-pikaday.css",
14 | "author": "Michal Szajter ",
15 | "license": "MIT",
16 | "homepage": "https://netcz.github.io/vue-pikaday",
17 | "repository": {
18 | "type": "git",
19 | "url": "https://git@github.com/NetCZ/vue-pikaday.git"
20 | },
21 | "bugs": {
22 | "url": "https://github.com/NetCZ/vue-pikaday/issues"
23 | },
24 | "husky": {
25 | "hooks": {
26 | "commit-msg": "commitlint -e"
27 | }
28 | },
29 | "scripts": {
30 | "dev": "rollup -c -w",
31 | "build": "rollup -c",
32 | "pretest": "yarn lint && yarn build",
33 | "test:esm": "yarn cy:run --env mode=esm",
34 | "test:standalone": "yarn cy:run --env mode=standalone",
35 | "test": "yarn test:esm && yarn test:standalone",
36 | "cy:run": "cypress run $RECORD",
37 | "cy:open": "cypress open",
38 | "lint": "eslint src",
39 | "semantic-release": "semantic-release",
40 | "snyk-protect": "snyk protect",
41 | "prepublishOnly": "yarn snyk-protect",
42 | "docs:dev": "vuepress dev docs",
43 | "docs:build": "vuepress build docs"
44 | },
45 | "keywords": [
46 | "vue",
47 | "vuejs",
48 | "pikaday",
49 | "datepicker"
50 | ],
51 | "dependencies": {
52 | "lodash": "^4.17.21"
53 | },
54 | "peerDependencies": {
55 | "moment": "^2.24.0",
56 | "pikaday": "^1.8.0",
57 | "vue": "^2.6.10"
58 | },
59 | "devDependencies": {
60 | "@babel/core": "^7.9.0",
61 | "@babel/preset-env": "^7.9.0",
62 | "@babel/preset-flow": "^7.9.0",
63 | "@babel/register": "^7.5.5",
64 | "@commitlint/cli": "^8.1.0",
65 | "@commitlint/config-conventional": "^8.1.0",
66 | "@commitlint/travis-cli": "^8.1.0",
67 | "@cypress/rollup-preprocessor": "https://github.com/dschulten/cypress-rollup-preprocessor",
68 | "babel-eslint": "^10.0.2",
69 | "chai": "^4.2.0",
70 | "cypress": "^4.2.0",
71 | "cypress-vue-unit-test": "^1.11.3",
72 | "eslint": "^6.1.0",
73 | "eslint-plugin-flowtype": "^4.3.1",
74 | "flow-bin": "^0.121.0",
75 | "husky": "^4.2.1",
76 | "moment": "^2.24.0",
77 | "pikaday": "^1.8.0",
78 | "rollup": "^2.3.0",
79 | "rollup-plugin-babel": "^4.4.0",
80 | "rollup-plugin-commonjs": "^10.0.2",
81 | "rollup-plugin-copy": "^3.1.0",
82 | "rollup-plugin-html": "^0.2.1",
83 | "rollup-plugin-node-resolve": "^5.2.0",
84 | "rollup-plugin-postcss": "^2.0.3",
85 | "rollup-plugin-terser": "^5.3.0",
86 | "semantic-release": "^17.0.0",
87 | "snyk": "^1.316.1",
88 | "vue": "^2.6.11",
89 | "vue-template-compiler": "^2.6.11",
90 | "lodash": "^4.17.21",
91 | "markdown-it": "^10.0.0",
92 | "markdown-it-attrs": "^3.0.2",
93 | "markdown-it-include": "^1.1.0",
94 | "vue-server-renderer": "^2.6.11",
95 | "vuepress": "^1.4.1"
96 | },
97 | "resolutions": {
98 | "tar": "^4.4.10",
99 | "marked": "^0.6.2"
100 | },
101 | "snyk": true
102 | }
103 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import camelCase from 'lodash/camelCase';
3 |
4 | import babel from 'rollup-plugin-babel';
5 | import resolve from 'rollup-plugin-node-resolve';
6 | import commonjs from 'rollup-plugin-commonjs';
7 | import { terser } from 'rollup-plugin-terser';
8 | import postcss from 'rollup-plugin-postcss';
9 | import copy from 'rollup-plugin-copy';
10 |
11 | import { peerDependencies, scope, name as nameWithScope } from './package.json';
12 |
13 | const name = nameWithScope.replace(`@${ scope }/`, '');
14 | const base = path.resolve(__dirname, './');
15 | const src = path.resolve(base, 'src');
16 | const dist = path.resolve(base, 'dist');
17 | const debug = process.env.DEBUG === 'true';
18 |
19 | const baseConfig = {
20 | input: path.resolve(src, 'index.js'),
21 | external: Object.keys(peerDependencies),
22 | plugins: [
23 | resolve({ external: ['vue'] }),
24 | babel({
25 | babelrc: false,
26 | presets: [
27 | ['@babel/preset-env', { 'modules': false }],
28 | '@babel/preset-flow'
29 | ],
30 | exclude: 'node_modules/**'
31 | }),
32 | commonjs(),
33 | postcss({
34 | extract: dist + '/vue-pikaday.min.css',
35 | minimize: true
36 | }),
37 | copy({
38 | targets: {
39 | 'node_modules/pikaday/css/pikaday.css': dist + '/vue-pikaday.css'
40 | }
41 | }),
42 | terser({
43 | mangle: {
44 | reserved: ['Pikaday', 'moment']
45 | },
46 | compress: {
47 | drop_console: !debug,
48 | drop_debugger: !debug
49 | }
50 | })
51 | ]
52 | };
53 |
54 | export default [
55 | {
56 | ...baseConfig,
57 | output: {
58 | format: 'cjs',
59 | name: camelCase(name),
60 | file: path.resolve(dist, name + '.common.js'),
61 | sourcemap: true,
62 | globals: {
63 | moment: 'moment',
64 | pikaday: 'Pikaday'
65 | },
66 | },
67 | },
68 | {
69 | ...baseConfig,
70 | output: {
71 | format: 'umd',
72 | name: camelCase(name),
73 | file: path.resolve(dist, name + '.js'),
74 | sourcemap: true,
75 | globals: {
76 | moment: 'moment',
77 | pikaday: 'Pikaday'
78 | },
79 | }
80 | },
81 | {
82 | ...baseConfig,
83 | output: {
84 | format: 'es',
85 | file: path.resolve(dist, name + '.esm.js'),
86 | sourcemap: true
87 | }
88 | }
89 | ];
90 |
--------------------------------------------------------------------------------
/src/component.js:
--------------------------------------------------------------------------------
1 | //@flow
2 |
3 | import moment from 'moment';
4 | import Pikaday from 'pikaday';
5 | import isDate from 'lodash/isDate';
6 | import isString from 'lodash/isString';
7 |
8 | import 'pikaday/css/pikaday.css';
9 |
10 | function isEvent(value) {
11 | return value instanceof Event || (value && value.constructor && value.constructor.name === 'Event');
12 | }
13 |
14 | export default {
15 | name: 'vue-pikaday',
16 | inheritAttrs: false,
17 | props: {
18 | value: {
19 | validator(value: typeof undefined | null | Date | Event | string): boolean {
20 | const allowedTypes: Array = [undefined, null];
21 |
22 | if (isEvent(value)) {
23 | return true;
24 | }
25 |
26 | if (isDate(value)) {
27 | return true;
28 | }
29 |
30 | if(isString(value) && moment(value).isValid()) {
31 | return true;
32 | }
33 |
34 | for (const type of allowedTypes) {
35 | const allowed: boolean = value === type || typeof value === type;
36 |
37 | if (allowed) {
38 | return true;
39 | }
40 | }
41 |
42 | return false;
43 | },
44 | required: true
45 | },
46 | options: {
47 | required: false,
48 | default() {
49 | return {};
50 | }
51 | },
52 | autoDefault: {
53 | type: Boolean,
54 | required: false,
55 | default: false
56 | }
57 | },
58 | data() {
59 | return {
60 | visible: false,
61 | elAttrs: {
62 | type: 'text'
63 | },
64 | defaultOptions: {
65 | format: 'D MMM YYYY'
66 | }
67 | };
68 | },
69 | computed: {
70 | elementAttributes(): Object {
71 | return Object.assign({}, this.$attrs, this.elAttrs);
72 | },
73 | mergedOptions() {
74 | return Object.assign({}, this.defaultOptions, this.options);
75 | }
76 | },
77 | render(h: Function): Object {
78 | return h('input', {
79 | attrs: this.elementAttributes,
80 | on: this.$listeners,
81 | value: this.inputValue(this.value)
82 | }, this.$slots.default);
83 | },
84 | mounted() {
85 | this.create();
86 |
87 | this.$watch('value', (value: typeof undefined | null | Date) => {
88 | if (!isDate(value)) {
89 | value = null;
90 | }
91 | if (!this.visible) {
92 | this.pikaday.setDate(value, true);
93 | }
94 | this.change(value);
95 | });
96 | },
97 | beforeDestroy() {
98 | this.destroy();
99 | },
100 | watch: {
101 | options: {
102 | handler() {
103 | this.reload();
104 | },
105 | deep: true
106 | }
107 | },
108 | methods: {
109 | create() {
110 | this.mergedOptions.field = this.$el;
111 |
112 | this.bindListener('onSelect', () => this.onSelect());
113 | this.bindListener('onOpen', () => this.onOpen());
114 | this.bindListener('onClose', () => this.onClose());
115 |
116 | this.pikaday = new Pikaday(this.mergedOptions);
117 |
118 | let defaultValue: typeof undefined | null | Date = this.value;
119 |
120 | if (!this.value && this.autoDefault) {
121 | defaultValue = moment().toDate();
122 | this.change(defaultValue);
123 | }
124 |
125 | this.pikaday.setDate(defaultValue, true);
126 |
127 | if (this.mergedOptions.bound === false) {
128 | this.hide();
129 | } else {
130 | this.visible ? this.show() : this.hide();
131 | }
132 | },
133 | destroy() {
134 | this.pikaday.destroy();
135 | },
136 | reload() {
137 | this.destroy();
138 | this.create();
139 | },
140 | change(value: typeof undefined | null | Date) {
141 | this.$emit('input', value);
142 | this.$emit('input-value', this.inputValue(value));
143 | },
144 | inputValue(value: typeof undefined | null | Date): string | null {
145 | if (!isDate(value)) {
146 | return null;
147 | }
148 | const inputValue: moment = moment(value);
149 | return inputValue.isValid() ? inputValue.format(this.mergedOptions.format) : null;
150 | },
151 | onSelect() {
152 | this.change(this.pikaday.getDate());
153 | },
154 | onOpen() {
155 | this.visible = true;
156 | },
157 | onClose() {
158 | if (!isDate(this.value)) {
159 | this.pikaday.setDate(null, true);
160 | this.change(null);
161 | }
162 | this.visible = false;
163 | },
164 | show() {
165 | this.pikaday.show();
166 | },
167 | hide() {
168 | this.pikaday.hide();
169 | },
170 | bindListener(listener: Function, cb: Function) {
171 | if (this.mergedOptions[listener]) {
172 | const old: Function = this.mergedOptions[listener];
173 | this.mergedOptions[listener] = (...args) => {
174 | old(args);
175 | cb.apply(this);
176 | };
177 | } else {
178 | this.mergedOptions[listener] = cb;
179 | }
180 | }
181 | }
182 | };
183 |
--------------------------------------------------------------------------------
/src/directives.js:
--------------------------------------------------------------------------------
1 | //@flow
2 |
3 | import _debounce from 'lodash/debounce';
4 |
5 | function changeState(state: boolean, target: Object, expression: string, instance: Object | null = null) {
6 | target[expression] = state;
7 | instance = instance || target;
8 | state ? instance.show() : instance.hide();
9 | }
10 |
11 | export const VuePikadayVisible = {
12 | inserted(el: Object, binding: Object, VNode: Object) {
13 | const instance: Object = VNode.componentInstance;
14 |
15 | instance.onOpen = () => _debounce(() => changeState(true, VNode.context, binding.expression, instance), 100);
16 |
17 | instance.onClose = _debounce(() => changeState(false, VNode.context, binding.expression, instance), 100);
18 | },
19 | update(el: Object, binding: Object, VNode: Object) {
20 | _debounce(() => changeState(binding.value, VNode.componentInstance, binding.expression), 100)();
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import VuePikaday from './component';
2 | import { VuePikadayVisible } from './directives';
3 |
4 | const VuePikadayPlugin = {
5 | install(Vue) {
6 | Vue.component(VuePikaday.name, VuePikaday);
7 | Vue.directive('p-visible', VuePikadayVisible);
8 | }
9 | };
10 |
11 | if (typeof window !== 'undefined' && window.Vue) {
12 | window.Vue.use(VuePikadayPlugin);
13 | }
14 |
15 | export default VuePikadayPlugin;
16 |
--------------------------------------------------------------------------------
/test/specs/attributes-event-listeners.js:
--------------------------------------------------------------------------------
1 | const component = {
2 | template: ``,
10 | methods: {
11 | focus() {
12 | this.$emit('focused');
13 | },
14 | blur() {
15 | this.$emit('blurred');
16 | }
17 | }
18 | };
19 |
20 | describe('Attributes & events', () => {
21 | before(() => cy.mount(component));
22 |
23 | it('renders custom attributes', () => {
24 | cy.get('[data-vue-pikaday]').should('have.attr', 'placeholder', 'Pick a date');
25 | });
26 |
27 | it('dispatches event listeners', () => {
28 | const focusSpy = cy.spy();
29 | const blurSpy = cy.spy();
30 |
31 | Cypress.vue.$on('focused', focusSpy);
32 | Cypress.vue.$on('blurred', blurSpy);
33 |
34 | cy.get('[data-vue-pikaday]').as('picker').click().then(() => {
35 | expect(focusSpy).to.be.calledOnce;
36 | });
37 |
38 | cy.get('@picker').blur().then(() => {
39 | expect(blurSpy).to.be.calledOnce;
40 | });
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/test/specs/basic.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 |
3 | const component = {
4 | template: ''
5 | };
6 |
7 | describe('Basic', () => {
8 | before(() => cy.mount(component));
9 |
10 | it('renders', () => {
11 | cy.get('[data-vue-pikaday]').should('be.visible');
12 | cy.get('.pika-single').should('not.be.visible');
13 | });
14 |
15 | it('focuses / blurs picker', () => {
16 | cy.get('[data-vue-pikaday]').as('picker').focus();
17 | cy.get('.pika-single').should('be.visible');
18 | cy.get('@picker').blur();
19 | cy.get('.pika-single').should('not.be.visible');
20 | });
21 |
22 | it('selects current date via picker', () => {
23 | cy.get('[data-vue-pikaday]').focus();
24 | cy.get(`[data-day="${ moment().date() }"] button`).click();
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/test/specs/custom-formating.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 |
3 | function customFormat(date, format) {
4 | const start = moment(date, format).day(1);
5 | const end = moment(date, format).day(5);
6 | return `${ start.format(format) } - ${ end.format(format) }`;
7 | }
8 |
9 | const component = {
10 | template: ``,
15 | data() {
16 | return {
17 | options: {
18 | pickWholeWeek: true,
19 | toString: customFormat,
20 | parse: () => moment(this.date)
21 | }
22 | };
23 | }
24 | };
25 |
26 | describe('Custom formatting', () => {
27 | before(() => cy.mount(component));
28 |
29 | it('renders input value using custom formatter', () => {
30 | cy.get('[data-vue-pikaday]').as('picker').focus();
31 | cy.get(`[data-day="${ moment().date() }"] button`).click();
32 |
33 | cy.get('@picker').should('have.value', customFormat(moment().date(), 'D MMM YYYY'));
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/test/specs/external-date-change.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 |
3 | const updatedDateCurrent = moment();
4 | const updatedDateWeekForward = moment().add(7, 'days');
5 |
6 | const component = {
7 | template: `
8 |
12 | Internal date: {{ date ? date : 'null' }}
13 |
14 |
15 |
16 |
`,
17 | methods: {
18 | updateDateCurrent() {
19 | this.date = updatedDateCurrent.toDate();
20 | },
21 | updateDateWeekForward() {
22 | this.date = updatedDateWeekForward.toDate();
23 | },
24 | clear() {
25 | this.date = null;
26 | }
27 | }
28 | };
29 |
30 | describe('External changes', () => {
31 | before(() => cy.mount(component));
32 |
33 | it('updates Pikaday value from outside', () => {
34 | cy.get('[data-test="update-button-current"]').click();
35 | cy.get('[data-vue-pikaday]').as('picker').should('have.value', updatedDateCurrent.format('D MMM YYYY'));
36 |
37 | cy.get('[data-test="update-button-week"]').click();
38 | cy.get('@picker').should('have.value', updatedDateWeekForward.format('D MMM YYYY'));
39 | });
40 |
41 | it('handles removing characters', () => {
42 | cy.get('[data-test="update-button-current"]').click();
43 |
44 | cy.get('[data-vue-pikaday]').as('picker').type('{backspace}');
45 |
46 | cy.get('[data-internal-value]').should('contain', 'null');
47 | });
48 |
49 | it('handles invalid value typed in', () => {
50 | cy.get('[data-vue-pikaday]').as('picker').clear().type('invalid value').blur().should('have.value', '');
51 |
52 | cy.get('@picker').clear().type('13 Se').blur().should('have.value', '');
53 | });
54 |
55 | it('handles clearing value from outside', () => {
56 | cy.get('[data-test="update-button-current"]').click();
57 | cy.get('[data-test="clear-button"]').click();
58 | cy.get('[data-vue-pikaday]').as('picker').should('have.value', '');
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/test/specs/manual-trigger.js:
--------------------------------------------------------------------------------
1 | const component = {
2 | template: `
3 |
9 |
10 |
`,
11 | data() {
12 | return {
13 | visible: false
14 | };
15 | }
16 | };
17 |
18 | describe('Manual trigger', () => {
19 | before(() => cy.mount(component));
20 |
21 | it('shows / hide via external trigger', () => {
22 | cy.get('[data-test="manual"]').click();
23 | cy.get('.pika-single').should('be.visible');
24 |
25 | cy.get('[data-test="manual"]').click();
26 | cy.get('.pika-single').should('not.be.visible');
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/test/specs/pikaday-options.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 |
3 | const allowedDateStart = moment().date(5);
4 | const allowedDateEnd = moment(allowedDateStart).add(6, 'days');
5 |
6 | const component = {
7 | template: `
8 |
14 |
15 |
`,
16 | data() {
17 | return {
18 | options: {
19 | format: 'YYYY/MM/DD',
20 | minDate: allowedDateStart.toDate(),
21 | maxDate: allowedDateEnd.toDate()
22 | }
23 | };
24 | },
25 | methods: {
26 | updateOptions() {
27 | this.options.minDate = moment(allowedDateStart).subtract(2, 'day').toDate();
28 | this.options.maxDate = moment(allowedDateEnd).subtract(2, 'day').toDate();
29 | }
30 | }
31 | };
32 |
33 | describe('Pikaday options', () => {
34 | before(() => cy.mount(component));
35 |
36 | it('have current date filled by default', () => {
37 | let testDate = moment();
38 |
39 | if (testDate >= allowedDateEnd) {
40 | testDate = allowedDateEnd;
41 | }
42 |
43 | if (testDate <= allowedDateStart) {
44 | testDate = allowedDateStart;
45 | }
46 |
47 | cy.get('[data-vue-pikaday]').should('have.value', testDate.format('YYYY/MM/DD'));
48 | });
49 |
50 | it('have custom display format', () => {
51 | cy.get('[data-vue-pikaday]').as('picker').click();
52 | cy.get(`[data-day="${ allowedDateStart.date() }"] button`).click();
53 | cy.get('@picker').should('have.value', allowedDateStart.format('YYYY/MM/DD'));
54 | });
55 |
56 | it('restricts dates selection to specific week', () => {
57 | cy.get('[data-vue-pikaday]').as('picker').click();
58 | cy.get(`[data-day="${ moment(allowedDateStart).subtract(1, 'day').date() }"]`).should('have.class', 'is-disabled');
59 |
60 | cy.get('@picker').click();
61 | cy.get(`[data-day="${ moment(allowedDateStart).add(8, 'days').date() }"]`).should('have.class', 'is-disabled');
62 |
63 | const plus5Days = moment(allowedDateStart).add(5, 'days');
64 |
65 | cy.get('@picker').click();
66 | cy.get(`[data-day="${ plus5Days.date() }"]`).should('not.have.class', 'is-disabled');
67 | cy.get(`[data-day="${ plus5Days.date() }"] button`).click();
68 | cy.get('@picker').should('have.value', plus5Days.format('YYYY/MM/DD'));
69 | });
70 |
71 | it('updates dates restriction selection', () => {
72 | cy.get('[data-test="update-options"]').click();
73 |
74 | cy.get('[data-vue-pikaday]').as('picker').click();
75 | cy.get(`[data-day="${ moment(allowedDateStart).subtract(3, 'days').date() }"]`).should('have.class', 'is-disabled');
76 |
77 | cy.get('@picker').click();
78 | cy.get(`[data-day="${ moment(allowedDateEnd).subtract(1, 'day').date() }"]`).should('have.class', 'is-disabled');
79 |
80 | const plus2Days = moment(allowedDateStart).add(2, 'days');
81 |
82 | cy.get('@picker').click();
83 | cy.get(`[data-day="${ plus2Days.date() }"]`).should('not.have.class', 'is-disabled');
84 | cy.get(`[data-day="${ plus2Days.date() }"] button`).click();
85 | cy.get('@picker').should('have.value', plus2Days.format('YYYY/MM/DD'));
86 | });
87 | });
88 |
--------------------------------------------------------------------------------