├── .babelrc ├── .gitignore ├── .npmignore ├── .scripts ├── build-api-docs.sh ├── build-docs.sh ├── build.sh ├── bump.sh ├── release-docs.sh └── template.hbs ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bower.json ├── docs ├── README.md ├── _coverpage.md ├── _demo.md ├── _sidebar.md ├── _stylesheets.md ├── demo │ ├── .babelrc │ ├── .gitignore │ ├── config │ │ └── webpack.config.js │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── app.js │ │ ├── dpicker.cycle.js │ │ ├── index.js │ │ └── style.scss ├── index.html └── logo.svg ├── examples ├── angular2.directive.ts └── index.html ├── jsdoc.conf.json ├── module.test.js ├── package-lock.json ├── package.json ├── src ├── adapters │ └── moment.js ├── all.js ├── datetime.js ├── dpicker.js ├── dpicker.moment.js ├── plugins │ ├── arrow-navigation.js │ ├── modifiers.js │ ├── monthAndYear.js │ ├── navigation.js │ └── time.js └── polyfills.js └── test ├── adapters └── moment.spec.js ├── dpicker.spec.js ├── mocha.global.js ├── mocha.opts └── plugins ├── arrow-navigation.spec.js ├── modifiers.spec.js └── time.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "yo-yoify" 4 | ], 5 | "presets": [ 6 | "env" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | docs/_api.md 5 | docs/CHANGELOG.md 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo 2 | !dist/*.js 3 | test.js 4 | gulpfile.js 5 | screen.png 6 | bower_components 7 | bump.sh 8 | src/*.spec.js 9 | -------------------------------------------------------------------------------- /.scripts/build-api-docs.sh: -------------------------------------------------------------------------------- 1 | #!env bash 2 | $(npm bin)/jsdoc2md --template .scripts/template.hbs -f src/dpicker.js src/plugins/*.js src/adapters/*.js > docs/_api.md 3 | -------------------------------------------------------------------------------- /.scripts/build-docs.sh: -------------------------------------------------------------------------------- 1 | #!env bash 2 | bash .scripts/build.sh docs 3 | cp CHANGELOG.md docs/CHANGELOG.md 4 | cd docs/demo 5 | npm install 6 | NODE_ENV=production $(npm bin)/webpack --config config/webpack.config.js 7 | cd ../../ 8 | bash .scripts/build-api-docs.sh 9 | -------------------------------------------------------------------------------- /.scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!env bash 2 | 3 | browserify="$(npm bin)/browserify" 4 | uglifyjs="$(npm bin)/uglifyjs" 5 | 6 | GLOBAL_ARGS="-g unassertify -t babelify -x moment" 7 | MIN_ARGS="$GLOBAL_ARGS -g uglifyify" 8 | 9 | RELEASE_BUILD=0 10 | if [[ $1 != '' ]]; then 11 | RELEASE_BUILD=1 12 | fi 13 | 14 | build() { 15 | args=$GLOBAL_ARGS 16 | 17 | if [[ $3 == 1 ]]; then 18 | args="-s dpicker $args" 19 | else 20 | args="-s ${2/./_} $args" 21 | fi 22 | 23 | echo "Browserifying src/$1 => $2" 24 | sh -c "$browserify $args src/$1 -o dist/$2.js" 25 | 26 | if [[ $RELEASE_BUILD == 1 ]]; then 27 | echo "Browserifying src/$1 => $2.min.js" 28 | sh -c "$browserify -g uglifyify $args src/$1 | $uglifyjs -c > dist/$2.min.js" 29 | fi 30 | } 31 | 32 | rm dist/* &> /dev/null 33 | mkdir dist &> /dev/null 34 | 35 | echo 36 | echo "Build" 37 | 38 | build "dpicker.moment" "dpicker" 1 & 39 | build "dpicker" "dpicker.core" 1 & 40 | build "plugins/time" "dpicker.time" & 41 | build "plugins/modifiers" "dpicker.modifiers" & 42 | build "plugins/arrow-navigation" "dpicker.arrow-navigation" & 43 | build "plugins/navigation" "dpicker.navigation" & 44 | build "plugins/monthAndYear" "dpicker.monthAndYear" & 45 | 46 | wait 47 | 48 | if [[ $RELEASE_BUILD == 1 ]]; then 49 | echo 50 | echo "Build release files" 51 | 52 | # Build date + time 53 | echo "Browserifying datetime" 54 | sh -c "$browserify $GLOBAL_ARGS -s dpicker src/datetime.js -o dist/dpicker.datetime.js" 55 | sh -c "$browserify $MIN_ARGS -s dpicker src/datetime.js -o dist/dpicker.datetime.min.js" 56 | # Build all 57 | echo "Browserifying all" 58 | sh -c "$browserify $GLOBAL_ARGS -s dpicker src/all.js -o dist/dpicker.all.js" 59 | sh -c "$browserify $MIN_ARGS -s dpicker src/all -o dist/dpicker.all.min.js" 60 | 61 | echo "Browserifying polyfills" 62 | $browserify -g uglifyify src/polyfills.js -o dist/polyfills.min.js 63 | 64 | echo 65 | echo "Sizes" 66 | 67 | for f in dist/*.min.js; do 68 | echo "$f.gz: " $(node -pe "$(gzip -c $f | wc -c) * 0.001") kb 69 | done 70 | fi 71 | 72 | exit 0 73 | -------------------------------------------------------------------------------- /.scripts/bump.sh: -------------------------------------------------------------------------------- 1 | #!env bash 2 | 3 | [[ '' == $1 ]] && echo "Please provide patch, minor, major argument" && exit 1 4 | 5 | tag='next' 6 | 7 | if [[ $1 == 'patch' || $1 == 'minor' || $1 == 'major' ]]; then 8 | tag='latest' 9 | fi 10 | 11 | bash .scripts/build.sh forpublish 12 | npm test 13 | newver=$(npm --no-git-tag-version version $1) 14 | git add -f dist package.json package-lock.json 15 | git commit -m $newver 16 | git tag $newver 17 | npm publish --tag $tag 18 | git reset --hard HEAD~1 19 | newver=$(npm --no-git-tag-version version $1) 20 | git add package.json package-lock.json 21 | git commit -m $newver 22 | git push --tags 23 | git push 24 | -------------------------------------------------------------------------------- /.scripts/release-docs.sh: -------------------------------------------------------------------------------- 1 | #!env bash 2 | DPICKER_VERSION=$(jq -r .version package.json) 3 | 4 | git checkout gh-pages 5 | git reset --hard origin/master 6 | bash .scripts/build-docs.sh 7 | mv -f docs/* . 8 | sed -i.bak "s/DPICKER_VERSION/$DPICKER_VERSION/g" _coverpage.md 9 | sed -i.bak "s/DPICKER_VERSION/$DPICKER_VERSION/g" README.md 10 | rm *.bak 11 | touch .nojekyll 12 | git add -f *.md 13 | git add .nojekyll index.html logo.svg demo 14 | git add -f demo/public/dist 15 | git commit -m 'bump doc' 16 | git push -fu origin gh-pages 17 | rm -r demo 18 | rm index.html logo.svg 19 | git checkout master 20 | git reset --hard origin/master 21 | -------------------------------------------------------------------------------- /.scripts/template.hbs: -------------------------------------------------------------------------------- 1 | {{#function name="DPicker"}} 2 | {{>docs}} 3 | {{/function}} 4 | {{#function name="onChange"}} 5 | {{>docs}} 6 | {{/function}} 7 | {{#module name="MomentAdapter"}} 8 | {{>docs}} 9 | {{/module}} 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "8" 5 | 6 | install: 7 | - npm install 8 | - npm run build 9 | 10 | before_script: 11 | - npm install -g codecov 12 | 13 | after_success: 14 | - npm run coverage 15 | - codecov coverage/lcov.info 16 | 17 | matrix: 18 | fast_finish: true 19 | 20 | # container-base 21 | sudo: false 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 5.0.2 4 | 5 | - Fix sibling months day click gave incorrect date 6 | 7 | ## 5.0.1 8 | 9 | - Globals are evil. By trying to make things easy to use, we declared a global `window.DPicker` variable. Since `5.0.1` it's not the case anymore and it will be declared as `window.dpicker`. 10 | It's recommended that you use a bundle system though (webpack, rollup, browserify...). 11 | 12 | - Plugins are exporting a `function`. Makes things easier to pre-build packages. This means that this won't work anymore: 13 | 14 | ```javascript 15 | 16 | 17 | ``` 18 | 19 | To get date + time, you should use: 20 | 21 | ```javascript 22 | 23 | ``` 24 | 25 | Or the package with every module: 26 | 27 | ```javascript 28 | 29 | ``` 30 | 31 | ## 5.0.0 32 | 33 | - Use browserify instead of gulp 34 | - Use real dom, real dates ([nanomorph](https://github.com/yoshuawuyts/nanomorph) for diffing and [bel](https://github.com/shama/bel) 35 | - Use [standard](https://github.com/feross/standard) 36 | - Get rid of momentjs hard dependency (still required for now) 37 | - Implement a Date Adapter for future libraries support 38 | - Simplify plugins api 39 | - uses immutable `Date` instances internally! [BC] 40 | 41 | No more `_modules` stuff. Just simple objects creation: 42 | 43 | ```javascript 44 | DPicker.renders.closeButton = function renderCloseButton(events, data) { 45 | const button = document.createElement('button') 46 | button.innerText = 'Confirm' 47 | button.type = 'button' 48 | button.classList.add('dpicker-close-button') 49 | button.addEventListener('click', events.hidePicker) 50 | return button 51 | } 52 | 53 | DPicker.events.hidePicker = function hidePicker() { 54 | this.display = false 55 | } 56 | ``` 57 | 58 | - Incremental builds 59 | 60 | You can use `dpicker.datetime.js` directly. Coming soon builds with `date-fns`. Available builds: 61 | 62 | ``` 63 | dist/dpicker.all.min.js.gz: 7.698 kb 64 | dist/dpicker.arrow-navigation.min.js.gz: 0.9410000000000001 kb 65 | dist/dpicker.core.min.js.gz: 4.892 kb 66 | dist/dpicker.datetime.min.js.gz: 6.947 kb 67 | dist/dpicker.min.js.gz: 5.699 kb 68 | dist/dpicker.modifiers.min.js.gz: 0.58 kb 69 | dist/dpicker.navigation.min.js.gz: 0.6900000000000001 kb 70 | dist/dpicker.time.min.js.gz: 2.08 kb 71 | dist/polyfills.min.js.gz: 4.5200000000000005 kb 72 | ``` 73 | 74 | - Improve docs (A LOT) https://soyuka.github.io/dpicker thanks to [jsdoc2md](https://github.com/jsdoc2md/jsdoc-to-markdown) and [docsify](https://github.com/QingWei-Li/docsify/) 75 | - Build a decent demo using cycle.js 76 | - naming public stuff #33 77 | 78 | `_events` => `events` 79 | `_data` => `data` 80 | 81 | - new getter for `empty` 82 | 83 | ## 4.2.0 84 | 85 | `onChange` now has a second argument giving informations about the recent change, for example: 86 | 87 | ```javascript 88 | dpicker.onChange = function(data, event) { 89 | console.log('Model changed? %s', event.modelChanged ? 'yes' : 'no') 90 | console.log('DPicker event: %s', event.name) 91 | console.log('DPicker original event: %s', event.event) 92 | } 93 | ``` 94 | 95 | ## 4.0.9 96 | 97 | - Adds an option to enable sibling month days 98 | 99 | ## 4.0.8 100 | 101 | - Adds an option `concatHoursAndMinutes` to merge hours and minutes in one `select` 102 | 103 | ## 4.0.0 104 | 105 | - Drop hyperscript library, it's not needed anymore and it's not possible to add a custom one 106 | - An invalid Date doesn't reset the value to the closest correct one, it leaves the choice to the user instead 107 | - A class `.invalid` is added to the input 108 | - `aria-*` attributes are now working 109 | 110 | ## 3.1.1 111 | 112 | Add touch friendly behavior #20 113 | 114 | ## 3.1.0 115 | 116 | Drop maquettejs hard dependency (see #1). You can now use your own hyperscript library, for example with mithriljs: 117 | 118 | ``` 119 | let dpicker = DPicker(label, { 120 | h: mithril, 121 | mount: function(element, toRender) { 122 | mithril.render(element, toRender()) 123 | }, 124 | redraw: mithril.redraw 125 | }) 126 | ``` 127 | 128 | ## 3.0.0 129 | 130 | `time-format` renamed to `meridiem` 131 | 132 | ## 2.0.0 133 | 134 | `isEmpty` renamed `empty` 135 | 136 | ## 1.3.0 137 | 138 | Drop `previousYear` and `futureYear`. Those are replaced by `min` and `max`: 139 | - `input[type="date"]` polyfill (#2) 140 | - allows to change the months selection on max/min dates 141 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Antoine Bluchet 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DPicker 2 | 3 | 4 | 5 | [![Build Status](https://img.shields.io/travis/soyuka/dpicker.svg)](https://travis-ci.org/soyuka/dpicker) 6 | [![codecov](https://img.shields.io/codecov/c/github/soyuka/dpicker.svg)](https://codecov.io/gh/soyuka/dpicker) 7 | 8 | A framework-agnostic minimal date picker. 9 | 10 | ## Quick start 11 | 12 | ```html 13 | 16 | 17 | 18 | 21 | ``` 22 | 23 | ## Read the docs 24 | 25 | - [Full Documentation (web)](https://soyuka.github.io/dpicker/) 26 | - [/docs](https://github.com/soyuka/dpicker/tree/master/docs) 27 | 28 | ## License 29 | 30 | MIT 31 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dpicker", 3 | "description": "Minimalist date picker", 4 | "main": [ 5 | "dist/dpicker.js" 6 | ], 7 | "authors": [ 8 | "soyuka" 9 | ], 10 | "license": "MIT", 11 | "keywords": [ 12 | "date", 13 | "picker" 14 | ], 15 | "homepage": "https://github.com/soyuka/dpicker", 16 | "ignore": [ 17 | "**/.*", 18 | "node_modules", 19 | "bower_components", 20 | "test.js", 21 | "src/*.spec.js", 22 | "!dist/*.js", 23 | "screen.png", 24 | "gulpfile.js", 25 | "demo", 26 | "bump.sh", 27 | "doc", 28 | "coverage" 29 | ], 30 | "dependencies": { 31 | "moment": "^2.13.0" 32 | }, 33 | "devDependencies": { 34 | "maquette": "^2.3.3", 35 | "foundation-sites": "^6.2.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://img.shields.io/travis/soyuka/dpicker.svg)](https://travis-ci.org/soyuka/dpicker) 2 | [![codecov](https://img.shields.io/codecov/c/github/soyuka/dpicker.svg)](https://codecov.io/gh/soyuka/dpicker) 3 | 4 | A framework-agnostic minimal date picker. 5 | 6 | ## Installation 7 | 8 | ### npm 9 | 10 | ``` 11 | npm install dpicker --save 12 | ``` 13 | 14 | ### bower 15 | 16 | ``` 17 | bower install dpicker --save 18 | ``` 19 | 20 | !> Package managers are only referencing `dpicker.core.js`! This is useful to build your own package! See below for pre-built packages. 21 | 22 | To keep `dpicker` small, every new feature is added through a module. You can either use those separatly, or use one of the pre-built package. 23 | 24 | We've built-in the following for an easy installation (suffix with `.min.js` for the minified version): 25 | 26 | - `dpicker.all` contains every module (~7.7kb) 27 | - `dpicker.datetime` contains only core and time (~6.9kb) 28 | - `dpicker` contains core with momentjs adapter (~5.6kb) 29 | - `dpicker.core` contains only core (~4.89kb) 30 | 31 | For example with the [unpkg cdn](https://unpkg.com): 32 | 33 | ```html 34 | 35 | ``` 36 | 37 | Modules alone: 38 | 39 | - `dpicker.arrow-navigation` enable keyboard arrows to navigate between days (~0.1kb) 40 | - `dpicker.modifiers` enable modifiers, for example `+` to get current day, `+100` to get the date in 100 days. (~0.5kb) 41 | - `dpicker.time` enable time (~2kb) 42 | 43 | When using modules, the best is to bundle them with your favorite tool, for example: 44 | 45 | ```javascript 46 | var DPicker = require('dpicker') //dpicker.core 47 | DPicker.dateAdapter = require('dpicker/src/adapters/moment') 48 | require('dpicker/dist/dpicker.time')(DPicker) 49 | ``` 50 | 51 | For old browser support, you will need the `polyfill` file: 52 | 53 | ```html 54 | 55 | ``` 56 | 57 | ?> Those polyfills are [array.prototype.fill](https://www.npmjs.com/package/array.prototype.fill) and [dom4](https://www.npmjs.com/package/dom4) 58 | 59 | The default library to handle dates is `momentjs`, just add it to your script: 60 | 61 | ```html 62 | 63 | ``` 64 | 65 | ?> I'm working on getting `date-fns` as alternative, please let me know if you wish to see other date adapters. Want to build your own take a look at the [Date adapter section](#date-adapter) 66 | 67 | ## Usage 68 | 69 | !> Global variables are not safe, but if you're not using a bundle system (wepback, rollup, browserify), you may use the global `window.dpicker` variable. 70 | 71 | To create a date picker, just init the DPicker module within a container: 72 | 73 | ```html 74 |
75 | 78 | ``` 79 | 80 | If you have an input already, you can init the datepicker with it, the date picker container will be the input parent node: 81 | 82 | ```html 83 | 87 | 90 | ``` 91 | 92 | ## Examples 93 | 94 | ### Html initalization 95 | 96 | Let's initialize every `date` and `datetime` inputs with DPicker by using this ugly one-liner: 97 | 98 | ```javascript 99 | [].slice.call(document.querySelectorAll('input[type="date"],input[type="datetime"]')).forEach(function(e){new dpicker(e);}); 100 | ``` 101 | 102 | HTML can now take multiple formats, the simplest would be: 103 | 104 | ```html 105 | 108 | ``` 109 | 110 | With a custom format and value: 111 | 112 | ```html 113 | 116 | ``` 117 | 118 | Or with every available options: 119 | 120 | ```html 121 | 124 | ``` 125 | 126 | Note that every specified attribute have to be strings, and if it's a date it should be in the given format. 127 | 128 | Now with time (24h format): 129 | 130 | ```html 131 | 134 | ``` 135 | 136 | Or with AM/PM time format (specify the time format, ie: 12, 12h) 137 | 138 | ```html 139 | 142 | ``` 143 | 144 | ### Javascript 145 | 146 | Let's do the oposite by declaring a simple html container: 147 | 148 | ```html 149 |
150 | ``` 151 | 152 | Basic initalization: 153 | 154 | ```javascript 155 | var container = document.getElementById('my-dpicker') 156 | var dp = new dpicker(container) 157 | ``` 158 | 159 | Setup almost every option: 160 | 161 | ```javascript 162 | var container = document.getElementById('my-dpicker') 163 | var dp = new dpicker(container, { 164 | model: moment(), //today 165 | min: moment('1986-01-01'), 166 | max: moment().add(1, 'year').month(11), //today + 1 year 167 | format: 'DD/MM/YYYY hh:mm A', 168 | time: true, //add time 169 | meridiem: true, //12h format 170 | months: moment.monthsShort(), 171 | days: moment.weekdaysMin() 172 | }) 173 | ``` 174 | 175 | More options are available, for a complete list [check the api documentation](_api#dpickerelement-options). 176 | 177 | Change values during the date picker life cycle: 178 | 179 | ```javascript 180 | var container = document.getElementById('my-dpicker') 181 | var dp = new dpicker(container) 182 | // do things 183 | 184 | dp.format = 'DD/MM/YYYY' 185 | dp.min = moment('01/01/1991') 186 | dp.max = moment('01/01/2020') 187 | dp.time = true 188 | ``` 189 | 190 | Available properties are listed in the [api documentation](_api#dpickerelement-options), where [some properties](_api#dpickerproperties) belong to modules. 191 | 192 | Let's get further with Javascript with the [`onChange` method](_api#onchangedata-dpickerevent). This is a hook to listen to any change that comes from DPicker. It helps integrating the date picker in any framework. 193 | 194 | ```javascript 195 | var container = document.getElementById('my-dpicker') 196 | var dp = new dpicker(container) 197 | 198 | dp.onChange = function(data, DPickerEvent) { 199 | // has the model changed? 200 | console.log(DPickerEvent.modelChanged) 201 | // the name of the internal event 202 | console.log(DPickerEvent.name) 203 | // the origin DOM event 204 | console.dir(DPickerEvent.event) 205 | } 206 | 207 | ``` 208 | 209 | ## CSS 210 | 211 | No css is included by default, here's the minimal style: 212 | 213 | ```css 214 | td.dpicker-inactive { 215 | color: grey; 216 | } 217 | 218 | button.dpicker-active { 219 | background: coral; 220 | } 221 | 222 | .dpicker-invisible { 223 | display: none; 224 | } 225 | .dpicker-visible { 226 | display: block; 227 | } 228 | ``` 229 | 230 | [You can find alternatives stylesheets here.](_stylesheets) 231 | 232 | ## Modules 233 | 234 | ### Concept 235 | 236 | As stated above, DPicker has a *built-in module system*. Actually, as DPicker is built with two core concepts, it's nothing more than: 237 | 238 | 1. render methods 239 | 2. event listeners 240 | 241 | A render method is a context-less function, which must return one DOM element. 242 | It always has two parameters: 243 | 244 | - `events` an Object with every event listener available in DPicker 245 | - `data` an Object with the DPicker data 246 | 247 | Example: 248 | 249 | ```javascript 250 | DPicker.renders.myRenderFn = function (data, events) { 251 | var el = document.createElement('div') 252 | el.onclick = events.doSomeStuff 253 | 254 | return el 255 | } 256 | ``` 257 | 258 | An event listener is a function that will be executed in DPicker context. This means that `this.data` will be the `data` object of our date picker. 259 | 260 | Example: 261 | 262 | ```javascript 263 | DPicker.events.doSomeStuff = function (evt) { 264 | evt.preventDefault() 265 | this.data.foo = 'foobar' 266 | this.redraw() // Call the public redraw method, those are documented in the API docs 267 | } 268 | ``` 269 | 270 | ### Example 271 | 272 | #### Confirm button 273 | 274 | Straightforward, plain javascript module that adds a close button: 275 | 276 | ```javascript 277 | //add 'closeButton' to the `order` array 278 | DPicker.renders.closeButton = function renderCloseButton(events, data) { 279 | const button = document.createElement('button') 280 | button.innerText = 'Confirm' 281 | button.type = 'button' 282 | button.classList.add('dpicker-close-button') 283 | button.addEventListener('click', events.hidePicker) 284 | return button 285 | } 286 | 287 | DPicker.events.hidePicker = function hidePicker() { 288 | this.display = false 289 | } 290 | ``` 291 | 292 | Then, initialize your DPicker with `new DPicker(container, {order: ['months', 'years', 'time', 'days', 'closeButton']})`. 293 | 294 | #### Previous/Next month buttons 295 | 296 | Let's build an other module that will add two arrows to navigate between previous and next months. 297 | 298 | To make this more readable, we will use the [`bel`](https://github.com/shama/bel) module. This module can then be [`yo-yoified`](https://github.com/shama/yo-yoify) or [`babel-yo-yoified`](https://github.com/goto-bus-stop/babel-plugin-yo-yoify) to transform our html text to real DOM elements. 299 | 300 | First, let's add two rendering methods for our buttons: 301 | 302 | ```javascript 303 | const html = require('bel') 304 | 305 | DPicker.renders.previousMonth = function renderPreviousMonth(events, data) { 306 | return html`` 307 | } 308 | 309 | DPicker.renders.nextMonth = function renderNextMonth(events, data) { 310 | return html`` 311 | } 312 | ``` 313 | 314 | Now let's add two events to make this work. Note that to play with dates you should use the [`DateAdapter`](_api#momentadapter): 315 | 316 | ```javascript 317 | DPicker.events.previousMonth = function previousMonth(evt) { 318 | // go one month back 319 | this.model = DPicker.dateAdapter.subMonths(this.data.model, 1) 320 | // redraw the DPicker 321 | this.redraw() 322 | // This is not mandatory but is good if you want your changes to reach DPicker.onChange 323 | this.onChange({modelChanged: true, name: 'previousMonth', event: evt}) 324 | } 325 | 326 | DPicker.events.nextMonth = function nextMonth(evt) { 327 | this.model = DPicker.dateAdapter.addMonths(this.data.model, 1) 328 | this.redraw() 329 | this.onChange({modelChanged: true, name: 'nextMonth', event: evt}) 330 | } 331 | ``` 332 | 333 | We're done. To enable your render functions, you have to specify their keys in the `order` option: 334 | 335 | ```javascript 336 | new dpicker(element, {order: ['previousMonth', 'months', 'years', 'nextMonth', 'days', 'time']}) 337 | ``` 338 | 339 | This module is actually available [here](https://github.com/soyuka/dpicker/blob/development/src/plugins/navigation.js). 340 | 341 | ### Go further 342 | 343 | Sometimes, you just want to do more work when one of the [available events or methods](_api) from DPicker are called. For this to work we can `decorate` public methods or events. 344 | 345 | For example, let's add a call when `dpicker#initialize` is called: 346 | 347 | ```javascript 348 | DPicker.prototype.initialize = DPicker.decorate(DPicker.prototype.initialize, function myPluginInit () { 349 | //do some stuff 350 | }) 351 | ``` 352 | 353 | Or with an event: 354 | 355 | ```javascript 356 | DPicker.events.dayKeyDown = DPicker.decorate(DPicker.events.dayKeyDown, function DayKeyDown (evt) { 357 | // a keydown event was called on a day 358 | }) 359 | ``` 360 | 361 | !> Note that if a decoration returns `false`, it'll stop the call chain. 362 | 363 | Last but not least, you can add options to your plugin. DPicker will automatically try to instantiate the given properties via: 364 | 365 | 1. attributes (the DOM attributes of the given `input`) 366 | 2. options (the options given to `DPicker` constructor) 367 | 3. a default value 368 | 369 | Properties should be added like this: 370 | 371 | ```javascript 372 | DPicker.properties.myOption = false 373 | ``` 374 | 375 | This sets up `this.data.myOption`, and has a default `false` value. 376 | 377 | If you want to customize the behavior on the `attributes` parsing, you can use a `function`: 378 | 379 | ```javascript 380 | DPicker.properties.step = function getStepAttribute (attributes) { 381 | return attributes.step ? parseInt(attributes.step, 10) : 1 382 | } 383 | ``` 384 | 385 | ### Share the module 386 | 387 | The best to share a module is to embed it in a function: 388 | 389 | ```javascript 390 | module.exports = function(DPicker) { 391 | 392 | } 393 | ``` 394 | 395 | Then you can bundle your own DPicker: 396 | 397 | ```javascript 398 | const DPicker = require('dpicker') 399 | const MomentDateAdapter = require('dpicker/src/adapters/moment') 400 | 401 | DPicker.dateAdapter = MomentDateAdapter 402 | 403 | // Require some modules here 404 | require('dpicker/src/plugins/time')(DPicker) 405 | require('./my-awesome-dpicker-module')(DPicker) 406 | 407 | module.exports = DPicker 408 | ``` 409 | 410 | ## Framework agnostic 411 | 412 | !> Those are only base examples, please adapt them to your needs! 413 | 414 | ### Angular 1 415 | 416 | ?> Simple Angular 1 example that leverages ngModelCtrl. Some more bits are needed for validation. 417 | 418 | ```javascript 419 | angular.module('DPicker', []) 420 | 421 | angular.module('DPicker') 422 | .directive('dpDpicker', function() { 423 | return { 424 | restrict: 'A', 425 | scope: { 426 | ngModel: '=' 427 | }, 428 | require: 'ngModel', 429 | link: function(scope, element, attrs, ngModelCtrl) { 430 | scope.dpicker = new dpicker(element[0]) 431 | scope.dpicker.onChange = function(data, DPickerEvent) { 432 | if (DPickerEvent.modelChanged === true) { 433 | ngModelCtrl.$setViewValue(scope.dpicker.model) 434 | } 435 | } 436 | 437 | if (scope.ngModel && scope.ngModel instanceof Date) { 438 | scope.dpicker.model = scope.ngModel 439 | } 440 | 441 | ngModelCtrl.$setViewValue(scope.dpicker.empty ? null : scope.dpicker.model) 442 | 443 | attrs.$observe('ngModel', function(value) { 444 | if (value instanceof Date) { 445 | scope.dpicker.model = value 446 | } 447 | }) 448 | } 449 | } 450 | }) 451 | ``` 452 | 453 | ### Angular 2 454 | 455 | ?> This Angular 2 example assumes that the ngModel is a Date. This example also attach an angular 2 validator. 456 | 457 | ```javascript 458 | import { forwardRef, ElementRef, Directive, Input, OnInit } from '@angular/core' 459 | import { NG_VALUE_ACCESSOR, ControlValueAccessor, NG_VALIDATORS, AbstractControl } from '@angular/forms' 460 | 461 | import * as DPicker from 'dpicker' 462 | 463 | @Directive({ 464 | selector: '[prefixDpicker]', 465 | providers: [{ 466 | provide: NG_VALUE_ACCESSOR, 467 | useExisting: forwardRef(() => PrefixDpickerDirective), 468 | multi: true 469 | }, 470 | { 471 | provide: NG_VALIDATORS, 472 | useExisting: forwardRef(() => PrefixDpickerDirective), 473 | multi: true 474 | } 475 | ], 476 | }) 477 | export class PrefixDpickerDirective implements ControlValueAccessor, OnInit { 478 | dpicker: any 479 | @Input() max: Date 480 | @Input() min: Date 481 | 482 | private onChangeCallback: (_: any) => void = () => {} 483 | private onTouchedCallback: () => void = () => {} 484 | 485 | constructor(public elementRef: ElementRef) {} 486 | 487 | ngOnInit() { 488 | try { 489 | this.dpicker = new dpicker(this.elementRef.nativeElement, {min: this.min, max: this.max}) 490 | } catch (e) { 491 | console.error(e.message) 492 | } 493 | 494 | this.dpicker.onChange = (data, DPickerEvent) => { 495 | if (DPickerEvent.modelChanged === true) { 496 | this.onChangeCallback(this.dpicker.model) 497 | } 498 | 499 | this.onTouchedCallback() 500 | } 501 | } 502 | 503 | registerOnChange(fn: any) { 504 | this.onChangeCallback = fn 505 | } 506 | 507 | registerOnTouched(fn: any) { 508 | this.onTouchedCallback = fn 509 | } 510 | 511 | writeValue(value: any) { 512 | this.dpicker.model = value 513 | } 514 | 515 | validate(c: AbstractControl): { [key: string]: any } { 516 | this.dpicker.isValid(c.value) 517 | 518 | if (this.dpicker.valid === true) { 519 | return null 520 | } 521 | 522 | return {validDate: false} 523 | } 524 | } 525 | ``` 526 | 527 | Html: 528 | 529 | ```html 530 |
531 | valid: {{f.valid}} {{foo}} 532 |
533 | 534 |
535 |
536 | ``` 537 | 538 | ### Cycle.js 539 | 540 | To bundle `DPicker` in the cyclejs DOM, let's create a [Component](https://cycle.js.org/components.html#components). 541 | 542 | ?> We are setting a [`snabbdom` hook](https://github.com/snabbdom/snabbdom#hooks) on `insert`. This allows us to set up DPicker on the real `Element`, once appended to the DOM. 543 | 544 | ```javascript 545 | import {div} from '@cycle/dom' 546 | import xs from 'xstream' 547 | import DPicker from 'dpicker/dist/dpicker.all' 548 | 549 | export function CycleDPicker (selector, sources) { 550 | 551 | const value$ = sources.DOM.select(selector) 552 | .events('dpicker:change') 553 | .map((ev) => { 554 | return ev.detail 555 | }) 556 | 557 | const state$ = sources.props 558 | .map((props) => { 559 | return value$ 560 | .startWith(props) 561 | }) 562 | .flatten() 563 | .remember() 564 | 565 | const vdom$ = state$.map((state) => { 566 | return div(selector, { 567 | hook: { 568 | insert: (vnode) => { 569 | const dpicker = new DPicker(vnode.elm, state) 570 | dpicker.onChange = function(data, modelChanged) { 571 | if (modelChanged === false) { 572 | return 573 | } 574 | 575 | const evt = new CustomEvent('dpicker:change', {bubbles: true, detail: dpicker.data}) 576 | vnode.elm.dispatchEvent(evt) 577 | } 578 | } 579 | } 580 | }) 581 | }) 582 | 583 | return { 584 | DOM: vdom$, 585 | state: state$ 586 | } 587 | } 588 | ``` 589 | 590 | Usage: 591 | 592 | ```javascript 593 | const myDpicker = CycleDPicker('.cycle-dpicker-max', { 594 | DOM: sources.DOM, 595 | props: xs.of({name: 'max', model: new Date()}) //dpicker options 596 | }) 597 | 598 | // Streams: 599 | 600 | const vdom$ = myDpicker.DOM 601 | const state$ = myDpicker.state 602 | ``` 603 | 604 | ?> **TODO** add more examples 605 | 606 | ## Date Adapter 607 | 608 | Because framework agnostic also means that we don't want to force you to use one or another Date library, DPicker uses a `DateAdapter`. It's a simple bridge module that exposes needed functions. If you want to implement your own date adapter, implement the [DateAdapter as documented in the API](_api#momentadapter). Dates MUST be immutable! 609 | 610 | Referencing the `dateAdapter` of your choice is done through the static property: 611 | 612 | ```javascript 613 | DPicker.dateAdapter = MyDateAdapter 614 | ``` 615 | 616 | For this to work nicely, I'd recommend to use `dpicker.core.js` and to build with `browserify`. The end file will look like this: 617 | 618 | ```javascript 619 | const DPicker = require('dpicker') 620 | const MyDateAdapter = require('./my.date.adapter') 621 | 622 | DPicker.dateAdapter = MyDateAdapter 623 | 624 | // Require some modules here 625 | require('dpicker/src/plugins/time')(DPicker) 626 | 627 | module.exports = DPicker 628 | ``` 629 | 630 | Use our 100% coverage test case instead of building your own tests! 631 | 632 | ## Why? 633 | 634 | I was searching for a simple date picker, with only basic features and an ability to work with any framework, or event plain javascript (VanillaJS). 635 | If you know one that does have less than 1000 SLOC, please let me know. 636 | 637 | This date picker: 638 | 639 | - is light and easy to use, especially easy to maintain (core has ~500 SLOC), uses `Date` and `DOM` objects. 640 | - is compliant and can be extended to suit your needs 641 | - no default css so that it fits well with foundation/bootstrap 642 | - is framework agnostic 643 | - has HTML attributes compatibility with `input[type="date"]` (adds a `format` attribute) and `input[type="datetime"]` (adds a `meridiem` attribute on top of the `format` one if you need 12 hours time range). Define minutes step through the `step` attribute. 644 | - has in mind to work with any Date module (momentjs, date-fns) 645 | - extensible through modules, use the core and implement your specific needs easily 646 | 647 | What I think is good, and isn't straightforward in other date pickers is that your input's `Date` instance is separated from the input real value: 648 | 649 | ```javascript 650 | const dpicker = new dpicker(input) 651 | 652 | console.log(dpicker.model) //the Date instance 653 | console.log(dpicker.input) //the input value, a formatted date 654 | ``` 655 | 656 | ## Credits 657 | 658 | DPicker refactor (from `4` to `5`) motivation: 659 | 660 | - [date-fns](https://github.com/date-fns/date-fns) - because I think this is a good alternative to momentjs, especially since it's immutable 661 | - [nanomorph](https://github.com/yoshuawuyts/nanomorph), in fact [@yoshuawuyts](https://github.com/yoshuawuyts) writings in general 662 | - [bel](https://github.com/shama/bel), why did I discover this in 2017? 663 | - [babel-plugin-yo-yoify](https://github.com/goto-bus-stop/babel-plugin-yo-yoify) for the reactivity and because this works great 664 | - [@florianpircher](https://github.com/florianpircher), [@stereonom](https://github.com/stereonom), [@thcolin](https://github.com/thcolin) for the motivation, the really good bug reports, and the design ideas/talks 665 | 666 | DPicker docs: 667 | 668 | - [jsdoc2md](https://github.com/jsdoc2md/jsdoc-to-markdown) 669 | - [docsify](https://github.com/QingWei-Li/docsify/) 670 | 671 | ## License 672 | 673 | ``` 674 | The MIT License (MIT) 675 | 676 | Copyright (c) 2016 Antoine Bluchet 677 | 678 | Permission is hereby granted, free of charge, to any person obtaining a copy 679 | of this software and associated documentation files (the "Software"), to deal 680 | in the Software without restriction, including without limitation the rights 681 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 682 | copies of the Software, and to permit persons to whom the Software is 683 | furnished to do so, subject to the following conditions: 684 | 685 | The above copyright notice and this permission notice shall be included in 686 | all copies or substantial portions of the Software. 687 | 688 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 689 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 690 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 691 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 692 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 693 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 694 | THE SOFTWARE. 695 | ``` 696 | -------------------------------------------------------------------------------- /docs/_coverpage.md: -------------------------------------------------------------------------------- 1 | ![logo](./logo.svg) 2 | 3 | # DPicker DPICKER_VERSION 4 | 5 | > A framework-agnostic minimal date picker. 6 | 7 | - Easy to use 8 | - Lightweight (core is 4.89kb) 9 | - Fully featured 10 | - Extendable - build you own modules 11 | 12 | [Demo](_demo) 13 | [Get Started](#installation) 14 | 15 | ![color](#f8f8f8) 16 | -------------------------------------------------------------------------------- /docs/_demo.md: -------------------------------------------------------------------------------- 1 | 39 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | - [Home](/) 4 | - [Demo](_demo.md) 5 | - [StyleSheets](_stylesheets.md) 6 | - [Api](_api.md) 7 | - [Changelog](CHANGELOG.md) 8 | 9 | -------------------------------------------------------------------------------- /docs/_stylesheets.md: -------------------------------------------------------------------------------- 1 | # Stylesheets 2 | 3 | ## Minimal 4 | 5 | ```css 6 | td.dpicker-inactive { 7 | color: grey; 8 | } 9 | 10 | button.dpicker-active { 11 | background: coral; 12 | } 13 | 14 | .dpicker-invisible { 15 | display: none; 16 | } 17 | 18 | .dpicker-visible { 19 | display: block; 20 | } 21 | ``` 22 | 23 | ## Foundation 24 | 25 | ```css 26 | @import 'foundation'; 27 | 28 | $black: #2f3439 !default; 29 | $white: #fefefe !default; 30 | $alt-white: #f0f0f0 !default; 31 | 32 | label.dpicker .dpicker-container { top: inherit; } 33 | 34 | .dpicker { 35 | position: relative; 36 | 37 | .dpicker-container { 38 | background: $white; 39 | border: $black; 40 | flex-wrap: wrap; 41 | justify-content: space-between; 42 | left: 0; 43 | padding: 15px; 44 | position: absolute; 45 | top: ($input-font-size + ($form-spacing * 1.5) - rem-calc(1)); 46 | width: 300px; 47 | z-index: 2; 48 | 49 | &.dpicker-invisible { display: none; } 50 | &.dpicker-visible { display: flex; } 51 | 52 | select { 53 | flex: 0 0 49%; 54 | } 55 | 56 | .dpicker-time { 57 | display: flex; 58 | 59 | select { 60 | flex: 0 0 30%; 61 | } 62 | } 63 | 64 | table { 65 | color: $black; 66 | text-align: center; 67 | $dpicker-hover: scale-color($alt-white, $lightness: -20%); 68 | 69 | td { 70 | border: 1px solid $black; 71 | border-collapse: collapse; 72 | height: 40px; 73 | width: 40px; 74 | } 75 | 76 | .dpicker-inactive { 77 | color: scale-color($black, $lightness: 50%); 78 | font-size: $small-font-size; 79 | } 80 | 81 | .dpicker-active button { 82 | 83 | @include button($expand: true, $background: $alt-white, $background-hover: $dpicker-hover) 84 | 85 | border: 0; 86 | color: inherit; 87 | height: 100%; 88 | margin: 0; 89 | padding: 0; 90 | 91 | &.dpicker-active { 92 | background-color: $dpicker-hover; 93 | } 94 | } 95 | } 96 | } 97 | } 98 | ``` 99 | 100 | ## Bootstrap 101 | 102 | From the [demo](_demo) (scss) 103 | 104 | ```css 105 | @import 'bootstrap'; 106 | 107 | @mixin form-control { 108 | display: block; 109 | width: 100%; 110 | height: $input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border) 111 | padding: $padding-base-vertical $padding-base-horizontal; 112 | font-size: $font-size-base; 113 | line-height: $line-height-base; 114 | color: $input-color; 115 | background-color: $input-bg; 116 | background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 117 | border: 1px solid $input-border; 118 | border-radius: $input-border-radius; // Note: This has no effect on s in CSS. 119 | @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); 120 | @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s); 121 | 122 | // Customize the `:focus` state to imitate native WebKit styles. 123 | @include form-control-focus; 124 | 125 | // Placeholder 126 | @include placeholder; 127 | 128 | // Unstyle the caret on `s in some browsers, due to the limited stylability of `s in IE10+. 53 | &::-ms-expand { 54 | border: 0; 55 | background-color: transparent; 56 | } 57 | 58 | // Disabled and read-only inputs 59 | // 60 | // HTML5 says that controls under a fieldset > legend:first-child won't be 61 | // disabled if the fieldset is disabled. Due to implementation difficulty, we 62 | // don't honor that edge case; we style them as disabled anyway. 63 | &[disabled], 64 | &[readonly], 65 | fieldset[disabled] & { 66 | background-color: $input-bg-disabled; 67 | opacity: 1; // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655 68 | } 69 | 70 | &[disabled], 71 | fieldset[disabled] & { 72 | cursor: $cursor-disabled; 73 | } 74 | } 75 | 76 | @mixin button { 77 | display: inline-block; 78 | width: 100%; 79 | margin-bottom: 0; // For input.btn 80 | font-weight: $btn-font-weight; 81 | text-align: center; 82 | vertical-align: middle; 83 | touch-action: manipulation; 84 | cursor: pointer; 85 | background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 86 | border: 1px solid transparent; 87 | white-space: nowrap; 88 | @include button-size($padding-base-vertical, $padding-base-horizontal, $font-size-base, $line-height-base, 0); 89 | @include user-select(none); 90 | @include button-variant($btn-default-color, $btn-default-bg, transparent); 91 | 92 | &, 93 | &:active, 94 | &.active { 95 | &:focus, 96 | &.focus { 97 | @include tab-focus; 98 | } 99 | } 100 | 101 | &:hover, 102 | &:focus, 103 | &.focus { 104 | color: $btn-default-color; 105 | text-decoration: none; 106 | } 107 | 108 | } 109 | 110 | #heading { 111 | padding: 50px 0; 112 | text-align: center; 113 | background-color: #F1F8E9; 114 | } 115 | 116 | #main { 117 | background-color: #fff; 118 | } 119 | 120 | .main-container, #demo { 121 | margin-top: 30px; 122 | } 123 | 124 | .dpicker-main { 125 | .dpicker-container { 126 | position: relative !important; 127 | border: 0 !important; 128 | min-width: auto !important; 129 | } 130 | } 131 | 132 | .dpicker { 133 | .dpicker-invalid { 134 | border: 1px solid #e65100; 135 | } 136 | 137 | @include form-inline; 138 | 139 | input, select { 140 | @include form-control; 141 | } 142 | 143 | .dpicker-inactive { 144 | color: lighten(#111, 50%); 145 | } 146 | 147 | .dpicker-next-month::before { 148 | content: '\2771'; 149 | } 150 | 151 | .dpicker-previous-month::before { 152 | content: '\2770'; 153 | } 154 | 155 | .dpicker-previous-month.dpicker-invisible, .dpicker-next-month.dpicker-invisible{ 156 | visibility: hidden; 157 | } 158 | 159 | .dpicker-container { 160 | width: 100%; 161 | margin-top: 15px; 162 | display: flex; 163 | flex-wrap: wrap; 164 | position: absolute; 165 | right: 0; 166 | z-index: 2; 167 | background: #fff; 168 | border: 1px solid darken(#fff, 20%); 169 | padding: 15px; 170 | min-width: 500px; 171 | text-align: center; 172 | justify-content: space-between; 173 | 174 | &.dpicker-invisible { 175 | display: none; 176 | } 177 | 178 | .dpicker-time { 179 | display: flex; 180 | justify-content: center; 181 | margin-bottom: 10px; 182 | flex: 0 0 100%; 183 | 184 | select { 185 | display: inline-block; 186 | width: auto; 187 | margin: 0 10px; 188 | } 189 | } 190 | 191 | .dpicker-previous-month, .dpicker-next-month { 192 | background: transparent; 193 | border: 0; 194 | flex: 0 0 10%; 195 | } 196 | 197 | select { 198 | flex: 0 0 30%; 199 | } 200 | 201 | table { 202 | text-align: center; 203 | border-collapse: 'collapse'; 204 | margin: 10px auto 0; 205 | width: 90%; 206 | 207 | td.dpicker-inactive { 208 | padding: 7px 8px; 209 | } 210 | 211 | th { 212 | text-align: center; 213 | } 214 | 215 | button { 216 | @include button(); 217 | @include button-size(6px, 8px, $font-size-base, $line-height-base, 3px); 218 | 219 | &.dpicker-active { 220 | @include button-variant(#111, #ffab40, darken(#ffab40, 20%)); 221 | } 222 | } 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | dpicker - A framework-agnostic minimal date picker. 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 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 | -------------------------------------------------------------------------------- /examples/angular2.directive.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, ElementRef, Directive, Input, OnInit } from '@angular/core' 2 | import { NG_VALUE_ACCESSOR, ControlValueAccessor, Validator, NG_VALIDATORS, AbstractControl } from '@angular/forms'; 3 | 4 | const defaultFormat = 'DD/MM/YYYY' 5 | const DPicker = (require('dpicker') as any) 6 | const moment = (require('moment') as any) 7 | 8 | @Directive({ 9 | selector: '[ngDpicker]', 10 | providers: [{ 11 | provide: NG_VALUE_ACCESSOR, 12 | useExisting: forwardRef(() => NgDpicker), 13 | multi: true 14 | }, 15 | { 16 | provide: NG_VALIDATORS, 17 | useExisting: forwardRef(() => NgDpicker), 18 | multi: true 19 | } 20 | ], 21 | }) 22 | export class NgDpicker implements ControlValueAccessor, OnInit { 23 | constructor(public elementRef: ElementRef) { 24 | try { 25 | this.dpicker = new DPicker(this.elementRef.nativeElement) 26 | } catch(e) { 27 | console.error(e.message) 28 | } 29 | } 30 | 31 | private onChangeCallback: (_: any) => void = () => {} 32 | private onTouchedCallback: () => void = () => {} 33 | dpicker: any 34 | @Input() max: string; 35 | @Input() min: string; 36 | 37 | getMoment(input: string|Date): any { 38 | if (!input) { 39 | return null 40 | } 41 | 42 | if (!(input instanceof Date) || !(moment.isMoment(input))) { 43 | return moment(input, dateFormat) 44 | } else { 45 | return moment(input) 46 | } 47 | } 48 | 49 | ngOnInit() { 50 | this.dpicker.onChange = () => { 51 | this.onChangeCallback(this.dpicker.model) 52 | this.onTouchedCallback() 53 | } 54 | 55 | //initialization 56 | setImmediate(() => { 57 | this.onChangeCallback(this.dpicker.model) 58 | this.onTouchedCallback() 59 | }) 60 | } 61 | 62 | registerOnChange(fn: any) { 63 | this.onChangeCallback = fn 64 | } 65 | 66 | registerOnTouched(fn:any) { 67 | this.onTouchedCallback = fn; 68 | } 69 | 70 | writeValue(value: any) { 71 | this.dpicker.model = this.getMoment(value) 72 | } 73 | 74 | validate(c: AbstractControl): { [key: string]: any } { 75 | if (moment.isMoment(c.value)) { 76 | this.dpicker.isValid(c.value) 77 | } 78 | 79 | if (this.dpicker.valid === true) { 80 | return null 81 | } 82 | 83 | return {validDate: false} 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 |
15 | 16 | 17 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /jsdoc.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["plugins/markdown"] 3 | } 4 | -------------------------------------------------------------------------------- /module.test.js: -------------------------------------------------------------------------------- 1 | const DPicker = require('./src/all.js') 2 | const html = require('bel') 3 | 4 | function test(DPicker) { 5 | DPicker.renders.test = function() { 6 | return html`
` 7 | } 8 | } 9 | 10 | test(DPicker) 11 | 12 | module.exports = DPicker 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dpicker", 3 | "version": "5.2.1", 4 | "description": "A framework-agnostic minimal date picker.", 5 | "main": "dist/dpicker.core.js", 6 | "scripts": { 7 | "test": "standard --global DPicker src/*.js test/**/*.spec.js && mocha -b", 8 | "build": "bash .scripts/build.sh", 9 | "coverage": "istanbul cover -x test.js -x src _mocha" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/soyuka/dpicker.git" 14 | }, 15 | "keywords": [ 16 | "date", 17 | "picker" 18 | ], 19 | "author": "soyuka", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/soyuka/dpicker/issues" 23 | }, 24 | "homepage": "https://github.com/soyuka/dpicker#readme", 25 | "dependencies": { 26 | "array.prototype.fill": "^1.0.2", 27 | "dom4": "^2.1.3", 28 | "moment": "^2.19.4", 29 | "nanohtml": "^1.2.4", 30 | "nanomorph": "^5.1.3", 31 | "on-load": "^3.4.1", 32 | "yo-yoify": "^4.3.0" 33 | }, 34 | "devDependencies": { 35 | "@soyuka/jhaml": "^1.0.8", 36 | "babel-core": "^6.26.3", 37 | "babel-plugin-yo-yoify": "^2.0.0", 38 | "babel-preset-env": "^1.7.0", 39 | "babelify": "^8.0.0", 40 | "browserify": "^16.2.2", 41 | "chai": "^4.1.2", 42 | "chai-spies": "^1.0.0", 43 | "docsify": "^4.7.0", 44 | "istanbul": "^0.4.5", 45 | "jsdoc": "^3.5.5", 46 | "jsdoc-to-markdown": "^4.0.1", 47 | "jsdom": "^11.12.0", 48 | "jsdom-global": "^3.0.2", 49 | "markdox": "^0.1.10", 50 | "mocha": "^5.2.0", 51 | "standard": "^11.0.1", 52 | "uglifyify": "^5.0.1", 53 | "unassertify": "^2.1.1" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/adapters/moment.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | let moment 3 | try { 4 | moment = require('moment') 5 | } catch (e) { 6 | moment = window.moment 7 | } 8 | 9 | const DAY_TOKEN = 'd' 10 | const YEAR_TOKEN = 'y' 11 | const MONTH_TOKEN = 'M' 12 | const HOURS_TOKEN = 'h' 13 | 14 | /** 15 | * @module MomentAdapter 16 | */ 17 | 18 | /** 19 | * Get an immutable moment date 20 | * @param {String} date 21 | * @param {Boolean} immutable 22 | * @private 23 | * @return {Moment} 24 | */ 25 | function _getMoment (date, immutable) { 26 | if (immutable === undefined) { 27 | immutable = true 28 | } 29 | 30 | if (moment.isMoment(date)) { 31 | return immutable === true ? date.clone() : date 32 | } 33 | 34 | if (date instanceof Date) { 35 | return moment(date) 36 | } 37 | 38 | return null 39 | } 40 | 41 | /** 42 | * Get months, an array of string 43 | * @return {Array} List of the available months 44 | */ 45 | function months () { 46 | return moment.months() 47 | } 48 | 49 | /** 50 | * Get week days 51 | * @return {Array} 52 | */ 53 | function weekdays () { 54 | return moment.weekdaysShort() 55 | } 56 | 57 | /** 58 | * First day of week according to locale 59 | * @return {Number} 60 | */ 61 | function firstDayOfWeek () { 62 | return moment.localeData().firstDayOfWeek() 63 | } 64 | 65 | /** 66 | * @param {Date} date 67 | * @return {Boolean} 68 | */ 69 | function isValid (date) { 70 | date = _getMoment(date, false) 71 | return moment.isMoment(date) && date.isValid() 72 | } 73 | 74 | /** 75 | * @param {String} dateString 76 | * @param {String} format 77 | * @return {Date|Boolean} false if invalid or the parsed date, parsing is heaving let's do this only once 78 | */ 79 | function isValidWithFormat (dateString, format) { 80 | if (Array.isArray(format)) { 81 | let date = false 82 | 83 | for (let i = 0; i < format.length; i++) { 84 | const testDate = moment(dateString, format[i], true) 85 | if (this.isValid(testDate) === true) { 86 | date = testDate.toDate() 87 | break 88 | } 89 | } 90 | 91 | return date 92 | } 93 | 94 | const testDate = moment(dateString, format, true) 95 | if (this.isValid(testDate) === true) { 96 | return testDate.toDate() 97 | } 98 | 99 | return false 100 | } 101 | 102 | /** 103 | * Get year 104 | * @param {Date} date 105 | * @return {Number} 106 | */ 107 | function getYear (date) { 108 | return _getMoment(date, false).year() 109 | } 110 | 111 | /** 112 | * Get Month 113 | * @param {Date} date 114 | * @return {Number} 115 | */ 116 | function getMonth (date) { 117 | return _getMoment(date, false).month() 118 | } 119 | 120 | /** 121 | * Get Date 122 | * @param {Date} date 123 | * @return {Number} 124 | */ 125 | function getDate (date) { 126 | return _getMoment(date, false).date() 127 | } 128 | 129 | /** 130 | * Get Hours 131 | * @param {Date} date 132 | * @return {Number} 133 | */ 134 | function getHours (date) { 135 | return _getMoment(date, false).hours() 136 | } 137 | 138 | /** 139 | * Get Minutes 140 | * @param {Date} date 141 | * @return {Number} 142 | */ 143 | function getMinutes (date) { 144 | return _getMoment(date, false).minutes() 145 | } 146 | 147 | /** 148 | * Get Seconds 149 | * @param {Date} date 150 | * @return {Number} 151 | */ 152 | function getSeconds (date) { 153 | return _getMoment(date, false).seconds() 154 | } 155 | 156 | /** 157 | * Get Milliseconds 158 | * @param {Date} date 159 | * @return {Number} 160 | */ 161 | function getMilliseconds (date) { 162 | return _getMoment(date, false).milliseconds() 163 | } 164 | 165 | /** 166 | * Set Date 167 | * @param {Date} date 168 | * @param {Number} day 169 | * @return {Date} 170 | */ 171 | function setDate (date, day) { 172 | return _getMoment(date).date(day).toDate() 173 | } 174 | 175 | /** 176 | * Set Minutes 177 | * @param {Date} date 178 | * @param {Number} minutes 179 | * @return {Date} 180 | */ 181 | function setMinutes (date, minutes) { 182 | return _getMoment(date).minutes(minutes).toDate() 183 | } 184 | 185 | /** 186 | * Set Hours 187 | * @param {Date} date 188 | * @param {Number} hours 189 | * @return {Date} 190 | */ 191 | function setHours (date, hours) { 192 | return _getMoment(date).hours(hours).toDate() 193 | } 194 | 195 | /** 196 | * Set Month 197 | * @param {Date} date 198 | * @param {Number} month 199 | * @return {Date} 200 | */ 201 | function setMonth (date, month) { 202 | return _getMoment(date).month(month).toDate() 203 | } 204 | 205 | /** 206 | * Set Year 207 | * @param {Date} date 208 | * @param {Number} year 209 | * @return {Date} 210 | */ 211 | function setYear (date, year) { 212 | return _getMoment(date).year(year).toDate() 213 | } 214 | 215 | /** 216 | * Add Days 217 | * @param {Date} date 218 | * @param {Number} num days to add 219 | * @return {Date} 220 | */ 221 | function addDays (date, num) { 222 | return _getMoment(date).add(num, DAY_TOKEN).toDate() 223 | } 224 | 225 | /** 226 | * Add Months 227 | * @param {Date} date 228 | * @param {Number} num months to add 229 | * @return {Date} 230 | */ 231 | function addMonths (date, num) { 232 | return _getMoment(date).add(num, MONTH_TOKEN).toDate() 233 | } 234 | 235 | /** 236 | * Add Years 237 | * @param {Date} date 238 | * @param {Number} num years to add 239 | * @return {Date} 240 | */ 241 | function addYears (date, year) { 242 | return _getMoment(date).add(year, YEAR_TOKEN).toDate() 243 | } 244 | 245 | /** 246 | * Add Hours 247 | * @param {Date} date 248 | * @param {Number} num hours to add 249 | * @return {Date} 250 | */ 251 | function addHours (date, hours) { 252 | return _getMoment(date).add(hours, HOURS_TOKEN).toDate() 253 | } 254 | 255 | /** 256 | * Subtract days 257 | * @param {Date} date 258 | * @param {Number} num days to subtract 259 | * @return {Date} 260 | */ 261 | function subDays (date, num) { 262 | return _getMoment(date).subtract(num, DAY_TOKEN).toDate() 263 | } 264 | 265 | /** 266 | * Subtract months 267 | * @param {Date} date 268 | * @param {Number} num months to subtract 269 | * @return {Date} 270 | */ 271 | function subMonths (date, num) { 272 | return _getMoment(date).subtract(num, MONTH_TOKEN).toDate() 273 | } 274 | 275 | /** 276 | * Format a Date and return a string 277 | * @param {Date} date 278 | * @param {String} format 279 | * @return {String} 280 | */ 281 | function format (date, format) { 282 | if (Array.isArray(format)) { format = format[0] } 283 | return _getMoment(date, false).format(format) 284 | } 285 | 286 | /** 287 | * Get the number of days in the current date month 288 | * @param {Date} date 289 | * @return {Number} 290 | */ 291 | function daysInMonth (date) { 292 | return _getMoment(date, false).daysInMonth() 293 | } 294 | 295 | /** 296 | * Get number of the day in the week (from 0 to 6) for the given month on the first day 297 | * @param {Date} date 298 | * @returns {Number} 299 | */ 300 | function firstWeekDay (date) { 301 | return +_getMoment(date).date(1).format('e') 302 | } 303 | 304 | /** 305 | * Reset a date seconds 306 | * @param {Date} date 307 | * @returns {Date} 308 | */ 309 | function resetSeconds (date) { 310 | return _getMoment(date).seconds(0).milliseconds(0).toDate() 311 | } 312 | 313 | /** 314 | * Reset a date minutes 315 | * @param {Date} date 316 | * @returns {Date} 317 | */ 318 | function resetMinutes (date) { 319 | return _getMoment(this.resetSeconds(date)).minutes(0).toDate() 320 | } 321 | 322 | /** 323 | * Reset a date hours 324 | * @param {Date} date 325 | * @returns {Date} 326 | */ 327 | function resetHours (date) { 328 | return _getMoment(this.resetMinutes(date)).hours(0).toDate() 329 | } 330 | 331 | /** 332 | * isBefore 333 | * @param {Date} date 334 | * @param {Date} comparison 335 | * @return {Boolean} 336 | */ 337 | function isBefore (date, comparison) { 338 | return _getMoment(date, false).isBefore(comparison) 339 | } 340 | 341 | /** 342 | * isAfter 343 | * @param {Date} date 344 | * @param {Date} comparison 345 | * @return {Boolean} 346 | */ 347 | function isAfter (date, comparison) { 348 | return _getMoment(date, false).isAfter(comparison) 349 | } 350 | 351 | /** 352 | * isSameOrAfter (comparison must be done on a DAY basis) 353 | * @param {Date} date 354 | * @param {Date} comparison 355 | * @return {Boolean} 356 | */ 357 | function isSameOrAfter (date, comparison) { 358 | return _getMoment(date, false).isSameOrAfter(comparison, DAY_TOKEN) 359 | } 360 | 361 | /** 362 | * isSameOrBefore (comparison must be done on a DAY basis) 363 | * @param {Date} date 364 | * @param {Date} comparison 365 | * @return {Boolean} 366 | */ 367 | function isSameOrBefore (date, comparison) { 368 | return _getMoment(date, false).isSameOrBefore(comparison, DAY_TOKEN) 369 | } 370 | 371 | /** 372 | * isSameDay 373 | * @param {Date} date 374 | * @param {Date} comparison 375 | * @return {Boolean} 376 | */ 377 | function isSameDay (date, comparison) { 378 | return _getMoment(date, false).isSame(comparison, DAY_TOKEN) 379 | } 380 | 381 | /** 382 | * isSameHours 383 | * @param {Date} date 384 | * @param {Date} comparison 385 | * @return {Boolean} 386 | */ 387 | function isSameHours (date, comparison) { 388 | return _getMoment(date, false).isSame(comparison, HOURS_TOKEN) 389 | } 390 | 391 | /** 392 | * isSameMonth 393 | * @param {Date} date 394 | * @param {Date} comparison 395 | * @return {Boolean} 396 | */ 397 | function isSameMonth (date, comparison) { 398 | return _getMoment(date, false).isSame(comparison, MONTH_TOKEN) 399 | } 400 | 401 | /** 402 | * An uppercased meridiem (AM or PM) 403 | * @param {Date} date 404 | * @return {String} 405 | */ 406 | function getMeridiem (date) { 407 | return _getMoment(date, false).format('A') 408 | } 409 | 410 | module.exports = { 411 | _getMoment: _getMoment, 412 | months: months, 413 | weekdays: weekdays, 414 | firstDayOfWeek: firstDayOfWeek, 415 | isValid: isValid, 416 | isValidWithFormat: isValidWithFormat, 417 | getYear: getYear, 418 | getHours: getHours, 419 | getMonth: getMonth, 420 | getDate: getDate, 421 | getMinutes: getMinutes, 422 | getSeconds: getSeconds, 423 | getMilliseconds: getMilliseconds, 424 | setDate: setDate, 425 | setMinutes: setMinutes, 426 | setHours: setHours, 427 | setMonth: setMonth, 428 | setYear: setYear, 429 | addDays: addDays, 430 | addMonths: addMonths, 431 | addYears: addYears, 432 | addHours: addHours, 433 | subDays: subDays, 434 | subMonths: subMonths, 435 | format: format, 436 | daysInMonth: daysInMonth, 437 | firstWeekDay: firstWeekDay, 438 | resetSeconds: resetSeconds, 439 | resetMinutes: resetMinutes, 440 | resetHours: resetHours, 441 | isBefore: isBefore, 442 | isAfter: isAfter, 443 | isSameOrAfter: isSameOrAfter, 444 | isSameOrBefore: isSameOrBefore, 445 | isSameDay: isSameDay, 446 | isSameHours: isSameHours, 447 | isSameMonth: isSameMonth, 448 | getMeridiem: getMeridiem, 449 | } 450 | -------------------------------------------------------------------------------- /src/all.js: -------------------------------------------------------------------------------- 1 | const DPicker = require('./dpicker.moment.js') 2 | require('./plugins/time.js')(DPicker) 3 | require('./plugins/modifiers.js')(DPicker) 4 | require('./plugins/arrow-navigation.js')(DPicker) 5 | require('./plugins/navigation.js')(DPicker) 6 | require('./plugins/monthAndYear.js')(DPicker) 7 | 8 | module.exports = DPicker 9 | -------------------------------------------------------------------------------- /src/datetime.js: -------------------------------------------------------------------------------- 1 | const DPicker = require('./dpicker.moment.js') 2 | require('./plugins/time.js')(DPicker) 3 | 4 | module.exports = DPicker 5 | -------------------------------------------------------------------------------- /src/dpicker.js: -------------------------------------------------------------------------------- 1 | const nanomorph = require('nanomorph') 2 | const html = require('nanohtml') 3 | 4 | /** 5 | * DPicker 6 | * 7 | * @param {Element} element DOM element where you want the date picker or an input 8 | * @param {Object} [options={}] 9 | * @param {Date} [options.model=new Date()] Your own model instance, defaults to `new Date()` (can be set by the `value` attribute on an input, transformed to moment according to the given format) 10 | * @param {Date} [options.min=1986-01-01] The minimum date (can be set by the `min` attribute on an input) 11 | * @param {Date} [options.max=today+1 year] The maximum date (can be set by the `max` attribute on an input) 12 | * @param {String|Array} [options.format='DD/MM/YYYY'] The input format, a moment format (can be set by the `format` attribute on an input). If the aformat is an array, it'll enable multiple input formats. The first one will be the output format. 13 | * @param {String} [options.months=adapter.months()] Months array, see also [adapter.months()](todo) 14 | * @param {String} [options.days=adapter.weekdaysShort()] Days array, see also [adapter.weekdays()](todo) 15 | * @param {Boolean} [options.display=false] Initial calendar display state (not that when false it won't render the calendar) 16 | * @param {Boolean} [options.hideOnOutsideClick=true] On click outside of the date picker, hide the calendar 17 | * @param {Boolean} [options.hideOnDayClick=true] Hides the date picker on day click 18 | * @param {Boolean} [options.hideOnDayEnter=true] Hides the date picker when Enter or Escape is hit 19 | * @param {Boolean} [options.showCalendarOnInputFocus=true] Shows the calendar on input focus 20 | * @param {Boolean} [options.showCalendarButton=false] Adds a calendar button 21 | * @param {Boolean} [options.siblingMonthDayClick=false] Enable sibling months day click 22 | * @param {Function} [options.onChange] A function to call whenever the data gets updated 23 | * @param {String} [options.inputId=uuid()] The input id, useful to add you own label (can only be set in the initiation phase) If element is an inputand it has an `id` attribute it'll be overriden by it 24 | * @param {String} [options.inputName='dpicker-input'] The input name. If element is an inputand it has a `name` attribute it'll be overriden by it 25 | * @param {Array} [options.order] The dom elements appending order. 26 | * @param {Boolean} [options.time=false] Enable time (must include the time module) 27 | * @param {Boolean} [options.meridiem=false] 12/24 hour format, default 24 28 | * @param {Boolean} [options.disabled=false] Disable the input box 29 | * @param {Number} [options.step=1] Minutes step 30 | * @param {Boolean} [options.concatHoursAndMinutes=false] Use only one select box for both hours and minutes 31 | * @param {Boolean} [options.empty=false] Use this so force DPicker with an empty input instead of setting it to the formatted current date 32 | * 33 | * @property {String} container Get container id 34 | * @property {String} inputId Get input id 35 | * @property {String} input Get current input value (formatted date) 36 | * @property {Function} onChange Set onChange method 37 | * @property {Boolean} valid Is the current input valid 38 | * @property {Boolean} empty Is the input empty 39 | * @property {Date} model Get/Set model, a Date instance 40 | * @property {String} format Get/Set format, a Date format string 41 | * @property {Boolean} display Get/Set display, hides or shows the date picker 42 | * @property {Date} min Get/Set min date 43 | * @property {Date} max Get/Set max date 44 | 45 | * @fires DPicker#hide 46 | */ 47 | function DPicker (element, options = {}) { 48 | if (!(this instanceof DPicker)) { 49 | return new DPicker(element, options) 50 | } 51 | 52 | const {container, attributes, reference} = this._getContainer(element) 53 | 54 | this._container = uuid() 55 | this.data = {} 56 | 57 | const defaults = { 58 | months: DPicker.dateAdapter.months(), 59 | days: DPicker.dateAdapter.weekdays(), 60 | empty: false, 61 | valid: true, 62 | order: ['months', 'years', 'time', 'days'], 63 | hideOnDayClick: true, 64 | hideOnEnter: true, 65 | hideOnOutsideClick: true, 66 | showCalendarOnInputFocus: true, 67 | showCalendarButton: false, 68 | disabled: false, 69 | siblingMonthDayClick: false, 70 | firstDayOfWeek: DPicker.dateAdapter.firstDayOfWeek() 71 | } 72 | 73 | for (let i in defaults) { 74 | if (options[i] !== undefined) { 75 | this.data[i] = options[i] 76 | continue 77 | } 78 | 79 | this.data[i] = defaults[i] 80 | } 81 | 82 | this.data.inputName = attributes.name ? attributes.name : options.inputName ? options.inputName : 'dpicker-input' 83 | this.data.inputId = attributes.id ? attributes.id : options.inputId ? options.inputId : uuid() 84 | 85 | this._setData('format', [attributes.format, 'DD/MM/YYYY']) 86 | 87 | this.events = {} 88 | for (let i in DPicker.events) { 89 | this.events[i] = DPicker.events[i].bind(this) 90 | } 91 | 92 | const methods = DPicker.properties 93 | 94 | methods.min = new Date(1986, 0, 1) 95 | methods.max = DPicker.dateAdapter.setMonth(DPicker.dateAdapter.addYears(new Date(), 1), 11) 96 | methods.format = this.data.format 97 | 98 | for (let i in methods) { 99 | this._createGetSet(i) 100 | if (typeof methods[i] === 'function') { 101 | this._setData(i, [options[i], methods[i](attributes)]) 102 | } else { 103 | this._setData(i, [options[i], attributes[i], methods[i]], methods[i] instanceof Date) 104 | } 105 | } 106 | 107 | if (options.empty === true) { 108 | this.data.empty = true 109 | } 110 | 111 | this._setData('model', [attributes.value, options.model, new Date()], true) 112 | 113 | this.onChange = options.onChange 114 | 115 | document.addEventListener('click', this.events.hide) 116 | document.addEventListener('touchend', (e) => { 117 | if (!this.data.hideOnOutsideClick) { 118 | return 119 | } 120 | 121 | if (isElementInContainer(e.target, this._container)) { 122 | return 123 | } 124 | 125 | this.events.inputBlur(e) 126 | }) 127 | 128 | this.initialize() 129 | 130 | this._mount(container) 131 | this.isValid(this.data.model) 132 | 133 | container.id = this._container 134 | container.addEventListener('keydown', this.events.keyDown) 135 | 136 | let input = container.querySelector('input') 137 | input.addEventListener('blur', this.events.inputBlur) 138 | 139 | if (reference) { 140 | const refAttributes = reference.attributes 141 | for (let i = 0; i < refAttributes.length; i++) { 142 | if (!input.hasAttribute(refAttributes[i].name)) { 143 | input.setAttribute(refAttributes[i].name, refAttributes[i].value) 144 | } 145 | } 146 | 147 | if (reference.classList && reference.classList.length) { 148 | [].slice.call(reference.classList).forEach((val) => { 149 | if (!input.classList.contains(val)) { 150 | input.classList.add(val) 151 | } 152 | }) 153 | } 154 | } 155 | 156 | return this 157 | } 158 | 159 | /** 160 | * _setData is a helper to set this.data values 161 | * @param {String} key 162 | * @param {Array} values the first value that is not undefined will be set in this.data[key] 163 | * @param {Boolean} isDate whether this value should be a date instance 164 | * @private 165 | */ 166 | DPicker.prototype._setData = function (key, values, isDate = false) { 167 | for (let i = 0; i < values.length; i++) { 168 | if (values[i] === undefined || values[i] === '') { 169 | continue 170 | } 171 | 172 | if (isDate === false) { 173 | this.data[key] = values[i] 174 | break 175 | } 176 | 177 | if (DPicker.dateAdapter.isValid(values[i])) { 178 | this.data[key] = values[i] 179 | break 180 | } 181 | 182 | this.data[key] = new Date() 183 | 184 | const temp = DPicker.dateAdapter.isValidWithFormat(values[i], this.data.format) 185 | 186 | if (temp !== false) { 187 | this.data[key] = temp 188 | break 189 | } 190 | } 191 | } 192 | 193 | /** 194 | * Creates getters and setters for a given key 195 | * When the setter is called we will redraw 196 | * @param {String} key 197 | * @private 198 | */ 199 | DPicker.prototype._createGetSet = function (key) { 200 | if (DPicker.prototype.hasOwnProperty(key)) { 201 | return 202 | } 203 | 204 | Object.defineProperty(DPicker.prototype, key, { 205 | get: function () { 206 | return this.data[key] 207 | }, 208 | set: function (newValue) { 209 | this.data[key] = newValue 210 | this.isValid(this.data.model) 211 | this.redraw() 212 | } 213 | }) 214 | } 215 | 216 | /** 217 | * Gives the dpicker container and it's attributes 218 | * If the container is an input, the parentNode is the container but the attributes are the input's ones 219 | * @param {Element} container 220 | * @private 221 | * @return {Object} { container, attributes } 222 | */ 223 | DPicker.prototype._getContainer = function (container) { 224 | if (!container) { 225 | throw new ReferenceError('Can not initialize DPicker without a container') 226 | } 227 | 228 | const attributes = {} 229 | ;[].slice.call(container.attributes).forEach((attribute) => { 230 | attributes[attribute.name] = attribute.value 231 | }) 232 | 233 | // small jquery fix: new DPicker($('')) 234 | if (container.length !== undefined && container[0]) { 235 | container = container[0] 236 | } 237 | 238 | let reference = null 239 | 240 | if (container.tagName === 'INPUT') { 241 | if (!container.parentNode) { 242 | throw new ReferenceError('Can not initialize DPicker on an input without parent node') 243 | } 244 | 245 | const parentNode = container.parentNode 246 | reference = container 247 | container.parentNode.removeChild(reference) 248 | container = parentNode 249 | container.classList.add('dpicker') 250 | } 251 | 252 | return { container, attributes, reference } 253 | } 254 | 255 | /** 256 | * Allows to render more child elements with modules 257 | * @private 258 | * @return Array 259 | */ 260 | DPicker.prototype._getRenderChild = function () { 261 | if (!this.data.display) { 262 | return '' 263 | } 264 | 265 | let children = { 266 | years: this.renderYears(this.events, this.data), 267 | months: this.renderMonths(this.events, this.data) 268 | } 269 | 270 | // add module render functions 271 | for (let render in DPicker.renders) { 272 | children[render] = DPicker.renders[render].call(this, this.events, this.data) 273 | } 274 | 275 | children.days = this.renderDays(this.events, this.data) 276 | 277 | return this.data.order.filter(e => children[e]).map(e => children[e]) 278 | } 279 | 280 | /** 281 | * Mount rendered element to the DOM 282 | * @private 283 | */ 284 | DPicker.prototype._mount = function (element) { 285 | this._tree = this.getTree() 286 | element.appendChild(this._tree) 287 | } 288 | 289 | /** 290 | * Return the whole nodes tree 291 | * @return {Element} 292 | */ 293 | DPicker.prototype.getTree = function () { 294 | return this.renderContainer(this.events, this.data, [ 295 | this.renderInput(this.events, this.data), 296 | this.renderCalendar(this.events, this.data), 297 | this.render(this.events, this.data, this._getRenderChild()) 298 | ]) 299 | } 300 | 301 | /** 302 | * Checks whether the given model is a valid moment instance 303 | * This method does set the `.valid` flag by checking min/max allowed inputs 304 | * Note that it will return `true` if the model is valid even if it's not in the allowed range 305 | * @param {Date} date 306 | * @return {boolean} 307 | */ 308 | DPicker.prototype.isValid = function checkValidity (date) { 309 | if (DPicker.dateAdapter.isValid(date) === false) { 310 | this.data.valid = false 311 | return false 312 | } 313 | 314 | let isSame 315 | let temp 316 | 317 | if (this.data.time === false) { 318 | temp = DPicker.dateAdapter.resetHours(date) 319 | isSame = DPicker.dateAdapter.isSameDay(temp, this.data.min) || DPicker.dateAdapter.isSameDay(temp, this.data.max) 320 | } else { 321 | temp = DPicker.dateAdapter.resetSeconds(date) 322 | isSame = DPicker.dateAdapter.isSameHours(temp, this.data.min) || DPicker.dateAdapter.isSameHours(temp, this.data.max) 323 | } 324 | 325 | if (isSame === false && (DPicker.dateAdapter.isBefore(temp, this.data.min) || DPicker.dateAdapter.isAfter(temp, this.data.max))) { 326 | this.data.valid = false 327 | return true 328 | } 329 | 330 | this.data.valid = true 331 | return true 332 | } 333 | 334 | /** 335 | * Render input 336 | * @fires DPicker#inputChange 337 | * @fires DPicker#inputBlur 338 | * @fires DPicker#inputFocus 339 | * @return {Element} the rendered virtual dom hierarchy 340 | */ 341 | DPicker.prototype.renderInput = function (events, data, toRender) { 342 | return html`` 356 | } 357 | 358 | /** 359 | * Dpicker container if no input is provided 360 | * if an input is given, it's parentNode will be the container 361 | * 362 | * ``` 363 | * div.dpicker 364 | * ``` 365 | * 366 | * @return {Element} the rendered virtual dom hierarchy 367 | */ 368 | DPicker.prototype.renderContainer = function (events, data, toRender) { 369 | return html`
${toRender}
` 370 | } 371 | 372 | /** 373 | * Render a DPicker 374 | * 375 | * ``` 376 | * div.dpicker#[uuid] 377 | * input[type=text] 378 | * div.dpicker-container.dpicker-[visible|invisible] 379 | * ``` 380 | * 381 | * @see {@link DPicker#renderYears} 382 | * @see {@link DPicker#renderMonths} 383 | * @see {@link DPicker#renderDays} 384 | * @return {Element} the rendered virtual dom hierarchy 385 | */ 386 | DPicker.prototype.render = function (events, data, toRender) { 387 | return html`
390 | ${toRender} 391 |
` 392 | } 393 | 394 | /** 395 | * Render Years 396 | * ``` 397 | * select[name='dpicker-year'] 398 | * ``` 399 | * @fires DPicker#yearChange 400 | * @return {Element} the rendered virtual dom hierarchy 401 | */ 402 | DPicker.prototype.renderYears = function (events, data, toRender) { 403 | let modelYear = DPicker.dateAdapter.getYear(data.model) 404 | let futureYear = DPicker.dateAdapter.getYear(data.max) + 1 405 | let pastYear = DPicker.dateAdapter.getYear(data.min) 406 | let options = [] 407 | 408 | while (--futureYear >= pastYear) { 409 | options.push(html``) 410 | } 411 | 412 | return html`` 413 | } 414 | 415 | /** 416 | * Render Months 417 | * ``` 418 | * select[name='dpicker-month'] 419 | * ``` 420 | * @fires DPicker#monthChange 421 | * @return {Element} the rendered virtual dom hierarchy 422 | */ 423 | DPicker.prototype.renderMonths = function (events, data, toRender) { 424 | let modelMonth = DPicker.dateAdapter.getMonth(data.model) 425 | let currentYear = DPicker.dateAdapter.getYear(data.model) 426 | let months = data.months 427 | let showMonths = data.months.map((e, i) => i) 428 | 429 | if (DPicker.dateAdapter.getYear(data.max) === currentYear) { 430 | let maxMonth = DPicker.dateAdapter.getMonth(data.max) 431 | showMonths = showMonths.filter(e => e <= maxMonth) 432 | } 433 | 434 | if (DPicker.dateAdapter.getYear(data.min) === currentYear) { 435 | let minMonth = DPicker.dateAdapter.getMonth(data.min) 436 | showMonths = showMonths.filter(e => e >= minMonth) 437 | } 438 | 439 | return html`` 442 | } 443 | 444 | /** 445 | * Render Days 446 | * ``` 447 | * table 448 | * tr 449 | * td 450 | * button|span 451 | * ``` 452 | * @method 453 | * @fires DPicker#dayClick 454 | * @fires DPicker#dayKeyDown 455 | * @return {Element} the rendered virtual dom hierarchy 456 | */ 457 | DPicker.prototype.renderDays = function (events, data, toRender) { 458 | let daysInMonth = DPicker.dateAdapter.daysInMonth(data.model) 459 | let daysInPreviousMonth = DPicker.dateAdapter.daysInMonth(DPicker.dateAdapter.subMonths(data.model, 1)) 460 | let firstLocaleDay = data.firstDayOfWeek 461 | let firstDay = DPicker.dateAdapter.firstWeekDay(data.model) - 1 462 | let currentDay = DPicker.dateAdapter.getDate(data.model) 463 | let currentMonth = DPicker.dateAdapter.getMonth(data.model) 464 | let currentYear = DPicker.dateAdapter.getYear(data.model) 465 | 466 | let days = new Array(7) 467 | 468 | data.days.map((e, i) => { 469 | days[i < firstLocaleDay ? 6 - i : i - firstLocaleDay] = e 470 | }) 471 | 472 | let rows = new Array(Math.ceil(0.1 + (firstDay + daysInMonth) / 7)).fill(0) 473 | let day 474 | let dayActive 475 | let previousMonth = false 476 | let nextMonth = false 477 | let loopend = true 478 | let classActive = '' 479 | 480 | return html` 481 | ${days.map(e => html``)} 482 | ${rows.map((e, row) => { 483 | return html`${new Array(7).fill(0).map((e, col) => { 484 | dayActive = loopend 485 | classActive = '' 486 | 487 | if (col <= firstDay && row === 0) { 488 | day = daysInPreviousMonth - (firstDay - col) 489 | dayActive = false 490 | previousMonth = true 491 | } else if (col === firstDay + 1 && row === 0) { 492 | previousMonth = false 493 | day = 1 494 | dayActive = true 495 | } else { 496 | if (day === daysInMonth) { 497 | day = 0 498 | dayActive = false 499 | loopend = false 500 | nextMonth = true 501 | } 502 | 503 | day++ 504 | } 505 | 506 | let dayMonth = previousMonth ? currentMonth : (nextMonth ? currentMonth + 2 : currentMonth + 1) 507 | let currentDayModel = new Date(currentYear, dayMonth - 1, day) 508 | 509 | if (dayActive === false && data.siblingMonthDayClick === true) { 510 | dayActive = true 511 | } 512 | 513 | if (data.min && dayActive) { 514 | dayActive = DPicker.dateAdapter.isSameOrAfter(currentDayModel, data.min) 515 | } 516 | 517 | if (data.max && dayActive) { 518 | dayActive = DPicker.dateAdapter.isSameOrBefore(currentDayModel, data.max) 519 | } 520 | 521 | if (dayActive === true && previousMonth === false && nextMonth === false && currentDay === day) { 522 | classActive = 'dpicker-active' 523 | } 524 | 525 | const button = html`` 526 | 527 | return html`` 530 | })}` 531 | })} 532 |
${e}
528 | ${dayActive === true ? button : html`${day}`} 529 |
` 533 | } 534 | 535 | /** 536 | * Outputs a calendar button 537 | * @param {DPicker.events} events 538 | * @param {DPicker.data} data 539 | * @param {Array} toRender 540 | * @fires DPicker#toggleCalendar 541 | * 542 | * @return {Element} 543 | */ 544 | DPicker.prototype.renderCalendar = function renderCalendar (events, data) { 545 | if (!data.showCalendarButton) return '' 546 | return html`` 547 | } 548 | 549 | /** 550 | * Called after parseInputAttributes but before render 551 | * Decorate it with modules to do things on initialization 552 | */ 553 | DPicker.prototype.initialize = function () { 554 | this.isValid(this.data.model) 555 | } 556 | 557 | /** 558 | * The model setter, feel free to decorate through modules 559 | * @param {Date} newValue 560 | */ 561 | DPicker.prototype.modelSetter = function (newValue) { 562 | this.data.empty = !newValue 563 | 564 | if (this.isValid(newValue) === true) { 565 | this.data.model = newValue 566 | } 567 | 568 | this.redraw() 569 | } 570 | 571 | /** 572 | * Redraws the date picker 573 | * Decorate it with modules to do things before redraw 574 | */ 575 | DPicker.prototype.redraw = function () { 576 | window.requestAnimationFrame(() => { 577 | this._tree = nanomorph(this._tree, this.getTree()) 578 | }) 579 | } 580 | 581 | Object.defineProperties(DPicker.prototype, { 582 | 'container': { 583 | get: function () { 584 | return this._container 585 | } 586 | }, 587 | 'inputId': { 588 | get: function () { 589 | return this.data.inputId 590 | } 591 | }, 592 | 'input': { 593 | get: function () { 594 | if (this.data.empty) { 595 | return '' 596 | } 597 | 598 | return DPicker.dateAdapter.format(this.data.model, this.data.format) 599 | } 600 | }, 601 | /** 602 | * @method onChange 603 | * @param {Object} data 604 | * @param {Object} DPickerEvent 605 | * @param {Boolean} DPickerEvent.modelChanged whether the model has changed 606 | * @param {String} DPickerEvent.name the DPicker internal event name 607 | * @param {Event} DPickerEvent.event the original DOM event 608 | * @description 609 | * Example: 610 | * 611 | * ```javascript 612 | * var dpicker = new DPicker(container) 613 | * 614 | * dpicker.onChange = function(data, DPickerEvent) { 615 | * // has the model changed? 616 | * console.log(DPickerEvent.modelChanged) 617 | * // the name of the internal event 618 | * console.log(DPickerEvent.name) 619 | * // the origin DOM event 620 | * console.dir(DPickerEvent.event) 621 | * } 622 | * ``` 623 | * 624 | */ 625 | 'onChange': { 626 | set: function (onChange) { 627 | this._onChange = (dpickerEvent) => { 628 | return !onChange ? false : onChange(this.data, dpickerEvent) 629 | } 630 | }, 631 | get: function () { 632 | return this._onChange 633 | } 634 | }, 635 | 636 | 'valid': { 637 | get: function () { 638 | return this.data.valid 639 | } 640 | }, 641 | 642 | 'empty': { 643 | get: function () { 644 | return this.data.empty 645 | } 646 | }, 647 | 648 | 'model': { 649 | set: function (newValue) { 650 | this.modelSetter(newValue) 651 | }, 652 | get: function () { 653 | return this.data.model 654 | } 655 | } 656 | }) 657 | 658 | /** 659 | * Creates a decorator, use it to decorate public methods. 660 | * 661 | * For example: 662 | * ```javascript 663 | * DPicker.events.inputChange = DPicker.decorate(DPicker.events.inputChange, function DoSomethingOnInputChange (evt) { 664 | * // do something 665 | * }) 666 | * 667 | * ``` 668 | * 669 | * The decoration will be stopped if the method returns `false`! It's like an internal `preventDefault` to avoid altering the original event. 670 | * 671 | * @param {String} which one of events, calls 672 | * @param {Function} origin the origin function that will be decorated 673 | */ 674 | DPicker.decorate = function (origin, decoration) { 675 | return function decorator () { 676 | if (decoration.apply(this, arguments) === false) { 677 | return false 678 | } 679 | 680 | return origin.apply(this, arguments) 681 | } 682 | } 683 | 684 | DPicker.events = { 685 | /** 686 | * Hides the date picker if user does not click inside the container 687 | * @event DPicker#hide 688 | */ 689 | hide: function hide (evt) { 690 | if (this.data.hideOnOutsideClick === false || this.display === false) { 691 | return 692 | } 693 | 694 | let node = evt.target 695 | 696 | if (isElementInContainer(node.parentNode, this._container)) { 697 | return 698 | } 699 | 700 | this.display = false 701 | this.onChange({modelChanged: false, name: 'hide', event: evt}) 702 | }, 703 | 704 | /** 705 | * Change model on input change 706 | * @event DPicker#inputChange 707 | */ 708 | inputChange: function inputChange (evt) { 709 | if (!evt.target.value) { 710 | this.data.empty = true 711 | } else { 712 | let newModel = DPicker.dateAdapter.isValidWithFormat(evt.target.value, this.data.format) 713 | 714 | if (newModel !== false) { 715 | if (this.isValid(newModel) === true) { 716 | this.data.model = newModel 717 | } 718 | } 719 | 720 | this.data.empty = false 721 | } 722 | 723 | this.redraw() 724 | this.onChange({modelChanged: true, name: 'inputChange', event: evt}) 725 | }, 726 | 727 | /** 728 | * Hide on input blur 729 | * @event DPicker#inputBlur 730 | */ 731 | inputBlur: function inputBlur (evt) { 732 | if (this.display === false) { 733 | return 734 | } 735 | 736 | let node = evt.relatedTarget || evt.target 737 | 738 | if (isElementInContainer(node.parentNode, this._container)) { 739 | return 740 | } 741 | 742 | this.display = false 743 | this.onChange({modelChanged: false, name: 'inputBlur', event: evt}) 744 | }, 745 | 746 | /** 747 | * Show the container on input focus 748 | * @event DPicker#inputFocus 749 | */ 750 | inputFocus: function inputFocus (evt) { 751 | if (this.data.showCalendarOnInputFocus === false) { 752 | return 753 | } 754 | 755 | this.display = true 756 | if (evt.target && evt.target.select) { 757 | evt.target.select() 758 | } 759 | 760 | this.onChange({modelChanged: false, name: 'inputFocus', event: evt}) 761 | }, 762 | 763 | /** 764 | * On year change, update the model value 765 | * @event DPicker#yearChange 766 | */ 767 | yearChange: function yearChange (evt) { 768 | this.data.empty = false 769 | this.model = DPicker.dateAdapter.setYear(this.data.model, evt.target.options[evt.target.selectedIndex].value) 770 | 771 | if (DPicker.dateAdapter.isAfter(this.model, this.data.max)) { 772 | this.model = DPicker.dateAdapter.setMonth(this.data.model, DPicker.dateAdapter.getMonth(this.data.max)) 773 | } else if (DPicker.dateAdapter.isBefore(this.model, this.data.min)) { 774 | this.model = DPicker.dateAdapter.setMonth(this.data.model, DPicker.dateAdapter.getMonth(this.data.min)) 775 | } 776 | 777 | this.redraw() 778 | this.onChange({modelChanged: true, name: 'yearChange', event: evt}) 779 | }, 780 | 781 | /** 782 | * On month change, update the model value 783 | * @event DPicker#monthChange 784 | */ 785 | monthChange: function monthChange (evt) { 786 | this.data.empty = false 787 | this.model = DPicker.dateAdapter.setMonth(this.data.model, evt.target.options[evt.target.selectedIndex].value) 788 | 789 | this.redraw() 790 | this.onChange({modelChanged: true, name: 'monthChange', event: evt}) 791 | }, 792 | 793 | /** 794 | * On day click, update the model value 795 | * @event DPicker#dayClick 796 | */ 797 | dayClick: function dayClick (evt) { 798 | evt.preventDefault() 799 | evt.stopPropagation() 800 | this.model = DPicker.dateAdapter.setDate(this.data.model, evt.target.value) 801 | this.data.empty = false 802 | 803 | if (this.data.hideOnDayClick) { 804 | this.display = false 805 | } 806 | 807 | this.redraw() 808 | this.onChange({modelChanged: true, name: 'dayClick', event: evt}) 809 | }, 810 | 811 | /** 812 | * On previous month day click (only if `siblingMonthDayClick` is enabled) 813 | * @event DPicker#previousMonthDayClick 814 | */ 815 | previousMonthDayClick: function previousMonthDayClick (evt) { 816 | if (!this.data.siblingMonthDayClick) { 817 | return 818 | } 819 | 820 | evt.preventDefault() 821 | evt.stopPropagation() 822 | 823 | this.model = DPicker.dateAdapter.setDate(DPicker.dateAdapter.subMonths(this.data.model, 1), evt.target.value) 824 | 825 | this.data.empty = false 826 | 827 | if (this.data.hideOnDayClick) { 828 | this.display = false 829 | } 830 | 831 | this.redraw() 832 | this.onChange({modelChanged: true, name: 'previousMonthDayClick', event: evt}) 833 | }, 834 | 835 | /** 836 | * On next month day click (only if `siblingMonthDayClick` is enabled) 837 | * @event DPicker#nextMonthDayClick 838 | */ 839 | nextMonthDayClick: function nextMonthDayClick (evt) { 840 | if (!this.data.siblingMonthDayClick) { 841 | return 842 | } 843 | 844 | evt.preventDefault() 845 | evt.stopPropagation() 846 | 847 | this.model = DPicker.dateAdapter.setDate(DPicker.dateAdapter.addMonths(this.data.model, 1), evt.target.value) 848 | 849 | this.data.empty = false 850 | 851 | if (this.data.hideOnDayClick) { 852 | this.display = false 853 | } 854 | 855 | this.redraw() 856 | this.onChange({modelChanged: true, name: 'nextMonthDayClick', event: evt}) 857 | }, 858 | 859 | /** 860 | * On day key down - not implemented 861 | * @event DPicker#dayKeyDown 862 | */ 863 | dayKeyDown: function dayKeyDown () { 864 | }, 865 | 866 | /** 867 | * On key down inside the dpicker container, 868 | * intercept enter and escape keys to hide the container 869 | * @event DPicker#keyDown 870 | */ 871 | keyDown: function keyDown (evt) { 872 | if (!this.data.hideOnEnter) { 873 | return 874 | } 875 | 876 | let key = evt.which || evt.keyCode 877 | 878 | if (key !== 13 && key !== 27) { 879 | return 880 | } 881 | 882 | document.getElementById(this.inputId).blur() 883 | this.display = false 884 | this.onChange({modelChanged: false, name: 'keyDown', event: evt}) 885 | }, 886 | 887 | /** 888 | * Show calendar 889 | * @event Dpicker#showCalendar 890 | */ 891 | toggleCalendar: function showCalendar (evt) { 892 | this.display = !this.display 893 | this.onChange({modelChanged: false, name: 'toggleCalendar', event: evt}) 894 | } 895 | 896 | } 897 | 898 | /** 899 | * @property {Object} renders Renders dictionnary 900 | */ 901 | DPicker.renders = {} 902 | 903 | /** 904 | * @property {Object} properties Properties dictionnary (getters and setters will be set) 905 | */ 906 | DPicker.properties = { display: false, disabled: false } 907 | 908 | /** 909 | * @property {DateAdapter} dateAdapter The date adapter 910 | * @see {@link /_api?id=module_momentadapter|MomentDateAdapter} 911 | */ 912 | DPicker.dateAdapter = undefined 913 | 914 | /** 915 | * uuid generator 916 | * https://gist.github.com/jed/982883 917 | * @private 918 | */ 919 | function uuid () { 920 | return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, a => (a ^ Math.random() * 16 >> a / 4).toString(16)) 921 | } 922 | 923 | /** 924 | * isElementInContainer tests if an element is inside a given id 925 | * @param {Element} parent a DOM node 926 | * @param {String} containerId the container id 927 | * @private 928 | * @return {Boolean} 929 | */ 930 | function isElementInContainer (parent, containerId) { 931 | while (parent && parent !== document) { 932 | if (parent.getAttribute('id') === containerId) { 933 | return true 934 | } 935 | 936 | parent = parent.parentNode 937 | } 938 | 939 | return false 940 | } 941 | 942 | module.exports = DPicker 943 | -------------------------------------------------------------------------------- /src/dpicker.moment.js: -------------------------------------------------------------------------------- 1 | const MomentDateAdapter = require('./adapters/moment.js') 2 | const DPicker = require('./dpicker') 3 | 4 | DPicker.dateAdapter = MomentDateAdapter 5 | 6 | module.exports = DPicker 7 | -------------------------------------------------------------------------------- /src/plugins/arrow-navigation.js: -------------------------------------------------------------------------------- 1 | module.exports = function (DPicker) { 2 | /** 3 | * Get element position in parent 4 | * @param {Element} children 5 | * @return {Number} 6 | * @private 7 | */ 8 | function positionInParent (children) { 9 | return [].indexOf.call(children.parentNode.children, children) 10 | } 11 | 12 | /** 13 | * Move left 14 | * @param {Element} td 15 | * @param {Element} table 16 | * @private 17 | */ 18 | function left (td, table) { 19 | // previous td 20 | let previous = td.previousElementSibling 21 | 22 | if (previous && previous.querySelector('button')) { 23 | previous.querySelector('button').focus() 24 | return 25 | } 26 | 27 | // previous row, last button 28 | previous = td.parentNode.previousElementSibling 29 | previous = previous ? previous.querySelector('td:last-child button') : null 30 | 31 | if (previous) { 32 | return previous.focus() 33 | } 34 | 35 | // last tr first td 36 | let last = table.querySelector('tr:last-child').querySelectorAll('td.dpicker-active') 37 | last[last.length - 1].querySelector('button').focus() 38 | } 39 | 40 | /** 41 | * Move right 42 | * @param {Element} td 43 | * @param {Element} table 44 | * @private 45 | */ 46 | function right (td, table) { 47 | let next = td.nextElementSibling 48 | 49 | if (next && next.querySelector('button')) { 50 | next.querySelector('button').focus() 51 | return 52 | } 53 | 54 | next = td.parentNode.nextElementSibling 55 | next = next ? next.querySelector('td:first-child button') : null 56 | 57 | if (next) { 58 | return next.focus() 59 | } 60 | 61 | table.querySelector('tr:first-child').nextElementSibling.querySelectorAll('td.dpicker-active')[0].querySelector('button').focus() 62 | } 63 | 64 | /** 65 | * Go up or down 66 | * @param {Element} td 67 | * @param {Element} table 68 | * @param {String} direction up or down 69 | * @private 70 | */ 71 | function upOrDown (td, table, direction) { 72 | let position = positionInParent(td) 73 | let sibling = (direction === 'up' ? 'previous' : 'next') + 'ElementSibling' 74 | // previous line (tr), element (td) at the same position 75 | let previousOrNext = td.parentNode[sibling] 76 | previousOrNext = previousOrNext ? previousOrNext.children[position] : null 77 | 78 | if (previousOrNext && previousOrNext.classList.contains('dpicker-active')) { 79 | previousOrNext.querySelector('button').focus() 80 | return 81 | } 82 | 83 | // last or first line 84 | let lastOrFirst = table.querySelector('tr:' + (direction === 'up' ? 'last-child' : 'first-child')) 85 | 86 | // find the last available position with a button beggining by the bottom 87 | while (lastOrFirst) { 88 | if (lastOrFirst.children[position].classList.contains('dpicker-active')) { 89 | lastOrFirst.children[position].querySelector('button').focus() 90 | return 91 | } 92 | 93 | lastOrFirst = lastOrFirst[sibling] 94 | } 95 | } 96 | 97 | /** 98 | * Go up 99 | * @param {Element} td 100 | * @param {Element} table 101 | * @private 102 | */ 103 | function up (td, table) { 104 | return upOrDown(td, table, 'up') 105 | } 106 | 107 | /** 108 | * Go down 109 | * @param {Element} td 110 | * @param {Element} table 111 | * @private 112 | */ 113 | function down (td, table) { 114 | return upOrDown(td, table, 'down') 115 | } 116 | 117 | /** 118 | * Enables arrow navigation inside days 119 | * @event DPicker#dayKeyDown 120 | */ 121 | DPicker.events.dayKeyDown = DPicker.decorate(DPicker.events.dayKeyDown, function DayKeyDown (evt) { 122 | let key = evt.which || evt.keyCode 123 | if (key > 40 || key < 37) { 124 | return 125 | } 126 | 127 | evt.preventDefault() 128 | 129 | let td = evt.target.parentNode 130 | let table = td.parentNode.parentNode 131 | 132 | switch (key) { 133 | // left 134 | case 37: { 135 | return left(td, table) 136 | } 137 | // right 138 | case 39: { 139 | return right(td, table) 140 | } 141 | // up 142 | case 38: { 143 | return up(td, table) 144 | } 145 | // down 146 | case 40: { 147 | return down(td, table) 148 | } 149 | } 150 | }) 151 | 152 | return DPicker 153 | } 154 | -------------------------------------------------------------------------------- /src/plugins/modifiers.js: -------------------------------------------------------------------------------- 1 | module.exports = function (DPicker) { 2 | /** 3 | * Enables modifiers on `+[num]` and `-[num]` where: 4 | * - `+` gives the current date 5 | * - `+10` gives the current date + 10 days 6 | * - `-` gives the previous date 7 | * - `-10` gives the previous date - 10 days 8 | * @param {Event} DOMEvent 9 | * @listens DPicker#inputChange 10 | */ 11 | DPicker.events.inputChange = DPicker.decorate(DPicker.events.inputChange, function ModifierInputChange (evt) { 12 | let first = evt.target.value.charAt(0) 13 | let x = evt.target.value.slice(1) || 0 14 | 15 | if (first !== '-' && first !== '+') { 16 | return 17 | } 18 | 19 | if (first === '-') { 20 | if (!x) { x = 1 } 21 | x = -x 22 | } 23 | 24 | if (x < 0) { 25 | this.model = DPicker.dateAdapter.subDays(new Date(), -x) 26 | } else { 27 | this.model = DPicker.dateAdapter.addDays(new Date(), x) 28 | } 29 | 30 | this.onChange({modelChanged: true, name: 'inputChange', event: evt}) 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /src/plugins/monthAndYear.js: -------------------------------------------------------------------------------- 1 | const html = require('nanohtml') 2 | 3 | module.exports = function (DPicker) { 4 | /** 5 | * Renders concated months and Years 6 | * @param {DPicker.events} events 7 | * @param {DPicker.data} data 8 | * @param {Array} toRender 9 | * @fires DPicker#monthYearChange 10 | * 11 | * @return {Element} 12 | */ 13 | DPicker.renders.monthsAndYears = function rendermonthsAndYears (events, data) { 14 | const minMonth = DPicker.dateAdapter.getMonth(data.min) 15 | const minYear = DPicker.dateAdapter.getYear(data.min) 16 | 17 | const modelMonth = DPicker.dateAdapter.getMonth(data.model) 18 | const modelYear = DPicker.dateAdapter.getYear(data.model) 19 | 20 | const maxMonth = DPicker.dateAdapter.getMonth(data.max) 21 | const maxYear = DPicker.dateAdapter.getYear(data.max) 22 | 23 | // start with min month in year of min 24 | let showMonths = data.months.map(function (e, i) { 25 | return {month: i, year: minYear} 26 | }).filter(obj => obj.month >= minMonth) 27 | 28 | // fill months of all years 29 | let yearsToShow = maxYear - minYear 30 | for (var index = 1; index <= yearsToShow; index++) { 31 | showMonths = showMonths.concat(data.months.map(function (e, i) { 32 | return {month: i, year: minYear + index} 33 | })) 34 | } 35 | 36 | // remove unnecessary months of max year 37 | showMonths = showMonths.filter(function (obj) { 38 | if (obj.year < maxYear) { 39 | return true 40 | } 41 | return obj.month <= maxMonth 42 | }) 43 | 44 | return html`` 47 | } 48 | 49 | /** 50 | * MonthYear 51 | * @event Dpicker#monthYearChange 52 | */ 53 | DPicker.events.monthYearChange = function monthYearChange (evt) { 54 | let selectedMonthYear = evt.target.value.split('-') 55 | this.model = DPicker.dateAdapter.setMonth(this.data.model, selectedMonthYear[0]) 56 | this.model = DPicker.dateAdapter.setYear(this.data.model, selectedMonthYear[1]) 57 | this.redraw() 58 | this.onChange({modelChanged: true, name: 'monthYearChange', event: evt}) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/plugins/navigation.js: -------------------------------------------------------------------------------- 1 | const html = require('nanohtml') 2 | 3 | module.exports = function (DPicker) { 4 | /** 5 | * Renders previous month arrow 6 | * @param {DPicker.events} events 7 | * @param {DPicker.data} data 8 | * @param {Array} toRender 9 | * @fires DPicker#previousMonth 10 | * 11 | * @return {Element} 12 | */ 13 | DPicker.renders.previousMonth = function renderPreviousMonth (events, data) { 14 | const previous = DPicker.dateAdapter.subMonths(data.model, 1) 15 | return html`` 16 | } 17 | 18 | /** 19 | * Renders next month arrow 20 | * @param {DPicker.events} events 21 | * @param {DPicker.data} data 22 | * @param {Array} toRender 23 | * @fires DPicker#nextMonth 24 | * 25 | * @return {Element} 26 | */ 27 | DPicker.renders.nextMonth = function renderNextMonth (events, data) { 28 | const next = DPicker.dateAdapter.addMonths(data.model, 1) 29 | return html`` 30 | } 31 | 32 | /** 33 | * Previous month 34 | * @event Dpicker#previousMonth 35 | */ 36 | DPicker.events.previousMonth = function previousMonth (evt) { 37 | this.model = DPicker.dateAdapter.subMonths(this.data.model, 1) 38 | this.redraw() 39 | this.onChange({modelChanged: true, name: 'previousMonth', event: evt}) 40 | } 41 | 42 | /** 43 | * Next month 44 | * @event Dpicker#nextMonth 45 | */ 46 | DPicker.events.nextMonth = function nextMonth (evt) { 47 | this.model = DPicker.dateAdapter.addMonths(this.data.model, 1) 48 | this.redraw() 49 | this.onChange({modelChanged: true, name: 'nextMonth', event: evt}) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/plugins/time.js: -------------------------------------------------------------------------------- 1 | const html = require('nanohtml') 2 | 3 | module.exports = function (DPicker) { 4 | const MINUTES = new Array(60).fill(0).map((e, i) => i) 5 | const HOURS24 = new Array(24).fill(0).map((e, i) => i) 6 | const HOURS12 = new Array(12).fill(0).map((e, i) => i === 0 ? 12 : i) 7 | HOURS12.push(HOURS12.shift()) 8 | const MERIDIEM_TOKENS = ['AM', 'PM'] 9 | 10 | /** 11 | * Get hours and minutes according to the given `data` (meridiem, min/max consideration) 12 | * @param {Object} data 13 | * @private 14 | * @return {Object} `{hours, minutes}` both arrays of numbers 15 | */ 16 | function getHoursMinutes (data) { 17 | let hours = data.meridiem ? HOURS12 : HOURS24 18 | const step = parseInt(data.step) 19 | let minutes = MINUTES.filter(e => e % step === 0) 20 | 21 | ;[data.min, data.max].map((e, i) => { 22 | if (!DPicker.dateAdapter.isSameDay(data.model, e)) { 23 | return 24 | } 25 | 26 | let xHours = DPicker.dateAdapter.getHours(e) 27 | let xMinutes = DPicker.dateAdapter.getMinutes(e) 28 | if (i === 0 && xMinutes + step > 60) { 29 | DPicker.dateAdapter.setMinutes(DPicker.dateAdapter.setHours(e, i === 0 ? ++xHours : --xHours), 0) 30 | xMinutes = 0 31 | } 32 | 33 | if (data.meridiem === true) { 34 | if (xHours > 12) { 35 | xHours = xHours - 12 36 | } else if (xHours === 0) { 37 | xHours = 12 38 | } 39 | } 40 | 41 | hours = hours.filter(e => i === 0 ? e >= xHours : e <= xHours) 42 | }) 43 | 44 | return {hours, minutes} 45 | } 46 | 47 | /** 48 | * Pad left for minutes \o/ 49 | * @param {Number} v 50 | * @private 51 | * @return {String} 52 | */ 53 | function padLeftZero (v) { 54 | return v < 10 ? '0' + v : '' + v 55 | } 56 | 57 | /** 58 | * Handles minutes steps to focus on the correct input and set the model minutes/hours 59 | * @private 60 | */ 61 | function minutesStep () { 62 | if (!this.data.time || parseInt(this.data.step, 10) <= 1) { 63 | return 64 | } 65 | 66 | let {minutes} = getHoursMinutes(this.data) 67 | 68 | let modelMinutes = DPicker.dateAdapter.getMinutes(this.data.model) 69 | 70 | if (modelMinutes < minutes[0]) { 71 | this.data.model = DPicker.dateAdapter.setMinutes(this.data.model, minutes[0]) 72 | modelMinutes = minutes[0] 73 | } 74 | 75 | if (modelMinutes > minutes[minutes.length - 1]) { 76 | this.data.model = DPicker.dateAdapter.setMinutes(DPicker.dateAdapter.addHours(this.data.model, 1), 0) 77 | return 78 | } 79 | 80 | minutes[minutes.length] = 60 81 | modelMinutes = minutes.reduce(function (prev, curr) { 82 | return (Math.abs(curr - modelMinutes) < Math.abs(prev - modelMinutes) ? curr : prev) 83 | }) 84 | 85 | minutes.length-- 86 | 87 | this.data.model = DPicker.dateAdapter.setMinutes(this.data.model, modelMinutes) 88 | } 89 | 90 | /** 91 | * Render Time 92 | * ``` 93 | * select[name='dpicker-hour'] 94 | * select[name='dpicker-minutes'] 95 | * ``` 96 | * 97 | * @fires DPicker#hoursChange 98 | * @fires DPicker#minutesChange 99 | * @fires DPicker#minuteHoursChange 100 | * @fires DPicker#meridiemChange 101 | * @return {Element} the rendered virtual dom hierarchy 102 | */ 103 | DPicker.renders.time = function renderTime (events, data) { 104 | if (!data.time) { return html`