mirror of https://github.com/grafana/grafana
feat(timepickerv2): big progress on new design of new timepicker, #2761
parent
a30f73fe36
commit
5eefa36111
@ -0,0 +1,90 @@ |
||||
<div class="row pull-right"> |
||||
<div class="gf-timepicker-relative-section"> |
||||
<h3>Quick ranges</h3> |
||||
<ul ng-repeat="group in timeOptions"> |
||||
<li bindonce ng-repeat='option in group' ng-class="{active: option.active}"> |
||||
<a ng-click="ctrl.setRelativeFilter(option)" bo-text="option.display"></a> |
||||
</li> |
||||
</ul> |
||||
</div> |
||||
|
||||
<div class="gf-timepicker-absolute-section"> |
||||
<h3>Time range</h3> |
||||
<label class="small">From:</label> |
||||
<div class="input-prepend"> |
||||
<input type="text" class="input-large" ng-model="timeRaw.from" input-datetime> |
||||
<button class="btn btn-primary" type="button" ng-click="openFromPicker=!openFromPicker"> |
||||
<i class="fa fa-calendar"></i> |
||||
</button> |
||||
</div> |
||||
|
||||
<div ng-if="openFromPicker"> |
||||
<datepicker ng-model="absolute.to" class="gf-timepicker-component" show-weeks="false"></datepicker> |
||||
</div> |
||||
|
||||
<label class="small">To:</label> |
||||
<div class="input-prepend"> |
||||
<input type="text" class="input-large" ng-model="timeRaw.to" input-datetime> |
||||
<button class="btn btn-primary" type="button" ng-click="openToPicker=!openToPicker"> |
||||
<i class="fa fa-calendar"></i> |
||||
</button> |
||||
</div> |
||||
|
||||
<label class="small">Refreshing every:</label> |
||||
<select ng-model="dashboard.refresh" class='input-large' ng-options="f for f in ['5m','10m']"></select> |
||||
</div> |
||||
</div> |
||||
<div class="clearfix"></div> |
||||
|
||||
<!-- <tabset> --> |
||||
<!-- <tab heading="Relative"> --> |
||||
<!-- --> |
||||
<!-- <div style="float:right; width: 200px" ng-repeat="group in timeOptions"> --> |
||||
<!-- <ul> --> |
||||
<!-- <li bindonce ng-repeat='option in group'> --> |
||||
<!-- <a ng-click="ctrl.setRelativeFilter(option)" bo-text="option.display"></a> --> |
||||
<!-- </li> --> |
||||
<!-- </ul> --> |
||||
<!-- </div> --> |
||||
<!-- --> |
||||
<!-- </tab> --> |
||||
<!-- <tab heading="Absolute"> --> |
||||
<!-- --> |
||||
<!-- <form name="timeForm" style="margin: 0 20px 20px 20px"> --> |
||||
<!-- <div class="gf-timepicker-section"> --> |
||||
<!-- <div> --> |
||||
<!-- <div class="tight-form last"> --> |
||||
<!-- </div> --> |
||||
<!-- <label class="small">From:</label> --> |
||||
<!-- <input type="text" required class="input-large" ng-model="absolute.from" input-datetime="MMMM Do YYYY, HH:mm:ss.SSS"> --> |
||||
<!-- <br> --> |
||||
<!-- </div> --> |
||||
<!-- <datepicker ng-model="absolute.from" class="gf-timepicker-component" show-weeks="false"></datepicker> --> |
||||
<!-- </div> --> |
||||
<!-- <div class="gf-timepicker-section"> --> |
||||
<!-- <div> --> |
||||
<!-- <label class="small">To:</label> --> |
||||
<!-- <input type="text" required class="input-large" ng-model="absolute.to" input-datetime="MMMM Do YYYY, HH:mm:ss.SSS"> --> |
||||
<!-- <br> --> |
||||
<!-- </div> --> |
||||
<!-- <datepicker ng-model="absolute.to" class="gf-timepicker-component" show-weeks="false"></datepicker> --> |
||||
<!-- </div> --> |
||||
<!-- <div class="clearfix"></div> --> |
||||
<!-- </form> --> |
||||
<!-- </tab> --> |
||||
<!-- </tabset> --> |
||||
<!-- --> |
||||
<!-- <!-- <!&#45;&#45; Auto refresh submenu &#45;&#45;> --> --> |
||||
<!-- <li class="dropdown-submenu"> --> |
||||
<!-- <a href="#">Auto-Refresh</a> --> |
||||
<!-- <ul class="dropdown-menu" ng-class="{'dropdown-submenu-left': refreshMenuLeftSide}"> --> |
||||
<!-- <li> --> |
||||
<!-- <a ng-click="timeSrv.set_interval(false)">Off</a> --> |
||||
<!-- </li> --> |
||||
<!-- <li bindonce ng-repeat="interval in panel.refresh_intervals track by $index"> --> |
||||
<!-- <a ng-click="timeSrv.set_interval(interval)" bo-text="'Every ' + interval"></a> --> |
||||
<!-- </li> --> |
||||
<!-- </ul> --> |
||||
<!-- </li> --> |
||||
<!-- <li><a ng-click="ctrl.customTime()">Custom</a></li> --> |
||||
<!-- </div> --> |
@ -0,0 +1,33 @@ |
||||
define([ |
||||
"angular", |
||||
"moment", |
||||
],function (angular, moment) { |
||||
'use strict'; |
||||
|
||||
angular. |
||||
module("grafana.directives"). |
||||
directive('inputDatetime', function () { |
||||
return { |
||||
restrict: 'A', |
||||
require: 'ngModel', |
||||
link: function ($scope, $elem, attrs, ngModel) { |
||||
// var format = 'YYYY-MM-DD HH:mm:ss.SSS';
|
||||
// // $elem.after('<div class="input-datetime-format">' + format + '</div>');
|
||||
//
|
||||
// // What should I make with the input from the user?
|
||||
// var fromUser = function (text) {
|
||||
// var parsed = moment(text, format);
|
||||
// return parsed.isValid() ? parsed : undefined;
|
||||
// };
|
||||
//
|
||||
// // How should I present the data back to the user in the input field?
|
||||
// var toUser = function (datetime) {
|
||||
// return moment(datetime).format(format);
|
||||
// };
|
||||
//
|
||||
// ngModel.$parsers.push(fromUser);
|
||||
// ngModel.$formatters.push(toUser);
|
||||
} |
||||
}; |
||||
}); |
||||
}); |
@ -0,0 +1,104 @@ |
||||
.timepicker-to-column { |
||||
margin-top: 10px; |
||||
} |
||||
|
||||
.timepicker-input input { |
||||
outline: 0 !important; |
||||
border: 0px !important; |
||||
-webkit-box-shadow: 0; |
||||
-moz-box-shadow: 0; |
||||
box-shadow: 0; |
||||
position: relative; |
||||
} |
||||
|
||||
.timepicker-input input::-webkit-outer-spin-button, |
||||
.timepicker-input input::-webkit-inner-spin-button { |
||||
-webkit-appearance: none; |
||||
margin: 0; |
||||
} |
||||
|
||||
input.timepicker-date { |
||||
width: 90px; |
||||
} |
||||
|
||||
input.timepicker-hms { |
||||
width: 20px; |
||||
} |
||||
|
||||
input.timepicker-ms { |
||||
width: 25px; |
||||
} |
||||
|
||||
div.timepicker-now { |
||||
float: right; |
||||
} |
||||
|
||||
.timepicker-timestring { |
||||
font-weight: normal; |
||||
} |
||||
|
||||
.gf-timepicker-dropdown { |
||||
margin: 15px; |
||||
padding: 10px 20px; |
||||
float: right; |
||||
background-color: @grafanaPanelBackground; |
||||
border: 1px solid @grafanaTargetFuncBackground; |
||||
} |
||||
|
||||
.gf-timepicker-absolute-section { |
||||
width: 300px; |
||||
float: left; |
||||
padding: 0 20px 0 20px; |
||||
} |
||||
|
||||
.gf-timepicker-relative-section { |
||||
border-right: @grafanaTriggerBorder; |
||||
padding: 0 20px 0 20px; |
||||
min-height: 258px; |
||||
float: left; |
||||
ul { |
||||
float: left; |
||||
margin: 0 20px 10px 25px; |
||||
li.active { |
||||
border-left: 1px solid @blue; |
||||
padding: 2px 0; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.gf-timepicker-component { |
||||
button.btn-sm { |
||||
.buttonBackground(@btnInverseBackground, @btnInverseBackgroundHighlight); |
||||
background-image: none; |
||||
border: none; |
||||
padding: 6px 9px; |
||||
color: @textColor; |
||||
&.active .text-info { |
||||
color: @orange; |
||||
font-weight: bold; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.faMixin { |
||||
display: inline-block; |
||||
font: normal normal normal 14px/1 FontAwesome; |
||||
font-size: inherit; |
||||
text-rendering: auto; |
||||
-webkit-font-smoothing: antialiased; |
||||
-moz-osx-font-smoothing: grayscale; |
||||
} |
||||
|
||||
.glyphicon-chevron-right:before { |
||||
.faMixin; |
||||
content: "\f054"; |
||||
} |
||||
|
||||
.glyphicon-chevron-left:before { |
||||
.faMixin; |
||||
content: "\f053"; |
||||
} |
||||
|
||||
.input-datetime-format { |
||||
color: @linkColorDisabled |
||||
} |
@ -0,0 +1,212 @@ |
||||
angular.module('ui.bootstrap.dateparser', []) |
||||
|
||||
.service('dateParser', ['$log', '$locale', 'orderByFilter', function($log, $locale, orderByFilter) { |
||||
// Pulled from https://github.com/mbostock/d3/blob/master/src/format/requote.js
|
||||
var SPECIAL_CHARACTERS_REGEXP = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g; |
||||
|
||||
var localeId; |
||||
var formatCodeToRegex; |
||||
|
||||
this.init = function() { |
||||
localeId = $locale.id; |
||||
|
||||
this.parsers = {}; |
||||
|
||||
formatCodeToRegex = { |
||||
'yyyy': { |
||||
regex: '\\d{4}', |
||||
apply: function(value) { this.year = +value; } |
||||
}, |
||||
'yy': { |
||||
regex: '\\d{2}', |
||||
apply: function(value) { this.year = +value + 2000; } |
||||
}, |
||||
'y': { |
||||
regex: '\\d{1,4}', |
||||
apply: function(value) { this.year = +value; } |
||||
}, |
||||
'MMMM': { |
||||
regex: $locale.DATETIME_FORMATS.MONTH.join('|'), |
||||
apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); } |
||||
}, |
||||
'MMM': { |
||||
regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), |
||||
apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); } |
||||
}, |
||||
'MM': { |
||||
regex: '0[1-9]|1[0-2]', |
||||
apply: function(value) { this.month = value - 1; } |
||||
}, |
||||
'M': { |
||||
regex: '[1-9]|1[0-2]', |
||||
apply: function(value) { this.month = value - 1; } |
||||
}, |
||||
'dd': { |
||||
regex: '[0-2][0-9]{1}|3[0-1]{1}', |
||||
apply: function(value) { this.date = +value; } |
||||
}, |
||||
'd': { |
||||
regex: '[1-2]?[0-9]{1}|3[0-1]{1}', |
||||
apply: function(value) { this.date = +value; } |
||||
}, |
||||
'EEEE': { |
||||
regex: $locale.DATETIME_FORMATS.DAY.join('|') |
||||
}, |
||||
'EEE': { |
||||
regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|') |
||||
}, |
||||
'HH': { |
||||
regex: '(?:0|1)[0-9]|2[0-3]', |
||||
apply: function(value) { this.hours = +value; } |
||||
}, |
||||
'hh': { |
||||
regex: '0[0-9]|1[0-2]', |
||||
apply: function(value) { this.hours = +value; } |
||||
}, |
||||
'H': { |
||||
regex: '1?[0-9]|2[0-3]', |
||||
apply: function(value) { this.hours = +value; } |
||||
}, |
||||
'h': { |
||||
regex: '[0-9]|1[0-2]', |
||||
apply: function(value) { this.hours = +value; } |
||||
}, |
||||
'mm': { |
||||
regex: '[0-5][0-9]', |
||||
apply: function(value) { this.minutes = +value; } |
||||
}, |
||||
'm': { |
||||
regex: '[0-9]|[1-5][0-9]', |
||||
apply: function(value) { this.minutes = +value; } |
||||
}, |
||||
'sss': { |
||||
regex: '[0-9][0-9][0-9]', |
||||
apply: function(value) { this.milliseconds = +value; } |
||||
}, |
||||
'ss': { |
||||
regex: '[0-5][0-9]', |
||||
apply: function(value) { this.seconds = +value; } |
||||
}, |
||||
's': { |
||||
regex: '[0-9]|[1-5][0-9]', |
||||
apply: function(value) { this.seconds = +value; } |
||||
}, |
||||
'a': { |
||||
regex: $locale.DATETIME_FORMATS.AMPMS.join('|'), |
||||
apply: function(value) { |
||||
if (this.hours === 12) { |
||||
this.hours = 0; |
||||
} |
||||
|
||||
if (value === 'PM') { |
||||
this.hours += 12; |
||||
} |
||||
} |
||||
} |
||||
}; |
||||
}; |
||||
|
||||
this.init(); |
||||
|
||||
function createParser(format) { |
||||
var map = [], regex = format.split(''); |
||||
|
||||
angular.forEach(formatCodeToRegex, function(data, code) { |
||||
var index = format.indexOf(code); |
||||
|
||||
if (index > -1) { |
||||
format = format.split(''); |
||||
|
||||
regex[index] = '(' + data.regex + ')'; |
||||
format[index] = '$'; // Custom symbol to define consumed part of format
|
||||
for (var i = index + 1, n = index + code.length; i < n; i++) { |
||||
regex[i] = ''; |
||||
format[i] = '$'; |
||||
} |
||||
format = format.join(''); |
||||
|
||||
map.push({ index: index, apply: data.apply }); |
||||
} |
||||
}); |
||||
|
||||
return { |
||||
regex: new RegExp('^' + regex.join('') + '$'), |
||||
map: orderByFilter(map, 'index') |
||||
}; |
||||
} |
||||
|
||||
this.parse = function(input, format, baseDate) { |
||||
if (!angular.isString(input) || !format) { |
||||
return input; |
||||
} |
||||
|
||||
format = $locale.DATETIME_FORMATS[format] || format; |
||||
format = format.replace(SPECIAL_CHARACTERS_REGEXP, '\\$&'); |
||||
|
||||
if ($locale.id !== localeId) { |
||||
this.init(); |
||||
} |
||||
|
||||
if (!this.parsers[format]) { |
||||
this.parsers[format] = createParser(format); |
||||
} |
||||
|
||||
var parser = this.parsers[format], |
||||
regex = parser.regex, |
||||
map = parser.map, |
||||
results = input.match(regex); |
||||
|
||||
if (results && results.length) { |
||||
var fields, dt; |
||||
if (angular.isDate(baseDate) && !isNaN(baseDate.getTime())) { |
||||
fields = { |
||||
year: baseDate.getFullYear(), |
||||
month: baseDate.getMonth(), |
||||
date: baseDate.getDate(), |
||||
hours: baseDate.getHours(), |
||||
minutes: baseDate.getMinutes(), |
||||
seconds: baseDate.getSeconds(), |
||||
milliseconds: baseDate.getMilliseconds() |
||||
}; |
||||
} else { |
||||
if (baseDate) { |
||||
$log.warn('dateparser:', 'baseDate is not a valid date'); |
||||
} |
||||
fields = { year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }; |
||||
} |
||||
|
||||
for (var i = 1, n = results.length; i < n; i++) { |
||||
var mapper = map[i-1]; |
||||
if (mapper.apply) { |
||||
mapper.apply.call(fields, results[i]); |
||||
} |
||||
} |
||||
|
||||
if (isValid(fields.year, fields.month, fields.date)) { |
||||
dt = new Date(fields.year, fields.month, fields.date, |
||||
fields.hours, fields.minutes, fields.seconds, |
||||
fields.milliseconds || 0); |
||||
} |
||||
|
||||
return dt; |
||||
} |
||||
}; |
||||
|
||||
// Check if date is valid for specific month (and year for February).
|
||||
// Month: 0 = Jan, 1 = Feb, etc
|
||||
function isValid(year, month, date) { |
||||
if (date < 1) { |
||||
return false; |
||||
} |
||||
|
||||
if (month === 1 && date > 28) { |
||||
return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0); |
||||
} |
||||
|
||||
if (month === 3 || month === 5 || month === 8 || month === 10) { |
||||
return date < 31; |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
}]); |
@ -0,0 +1,823 @@ |
||||
angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position']) |
||||
|
||||
.value('$datepickerSuppressError', false) |
||||
|
||||
.constant('datepickerConfig', { |
||||
formatDay: 'dd', |
||||
formatMonth: 'MMMM', |
||||
formatYear: 'yyyy', |
||||
formatDayHeader: 'EEE', |
||||
formatDayTitle: 'MMMM yyyy', |
||||
formatMonthTitle: 'yyyy', |
||||
datepickerMode: 'day', |
||||
minMode: 'day', |
||||
maxMode: 'year', |
||||
showWeeks: true, |
||||
startingDay: 0, |
||||
yearRange: 20, |
||||
minDate: null, |
||||
maxDate: null, |
||||
shortcutPropagation: false |
||||
}) |
||||
|
||||
.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$log', 'dateFilter', 'datepickerConfig', '$datepickerSuppressError', function($scope, $attrs, $parse, $interpolate, $log, dateFilter, datepickerConfig, $datepickerSuppressError) { |
||||
var self = this, |
||||
ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl;
|
||||
|
||||
// Modes chain
|
||||
this.modes = ['day', 'month', 'year']; |
||||
|
||||
// Configuration attributes
|
||||
angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', |
||||
'showWeeks', 'startingDay', 'yearRange', 'shortcutPropagation'], function(key, index) { |
||||
self[key] = angular.isDefined($attrs[key]) ? (index < 6 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key]; |
||||
}); |
||||
|
||||
// Watchable date attributes
|
||||
angular.forEach(['minDate', 'maxDate'], function(key) { |
||||
if ($attrs[key]) { |
||||
$scope.$parent.$watch($parse($attrs[key]), function(value) { |
||||
self[key] = value ? new Date(value) : null; |
||||
self.refreshView(); |
||||
}); |
||||
} else { |
||||
self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null; |
||||
} |
||||
}); |
||||
|
||||
angular.forEach(['minMode', 'maxMode'], function(key) { |
||||
if ($attrs[key]) { |
||||
$scope.$parent.$watch($parse($attrs[key]), function(value) { |
||||
self[key] = angular.isDefined(value) ? value : $attrs[key]; |
||||
$scope[key] = self[key]; |
||||
if ((key == 'minMode' && self.modes.indexOf($scope.datepickerMode) < self.modes.indexOf(self[key])) || (key == 'maxMode' && self.modes.indexOf($scope.datepickerMode) > self.modes.indexOf(self[key]))) { |
||||
$scope.datepickerMode = self[key]; |
||||
} |
||||
}); |
||||
} else { |
||||
self[key] = datepickerConfig[key] || null; |
||||
$scope[key] = self[key]; |
||||
} |
||||
}); |
||||
|
||||
$scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode; |
||||
$scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000); |
||||
|
||||
if (angular.isDefined($attrs.initDate)) { |
||||
this.activeDate = $scope.$parent.$eval($attrs.initDate) || new Date(); |
||||
$scope.$parent.$watch($attrs.initDate, function(initDate) { |
||||
if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) { |
||||
self.activeDate = initDate; |
||||
self.refreshView(); |
||||
} |
||||
}); |
||||
} else { |
||||
this.activeDate = new Date(); |
||||
} |
||||
|
||||
$scope.isActive = function(dateObject) { |
||||
if (self.compare(dateObject.date, self.activeDate) === 0) { |
||||
$scope.activeDateId = dateObject.uid; |
||||
return true; |
||||
} |
||||
return false; |
||||
}; |
||||
|
||||
this.init = function(ngModelCtrl_) { |
||||
ngModelCtrl = ngModelCtrl_; |
||||
|
||||
ngModelCtrl.$render = function() { |
||||
self.render(); |
||||
}; |
||||
}; |
||||
|
||||
this.render = function() { |
||||
if (ngModelCtrl.$viewValue) { |
||||
var date = new Date(ngModelCtrl.$viewValue), |
||||
isValid = !isNaN(date); |
||||
|
||||
if (isValid) { |
||||
this.activeDate = date; |
||||
} else if (!$datepickerSuppressError) { |
||||
$log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); |
||||
} |
||||
} |
||||
this.refreshView(); |
||||
}; |
||||
|
||||
this.refreshView = function() { |
||||
if (this.element) { |
||||
this._refreshView(); |
||||
|
||||
var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null; |
||||
ngModelCtrl.$setValidity('dateDisabled', !date || (this.element && !this.isDisabled(date))); |
||||
} |
||||
}; |
||||
|
||||
this.createDateObject = function(date, format) { |
||||
var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null; |
||||
return { |
||||
date: date, |
||||
label: dateFilter(date, format), |
||||
selected: model && this.compare(date, model) === 0, |
||||
disabled: this.isDisabled(date), |
||||
current: this.compare(date, new Date()) === 0, |
||||
customClass: this.customClass(date) |
||||
}; |
||||
}; |
||||
|
||||
this.isDisabled = function(date) { |
||||
return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($attrs.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode}))); |
||||
}; |
||||
|
||||
this.customClass = function(date) { |
||||
return $scope.customClass({date: date, mode: $scope.datepickerMode}); |
||||
}; |
||||
|
||||
// Split array into smaller arrays
|
||||
this.split = function(arr, size) { |
||||
var arrays = []; |
||||
while (arr.length > 0) { |
||||
arrays.push(arr.splice(0, size)); |
||||
} |
||||
return arrays; |
||||
}; |
||||
|
||||
// Fix a hard-reprodusible bug with timezones
|
||||
// The bug depends on OS, browser, current timezone and current date
|
||||
// i.e.
|
||||
// var date = new Date(2014, 0, 1);
|
||||
// console.log(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours());
|
||||
// can result in "2013 11 31 23" because of the bug.
|
||||
this.fixTimeZone = function(date) { |
||||
var hours = date.getHours(); |
||||
date.setHours(hours === 23 ? hours + 2 : 0); |
||||
}; |
||||
|
||||
$scope.select = function(date) { |
||||
if ($scope.datepickerMode === self.minMode) { |
||||
var dt = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : new Date(0, 0, 0, 0, 0, 0, 0); |
||||
dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate()); |
||||
ngModelCtrl.$setViewValue(dt); |
||||
ngModelCtrl.$render(); |
||||
} else { |
||||
self.activeDate = date; |
||||
$scope.datepickerMode = self.modes[self.modes.indexOf($scope.datepickerMode) - 1]; |
||||
} |
||||
}; |
||||
|
||||
$scope.move = function(direction) { |
||||
var year = self.activeDate.getFullYear() + direction * (self.step.years || 0), |
||||
month = self.activeDate.getMonth() + direction * (self.step.months || 0); |
||||
self.activeDate.setFullYear(year, month, 1); |
||||
self.refreshView(); |
||||
}; |
||||
|
||||
$scope.toggleMode = function(direction) { |
||||
direction = direction || 1; |
||||
|
||||
if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) { |
||||
return; |
||||
} |
||||
|
||||
$scope.datepickerMode = self.modes[self.modes.indexOf($scope.datepickerMode) + direction]; |
||||
}; |
||||
|
||||
// Key event mapper
|
||||
$scope.keys = { 13: 'enter', 32: 'space', 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home', 37: 'left', 38: 'up', 39: 'right', 40: 'down' }; |
||||
|
||||
var focusElement = function() { |
||||
self.element[0].focus(); |
||||
}; |
||||
|
||||
// Listen for focus requests from popup directive
|
||||
$scope.$on('datepicker.focus', focusElement); |
||||
|
||||
$scope.keydown = function(evt) { |
||||
var key = $scope.keys[evt.which]; |
||||
|
||||
if (!key || evt.shiftKey || evt.altKey) { |
||||
return; |
||||
} |
||||
|
||||
evt.preventDefault(); |
||||
if (!self.shortcutPropagation) { |
||||
evt.stopPropagation(); |
||||
} |
||||
|
||||
if (key === 'enter' || key === 'space') { |
||||
if (self.isDisabled(self.activeDate)) { |
||||
return; // do nothing
|
||||
} |
||||
$scope.select(self.activeDate); |
||||
} else if (evt.ctrlKey && (key === 'up' || key === 'down')) { |
||||
$scope.toggleMode(key === 'up' ? 1 : -1); |
||||
} else { |
||||
self.handleKeyDown(key, evt); |
||||
self.refreshView(); |
||||
} |
||||
}; |
||||
}]) |
||||
|
||||
.directive('datepicker', function() { |
||||
return { |
||||
restrict: 'EA', |
||||
replace: true, |
||||
templateUrl: function(element, attrs) { |
||||
return attrs.templateUrl || 'template/datepicker/datepicker.html'; |
||||
}, |
||||
scope: { |
||||
datepickerMode: '=?', |
||||
dateDisabled: '&', |
||||
customClass: '&', |
||||
shortcutPropagation: '&?' |
||||
}, |
||||
require: ['datepicker', '^ngModel'], |
||||
controller: 'DatepickerController', |
||||
controllerAs: 'datepicker', |
||||
link: function(scope, element, attrs, ctrls) { |
||||
var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; |
||||
|
||||
datepickerCtrl.init(ngModelCtrl); |
||||
} |
||||
}; |
||||
}) |
||||
|
||||
.directive('daypicker', ['dateFilter', function(dateFilter) { |
||||
return { |
||||
restrict: 'EA', |
||||
replace: true, |
||||
templateUrl: 'template/datepicker/day.html', |
||||
require: '^datepicker', |
||||
link: function(scope, element, attrs, ctrl) { |
||||
scope.showWeeks = ctrl.showWeeks; |
||||
|
||||
ctrl.step = { months: 1 }; |
||||
ctrl.element = element; |
||||
|
||||
var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; |
||||
function getDaysInMonth(year, month) { |
||||
return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month]; |
||||
} |
||||
|
||||
function getDates(startDate, n) { |
||||
var dates = new Array(n), current = new Date(startDate), i = 0, date; |
||||
while (i < n) { |
||||
date = new Date(current); |
||||
ctrl.fixTimeZone(date); |
||||
dates[i++] = date; |
||||
current.setDate(current.getDate() + 1); |
||||
} |
||||
return dates; |
||||
} |
||||
|
||||
ctrl._refreshView = function() { |
||||
var year = ctrl.activeDate.getFullYear(), |
||||
month = ctrl.activeDate.getMonth(), |
||||
firstDayOfMonth = new Date(year, month, 1), |
||||
difference = ctrl.startingDay - firstDayOfMonth.getDay(), |
||||
numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, |
||||
firstDate = new Date(firstDayOfMonth); |
||||
|
||||
if (numDisplayedFromPreviousMonth > 0) { |
||||
firstDate.setDate(-numDisplayedFromPreviousMonth + 1); |
||||
} |
||||
|
||||
// 42 is the number of days on a six-month calendar
|
||||
var days = getDates(firstDate, 42); |
||||
for (var i = 0; i < 42; i ++) { |
||||
days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), { |
||||
secondary: days[i].getMonth() !== month, |
||||
uid: scope.uniqueId + '-' + i |
||||
}); |
||||
} |
||||
|
||||
scope.labels = new Array(7); |
||||
for (var j = 0; j < 7; j++) { |
||||
scope.labels[j] = { |
||||
abbr: dateFilter(days[j].date, ctrl.formatDayHeader), |
||||
full: dateFilter(days[j].date, 'EEEE') |
||||
}; |
||||
} |
||||
|
||||
scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle); |
||||
scope.rows = ctrl.split(days, 7); |
||||
|
||||
if (scope.showWeeks) { |
||||
scope.weekNumbers = []; |
||||
var thursdayIndex = (4 + 7 - ctrl.startingDay) % 7, |
||||
numWeeks = scope.rows.length; |
||||
for (var curWeek = 0; curWeek < numWeeks; curWeek++) { |
||||
scope.weekNumbers.push( |
||||
getISO8601WeekNumber(scope.rows[curWeek][thursdayIndex].date)); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
ctrl.compare = function(date1, date2) { |
||||
return (new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate())); |
||||
}; |
||||
|
||||
function getISO8601WeekNumber(date) { |
||||
var checkDate = new Date(date); |
||||
checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday
|
||||
var time = checkDate.getTime(); |
||||
checkDate.setMonth(0); // Compare with Jan 1
|
||||
checkDate.setDate(1); |
||||
return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; |
||||
} |
||||
|
||||
ctrl.handleKeyDown = function(key, evt) { |
||||
var date = ctrl.activeDate.getDate(); |
||||
|
||||
if (key === 'left') { |
||||
date = date - 1; // up
|
||||
} else if (key === 'up') { |
||||
date = date - 7; // down
|
||||
} else if (key === 'right') { |
||||
date = date + 1; // down
|
||||
} else if (key === 'down') { |
||||
date = date + 7; |
||||
} else if (key === 'pageup' || key === 'pagedown') { |
||||
var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1); |
||||
ctrl.activeDate.setMonth(month, 1); |
||||
date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date); |
||||
} else if (key === 'home') { |
||||
date = 1; |
||||
} else if (key === 'end') { |
||||
date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()); |
||||
} |
||||
ctrl.activeDate.setDate(date); |
||||
}; |
||||
|
||||
ctrl.refreshView(); |
||||
} |
||||
}; |
||||
}]) |
||||
|
||||
.directive('monthpicker', ['dateFilter', function(dateFilter) { |
||||
return { |
||||
restrict: 'EA', |
||||
replace: true, |
||||
templateUrl: 'template/datepicker/month.html', |
||||
require: '^datepicker', |
||||
link: function(scope, element, attrs, ctrl) { |
||||
ctrl.step = { years: 1 }; |
||||
ctrl.element = element; |
||||
|
||||
ctrl._refreshView = function() { |
||||
var months = new Array(12), |
||||
year = ctrl.activeDate.getFullYear(), |
||||
date; |
||||
|
||||
for (var i = 0; i < 12; i++) { |
||||
date = new Date(year, i, 1); |
||||
ctrl.fixTimeZone(date); |
||||
months[i] = angular.extend(ctrl.createDateObject(date, ctrl.formatMonth), { |
||||
uid: scope.uniqueId + '-' + i |
||||
}); |
||||
} |
||||
|
||||
scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle); |
||||
scope.rows = ctrl.split(months, 3); |
||||
}; |
||||
|
||||
ctrl.compare = function(date1, date2) { |
||||
return new Date(date1.getFullYear(), date1.getMonth()) - new Date(date2.getFullYear(), date2.getMonth()); |
||||
}; |
||||
|
||||
ctrl.handleKeyDown = function(key, evt) { |
||||
var date = ctrl.activeDate.getMonth(); |
||||
|
||||
if (key === 'left') { |
||||
date = date - 1; // up
|
||||
} else if (key === 'up') { |
||||
date = date - 3; // down
|
||||
} else if (key === 'right') { |
||||
date = date + 1; // down
|
||||
} else if (key === 'down') { |
||||
date = date + 3; |
||||
} else if (key === 'pageup' || key === 'pagedown') { |
||||
var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); |
||||
ctrl.activeDate.setFullYear(year); |
||||
} else if (key === 'home') { |
||||
date = 0; |
||||
} else if (key === 'end') { |
||||
date = 11; |
||||
} |
||||
ctrl.activeDate.setMonth(date); |
||||
}; |
||||
|
||||
ctrl.refreshView(); |
||||
} |
||||
}; |
||||
}]) |
||||
|
||||
.directive('yearpicker', ['dateFilter', function(dateFilter) { |
||||
return { |
||||
restrict: 'EA', |
||||
replace: true, |
||||
templateUrl: 'template/datepicker/year.html', |
||||
require: '^datepicker', |
||||
link: function(scope, element, attrs, ctrl) { |
||||
var range = ctrl.yearRange; |
||||
|
||||
ctrl.step = { years: range }; |
||||
ctrl.element = element; |
||||
|
||||
function getStartingYear( year ) { |
||||
return parseInt((year - 1) / range, 10) * range + 1; |
||||
} |
||||
|
||||
ctrl._refreshView = function() { |
||||
var years = new Array(range), date; |
||||
|
||||
for (var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++) { |
||||
date = new Date(start + i, 0, 1); |
||||
ctrl.fixTimeZone(date); |
||||
years[i] = angular.extend(ctrl.createDateObject(date, ctrl.formatYear), { |
||||
uid: scope.uniqueId + '-' + i |
||||
}); |
||||
} |
||||
|
||||
scope.title = [years[0].label, years[range - 1].label].join(' - '); |
||||
scope.rows = ctrl.split(years, 5); |
||||
}; |
||||
|
||||
ctrl.compare = function(date1, date2) { |
||||
return date1.getFullYear() - date2.getFullYear(); |
||||
}; |
||||
|
||||
ctrl.handleKeyDown = function(key, evt) { |
||||
var date = ctrl.activeDate.getFullYear(); |
||||
|
||||
if (key === 'left') { |
||||
date = date - 1; // up
|
||||
} else if (key === 'up') { |
||||
date = date - 5; // down
|
||||
} else if (key === 'right') { |
||||
date = date + 1; // down
|
||||
} else if (key === 'down') { |
||||
date = date + 5; |
||||
} else if (key === 'pageup' || key === 'pagedown') { |
||||
date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years; |
||||
} else if (key === 'home') { |
||||
date = getStartingYear(ctrl.activeDate.getFullYear()); |
||||
} else if (key === 'end') { |
||||
date = getStartingYear(ctrl.activeDate.getFullYear()) + range - 1; |
||||
} |
||||
ctrl.activeDate.setFullYear(date); |
||||
}; |
||||
|
||||
ctrl.refreshView(); |
||||
} |
||||
}; |
||||
}]) |
||||
|
||||
.constant('datepickerPopupConfig', { |
||||
datepickerPopup: 'yyyy-MM-dd', |
||||
datepickerPopupTemplateUrl: 'template/datepicker/popup.html', |
||||
datepickerTemplateUrl: 'template/datepicker/datepicker.html', |
||||
html5Types: { |
||||
date: 'yyyy-MM-dd', |
||||
'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss', |
||||
'month': 'yyyy-MM' |
||||
}, |
||||
currentText: 'Today', |
||||
clearText: 'Clear', |
||||
closeText: 'Done', |
||||
closeOnDateSelection: true, |
||||
appendToBody: false, |
||||
showButtonBar: true, |
||||
onOpenFocus: true |
||||
}) |
||||
|
||||
.directive('datepickerPopup', ['$compile', '$parse', '$document', '$rootScope', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig', '$timeout', |
||||
function($compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout) { |
||||
return { |
||||
restrict: 'EA', |
||||
require: 'ngModel', |
||||
scope: { |
||||
isOpen: '=?', |
||||
currentText: '@', |
||||
clearText: '@', |
||||
closeText: '@', |
||||
dateDisabled: '&', |
||||
customClass: '&' |
||||
}, |
||||
link: function(scope, element, attrs, ngModel) { |
||||
var dateFormat, |
||||
closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection, |
||||
appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody, |
||||
onOpenFocus = angular.isDefined(attrs.onOpenFocus) ? scope.$parent.$eval(attrs.onOpenFocus) : datepickerPopupConfig.onOpenFocus, |
||||
datepickerPopupTemplateUrl = angular.isDefined(attrs.datepickerPopupTemplateUrl) ? attrs.datepickerPopupTemplateUrl : datepickerPopupConfig.datepickerPopupTemplateUrl, |
||||
datepickerTemplateUrl = angular.isDefined(attrs.datepickerTemplateUrl) ? attrs.datepickerTemplateUrl : datepickerPopupConfig.datepickerTemplateUrl, |
||||
cache = {}; |
||||
|
||||
scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar; |
||||
|
||||
scope.getText = function(key) { |
||||
return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; |
||||
}; |
||||
|
||||
scope.isDisabled = function(date) { |
||||
if (date === 'today') { |
||||
date = new Date(); |
||||
} |
||||
|
||||
return ((scope.watchData.minDate && scope.compare(date, cache.minDate) < 0) || |
||||
(scope.watchData.maxDate && scope.compare(date, cache.maxDate) > 0)); |
||||
}; |
||||
|
||||
scope.compare = function(date1, date2) { |
||||
return (new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate())); |
||||
}; |
||||
|
||||
var isHtml5DateInput = false; |
||||
if (datepickerPopupConfig.html5Types[attrs.type]) { |
||||
dateFormat = datepickerPopupConfig.html5Types[attrs.type]; |
||||
isHtml5DateInput = true; |
||||
} else { |
||||
dateFormat = attrs.datepickerPopup || datepickerPopupConfig.datepickerPopup; |
||||
attrs.$observe('datepickerPopup', function(value, oldValue) { |
||||
var newDateFormat = value || datepickerPopupConfig.datepickerPopup; |
||||
// Invalidate the $modelValue to ensure that formatters re-run
|
||||
// FIXME: Refactor when PR is merged: https://github.com/angular/angular.js/pull/10764
|
||||
if (newDateFormat !== dateFormat) { |
||||
dateFormat = newDateFormat; |
||||
ngModel.$modelValue = null; |
||||
|
||||
if (!dateFormat) { |
||||
throw new Error('datepickerPopup must have a date format specified.'); |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
||||
if (!dateFormat) { |
||||
throw new Error('datepickerPopup must have a date format specified.'); |
||||
} |
||||
|
||||
if (isHtml5DateInput && attrs.datepickerPopup) { |
||||
throw new Error('HTML5 date input types do not support custom formats.'); |
||||
} |
||||
|
||||
// popup element used to display calendar
|
||||
var popupEl = angular.element('<div datepicker-popup-wrap><div datepicker></div></div>'); |
||||
popupEl.attr({ |
||||
'ng-model': 'date', |
||||
'ng-change': 'dateSelection(date)', |
||||
'template-url': datepickerPopupTemplateUrl |
||||
}); |
||||
|
||||
function cameltoDash(string) { |
||||
return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); |
||||
} |
||||
|
||||
// datepicker element
|
||||
var datepickerEl = angular.element(popupEl.children()[0]); |
||||
datepickerEl.attr('template-url', datepickerTemplateUrl); |
||||
|
||||
if (isHtml5DateInput) { |
||||
if (attrs.type === 'month') { |
||||
datepickerEl.attr('datepicker-mode', '"month"'); |
||||
datepickerEl.attr('min-mode', 'month'); |
||||
} |
||||
} |
||||
|
||||
if (attrs.datepickerOptions) { |
||||
var options = scope.$parent.$eval(attrs.datepickerOptions); |
||||
if (options && options.initDate) { |
||||
scope.initDate = options.initDate; |
||||
datepickerEl.attr('init-date', 'initDate'); |
||||
delete options.initDate; |
||||
} |
||||
angular.forEach(options, function(value, option) { |
||||
datepickerEl.attr(cameltoDash(option), value); |
||||
}); |
||||
} |
||||
|
||||
scope.watchData = {}; |
||||
angular.forEach(['minMode', 'maxMode', 'minDate', 'maxDate', 'datepickerMode', 'initDate', 'shortcutPropagation'], function(key) { |
||||
if (attrs[key]) { |
||||
var getAttribute = $parse(attrs[key]); |
||||
scope.$parent.$watch(getAttribute, function(value) { |
||||
scope.watchData[key] = value; |
||||
if (key === 'minDate' || key === 'maxDate') { |
||||
cache[key] = new Date(value); |
||||
} |
||||
}); |
||||
datepickerEl.attr(cameltoDash(key), 'watchData.' + key); |
||||
|
||||
// Propagate changes from datepicker to outside
|
||||
if (key === 'datepickerMode') { |
||||
var setAttribute = getAttribute.assign; |
||||
scope.$watch('watchData.' + key, function(value, oldvalue) { |
||||
if (angular.isFunction(setAttribute) && value !== oldvalue) { |
||||
setAttribute(scope.$parent, value); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
}); |
||||
if (attrs.dateDisabled) { |
||||
datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); |
||||
} |
||||
|
||||
if (attrs.showWeeks) { |
||||
datepickerEl.attr('show-weeks', attrs.showWeeks); |
||||
} |
||||
|
||||
if (attrs.customClass) { |
||||
datepickerEl.attr('custom-class', 'customClass({ date: date, mode: mode })'); |
||||
} |
||||
|
||||
function parseDate(viewValue) { |
||||
if (angular.isNumber(viewValue)) { |
||||
// presumably timestamp to date object
|
||||
viewValue = new Date(viewValue); |
||||
} |
||||
|
||||
if (!viewValue) { |
||||
return null; |
||||
} else if (angular.isDate(viewValue) && !isNaN(viewValue)) { |
||||
return viewValue; |
||||
} else if (angular.isString(viewValue)) { |
||||
var date = dateParser.parse(viewValue, dateFormat, scope.date); |
||||
if (isNaN(date)) { |
||||
return undefined; |
||||
} else { |
||||
return date; |
||||
} |
||||
} else { |
||||
return undefined; |
||||
} |
||||
} |
||||
|
||||
function validator(modelValue, viewValue) { |
||||
var value = modelValue || viewValue; |
||||
|
||||
if (!attrs.ngRequired && !value) { |
||||
return true; |
||||
} |
||||
|
||||
if (angular.isNumber(value)) { |
||||
value = new Date(value); |
||||
} |
||||
if (!value) { |
||||
return true; |
||||
} else if (angular.isDate(value) && !isNaN(value)) { |
||||
return true; |
||||
} else if (angular.isString(value)) { |
||||
var date = dateParser.parse(value, dateFormat); |
||||
return !isNaN(date); |
||||
} else { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
if (!isHtml5DateInput) { |
||||
// Internal API to maintain the correct ng-invalid-[key] class
|
||||
ngModel.$$parserName = 'date'; |
||||
ngModel.$validators.date = validator; |
||||
ngModel.$parsers.unshift(parseDate); |
||||
ngModel.$formatters.push(function(value) { |
||||
scope.date = value; |
||||
return ngModel.$isEmpty(value) ? value : dateFilter(value, dateFormat); |
||||
}); |
||||
} else { |
||||
ngModel.$formatters.push(function(value) { |
||||
scope.date = value; |
||||
return value; |
||||
}); |
||||
} |
||||
|
||||
// Inner change
|
||||
scope.dateSelection = function(dt) { |
||||
if (angular.isDefined(dt)) { |
||||
scope.date = dt; |
||||
} |
||||
var date = scope.date ? dateFilter(scope.date, dateFormat) : null; // Setting to NULL is necessary for form validators to function
|
||||
element.val(date); |
||||
ngModel.$setViewValue(date); |
||||
|
||||
if (closeOnDateSelection) { |
||||
scope.isOpen = false; |
||||
element[0].focus(); |
||||
} |
||||
}; |
||||
|
||||
// Detect changes in the view from the text box
|
||||
ngModel.$viewChangeListeners.push(function() { |
||||
scope.date = dateParser.parse(ngModel.$viewValue, dateFormat, scope.date); |
||||
}); |
||||
|
||||
var documentClickBind = function(event) { |
||||
var popup = $popup[0]; |
||||
var dpContainsTarget = element[0].contains(event.target); |
||||
// The popup node may not be an element node
|
||||
// In some browsers (IE) only element nodes have the 'contains' function
|
||||
var popupContainsTarget = popup.contains !== undefined && popup.contains(event.target); |
||||
if (scope.isOpen && !(dpContainsTarget || popupContainsTarget)) { |
||||
scope.$apply(function() { |
||||
scope.isOpen = false; |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
var inputKeydownBind = function(evt) { |
||||
if (evt.which === 27 && scope.isOpen) { |
||||
evt.preventDefault(); |
||||
evt.stopPropagation(); |
||||
scope.$apply(function() { |
||||
scope.isOpen = false; |
||||
}); |
||||
element[0].focus(); |
||||
} else if (evt.which === 40 && !scope.isOpen) { |
||||
evt.preventDefault(); |
||||
evt.stopPropagation(); |
||||
scope.$apply(function() { |
||||
scope.isOpen = true; |
||||
}); |
||||
} |
||||
}; |
||||
element.bind('keydown', inputKeydownBind); |
||||
|
||||
scope.keydown = function(evt) { |
||||
if (evt.which === 27) { |
||||
scope.isOpen = false; |
||||
element[0].focus(); |
||||
} |
||||
}; |
||||
|
||||
scope.$watch('isOpen', function(value) { |
||||
if (value) { |
||||
scope.position = appendToBody ? $position.offset(element) : $position.position(element); |
||||
scope.position.top = scope.position.top + element.prop('offsetHeight'); |
||||
|
||||
$timeout(function() { |
||||
if (onOpenFocus) { |
||||
scope.$broadcast('datepicker.focus'); |
||||
} |
||||
$document.bind('click', documentClickBind); |
||||
}, 0, false); |
||||
} else { |
||||
$document.unbind('click', documentClickBind); |
||||
} |
||||
}); |
||||
|
||||
scope.select = function(date) { |
||||
if (date === 'today') { |
||||
var today = new Date(); |
||||
if (angular.isDate(scope.date)) { |
||||
date = new Date(scope.date); |
||||
date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); |
||||
} else { |
||||
date = new Date(today.setHours(0, 0, 0, 0)); |
||||
} |
||||
} |
||||
scope.dateSelection(date); |
||||
}; |
||||
|
||||
scope.close = function() { |
||||
scope.isOpen = false; |
||||
element[0].focus(); |
||||
}; |
||||
|
||||
var $popup = $compile(popupEl)(scope); |
||||
// Prevent jQuery cache memory leak (template is now redundant after linking)
|
||||
popupEl.remove(); |
||||
|
||||
if (appendToBody) { |
||||
$document.find('body').append($popup); |
||||
} else { |
||||
element.after($popup); |
||||
} |
||||
|
||||
scope.$on('$destroy', function() { |
||||
if (scope.isOpen === true) { |
||||
if (!$rootScope.$$phase) { |
||||
scope.$apply(function() { |
||||
scope.isOpen = false; |
||||
}); |
||||
} |
||||
} |
||||
|
||||
$popup.remove(); |
||||
element.unbind('keydown', inputKeydownBind); |
||||
$document.unbind('click', documentClickBind); |
||||
}); |
||||
} |
||||
}; |
||||
}]) |
||||
|
||||
.directive('datepickerPopupWrap', function() { |
||||
return { |
||||
restrict:'EA', |
||||
replace: true, |
||||
transclude: true, |
||||
templateUrl: function(element, attrs) { |
||||
return attrs.templateUrl || 'template/datepicker/popup.html'; |
||||
} |
||||
}; |
||||
}); |
@ -0,0 +1,150 @@ |
||||
angular.module('ui.bootstrap.position', []) |
||||
|
||||
/** |
||||
* A set of utility methods that can be use to retrieve position of DOM elements. |
||||
* It is meant to be used where we need to absolute-position DOM elements in |
||||
* relation to other, existing elements (this is the case for tooltips, popovers, |
||||
* typeahead suggestions etc.). |
||||
*/ |
||||
.factory('$position', ['$document', '$window', function($document, $window) { |
||||
function getStyle(el, cssprop) { |
||||
if (el.currentStyle) { //IE
|
||||
return el.currentStyle[cssprop]; |
||||
} else if ($window.getComputedStyle) { |
||||
return $window.getComputedStyle(el)[cssprop]; |
||||
} |
||||
// finally try and get inline style
|
||||
return el.style[cssprop]; |
||||
} |
||||
|
||||
/** |
||||
* Checks if a given element is statically positioned |
||||
* @param element - raw DOM element |
||||
*/ |
||||
function isStaticPositioned(element) { |
||||
return (getStyle(element, 'position') || 'static' ) === 'static'; |
||||
} |
||||
|
||||
/** |
||||
* returns the closest, non-statically positioned parentOffset of a given element |
||||
* @param element |
||||
*/ |
||||
var parentOffsetEl = function(element) { |
||||
var docDomEl = $document[0]; |
||||
var offsetParent = element.offsetParent || docDomEl; |
||||
while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { |
||||
offsetParent = offsetParent.offsetParent; |
||||
} |
||||
return offsetParent || docDomEl; |
||||
}; |
||||
|
||||
return { |
||||
/** |
||||
* Provides read-only equivalent of jQuery's position function: |
||||
* http://api.jquery.com/position/
|
||||
*/ |
||||
position: function(element) { |
||||
var elBCR = this.offset(element); |
||||
var offsetParentBCR = { top: 0, left: 0 }; |
||||
var offsetParentEl = parentOffsetEl(element[0]); |
||||
if (offsetParentEl != $document[0]) { |
||||
offsetParentBCR = this.offset(angular.element(offsetParentEl)); |
||||
offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; |
||||
offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; |
||||
} |
||||
|
||||
var boundingClientRect = element[0].getBoundingClientRect(); |
||||
return { |
||||
width: boundingClientRect.width || element.prop('offsetWidth'), |
||||
height: boundingClientRect.height || element.prop('offsetHeight'), |
||||
top: elBCR.top - offsetParentBCR.top, |
||||
left: elBCR.left - offsetParentBCR.left |
||||
}; |
||||
}, |
||||
|
||||
/** |
||||
* Provides read-only equivalent of jQuery's offset function: |
||||
* http://api.jquery.com/offset/
|
||||
*/ |
||||
offset: function(element) { |
||||
var boundingClientRect = element[0].getBoundingClientRect(); |
||||
return { |
||||
width: boundingClientRect.width || element.prop('offsetWidth'), |
||||
height: boundingClientRect.height || element.prop('offsetHeight'), |
||||
top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop), |
||||
left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft) |
||||
}; |
||||
}, |
||||
|
||||
/** |
||||
* Provides coordinates for the targetEl in relation to hostEl |
||||
*/ |
||||
positionElements: function(hostEl, targetEl, positionStr, appendToBody) { |
||||
var positionStrParts = positionStr.split('-'); |
||||
var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center'; |
||||
|
||||
var hostElPos, |
||||
targetElWidth, |
||||
targetElHeight, |
||||
targetElPos; |
||||
|
||||
hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl); |
||||
|
||||
targetElWidth = targetEl.prop('offsetWidth'); |
||||
targetElHeight = targetEl.prop('offsetHeight'); |
||||
|
||||
var shiftWidth = { |
||||
center: function() { |
||||
return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2; |
||||
}, |
||||
left: function() { |
||||
return hostElPos.left; |
||||
}, |
||||
right: function() { |
||||
return hostElPos.left + hostElPos.width; |
||||
} |
||||
}; |
||||
|
||||
var shiftHeight = { |
||||
center: function() { |
||||
return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2; |
||||
}, |
||||
top: function() { |
||||
return hostElPos.top; |
||||
}, |
||||
bottom: function() { |
||||
return hostElPos.top + hostElPos.height; |
||||
} |
||||
}; |
||||
|
||||
switch (pos0) { |
||||
case 'right': |
||||
targetElPos = { |
||||
top: shiftHeight[pos1](), |
||||
left: shiftWidth[pos0]() |
||||
}; |
||||
break; |
||||
case 'left': |
||||
targetElPos = { |
||||
top: shiftHeight[pos1](), |
||||
left: hostElPos.left - targetElWidth |
||||
}; |
||||
break; |
||||
case 'bottom': |
||||
targetElPos = { |
||||
top: shiftHeight[pos0](), |
||||
left: shiftWidth[pos1]() |
||||
}; |
||||
break; |
||||
default: |
||||
targetElPos = { |
||||
top: hostElPos.top - targetElHeight, |
||||
left: shiftWidth[pos1]() |
||||
}; |
||||
break; |
||||
} |
||||
|
||||
return targetElPos; |
||||
} |
||||
}; |
||||
}]); |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue