├── .npmignore ├── .jshintignore ├── website ├── images │ └── anytime.png ├── js │ └── index.js ├── styles │ ├── _skin.scss │ └── index.scss └── index.jade ├── .gitignore ├── src ├── lib │ ├── get-time-separator.js │ ├── create-button.js │ ├── get-year-list.js │ ├── create-moment.js │ ├── get-month-details.js │ └── create-slider.js ├── anytime.css ├── anytime.d.ts └── anytime.js ├── test ├── browser-env.js ├── get-month-details.test.js ├── create-moment.test.js ├── get-year-list.test.js └── anytime.test.js ├── .jshintrc ├── package.json ├── README.md ├── gulpfile.js └── dist └── anytime.js /.npmignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | test/ 3 | website/ 4 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | dist/ 3 | node_modules/ 4 | test/ 5 | website/ 6 | gulpfile.js 7 | -------------------------------------------------------------------------------- /website/images/anytime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bengourley/anytime/HEAD/website/images/anytime.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | website/build/ 4 | .DS_Store 5 | .zuulrc 6 | npm-debug.log 7 | .publish 8 | -------------------------------------------------------------------------------- /src/lib/get-time-separator.js: -------------------------------------------------------------------------------- 1 | module.exports = getTimeSeparator 2 | 3 | function getTimeSeparator() { 4 | var colonEl = document.createElement('span') 5 | colonEl.classList.add('anytime-picker__time-separator') 6 | colonEl.textContent = ':' 7 | return colonEl 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/create-button.js: -------------------------------------------------------------------------------- 1 | module.exports = createButton 2 | 3 | function createButton(text, classes) { 4 | var button = document.createElement('button') 5 | classes.forEach(function (c) { button.classList.add(c) }) 6 | button.textContent = text 7 | return button 8 | } 9 | -------------------------------------------------------------------------------- /src/anytime.css: -------------------------------------------------------------------------------- 1 | .anytime-picker { display: none; position: absolute; top: 0; left: 0; } 2 | .anytime-picker--is-visible { display: block; } 3 | .anytime-picker__dates { width: 200px; } 4 | .anytime-picker__dates > * { box-sizing: border-box; width: 14.28%; display: inline-block; } 5 | -------------------------------------------------------------------------------- /website/js/index.js: -------------------------------------------------------------------------------- 1 | var anytime = require('../../src/anytime') 2 | , time = document.getElementsByClassName('js-splash-input')[0] 3 | , button = document.getElementsByClassName('js-splash-button')[0] 4 | , picker = new anytime({ input: time, button: button, anchor: button }) 5 | 6 | picker.render() 7 | 8 | window.picker = picker 9 | -------------------------------------------------------------------------------- /test/browser-env.js: -------------------------------------------------------------------------------- 1 | module.exports = createBrowserEnv 2 | 3 | var jsdom = require('jsdom') 4 | 5 | function createBrowserEnv(cb) { 6 | jsdom.env('', function (errors, window) { 7 | if (errors) return cb(new Error(errors)) 8 | global.window = window 9 | global.document = window.document 10 | cb() 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/get-year-list.js: -------------------------------------------------------------------------------- 1 | module.exports = getYearList 2 | 3 | function getYearList(min, max) { 4 | if (parseInt(min, 10) !== min || parseInt(max, 10) !== max) throw new Error('min and max years must be integers') 5 | if (min > max) throw new Error('min year must be before max year') 6 | var years = [] 7 | for (var i = min; i <= max; i++) years.push(i) 8 | return years 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/create-moment.js: -------------------------------------------------------------------------------- 1 | module.exports = createMoment 2 | 3 | function createMoment(value) { 4 | var m = this.options.moment 5 | , args = [ value !== null ? value : undefined ] 6 | if (typeof value === 'string') args.push(this.options.format) 7 | if (this.options.timezone && typeof m.tz === 'function') { 8 | args.push(this.options.timezone) 9 | return m.tz.apply(m, args) 10 | } 11 | return m.apply(null, args) 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/get-month-details.js: -------------------------------------------------------------------------------- 1 | module.exports = getMonthDetails 2 | 3 | var moment = require('moment') 4 | 5 | /* 6 | * Given the year and month, this function returns which day of the 7 | * week that month starts, and how many days it has. 8 | */ 9 | function getMonthDetails(month, year) { 10 | var start = moment({ year: year, month: month }) 11 | return { startDay: start.isoWeekday(), length: start.endOf('month').date() } 12 | } 13 | -------------------------------------------------------------------------------- /test/get-month-details.test.js: -------------------------------------------------------------------------------- 1 | var getMonthDetails = require('../src/lib/get-month-details') 2 | , assert = require('assert') 3 | 4 | describe('getMonthDetails()', function () { 5 | 6 | it('should respond with the correct length and start day for a variety of examples', function () { 7 | 8 | // NB. Month is 0-based, day is 1-based starting from Monday 9 | 10 | assert.deepEqual(getMonthDetails(11, 2014), { startDay: 1, length: 31 }) 11 | assert.deepEqual(getMonthDetails(0, 2015), { startDay: 4, length: 31 }) 12 | assert.deepEqual(getMonthDetails(8, 1988), { startDay: 4, length: 30 }) 13 | assert.deepEqual(getMonthDetails(1, 2004), { startDay: 7, length: 29 }) 14 | 15 | }) 16 | 17 | }) 18 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { "asi": true 2 | , "boss": true 3 | , "browser": true 4 | , "camelcase": true 5 | , "curly": false 6 | , "devel": false 7 | , "devel": true 8 | , "eqeqeq": true 9 | , "eqnull": true 10 | , "es5": false 11 | , "evil": false 12 | , "immed": false 13 | , "indent": 2 14 | , "jquery": true 15 | , "latedef": false 16 | , "laxbreak": true 17 | , "laxcomma": true 18 | , "maxcomplexity": 7 19 | , "maxdepth": 4 20 | , "maxstatements": 25 21 | , "newcap": true 22 | , "node": true 23 | , "noempty": false 24 | , "nonew": true 25 | , "quotmark": "single" 26 | , "smarttabs": true 27 | , "strict": false 28 | , "trailing": false 29 | , "undef": true 30 | , "unused": true 31 | , "predef": 32 | [ "describe" 33 | , "it" 34 | , "before" 35 | , "beforeEach" 36 | , "after" 37 | , "afterEach" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/create-slider.js: -------------------------------------------------------------------------------- 1 | module.exports = createSlider 2 | 3 | function createSlider(options) { 4 | 5 | var sliderEl = document.createElement('div') 6 | sliderEl.classList.add('anytime-picker__slider') 7 | sliderEl.classList.add(options.className) 8 | 9 | var sliderTitleEl = document.createElement('span') 10 | sliderTitleEl.classList.add('anytime-picker__slider--title') 11 | sliderTitleEl.textContent = options.title 12 | 13 | sliderEl.appendChild(sliderTitleEl) 14 | 15 | var sliderInputEl = document.createElement('input') 16 | sliderInputEl.classList.add('anytime-picker__slider--input') 17 | sliderInputEl.type = 'range' 18 | sliderInputEl.min = options.min 19 | sliderInputEl.max = options.max 20 | sliderInputEl.value = options.value 21 | 22 | sliderEl.appendChild(sliderInputEl) 23 | 24 | return sliderEl 25 | 26 | } 27 | -------------------------------------------------------------------------------- /test/create-moment.test.js: -------------------------------------------------------------------------------- 1 | var createMoment = require('../src/lib/create-moment') 2 | , assert = require('assert') 3 | 4 | describe('createMoment()', function () { 5 | 6 | it('should use the `moment` setting to create the instance', function (done) { 7 | 8 | var value = '123' 9 | 10 | function spyMoment(val) { 11 | assert.equal(value, val) 12 | done() 13 | } 14 | 15 | createMoment.call({ options: { moment: spyMoment } }, value) 16 | 17 | }) 18 | 19 | it('should use pass the format option so that dates are correctly parsed', function (done) { 20 | 21 | var value = '123' 22 | , format = 'YYYY-MM-DD' 23 | 24 | function spyMoment(val, f) { 25 | assert.equal(value, val) 26 | assert.equal(format, f) 27 | done() 28 | } 29 | 30 | createMoment.call({ options: { moment: spyMoment, format: format } }, value) 31 | 32 | }) 33 | 34 | }) 35 | -------------------------------------------------------------------------------- /test/get-year-list.test.js: -------------------------------------------------------------------------------- 1 | var getYearList = require('../src/lib/get-year-list') 2 | , assert = require('assert') 3 | 4 | describe('getYearList()', function () { 5 | 6 | it('should list years between and inclusive of the given start/end years', function () { 7 | assert.deepEqual( 8 | [ 1984, 1985, 1986, 1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994 9 | , 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005 10 | ] 11 | , getYearList(1984, 2005)) 12 | }) 13 | 14 | it('should error if end year is less than start year', function () { 15 | assert.throws(function () { getYearList(2006, 2001) }) 16 | }) 17 | 18 | it('should error if either year is not an integer', function () { 19 | assert.throws(function () { getYearList('1996', 2001) }) 20 | assert.throws(function () { getYearList('2006', '2050') }) 21 | assert.throws(function () { getYearList(2006, '2050') }) 22 | assert.throws(function () { getYearList(2.04, 2080) }) 23 | }) 24 | 25 | it('should work with a single year range', function () { 26 | assert.deepEqual([ 2015 ], getYearList(2015, 2015)) 27 | }) 28 | 29 | }) 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anytime", 3 | "version": "1.4.2", 4 | "description": "A date/time picker", 5 | "author": "Ben Gourley", 6 | "license": "ISC", 7 | "publishConfig": { 8 | "registry": "http://registry.npmjs.org" 9 | }, 10 | "homepage": "http://bengourley.github.io/anytime/", 11 | "repository": { 12 | "type": "git", 13 | "url": "http://github.com/bengourley/anytime.git" 14 | }, 15 | "main": "src/anytime.js", 16 | "typings": "src/anytime.d.ts", 17 | "scripts": { 18 | "lint": "jshint . --reporter=./node_modules/jshint-full-path/index.js", 19 | "pretest": "npm run-script lint", 20 | "test": "istanbul cover ./node_modules/.bin/_mocha -- -R spec test", 21 | "posttest": "istanbul check-coverage && rm -rf coverage", 22 | "build": "gulp build:lib", 23 | "prepublish": "npm test && npm prune && npm run-script build", 24 | "buildwebsite": "gulp build:web", 25 | "watch": "gulp watch:web", 26 | "deploy": "gulp deploy" 27 | }, 28 | "dependencies": { 29 | "lodash.assign": "*", 30 | "lodash.throttle": "*", 31 | "moment": "^2.11.1", 32 | "pad-number": "^0.0.4" 33 | }, 34 | "devDependencies": { 35 | "del": "^2.2.0", 36 | "gulp": "^3.9.0", 37 | "gulp-autoprefixer": "^3.1.0", 38 | "gulp-gh-pages": "^0.5.4", 39 | "gulp-cssnano": "^2.1.0", 40 | "gulp-header": "^1.7.1", 41 | "gulp-htmlmin": "^1.3.0", 42 | "gulp-jade": "^1.1.0", 43 | "gulp-sass": "^2.1.1", 44 | "istanbul": "^0.4.2", 45 | "jsdom": "^7.2.2", 46 | "jshint": "^2.9.1", 47 | "jshint-full-path": "^1.1.1", 48 | "mocha": "^2.3.4", 49 | "moment-timezone": "^0.5.0", 50 | "st": "^1.1.0", 51 | "webpack": "^1.12.11" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # anytime 2 | 3 | [![NPM](https://nodei.co/npm/anytime.png?compact=true)](https://nodei.co/npm/anytime/) 4 | 5 | [Documentation](https://bengourley.github.io/anytime) 6 | 7 | A date/time picker. 8 | 9 | ![Anytime!](https://raw.githubusercontent.com/bengourley/anytime/master/website/images/anytime.png) 10 | 11 | ## Time, anyone? 12 | 13 | I *really* didn't want to write this module but there are no good open source alternatives. In our CMSs at [clock](https://github.com/clocklimited/) we have tonnes of instances where a **time** needs to be selected: article live dates, offer expiry dates and other scheduling. 14 | 15 | Until now we've made-do with the bloaty [jQuery UI datepicker](http://jqueryui.com/datepicker/) with the hacky [timepicker extension](http://trentrichardson.com/examples/timepicker/). I thought, "surely, someone must have built a decent, modular date *and* time picker by now?". [pikaday](https://github.com/dbushell/Pikaday) comes close – at least it's on npm, but you still have to rely on a choice of [three](https://github.com/stas/Pikaday) [different](https://github.com/xeeali/Pikaday) [forks](https://github.com/owenmead/Pikaday) for time picking. 16 | 17 | So please join me, on a journey of modularity and package managed glory in creating a date/time picker once and for all! 18 | 19 | Stay tuned. 20 | 21 | ### [Documentation Site](https://bengourley.github.io/anytime) 22 | 23 | ## Credits 24 | * [Ben Gourley](https://github.com/bengourley/) 25 | * [Eugene Cheung](https://github.com/arkon/) 26 | 27 | ## Licence 28 | Copyright (c) 2014 - present, Ben Gourley 29 | 30 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 31 | 32 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 33 | -------------------------------------------------------------------------------- /website/styles/_skin.scss: -------------------------------------------------------------------------------- 1 | // 2 | // ANYTIME DATE/TIME PICKER 3 | // ======================== 4 | // 5 | 6 | $base--border-radius: 0.3em; 7 | $color--black: #333; 8 | $color--border: #ccc; 9 | $color--grey--light: #ddd; 10 | $color--primary: #cb573e; 11 | $color--white: #fff; 12 | 13 | .anytime-picker { 14 | background-color: $color--white; 15 | border: 1px solid $color--border; 16 | border-radius: $base--border-radius; 17 | color: $color--black; 18 | display: none; 19 | line-height: 1; 20 | padding: 3px; 21 | position: absolute; 22 | width: 240px; 23 | z-index: 100; 24 | } 25 | 26 | .anytime-picker--is-visible { 27 | display: block 28 | } 29 | 30 | .anytime-picker__dropdown { 31 | display: inline-block; 32 | height: 24px; 33 | margin: 0 3px; 34 | width: auto; 35 | } 36 | 37 | .anytime-picker__dates .anytime-picker__day-name { 38 | font-size: 11px; 39 | padding-bottom: 3px; 40 | text-align: center; 41 | } 42 | 43 | .anytime-picker__dates { 44 | margin: 3px 0; 45 | width: auto; 46 | 47 | span, 48 | .anytime-picker__date { 49 | display: inline-block; 50 | padding: 8px 3px; 51 | width: (100% / 7); 52 | } 53 | } 54 | 55 | .anytime-picker__date { 56 | background: $color--grey--light; 57 | border: 0; 58 | box-shadow: inset 0 0 0 1px $color--white; // Fake table cell borders 59 | color: $color--black; 60 | cursor: pointer; 61 | position: relative; 62 | text-align: center; 63 | transition: background-color 0.4s ease, color 0.4s ease; 64 | 65 | &:hover, 66 | &:focus { 67 | background: $color--primary; 68 | color: $color--white; 69 | outline: 0; 70 | transition-duration: 0.1s; 71 | } 72 | } 73 | 74 | .anytime-picker__header, 75 | .anytime-picker__footer { 76 | padding: 5px; 77 | text-align: center; 78 | 79 | .anytime-picker__dropdown, 80 | .anytime-picker__button { 81 | margin: 3px 2px; 82 | vertical-align: middle; 83 | } 84 | } 85 | 86 | .anytime-picker__footer { 87 | background-color: $color--grey--light; 88 | 89 | .anytime-picker__button { 90 | font-size: 13px; 91 | line-height: 1em; 92 | } 93 | } 94 | 95 | .anytime-picker__time { 96 | padding: 10px 0; 97 | text-align: center; 98 | 99 | .anytime-picker__dropdown { 100 | width: 30%; 101 | } 102 | } 103 | 104 | .anytime-picker__button { 105 | background-color: $color--grey--light; 106 | border: 1px solid shade($color--grey--light, 10%); 107 | border-radius: $base--border-radius; 108 | color: $color--black; 109 | cursor: pointer; 110 | display: inline-block; 111 | font-family: inherit; 112 | line-height: 0.4; 113 | min-width: 25px; 114 | overflow: visible; // removes padding in IE 115 | padding: 6px 7px; 116 | position: relative; 117 | text-align: center; 118 | text-decoration: none; 119 | transition: all 0.3s ease; 120 | vertical-align: middle; 121 | 122 | &:focus, 123 | &:hover { 124 | background-color: shade($color--grey--light, 15%); 125 | border-color: shade($color--grey--light, 25%); 126 | color: $color--black; 127 | outline: 0; 128 | text-decoration: none; 129 | transition-duration: 0.1s; 130 | } 131 | 132 | &:active { 133 | background-color: shade($color--grey--light, 25%); 134 | border-color: shade($color--grey--light, 35%); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /website/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../src/anytime.css'; 2 | @import '_skin'; 3 | 4 | * { 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | background-color: #cb573e; 10 | color: #fffddc; 11 | font: 300 112.5%/1.7 'Roboto', sans-serif; 12 | } 13 | 14 | ::placeholder { 15 | color: #fff; 16 | } 17 | 18 | a { 19 | color: #fffddc; 20 | } 21 | 22 | h1, 23 | h2, 24 | h3, 25 | h4, 26 | h5 { 27 | a { 28 | text-decoration: none; 29 | 30 | &:hover { 31 | text-decoration: underline; 32 | } 33 | } 34 | } 35 | 36 | pre { 37 | font: 16px/1.5 'Consolas', monospace; 38 | } 39 | 40 | p, 41 | tr { 42 | code { 43 | background-color: rgba(#fffddc, 0.1); 44 | padding: 5px 3px; 45 | } 46 | } 47 | 48 | table { 49 | border: none; 50 | display: block; 51 | font-size: 0.8em; 52 | overflow: auto; 53 | } 54 | 55 | td { 56 | background: none; 57 | border: none; 58 | border-collapse: collapse; 59 | border-spacing: 0; 60 | margin: 0; 61 | padding: 0.5em; 62 | 63 | thead & { 64 | border-bottom: 1px solid #fffddc; 65 | font-weight: 700; 66 | } 67 | 68 | tbody & { 69 | border-bottom: 1px solid rgba(#fffddc, 0.1); 70 | max-width: 20%; 71 | } 72 | } 73 | 74 | .main-container { 75 | margin: 0 auto; 76 | max-width: 700px; 77 | padding: 50px 0; 78 | } 79 | 80 | .main-title { 81 | font-size: 50px; 82 | font-weight: 100; 83 | margin-bottom: 0; 84 | text-align: center; 85 | text-shadow: 3px 3px rgba(0,0,0,0.2); 86 | text-transform: uppercase; 87 | } 88 | 89 | .subtitle { 90 | font-size: 15px; 91 | font-weight: 100; 92 | margin-bottom: 80px; 93 | margin-top: 0; 94 | text-align: center; 95 | text-transform: uppercase; 96 | } 97 | 98 | .main-footer { 99 | padding: 50px 0; 100 | text-align: center; 101 | } 102 | 103 | .terminal { 104 | background: #e3e3e3; 105 | border-radius: 0.5em; 106 | margin: 6em 0 3em; 107 | padding-top: 2.3em; 108 | position: relative; 109 | } 110 | 111 | .terminal::before, 112 | .terminal::after, 113 | .terminal__content::after { 114 | background: #c8c8c8; 115 | border-radius: 50%; 116 | content: ''; 117 | height: 0.75em; 118 | position: absolute; 119 | top: 0.85em; 120 | width: 0.75em; 121 | } 122 | 123 | .terminal::before { 124 | left: 1em; 125 | } 126 | 127 | .terminal::after { 128 | left: 2.25em; 129 | } 130 | 131 | .terminal__content::after { 132 | left: 3.5em; 133 | } 134 | 135 | .terminal__content { 136 | background: #fff; 137 | border-radius: 0 0 .5em .5em; 138 | color: #000; 139 | display: block; 140 | overflow: scroll; 141 | padding: 1em 1.5em; 142 | } 143 | 144 | .splash__image { 145 | text-align: center; 146 | } 147 | 148 | .splash__input { 149 | background-color: rgba(#fff, 0.2); 150 | border-radius: 0.2em; 151 | border: 3px solid rgba(#fff, 0.3); 152 | line-height: 1.75em; 153 | margin: 3em 0; 154 | padding: 0.5em; 155 | position: relative; 156 | width: 100%; 157 | 158 | input { 159 | background: transparent; 160 | border: none; 161 | color: #fff; 162 | font-weight: 300; 163 | font: inherit; 164 | width: 100%; 165 | } 166 | 167 | > button { 168 | background-color: darken(#cb573e, 20%); 169 | border: none; 170 | border-radius: 0.2em; 171 | color: #fffddc; 172 | cursor: pointer; 173 | font: 100 0.8em/1 'Roboto', sans-serif; 174 | padding: 10px 13px 7px; 175 | position: absolute; 176 | right: 0.5em; 177 | text-transform: uppercase; 178 | 179 | &:hover { 180 | background-color: darken(#cb573e, 10%) 181 | } 182 | } 183 | } 184 | 185 | .splash__prose { 186 | margin: 5em 0; 187 | } 188 | 189 | .code-sample { 190 | background-color: #fffddc; 191 | color: #cb573e; 192 | overflow: auto; 193 | padding: 1em; 194 | } 195 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | //////////////////////////////////////////////////////////////////////////////// 4 | // Dependencies // 5 | //////////////////////////////////////////////////////////////////////////////// 6 | 7 | var autoprefixer = require('gulp-autoprefixer') 8 | , del = require('del') 9 | , ghPages = require('gulp-gh-pages') 10 | , gulp = require('gulp') 11 | , header = require('gulp-header') 12 | , htmlmin = require('gulp-htmlmin') 13 | , http = require('http') 14 | , jade = require('gulp-jade') 15 | , nano = require('gulp-cssnano') 16 | , sass = require('gulp-sass') 17 | , st = require('st') 18 | , webpack = require('webpack') 19 | 20 | 21 | //////////////////////////////////////////////////////////////////////////////// 22 | // Config // 23 | //////////////////////////////////////////////////////////////////////////////// 24 | 25 | var paths = 26 | { webSrcHtml: './website/index.jade' 27 | , webSrcJs: './website/js/index.js' 28 | , webSrcScss: './website/styles/index.scss' 29 | , webDest: './website/build/' 30 | , libSrcJs: './src/anytime.js' 31 | , libDest: './dist/' 32 | } 33 | 34 | var webpackPlugins = 35 | [ new webpack.optimize.DedupePlugin() 36 | , new webpack.optimize.OccurenceOrderPlugin() 37 | , new webpack.optimize.UglifyJsPlugin( 38 | { compress: { warnings: false } 39 | , output: { comments: false } 40 | , sourceMap: false 41 | } 42 | ) 43 | ] 44 | 45 | 46 | //////////////////////////////////////////////////////////////////////////////// 47 | // Gulp tasks for the website // 48 | //////////////////////////////////////////////////////////////////////////////// 49 | 50 | // Delete generated website files 51 | gulp.task('clean:web', function() { 52 | del.sync(paths.webDest, { force: true }) 53 | }) 54 | 55 | // Process main HTML file 56 | gulp.task('html', function() { 57 | return gulp.src(paths.webSrcHtml) 58 | .pipe(jade()) 59 | .pipe(htmlmin({ removeComments: true, collapseWhitespace: true })) 60 | .pipe(gulp.dest(paths.webDest)) 61 | }) 62 | 63 | // Process SCSS files 64 | gulp.task('styles', function() { 65 | return gulp.src(paths.webSrcScss) 66 | .pipe(sass().on('error', sass.logError)) 67 | .pipe(autoprefixer({ browsers: ['last 2 versions'] })) 68 | .pipe(nano()) 69 | .pipe(gulp.dest(paths.webDest)) 70 | }) 71 | 72 | // Process JS scripts 73 | gulp.task('scripts', function(cb) { 74 | webpack( 75 | { entry: paths.webSrcJs 76 | , output: 77 | { path: paths.webDest 78 | , filename: 'index.js' 79 | } 80 | , plugins: webpackPlugins 81 | } 82 | , function(webpack_err, stats) { 83 | if (webpack_err) cb(webpack_err) 84 | cb() 85 | } 86 | ) 87 | }) 88 | 89 | // Compile library website 90 | gulp.task('build:web', ['clean:web', 'html', 'styles', 'scripts']) 91 | 92 | // Launch static server for website (localhost:8080) and watch for changes 93 | gulp.task('watch:web', ['build:web'], function() { 94 | gulp.watch(paths.webSrcHtml, ['html']) 95 | gulp.watch(paths.webSrcScss, ['styles']) 96 | gulp.watch(paths.webSrcJs, ['scripts']) 97 | 98 | http.createServer( 99 | st( 100 | { path: paths.webDest 101 | , index: 'index.html' 102 | } 103 | ) 104 | ).listen(8080) 105 | }) 106 | 107 | // Deploy to GitHub Pages 108 | gulp.task('deploy', ['build:web'], function() { 109 | return gulp.src(paths.webDest + '/**/*') 110 | .pipe(ghPages()) 111 | }) 112 | 113 | 114 | //////////////////////////////////////////////////////////////////////////////// 115 | // Gulp tasks for the library // 116 | //////////////////////////////////////////////////////////////////////////////// 117 | 118 | // Delete generated Anytime files 119 | gulp.task('clean:lib', function() { 120 | del.sync(paths.libDest, { force: true }) 121 | }) 122 | 123 | var buildLib = function(filename, externals, cb) { 124 | var pkg = require('./package.json') 125 | 126 | var banner = 127 | [ '/**' 128 | , ' * <%= pkg.name %> - <%= pkg.description %>' 129 | , ' * @version <%= pkg.version %>' 130 | , ' * @link <%= pkg.homepage %>' 131 | , ' * @license <%= pkg.license %>' 132 | , ' */' 133 | , '' 134 | ].join('\n') 135 | 136 | // Bundle with Webpack 137 | webpack( 138 | { entry: paths.libSrcJs 139 | , output: 140 | { path: paths.libDest 141 | , filename: filename 142 | , library: 'anytime' 143 | , libraryTarget: 'umd' 144 | } 145 | , plugins: webpackPlugins 146 | , externals: externals 147 | } 148 | , function(webpack_err, stats) { 149 | if (webpack_err) cb(webpack_err) 150 | 151 | gulp.src(paths.libDest + filename) 152 | .pipe(header(banner, { pkg: pkg } )) 153 | .pipe(gulp.dest(paths.libDest)) 154 | .on('end', cb) 155 | } 156 | ) 157 | } 158 | 159 | gulp.task('build:lib-with-moment', function(cb) { 160 | buildLib('anytime-with-moment.js', {}, cb); 161 | }) 162 | 163 | gulp.task('build:lib-without-moment', function(cb) { 164 | buildLib('anytime.js', { 'moment': 'moment' }, cb); 165 | }) 166 | 167 | // Compile Anytime library 168 | gulp.task('build:lib', ['clean:lib', 'build:lib-with-moment', 'build:lib-without-moment']) 169 | -------------------------------------------------------------------------------- /src/anytime.d.ts: -------------------------------------------------------------------------------- 1 | export interface AnytimeOptions { 2 | /** 3 | * An input whose value should update when the picker’s value changes. 4 | * @default null 5 | */ 6 | input?: HTMLInputElement; 7 | 8 | /** 9 | * An element that the picker will orient itself near when displayed. 10 | * If options.input is not provided, this option is required. 11 | * @default options.input 12 | */ 13 | anchor?: HTMLElement; 14 | 15 | /** 16 | * An element that when clicked will show/hide the picker interface. 17 | * @default null 18 | */ 19 | button?: HTMLElement; 20 | 21 | /** 22 | * The earliest year that can be shown or selected in the picker interface. 23 | * @default 1960 24 | */ 25 | minYear?: number; 26 | 27 | /** 28 | * The latest year that can be shown or selected in the picker interface. 29 | * @default 2030 30 | */ 31 | maxYear?: number; 32 | 33 | /** 34 | * By default anytime will show every minute. 35 | * Set this to 5 or 15 etc to show fewer options at greater intervals. 36 | * @default 1 37 | */ 38 | minuteIncrement?: number; 39 | 40 | /** 41 | * The distance (px) from options.anchor the picker interface should be displayed. 42 | * @default 5 43 | */ 44 | offset?: number; 45 | 46 | /** 47 | * The initial value to set the picker’s internal value. 48 | * @default null 49 | */ 50 | initialValue?: Date; 51 | 52 | /** 53 | * Value to indicate which month/year to display when picker is first shown. 54 | * If options.initialValue is selected, that will take precedence. 55 | * @default new Date() 56 | */ 57 | initialView?: Date; 58 | 59 | /** 60 | * moment-style date format string. 61 | * @default 'h:mma on dddd D MMMM YYYY' 62 | */ 63 | format?: string; 64 | 65 | /** 66 | * By default anytime uses an instance of moment in the browser’s timezone with the English locale. 67 | * If you want to use a different language or a different timezone, you must load in a locale to moment 68 | * and/or pass in a version of moment-timezone. 69 | * @type moment or moment-timezone 70 | * @default moment 71 | */ 72 | moment?: any; 73 | 74 | /** 75 | * moment-style timezone string (e.g. 'Europe/London'). 76 | * Only functions if moment-timezone is provided as options.moment! 77 | * @default Browser’s timezone 78 | */ 79 | timezone?: string 80 | 81 | /** 82 | * 83 | */ 84 | showTime?: boolean; 85 | 86 | /** 87 | * Use sliders instead of the default dropdowns for the time input. 88 | * @default false 89 | */ 90 | timeSliders?: boolean; 91 | 92 | /** 93 | * Choose whether to abbreviate month names, e.g "Jan" vs. "January". 94 | * @default true 95 | */ 96 | shortMonthNames?: boolean; 97 | 98 | /** 99 | * Set the text of the button that closes the picker interface. 100 | * @default 'Done' 101 | */ 102 | doneText?: string; 103 | 104 | /** 105 | * Set the text of the button that clears the picker value and closes the picker interface. 106 | * @default 'Clear' 107 | */ 108 | clearText?: string; 109 | 110 | /** 111 | * Set the text of the label before the time sliders. 112 | * @default 'Time:' 113 | */ 114 | timeSlidersText?: string; 115 | 116 | /** 117 | * Set the text of the label before the hour slider. 118 | * @default 'Hour:' 119 | */ 120 | timeSlidersHourText?: string; 121 | 122 | /** 123 | * Set the text of the label before the minute slider. 124 | * @default 'Minute:' 125 | */ 126 | timeSlidersMinuteText?: string; 127 | } 128 | 129 | export interface Anytime { 130 | /** 131 | * Instantiates and returns a new picker with the provided options. 132 | */ 133 | new (options?: AnytimeOptions): Anytime; 134 | 135 | /** 136 | * Renders the picker interface. This method must be called before show() is called. 137 | */ 138 | render(): void; 139 | 140 | /** 141 | * Displays the picker interface. 142 | */ 143 | show(): void; 144 | 145 | /** 146 | * Removes the picker interface from display. 147 | */ 148 | hide(): void; 149 | 150 | /** 151 | * Shows the picker if it is currently hidden, hides it if currently displayed. 152 | */ 153 | toggle(): void; 154 | 155 | /** 156 | * Update the internal value of the picker. This will also update the related input (if there is one). 157 | * @param {string | Date} val An ISO8601 string or Date object. Passing in null clears the picker. 158 | */ 159 | update(val: string | Date): void; 160 | 161 | /** 162 | * Update the internal value of the picker. This will also update the related input (if there is one). 163 | * @param {function} fn A function where you can manipulate the internal moment object. 164 | * The moment object must be returned. 165 | */ 166 | update(fn: (m: any) => any): void; 167 | 168 | /** 169 | * When a value is selected (or cleared) with the picker, the change event will emit with the new value. 170 | * @param {string} event This must be "change". 171 | * @param {function} fn A callback function with the new value. 172 | */ 173 | on(event: string, fn: (d: Date) => any): void; 174 | 175 | /** 176 | * Removes all event listeners and removes the picker interface element from the DOM. 177 | */ 178 | destroy(): void; 179 | } 180 | 181 | export var anytime: Anytime; 182 | 183 | export default anytime; 184 | -------------------------------------------------------------------------------- /test/anytime.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | , moment = require('moment-timezone') 3 | , createBrowserEnv = require('./browser-env') 4 | 5 | describe('anytime', function () { 6 | 7 | beforeEach(createBrowserEnv) 8 | 9 | // Clear the module cache for anytime so that it can be reloaded again. 10 | // This is needed to test option defaults that are set in the module scope 11 | beforeEach(function () { delete require.cache[require.resolve('../')] }) 12 | 13 | describe('destroy()', function () { 14 | 15 | it('should remove any bound events', function () { 16 | 17 | var Picker = require('../src/anytime') 18 | , p = new Picker({ input: document.createElement('input') }) 19 | 20 | p.render() 21 | p.on('change', function () {}) 22 | assert(Object.keys(p._events).length) 23 | p.destroy() 24 | assert.equal(Object.keys(p._events).length, 0) 25 | 26 | }) 27 | 28 | it('should remove the element from the DOM', function () { 29 | 30 | var Picker = require('../src/anytime') 31 | , parent = document.createElement('div') 32 | , p = new Picker({ input: document.createElement('input') }) 33 | 34 | parent.appendChild(p.render().el) 35 | 36 | p.on('change', function () {}) 37 | assert.equal(parent.childNodes.length, 1) 38 | p.destroy() 39 | assert.equal(parent.childNodes.length, 0) 40 | 41 | }) 42 | 43 | }) 44 | 45 | describe('timezone', function () { 46 | it('should allow you to pass in a timezone which modifies all displayed dates', function () { 47 | var Picker = require('../src/anytime') 48 | , parent = document.createElement('input') 49 | , moment = require('moment-timezone') 50 | , p = new Picker( 51 | { input: parent 52 | , moment: moment 53 | , timezone: 'America/New_York' 54 | , format: 'z' 55 | }) 56 | 57 | p.render() 58 | 59 | p.update(new Date(Date.UTC(2015, 4, 11, 0, 0, 0))) 60 | assert.equal(parent.value, 'EDT') 61 | }) 62 | 63 | it('should display the correct time in the time input', function () { 64 | // 9am UTC may 11th is 5am new york time 65 | var Picker = require('../src/anytime') 66 | , parent = document.createElement('input') 67 | , date = new Date(Date.UTC(2015, 4, 11, 9, 0, 0)) 68 | , moment = require('moment-timezone') 69 | , p = new Picker( 70 | { input: parent 71 | , moment: moment 72 | , timezone: 'America/New_York' 73 | , initialValue: date 74 | }) 75 | 76 | p.render() 77 | 78 | var hourSelect = p.el.querySelector('.anytime-picker__dropdown--hours') 79 | assert.equal(hourSelect.value, '5') 80 | }) 81 | }) 82 | 83 | describe('selected day', function () { 84 | it('should add a class to the selected day', function () { 85 | var Picker = require('../src/anytime') 86 | , parent = document.createElement('input') 87 | , p = new Picker({ input: parent }) 88 | , date = moment().toDate() 89 | 90 | p.render() 91 | p.update(date) 92 | 93 | var day = p.el.querySelector('button[data-date=\'' + date.getDate() + '\']') 94 | 95 | assert(day.getAttribute('class').indexOf('anytime-picker__date--selected') > -1, 'Should have a class on it') 96 | }) 97 | 98 | it('should update the class when the date changes', function () { 99 | var Picker = require('../src/anytime') 100 | , parent = document.createElement('input') 101 | , p = new Picker({ input: parent, initialValue: moment('2015-04-01') }) 102 | , cls = 'anytime-picker__date--selected' 103 | 104 | p.render() 105 | p.update(moment('2015-04-10').toDate()) 106 | p.update(moment('2015-04-12').toDate()) 107 | 108 | var firstSelectedDay = p.el.querySelector('button[data-date=\'10\']') 109 | , secondSelectedDay = p.el.querySelector('button[data-date=\'12\']') 110 | 111 | assert(firstSelectedDay.getAttribute('class').indexOf(cls) === -1, 'Should not have a class on it') 112 | assert(secondSelectedDay.getAttribute('class').indexOf(cls) > -1, 'Should have a class on it') 113 | }) 114 | 115 | it('should only have the class in the correct month', function () { 116 | var Picker = require('../src/anytime') 117 | , parent = document.createElement('input') 118 | , selectedDay = 13 119 | , p = new Picker({ input: parent, initialValue: moment('2015-02-' + selectedDay).toDate() }) 120 | 121 | p.render() 122 | p.showNextMonth() 123 | 124 | var day = p.el.querySelector('button[data-date=\'' + selectedDay + '\']') 125 | 126 | assert(day.getAttribute('class').indexOf('anytime-picker__date--selected') === -1, 'Should not have a class on it') 127 | }) 128 | 129 | it('should only have the class in the correct year', function () { 130 | var Picker = require('../src/anytime') 131 | , parent = document.createElement('input') 132 | , selectedDay = 13 133 | , p = new Picker({ input: parent, initialValue: moment('2015-02-' + selectedDay).toDate() }) 134 | 135 | p.render() 136 | 137 | // Pushing the date forward by a year 138 | for (var i = 0; i <= 11; i += 1) { 139 | p.showNextMonth() 140 | } 141 | 142 | var day = p.el.querySelector('button[data-date=\'' + selectedDay + '\']') 143 | 144 | assert(day.getAttribute('class').indexOf('anytime-picker__date--selected') === -1, 'Should not have a class on it') 145 | }) 146 | }) 147 | 148 | describe('current day', function () { 149 | it('should add a class to the current day', function () { 150 | var Picker = require('../src/anytime') 151 | , parent = document.createElement('input') 152 | , p = new Picker({ input: parent }) 153 | , date = moment() 154 | , currentDay = +date.format('D') 155 | 156 | p.render() 157 | 158 | var day = p.el.querySelector('button[data-date=\'' + currentDay + '\']') 159 | 160 | assert(day.getAttribute('class').indexOf('anytime-picker__date--current') > -1, 'Should have a class on it') 161 | }) 162 | 163 | it('should only have the class in the correct month', function () { 164 | var Picker = require('../src/anytime') 165 | , parent = document.createElement('input') 166 | , p = new Picker({ input: parent }) 167 | , date = moment() 168 | , currentDay = +date.format('D') 169 | 170 | p.render() 171 | 172 | p.showNextMonth() 173 | 174 | var day = p.el.querySelector('button[data-date=\'' + currentDay + '\']') 175 | 176 | assert(day.getAttribute('class').indexOf('anytime-picker__date--current') === -1, 'Should not have a class on it') 177 | }) 178 | 179 | it('should only have the class in the correct year', function () { 180 | var Picker = require('../src/anytime') 181 | , parent = document.createElement('input') 182 | , p = new Picker({ input: parent }) 183 | , date = moment() 184 | , currentDay = +date.format('D') 185 | 186 | p.render() 187 | 188 | // Pushing the date forward by a year 189 | for (var i = 0; i <= 11; i += 1) { 190 | p.showNextMonth() 191 | } 192 | 193 | var day = p.el.querySelector('button[data-date=\'' + currentDay + '\']') 194 | 195 | assert(day.getAttribute('class').indexOf('anytime-picker__date--current') === -1, 'Should not have a class on it') 196 | }) 197 | }) 198 | 199 | describe('minutes', function () { 200 | 201 | it('should output 60 minutes', function () { 202 | var Picker = require('../src/anytime') 203 | , parent = document.createElement('input') 204 | , p = new Picker({ input: parent }) 205 | 206 | p.render() 207 | 208 | var minutes = p.el.querySelector('.anytime-picker__dropdown--minutes') 209 | assert.equal(minutes.length, 60) 210 | }) 211 | 212 | }) 213 | 214 | it('should not throw when rendered with a null initialValue', function () { 215 | var Picker = require('../src/anytime') 216 | , parent = document.createElement('input') 217 | , p = new Picker({ input: parent, initialValue: null }) 218 | 219 | assert.doesNotThrow(function () { 220 | p.render() 221 | }, /setAttribute/) 222 | }) 223 | 224 | describe('locale', function () { 225 | 226 | it('should default to english', function () { 227 | 228 | var Picker = require('../src/anytime') 229 | , parent = document.createElement('input') 230 | , p = new Picker({ input: parent, initialValue: new Date() }) 231 | 232 | assert.equal('Jan', p.monthNames[0]) 233 | assert.equal('en', p.value.locale()) 234 | 235 | }) 236 | 237 | it('should use a provided locale', function () { 238 | 239 | var Picker = require('../src/anytime') 240 | , parent = document.createElement('input') 241 | , moment = require('moment') 242 | 243 | moment.locale('fr') 244 | 245 | var p = new Picker({ input: parent, initialValue: new Date(), moment: moment }) 246 | 247 | assert.equal('janv.', p.monthNames[0]) 248 | assert.equal('fr', p.value.locale()) 249 | 250 | }) 251 | 252 | it('should set "done" and "clear" button text to provided option', function () { 253 | var Picker = require('../src/anytime') 254 | , parent = document.createElement('input') 255 | , p = new Picker({ 256 | input: parent, 257 | initialValue: null , 258 | doneText: 'Set Time', 259 | clearText: 'Goodbye' 260 | }) 261 | 262 | p.render() 263 | 264 | var doneButton = p.el.querySelector('.anytime-picker__button--done') 265 | assert.equal(doneButton.textContent, 'Set Time', 'should set done button text') 266 | var clearButton = p.el.querySelector('.anytime-picker__button--clear') 267 | assert.equal(clearButton.textContent, 'Goodbye', 'should set clear button text') 268 | }) 269 | 270 | }) 271 | 272 | }) 273 | -------------------------------------------------------------------------------- /website/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset='utf8') 5 | title Anytime – a JavaScript date and time picker 6 | link(rel='stylesheet', href='https://fonts.googleapis.com/css?family=Roboto:300,100,400,700') 7 | link(rel='stylesheet', href='./index.css') 8 | body 9 | .main-container 10 | header.main-header 11 | h1.main-title Anytime 12 | h2.subtitle A JS date and time picker 13 | .splash 14 | .splash__input 15 | input(type='text', disabled, placeholder='If not now, when?').js-splash-input 16 | button.js-splash-button Select a time 17 | .splash__prose 18 | ul 19 | li Say goodbye to jQuery and jQuery UI! 20 | li Modularity FTW! For use with browserify 21 | li Only 4 lines of essential CSS (the rest is up to you) 22 | li Works in modern browsers: Chrome, FF, Safari, Edge 23 | 24 | pre.terminal: code.terminal__content $ npm i --save anytime 25 | 26 | .docs 27 | h1: a(id='usage', href='#usage') Usage 28 | 29 | p The only supported way of using Anytime is with browserify. 30 | 31 | pre.code-sample: code. 32 | var anytime = require('anytime') 33 | 34 | p. 35 | Here’s 36 | the minimum CSS required to get Anytime functional. It’ll look pretty bare 37 | and I’m sure you’ll want to style it up. There are plenty of classes on each 38 | component that you can hook into for styling. 39 | 40 | h2: a(id='constructor', href='#constructor') Creating a date/time picker: 41 | pre.code-sample: code. 42 | var p = new anytime(options) 43 | 44 | h3: a(id='options', href='#options') Options 45 | table(cellpadding='0' cellspacing='0') 46 | thead 47 | tr 48 | td Name 49 | td Type 50 | td Description 51 | td Default 52 | 53 | tbody 54 | tr 55 | td: code input 56 | td HTMLInputElement 57 | td: p An input whose value should update when the picker’s value changes. 58 | td: code null 59 | 60 | tr 61 | td: code anchor 62 | td HTMLElement 63 | td: p An element that the picker will orient itself near when displayed. If options.input is not provided, this option is required. 64 | td: code options.input 65 | 66 | tr 67 | td: code button 68 | td HTMLElement 69 | td: p An element that when clicked will show/hide the picker interface. 70 | td: code null 71 | 72 | tr 73 | td: code minYear 74 | td Number 75 | td: p The earliest year that can be shown or selected in the picker interface. 76 | td: code 1960 77 | 78 | tr 79 | td: code maxYear 80 | td Number 81 | td: p The latest year that can be shown or selected in the picker interface. 82 | td: code 2030 83 | 84 | tr 85 | td: code minuteIncrement 86 | td Number 87 | td: p By default anytime will show every minute. Set this to 5 or 15 etc to show fewer options at greater intervals. 88 | td: code 1 89 | 90 | tr 91 | td: code offset 92 | td Number 93 | td: p The distance (px) from options.anchor the picker interface should be displayed. 94 | td: code 5 95 | 96 | tr 97 | td: code initialValue 98 | td Date 99 | td: p The initial value to set the picker’s internal value. 100 | td: code null 101 | 102 | tr 103 | td: code initialView 104 | td Date 105 | td: p Value to indicate which month/year to display when picker is first shown. If options.initialValue is selected, that will take precedence. 106 | td: code new Date() 107 | 108 | tr 109 | td: code format 110 | td String 111 | td: p moment-style date format string. 112 | td: code 'h:mma on dddd D MMMM YYYY' 113 | 114 | tr 115 | td: code moment 116 | td moment, moment-timezone 117 | td: p By default anytime uses an instance of moment in the browser’s timezone with the English locale. If you want to use a different language or a different timezone, you must load in a locale to moment and/or pass in a version of moment-timezone. 118 | td: code moment 119 | 120 | tr 121 | td: code timezone 122 | td String 123 | td: p moment-style timezone string (e.g. 'Europe/London'). Only functions if moment-timezone is provided as options.moment! 124 | td Browser’s timezone 125 | 126 | tr 127 | td: code showTime 128 | td Boolean 129 | td: p Display the time picker portion. 130 | td: code true 131 | 132 | tr 133 | td: code timeSliders 134 | td Boolean 135 | td: p Use sliders instead of the default dropdowns for the time input. 136 | td: code false 137 | 138 | tr 139 | td: code shortMonthNames 140 | td Boolean 141 | td: p Choose whether to abbreviate month names, e.g "Jan" vs. "January". 142 | td: code true 143 | 144 | tr 145 | td: code doneText 146 | td String 147 | td: p Set the text of the button that closes the picker interface. 148 | td: code 'Done' 149 | 150 | tr 151 | td: code clearText 152 | td String 153 | td: p Set the text of the button that clears the picker value and closes the picker interface. 154 | td: code 'Clear' 155 | 156 | tr 157 | td: code timeSlidersText 158 | td String 159 | td: p Set the text of the label before the time sliders. 160 | td: code 'Time:' 161 | 162 | tr 163 | td: code timeSlidersHourText 164 | td String 165 | td: p Set the text of the label before the hour slider. 166 | td: code 'Hour:' 167 | 168 | tr 169 | td: code timeSlidersMinuteText 170 | td String 171 | td: p Set the text of the label before the minute slider. 172 | td: code 'Minute:' 173 | 174 | h2: a(id='api', href='#api') API 175 | 176 | h3: a(id='render', href='#render') p.render() 177 | p Renders the picker interface. This method must be called before p.show() is called. 178 | 179 | h3: a(id='show', href='#show') p.show() 180 | p Displays the picker interface. 181 | 182 | h3: a(id='hide', href='#hide') p.hide() 183 | p Removes the picker interface from display. 184 | 185 | h3: a(id='toggle', href='#toggle') p.toggle() 186 | p Shows the picker if it is currently hidden, hides it if currently displayed. 187 | 188 | h3: a(id='update', href='#update') p.update(value or fn) 189 | p Update the internal value of the picker. This will also update the related input (if there is one). 190 | p There are two ways to update the value: 191 | 192 | h4: a(id='update-value', href='#update-value') Pass a value 193 | pre.code-sample: code. 194 | p.update(new Date(2015, 4, 0)) // JS Date object 195 | p.update('2015-06-10T12:16:47.997Z') // String 196 | p.update(null) // Clear the value 197 | 198 | h4: a(id='update-fn', href='#update-fn') Use a function to manipulate the internal moment object 199 | p The return value is used to set the new date so you must return the moment object! 200 | pre.code-sample: code. 201 | picker.update(function (m) { 202 | return m.add(1, 'day') // increment the day 203 | }) 204 | 205 | h3: a(id='change', href='#change') p.on('change', fn) 206 | p When a value is selected (or cleared) with the picker, the change event will emit with the new value. 207 | 208 | pre.code-sample: code. 209 | p.on('change', function (d) { 210 | // When set, d will be a date 211 | // When cleared d will be null 212 | console.log('The new date/time is…', d) 213 | }) 214 | 215 | h3: a(id='destroy', href='#destroy') p.destroy() 216 | p Removes all event listeners and removes the picker interface element from the DOM. 217 | 218 | h2: a(id='examples', href='#examples') Examples 219 | 220 | h3: a(id='i18n', href='#i18n') i18n 221 | p. 222 | To use Anytime in a language other than the default (English) you need to load in your desired locale 223 | to moment and pass it in as an option like so: 224 | 225 | pre.code-sample: code. 226 | var moment = require('moment') 227 | require('moment/locale/fr') 228 | moment.locale('fr') 229 | var picker = new anytime({ moment: moment }) 230 | 231 | p If you want timezone support, you must pass in a moment-timezone instance and set the timezone option: 232 | 233 | pre.code-sample: code. 234 | var moment = require('moment-timezone') 235 | require('moment/locale/fr') 236 | moment.locale('fr') 237 | var picker = new anytime({ moment: moment, timezone: 'Europe/Paris' }) 238 | 239 | footer.main-footer 240 | p Made by Ben Gourley at Clock. 241 | 242 | script(src='index.js') 243 | -------------------------------------------------------------------------------- /src/anytime.js: -------------------------------------------------------------------------------- 1 | module.exports = AnytimePicker 2 | 3 | var Emitter = require('events').EventEmitter 4 | , extend = require('lodash.assign') 5 | , throttle = require('lodash.throttle') 6 | , pad = require('pad-number') 7 | , moment = require('moment') 8 | , getYearList = require('./lib/get-year-list') 9 | , getTimeSeparator = require('./lib/get-time-separator') 10 | , createButton = require('./lib/create-button') 11 | , createSlider = require('./lib/create-slider') 12 | , getMonthDetails = require('./lib/get-month-details') 13 | , createMoment = require('./lib/create-moment') 14 | , defaults = 15 | { minYear: 1960 16 | , maxYear: 2030 17 | , offset: 5 18 | , initialValue: null 19 | , initialView: new Date() 20 | , format: 'h:mma on dddd D MMMM YYYY' 21 | , moment: moment 22 | , minuteIncrement: 1 23 | , showTime: true 24 | , timeSliders: false 25 | , shortMonthNames: true 26 | , doneText: 'Done' 27 | , clearText: 'Clear' 28 | , timeSlidersText: 'Time:' 29 | , timeSlidersHourText: 'Hour:' 30 | , timeSlidersMinuteText: 'Minute:' 31 | } 32 | 33 | function AnytimePicker(options) { 34 | this.options = extend({}, defaults, options) 35 | 36 | Emitter.call(this) 37 | 38 | // A place to store references to event callback functions so they can be specifically unbound later on 39 | this.__events = {} 40 | 41 | this.el = document.createElement('div') 42 | this.el.className = 'js-anytime-picker anytime-picker' 43 | 44 | this.options.initialValue = this.getInitialValue() 45 | 46 | var initialView = this.createMoment(this.options.initialValue || this.options.initialView) 47 | this.currentView = { month: initialView.month(), year: initialView.year() } 48 | 49 | this.value = this.options.initialValue ? this.createMoment(this.options.initialValue).seconds(0).milliseconds(0) : null 50 | 51 | if (this.value && !this.options.showTime) { 52 | this.value = this.value.hour(0).minute(0) 53 | } 54 | 55 | this.monthNames = this.getMonthNames() 56 | 57 | this.el.addEventListener('click', function (e) { 58 | if (e.target.classList.contains('js-anytime-picker-day')) { 59 | e.stopPropagation() 60 | this.update(function (value) { 61 | return value 62 | .date(parseInt(e.target.getAttribute('data-date'), 10)) 63 | .month(parseInt(e.target.getAttribute('data-month'), 10)) 64 | .year(parseInt(e.target.getAttribute('data-year'), 10)) 65 | }) 66 | } 67 | }.bind(this)) 68 | 69 | // If the target element is within a form element this stops button clicks from submitting it 70 | this.el.addEventListener('click', function (e) { e.preventDefault() }) 71 | 72 | this.__events['misc toggle'] = this.toggle.bind(this) 73 | if (this.options.button) this.options.button.addEventListener('click', this.__events['misc toggle']) 74 | this.options.input.addEventListener('click', this.__events['misc toggle']) 75 | 76 | this.root = this.options.anchor ? this.options.anchor : this.options.input 77 | 78 | if (this.options.input) { 79 | this.updateInput(this) 80 | this.on('change', this.updateInput.bind(this)) 81 | } 82 | } 83 | 84 | AnytimePicker.prototype = Object.create(Emitter.prototype) 85 | 86 | AnytimePicker.prototype.createMoment = createMoment 87 | 88 | AnytimePicker.prototype.updateInput = function () { 89 | this.options.input.value = this.value ? this.value.format(this.options.format) : '' 90 | } 91 | 92 | AnytimePicker.prototype.getInitialValue = function () { 93 | if (this.options.initialValue) return this.options.initialValue 94 | if (this.options.input && this.options.input.value) return this.options.input.value 95 | return null 96 | } 97 | 98 | AnytimePicker.prototype.getMonthNames = function () { 99 | return this.options.moment[this.options.shortMonthNames ? 'monthsShort' : 'months']() 100 | } 101 | 102 | AnytimePicker.prototype.update = function (update) { 103 | if (update === null || update === undefined) { 104 | this.value = null 105 | this.updateDisplay() 106 | this.emit('change', null) 107 | return 108 | } 109 | 110 | if (typeof update !== 'function') { 111 | var newVal = update 112 | update = function () { return this.createMoment(newVal) }.bind(this) 113 | } 114 | 115 | var updated = update(this.value || this.createMoment()) 116 | this.value = updated 117 | 118 | if (!this.options.showTime) { 119 | this.value = this.value.hour(0).minute(0) 120 | } 121 | 122 | this.currentView = { month: this.value.month(), year: this.value.year() } 123 | this.updateDisplay() 124 | this.emit('change', this.value.toDate()) 125 | } 126 | 127 | AnytimePicker.prototype.render = function () { 128 | // Header 129 | var header = document.createElement('div') 130 | header.classList.add('anytime-picker__header') 131 | this.renderHeader(header) 132 | 133 | // Dates 134 | var dates = document.createElement('div') 135 | dates.classList.add('anytime-picker__dates') 136 | dates.classList.add('js-anytime-picker-dates') 137 | 138 | // Time 139 | var time 140 | if (this.options.showTime) { 141 | time = document.createElement('div') 142 | time.classList.add('anytime-picker__time') 143 | time.classList.add('js-anytime-picker-time') 144 | this.renderTimeInput(time) 145 | } 146 | 147 | // Footer 148 | var footer = document.createElement('div') 149 | footer.classList.add('anytime-picker__footer') 150 | this.renderFooter(footer) 151 | 152 | this.el.appendChild(header) 153 | this.el.appendChild(dates) 154 | if (this.options.showTime) this.el.appendChild(time) 155 | this.el.appendChild(footer) 156 | 157 | this.dateContainer = dates 158 | 159 | this.updateDisplay() 160 | 161 | return this 162 | } 163 | 164 | AnytimePicker.prototype.renderHeader = function (headerEl) { 165 | // Previous month button 166 | var prevBtn = createButton('<', [ 'anytime-picker__button', 'anytime-picker__button--prev' ]) 167 | headerEl.appendChild(prevBtn) 168 | prevBtn.addEventListener('click', this.showPrevMonth.bind(this)) 169 | 170 | // Months 171 | var monthSelect = document.createElement('select') 172 | monthSelect.classList.add('js-anytime-picker-month') 173 | monthSelect.classList.add('anytime-picker__dropdown') 174 | this.monthNames.forEach(function (month, i) { 175 | var monthOption = document.createElement('option') 176 | monthOption.textContent = month 177 | if (i === this.currentView.month) monthOption.selected = true 178 | monthSelect.appendChild(monthOption) 179 | }.bind(this)) 180 | headerEl.appendChild(monthSelect) 181 | this.monthSelect = monthSelect 182 | 183 | monthSelect.addEventListener('change', function (e) { 184 | this.currentView.month = this.monthNames.indexOf(e.target.value) 185 | this.updateDisplay() 186 | }.bind(this)) 187 | 188 | // Years 189 | var yearSelect = document.createElement('select') 190 | yearSelect.classList.add('js-anytime-picker-year') 191 | yearSelect.classList.add('anytime-picker__dropdown') 192 | getYearList(this.options.minYear, this.options.maxYear).forEach(function (year) { 193 | var yearOption = document.createElement('option') 194 | yearOption.textContent = year 195 | if (year === this.currentView.year) yearOption.selected = true 196 | yearSelect.appendChild(yearOption) 197 | }.bind(this)) 198 | headerEl.appendChild(yearSelect) 199 | this.yearSelect = yearSelect 200 | 201 | yearSelect.addEventListener('change', function (e) { 202 | this.currentView.year = e.target.value 203 | this.updateDisplay() 204 | }.bind(this)) 205 | 206 | // Next month button 207 | var nextBtn = createButton('>', [ 'anytime-picker__button', 'anytime-picker__button--next' ]) 208 | headerEl.appendChild(nextBtn) 209 | nextBtn.addEventListener('click', this.showNextMonth.bind(this)) 210 | } 211 | 212 | AnytimePicker.prototype.renderFooter = function (footerEl) { 213 | // "Done" button 214 | var doneBtn = createButton(this.options.doneText, [ 'anytime-picker__button', 'anytime-picker__button--done' ]) 215 | footerEl.appendChild(doneBtn) 216 | doneBtn.addEventListener('click', this.hide.bind(this)) 217 | 218 | // "Clear" button 219 | var clearBtn = createButton(this.options.clearText, [ 'anytime-picker__button', 'anytime-picker__button--clear' ]) 220 | footerEl.appendChild(clearBtn) 221 | clearBtn.addEventListener('click', function () { 222 | this.update(null) 223 | this.hide() 224 | }.bind(this)) 225 | } 226 | 227 | AnytimePicker.prototype.updateDisplay = function () { 228 | this.monthSelect.children[this.currentView.month].selected = true 229 | Array.prototype.slice.call(this.yearSelect.children).some(function (yearEl) { 230 | if (yearEl.textContent !== '' + this.currentView.year) return false 231 | yearEl.selected = true 232 | return true 233 | }.bind(this)) 234 | 235 | var daysEl = document.createElement('div') 236 | , monthDetails = getMonthDetails(this.currentView.month, this.currentView.year) 237 | 238 | /* 239 | * Create the day column headers 240 | */ 241 | function renderDayNames() { 242 | var names = this.options.moment.weekdaysMin() 243 | // Moment gives Sunday as the first item, but uses Monday as the first day of the week. 244 | // This is due to getMonthDetails returning the value of ISO weekday, which has Monday 245 | // as index 1 and Sunday at index 7. For this reason, Sunday is shifted from the from 246 | // of the array and pushed to the back. 247 | names.push(names.shift()) 248 | names.forEach(function (d) { 249 | var dayName = document.createElement('span') 250 | dayName.textContent = d 251 | dayName.classList.add('anytime-picker__day-name') 252 | daysEl.appendChild(dayName) 253 | }) 254 | } 255 | 256 | /* 257 | * Create the blank days ahead of the first day of the current month so that 258 | * the days appear in the corresponding columns of the days of the week 259 | */ 260 | function padDays() { 261 | for (var x = 1; x < monthDetails.startDay; x++) { 262 | var blank = document.createElement('span') 263 | blank.textContent = '' 264 | daysEl.appendChild(blank) 265 | } 266 | } 267 | 268 | /* 269 | * Create a day element for each day of the current month 270 | */ 271 | function populateDays() { 272 | var now = this.createMoment() 273 | , currentDayOfMonth = parseInt(now.format('D'), 10) 274 | , isCurrentMonth = parseInt(now.month(), 10) === this.currentView.month 275 | , isCurrentYear = parseInt(now.year(), 10) === this.currentView.year 276 | , selectedDayOfMonth = null 277 | , isSelectedCurrentMonth = false 278 | , isSelectedCurrentYear = false 279 | 280 | if (this.value) { 281 | selectedDayOfMonth = parseInt(this.value.format('D'), 10) 282 | isSelectedCurrentMonth = parseInt(this.value.month(), 10) === this.currentView.month 283 | isSelectedCurrentYear = parseInt(this.value.year(), 10) === this.currentView.year 284 | } 285 | 286 | for (var y = 1; y <= monthDetails.length; y++) { 287 | var date = createButton(y, [ 'anytime-picker__date', 'js-anytime-picker-day' ]) 288 | 289 | if (y === currentDayOfMonth && isCurrentMonth && isCurrentYear) { 290 | date.classList.add('anytime-picker__date--current') 291 | } 292 | 293 | // Needs to add or remove because the current selected day can change 294 | // within the current month and need to be cleared from others 295 | var current = y === selectedDayOfMonth && isSelectedCurrentMonth && isSelectedCurrentYear 296 | date.classList.toggle('anytime-picker__date--selected', current) 297 | 298 | date.setAttribute('data-date', y) 299 | date.setAttribute('data-month', this.currentView.month) 300 | date.setAttribute('data-year', this.currentView.year) 301 | daysEl.appendChild(date) 302 | } 303 | } 304 | 305 | renderDayNames.call(this) 306 | padDays.call(this) 307 | populateDays.call(this) 308 | 309 | // Remove all of the old days 310 | Array.prototype.slice.call(this.dateContainer.children).forEach(function (child) { 311 | if (child.parentNode) child.parentNode.removeChild(child) 312 | }) 313 | 314 | // Add all the new days 315 | Array.prototype.slice.call(daysEl.children).forEach(function (child) { 316 | this.dateContainer.appendChild(child) 317 | }.bind(this)) 318 | 319 | if (this.value && this.timeEls) { 320 | this.timeEls.hours.value = this.value.hour() + '' 321 | this.timeEls.minutes.value = this.value.minute() + '' 322 | if (this.timeEls.hourLabel) this.timeEls.hourLabel.textContent = pad(this.value.hour(), 2) 323 | if (this.timeEls.minuteLabel) this.timeEls.minuteLabel.textContent = pad(this.value.minute(), 2) 324 | } 325 | } 326 | 327 | AnytimePicker.prototype.show = function () { 328 | this.root.offsetParent.appendChild(this.el) 329 | 330 | this.el.classList.add('anytime-picker--is-visible') 331 | 332 | this.updatePosition() 333 | 334 | this.__events['doc escape hide'] = function (e) { 335 | // Hide if escape is pressed 336 | if (e.keyCode === 27) this.hide() 337 | }.bind(this) 338 | 339 | this.__events['doc click hide'] = function (e) { 340 | // Hide if document outside of anytime is clicked 341 | if (e.target === this.el) return 342 | if (this.el.contains(e.target)) return 343 | this.hide() 344 | }.bind(this) 345 | 346 | this.__events['other anytime open'] = function (e) { 347 | // Hide if another instance is opened 348 | if (e.detail.instance !== this) this.hide() 349 | }.bind(this) 350 | 351 | this.__events['window resize position'] = throttle(function () { 352 | // Update position when window is resized 353 | this.updatePosition() 354 | }.bind(this), 100) 355 | 356 | process.nextTick(function () { 357 | document.addEventListener('keyup', this.__events['doc escape hide']) 358 | document.addEventListener('click', this.__events['doc click hide']) 359 | document.addEventListener('anytime::open', this.__events['other anytime open']) 360 | window.addEventListener('resize', this.__events['window resize position']) 361 | document.dispatchEvent(new CustomEvent('anytime::open', { detail: { instance: this } })) 362 | }.bind(this)) 363 | } 364 | 365 | AnytimePicker.prototype.hide = function () { 366 | this.el.classList.remove('anytime-picker--is-visible') 367 | 368 | document.removeEventListener('keyup', this.__events['doc escape hide']) 369 | delete this.__events['doc escape hide'] 370 | 371 | document.removeEventListener('click', this.__events['doc click hide']) 372 | delete this.__events['doc click hide'] 373 | 374 | document.removeEventListener('anytime::open', this.__events['other anytime open']) 375 | delete this.__events['keyup other anytime open'] 376 | 377 | window.removeEventListener('resize', this.__events['window resize position']) 378 | delete this.__events['window resize position'] 379 | 380 | if (this.el.parentNode) this.el.parentNode.removeChild(this.el) 381 | } 382 | 383 | AnytimePicker.prototype.updatePosition = function () { 384 | var position = { top: this.root.offsetTop, left: this.root.offsetLeft } 385 | var topOffset = (position.top + this.root.offsetHeight + this.options.offset) 386 | var leftOffset = (position.left + this.root.offsetWidth - this.el.offsetWidth) 387 | 388 | if (leftOffset < 0) { 389 | leftOffset = 0 390 | } 391 | 392 | var transformValue = 'translate(' + leftOffset + 'px, ' + topOffset + 'px)' 393 | 394 | this.el.style.webkitTransform = transformValue 395 | this.el.style.transform = transformValue 396 | this.el.style.top = 0 397 | this.el.style.left = 0 398 | } 399 | 400 | AnytimePicker.prototype.toggle = function () { 401 | if (this.el.classList.contains('anytime-picker--is-visible')) { 402 | this.hide() 403 | } else { 404 | this.show() 405 | } 406 | } 407 | 408 | AnytimePicker.prototype.showPrevMonth = function () { 409 | if (this.currentView.month > 0) { 410 | this.currentView.month-- 411 | this.updateDisplay() 412 | return 413 | } 414 | if (this.currentView.year - 1 > this.options.minYear) { 415 | this.currentView.month = 11 416 | this.currentView.year-- 417 | this.updateDisplay() 418 | } 419 | } 420 | 421 | AnytimePicker.prototype.showNextMonth = function () { 422 | if (this.currentView.month < 11) { 423 | this.currentView.month++ 424 | this.updateDisplay() 425 | return 426 | } 427 | if (this.currentView.year + 1 < this.options.maxYear) { 428 | this.currentView.month = 0 429 | this.currentView.year++ 430 | this.updateDisplay() 431 | } 432 | } 433 | 434 | AnytimePicker.prototype.renderTimeSelect = function (timeEl) { 435 | var hourSelect = document.createElement('select') 436 | hourSelect.classList.add('anytime-picker__dropdown') 437 | hourSelect.classList.add('anytime-picker__dropdown--hours') 438 | for (var i = 0; i < 24; i++) { 439 | var hour = document.createElement('option') 440 | hour.value = i 441 | hour.textContent = pad(i, 2) 442 | if (this.createMoment(this.options.initialValue).hours() === i) hour.selected = true 443 | hourSelect.appendChild(hour) 444 | } 445 | 446 | hourSelect.addEventListener('change', function (e) { 447 | this.update(function (value) { 448 | return value.hours(e.target.value) 449 | }) 450 | }.bind(this)) 451 | 452 | timeEl.appendChild(hourSelect) 453 | 454 | var colonEl = getTimeSeparator() 455 | timeEl.appendChild(colonEl) 456 | 457 | var minuteSelect = document.createElement('select') 458 | minuteSelect.classList.add('anytime-picker__dropdown') 459 | minuteSelect.classList.add('anytime-picker__dropdown--minutes') 460 | for (var j = 0; j < 60; j += this.options.minuteIncrement) { 461 | var minute = document.createElement('option') 462 | minute.value = j 463 | minute.textContent = pad(j, 2) 464 | if (this.createMoment(this.options.initialValue).minutes() === j) minute.selected = true 465 | minuteSelect.appendChild(minute) 466 | } 467 | 468 | minuteSelect.addEventListener('change', function (e) { 469 | this.update(function (value) { 470 | return value.minutes(e.target.value) 471 | }) 472 | }.bind(this)) 473 | 474 | timeEl.appendChild(minuteSelect) 475 | 476 | this.timeEls = { hours: hourSelect, minutes: minuteSelect } 477 | } 478 | 479 | AnytimePicker.prototype.renderTimeSliders = function (timeEl) { 480 | /* jshint maxstatements: 28 */ 481 | var timeLabelEl = document.createElement('p') 482 | timeLabelEl.classList.add('anytime-picker__time-label') 483 | 484 | var timeLabelTitleEl = document.createElement('span') 485 | timeLabelTitleEl.classList.add('anytime-picker__time-label--title') 486 | timeLabelEl.appendChild(timeLabelTitleEl) 487 | timeLabelTitleEl.textContent = this.options.timeSlidersText 488 | 489 | var timeLabelHourEl = document.createElement('span') 490 | timeLabelHourEl.classList.add('anytime-picker__time-label--hour') 491 | timeLabelEl.appendChild(timeLabelHourEl) 492 | timeLabelHourEl.textContent = pad(this.createMoment(this.options.initialValue).hours(), 2) 493 | 494 | var colonEl = getTimeSeparator() 495 | timeLabelEl.appendChild(colonEl) 496 | 497 | var timeLabelMinuteEl = document.createElement('span') 498 | timeLabelMinuteEl.classList.add('anytime-picker__time-label--minute') 499 | timeLabelEl.appendChild(timeLabelMinuteEl) 500 | timeLabelMinuteEl.textContent = pad(this.createMoment(this.options.initialValue).minutes(), 2) 501 | 502 | timeEl.appendChild(timeLabelEl) 503 | 504 | var hourSlider = createSlider( 505 | { className: 'anytime-picker__slider--hours' 506 | , min: 0 507 | , max: 23 508 | , value: this.createMoment(this.options.initialValue).hours() 509 | , title: this.options.timeSlidersHourText 510 | }) 511 | 512 | function updateHour(e) { 513 | this.update(function (value) { 514 | return value.hours(e.target.value) 515 | }) 516 | timeLabelHourEl.textContent = pad(e.target.value, 2) 517 | } 518 | 519 | hourSlider.addEventListener('change', updateHour.bind(this)) 520 | hourSlider.addEventListener('input', updateHour.bind(this)) 521 | 522 | timeEl.appendChild(hourSlider) 523 | 524 | var minuteSlider = createSlider( 525 | { className: 'anytime-picker__slider--minutes' 526 | , min: 0 527 | , max: 59 528 | , value: this.createMoment(this.options.initialValue).minutes() 529 | , title: this.options.timeSlidersMinuteText 530 | }) 531 | 532 | function updateMinute(e) { 533 | this.update(function (value) { 534 | return value.minutes(e.target.value) 535 | }) 536 | timeLabelMinuteEl.textContent = pad(e.target.value, 2) 537 | } 538 | 539 | minuteSlider.addEventListener('change', updateMinute.bind(this)) 540 | minuteSlider.addEventListener('input', updateMinute.bind(this)) 541 | 542 | timeEl.appendChild(minuteSlider) 543 | 544 | this.timeEls = 545 | { hours: hourSlider 546 | , minutes: minuteSlider 547 | , hourLabel: timeLabelHourEl 548 | , minuteLabel: timeLabelMinuteEl 549 | } 550 | } 551 | 552 | AnytimePicker.prototype.renderTimeInput = function (timeEl) { 553 | if (this.options.showTime) { 554 | if (this.options.timeSliders) { 555 | this.renderTimeSliders(timeEl) 556 | } else { 557 | this.renderTimeSelect(timeEl) 558 | } 559 | } 560 | } 561 | 562 | AnytimePicker.prototype.destroy = function () { 563 | if (this.el) { 564 | this.hide() 565 | this.emit('destroy') 566 | this.removeAllListeners() 567 | if (this.options.button) this.options.button.removeEventListener('click', this.__events['misc toggle']) 568 | this.options.input.removeEventListener('click', this.__events['misc toggle']) 569 | delete this.__events['misc toggle'] 570 | this.el = null 571 | } 572 | } 573 | -------------------------------------------------------------------------------- /dist/anytime.js: -------------------------------------------------------------------------------- 1 | /** 2 | * anytime - A date/time picker 3 | * @version 1.4.2 4 | * @link http://bengourley.github.io/anytime/ 5 | * @license ISC 6 | */ 7 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e(require("moment")):"function"==typeof define&&define.amd?define(["moment"],e):"object"==typeof exports?exports.anytime=e(require("moment")):t.anytime=e(t.moment)}(this,function(t){return function(t){function e(i){if(n[i])return n[i].exports;var r=n[i]={exports:{},id:i,loaded:!1};return t[i].call(r.exports,r,r.exports,e),r.loaded=!0,r.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){(function(e){function i(t){this.options=s({},f,t),r.call(this),this.__events={},this.el=document.createElement("div"),this.el.className="js-anytime-picker anytime-picker",this.options.initialValue=this.getInitialValue();var e=this.createMoment(this.options.initialValue||this.options.initialView);this.currentView={month:e.month(),year:e.year()},this.value=this.options.initialValue?this.createMoment(this.options.initialValue).seconds(0).milliseconds(0):null,this.value&&!this.options.showTime&&(this.value=this.value.hour(0).minute(0)),this.monthNames=this.getMonthNames(),this.el.addEventListener("click",function(t){t.target.classList.contains("js-anytime-picker-day")&&(t.stopPropagation(),this.update(function(e){return e.date(parseInt(t.target.getAttribute("data-date"),10)).month(parseInt(t.target.getAttribute("data-month"),10)).year(parseInt(t.target.getAttribute("data-year"),10))}))}.bind(this)),this.el.addEventListener("click",function(t){t.preventDefault()}),this.__events["misc toggle"]=this.toggle.bind(this),this.options.button&&this.options.button.addEventListener("click",this.__events["misc toggle"]),this.options.input.addEventListener("click",this.__events["misc toggle"]),this.root=this.options.anchor?this.options.anchor:this.options.input,this.options.input&&(this.updateInput(this),this.on("change",this.updateInput.bind(this)))}t.exports=i;var r=n(8).EventEmitter,s=n(2),o=n(5),a=n(7),u=n(1),c=n(15),h=n(14),l=n(10),p=n(12),d=n(13),m=n(11),f={minYear:1960,maxYear:2030,offset:5,initialValue:null,initialView:new Date,format:"h:mma on dddd D MMMM YYYY",moment:u,minuteIncrement:1,showTime:!0,timeSliders:!1,shortMonthNames:!0,doneText:"Done",clearText:"Clear",timeSlidersText:"Time:",timeSlidersHourText:"Hour:",timeSlidersMinuteText:"Minute:"};i.prototype=Object.create(r.prototype),i.prototype.createMoment=m,i.prototype.updateInput=function(){this.options.input.value=this.value?this.value.format(this.options.format):""},i.prototype.getInitialValue=function(){return this.options.initialValue?this.options.initialValue:this.options.input&&this.options.input.value?this.options.input.value:null},i.prototype.getMonthNames=function(){return this.options.moment[this.options.shortMonthNames?"monthsShort":"months"]()},i.prototype.update=function(t){if(null===t||void 0===t)return this.value=null,this.updateDisplay(),void this.emit("change",null);if("function"!=typeof t){var e=t;t=function(){return this.createMoment(e)}.bind(this)}var n=t(this.value||this.createMoment());this.value=n,this.options.showTime||(this.value=this.value.hour(0).minute(0)),this.currentView={month:this.value.month(),year:this.value.year()},this.updateDisplay(),this.emit("change",this.value.toDate())},i.prototype.render=function(){var t=document.createElement("div");t.classList.add("anytime-picker__header"),this.renderHeader(t);var e=document.createElement("div");e.classList.add("anytime-picker__dates"),e.classList.add("js-anytime-picker-dates");var n;this.options.showTime&&(n=document.createElement("div"),n.classList.add("anytime-picker__time"),n.classList.add("js-anytime-picker-time"),this.renderTimeInput(n));var i=document.createElement("div");return i.classList.add("anytime-picker__footer"),this.renderFooter(i),this.el.appendChild(t),this.el.appendChild(e),this.options.showTime&&this.el.appendChild(n),this.el.appendChild(i),this.dateContainer=e,this.updateDisplay(),this},i.prototype.renderHeader=function(t){var e=l("<",["anytime-picker__button","anytime-picker__button--prev"]);t.appendChild(e),e.addEventListener("click",this.showPrevMonth.bind(this));var n=document.createElement("select");n.classList.add("js-anytime-picker-month"),n.classList.add("anytime-picker__dropdown"),this.monthNames.forEach(function(t,e){var i=document.createElement("option");i.textContent=t,e===this.currentView.month&&(i.selected=!0),n.appendChild(i)}.bind(this)),t.appendChild(n),this.monthSelect=n,n.addEventListener("change",function(t){this.currentView.month=this.monthNames.indexOf(t.target.value),this.updateDisplay()}.bind(this));var i=document.createElement("select");i.classList.add("js-anytime-picker-year"),i.classList.add("anytime-picker__dropdown"),c(this.options.minYear,this.options.maxYear).forEach(function(t){var e=document.createElement("option");e.textContent=t,t===this.currentView.year&&(e.selected=!0),i.appendChild(e)}.bind(this)),t.appendChild(i),this.yearSelect=i,i.addEventListener("change",function(t){this.currentView.year=t.target.value,this.updateDisplay()}.bind(this));var r=l(">",["anytime-picker__button","anytime-picker__button--next"]);t.appendChild(r),r.addEventListener("click",this.showNextMonth.bind(this))},i.prototype.renderFooter=function(t){var e=l(this.options.doneText,["anytime-picker__button","anytime-picker__button--done"]);t.appendChild(e),e.addEventListener("click",this.hide.bind(this));var n=l(this.options.clearText,["anytime-picker__button","anytime-picker__button--clear"]);t.appendChild(n),n.addEventListener("click",function(){this.update(null),this.hide()}.bind(this))},i.prototype.updateDisplay=function(){function t(){var t=this.options.moment.weekdaysMin();t.push(t.shift()),t.forEach(function(t){var e=document.createElement("span");e.textContent=t,e.classList.add("anytime-picker__day-name"),i.appendChild(e)})}function e(){for(var t=1;tn&&(n=0);var i="translate("+n+"px, "+e+"px)";this.el.style.webkitTransform=i,this.el.style.transform=i,this.el.style.top=0,this.el.style.left=0},i.prototype.toggle=function(){this.el.classList.contains("anytime-picker--is-visible")?this.hide():this.show()},i.prototype.showPrevMonth=function(){return this.currentView.month>0?(this.currentView.month--,void this.updateDisplay()):void(this.currentView.year-1>this.options.minYear&&(this.currentView.month=11,this.currentView.year--,this.updateDisplay()))},i.prototype.showNextMonth=function(){return this.currentView.month<11?(this.currentView.month++,void this.updateDisplay()):void(this.currentView.year+1n;n++){var i=document.createElement("option");i.value=n,i.textContent=a(n,2),this.createMoment(this.options.initialValue).hours()===n&&(i.selected=!0),e.appendChild(i)}e.addEventListener("change",function(t){this.update(function(e){return e.hours(t.target.value)})}.bind(this)),t.appendChild(e);var r=h();t.appendChild(r);var s=document.createElement("select");s.classList.add("anytime-picker__dropdown"),s.classList.add("anytime-picker__dropdown--minutes");for(var o=0;60>o;o+=this.options.minuteIncrement){var u=document.createElement("option");u.value=o,u.textContent=a(o,2),this.createMoment(this.options.initialValue).minutes()===o&&(u.selected=!0),s.appendChild(u)}s.addEventListener("change",function(t){this.update(function(e){return e.minutes(t.target.value)})}.bind(this)),t.appendChild(s),this.timeEls={hours:e,minutes:s}},i.prototype.renderTimeSliders=function(t){function e(t){this.update(function(e){return e.hours(t.target.value)}),s.textContent=a(t.target.value,2)}function n(t){this.update(function(e){return e.minutes(t.target.value)}),u.textContent=a(t.target.value,2)}var i=document.createElement("p");i.classList.add("anytime-picker__time-label");var r=document.createElement("span");r.classList.add("anytime-picker__time-label--title"),i.appendChild(r),r.textContent=this.options.timeSlidersText;var s=document.createElement("span");s.classList.add("anytime-picker__time-label--hour"),i.appendChild(s),s.textContent=a(this.createMoment(this.options.initialValue).hours(),2);var o=h();i.appendChild(o);var u=document.createElement("span");u.classList.add("anytime-picker__time-label--minute"),i.appendChild(u),u.textContent=a(this.createMoment(this.options.initialValue).minutes(),2),t.appendChild(i);var c=p({className:"anytime-picker__slider--hours",min:0,max:23,value:this.createMoment(this.options.initialValue).hours(),title:this.options.timeSlidersHourText});c.addEventListener("change",e.bind(this)),c.addEventListener("input",e.bind(this)),t.appendChild(c);var l=p({className:"anytime-picker__slider--minutes",min:0,max:59,value:this.createMoment(this.options.initialValue).minutes(),title:this.options.timeSlidersMinuteText});l.addEventListener("change",n.bind(this)),l.addEventListener("input",n.bind(this)),t.appendChild(l),this.timeEls={hours:c,minutes:l,hourLabel:s,minuteLabel:u}},i.prototype.renderTimeInput=function(t){this.options.showTime&&(this.options.timeSliders?this.renderTimeSliders(t):this.renderTimeSelect(t))},i.prototype.destroy=function(){this.el&&(this.hide(),this.emit("destroy"),this.removeAllListeners(),this.options.button&&this.options.button.removeEventListener("click",this.__events["misc toggle"]),this.options.input.removeEventListener("click",this.__events["misc toggle"]),delete this.__events["misc toggle"],this.el=null)}}).call(e,n(9))},function(e,n){e.exports=t},function(t,e,n){function i(t,e){return t="number"==typeof t||b.test(t)?+t:-1,e=null==e?y:e,t>-1&&t%1==0&&e>t}function r(t,e,n){var i=t[e];L.call(t,e)&&h(i,n)&&(void 0!==n||e in t)||(t[e]=n)}function s(t){return function(e){return null==e?void 0:e[t]}}function o(t,e,n){return a(t,e,n)}function a(t,e,n,i){n||(n={});for(var s=-1,o=e.length;++s1?n[r-1]:void 0,o=r>2?n[2]:void 0;for(s="function"==typeof s?(r--,s):void 0,o&&c(n[0],n[1],o)&&(s=3>r?void 0:s,r=1),e=Object(e);++i-1&&t%1==0&&y>=t}function m(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}var f=n(3),v=n(4),y=9007199254740991,_="[object Function]",g="[object GeneratorFunction]",b=/^(?:0|[1-9]\d*)$/,w=Object.prototype,L=w.hasOwnProperty,E=w.toString,x=s("length"),k=u(function(t,e){o(e,f(e),t)});t.exports=k},function(t,e){function n(t,e){for(var n=-1,i=Array(t);++n-1&&t%1==0&&e>t}function r(t,e){return k.call(t,e)||"object"==typeof t&&e in t&&null===T(t)}function s(t){return j(Object(t))}function o(t){return function(e){return null==e?void 0:e[t]}}function a(t){var e=t?t.length:void 0;return d(e)&&(A(t)||v(t)||c(t))?n(e,String):null}function u(t){var e=t&&t.constructor,n=p(e)&&e.prototype||x;return t===n}function c(t){return l(t)&&k.call(t,"callee")&&(!V.call(t,"callee")||C.call(t)==g)}function h(t){return null!=t&&!("function"==typeof t&&p(t))&&d(M(t))}function l(t){return f(t)&&h(t)}function p(t){var e=m(t)?C.call(t):"";return e==b||e==w}function d(t){return"number"==typeof t&&t>-1&&t%1==0&&_>=t}function m(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}function f(t){return!!t&&"object"==typeof t}function v(t){return"string"==typeof t||!A(t)&&f(t)&&C.call(t)==L}function y(t){var e=u(t);if(!e&&!h(t))return s(t);var n=a(t),o=!!n,c=n||[],l=c.length;for(var p in t)!r(t,p)||o&&("length"==p||i(p,l))||e&&"constructor"==p||c.push(p);return c}var _=9007199254740991,g="[object Arguments]",b="[object Function]",w="[object GeneratorFunction]",L="[object String]",E=/^(?:0|[1-9]\d*)$/,x=Object.prototype,k=x.hasOwnProperty,C=x.toString,T=Object.getPrototypeOf,V=x.propertyIsEnumerable,j=Object.keys,M=o("length"),A=Array.isArray;t.exports=y},function(t,e){function n(t,e,n){var i=n.length;switch(i){case 0:return t.call(e);case 1:return t.call(e,n[0]);case 2:return t.call(e,n[0],n[1]);case 3:return t.call(e,n[0],n[1],n[2])}return t.apply(e,n)}function i(t,e){if("function"!=typeof t)throw new TypeError(u);return e=w(void 0===e?t.length-1:o(e),0),function(){for(var i=arguments,r=-1,s=w(i.length-e,0),o=Array(s);++rt?-1:1;return e*h}var n=t%1;return t===t?n?t-n:t:0}function a(t){if(s(t)){var e=r(t.valueOf)?t.valueOf():t;t=s(e)?e+"":e}if("string"!=typeof t)return 0===t?t:+t;t=t.replace(m,"");var n=v.test(t);return n||y.test(t)?_(t.slice(2),n?2:8):f.test(t)?l:+t}var u="Expected a function",c=1/0,h=1.7976931348623157e308,l=NaN,p="[object Function]",d="[object GeneratorFunction]",m=/^\s+|\s+$/g,f=/^[-+]0x[0-9a-f]+$/i,v=/^0b[01]+$/i,y=/^0o[0-7]+$/i,_=parseInt,g=Object.prototype,b=g.toString,w=Math.max;t.exports=i},function(t,e,n){function i(t,e,n){var i=!0,a=!0;if("function"!=typeof t)throw new TypeError(o);return r(n)&&(i="leading"in n?!!n.leading:i,a="trailing"in n?!!n.trailing:a),s(t,e,{leading:i,maxWait:e,trailing:a})}function r(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}var s=n(6),o="Expected a function";t.exports=i},function(t,e){function n(t,e,n){function i(){g&&clearTimeout(g),d&&clearTimeout(d),w=0,p=d=v=g=b=void 0}function a(e,n){n&&clearTimeout(n),d=g=b=void 0,e&&(w=_(),m=t.apply(v,p),g||d||(p=v=void 0))}function u(){var t=e-(_()-f);0>=t||t>e?a(b,d):g=setTimeout(u,t)}function c(){return(g&&b||d&&x)&&(m=t.apply(v,p)),i(),m}function h(){a(x,g)}function l(){if(p=arguments,f=_(),v=this,b=x&&(g||!L),E===!1)var n=L&&!g;else{w||d||L||(w=f);var i=E-(f-w),r=(0>=i||i>E)&&(L||d);r?(d&&(d=clearTimeout(d)),w=f,m=t.apply(v,p)):d||(d=setTimeout(h,i))}return r&&g?g=clearTimeout(g):g||e===E||(g=setTimeout(u,e)),n&&(r=!0,m=t.apply(v,p)),!r||g||d||(p=v=void 0),m}var p,d,m,f,v,g,b,w=0,L=!1,E=!1,x=!0;if("function"!=typeof t)throw new TypeError(o);return e=s(e)||0,r(n)&&(L=!!n.leading,E="maxWait"in n&&y(s(n.maxWait)||0,e),x="trailing"in n?!!n.trailing:x),l.cancel=i,l.flush=c,l}function i(t){var e=r(t)?v.call(t):"";return e==u||e==c}function r(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}function s(t){if(r(t)){var e=i(t.valueOf)?t.valueOf():t;t=r(e)?e+"":e}if("string"!=typeof t)return 0===t?t:+t;t=t.replace(h,"");var n=p.test(t);return n||d.test(t)?m(t.slice(2),n?2:8):l.test(t)?a:+t}var o="Expected a function",a=NaN,u="[object Function]",c="[object GeneratorFunction]",h=/^\s+|\s+$/g,l=/^[-+]0x[0-9a-f]+$/i,p=/^0b[01]+$/i,d=/^0o[0-7]+$/i,m=parseInt,f=Object.prototype,v=f.toString,y=Math.max,_=Date.now;t.exports=n},function(t,e){"use strict";function n(t,e,n){var i=t.toString();if(!e||i.length>=e)return i;var r=new Array(e-i.length+1).join(n||"0");return r+i}t.exports=n},function(t,e){function n(){this._events=this._events||{},this._maxListeners=this._maxListeners||void 0}function i(t){return"function"==typeof t}function r(t){return"number"==typeof t}function s(t){return"object"==typeof t&&null!==t}function o(t){return void 0===t}t.exports=n,n.EventEmitter=n,n.prototype._events=void 0,n.prototype._maxListeners=void 0,n.defaultMaxListeners=10,n.prototype.setMaxListeners=function(t){if(!r(t)||0>t||isNaN(t))throw TypeError("n must be a positive number");return this._maxListeners=t,this},n.prototype.emit=function(t){var e,n,r,a,u,c;if(this._events||(this._events={}),"error"===t&&(!this._events.error||s(this._events.error)&&!this._events.error.length)){if(e=arguments[1],e instanceof Error)throw e;throw TypeError('Uncaught, unspecified "error" event.')}if(n=this._events[t],o(n))return!1;if(i(n))switch(arguments.length){case 1:n.call(this);break;case 2:n.call(this,arguments[1]);break;case 3:n.call(this,arguments[1],arguments[2]);break;default:a=Array.prototype.slice.call(arguments,1),n.apply(this,a)}else if(s(n))for(a=Array.prototype.slice.call(arguments,1),c=n.slice(),r=c.length,u=0;r>u;u++)c[u].apply(this,a);return!0},n.prototype.addListener=function(t,e){var r;if(!i(e))throw TypeError("listener must be a function");return this._events||(this._events={}),this._events.newListener&&this.emit("newListener",t,i(e.listener)?e.listener:e),this._events[t]?s(this._events[t])?this._events[t].push(e):this._events[t]=[this._events[t],e]:this._events[t]=e,s(this._events[t])&&!this._events[t].warned&&(r=o(this._maxListeners)?n.defaultMaxListeners:this._maxListeners,r&&r>0&&this._events[t].length>r&&(this._events[t].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[t].length),"function"==typeof console.trace&&console.trace())),this},n.prototype.on=n.prototype.addListener,n.prototype.once=function(t,e){function n(){this.removeListener(t,n),r||(r=!0,e.apply(this,arguments))}if(!i(e))throw TypeError("listener must be a function");var r=!1;return n.listener=e,this.on(t,n),this},n.prototype.removeListener=function(t,e){var n,r,o,a;if(!i(e))throw TypeError("listener must be a function");if(!this._events||!this._events[t])return this;if(n=this._events[t],o=n.length,r=-1,n===e||i(n.listener)&&n.listener===e)delete this._events[t],this._events.removeListener&&this.emit("removeListener",t,e);else if(s(n)){for(a=o;a-- >0;)if(n[a]===e||n[a].listener&&n[a].listener===e){r=a;break}if(0>r)return this;1===n.length?(n.length=0,delete this._events[t]):n.splice(r,1),this._events.removeListener&&this.emit("removeListener",t,e)}return this},n.prototype.removeAllListeners=function(t){var e,n;if(!this._events)return this;if(!this._events.removeListener)return 0===arguments.length?this._events={}:this._events[t]&&delete this._events[t],this;if(0===arguments.length){for(e in this._events)"removeListener"!==e&&this.removeAllListeners(e);return this.removeAllListeners("removeListener"),this._events={},this}if(n=this._events[t],i(n))this.removeListener(t,n);else if(n)for(;n.length;)this.removeListener(t,n[n.length-1]);return delete this._events[t],this},n.prototype.listeners=function(t){var e;return e=this._events&&this._events[t]?i(this._events[t])?[this._events[t]]:this._events[t].slice():[]},n.prototype.listenerCount=function(t){if(this._events){var e=this._events[t];if(i(e))return 1;if(e)return e.length}return 0},n.listenerCount=function(t,e){return t.listenerCount(e)}},function(t,e){function n(){c=!1,o.length?u=o.concat(u):h=-1,u.length&&i()}function i(){if(!c){var t=setTimeout(n);c=!0;for(var e=u.length;e;){for(o=u,u=[];++h1)for(var n=1;ne)throw new Error("min year must be before max year");for(var n=[],i=t;e>=i;i++)n.push(i);return n}t.exports=n}])}); --------------------------------------------------------------------------------