public/components/date-time-picker/date-time-picker.js (175 lines of code) (raw):

/** * Module which exposes a directive outputting a Date picker form field. * * To show just a date picker field: * <div wf-date-time-picker ng-model="stub.due"/> * * To show date picker field with a label: * <div wf-date-time-picker label="Due date" ng-model="stub.due"/> * * To show date picker field with a label and help text: * <div wf-date-time-picker label="Due something" help-text="true" ng-model="stub.due"/> * * To show date picker field with a label and custom help text: * <div wf-date-time-picker label="Due something" help-text="custom help text" ng-model="stub.due"/> */ // 3rd party dependencies import angular from 'angular'; import moment from 'moment'; import 'angular-bootstrap-datetimepicker'; // local dependencies import 'lib/date-service'; import dateTimePickerTemplate from './date-time-picker.html'; angular.module('wfDateTimePicker', ['ui.bootstrap.datetimepicker', 'wfDateService']) // Add a listener to ui.bootstrap.datetimepicker to reset the picker to day view // Written using the second example from here http://angular-tips.com/blog/2013/09/experiment-decorating-directives/ .config(function dateTimePickerMonkeyPatch($provide) { $provide.decorator('datetimepickerDirective', function($delegate) { var directive = $delegate[0]; function getUTCTimeNow() { var timeNow = new Date(); return timeNow.getTime() - (timeNow.getTimezoneOffset() * 60000); } directive.compile = function() { return function(scope) { // add extra listener to link scope.$on('resetPicker', function () { scope.changeView('day', getUTCTimeNow()); }) } }; return $delegate }); }) .directive('wfDateTimePicker', ['$log', '$timeout', 'wfDateParser', 'wfLocaliseDateTimeFilter', 'wfFormatDateTimeFilter', function ($log, $timeout, wfDateParser, wfLocaliseDateTimeFilter) { var pickerCount = 0; return { restrict: 'A', require: '^ngModel', scope: { dateValue: '=ngModel', dateFormat: '@wfDateFormat', label: '@', helpText: '@', small: '@wfSmall', updateOn: '@wfUpdateOn', cancelOn: '@wfCancelOn', onCancel: '&wfOnCancel', onUpdate: '&wfOnUpdate', onSubmit: '&wfOnSubmit', }, template: dateTimePickerTemplate, controller: function ($scope, $element) { var idSuffix = pickerCount++; const dateOnly = $element.attr('wf-date-only') $scope._config = dateOnly?{ minView: 'day' }:{}; this.textInputId = 'wfDateTimePickerText' + idSuffix; this.dropDownButtonId = 'wfDateTimePickerButton' + idSuffix; const dropDownId = 'wfDateTimePickerDropdown' + idSuffix; this.dropDownId = dropDownId; $element.addClass('date-time-picker'); // Watch for model updates to dateValue, and update datePicker when changes $scope.$watch('dateValue', function (newValue) { if ($scope.datePickerValue !== newValue) { // Date picker will support a localised date when passed a moment object $scope.datePickerValue = wfDateParser.normaliseDateString(wfLocaliseDateTimeFilter(newValue)); } }); this.onDatePicked = function (newValue) { const newDate = wfDateParser.parseDate(newValue) $scope.dateValue = newDate; // Delay running onUpdate so digest can run and update dateValue properly. $timeout(function () { $scope.onUpdate($scope.dateValue); $scope.onSubmit(); angular.element(document.getElementById(dropDownId)).removeClass('open') }, 0); }; }, controllerAs: 'dateTimePicker' }; }]) .directive('wfDateTimeField', ['wfFormatDateTimeFilter', 'wfLocaliseDateTimeFilter', 'wfDateParser', '$browser', '$log', function (wfFormatDateTimeFilter, wfLocaliseDateTimeFilter, wfDateParser, $browser) { // Utility methods function isArrowKey(keyCode) { return 37 <= keyCode && keyCode <= 40; } function isModifierKey(keyCode) { return 15 < keyCode && keyCode < 19; } // Constants var KEYCODE_COMMAND = 91; var KEYCODE_ESCAPE = 27; var KEYCODE_ENTER = 13; return { require: '^ngModel', scope: { textValue: '=ngModel', updateOn: '@wfUpdateOn', cancelOn: '@wfCancelOn', onCancel: '&wfOnCancel', onUpdate: '&wfOnUpdate', onSubmit: '&wfOnSubmit' }, link: function (scope, elem, attrs, ngModel) { var updateOn = scope.updateOn || 'default'; function formatText(input) { return wfFormatDateTimeFilter(wfLocaliseDateTimeFilter(input), 'D MMM YYYY HH:mm'); } function commitUpdate() { scope.$apply(function () { ngModel.$setViewValue(elem.val()); scope.onUpdate(ngModel.$modelValue); }); } function cancelUpdate() { scope.$apply(function () { if (hasChanged()) { // reset to model value ngModel.$setViewValue(formatText(ngModel.$modelValue)); ngModel.$render(); } scope.onCancel(); }); } function parseDate(input) { if (!input || input === '') { return null; } try { return wfDateParser.parseDate(input); } catch (err) { // ignore parse errors - these are handled by angular (ng-invalid-parse) if (err.message.substr(0,20) !== 'Could not parse date') { throw err; } } } function hasChanged() { return !moment(ngModel.$modelValue).isSame(parseDate(elem.val())); } // Setup input handlers // Slightly hacky, but it works.. angular.element(elem[0].form).on('submit', commitUpdate); // Set event handlers on the input element elem.off('input keydown change'); // reset default angular input event handlers elem.on('input keydown change blur', function (ev) { var key = ev.keyCode, type = ev.type; if (type === 'keydown') { // ignore the following keys on input if ((key === KEYCODE_COMMAND) || isModifierKey(key) || isArrowKey(key)) { return; } $browser.defer(commitUpdate); if (updateOn === 'enter' && key === KEYCODE_ENTER) { scope.updateOnEnter(); } } if (type === 'blur' && scope.cancelOn === 'blur') { cancelUpdate(); } // cancel via escape if (type === 'keydown' && key === KEYCODE_ESCAPE) { cancelUpdate(); } }); ngModel.$render = function () { elem.val(ngModel.$viewValue || ''); }; ngModel.$parsers.push(parseDate); ngModel.$formatters.push(formatText); // Watch for changes to timezone scope.$on('location:change', function () { ngModel.$setViewValue(formatText(ngModel.$modelValue)); ngModel.$render(); }); scope.updateOnEnter = function () { switch (ngModel.$modelValue) { // If an empty value is submitted, clear the field case null: scope.onSubmit(); break; // If date is invalid, revert date in text box to previous value (and don't update database) case undefined: scope.onCancel(); break; // If the date is valid, parse it to a string and update the database default: var parsedDate, parsedDateAsString; parsedDate = moment(ngModel.$modelValue); if (parsedDate.isValid()) { parsedDateAsString = parsedDate.toISOString(); ngModel.$modelValue = parsedDateAsString; scope.onSubmit(); } } }; } }; }]);