Repository: indrimuska/angular-moment-picker Branch: master Commit: eb4e0f3adb85 Files: 42 Total size: 152.7 KB Directory structure: gitextract_3k2fmo8k/ ├── .gitignore ├── .travis.yml ├── .vscode/ │ ├── settings.json │ └── tasks.json ├── LICENSE ├── README.md ├── bower.json ├── dist/ │ ├── angular-moment-picker.css │ ├── angular-moment-picker.js │ └── themes/ │ ├── README.md │ └── material-ui.css ├── karma.conf.js ├── package.json ├── src/ │ ├── definitions.d.ts │ ├── directive.ts │ ├── helpers.ts │ ├── index.less │ ├── index.ts │ ├── provider.ts │ ├── template.tpl.html │ ├── themes/ │ │ └── material-ui.less │ ├── utility.ts │ └── views/ │ ├── dayView.ts │ ├── decadeView.ts │ ├── hourView.ts │ ├── index.ts │ ├── minuteView.ts │ ├── monthView.ts │ └── yearView.ts ├── tests/ │ ├── elementCreation.ts │ ├── hacks.ts │ ├── openClosePicker.ts │ ├── properties/ │ │ ├── isOpen.ts │ │ ├── keyboard.ts │ │ ├── locale.ts │ │ ├── startDate.ts │ │ └── validate.ts │ ├── utility.ts │ └── value.ts ├── tsconfig.json ├── tslint.json └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .DS_Store bower_components node_modules ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - 'node' install: - npm install script: - npm test ================================================ FILE: .vscode/settings.json ================================================ { "typescript.tsdk": "node_modules/typescript/lib" } ================================================ FILE: .vscode/tasks.json ================================================ { // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "0.1.0", "command": "npm", "isShellCommand": true, "args": ["run", "build"] } ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 Indri Muska Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Angular Moment Picker [![Join the chat at https://gitter.im/indrimuska/angular-moment-picker](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/indrimuska/angular-moment-picker?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/indrimuska/angular-moment-picker.svg)](https://travis-ci.org/indrimuska/angular-moment-picker) [![NPM version](http://img.shields.io/npm/v/angular-moment-picker.svg?style=flat)](https://npmjs.org/package/angular-moment-picker) [![NPM downloads](http://img.shields.io/npm/dm/angular-moment-picker.svg?style=flat)](https://npmjs.org/package/angular-moment-picker) [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) [![CDNJS](https://img.shields.io/cdnjs/v/angular-moment-picker.svg)](https://cdnjs.com/libraries/angular-moment-picker) Check out the homepage at [http://indrimuska.github.io/angular-moment-picker/](http://indrimuska.github.io/angular-moment-picker/). *Angular Moment Picker* is a native AngularJS directive for date and time picker that uses *Moment.js* and **does not require jQuery**.

Angular Moment Picker demo

## Installation Get Angular Moment Picker from [**npm**](https://www.npmjs.com/), [**bower**](http://bower.io/) or [**git**](https://git-scm.com/): ``` npm install angular-moment-picker bower install moment-picker git clone https://github.com/indrimuska/angular-moment-picker.git ``` Include style and script in your page: ```html ``` Add *moment-picker* dependency to your module: ```js var myApp = angular.module('myApp', ['moment-picker']); ``` Provide the attribute to your element: ```html
{{ myDate }}
``` ## Demo Check out the demo page at [http://indrimuska.github.io/angular-moment-picker/](http://indrimuska.github.io/angular-moment-picker/). ## The views ***Decade view*** | ***Year view*** | ***Month view*** :---:|:---:|:---: ![Decade view](http://indrimuska.github.io/angular-moment-picker/img/themes/default/decade-view.png?) | ![Year view](http://indrimuska.github.io/angular-moment-picker/img/themes/default/year-view.png?) | ![Month view](http://indrimuska.github.io/angular-moment-picker/img/themes/default/month-view.png?) ***Day view*** | ***Hour view*** | ***Minute view*** ![Day view](http://indrimuska.github.io/angular-moment-picker/img/themes/default/day-view.png?) | ![Hour view](http://indrimuska.github.io/angular-moment-picker/img/themes/default/hour-view.png?) | ![Minute view](http://indrimuska.github.io/angular-moment-picker/img/themes/default/minute-view.png?) ### Additional themes Angular Moment Picker provides the following additional themes. Each theme has a dedicate stylesheet to be included in the application the overrides the default style. - **Material UI** - [Plunker](https://embed.plnkr.co/P48UnN) ```html ``` A preview of the each theme is available [here](dist/themes/). ## Options To configure Angular Moment Picker you have to add to your element or your input the attribute relative to the options you want to set. ```html
Mon anniversaire est le {{ ctrl.birthday }}
``` ```html ``` Property | Default | Description | Sample ---|---|---|--- moment-picker | | Two-way bindable property as **formatted datetime string**. | [Plunker](https://embed.plnkr.co/nPGbO3KkmmPqf7mfN2PC/) ng-model | | Two-way bindable property as **Moment.js object**. | [Plunker](https://embed.plnkr.co/hs10SM) locale | `"en"` | Locale code. 1 | [Plunker](https://embed.plnkr.co/z3KSxy) format | `"L LTS"` | Format of the output value and min/max date. 1 | [Plunker](https://embed.plnkr.co/rWtdhO) min-view | `"decade"` | Minimum navigable view. | [Plunker](https://embed.plnkr.co/wAGqtl) max-view | `"minute"` | Maximum navigable view. | [Plunker](https://embed.plnkr.co/GYRv7J) start-view | `"year"` | Initial view when the picker is open. | [Plunker](https://embed.plnkr.co/wFXcGL) min-date | | Two-way bindable property representing the minimum selectable date (as String in the same format of the value, or as a Moment.js object). | [Plunker](https://embed.plnkr.co/L9dOc4) max-date | | Two-way bindable property representing the maximum selectable date (as String in the same format of the value, or as a Moment.js object). | [Plunker](https://embed.plnkr.co/OvvfAQ) start-date | | Two-way bindable property representing the initial date to be shown in picker (as String in the same format of the value, or as a Moment.js object). | [Plunker](https://embed.plnkr.co/rjFk9d) disable | `false` | Disables the picker if truly. | [Plunker](https://embed.plnkr.co/Zeaxd3) position | | Sets a fixed position for the picker. Available values are `"top left"`, `"top right"`, `"bottom left"`, `"bottom right"`. | [Plunker](https://embed.plnkr.co/v9AZFu) inline | `false` | Views the picker inline. | [Plunker](https://embed.plnkr.co/5PhKOc) validate | `true` | Forces picker value between the range `minDate` and `maxDate`. | [Plunker](https://embed.plnkr.co/hFTyMV) autoclose | `true` | Closes the picker after selecting a date. | [Plunker](https://embed.plnkr.co/z7M6WK) set-on-select | `false` | Updates picker model after selecting a date in each view. | [Plunker](https://embed.plnkr.co/hJRNcT) is-open | | Open/closes the picker when set to `true` or `false`. | [Plunker](https://embed.plnkr.co/7T4sbs) today | `false` | Highlights the current day. | [Plunker](https://embed.plnkr.co/YYbV4C) keyboard | `false` | Allows using the keyboard to navigate the picker. | [Plunker](https://embed.plnkr.co/OdUhHx) show-header | `true` | Shows the header in the view. | [Plunker](https://embed.plnkr.co/PCL4mh) additions | `{ top: undefined, bottom: undefined }` | Template url for custom contents above and below each picker views (inside the dialog). | [Plunker](https://embed.plnkr.co/CXOH5U) ## Methods Append your method to your element and define its behavior in the controller. ```html
Next exhibition is on {{ ctrl.exhibition }}.
``` ```javascript ctrl.isSelectable = function (date, type) { // disable all Sundays in the Month View return type != 'day' || date.format('dddd') != 'Sunday'; }; ``` Method | Parameters | Description | Sample ---|---|---|--- selectable | `date`, `type` | Return `true` if the given date can be selected in the current view. **Please note** that this method is called for every date in the view, every time a view is rendered, so be careful, it may affect performances. | [Plunker](https://embed.plnkr.co/6wxtMn) ## Events As for methods, to bind an event you only need to attach the right property to your picker. ```html
The meeting starts at {{ ctrl.meeting }}.
``` ```javascript ctrl.onChange = function (newValue, oldValue) { $log.log('Meeting changed from ' + oldValue + ' to ' + newValue); }; ``` Event | Parameters | Description | Sample ---|---|---|--- change | `newValue`, `oldValue` | Function fired upon change in picker value. | [Plunker](https://embed.plnkr.co/IIhjjv) ## momentPickerProvider Angular Moment Picker comes out with its own provider, in order to define your own configuration for all the pickers in your app. ```javascript angular .module('myApp', ['moment-picker']) .config(['momentPickerProvider', function (momentPickerProvider) { momentPickerProvider.options({ /* ... */ }); }]); ``` Property | Default | Description ---|---|--- locale | `"en"` | Locale code. 1 format | `"L LTS"` | Format of the output value and min/max date. 1 min-view | `"decade"` | Minimum navigable view. max-view | `"minute"` | Maximum navigable view. start-view | `"year"` | Initial view after picker opening. position | | Sets a fixed position for the picker. Available values are `"top left"`, `"top right"`, `"bottom left"`, `"bottom right"`. inline | `false` | Views the picker inline. validate | `true` | Forces picker value between the range `minDate` and `maxDate`. autoclose | `true` | Closes the picker after selecting a date. set-on-select | `false` | Updates picker model after selecting a date in each view. today | `false` | Highlights the current day. keyboard | `false` | Allows using the keyboard to navigate the picker. show-header | `true` | Shows the header in the view. left-arrow | `"←"` | Left arrow string (HTML allowed). right-arrow | `"→"` | Right arrow string (HTML allowed). additions | `{ top: undefined, bottom: undefined }` | Template url for custom contents above and below each picker views (inside the dialog). years-format | `"YYYY"` | Years format in `decade` view. months-format | `"MMM"` | Months format in `year` view. days-format | `"D"` | Days format in `month` view. hours-format | `"HH:[00]"` | Hours format in `day` view. hours-start | `0` | First rendered hour in `day` view (24h format). hours-end | `23` | Last rendered hour in `day` view (24h format). minutes-format | 2 | Minutes format in `hour` view. minutes-step | `5` | Step between each visible minute in `hour` view. minutes-start | `0` | First rendered minute in `hour` view. minutes-end | `59` | Last rendered minute in `hour` view. seconds-format | `"ss"` | Seconds format in `minute` view. seconds-step | `1` | Step between each visible second in `minute` view. seconds-start | `0` | First rendered second in `minute` view. seconds-end | `59` | Last rendered second in `minute` view. ## Notes 1. Locale codes and format tokens are available at http://momentjs.com/. 2. Locale format `LT` without meridiem part (AM/PM, am/pm). ## Builder Try the online [Angular Moment Picker Builder](http://indrimuska.github.io/angular-moment-picker/#builder): [http://indrimuska.github.io/angular-moment-picker/#builder](http://indrimuska.github.io/angular-moment-picker/#builder). ## Dev scripts - `npm run build`: compile sources and generate built files in `dist` folder. - `npm run minify`: generate built files and minified ones. - `npm run release`: increase package version and compile the project. - `npm run test`: run all tests in the `tests` folder. ## License Copyright (c) 2015 Indri Muska. Licensed under the MIT license. ================================================ FILE: bower.json ================================================ { "name": "angular-moment-picker", "version": "0.10.2", "authors": [ "Indri Muska " ], "description": "Angular Moment Picker is an AngularJS directive for date and time picker using Moment.js", "main": [ "dist/angular-moment-picker.min.js", "dist/angular-moment-picker.min.css" ], "keywords": [ "datepicker", "timepicker", "datetime", "picker", "calendar", "moment.js", "angular" ], "license": "MIT", "homepage": "http://indrimuska.github.io/angular-moment-picker", "ignore": [ "**/.*", "node_modules", "bower_components", "test", "tests" ], "dependencies": { "angular": "^1.3", "moment": "^2.16.0" } } ================================================ FILE: dist/angular-moment-picker.css ================================================ /*! Angular Moment Picker - v0.10.2 - http://indrimuska.github.io/angular-moment-picker - (c) 2015 Indri Muska - MIT */ .moment-picker-input { cursor: pointer; } .moment-picker { position: absolute; z-index: 1060; } .moment-picker .moment-picker-container { color: #404040; min-width: 15em; background: #fff; padding: 4px; border: 1px solid #f0f3f4; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; position: absolute; margin-top: 4px; margin-left: -0.5em; -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.075); -moz-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.075); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.075); } .moment-picker .moment-picker-container:before, .moment-picker .moment-picker-container:after { content: ''; display: block; width: 0; height: 0; border: 8px solid transparent; border-top: none; position: absolute; top: -9px; left: 15px; } .moment-picker .moment-picker-container:before { border-bottom-color: #f0f3f4; border-width: 9px; } .moment-picker .moment-picker-container:after { border-bottom-color: #fff; margin-top: 1px; margin-left: 1px; } .moment-picker.inline { display: block; position: relative; } .moment-picker.inline .moment-picker-container { position: relative; margin: 0; } .moment-picker.inline .moment-picker-container:before, .moment-picker.inline .moment-picker-container:after { content: none; } .moment-picker.top .moment-picker-container { bottom: 100%; margin-top: auto; margin-bottom: 4px; } .moment-picker.top .moment-picker-container:before, .moment-picker.top .moment-picker-container:after { border: 8px solid transparent; border-bottom: none; top: auto; bottom: -9px; } .moment-picker.top .moment-picker-container:before { border-top-color: #f0f3f4; border-width: 9px; } .moment-picker.top .moment-picker-container:after { border-top-color: #fff; margin-top: auto; margin-bottom: 1px; } .moment-picker.right .moment-picker-container { right: 0; margin-left: auto; margin-right: -0.5em; } .moment-picker.right .moment-picker-container:before, .moment-picker.right .moment-picker-container:after { left: auto; right: 15px; } .moment-picker.right .moment-picker-container:after { margin-left: auto; margin-right: 1px; } .moment-picker table { border-collapse: collapse; border-spacing: 0; min-width: 100%; table-layout: fixed; } .moment-picker th { font-weight: bold; } .moment-picker th:first-child, .moment-picker th:last-child { width: 2em; } .moment-picker th, .moment-picker td { padding: 0; text-align: center; min-width: 2em; height: 2em; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.9); cursor: pointer; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; } .moment-picker th:hover, .moment-picker td:hover { background-color: #fafbfb; background-image: -webkit-gradient(linear, left top, left bottom, from(#f0f3f4), to(#fafbfb)); background-image: -webkit-linear-gradient(#f0f3f4, #fafbfb); background-image: -moz-linear-gradient(#f0f3f4, #fafbfb); background-image: -o-linear-gradient(#f0f3f4, #fafbfb); background-image: linear-gradient(#f0f3f4, #fafbfb); } .moment-picker th.disabled, .moment-picker td.disabled, .moment-picker th.disabled:hover, .moment-picker td.disabled:hover { color: #abbbc7; background: none; cursor: default; } .moment-picker td.today { background: #e4eef5; color: #404040; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.9); } .moment-picker td.selected { color: #fff; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3); border-color: #3ca0dd; background-color: #45b1e8; background-image: -webkit-gradient(linear, left top, left bottom, from(#45b1e8), to(#3097de)); background-image: -webkit-linear-gradient(#45b1e8, #3097de); background-image: -moz-linear-gradient(#45b1e8, #3097de); background-image: -o-linear-gradient(#45b1e8, #3097de); background-image: linear-gradient(#45b1e8, #3097de); } .moment-picker td.highlighted { background-image: -webkit-radial-gradient(transparent, rgba(0, 0, 0, 0.15)); background-image: -moz-radial-gradient(transparent, rgba(0, 0, 0, 0.15)); background-image: -o-radial-gradient(transparent, rgba(0, 0, 0, 0.15)); background-image: radial-gradient(transparent, rgba(0, 0, 0, 0.15)); } .moment-picker .decade-view td, .moment-picker .year-view td { height: 3.4em; } .moment-picker .month-view .moment-picker-specific-views th { background: none; cursor: default; } .moment-picker .month-view td { width: 1.4285714286em; } .moment-picker .day-view td, .moment-picker .hour-view td { height: 2.3333333333em; } .moment-picker .minute-view td { height: 1.8em; } ================================================ FILE: dist/angular-moment-picker.js ================================================ /*! Angular Moment Picker - v0.10.2 - http://indrimuska.github.io/angular-moment-picker - (c) 2015 Indri Muska - MIT */ /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // identity function for calling harmony imports with the correct context /******/ __webpack_require__.i = function(value) { return value; }; /******/ /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { /******/ configurable: false, /******/ enumerable: true, /******/ get: getter /******/ }); /******/ } /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = function(module) { /******/ var getter = module && module.__esModule ? /******/ function getDefault() { return module['default']; } : /******/ function getModuleExports() { return module; }; /******/ __webpack_require__.d(getter, 'a', getter); /******/ return getter; /******/ }; /******/ /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 17); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; exports.__esModule = true; var angular = __webpack_require__(1); var moment = __webpack_require__(2); exports.KEYS = { up: 38, down: 40, left: 37, right: 39, escape: 27, enter: 13 }; exports.isValidMoment = function (value) { return moment.isMoment(value) && value.isValid(); }; exports.toValue = function (date, format, locale) { var momentDate = date; if (!exports.isValidMoment(date)) momentDate = exports.toMoment(date, format, locale); return exports.momentToValue(momentDate, format); }; exports.toMoment = function (date, format, locale) { var momentDate = moment(date, format, locale); if (!exports.isValidMoment(momentDate)) momentDate = undefined; return momentDate; }; exports.momentToValue = function (momentObject, format) { if (!exports.isValidMoment(momentObject)) return undefined; return !format ? momentObject.valueOf() : momentObject.format(format); }; exports.valueToMoment = function (formattedValue, $scope) { var momentValue; if (!formattedValue) return momentValue; if (!$scope.format) momentValue = moment(formattedValue); else momentValue = moment(formattedValue, $scope.format, $scope.locale); if ($scope.model) { // set value for each view precision (from Decade View to minView) var views = $scope.views.all.slice(0, $scope.views.all.indexOf($scope.detectedMinView)); angular.forEach(views, function (view) { var precision = $scope.views.precisions[view]; momentValue[precision]($scope.model[precision]()); }); } return momentValue; }; exports.setValue = function (value, $scope, $ctrl, $attrs) { var modelValue = exports.isValidMoment(value) ? value.clone() : exports.valueToMoment(value, $scope), viewValue = exports.momentToValue(modelValue, $scope.format); $scope.model = exports.updateMoment($scope.model, modelValue, $scope); $ctrl.$modelValue = exports.updateMoment($ctrl.$modelValue, modelValue, $scope); if ($attrs['ngModel'] != $attrs['momentPicker']) $scope.value = viewValue; if ($attrs['ngModel']) { $ctrl.$setViewValue(viewValue); $ctrl.$render(); // render input value } }; exports.updateMoment = function (model, value, $scope) { if (!exports.isValidMoment(model) || !value) model = value; else { if (!model.isSame(value)) { // set value for each view precision (from Decade View to maxView) var views = $scope.views.all.slice(0, $scope.views.all.indexOf($scope.detectedMaxView) + 1); angular.forEach(views, function (view) { var precision = $scope.views.precisions[view]; model[precision](value[precision]()); }); } } return model; }; /***/ }), /* 1 */ /***/ (function(module, exports) { module.exports = angular; /***/ }), /* 2 */ /***/ (function(module, exports) { module.exports = moment; /***/ }), /* 3 */ /***/ (function(module, exports) { // removed by extract-text-webpack-plugin /***/ }), /* 4 */ /***/ (function(module, exports) { // removed by extract-text-webpack-plugin /***/ }), /* 5 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; exports.__esModule = true; var angular = __webpack_require__(1); var provider_1 = __webpack_require__(9); exports.Provider = provider_1["default"]; var directive_1 = __webpack_require__(7); exports.Directive = directive_1["default"]; angular .module('moment-picker', []) .provider('momentPicker', [function () { return new provider_1["default"](); }]) .directive('momentPicker', [ '$timeout', '$sce', '$log', '$window', 'momentPicker', '$compile', '$templateCache', function ($timeout, $sce, $log, $window, momentPicker, $compile, $templateCache) { return new directive_1["default"]($timeout, $sce, $log, $window, momentPicker, $compile, $templateCache); } ]); /***/ }), /* 6 */ /***/ (function(module, exports) { module.exports = "
"; /***/ }), /* 7 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; exports.__esModule = true; var angular = __webpack_require__(1); var moment = __webpack_require__(2); var helpers_1 = __webpack_require__(8); var views_1 = __webpack_require__(13); var utility_1 = __webpack_require__(0); var templateHtml = __webpack_require__(6); var Directive = (function () { function Directive($timeout, $sce, $log, $window, provider, $compile, $templateCache) { var _this = this; this.$timeout = $timeout; this.$sce = $sce; this.$log = $log; this.$window = $window; this.provider = provider; this.$compile = $compile; this.$templateCache = $templateCache; this.restrict = 'A'; this.require = '?ngModel'; this.transclude = true; this.template = templateHtml; this.scope = { value: '=?momentPicker', model: '=?ngModel', locale: '@?', format: '@?', minView: '@?', maxView: '@?', startView: '@?', minDate: '=?', maxDate: '=?', startDate: '=?', disabled: '=?disable', position: '@?', inline: '@?', validate: '=?', autoclose: '=?', setOnSelect: '=?', isOpen: '=?', today: '=?', keyboard: '=?', showHeader: '=?', additions: '=?', change: '&?', selectable: '&?' }; this.link = function ($scope, $element, $attrs, $ctrl, $transclude) { $transclude(function ($transElement) { // one-way binding attributes angular.forEach([ 'locale', 'format', 'minView', 'maxView', 'startView', 'position', 'inline', 'validate', 'autoclose', 'setOnSelect', 'today', 'keyboard', 'showHeader', 'leftArrow', 'rightArrow', 'additions' ], function (attr) { if (!angular.isDefined($scope[attr])) $scope[attr] = _this.provider[attr]; if (!angular.isDefined($attrs[attr])) $attrs[attr] = $scope[attr]; }); // check if ngModel has been set if (!$attrs['ngModel']) $ctrl = {}; // limits $scope.limits = { minDate: utility_1.toMoment($scope.minDate, $scope.format, $scope.locale), maxDate: utility_1.toMoment($scope.maxDate, $scope.format, $scope.locale), isAfterOrEqualMin: function (value, precision) { return !angular.isDefined($scope.limits.minDate) || value.isAfter($scope.limits.minDate, precision) || value.isSame($scope.limits.minDate, precision); }, isBeforeOrEqualMax: function (value, precision) { return !angular.isDefined($scope.limits.maxDate) || value.isBefore($scope.limits.maxDate, precision) || value.isSame($scope.limits.maxDate, precision); }, isSelectable: function (value, precision) { var selectable = true; try { if (angular.isFunction($scope.selectable) && $attrs['selectable']) selectable = $scope.selectable({ date: value, type: precision }); } catch (e) { _this.$log.error(e); } return $scope.limits.isAfterOrEqualMin(value, precision) && $scope.limits.isBeforeOrEqualMax(value, precision) && selectable; }, checkValue: function () { if (!utility_1.isValidMoment($ctrl.$modelValue) || !$scope.validate) return; if (!$scope.limits.isAfterOrEqualMin($ctrl.$modelValue)) utility_1.setValue($scope.limits.minDate, $scope, $ctrl, $attrs); if (!$scope.limits.isBeforeOrEqualMax($ctrl.$modelValue)) utility_1.setValue($scope.limits.maxDate, $scope, $ctrl, $attrs); }, checkView: function () { if (!angular.isDefined($scope.view.moment)) $scope.view.moment = moment().locale($scope.locale); if (!$scope.limits.isAfterOrEqualMin($scope.view.moment)) $scope.view.moment = $scope.limits.minDate.clone(); if (!$scope.limits.isBeforeOrEqualMax($scope.view.moment)) $scope.view.moment = $scope.limits.maxDate.clone(); $scope.view.update(); $scope.view.render(); } }; $scope.views = { all: ['decade', 'year', 'month', 'day', 'hour', 'minute'], precisions: { decade: 'year', year: 'month', month: 'date', day: 'hour', hour: 'minute', minute: 'second' }, // for each view, `$scope.views.formats` object contains the available moment formats // formats present in more views are used to perform min/max view detection (i.e. 'LTS', 'LT', ...) formats: { decade: 'Y{1,2}(?!Y)|YYYY|[Ll]{1,4}(?!T)', /* formats: Y,YY,YYYY,L,LL,LLL,LLLL,l,ll,lll,llll */ year: 'M{1,4}(?![Mo])|Mo|Q', /* formats: M,MM,MMM,MMM,Mo,Q */ month: '[Dd]{1,4}(?![Ddo])|DDDo|[Dd]o|[Ww]{1,2}(?![Wwo])|[Ww]o|[Ee]|L{1,2}(?!T)|l{1,2}', /* formats: D,DD,DDD,DDDD,d,dd,ddd,dddd,DDDo,Do,do,W,WW,w,ww,Wo,wo,E,e,L,LL,l,ll */ day: '[Hh]{1,2}|LTS?', /* formats: H,HH,h,hh,LT,LTS */ hour: 'm{1,2}|[Ll]{3,4}|LT(?!S)', /* formats: m,mm,LLL,LLLL,lll,llll,LT */ minute: 's{1,2}|S{1,}|X|LTS' /* formats: s,ss,S,SS,SSS..,X,LTS */ }, detectMinMax: function () { $scope.detectedMinView = $scope.detectedMaxView = undefined; if (!$scope.format) return; var minView, maxView; angular.forEach($scope.views.formats, function (formats, view) { var regexp = new RegExp('(' + formats + ')(?![^\[]*\])', 'g'); if (!$scope.format.match(regexp)) return; if (!angular.isDefined(minView)) minView = view; maxView = view; }); if (!angular.isDefined(minView)) minView = 0; else minView = Math.max(0, $scope.views.all.indexOf(minView)); if (!angular.isDefined(maxView)) maxView = $scope.views.all.length - 1; else maxView = Math.min($scope.views.all.length - 1, $scope.views.all.indexOf(maxView)); if (minView > $scope.views.all.indexOf($scope.minView)) $scope.minView = $scope.views.all[minView]; if (maxView < $scope.views.all.indexOf($scope.maxView)) $scope.maxView = $scope.views.all[maxView]; // save detected min/max view to use them to update the model value properly $scope.detectedMinView = $scope.views.all[minView]; $scope.detectedMaxView = $scope.views.all[maxView]; }, // specific views decade: new views_1.DecadeView($scope, $ctrl, _this.provider), year: new views_1.YearView($scope, $ctrl, _this.provider), month: new views_1.MonthView($scope, $ctrl, _this.provider), day: new views_1.DayView($scope, $ctrl, _this.provider), hour: new views_1.HourView($scope, $ctrl, _this.provider), minute: new views_1.MinuteView($scope, $ctrl, _this.provider) }; $scope.view = { moment: undefined, value: undefined, isOpen: false, selected: $scope.startView, update: function () { $scope.view.value = utility_1.momentToValue($scope.view.moment, $scope.format); }, toggle: function () { $scope.view.isOpen ? $scope.view.close() : $scope.view.open(); }, open: function () { if ($scope.disabled || $scope.view.isOpen || $scope.inline) return; $scope.isOpen = true; $scope.view.isOpen = true; document.body.appendChild($scope.picker[0]); $scope.view.position(); }, close: function () { if (!$scope.view.isOpen || $scope.inline) return; $scope.isOpen = false; $scope.view.isOpen = false; $scope.view.selected = $scope.startView; $scope.picker[0].parentNode.removeChild($scope.picker[0]); }, position: function () { if (!$scope.view.isOpen || $scope.position || $scope.inline) return; var element = $element[0], picker = $scope.picker.children()[0], hasClassTop = $scope.picker.hasClass('top'), hasClassRight = $scope.picker.hasClass('right'), offset = helpers_1.getOffset($element[0]), top = offset.top - _this.$window.pageYOffset, left = offset.left - _this.$window.pageXOffset, winWidth = _this.$window.innerWidth, winHeight = _this.$window.innerHeight, shouldHaveClassTop = top + _this.$window.pageYOffset - picker.offsetHeight > 0 && top > winHeight / 2, shouldHaveClassRight = left + picker.offsetWidth > winWidth, pickerTop = offset.top + (shouldHaveClassTop ? 0 : element.offsetHeight) + 'px', pickerLeft = offset.left + 'px', pickerWidth = element.offsetWidth + 'px'; if (!hasClassTop && shouldHaveClassTop) $scope.picker.addClass('top'); if (hasClassTop && !shouldHaveClassTop) $scope.picker.removeClass('top'); if (!hasClassRight && shouldHaveClassRight) $scope.picker.addClass('right'); if (hasClassRight && !shouldHaveClassRight) $scope.picker.removeClass('right'); if ($scope.picker.css('top') !== pickerTop) $scope.picker.css('top', pickerTop); if ($scope.picker.css('left') !== pickerLeft) $scope.picker.css('left', pickerLeft); if ($scope.picker.css('width') !== pickerWidth) $scope.picker.css('width', pickerWidth); }, keydown: function (e) { var view = $scope.views[$scope.view.selected], precision = $scope.views.precisions[$scope.view.selected].replace('date', 'day'), singleUnit = _this.provider[precision + 'sStep'] || 1, operation = [utility_1.KEYS.up, utility_1.KEYS.left].indexOf(e.keyCode) >= 0 ? 'subtract' : 'add', highlight = function (vertical) { var unitMultiplier = vertical ? view.perLine : 1, nextDate = $scope.view.moment.clone()[operation](singleUnit * unitMultiplier, precision); if ($scope.limits.isSelectable(nextDate, precision)) { $scope.view.moment = nextDate; $scope.view.update(); $scope.view.render(); } }; switch (e.keyCode) { case utility_1.KEYS.up: case utility_1.KEYS.down: e.preventDefault(); if (!$scope.view.isOpen) $scope.view.open(); else highlight(true); break; case utility_1.KEYS.left: case utility_1.KEYS.right: if (!$scope.view.isOpen) break; e.preventDefault(); highlight(); break; case utility_1.KEYS.enter: if (!$scope.view.isOpen) break; $scope.view.change(precision); e.preventDefault(); break; case utility_1.KEYS.escape: $scope.view.toggle(); break; } $scope.$evalAsync(); }, // utility unit: function () { return $scope.view.selected == 'decade' ? 10 : 1; }, precision: function () { return $scope.view.selected.replace('decade', 'year'); }, // header title: '', previous: { label: _this.$sce.trustAsHtml($scope.leftArrow), selectable: true, set: function () { if ($scope.view.previous.selectable) { $scope.view.moment.subtract($scope.view.unit(), $scope.view.precision()); $scope.view.update(); $scope.view.render(); } } }, next: { selectable: true, label: _this.$sce.trustAsHtml($scope.rightArrow), set: function () { if ($scope.view.next.selectable) { $scope.view.moment.add($scope.view.unit(), $scope.view.precision()); $scope.view.update(); $scope.view.render(); } } }, setParentView: function () { $scope.view.change($scope.views.all[Math.max(0, $scope.views.all.indexOf($scope.view.selected) - 1)]); }, // body render: function () { var momentPrevious = $scope.view.moment.clone().startOf($scope.view.precision()).subtract($scope.view.unit(), $scope.view.precision()), momentNext = $scope.view.moment.clone().endOf($scope.view.precision()).add($scope.view.unit(), $scope.view.precision()); $scope.view.previous.selectable = $scope.limits.isAfterOrEqualMin(momentPrevious, $scope.view.precision()); $scope.view.previous.label = _this.$sce.trustAsHtml($scope.view.previous.selectable ? $scope.leftArrow : ' '); $scope.view.next.selectable = $scope.limits.isBeforeOrEqualMax(momentNext, $scope.view.precision()); $scope.view.next.label = _this.$sce.trustAsHtml($scope.view.next.selectable ? $scope.rightArrow : ' '); $scope.view.title = $scope.views[$scope.view.selected].render(); }, change: function (view) { var nextView = $scope.views.all.indexOf(view), minView = $scope.views.all.indexOf($scope.minView), maxView = $scope.views.all.indexOf($scope.maxView); var update = function () { utility_1.setValue($scope.view.moment, $scope, $ctrl, $attrs); $scope.view.update(); if ($attrs['ngModel']) $ctrl.$commitViewValue(); }; if ($scope.setOnSelect) update(); if (nextView < 0 || nextView > maxView) { if (!$scope.setOnSelect) update(); if ($scope.autoclose) _this.$timeout($scope.view.close); } else if (nextView >= minView) $scope.view.selected = view; } }; // creation $element.prepend($transElement); $scope.picker = angular.element($element[0].querySelectorAll('.moment-picker')); $scope.container = angular.element($scope.picker[0].querySelectorAll('.moment-picker-container')); $scope.input = $element[0].tagName.toLowerCase() != 'input' && $element[0].querySelectorAll('input').length > 0 ? angular.element($element[0].querySelectorAll('input')) : angular.element($element[0]); $scope.input.addClass('moment-picker-input').attr('tabindex', 0); ($scope.position || '').split(' ').forEach(function (className) { return $scope.picker.addClass(className); }); if (!$scope.inline) $scope.picker[0].parentNode.removeChild($scope.picker[0]); else { $element.after($scope.picker); $scope.picker.addClass('inline'); } // transclude scope to template additions _this.$timeout(function () { angular.forEach($scope.additions || {}, function (tempalteUrl, key) { var placeholder = angular.element($scope.container[0].querySelector('.moment-picker-addition.' + key)); var template = _this.$templateCache.get(tempalteUrl); var compiled = _this.$compile(template)($scope.$parent); placeholder.append(compiled); }); }); // initialization $scope.views.detectMinMax(); $scope.limits.checkView(); // model controller is initialized after linking function _this.$timeout(function () { if ($attrs['ngModel']) { if (!$ctrl.$modelValue && $scope.value) $ctrl.$setViewValue($scope.value); $ctrl.$commitViewValue(); $ctrl.$render(); } else { if ($scope.value) $ctrl.$modelValue = utility_1.valueToMoment($scope.value, $scope); } // view initialization if ($scope.startDate) $scope.view.moment = utility_1.toMoment($scope.startDate, $scope.format, $scope.locale); else if (utility_1.isValidMoment($ctrl.$modelValue)) $scope.view.moment = $ctrl.$modelValue.clone(); $scope.view.update(); $scope.view.render(); }); // model <-> view conversion if ($attrs['ngModel']) { $ctrl.$parsers.push(function (viewValue) { return utility_1.updateMoment($ctrl.$modelValue, utility_1.valueToMoment(viewValue, $scope), $scope) || true; }); $ctrl.$formatters.push(function (modelValue) { return utility_1.momentToValue(modelValue, $scope.format) || ''; }); $ctrl.$viewChangeListeners.push(function () { if ($attrs['ngModel'] != $attrs['momentPicker']) $scope.value = $ctrl.$viewValue; }); $ctrl.$validators.minDate = function (value) { return $scope.validate || !utility_1.isValidMoment(value) || $scope.limits.isAfterOrEqualMin(value); }; $ctrl.$validators.maxDate = function (value) { return $scope.validate || !utility_1.isValidMoment(value) || $scope.limits.isBeforeOrEqualMax(value); }; } // properties listeners if ($attrs['ngModel'] != $attrs['momentPicker']) $scope.$watch('value', function (newValue, oldValue) { if (newValue !== oldValue) utility_1.setValue(newValue, $scope, $ctrl, $attrs); }); $scope.$watch(function () { return utility_1.momentToValue($ctrl.$modelValue, $scope.format); }, function (newViewValue, oldViewValue) { if (newViewValue == oldViewValue) return; var newModelValue = utility_1.valueToMoment(newViewValue, $scope); utility_1.setValue(newModelValue, $scope, $ctrl, $attrs); $scope.limits.checkValue(); $scope.view.moment = (newModelValue || moment().locale($scope.locale)).clone(); $scope.view.update(); $scope.view.render(); if (angular.isFunction($scope.change) && $attrs['change']) { var oldModelValue_1 = utility_1.valueToMoment(oldViewValue, $scope); $scope.$evalAsync(function () { return $scope.change({ newValue: newModelValue, oldValue: oldModelValue_1 }); }); } }); $scope.$watch(function () { return $ctrl.$modelValue && $ctrl.$modelValue.valueOf(); }, function () { var viewMoment = (utility_1.isValidMoment($ctrl.$modelValue) ? $ctrl.$modelValue : moment().locale($scope.locale)).clone(); if (!viewMoment.isSame($scope.view.moment)) { $scope.view.moment = viewMoment; $scope.view.update(); $scope.view.render(); } }); $scope.$watch('view.selected', function () { return $scope.view.render(); }); $scope.$watchGroup(['minView', 'maxView'], function () { // auto-detect minView/maxView $scope.views.detectMinMax(); // limit startView $scope.startView = $scope.views.all[Math.max(Math.min($scope.views.all.indexOf($scope.startView), $scope.views.all.indexOf($scope.maxView)), $scope.views.all.indexOf($scope.minView))]; $scope.view.selected = $scope.startView; }); $scope.$watchGroup([ function () { return utility_1.toValue($scope.minDate, $scope.format, $scope.locale); }, function () { return utility_1.toValue($scope.maxDate, $scope.format, $scope.locale); } ], function () { angular.forEach(['minDate', 'maxDate'], function (field) { $scope.limits[field] = utility_1.toMoment($scope[field], $scope.format, $scope.locale); }); $scope.limits.checkValue(); $scope.limits.checkView(); $scope.view.render(); }); $scope.$watch(function () { return utility_1.toValue($scope.startDate, $scope.format, $scope.locale); }, function (newViewValue, oldViewValue) { if (newViewValue == oldViewValue) return; $scope.view.moment = utility_1.valueToMoment(newViewValue, $scope); $scope.view.update(); $scope.view.render(); }); $attrs.$observe('locale', function (locale) { return $scope.locale = locale; }); $scope.$watch('locale', function (locale, previous) { if (!angular.isDefined(previous) || locale == previous) return; if (utility_1.isValidMoment($ctrl.$modelValue)) utility_1.setValue($ctrl.$modelValue.locale(locale), $scope, $ctrl, $attrs); if (utility_1.isValidMoment($scope.view.moment)) $scope.view.moment = $scope.view.moment.locale(locale); if (utility_1.isValidMoment($scope.limits.minDate)) $scope.limits.minDate = $scope.limits.minDate.locale(locale); if (utility_1.isValidMoment($scope.limits.maxDate)) $scope.limits.maxDate = $scope.limits.maxDate.locale(locale); $scope.view.render(); }); $scope.$watch('validate', $scope.limits.checkValue); $scope.$watch('isOpen', function (isOpen) { if ($scope.inline) $scope.view.isOpen = true; else if (angular.isDefined(isOpen) && isOpen != $scope.view.isOpen) $scope.view.toggle(); }); // event listeners var focusInput = function (e) { if (e) e.preventDefault(); $scope.input[0].focus(); }; // use `touchstart` for iOS Safari, where click events aren't propogated under most circumstances. $scope.input .on('focus click touchstart', function () { return $scope.$evalAsync($scope.view.open); }) .on('blur', function () { return $scope.$evalAsync($scope.view.close); }) .on('keydown', function (e) { if ($scope.keyboard) $scope.view.keydown(e); }); $element.on('click touchstart', function () { return focusInput(); }); $scope.container.on('mousedown', function (e) { return focusInput(e); }); angular.element(_this.$window).on('resize scroll', $scope.view.position); // unbind events on destroy $scope.$on('$destroy', function () { $scope.input.off('focus click touchstart blur keydown'); $element.off('click touchstart'); $scope.container.off('mousedown'); $scope.picker.remove(); angular.element(_this.$window).off('resize scroll', $scope.view.position); }); }); }; } return Directive; }()); exports["default"] = Directive; /***/ }), /* 8 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; exports.__esModule = true; /** * Offset getter method from jQuery: https://github.com/jquery/jquery/blob/3.1.1/src/offset.js#L78 */ exports.getOffset = function (element) { if (!element) return; if (!element.getClientRects().length) return { top: 0, left: 0 }; // https://github.com/jquery/jquery/blob/3.1.1/src/core.js#L220 var isWindow = function (obj) { return obj != null && obj === obj.window; }; var getWindow = function (elem) { return isWindow(elem) ? elem : elem.nodeType === 9 && elem.defaultView; }; // tslint:disable-line:no-any var rect = element.getBoundingClientRect(); if (!rect.width && !rect.height) return rect; var doc = element.ownerDocument; var win = getWindow(doc); var docElem = doc.documentElement; return { top: rect.top + win.pageYOffset - docElem.clientTop, left: rect.left + win.pageXOffset - docElem.clientLeft }; }; /***/ }), /* 9 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; exports.__esModule = true; var angular = __webpack_require__(1); var Provider = (function () { function Provider() { this.settings = { locale: 'en', format: 'L LTS', minView: 'decade', maxView: 'minute', startView: 'year', inline: false, validate: true, autoclose: true, setOnSelect: false, today: false, keyboard: false, showHeader: true, leftArrow: '←', rightArrow: '→', // Decade View yearsFormat: 'YYYY', // Year View monthsFormat: 'MMM', // Month View daysFormat: 'D', // Day View hoursFormat: 'HH:[00]', hoursStart: 0, hoursEnd: 23, // Hour View minutesStep: 5, minutesStart: 0, minutesEnd: 59, // Minute View secondsFormat: 'ss', secondsStep: 1, secondsStart: 0, secondsEnd: 59 }; } Provider.prototype.options = function (options) { angular.extend(this.settings, options); return angular.copy(this.settings); }; Provider.prototype.$get = function () { return this.settings; }; return Provider; }()); exports["default"] = Provider; /***/ }), /* 10 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; exports.__esModule = true; var utility_1 = __webpack_require__(0); var DayView = (function () { function DayView($scope, $ctrl, provider) { this.$scope = $scope; this.$ctrl = $ctrl; this.provider = provider; this.perLine = 4; this.rows = {}; } DayView.prototype.render = function () { var hour = this.$scope.view.moment.clone().startOf('day').hour(this.provider.hoursStart); this.rows = {}; for (var h = 0; h <= this.provider.hoursEnd - this.provider.hoursStart; h++) { var index = Math.floor(h / this.perLine), selectable = this.$scope.limits.isSelectable(hour, 'hour'); if (!this.rows[index]) this.rows[index] = []; this.rows[index].push({ index: h, label: hour.format(this.provider.hoursFormat), year: hour.year(), month: hour.month(), date: hour.date(), hour: hour.hour(), "class": [ this.$scope.keyboard && hour.isSame(this.$scope.view.moment, 'hour') ? 'highlighted' : '', !selectable ? 'disabled' : utility_1.isValidMoment(this.$ctrl.$modelValue) && hour.isSame(this.$ctrl.$modelValue, 'hour') ? 'selected' : '' ].join(' ').trim(), selectable: selectable }); hour.add(1, 'hours'); } // return title return this.$scope.view.moment.format('LL'); }; DayView.prototype.set = function (hour) { if (!hour.selectable) return; this.$scope.view.moment.year(hour.year).month(hour.month).date(hour.date).hour(hour.hour); this.$scope.view.update(); this.$scope.view.change('hour'); }; return DayView; }()); exports["default"] = DayView; /***/ }), /* 11 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; exports.__esModule = true; var utility_1 = __webpack_require__(0); var DecadeView = (function () { function DecadeView($scope, $ctrl, provider) { this.$scope = $scope; this.$ctrl = $ctrl; this.provider = provider; this.perLine = 4; this.rows = {}; } DecadeView.prototype.render = function () { var year = this.$scope.view.moment.clone(), firstYear = Math.floor(year.year() / 10) * 10 - 1; this.rows = {}; year.year(firstYear); for (var y = 0; y < 12; y++) { var index = Math.floor(y / this.perLine), selectable = this.$scope.limits.isSelectable(year, 'year'); if (!this.rows[index]) this.rows[index] = []; this.rows[index].push({ index: year.year(), label: year.format(this.provider.yearsFormat), year: year.year(), "class": [ this.$scope.keyboard && year.isSame(this.$scope.view.moment, 'year') ? 'highlighted' : '', !selectable || [0, 11].indexOf(y) >= 0 ? 'disabled' : utility_1.isValidMoment(this.$ctrl.$modelValue) && year.isSame(this.$ctrl.$modelValue, 'year') ? 'selected' : '' ].join(' ').trim(), selectable: selectable }); year.add(1, 'years'); } // return title return [year.subtract(2, 'years').format('YYYY'), year.subtract(9, 'years').format('YYYY')].reverse().join(' - '); }; DecadeView.prototype.set = function (year) { if (!year.selectable) return; this.$scope.view.moment.year(year.year); this.$scope.view.update(); this.$scope.view.change('year'); }; return DecadeView; }()); exports["default"] = DecadeView; /***/ }), /* 12 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; exports.__esModule = true; var angular = __webpack_require__(1); var moment = __webpack_require__(2); var utility_1 = __webpack_require__(0); var HourView = (function () { function HourView($scope, $ctrl, provider) { this.$scope = $scope; this.$ctrl = $ctrl; this.provider = provider; this.perLine = 4; this.rows = {}; } HourView.prototype.render = function () { var i = 0, minute = this.$scope.view.moment.clone().startOf('hour').minute(this.provider.minutesStart), minutesFormat = this.provider.minutesFormat || moment.localeData(this.$scope.locale).longDateFormat('LT').replace(/[aA]/, '').trim(); this.rows = {}; for (var m = 0; m <= this.provider.minutesEnd - this.provider.minutesStart; m += this.provider.minutesStep) { var index = Math.floor(i / this.perLine), selectable = this.$scope.limits.isSelectable(minute, 'minute'); if (!this.rows[index]) this.rows[index] = []; this.rows[index].push({ index: minute.minute(), label: minute.format(minutesFormat), year: minute.year(), month: minute.month(), date: minute.date(), hour: minute.hour(), minute: minute.minute(), "class": [ this.$scope.keyboard && minute.isSame(this.$scope.view.moment, 'minute') ? 'highlighted' : '', !selectable ? 'disabled' : utility_1.isValidMoment(this.$ctrl.$modelValue) && minute.isSame(this.$ctrl.$modelValue, 'minute') ? 'selected' : '' ].join(' ').trim(), selectable: selectable }); i++; minute.add(this.provider.minutesStep, 'minutes'); } if (this.$scope.keyboard) this.highlightClosest(); // return title return this.$scope.view.moment.clone().startOf('hour').format('lll'); }; HourView.prototype.set = function (minute) { if (!minute.selectable) return; this.$scope.view.moment.year(minute.year).month(minute.month).date(minute.date).hour(minute.hour).minute(minute.minute); this.$scope.view.update(); this.$scope.view.change('minute'); }; HourView.prototype.highlightClosest = function () { var _this = this; var minutes = [], minute; angular.forEach(this.rows, function (row) { angular.forEach(row, function (value) { if (Math.abs(value.minute - _this.$scope.view.moment.minute()) < _this.provider.minutesStep) minutes.push(value); }); }); minute = minutes.sort(function (value1, value2) { return Math.abs(value1.minute - _this.$scope.view.moment.minute()) > Math.abs(value2.minute - _this.$scope.view.moment.minute()) ? 1 : 0; })[0]; if (!minute || minute.minute - this.$scope.view.moment.minute() == 0) return; this.$scope.view.moment.year(minute.year).month(minute.month).date(minute.date).hour(minute.hour).minute(minute.minute); this.$scope.view.update(); if (minute.selectable) minute["class"] = (minute["class"] + ' highlighted').trim(); }; return HourView; }()); exports["default"] = HourView; /***/ }), /* 13 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; exports.__esModule = true; var decadeView_1 = __webpack_require__(11); exports.DecadeView = decadeView_1["default"]; var yearView_1 = __webpack_require__(16); exports.YearView = yearView_1["default"]; var monthView_1 = __webpack_require__(15); exports.MonthView = monthView_1["default"]; var dayView_1 = __webpack_require__(10); exports.DayView = dayView_1["default"]; var hourView_1 = __webpack_require__(12); exports.HourView = hourView_1["default"]; var minuteView_1 = __webpack_require__(14); exports.MinuteView = minuteView_1["default"]; /***/ }), /* 14 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; exports.__esModule = true; var angular = __webpack_require__(1); var utility_1 = __webpack_require__(0); var MinuteView = (function () { function MinuteView($scope, $ctrl, provider) { this.$scope = $scope; this.$ctrl = $ctrl; this.provider = provider; this.perLine = 6; this.rows = {}; } MinuteView.prototype.render = function () { var i = 0, second = this.$scope.view.moment.clone().startOf('minute').second(this.provider.secondsStart); this.rows = {}; for (var s = 0; s <= this.provider.secondsEnd - this.provider.secondsStart; s += this.provider.secondsStep) { var index = Math.floor(i / this.perLine), selectable = this.$scope.limits.isSelectable(second, 'second'); if (!this.rows[index]) this.rows[index] = []; this.rows[index].push({ index: second.second(), label: second.format(this.provider.secondsFormat), year: second.year(), month: second.month(), date: second.date(), hour: second.hour(), minute: second.minute(), second: second.second(), "class": [ this.$scope.keyboard && second.isSame(this.$scope.view.moment, 'second') ? 'highlighted' : '', !selectable ? 'disabled' : utility_1.isValidMoment(this.$ctrl.$modelValue) && second.isSame(this.$ctrl.$modelValue, 'second') ? 'selected' : '' ].join(' ').trim(), selectable: selectable }); i++; second.add(this.provider.secondsStep, 'seconds'); } if (this.$scope.keyboard) this.highlightClosest(); // return title return this.$scope.view.moment.clone().startOf('minute').format('lll'); }; MinuteView.prototype.set = function (second) { if (!second.selectable) return; this.$scope.view.moment.year(second.year).month(second.month).date(second.date).hour(second.hour).minute(second.minute).second(second.second); this.$scope.view.update(); this.$scope.view.change(); }; MinuteView.prototype.highlightClosest = function () { var _this = this; var seconds = [], second; angular.forEach(this.rows, function (row) { angular.forEach(row, function (value) { if (Math.abs(value.second - _this.$scope.view.moment.second()) < _this.provider.secondsStep) seconds.push(value); }); }); second = seconds.sort(function (value1, value2) { return Math.abs(value1.second - _this.$scope.view.moment.second()) > Math.abs(value2.second - _this.$scope.view.moment.second()) ? 1 : 0; })[0]; if (!second || second.second - this.$scope.view.moment.second() == 0) return; this.$scope.view.moment.year(second.year).month(second.month).date(second.date).hour(second.hour).minute(second.minute).second(second.second); this.$scope.view.update(); if (second.selectable) second["class"] = (second["class"] + ' highlighted').trim(); }; return MinuteView; }()); exports["default"] = MinuteView; /***/ }), /* 15 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; exports.__esModule = true; var angular = __webpack_require__(1); var moment = __webpack_require__(2); var utility_1 = __webpack_require__(0); var MonthView = (function () { function MonthView($scope, $ctrl, provider) { this.$scope = $scope; this.$ctrl = $ctrl; this.provider = provider; this.perLine = moment.weekdays().length; this.rows = []; } MonthView.prototype.render = function () { var _this = this; var month = this.$scope.view.moment.month(), day = this.$scope.view.moment.clone().startOf('month').startOf('week').hour(12), rows = {}, firstWeek = day.week(), lastWeek = firstWeek + 5; this.rows = []; for (var week = firstWeek; week <= lastWeek; week++) rows[week] = Array.apply(null, Array(this.perLine)).map(function () { var selectable = _this.$scope.limits.isSelectable(day, 'day'); var date = { index: day.date(), label: day.format(_this.provider.daysFormat), year: day.year(), month: day.month(), date: day.date(), "class": [ _this.$scope.keyboard && day.isSame(_this.$scope.view.moment, 'day') ? 'highlighted' : '', !!_this.$scope.today && day.isSame(new Date(), 'day') ? 'today' : '', !selectable || day.month() != month ? 'disabled' : utility_1.isValidMoment(_this.$ctrl.$modelValue) && day.isSame(_this.$ctrl.$modelValue, 'day') ? 'selected' : '' ].join(' ').trim(), selectable: selectable }; day.add(1, 'days'); return date; }); // object to array - see https://github.com/indrimuska/angular-moment-picker/issues/9 angular.forEach(rows, function (row) { return _this.rows.push(row); }); // render headers this.headers = moment.weekdays().map(function (d, i) { return moment().locale(_this.$scope.locale).startOf('week').add(i, 'day').format('dd'); }); // return title return this.$scope.view.moment.format('MMMM YYYY'); }; MonthView.prototype.set = function (day) { if (!day.selectable) return; this.$scope.view.moment.year(day.year).month(day.month).date(day.date); this.$scope.view.update(); this.$scope.view.change('day'); }; return MonthView; }()); exports["default"] = MonthView; /***/ }), /* 16 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; exports.__esModule = true; var moment = __webpack_require__(2); var utility_1 = __webpack_require__(0); var YearView = (function () { function YearView($scope, $ctrl, provider) { this.$scope = $scope; this.$ctrl = $ctrl; this.provider = provider; this.perLine = 4; this.rows = {}; } YearView.prototype.render = function () { var _this = this; var month = this.$scope.view.moment.clone().startOf('year'), months = moment.monthsShort(); this.rows = {}; months.forEach(function (label, i) { var index = Math.floor(i / _this.perLine), selectable = _this.$scope.limits.isSelectable(month, 'month'); if (!_this.rows[index]) _this.rows[index] = []; _this.rows[index].push({ index: month.month(), label: month.format(_this.provider.monthsFormat), year: month.year(), month: month.month(), "class": [ _this.$scope.keyboard && month.isSame(_this.$scope.view.moment, 'month') ? 'highlighted' : '', !selectable ? 'disabled' : utility_1.isValidMoment(_this.$ctrl.$modelValue) && month.isSame(_this.$ctrl.$modelValue, 'month') ? 'selected' : '' ].join(' ').trim(), selectable: selectable }); month.add(1, 'months'); }); // return title return this.$scope.view.moment.format('YYYY'); }; YearView.prototype.set = function (month) { if (!month.selectable) return; this.$scope.view.moment.year(month.year).month(month.month); this.$scope.view.update(); this.$scope.view.change('month'); }; return YearView; }()); exports["default"] = YearView; /***/ }), /* 17 */ /***/ (function(module, exports, __webpack_require__) { __webpack_require__(5); __webpack_require__(3); module.exports = __webpack_require__(4); /***/ }) /******/ ]); ================================================ FILE: dist/themes/README.md ================================================ # Themes ## Default ```html ``` ***Decade view*** | ***Year view*** | ***Month view*** | ***Day view*** | ***Hour view*** | ***Minute view*** :---:|:---:|:---:|:---:|:---:|:---: ![Decade view](http://indrimuska.github.io/angular-moment-picker/img/themes/default/decade-view.png?) | ![Year view](http://indrimuska.github.io/angular-moment-picker/img/themes/default/year-view.png?) | ![Month view](http://indrimuska.github.io/angular-moment-picker/img/themes/default/month-view.png?) | ![Day view](http://indrimuska.github.io/angular-moment-picker/img/themes/default/day-view.png?) | ![Hour view](http://indrimuska.github.io/angular-moment-picker/img/themes/default/hour-view.png?) | ![Minute view](http://indrimuska.github.io/angular-moment-picker/img/themes/default/minute-view.png?) ## Material UI ```html ``` ***Decade view*** | ***Year view*** | ***Month view*** | ***Day view*** | ***Hour view*** | ***Minute view*** :---:|:---:|:---:|:---:|:---:|:---: ![Decade view](http://indrimuska.github.io/angular-moment-picker/img/themes/material-ui/decade-view.png?) | ![Year view](http://indrimuska.github.io/angular-moment-picker/img/themes/material-ui/year-view.png?) | ![Month view](http://indrimuska.github.io/angular-moment-picker/img/themes/material-ui/month-view.png?) | ![Day view](http://indrimuska.github.io/angular-moment-picker/img/themes/material-ui/day-view.png?) | ![Hour view](http://indrimuska.github.io/angular-moment-picker/img/themes/material-ui/hour-view.png?) | ![Minute view](http://indrimuska.github.io/angular-moment-picker/img/themes/material-ui/minute-view.png?) ================================================ FILE: dist/themes/material-ui.css ================================================ /*! Angular Moment Picker - v0.10.2 - http://indrimuska.github.io/angular-moment-picker - (c) 2015 Indri Muska - MIT */ .moment-picker .moment-picker-container { text-shadow: none; -webkit-border-radius: 0; -moz-border-radius: 0; border-radius: 0; padding: 0; } .moment-picker .header-view { border: 1px solid #fff; } .moment-picker .header-view th { background: #eee; -webkit-border-radius: 0; -moz-border-radius: 0; border-radius: 0; min-width: 40px; height: 40px; } .moment-picker .header-view th:hover, .moment-picker td:hover { background-color: #e0e0e0; } .moment-picker td { -webkit-border-radius: 60px; -moz-border-radius: 60px; border-radius: 60px; } .moment-picker .moment-picker-specific-views { padding: 4px 8px; } .moment-picker .moment-picker-specific-views table { border-collapse: separate; border-spacing: 3px; } .moment-picker .moment-picker-specific-views th { background: none; cursor: default; } .moment-picker .decade-view td, .moment-picker .year-view td { min-width: 60px; height: 60px; } .moment-picker .month-view td { min-width: 33px; height: 33px; } .moment-picker .day-view td, .moment-picker .hour-view td { min-width: 60px; height: 40px; } .moment-picker .minute-view table { border-spacing: 2px; } .moment-picker .minute-view td { min-width: 40px; height: 40px; } ================================================ FILE: karma.conf.js ================================================ let webpackConfig = require('./webpack.config'); let generateJsonPlugin = require('generate-json-webpack-plugin'); webpackConfig.devtool = 'source-map'; webpackConfig.plugins = webpackConfig.plugins.filter(plugin => !(plugin instanceof generateJsonPlugin)); module.exports = function (config) { config.set({ files: [ 'node_modules/jquery/dist/jquery.js', 'node_modules/angular/angular.js', 'node_modules/angular-mocks/angular-mocks.js', 'node_modules/moment/min/moment-with-locales.js', 'src/index.ts', 'tests/hacks.ts', 'tests/**/!(hacks|utility).ts' ], preprocessors: { '**/*.ts': ['webpack'] }, webpack: webpackConfig, webpackMiddleware: { noInfo: true, stats: 'errors-only' }, mime: { 'text/x-typescript': ['ts'] }, frameworks: ['jasmine'], browsers: ['PhantomJS'], singleRun: true }); }; ================================================ FILE: package.json ================================================ { "name": "angular-moment-picker", "version": "0.10.2", "description": "Angular Moment Picker is an AngularJS directive for date and time picker using Moment.js", "main": "dist/angular-moment-picker.js", "repository": { "type": "git", "url": "git+https://github.com/indrimuska/angular-moment-picker.git" }, "keywords": [ "datepicker", "timepicker", "datetime", "picker", "calendar", "moment.js", "angular" ], "author": "Indri Muska ", "license": "MIT", "bugs": { "url": "https://github.com/indrimuska/angular-moment-picker/issues" }, "homepage": "http://indrimuska.github.io/angular-moment-picker", "scripts": { "build": "webpack", "minify": "webpack && webpack -p", "release": "npm run test && webpack --define increase='patch' && webpack -p", "test": "node ./node_modules/karma/bin/karma start" }, "dependencies": { "angular": "^1.3", "moment": "^2.16.0" }, "devDependencies": { "@types/angular": "^1.5.21", "@types/angular-mocks": "^1.5.8", "@types/jasmine": "^2.5.38", "@types/node": "^6.0.52", "angular-mocks": "^1.6.1", "autoprefixer": "^6.5.4", "css-loader": "^0.28.1", "extract-text-webpack-plugin": "^2.1.0", "generate-json-webpack-plugin": "^0.2.1", "html-loader": "^0.4.5", "jasmine": "^2.6.0", "jquery": "^3.1.1", "karma": "^1.7.0", "karma-jasmine": "^1.1.0", "karma-phantomjs-launcher": "^1.0.4", "karma-webpack": "^2.0.3", "less": "^2.7.1", "less-loader": "^4.0.3", "postcss": "^5.2.6", "postcss-loader": "^2.0.5", "semver": "^5.3.0", "style-loader": "^0.17.0", "ts-loader": "^2.0.3", "tslint": "^4.1.1", "tslint-loader": "^3.5.3", "typescript": "^2.1.4", "webpack": "^2.5.1" } } ================================================ FILE: src/definitions.d.ts ================================================ import * as angular from 'angular'; import * as moment from 'moment'; import { IProviderOptions } from './provider'; export type ViewString = 'decade' | 'year' | 'month' | 'day' | 'hour' | 'minute'; export type Value = string | number; export type Position = 'top left' | 'top right' | 'bottom left' | 'bottom right'; export interface IDirectiveScope extends ng.IScope { value?: Value; model?: moment.Moment; locale?: string; format?: string; minView?: ViewString; maxView?: ViewString; startView?: ViewString; minDate?: Value; maxDate?: Value; startDate?: Value; disabled?: boolean; position?: Position; inline?: boolean; validate?: boolean; autoclose?: boolean; setOnSelect?: boolean; isOpen?: boolean; today?: boolean; keyboard?: boolean; additions?: { top?: string; bottom?: string }; change?: (context: any) => boolean; selectable?: (context: any) => boolean; } export interface IUtility { isValidMoment: (value: any) => boolean; toValue: (date: any) => Value; toMoment: (date: any) => moment.Moment; momentToValue: (momentObject: moment.Moment) => Value; valueToMoment: (formattedValue: Value) => moment.Moment; setValue: (value: any) => void; } export interface IViewItem { index: number; label: string; year?: number; month?: number; date?: number; hour?: number; minute?: number; second?: number; class: string; selectable: boolean; } export interface IView { perLine: number; headers?: string[]; rows: { [index: number]: IViewItem[] }; render(): string; // return view title set(value: IViewItem): void; highlightClosest?(): void; } export interface IViewHeaderButton { selectable: boolean; label: string; set: () => void; } export interface IDirectiveScopeInternal extends IDirectiveScope, IProviderOptions { // utilities limits: { minDate: moment.Moment; maxDate: moment.Moment; isAfterOrEqualMin: (value: moment.Moment, precision?: moment.unitOfTime.StartOf) => boolean; isBeforeOrEqualMax: (value: moment.Moment, precision?: moment.unitOfTime.StartOf) => boolean; isSelectable: (value: moment.Moment, precision?: moment.unitOfTime.StartOf) => boolean; checkValue: () => void; checkView: () => void; }; // views views: { all: ViewString[]; precisions: { [viewString: string]: moment.unitOfTime.StartOf }; formats: { [viewString: string]: string }; detectMinMax: () => void; // specific view controllers decade: IView; year: IView; month: IView; day: IView; hour: IView; minute: IView; }; // current view view: { moment: moment.Moment; value: Value; isOpen: boolean; selected: ViewString; update: () => void; toggle: () => void; open: () => void; close: () => void; position: () => void; keydown: (e: JQueryEventObject) => void; // utility unit: () => number; precision: () => moment.unitOfTime.DurationConstructor; // header title: string; previous: IViewHeaderButton; next: IViewHeaderButton; setParentView: () => void; // body render: () => void; change: (view?: ViewString) => void; }; // limits detection detectedMinView: ViewString; detectedMaxView: ViewString; // elements picker: ng.IAugmentedJQuery; container: ng.IAugmentedJQuery; input: ng.IAugmentedJQuery; } export interface IModelValidators extends ng.IModelValidators { minDate: (modelValue: moment.Moment, viewValue: string) => boolean; maxDate: (modelValue: moment.Moment, viewValue: string) => boolean; } export interface IModelController extends ng.INgModelController { $validators: IModelValidators; $modelValue: moment.Moment; $viewValue: string; } ================================================ FILE: src/directive.ts ================================================ import * as angular from 'angular'; import * as moment from 'moment'; import { getOffset } from './helpers'; import { IProviderOptions } from './provider'; import { ViewString, IView, IViewItem, IDirectiveScopeInternal, IModelController } from './definitions'; import { DecadeView, YearView, MonthView, DayView, HourView, MinuteView } from './views'; import { isValidMoment, toValue, toMoment, momentToValue, valueToMoment, setValue, updateMoment, KEYS } from './utility'; const templateHtml = require('./template.tpl.html'); export default class Directive implements ng.IDirective { public restrict = 'A'; public require = '?ngModel'; public transclude = true; public template = templateHtml; public scope = { value: '=?momentPicker', model: '=?ngModel', locale: '@?', format: '@?', minView: '@?', maxView: '@?', startView: '@?', minDate: '=?', maxDate: '=?', startDate: '=?', disabled: '=?disable', position: '@?', inline: '@?', validate: '=?', autoclose: '=?', setOnSelect: '=?', isOpen: '=?', today: '=?', keyboard: '=?', showHeader: '=?', additions: '=?', change: '&?', selectable: '&?' }; constructor( private $timeout: ng.ITimeoutService, private $sce: ng.ISCEService, private $log: ng.ILogService, private $window: ng.IWindowService, private provider: IProviderOptions, private $compile: ng.ICompileService, private $templateCache: ng.ITemplateCacheService) { } public link = ($scope: IDirectiveScopeInternal, $element: ng.IAugmentedJQuery, $attrs: ng.IAttributes, $ctrl: IModelController, $transclude: ng.ITranscludeFunction) => { $transclude(($transElement: ng.IAugmentedJQuery) => { // one-way binding attributes angular.forEach([ 'locale', 'format', 'minView', 'maxView', 'startView', 'position', 'inline', 'validate', 'autoclose', 'setOnSelect', 'today', 'keyboard', 'showHeader', 'leftArrow', 'rightArrow', 'additions' ], (attr: string) => { if (!angular.isDefined($scope[attr])) $scope[attr] = this.provider[attr]; if (!angular.isDefined($attrs[attr])) $attrs[attr] = $scope[attr]; }); // check if ngModel has been set if (!$attrs['ngModel']) $ctrl = {}; // limits $scope.limits = { minDate: toMoment($scope.minDate, $scope.format, $scope.locale), maxDate: toMoment($scope.maxDate, $scope.format, $scope.locale), isAfterOrEqualMin: (value: moment.Moment, precision?: moment.unitOfTime.StartOf) => { return !angular.isDefined($scope.limits.minDate) || value.isAfter($scope.limits.minDate, precision) || value.isSame($scope.limits.minDate, precision); }, isBeforeOrEqualMax: (value: moment.Moment, precision?: moment.unitOfTime.StartOf) => { return !angular.isDefined($scope.limits.maxDate) || value.isBefore($scope.limits.maxDate, precision) || value.isSame($scope.limits.maxDate, precision); }, isSelectable: (value: moment.Moment, precision?: moment.unitOfTime.StartOf) => { let selectable: boolean = true; try { if (angular.isFunction($scope.selectable) && $attrs['selectable']) selectable = $scope.selectable({ date: value, type: precision }); } catch (e) { this.$log.error(e); } return $scope.limits.isAfterOrEqualMin(value, precision) && $scope.limits.isBeforeOrEqualMax(value, precision) && selectable; }, checkValue: () => { if (!isValidMoment($ctrl.$modelValue) || !$scope.validate) return; if (!$scope.limits.isAfterOrEqualMin($ctrl.$modelValue)) setValue($scope.limits.minDate, $scope, $ctrl, $attrs); if (!$scope.limits.isBeforeOrEqualMax($ctrl.$modelValue)) setValue($scope.limits.maxDate, $scope, $ctrl, $attrs); }, checkView: () => { if (!angular.isDefined($scope.view.moment)) $scope.view.moment = moment().locale($scope.locale); if (!$scope.limits.isAfterOrEqualMin($scope.view.moment)) $scope.view.moment = $scope.limits.minDate.clone(); if (!$scope.limits.isBeforeOrEqualMax($scope.view.moment)) $scope.view.moment = $scope.limits.maxDate.clone(); $scope.view.update(); $scope.view.render(); } }; $scope.views = { all: ['decade', 'year', 'month', 'day', 'hour', 'minute'], precisions: { decade: 'year', year: 'month', month: 'date', day: 'hour', hour: 'minute', minute: 'second' }, // for each view, `$scope.views.formats` object contains the available moment formats // formats present in more views are used to perform min/max view detection (i.e. 'LTS', 'LT', ...) formats: { decade: 'Y{1,2}(?!Y)|YYYY|[Ll]{1,4}(?!T)', /* formats: Y,YY,YYYY,L,LL,LLL,LLLL,l,ll,lll,llll */ year: 'M{1,4}(?![Mo])|Mo|Q', /* formats: M,MM,MMM,MMM,Mo,Q */ month: '[Dd]{1,4}(?![Ddo])|DDDo|[Dd]o|[Ww]{1,2}(?![Wwo])|[Ww]o|[Ee]|L{1,2}(?!T)|l{1,2}', /* formats: D,DD,DDD,DDDD,d,dd,ddd,dddd,DDDo,Do,do,W,WW,w,ww,Wo,wo,E,e,L,LL,l,ll */ day: '[Hh]{1,2}|LTS?', /* formats: H,HH,h,hh,LT,LTS */ hour: 'm{1,2}|[Ll]{3,4}|LT(?!S)', /* formats: m,mm,LLL,LLLL,lll,llll,LT */ minute: 's{1,2}|S{1,}|X|LTS' /* formats: s,ss,S,SS,SSS..,X,LTS */ }, detectMinMax: () => { $scope.detectedMinView = $scope.detectedMaxView = undefined; if (!$scope.format) return; let minView, maxView; angular.forEach($scope.views.formats, (formats, view) => { let regexp = new RegExp('(' + formats + ')(?![^\[]*\])', 'g'); if (!$scope.format.match(regexp)) return; if (!angular.isDefined(minView)) minView = view; maxView = view; }); if (!angular.isDefined(minView)) minView = 0; else minView = Math.max(0, $scope.views.all.indexOf(minView)); if (!angular.isDefined(maxView)) maxView = $scope.views.all.length - 1; else maxView = Math.min($scope.views.all.length - 1, $scope.views.all.indexOf(maxView)); if (minView > $scope.views.all.indexOf($scope.minView)) $scope.minView = $scope.views.all[minView]; if (maxView < $scope.views.all.indexOf($scope.maxView)) $scope.maxView = $scope.views.all[maxView]; // save detected min/max view to use them to update the model value properly $scope.detectedMinView = $scope.views.all[minView]; $scope.detectedMaxView = $scope.views.all[maxView]; }, // specific views decade: new DecadeView ($scope, $ctrl, this.provider), year: new YearView ($scope, $ctrl, this.provider), month: new MonthView ($scope, $ctrl, this.provider), day: new DayView ($scope, $ctrl, this.provider), hour: new HourView ($scope, $ctrl, this.provider), minute: new MinuteView ($scope, $ctrl, this.provider), }; $scope.view = { moment: undefined, value: undefined, isOpen: false, selected: $scope.startView, update: () => { $scope.view.value = momentToValue($scope.view.moment, $scope.format); }, toggle: () => { $scope.view.isOpen ? $scope.view.close() : $scope.view.open(); }, open: () => { if ($scope.disabled || $scope.view.isOpen || $scope.inline) return; $scope.isOpen = true; $scope.view.isOpen = true; document.body.appendChild($scope.picker[0]); $scope.view.position(); }, close: () => { if (!$scope.view.isOpen || $scope.inline) return; $scope.isOpen = false; $scope.view.isOpen = false; $scope.view.selected = $scope.startView; $scope.picker[0].parentNode.removeChild($scope.picker[0]); }, position: () => { if (!$scope.view.isOpen || $scope.position || $scope.inline) return; let element = $element[0], picker = $scope.picker.children()[0], hasClassTop = $scope.picker.hasClass('top'), hasClassRight = $scope.picker.hasClass('right'), offset = getOffset($element[0]), top = offset.top - this.$window.pageYOffset, left = offset.left - this.$window.pageXOffset, winWidth = this.$window.innerWidth, winHeight = this.$window.innerHeight, shouldHaveClassTop = top + this.$window.pageYOffset - picker.offsetHeight > 0 && top > winHeight / 2, shouldHaveClassRight = left + picker.offsetWidth > winWidth, pickerTop = offset.top + (shouldHaveClassTop ? 0 : element.offsetHeight) + 'px', pickerLeft = offset.left + 'px', pickerWidth = element.offsetWidth + 'px'; if (!hasClassTop && shouldHaveClassTop) $scope.picker.addClass('top'); if (hasClassTop && !shouldHaveClassTop) $scope.picker.removeClass('top'); if (!hasClassRight && shouldHaveClassRight) $scope.picker.addClass('right'); if (hasClassRight && !shouldHaveClassRight) $scope.picker.removeClass('right'); if ($scope.picker.css('top') !== pickerTop) $scope.picker.css('top', pickerTop); if ($scope.picker.css('left') !== pickerLeft) $scope.picker.css('left', pickerLeft); if ($scope.picker.css('width') !== pickerWidth) $scope.picker.css('width', pickerWidth); }, keydown: (e) => { let view: IView = $scope.views[$scope.view.selected], precision = $scope.views.precisions[$scope.view.selected].replace('date', 'day'), singleUnit = this.provider[precision + 'sStep'] || 1, operation = [KEYS.up, KEYS.left].indexOf(e.keyCode) >= 0 ? 'subtract' : 'add', highlight = (vertical?: boolean) => { let unitMultiplier = vertical ? view.perLine : 1, nextDate = $scope.view.moment.clone()[operation](singleUnit * unitMultiplier, precision); if ($scope.limits.isSelectable(nextDate, precision)) { $scope.view.moment = nextDate; $scope.view.update(); $scope.view.render(); } }; switch (e.keyCode) { case KEYS.up: case KEYS.down: e.preventDefault(); if (!$scope.view.isOpen) $scope.view.open(); else highlight(true); break; case KEYS.left: case KEYS.right: if (!$scope.view.isOpen) break; e.preventDefault(); highlight(); break; case KEYS.enter: if (!$scope.view.isOpen) break; $scope.view.change(precision); e.preventDefault(); break; case KEYS.escape: $scope.view.toggle(); break; } $scope.$evalAsync(); }, // utility unit: () => $scope.view.selected == 'decade' ? 10 : 1, precision: () => $scope.view.selected.replace('decade', 'year'), // header title: '', previous: { label: this.$sce.trustAsHtml($scope.leftArrow), selectable: true, set: () => { if ($scope.view.previous.selectable) { $scope.view.moment.subtract($scope.view.unit(), $scope.view.precision()); $scope.view.update(); $scope.view.render(); } } }, next: { selectable: true, label: this.$sce.trustAsHtml($scope.rightArrow), set: () => { if ($scope.view.next.selectable) { $scope.view.moment.add($scope.view.unit(), $scope.view.precision()); $scope.view.update(); $scope.view.render(); } } }, setParentView: () => { $scope.view.change($scope.views.all[Math.max(0, $scope.views.all.indexOf($scope.view.selected) - 1)]); }, // body render: () => { let momentPrevious = $scope.view.moment.clone().startOf($scope.view.precision()).subtract($scope.view.unit(), $scope.view.precision()), momentNext = $scope.view.moment.clone().endOf($scope.view.precision()).add($scope.view.unit(), $scope.view.precision()); $scope.view.previous.selectable = $scope.limits.isAfterOrEqualMin(momentPrevious, $scope.view.precision()); $scope.view.previous.label = this.$sce.trustAsHtml($scope.view.previous.selectable ? $scope.leftArrow : ' '); $scope.view.next.selectable = $scope.limits.isBeforeOrEqualMax(momentNext, $scope.view.precision()); $scope.view.next.label = this.$sce.trustAsHtml($scope.view.next.selectable ? $scope.rightArrow : ' '); $scope.view.title = $scope.views[$scope.view.selected].render(); }, change: (view) => { let nextView = $scope.views.all.indexOf(view), minView = $scope.views.all.indexOf($scope.minView), maxView = $scope.views.all.indexOf($scope.maxView); const update = () => { setValue($scope.view.moment, $scope, $ctrl, $attrs); $scope.view.update(); if ($attrs['ngModel']) $ctrl.$commitViewValue(); }; if ($scope.setOnSelect) update(); if (nextView < 0 || nextView > maxView) { if (!$scope.setOnSelect) update(); if ($scope.autoclose) this.$timeout($scope.view.close); } else if (nextView >= minView) $scope.view.selected = view; } }; // creation $element.prepend($transElement); $scope.picker = angular.element($element[0].querySelectorAll('.moment-picker')); $scope.container = angular.element($scope.picker[0].querySelectorAll('.moment-picker-container')); $scope.input = $element[0].tagName.toLowerCase() != 'input' && $element[0].querySelectorAll('input').length > 0 ? angular.element($element[0].querySelectorAll('input')) : angular.element($element[0]); $scope.input.addClass('moment-picker-input').attr('tabindex', 0); ($scope.position || '').split(' ').forEach((className: string) => $scope.picker.addClass(className)); if (!$scope.inline) $scope.picker[0].parentNode.removeChild($scope.picker[0]); else { $element.after($scope.picker); $scope.picker.addClass('inline'); } // transclude scope to template additions this.$timeout(() => { angular.forEach($scope.additions || {}, (tempalteUrl: string, key: 'top' | 'bottom') => { let placeholder = angular.element($scope.container[0].querySelector('.moment-picker-addition.' + key)); let template = this.$templateCache.get(tempalteUrl); let compiled = this.$compile(template)($scope.$parent); placeholder.append(compiled); }); }); // initialization $scope.views.detectMinMax(); $scope.limits.checkView(); // model controller is initialized after linking function this.$timeout(() => { if ($attrs['ngModel']) { if (!$ctrl.$modelValue && $scope.value) $ctrl.$setViewValue($scope.value); $ctrl.$commitViewValue(); $ctrl.$render(); } else { if ($scope.value) $ctrl.$modelValue = valueToMoment($scope.value, $scope); } // view initialization if ($scope.startDate) $scope.view.moment = toMoment($scope.startDate, $scope.format, $scope.locale); else if (isValidMoment($ctrl.$modelValue)) $scope.view.moment = $ctrl.$modelValue.clone(); $scope.view.update(); $scope.view.render(); }); // model <-> view conversion if ($attrs['ngModel']) { $ctrl.$parsers.push((viewValue) => updateMoment($ctrl.$modelValue, valueToMoment(viewValue, $scope), $scope) || true); $ctrl.$formatters.push((modelValue) => momentToValue(modelValue, $scope.format) || ''); $ctrl.$viewChangeListeners.push(() => { if ($attrs['ngModel'] != $attrs['momentPicker']) $scope.value = $ctrl.$viewValue; }); $ctrl.$validators.minDate = (value) => $scope.validate || !isValidMoment(value) || $scope.limits.isAfterOrEqualMin(value); $ctrl.$validators.maxDate = (value) => $scope.validate || !isValidMoment(value) || $scope.limits.isBeforeOrEqualMax(value); } // properties listeners if ($attrs['ngModel'] != $attrs['momentPicker']) $scope.$watch('value', (newValue: string, oldValue: string) => { if (newValue !== oldValue) setValue(newValue, $scope, $ctrl, $attrs); }); $scope.$watch(() => momentToValue($ctrl.$modelValue, $scope.format), (newViewValue, oldViewValue) => { if (newViewValue == oldViewValue) return; let newModelValue = valueToMoment(newViewValue, $scope); setValue(newModelValue, $scope, $ctrl, $attrs); $scope.limits.checkValue(); $scope.view.moment = (newModelValue || moment().locale($scope.locale)).clone(); $scope.view.update(); $scope.view.render(); if (angular.isFunction($scope.change) && $attrs['change']) { let oldModelValue = valueToMoment(oldViewValue, $scope); $scope.$evalAsync(() => $scope.change({ newValue: newModelValue, oldValue: oldModelValue })); } }); $scope.$watch(() => $ctrl.$modelValue && $ctrl.$modelValue.valueOf(), () => { let viewMoment = (isValidMoment($ctrl.$modelValue) ? $ctrl.$modelValue : moment().locale($scope.locale)).clone(); if (!viewMoment.isSame($scope.view.moment)) { $scope.view.moment = viewMoment; $scope.view.update(); $scope.view.render(); } }); $scope.$watch('view.selected', () => $scope.view.render()); $scope.$watchGroup(['minView', 'maxView'], () => { // auto-detect minView/maxView $scope.views.detectMinMax(); // limit startView $scope.startView = $scope.views.all[ Math.max( Math.min( $scope.views.all.indexOf($scope.startView), $scope.views.all.indexOf($scope.maxView) ), $scope.views.all.indexOf($scope.minView) ) ]; $scope.view.selected = $scope.startView; }); $scope.$watchGroup([ () => toValue($scope.minDate, $scope.format, $scope.locale), () => toValue($scope.maxDate, $scope.format, $scope.locale) ], () => { angular.forEach(['minDate', 'maxDate'], (field: string) => { $scope.limits[field] = toMoment($scope[field], $scope.format, $scope.locale); }); $scope.limits.checkValue(); $scope.limits.checkView(); $scope.view.render(); }); $scope.$watch(() => toValue($scope.startDate, $scope.format, $scope.locale), (newViewValue, oldViewValue) => { if (newViewValue == oldViewValue) return; $scope.view.moment = valueToMoment(newViewValue, $scope); $scope.view.update(); $scope.view.render(); }); $attrs.$observe('locale', (locale: string) => $scope.locale = locale); $scope.$watch('locale', (locale: string, previous: string) => { if (!angular.isDefined(previous) || locale == previous) return; if (isValidMoment($ctrl.$modelValue)) setValue($ctrl.$modelValue.locale(locale), $scope, $ctrl, $attrs); if (isValidMoment($scope.view.moment)) $scope.view.moment = $scope.view.moment.locale(locale); if (isValidMoment($scope.limits.minDate)) $scope.limits.minDate = $scope.limits.minDate.locale(locale); if (isValidMoment($scope.limits.maxDate)) $scope.limits.maxDate = $scope.limits.maxDate.locale(locale); $scope.view.render(); }); $scope.$watch('validate', $scope.limits.checkValue); $scope.$watch('isOpen', (isOpen: boolean) => { if ($scope.inline) $scope.view.isOpen = true; else if (angular.isDefined(isOpen) && isOpen != $scope.view.isOpen) $scope.view.toggle(); }); // event listeners const focusInput = (e?: JQueryEventObject) => { if (e) e.preventDefault(); $scope.input[0].focus(); }; // use `touchstart` for iOS Safari, where click events aren't propogated under most circumstances. $scope.input .on('focus click touchstart', () => $scope.$evalAsync($scope.view.open)) .on('blur', () => $scope.$evalAsync($scope.view.close)) .on('keydown', (e) => { if ($scope.keyboard) $scope.view.keydown(e); }); $element.on('click touchstart', () => focusInput()); $scope.container.on('mousedown', (e: JQueryEventObject) => focusInput(e)); angular.element(this.$window).on('resize scroll', $scope.view.position); // unbind events on destroy $scope.$on('$destroy', () => { $scope.input.off('focus click touchstart blur keydown'); $element.off('click touchstart'); $scope.container.off('mousedown'); $scope.picker.remove(); angular.element(this.$window).off('resize scroll', $scope.view.position); }); }); } } ================================================ FILE: src/helpers.ts ================================================ /** * Offset getter method from jQuery: https://github.com/jquery/jquery/blob/3.1.1/src/offset.js#L78 */ export const getOffset = (element: HTMLElement): { top: number, left: number } => { if (!element) return; if (!element.getClientRects().length) return { top: 0, left: 0 }; // https://github.com/jquery/jquery/blob/3.1.1/src/core.js#L220 const isWindow = (obj: Window): boolean => obj != null && obj === obj.window; const getWindow = (elem: any): Window => isWindow(elem) ? elem : elem.nodeType === 9 && elem.defaultView; // tslint:disable-line:no-any let rect: ClientRect = element.getBoundingClientRect(); if (!rect.width && !rect.height) return rect; let doc: Document = element.ownerDocument; let win: Window = getWindow(doc); let docElem: HTMLElement = doc.documentElement; return { top: rect.top + win.pageYOffset - docElem.clientTop, left: rect.left + win.pageXOffset - docElem.clientLeft }; }; ================================================ FILE: src/index.less ================================================ @border-color: #f0f3f4; @text-color: #404040; @text-shadow: fade(#fff, 90%); @background: #fff; @box-shadow-color: fade(#000, 7.5%); @today-fg: #404040; @today-fg-shadow: fade(#fff, 90%); @today-bg: #e4eef5; @hover-bg-color: #fafbfb; @hover-bg-image: linear-gradient(#f0f3f4, @hover-bg-color); @disabled-fg: #abbbc7; @highlighted-bg-image: radial-gradient(transparent, fade(#000, 15%)); @selected-fg: #fff; @selected-fg-shadow: 0 -1px 0 fade(#000, 30%); @selected-bg-color: #45b1e8; @selected-bg-image: linear-gradient(#45b1e8, #3097de); @selected-border-color: #3ca0dd; .moment-picker-input { cursor: pointer; } .moment-picker { position: absolute; z-index: 1060; .moment-picker-container { color: @text-color; min-width: 15em; background: @background; padding: 4px; border: 1px solid @border-color; border-radius: 4px; position: absolute; margin-top: 4px; margin-left: -.5em; box-shadow: 0 2px 4px @box-shadow-color; &:before, &:after { content: ''; display: block; width: 0; height: 0; border: 8px solid transparent; border-top: none; position: absolute; top: -9px; left: 15px; } &:before { border-bottom-color: @border-color; border-width: 9px; } &:after { border-bottom-color: @background; margin-top: 1px; margin-left: 1px; } } // inline picker &.inline { display: block; position: relative; .moment-picker-container { position: relative; margin: 0; &:before, &:after { content: none; } } } // picker on top &.top .moment-picker-container { bottom: 100%; margin-top: auto; margin-bottom: 4px; &:before, &:after { border: 8px solid transparent; border-bottom: none; top: auto; bottom: -9px; } &:before { border-top-color: @border-color; border-width: 9px; } &:after { border-top-color: @background; margin-top: auto; margin-bottom: 1px; } } // picker on right &.right .moment-picker-container { right: 0; margin-left: auto; margin-right: -.5em; &:before, &:after { left: auto; right: 15px; } &:after { margin-left: auto; margin-right: 1px; } } // all views table { border-collapse: collapse; border-spacing: 0; min-width: 100%; table-layout: fixed; } th { font-weight: bold; &:first-child, &:last-child { width: 2em; } } th, td { padding: 0; text-align: center; min-width: 2em; height: 2em; text-shadow: 0 1px 0 @text-shadow; cursor: pointer; border-radius: 4px; &:hover { background-color: @hover-bg-color; background-image: @hover-bg-image; } &.disabled, &.disabled:hover { color: @disabled-fg; background: none; cursor: default; } } td { &.today { background: @today-bg; color: @today-fg; text-shadow: 0 1px 0 @today-fg-shadow; } &.selected { color: @selected-fg; text-shadow: @selected-fg-shadow; border-color: @selected-border-color; background-color: @selected-bg-color; background-image: @selected-bg-image; } &.highlighted { background-image: @highlighted-bg-image; } } // decade view, year view .decade-view td, .year-view td { height: 3.4em; } // month view .month-view { .moment-picker-specific-views th { background: none; cursor: default; } td { width: 1.4285714286em; } } // day view, hour view .day-view td, .hour-view td { height: 2.3333333333em; } // minute view .minute-view td { height: 1.8em; } } ================================================ FILE: src/index.ts ================================================ import * as angular from 'angular'; import Provider from './provider'; import Directive from './directive'; angular .module('moment-picker', []) .provider('momentPicker', [() => new Provider()]) .directive('momentPicker', [ '$timeout', '$sce', '$log', '$window', 'momentPicker', '$compile', '$templateCache', ($timeout: ng.ITimeoutService, $sce: ng.ISCEService, $log: ng.ILogService, $window: ng.IWindowService, momentPicker: Provider, $compile: ng.ICompileService, $templateCache: ng.ITemplateCacheService) => { return new Directive($timeout, $sce, $log, $window, momentPicker, $compile, $templateCache); } ]); export { Provider, Directive }; ================================================ FILE: src/provider.ts ================================================ import * as angular from 'angular'; import { ViewString, Position } from './definitions'; export interface IProviderOptions { locale?: string; format?: string; minView?: ViewString; maxView?: ViewString; startView?: ViewString; position?: Position; inline?: boolean; validate?: boolean; autoclose?: boolean; setOnSelect?: boolean; today?: boolean; keyboard?: boolean; showHeader?: boolean; leftArrow?: string; rightArrow?: string; // Decade View yearsFormat?: string; // Year View monthsFormat?: string; // Month View daysFormat?: string; // Day View hoursFormat?: string; hoursStart?: number; hoursEnd?: number; // Hour View minutesFormat?: string; minutesStep?: number; minutesStart?: number; minutesEnd?: number; // Minute View secondsFormat?: string; secondsStep?: number; secondsStart?: number; secondsEnd?: number; } export default class Provider implements angular.IServiceProvider { private settings: IProviderOptions = { locale: 'en', format: 'L LTS', minView: 'decade', maxView: 'minute', startView: 'year', inline: false, validate: true, autoclose: true, setOnSelect: false, today: false, keyboard: false, showHeader: true, leftArrow: '←', rightArrow: '→', // Decade View yearsFormat: 'YYYY', // Year View monthsFormat: 'MMM', // Month View daysFormat: 'D', // Day View hoursFormat: 'HH:[00]', hoursStart: 0, hoursEnd: 23, // Hour View minutesStep: 5, minutesStart: 0, minutesEnd: 59, // Minute View secondsFormat: 'ss', secondsStep: 1, secondsStart: 0, secondsEnd: 59 }; public options(options: IProviderOptions): IProviderOptions { angular.extend(this.settings, options); return angular.copy(this.settings); } public $get(): IProviderOptions { return this.settings; } } ================================================ FILE: src/template.tpl.html ================================================
================================================ FILE: src/themes/material-ui.less ================================================ @cell-big-size: 60px; @cell-mid-size: 50px; @cell-def-size: 40px; @cell-min-size: 33px; @bg-gray: #eee; @bg-dark-gray: #e0e0e0; .moment-picker { .moment-picker-container { text-shadow: none; border-radius: 0; padding: 0; } .header-view { border: 1px solid #fff; th { background: @bg-gray; border-radius: 0; min-width: @cell-def-size; height: @cell-def-size; } } .header-view th, td { &:hover { background-color: @bg-dark-gray; } } td { border-radius: @cell-big-size; } .moment-picker-specific-views { padding: 4px 8px; table { border-collapse: separate; border-spacing: 3px; } th { background: none; cursor: default; } } // specific view - custom sizes .decade-view, .year-view { td { min-width: @cell-big-size; height: @cell-big-size; } } .month-view { td { min-width: @cell-min-size; height: @cell-min-size; } } .day-view, .hour-view { td { min-width: @cell-big-size; height: @cell-def-size; } } .minute-view { table { border-spacing: 2px; } td { min-width: @cell-def-size; height: @cell-def-size; } } } ================================================ FILE: src/utility.ts ================================================ import * as angular from 'angular'; import * as moment from 'moment'; import { Value, IDirectiveScopeInternal, IModelController, ViewString } from './definitions'; export const KEYS = { up: 38, down: 40, left: 37, right: 39, escape: 27, enter: 13 }; export const isValidMoment = (value: moment.Moment | Value): boolean => { return moment.isMoment(value) && value.isValid(); }; export const toValue = (date: moment.Moment | Value, format: string, locale: string): Value => { let momentDate = date; if (!isValidMoment(date)) momentDate = toMoment(date, format, locale); return momentToValue(momentDate, format); }; export const toMoment = (date: moment.Moment | Value, format: string, locale: string): moment.Moment => { let momentDate = moment(date, format, locale); if (!isValidMoment(momentDate)) momentDate = undefined; return momentDate; }; export const momentToValue = (momentObject: moment.Moment, format: string): Value => { if (!isValidMoment(momentObject)) return undefined; return !format ? momentObject.valueOf() : momentObject.format(format); }; export const valueToMoment = (formattedValue: Value, $scope: IDirectiveScopeInternal): moment.Moment => { let momentValue: moment.Moment; if (!formattedValue) return momentValue; if (!$scope.format) momentValue = moment(formattedValue); else momentValue = moment(formattedValue, $scope.format, $scope.locale); if ($scope.model) { // set value for each view precision (from Decade View to minView) const views = $scope.views.all.slice(0, $scope.views.all.indexOf($scope.detectedMinView)); angular.forEach(views, (view: ViewString) => { const precision = $scope.views.precisions[view]; momentValue[precision]($scope.model[precision]()); }); } return momentValue; }; export const setValue = (value: moment.Moment | Value, $scope: IDirectiveScopeInternal, $ctrl: IModelController, $attrs: ng.IAttributes): void => { let modelValue = isValidMoment(value) ? (value).clone() : valueToMoment(value, $scope), viewValue = momentToValue(modelValue, $scope.format); $scope.model = updateMoment($scope.model, modelValue, $scope); $ctrl.$modelValue = updateMoment($ctrl.$modelValue, modelValue, $scope); if ($attrs['ngModel'] != $attrs['momentPicker']) $scope.value = viewValue; if ($attrs['ngModel']) { $ctrl.$setViewValue(viewValue); $ctrl.$render(); // render input value } }; export const updateMoment = (model: moment.Moment, value: moment.Moment, $scope: IDirectiveScopeInternal): moment.Moment => { if (!isValidMoment(model) || !value) model = value; else { if (!model.isSame(value)) { // set value for each view precision (from Decade View to maxView) const views = $scope.views.all.slice(0, $scope.views.all.indexOf($scope.detectedMaxView) + 1); angular.forEach(views, (view: ViewString) => { const precision = $scope.views.precisions[view]; model[precision](value[precision]()); }); } } return model; }; ================================================ FILE: src/views/dayView.ts ================================================ import { IView, IViewItem, IDirectiveScopeInternal, IModelController } from '../definitions'; import { IProviderOptions } from '../provider'; import { isValidMoment } from '../utility'; export default class DayView implements IView { public perLine: number = 4; public rows: { [index: number]: IViewItem[] } = {}; constructor( private $scope: IDirectiveScopeInternal, private $ctrl: IModelController, private provider: IProviderOptions) { } public render(): string { let hour = this.$scope.view.moment.clone().startOf('day').hour(this.provider.hoursStart); this.rows = {}; for (let h = 0; h <= this.provider.hoursEnd - this.provider.hoursStart; h++) { let index = Math.floor(h / this.perLine), selectable = this.$scope.limits.isSelectable(hour, 'hour'); if (!this.rows[index]) this.rows[index] = []; this.rows[index].push({ index: h, // this is to prevent DST conflicts label: hour.format(this.provider.hoursFormat), year: hour.year(), month: hour.month(), date: hour.date(), hour: hour.hour(), class: [ this.$scope.keyboard && hour.isSame(this.$scope.view.moment, 'hour') ? 'highlighted' : '', !selectable ? 'disabled' : isValidMoment(this.$ctrl.$modelValue) && hour.isSame(this.$ctrl.$modelValue, 'hour') ? 'selected' : '' ].join(' ').trim(), selectable: selectable }); hour.add(1, 'hours'); } // return title return this.$scope.view.moment.format('LL'); } public set(hour: IViewItem): void { if (!hour.selectable) return; this.$scope.view.moment.year(hour.year).month(hour.month).date(hour.date).hour(hour.hour); this.$scope.view.update(); this.$scope.view.change('hour'); } } ================================================ FILE: src/views/decadeView.ts ================================================ import { IView, IViewItem, IDirectiveScopeInternal, IModelController } from '../definitions'; import { IProviderOptions } from '../provider'; import { isValidMoment } from '../utility'; export default class DecadeView implements IView { public perLine: number = 4; public rows: { [index: number]: IViewItem[] } = {}; constructor( private $scope: IDirectiveScopeInternal, private $ctrl: IModelController, private provider: IProviderOptions) { } public render(): string { let year = this.$scope.view.moment.clone(), firstYear = Math.floor(year.year() / 10) * 10 - 1; this.rows = {}; year.year(firstYear); for (let y = 0; y < 12; y++) { let index = Math.floor(y / this.perLine), selectable = this.$scope.limits.isSelectable(year, 'year'); if (!this.rows[index]) this.rows[index] = []; this.rows[index].push({ index: year.year(), label: year.format(this.provider.yearsFormat), year: year.year(), class: [ this.$scope.keyboard && year.isSame(this.$scope.view.moment, 'year') ? 'highlighted' : '', !selectable || [0, 11].indexOf(y) >= 0 ? 'disabled' : isValidMoment(this.$ctrl.$modelValue) && year.isSame(this.$ctrl.$modelValue, 'year') ? 'selected' : '' ].join(' ').trim(), selectable: selectable }); year.add(1, 'years'); } // return title return [year.subtract(2, 'years').format('YYYY'), year.subtract(9, 'years').format('YYYY')].reverse().join(' - '); } public set(year: IViewItem): void { if (!year.selectable) return; this.$scope.view.moment.year(year.year); this.$scope.view.update(); this.$scope.view.change('year'); } } ================================================ FILE: src/views/hourView.ts ================================================ import * as angular from 'angular'; import * as moment from 'moment'; import { IView, IViewItem, IDirectiveScopeInternal, IModelController } from '../definitions'; import { IProviderOptions } from '../provider'; import { isValidMoment } from '../utility'; export default class HourView implements IView { public perLine: number = 4; public rows: { [index: number]: IViewItem[] } = {}; constructor( private $scope: IDirectiveScopeInternal, private $ctrl: IModelController, private provider: IProviderOptions) { } public render(): string { let i = 0, minute = this.$scope.view.moment.clone().startOf('hour').minute(this.provider.minutesStart), minutesFormat = this.provider.minutesFormat || moment.localeData(this.$scope.locale).longDateFormat('LT').replace(/[aA]/, '').trim(); this.rows = {}; for (let m = 0; m <= this.provider.minutesEnd - this.provider.minutesStart; m += this.provider.minutesStep) { let index = Math.floor(i / this.perLine), selectable = this.$scope.limits.isSelectable(minute, 'minute'); if (!this.rows[index]) this.rows[index] = []; this.rows[index].push({ index: minute.minute(), label: minute.format(minutesFormat), year: minute.year(), month: minute.month(), date: minute.date(), hour: minute.hour(), minute: minute.minute(), class: [ this.$scope.keyboard && minute.isSame(this.$scope.view.moment, 'minute') ? 'highlighted' : '', !selectable ? 'disabled' : isValidMoment(this.$ctrl.$modelValue) && minute.isSame(this.$ctrl.$modelValue, 'minute') ? 'selected' : '' ].join(' ').trim(), selectable: selectable }); i++; minute.add(this.provider.minutesStep, 'minutes'); } if (this.$scope.keyboard) this.highlightClosest(); // return title return this.$scope.view.moment.clone().startOf('hour').format('lll'); } public set(minute: IViewItem): void { if (!minute.selectable) return; this.$scope.view.moment.year(minute.year).month(minute.month).date(minute.date).hour(minute.hour).minute(minute.minute); this.$scope.view.update(); this.$scope.view.change('minute'); } public highlightClosest(): void { let minutes = [], minute; angular.forEach(this.rows, (row) => { angular.forEach(row, (value) => { if (Math.abs(value.minute - this.$scope.view.moment.minute()) < this.provider.minutesStep) minutes.push(value); }); }); minute = minutes.sort((value1, value2) => { return Math.abs(value1.minute - this.$scope.view.moment.minute()) > Math.abs(value2.minute - this.$scope.view.moment.minute()) ? 1 : 0; })[0]; if (!minute || minute.minute - this.$scope.view.moment.minute() == 0) return; this.$scope.view.moment.year(minute.year).month(minute.month).date(minute.date).hour(minute.hour).minute(minute.minute); this.$scope.view.update(); if (minute.selectable) minute.class = (minute.class + ' highlighted').trim(); } } ================================================ FILE: src/views/index.ts ================================================ import DecadeView from './decadeView'; import YearView from './yearView'; import MonthView from './monthView'; import DayView from './dayView'; import HourView from './hourView'; import MinuteView from './minuteView'; export { DecadeView, YearView, MonthView, DayView, HourView, MinuteView }; ================================================ FILE: src/views/minuteView.ts ================================================ import * as angular from 'angular'; import { IView, IViewItem, IDirectiveScopeInternal, IModelController } from '../definitions'; import { IProviderOptions } from '../provider'; import { isValidMoment } from '../utility'; export default class MinuteView implements IView { public perLine: number = 6; public rows: { [index: number]: IViewItem[] } = {}; constructor( private $scope: IDirectiveScopeInternal, private $ctrl: IModelController, private provider: IProviderOptions) { } public render(): string { let i = 0, second = this.$scope.view.moment.clone().startOf('minute').second(this.provider.secondsStart); this.rows = {}; for (let s = 0; s <= this.provider.secondsEnd - this.provider.secondsStart; s += this.provider.secondsStep) { let index = Math.floor(i / this.perLine), selectable = this.$scope.limits.isSelectable(second, 'second'); if (!this.rows[index]) this.rows[index] = []; this.rows[index].push({ index: second.second(), label: second.format(this.provider.secondsFormat), year: second.year(), month: second.month(), date: second.date(), hour: second.hour(), minute: second.minute(), second: second.second(), class: [ this.$scope.keyboard && second.isSame(this.$scope.view.moment, 'second') ? 'highlighted' : '', !selectable ? 'disabled' : isValidMoment(this.$ctrl.$modelValue) && second.isSame(this.$ctrl.$modelValue, 'second') ? 'selected' : '' ].join(' ').trim(), selectable: selectable }); i++; second.add(this.provider.secondsStep, 'seconds'); } if (this.$scope.keyboard) this.highlightClosest(); // return title return this.$scope.view.moment.clone().startOf('minute').format('lll'); } public set(second: IViewItem): void { if (!second.selectable) return; this.$scope.view.moment.year(second.year).month(second.month).date(second.date).hour(second.hour).minute(second.minute).second(second.second); this.$scope.view.update(); this.$scope.view.change(); } public highlightClosest(): void { let seconds = [], second; angular.forEach(this.rows, (row) => { angular.forEach(row, (value) => { if (Math.abs(value.second - this.$scope.view.moment.second()) < this.provider.secondsStep) seconds.push(value); }); }); second = seconds.sort((value1, value2) => { return Math.abs(value1.second - this.$scope.view.moment.second()) > Math.abs(value2.second - this.$scope.view.moment.second()) ? 1 : 0; })[0]; if (!second || second.second - this.$scope.view.moment.second() == 0) return; this.$scope.view.moment.year(second.year).month(second.month).date(second.date).hour(second.hour).minute(second.minute).second(second.second); this.$scope.view.update(); if (second.selectable) second.class = (second.class + ' highlighted').trim(); } } ================================================ FILE: src/views/monthView.ts ================================================ import * as angular from 'angular'; import * as moment from 'moment'; import { IView, IViewItem, IDirectiveScopeInternal, IModelController } from '../definitions'; import { IProviderOptions } from '../provider'; import { isValidMoment } from '../utility'; export default class MonthView implements IView { public perLine: number = moment.weekdays().length; public rows: { [index: number]: IViewItem[] } = []; public headers: string[]; constructor( private $scope: IDirectiveScopeInternal, private $ctrl: IModelController, private provider: IProviderOptions) { } public render(): string { let month: number = this.$scope.view.moment.month(), day: moment.Moment = this.$scope.view.moment.clone().startOf('month').startOf('week').hour(12), rows: { [week: number]: IViewItem[] } = {}, firstWeek: number = day.week(), lastWeek: number = firstWeek + 5; this.rows = []; for (let week = firstWeek; week <= lastWeek; week++) rows[week] = Array.apply(null, Array(this.perLine)).map(() => { let selectable = this.$scope.limits.isSelectable(day, 'day'); let date = { index: day.date(), label: day.format(this.provider.daysFormat), year: day.year(), month: day.month(), date: day.date(), class: [ this.$scope.keyboard && day.isSame(this.$scope.view.moment, 'day') ? 'highlighted' : '', !!this.$scope.today && day.isSame(new Date(), 'day') ? 'today' : '', !selectable || day.month() != month ? 'disabled' : isValidMoment(this.$ctrl.$modelValue) && day.isSame(this.$ctrl.$modelValue, 'day') ? 'selected' : '' ].join(' ').trim(), selectable: selectable }; day.add(1, 'days'); return date; }); // object to array - see https://github.com/indrimuska/angular-moment-picker/issues/9 angular.forEach(rows, (row: IViewItem[]) => (this.rows).push(row)); // render headers this.headers = moment.weekdays().map((d: string, i: number) => moment().locale(this.$scope.locale).startOf('week').add(i, 'day').format('dd')); // return title return this.$scope.view.moment.format('MMMM YYYY'); } public set(day: IViewItem): void { if (!day.selectable) return; this.$scope.view.moment.year(day.year).month(day.month).date(day.date); this.$scope.view.update(); this.$scope.view.change('day'); } } ================================================ FILE: src/views/yearView.ts ================================================ import * as moment from 'moment'; import { IView, IViewItem, IDirectiveScopeInternal, IModelController } from '../definitions'; import { IProviderOptions } from '../provider'; import { isValidMoment } from '../utility'; class YearView implements IView { public perLine: number = 4; public rows: { [index: number]: IViewItem[] } = {}; constructor( private $scope: IDirectiveScopeInternal, private $ctrl: IModelController, private provider: IProviderOptions) { } public render(): string { let month = this.$scope.view.moment.clone().startOf('year'), months = moment.monthsShort(); this.rows = {}; months.forEach((label, i) => { let index = Math.floor(i / this.perLine), selectable = this.$scope.limits.isSelectable(month, 'month'); if (!this.rows[index]) this.rows[index] = []; this.rows[index].push({ index: month.month(), label: month.format(this.provider.monthsFormat), year: month.year(), month: month.month(), class: [ this.$scope.keyboard && month.isSame(this.$scope.view.moment, 'month') ? 'highlighted' : '', !selectable ? 'disabled' : isValidMoment(this.$ctrl.$modelValue) && month.isSame(this.$ctrl.$modelValue, 'month') ? 'selected' : '' ].join(' ').trim(), selectable: selectable }); month.add(1, 'months'); }); // return title return this.$scope.view.moment.format('YYYY'); } public set(month: IViewItem): void { if (!month.selectable) return; this.$scope.view.moment.year(month.year).month(month.month); this.$scope.view.update(); this.$scope.view.change('month'); } } export default YearView; ================================================ FILE: tests/elementCreation.ts ================================================ import * as angular from 'angular'; import * as test from './utility'; describe('Element creation', () => { // init test test.bootstrap(); // check transcluded DIV content it('should transclude DIV content', () => { let $element = test.buildTemplate('div', { class: 'my-content' }, 'My content'); expect($element.length).toEqual(1); expect(angular.element($element[0]).hasClass('my-content')).toBe(true); expect(angular.element($element[0]).text()).toEqual('My content'); }); // check transcluded INPUT content it('should transclude INPUT content', () => { let $element = test.buildTemplate('input', { class: 'my-content' }); expect($element.length).toEqual(1); expect(angular.element($element[0]).hasClass('my-content')).toBe(true); }); }); ================================================ FILE: tests/hacks.ts ================================================ // extending jQLite angular.element.prototype.find = function (query: string) { // tslint:disable-line:only-arrow-functions return angular.element(this[0].querySelectorAll(query)); }; // fix PhantomJS missing blur-on-clickout features let lastFocused; angular.element(document.body).on('click', (event) => { if (lastFocused) lastFocused.trigger('blur'); lastFocused = angular.element(event.target); }); ================================================ FILE: tests/openClosePicker.ts ================================================ import * as angular from 'angular'; import * as test from './utility'; describe('Open / close picker', () => { let $inputContent: ng.IAugmentedJQuery; let $divContent: ng.IAugmentedJQuery; // init test test.bootstrap(); // create two pickers for all tests beforeEach(() => { // tslint:disable-next-line:no-unused-expression $inputContent = test.buildTemplate('input', { class: 'input-picker' }); $divContent = test.buildTemplate('div', { class: 'div-picker' }); }); const isVisible = ($element: ng.IAugmentedJQuery) => test.getPicker($element).is(':visible'); // open picker on click it('should open the picker on click', () => { test.trigger($inputContent, 'click'); expect(isVisible($inputContent)).toBe(true); test.trigger($divContent, 'click'); expect(isVisible($divContent)).toBe(true); }); // open picker on focus // it('should open the picker on focus', () => { // test.trigger($inputContent, 'focus'); // expect(isVisible($inputContent)).toBe(true); // test.trigger($divContent, 'focus'); // expect(isVisible($divContent)).toBe(true); // }); // close picker on blur it('should close the picker on blur', () => { test.trigger($inputContent, 'click'); expect(isVisible($inputContent)).toBe(true); test.trigger($inputContent, 'blur'); expect(isVisible($inputContent)).toBe(false); test.trigger($divContent, 'click'); expect(isVisible($divContent)).toBe(true); test.trigger($divContent, 'blur'); expect(isVisible($divContent)).toBe(false); }); // close picker clicking on another one it('should close a picker when clicking to another picker', () => { test.trigger($inputContent, 'click'); expect(isVisible($inputContent)).toBe(true); test.trigger($divContent, 'click'); expect(isVisible($divContent)).toBe(true); expect(isVisible($inputContent)).toBe(false); }); }); ================================================ FILE: tests/properties/isOpen.ts ================================================ import * as angular from 'angular'; import * as test from '../utility'; describe('Property `isOpen`', () => { let $rootScope: ng.IRootScopeService; // init test test.bootstrap(); // get $rootScope service beforeEach(inject((_$rootScope_: ng.IRootScopeService) => { // tslint:disable-line:variable-name // The injector unwraps the underscores (_) from around the parameter names when matching $rootScope = _$rootScope_; })); const isVisible = ($element: ng.IAugmentedJQuery) => test.getPicker($element).is(':visible'); it('should open the picker when set to `true`', () => { let $picker = test.buildTemplate('input', { isOpen: true }); expect(isVisible($picker)).toBe(true); }); it('should close the picker when set to `false`', () => { let $picker = test.buildTemplate('input', { isOpen: false }); expect(isVisible($picker)).toBe(false); }); it('should open and close the picker again when toggling value', () => { let $scope = $rootScope.$new(), $picker = test.buildTemplate('input', { isOpen: 'isOpen' }, undefined, $scope); // value to toggle [true, false, true].forEach((value: boolean) => { $scope['isOpen'] = value; $scope.$digest(); expect(isVisible($picker)).toBe(value); }); }); }); ================================================ FILE: tests/properties/keyboard.ts ================================================ import * as angular from 'angular'; import * as moment from 'moment'; import * as test from '../utility'; import { IProviderOptions } from '../../src/provider'; import { ViewString, IView } from '../../src/definitions'; import { KEYS } from '../../src/utility'; import * as views from '../../src/views'; describe('Keyboard', () => { // init test test.bootstrap(); // available keyboard keys type MockKeyboardKeys = 'up' | 'down' | 'left' | 'right' | 'enter' | 'escape' | 'a' | 'b' | 'c'; KEYS['a'] = 65; KEYS['b'] = 66; KEYS['c'] = 67; // create an event object for each key to test const EVENTS: { [name: string]: () => JQueryEventObject } = {}; angular.forEach(KEYS, (code: number, key: MockKeyboardKeys) => { EVENTS[key] = () => $.Event('keydown', { keyCode: code }); }); /** * Utility function to trigger a custom event to a picker input field. * It first focus on the input to ensure the key is sent from the input field. */ const sendKey = ($input: ng.IAugmentedJQuery, key: MockKeyboardKeys): JQueryEventObject => { // get input focus first test.trigger($input, 'focus'); // get a fresh event let event = EVENTS[key](); // and trigger it! test.trigger($input, event); return event; }; // preventDefault() describe('default prevented event', () => { let $input: ng.IAugmentedJQuery; beforeEach(() => { $input = test.buildTemplate('input', { keyboard: 'true' }); }); // prevent default event ['up', 'down', 'left', 'right', 'enter'].forEach((key: MockKeyboardKeys) => { it('should be set for ' + key.toUpperCase() + ' key', () => { let event = sendKey($input, key); expect(event.isDefaultPrevented()).toBe(true); }); }); // do not prevent default event it('should be set for all the other keys', () => { ['escape', 'a', 'b', 'c'].forEach((key: MockKeyboardKeys) => { let event = sendKey($input, key); expect(event.isDefaultPrevented()).toBe(false); }); }); }); describe('picker open/close', () => { let $input: ng.IAugmentedJQuery; const isOpen = () => test.getPicker($input).is(':visible'); beforeEach(inject(($rootScope) => { $input = test.buildTemplate('input', { keyboard: 'true' }); })); // close picker on pressing ESC it('should close the picker after pressing ESC key', () => { // focus on input test.trigger($input, 'focus'); // press ESC to close the picker (without clicking on the input first) test.trigger($input, EVENTS['escape']()); // check if the picker is closed expect(isOpen()).toBe(false); }); // open picker after pressing UP or DOWN key ['up', 'down'].forEach((key: MockKeyboardKeys) => { it('should open the picker after pressing ' + key.toUpperCase() + ' key', () => { // focus on input and close the picker sendKey($input, 'escape'); // send key to be tested test.trigger($input, EVENTS[key]()); // check picker opening expect(isOpen()).toBe(true); }); }); }); describe('navigation', () => { let locale = 'en'; let $scope: ng.IScope; let formats: { [name: string]: string } = {}; let date = moment('2017-12-20 15:27:55', 'YYYY-MM-DD HH:mm:ss'); let previousSettings: IProviderOptions; const pickerViews = ['decade', 'year', 'month', 'day', 'hour', 'minute']; const commonOpts = { keyboard: 'true', ngModel: 'date', format: 'YYYY-MM-DD HH:mm:ss', class: 'input-picker', locale: locale }; const getHighlightedText = ($element: ng.IAugmentedJQuery) => test.getPicker($element).find('td.highlighted').text(); // get formats from momentPickerProvider beforeEach(inject(($rootScope: ng.IRootScopeService, momentPicker: IProviderOptions) => { // tslint:disable-line:variable-name $scope = $rootScope.$new(); $scope['date'] = date; // get provider options previousSettings = momentPicker; formats['decade'] = momentPicker.yearsFormat; formats['year'] = momentPicker.monthsFormat; formats['month'] = momentPicker.daysFormat; formats['day'] = momentPicker.hoursFormat; formats['hour'] = momentPicker.minutesFormat || moment.localeData(locale).longDateFormat('LT').replace(/[aA]/, '').trim(); formats['minute'] = momentPicker.secondsFormat; // set steps on HourView momentPicker.minutesStep = 1; momentPicker.secondsStep = 1; })); // after each tests reset momentPickerProvider afterEach(inject((momentPicker: IProviderOptions) => { momentPicker = previousSettings; })); pickerViews.forEach((view: ViewString, index: number) => { const viewPrecision = (index === pickerViews.length - 1 ? 'seconds' : pickerViews[index + 1]); const viewMultiplier = view == 'decade' ? 10 : 1; const itemsPerLine = { decade: 4, year: 4, month: 7, day: 4, hour: 4, minute: 6 }[view]; const keysOperations = { up: 'subtract', down: 'add', left: 'subtract', right: 'add' }; // highlight on open it('should highlight the selected ' + view + ' on picker open', () => { let options = angular.extend({ startView: view }, commonOpts), $input = test.buildTemplate('input', options, undefined, $scope); expect(getHighlightedText($input)).toBe(date.format(formats[view])); }); // highlight on key press angular.forEach(keysOperations, (operation: 'add' | 'subtract', key: MockKeyboardKeys) => { const datesToShift = key == 'left' || key == 'right' ? 1 : itemsPerLine; const title = 'should highlight ' + (keysOperations[key] == 'add' ? 'next' : 'previous') + ' ' + datesToShift + ' ' + viewPrecision + (datesToShift != 1 ? 's' : '') + ' after pressing ' + key.toUpperCase() + ' key'; it(title, () => { let options = angular.extend({ startView: view }, commonOpts), $input = test.buildTemplate('input', options, undefined, $scope), finalDate = date.clone()[operation](datesToShift, viewPrecision); sendKey($input, key); expect(getHighlightedText($input)).toBe(finalDate.format(formats[view])); }); }); }); }); }); ================================================ FILE: tests/properties/locale.ts ================================================ import * as angular from 'angular'; import * as moment from 'moment'; import * as test from '../utility'; describe('Property `locale`', () => { let $rootScope: ng.IRootScopeService; // init test test.bootstrap(); // get $rootScope service beforeEach(inject((_$rootScope_: ng.IRootScopeService) => { // tslint:disable-line:variable-name // The injector unwraps the underscores (_) from around the parameter names when matching $rootScope = _$rootScope_; })); describe('Picker labels', () => { let format = 'YYYY-MM-DD HH:mm:ss', startDate = moment(), stringDivider = '|', expectedHeaders = { month: {}, day: {}, hour: {} }; // build expected headers ['en', 'it', 'fr', 'bn'].forEach((locale: string) => { let localeDate = startDate.clone().locale(locale); const getWeekDays = () => moment.weekdays().map((day: string, i: number) => localeDate.clone().startOf('week').add(i, 'day').format('dd')); expectedHeaders.month[locale] = [localeDate.format('MMMM YYYY')].concat(getWeekDays()).join(stringDivider); expectedHeaders.day [locale] = localeDate.format('LL'); expectedHeaders.hour [locale] = localeDate.startOf('hour').format('lll'); }); const getHeader = ($element: ng.IAugmentedJQuery) => Array.prototype.slice.call(test.getPicker($element).find('th'), 0) .map((e: Node) => e.textContent) .filter((s: string) => s != '←' && s != '→') .join(stringDivider); it('should change locale dinamically', () => { angular.forEach(expectedHeaders, (header, view: string) => { let $scope = $rootScope.$new(), $input = test.buildTemplate('input', { locale: '\{\{locale\}\}', format: format, startView: view, startDate: startDate }, undefined, $scope); angular.forEach(header, (expectedHeader: string, locale: string) => { $scope['locale'] = locale; $scope.$apply(); expect(getHeader($input)).toBe(expectedHeader); }); }); }); }); }); ================================================ FILE: tests/properties/startDate.ts ================================================ import * as angular from 'angular'; import * as moment from 'moment'; import * as test from '../utility'; describe('Property `startDate`', () => { let format = 'YYYY-MM-DD HH:mm:ss', startDate = moment('2005-05-15 13:20:15', format), startDateStr = '\'' + startDate.format(format) + '\'', expectedHeaders = { decade: '2000 - 2009', year: '2005', month: 'May 2005', day: 'May 15, 2005', hour: 'May 15, 2005 1:00 PM', minute: 'May 15, 2005 1:20 PM' }; // init test test.bootstrap(); const getHeaderText = ($element: ng.IAugmentedJQuery) => angular.element(test.getPicker($element).find('.header-view th')[1]).text(); // test all views angular.forEach(expectedHeaders, (expectedHeader, view) => { let viewName = view[0].toUpperCase() + view.slice(1), title = 'should open ' + viewName + ' View in ' + expectedHeader; it(title, () => { let $propAsMoment = test.buildTemplate('div', { locale: 'en', format: format, startView: view, startDate: startDate }), $propAsString = test.buildTemplate('div', { locale: 'en', format: format, startView: view, startDate: startDateStr }); // Moment object input expect(getHeaderText($propAsMoment)).toBe(expectedHeader); // string input expect(getHeaderText($propAsString)).toBe(expectedHeader); }); }); }); ================================================ FILE: tests/properties/validate.ts ================================================ import * as angular from 'angular'; import * as moment from 'moment'; import * as test from '../utility'; describe('Property `validate`', () => { let $rootScope: ng.IRootScopeService; // init test test.bootstrap(); // get $rootScope service beforeEach(inject((_$rootScope_: ng.IRootScopeService) => { // tslint:disable-line:variable-name // The injector unwraps the underscores (_) from around the parameter names when matching $rootScope = _$rootScope_; })); const buildFormScope = (options) => { let $scope = $rootScope.$new(), $form = angular.element('
').appendTo(document.body), $input = test.buildTemplate('input', options, undefined, $scope, $form); return $scope; }; describe('set to false', () => { // required it('should raise `required` validation error', () => { let $scope = buildFormScope({ name: 'date', validate: 'false', required: 'true' }); expect($scope['form'].date.$error.required).toBe(true); }); // minDate it('should raise `minDate` validation error', () => { let format = 'YYYY-MM-DD', ngModel = moment('2016-12-15', format), minDate = ngModel.clone().add(1, 'month'), $scope = buildFormScope({ name: 'date', validate: 'false', format: format, ngModel: ngModel, minDate: minDate }); expect($scope['form'].date.$error.minDate).toBe(true); }); // maxDate it('should raise `maxDate` validation error', () => { let format = 'YYYY-MM-DD', ngModel = moment('2016-12-15', format), maxDate = ngModel.clone().subtract(1, 'month'), $scope = buildFormScope({ name: 'date', validate: 'false', format: format, ngModel: ngModel, maxDate: maxDate }); expect($scope['form'].date.$error.maxDate).toBe(true); }); }); describe('set to true', () => { // required it('should raise `required` validation error', () => { let $scope = buildFormScope({ name: 'date', validate: 'true', required: 'true' }); expect($scope['form'].date.$error.required).toBe(true); }); // minDate it('should set date to minimum allowable value', () => { let format = 'YYYY-MM-DD', ngModel = moment('2016-12-15', format), minDate = ngModel.clone().add(1, 'month'), $scope = buildFormScope({ validate: 'true', format: format, ngModel: ngModel, minDate: minDate }); expect($scope['ngModel'].isSame(minDate)).toBe(true); }); // maxDate it('should set date to maximum allowable value', () => { let format = 'YYYY-MM-DD', ngModel = moment('2016-12-15', format), maxDate = ngModel.clone().subtract(1, 'month'), $scope = buildFormScope({ validate: 'true', format: format, ngModel: ngModel, maxDate: maxDate }); expect($scope['ngModel'].isSame(maxDate)).toBe(true); }); }); }); ================================================ FILE: tests/utility.ts ================================================ import * as angular from 'angular'; import { IDirectiveScopeInternal } from '../src/definitions'; let $compile, $timeout, $rootScope; export const bootstrap = (): any => { // tslint:disable-line:no-any // load the moment-picker module, which contains the directive beforeEach(angular.mock.module('moment-picker')); // store references to $rootScope and $compile // so they are available to all tests in this describe block beforeEach(inject(( _$compile_: ng.ICompileService, // tslint:disable-line:variable-name _$timeout_: ng.ITimeoutService, // tslint:disable-line:variable-name _$rootScope_: ng.IRootScopeService, // tslint:disable-line:variable-name ) => { // The injector unwraps the underscores (_) from around the parameter names when matching $compile = _$compile_; $timeout = _$timeout_; $rootScope = _$rootScope_; })); }; export const $digest = () => $rootScope.$digest(); export const buildTemplate = (tag: string, options?: any, content?: any, $scope?: ng.IScope, $parent?: ng.IAugmentedJQuery) => { // tslint:disable-line:no-any let $template, $container, $element; if (!$scope) $scope = $rootScope.$new(); tag = tag.toLowerCase(); // template string let template = '<' + tag; // build attributes map if (!options) options = {}; if (!options.momentPicker) options.momentPicker = 'mpTestFormattedString'; if (tag === 'input' && !options.ngModel) options.ngModel = 'mpTestMomentObject'; angular.forEach(options, (value, name) => { let valueStr = name; if (typeof value === 'string') valueStr = value; else $scope[name] = value; template += ' ' + name.replace(/([A-Z])/g, '-$1').toLowerCase() + '="' + valueStr + '"'; }); // close template template += tag === 'input' ? '>' : '>'; $template = angular.element(template); if (tag !== 'input') { if (content) $template.append(content); else $template.append('{{' + options.momentPicker + '}}'); } // append template to DOM $container = angular.element('
').append($template); angular.element($parent || document.body).append($container); // return compiled element $compile($parent || $container)($scope); $element = angular.element($container.children()[0]); $digest(); $timeout.flush(); return $element; }; export const getPicker = (element: ng.IAugmentedJQuery) => (element.isolateScope()).picker; // wrap jquery trigger fn: event trigger + digest stimulation export const trigger = (element: ng.IAugmentedJQuery, event: string | JQueryEventObject) => { // use jquey trigger method to propagate event to parent nodes angular.element(element).trigger(event); $digest(); }; ================================================ FILE: tests/value.ts ================================================ import * as moment from 'moment'; import * as test from './utility'; describe('Value', () => { let $scope: ng.IScope; let $input: ng.IAugmentedJQuery; let format = 'YYYY-MM-DD'; // init test test.bootstrap(); // same picker settings for all tests in this suite beforeEach(inject(($rootScope: ng.IRootScopeService) => { $scope = $rootScope.$new(); $input = test.buildTemplate('input', { momentPicker: 'dateStr', ngModel: 'dateObj', format: format }, undefined, $scope); })); // set Model Value from View Value it('should initialize Model Value from View Value', () => { let dateObj = moment('2017-01-12', format), dateStr = dateObj.format(format); $scope['dateStr'] = dateStr; $scope.$digest(); expect($scope['dateObj'].isSame(dateObj)).toBe(true); }); // set View Value from Model Value it('should initialize View Value from Model Value', () => { let dateObj = moment('2017-01-12', format), dateStr = dateObj.format(format); $scope['dateObj'] = dateObj; $scope.$digest(); expect($scope['dateStr']).toBe(dateStr); expect($input.val()).toBe(dateStr); }); // update Model Value it('should update Model Value after View Value update', () => { let dateObj = moment('2017-01-12', format), dateStr = dateObj.format(format); // first set an initial date $scope['dateObj'] = dateObj; $scope['dateStr'] = dateStr; $scope.$digest(); // then change it and listen for value update dateObj = moment('2016-11-23', format); dateStr = dateObj.format(format); $scope['dateStr'] = dateStr; $scope.$digest(); expect($scope['dateObj'].isSame(dateObj)).toBe(true); }); // update View Value it('should update View Value after Model Value update', () => { let dateObj = moment('2017-01-12', format), dateStr = dateObj.format(format); // first set an initial date $scope['dateObj'] = dateObj; $scope['dateStr'] = dateStr; $scope.$digest(); // then change it and listen for value update dateObj = moment('2016-11-23', format); dateStr = dateObj.format(format); $scope['dateObj'] = dateObj; $scope.$digest(); expect($scope['dateStr']).toBe(dateStr); expect($input.val()).toBe(dateStr); }); // same property for View Value and Model Value it('should update (View) Value when using the same property value', () => { let date = moment('2017-01-12', format); $scope['date'] = date; $input = test.buildTemplate('input', { momentPicker: 'date', ngModel: 'date', format: format }, undefined, $scope); expect($scope['date'].isSame(date)).toBe(true); expect($input.val()).toBe(date.format(format)); }); // sync model across pickers it('should sync model updates across pickers', () => { let dateFormat = 'YYYY-MM-DD', timeFormat = 'HH:mm', $date = test.buildTemplate('input', { momentPicker: 'date', ngModel: 'datetime', format: dateFormat }, undefined, $scope), $time = test.buildTemplate('input', { momentPicker: 'time', ngModel: 'datetime', format: timeFormat }, undefined, $scope); $scope['datetime'] = moment('2017-01-12 20:32', dateFormat + ' ' + timeFormat); $scope.$digest(); expect($date.val()).toBe($scope['datetime'].format(dateFormat)); expect($time.val()).toBe($scope['datetime'].format(timeFormat)); expect($scope['date']).toBe($scope['datetime'].format(dateFormat)); expect($scope['time']).toBe($scope['datetime'].format(timeFormat)); $scope['date'] = '2015-08-29'; $scope.$digest(); expect($date.val()).toBe($scope['date']); expect($scope['datetime'].format(dateFormat)).toBe($scope['date']); $scope['time'] = '23:11'; $scope.$digest(); expect($time.val()).toBe($scope['time']); expect($scope['datetime'].format(timeFormat)).toBe($scope['time']); expect($scope['datetime'].format(dateFormat + ' ' + timeFormat)).toBe('2015-08-29 23:11'); }); }); ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "es3", "outDir": "./dist", "sourceMap": true, "alwaysStrict": true }, "exclude": [ "node_modules" ] } ================================================ FILE: tslint.json ================================================ { "extends": "tslint:latest", "env": { "browser": true }, "globals": { "angular": 1, "moment": 1 }, "rules": { "align": [true, "parameters"], "array-type": [true, "array"], "curly": false, "eofline": false, "indent": [true, "tabs"], "interface-name": [true, "always-prefix"], "linebreak-style": [false], "max-line-length": [false], "member-access": [true, "check-accessor"], "member-ordering": [false], "no-angle-bracket-type-assertion": false, "no-any": true, "no-arg": true, "no-consecutive-blank-lines": [true], "no-console": [true], "no-debugger": true, "no-duplicate-variable": true, "no-empty": true, "no-namespace": true, "no-reference": true, "no-string-literal": false, "no-trailing-whitespace": false, "no-unused-expression": true, "no-unused-new": true, "no-var-keyword": true, "no-var-requires": false, // mandatory to allow webpack to grab HTML, CSS, LESS files "object-literal-sort-keys": false, "object-literal-shorthand": false, "one-line": [true, "check-whitespace"], "one-variable-per-declaration": [false], "only-arrow-functions": [true], "ordered-imports": [false], "prefer-const": false, "quotemark": [true, "single"], "space-before-function-paren": ["error", {"anonymous": "always" }], "switch-default": false, "trailing-comma": [false], "triple-equals": [false], "whitespace": [true, "check-branch", "check-decl", "check-operator", "check-module", "check-separator", "check-type"] }, "jsRules": { "align": [true, "parameters"], "array-type": [true, "array"], "curly": false, "eofline": false, "indent": [true, "tabs"], "interface-name": [true, "always-prefix"], "linebreak-style": [false], "max-line-length": [false], "member-access": [true, "check-accessor"], "member-ordering": [false], "no-any": true, "no-arg": true, "no-consecutive-blank-lines": [true], "no-console": [true], "no-debugger": true, "no-duplicate-variable": true, "no-empty": true, "no-namespace": true, "no-reference": true, "no-string-literal": false, "no-trailing-whitespace": false, "no-unused-expression": true, "no-unused-new": true, "no-var-keyword": false, "no-var-requires": true, "object-literal-sort-keys": false, "object-literal-shorthand": false, "one-line": [true, "check-whitespace"], "one-variable-per-declaration": [false], "only-arrow-functions": [false], "ordered-imports": [false], "quotemark": [true, "single"], "switch-default": false, "trailing-comma": [false], "triple-equals": [false], "whitespace": [true, "check-branch", "check-decl", "check-operator", "check-module", "check-separator", "check-type"] } } ================================================ FILE: webpack.config.js ================================================ 'use strict'; let fs = require('fs'); let path = require('path'); let pkg = require('./package'); let bower = require('./bower'); let semver = require('semver'); let webpack = require('webpack'); let autoprefixer = require('autoprefixer'); let extractTextPlugin = require('extract-text-webpack-plugin'); let generateJsonPlugin = require('generate-json-webpack-plugin'); let isProduction = process.argv.indexOf('-p') != -1; let filesuffix = (isProduction ? '.min' : ''); let filename = 'angular-moment-picker' + filesuffix; let increase = (process.argv.filter(argv => argv.match(/^increase=.+$/))[0] || '').replace('increase=', ''); // sync bower.json with package.json pkg.version = increase ? semver.inc(pkg.version, increase) : pkg.version; ['name', 'version', 'description', 'homepage', 'license', 'keywords', 'dependencies'].forEach(field => bower[field] = pkg[field]); // themes - read file from `src/themes` folder let themes = []; fs.readdirSync('src/themes').forEach(file => themes.push(file.replace('.less', ''))); // extract plugin settings for css file generation let extractBaseTheme = new extractTextPlugin(filename + '.css'); let extractOtherThemes = themes.map(theme => ({ file: path.resolve(__dirname, 'src/themes/' + theme + '.less'), extract: new extractTextPlugin('themes/' + theme + filesuffix + '.css') })); let extractPluginLoaders = [ 'css-loader', { loader: 'postcss-loader', options: { plugins: [ autoprefixer({ browsers: ['> 0%'] }) ] } }, 'less-loader' ]; module.exports = { entry: [ './src/index.ts', './src/index.less', ...extractOtherThemes.map(theme => theme.file) ], output: { path: path.join(__dirname, 'dist'), filename: filename + '.js' }, bail: true, externals: Object.keys(pkg.dependencies), resolve: { extensions: ['.ts', '.html', '.less'] }, module: { rules: [ { test: /\.ts$/, use: ['ts-loader', 'tslint-loader'] }, { test: /\.html$/, use: 'html-loader?minimize=true' }, { test: /\.less$/, exclude: /themes/, use: extractBaseTheme.extract(extractPluginLoaders) }, ...extractOtherThemes.map(theme => { return { include: theme.file, use: theme.extract.extract(extractPluginLoaders) }; }) ] }, plugins: [ extractBaseTheme, ...extractOtherThemes.map(theme => theme.extract), new webpack.BannerPlugin('Angular Moment Picker - v' + pkg.version + ' - ' + pkg.homepage + ' - (c) 2015 Indri Muska - ' + pkg.license), new generateJsonPlugin('../bower.json', bower, undefined, 2), new generateJsonPlugin('../package.json', pkg, undefined, 2) ] };