feat(timepicker2): more progress

This commit is contained in:
Torkel Ödegaard 2015-09-18 10:36:47 +02:00
parent 2a52d9bdf6
commit f93215f4ec
11 changed files with 38 additions and 3112 deletions

View File

@ -16,8 +16,8 @@ var spans = {
var rangeOptions = [
{ from: 'now/d', to: 'now/d', display: 'Today', section: 2 },
{ from: 'now/w', to: 'now/w', display: 'This week', section: 2 },
{ from: 'now/d', to: 'now', display: 'The day so far', section: 2 },
{ from: 'now/w', to: 'now/w', display: 'This week', section: 2 },
{ from: 'now/w', to: 'now', display: 'Week to date', section: 2 },
{ from: 'now/M', to: 'now/M', display: 'This month', section: 2 },
{ from: 'now/y', to: 'now/y', display: 'This year', section: 2 },

View File

@ -23,7 +23,7 @@ define([
this._parseTime();
if(this.dashboard.refresh) {
this.set_interval(this.dashboard.refresh);
this.setAutoRefresh(this.dashboard.refresh);
}
};
@ -64,7 +64,7 @@ define([
}
};
this.set_interval = function (interval) {
this.setAutoRefresh = function (interval) {
this.dashboard.refresh = interval;
if (interval) {
var _i = kbn.interval_to_ms(interval);
@ -96,10 +96,10 @@ define([
// disable refresh if we have an absolute time
if (_.isString(time.to) && time.to.indexOf('now') === -1) {
this.old_refresh = this.dashboard.refresh || this.old_refresh;
this.set_interval(false);
this.setAutoRefresh(false);
}
else if (this.old_refresh && this.old_refresh !== this.dashboard.refresh) {
this.set_interval(this.old_refresh);
this.setAutoRefresh(this.old_refresh);
this.old_refresh = null;
}

View File

@ -22,9 +22,10 @@
</div>
<label class="small">Refreshing every:</label>
<select ng-model="currentRefresh" class='input-large' ng-options="f for f in refreshOptions"></select>
<select ng-model="refresh.value" class='input-medium' ng-options="f.value as f.text for f in refresh.options">
</select>
<button class="btn btn-inverse pull-right" type="button" ng-click="applyCustomTimeRange()">
<button class="btn btn-inverse gf-timepicker-btn-apply" type="button" ng-click="ctrl.applyCustom()">
Apply
</button>
</div>
@ -41,55 +42,3 @@
</div>
<div class="clearfix"></div>
<!-- <tabset> -->
<!-- <tab heading="Relative"> -->
<!-- -->
<!-- <div style="float:right; width: 200px" ng&#45;repeat="group in timeOptions"> -->
<!-- <ul> -->
<!-- <li bindonce ng&#45;repeat='option in group'> -->
<!-- <a ng&#45;click="ctrl.setRelativeFilter(option)" bo&#45;text="option.display"></a> -->
<!-- </li> -->
<!-- </ul> -->
<!-- </div> -->
<!-- -->
<!-- </tab> -->
<!-- <tab heading="Absolute"> -->
<!-- -->
<!-- <form name="timeForm" style="margin: 0 20px 20px 20px"> -->
<!-- <div class="gf&#45;timepicker&#45;section"> -->
<!-- <div> -->
<!-- <div class="tight&#45;form last"> -->
<!-- </div> -->
<!-- <label class="small">From:</label> -->
<!-- <input type="text" required class="input&#45;large" ng&#45;model="absolute.from" input&#45;datetime="MMMM Do YYYY, HH:mm:ss.SSS"> -->
<!-- <br> -->
<!-- </div> -->
<!-- <datepicker ng&#45;model="absolute.from" class="gf&#45;timepicker&#45;component" show&#45;weeks="false"></datepicker> -->
<!-- </div> -->
<!-- <div class="gf&#45;timepicker&#45;section"> -->
<!-- <div> -->
<!-- <label class="small">To:</label> -->
<!-- <input type="text" required class="input&#45;large" ng&#45;model="absolute.to" input&#45;datetime="MMMM Do YYYY, HH:mm:ss.SSS"> -->
<!-- <br> -->
<!-- </div> -->
<!-- <datepicker ng&#45;model="absolute.to" class="gf&#45;timepicker&#45;component" show&#45;weeks="false"></datepicker> -->
<!-- </div> -->
<!-- <div class="clearfix"></div> -->
<!-- </form> -->
<!-- </tab> -->
<!-- </tabset> -->
<!-- -->
<!-- <!&#45;&#45; <!&#38;#45;&#38;#45; Auto refresh submenu &#38;#45;&#38;#45;> &#45;&#45;> -->
<!-- <li class="dropdown&#45;submenu"> -->
<!-- <a href="#">Auto&#45;Refresh</a> -->
<!-- <ul class="dropdown&#45;menu" ng&#45;class="{'dropdown&#45;submenu&#45;left': refreshMenuLeftSide}"> -->
<!-- <li> -->
<!-- <a ng&#45;click="timeSrv.set_interval(false)">Off</a> -->
<!-- </li> -->
<!-- <li bindonce ng&#45;repeat="interval in panel.refresh_intervals track by $index"> -->
<!-- <a ng&#45;click="timeSrv.set_interval(interval)" bo&#45;text="'Every ' + interval"></a> -->
<!-- </li> -->
<!-- </ul> -->
<!-- </li> -->
<!-- <li><a ng&#45;click="ctrl.customTime()">Custom</a></li> -->
<!-- </div> -->

View File

@ -1,4 +1,4 @@
<ul class="nav timepicker-dropdown">
<ul class="nav gf-timepicker-nav">
<li class="grafana-menu-zoom-out">
<a class='small' ng-click='zoom(2)'>
@ -7,7 +7,7 @@
</li>
<li>
<a class="timepicker-dropdown" bs-tooltip="tooltip" data-placement="bottom" ng-click="ctrl.openDropdown()">
<a bs-tooltip="tooltip" data-placement="bottom" ng-click="ctrl.openDropdown()">
<i class="fa fa-clock-o"></i>
<span ng-bind="rangeString"></span>

View File

@ -54,7 +54,7 @@ export class TimePickerCtrl {
this.$scope.dashboard.formatDate(time.to, format);
}
this.$scope.absolute = {form: time.from.toDate(), to: time.to.toDate()};
this.$scope.absoluteJs = {form: time.from.toDate(), to: time.to.toDate()};
this.$scope.timeRaw = timeRaw;
this.$scope.tooltip = this.$scope.dashboard.formatDate(time.from) + ' <br>to<br>';
this.$scope.tooltip += this.$scope.dashboard.formatDate(time.to);
@ -66,9 +66,14 @@ export class TimePickerCtrl {
openDropdown() {
this.$scope.timeOptions = rangeUtil.getRelativeTimesList(this.$scope.panel, this.$scope.rangeString);
this.$scope.currentRefresh = this.$scope.dashboard.refresh || 'off';
this.$scope.refreshOptions = this.$scope.panel.refresh_intervals;
this.$scope.refreshOptions.unshift('off');
this.$scope.refresh = {
value: this.$scope.dashboard.refresh,
options: _.map(this.$scope.panel.refresh_intervals, (interval: any) => {
return {text: interval, value: interval};
})
};
this.$scope.refresh.options.unshift({text: 'off'});
this.$scope.appEvent('show-dash-editor', {
src: 'app/features/dashboard/timepicker/dropdown.html',
@ -77,36 +82,14 @@ export class TimePickerCtrl {
});
}
customTime() {
// Assume the form is valid since we're setting it to something valid
this.$scope.input.$setValidity("dummy", true);
this.$scope.temptime = angular.copy(this.$scope.time);
this.$scope.temptime.now = this.$scope.panel.now;
// this.$scope.temptime.from.date.setHours(0, 0, 0, 0);
// this.$scope.temptime.to.date.setHours(0, 0, 0, 0);
// Date picker needs the date to be at the start of the day
if (new Date().getTimezoneOffset() < 0) {
this.$scope.temptime.from.date = moment(this.$scope.temptime.from.date).add(1, 'days').toDate();
this.$scope.temptime.to.date = moment(this.$scope.temptime.to.date).add(1, 'days').toDate();
applyCustom() {
console.log(this.$scope.refresh.value);
if (this.$scope.refresh.value !== this.$scope.dashboard.refresh) {
this.timeSrv.setAutoRefresh(this.$scope.refresh.value);
}
this.$scope.appEvent('show-dash-editor', {
src: 'app/features/dashboard/timepicker/custom.html',
scope: this.$scope
});
}
setAbsoluteTimeFilter(time) {
// Create filter object
var _filter = _.clone(time);
if (time.now) {
_filter.to = "now";
}
this.timeSrv.setTime(_filter);
this.timeSrv.setTime(this.$scope.timeRaw);
this.$scope.appEvent('hide-dash-editor');
}
setRelativeFilter(timespan) {
@ -122,35 +105,6 @@ export class TimePickerCtrl {
this.$scope.appEvent('hide-dash-editor');
}
validate(time): any {
// Assume the form is valid. There is a hidden dummy input for invalidating it programatically.
this.$scope.input.$setValidity("dummy", true);
var _from = this.datepickerToLocal(time.from.date);
var _to = this.datepickerToLocal(time.to.date);
var _t = time;
if (this.$scope.input.$valid) {
_from.setHours(_t.from.hour, _t.from.minute, _t.from.second, _t.from.millisecond);
_to.setHours(_t.to.hour, _t.to.minute, _t.to.second, _t.to.millisecond);
// Check that the objects are valid and to is after from
if (isNaN(_from.getTime()) || isNaN(_to.getTime()) || _from.getTime() >= _to.getTime()) {
this.$scope.input.$setValidity("dummy", false);
return false;
}
} else {
return false;
}
return { from: _from, to: _to, now: time.now };
}
datepickerToLocal(date) {
date = moment(date).clone().toDate();
return moment(new Date(date.getTime() + date.getTimezoneOffset() * 60000)).toDate();
}
}
export function settingsDirective() {

View File

@ -1,36 +1,5 @@
.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;
.nav.gf-timepicker-nav {
margin-right: 0;
}
.timepicker-timestring {
@ -38,7 +7,7 @@ div.timepicker-now {
}
.gf-timepicker-dropdown {
margin: 0px;
margin: 0px 0 15px 0;
padding: 10px 20px;
float: right;
background-color: @grafanaPanelBackground;
@ -49,11 +18,19 @@ div.timepicker-now {
width: 300px;
float: left;
border-right: @grafanaTriggerBorder;
padding: 0 20px 0 20px;
padding: 0 0 0 20px;
select {
width: 183px;
margin-bottom: 0;
}
}
.gf-timepicker-btn-apply {
margin: 0 0 0 15px;
}
.gf-timepicker-relative-section {
padding: 0 20px 0 20px;
padding: 0 20px 0 25px;
min-height: 258px;
float: left;
ul {

View File

@ -1,212 +0,0 @@
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;
}
}]);

View File

@ -1,823 +0,0 @@
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';
}
};
});

View File

@ -1,150 +0,0 @@
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;
}
};
}]);

View File

@ -1,293 +0,0 @@
/**
* @ngdoc overview
* @name ui.bootstrap.tabs
*
* @description
* AngularJS version of the tabs directive.
*/
angular.module('ui.bootstrap.tabs', [])
.controller('TabsetController', ['$scope', function TabsetCtrl($scope) {
var ctrl = this,
tabs = ctrl.tabs = $scope.tabs = [];
ctrl.select = function(selectedTab) {
angular.forEach(tabs, function(tab) {
if (tab.active && tab !== selectedTab) {
tab.active = false;
tab.onDeselect();
}
});
selectedTab.active = true;
selectedTab.onSelect();
};
ctrl.addTab = function addTab(tab) {
tabs.push(tab);
// we can't run the select function on the first tab
// since that would select it twice
if (tabs.length === 1 && tab.active !== false) {
tab.active = true;
} else if (tab.active) {
ctrl.select(tab);
}
else {
tab.active = false;
}
};
ctrl.removeTab = function removeTab(tab) {
var index = tabs.indexOf(tab);
//Select a new tab if the tab to be removed is selected and not destroyed
if (tab.active && tabs.length > 1 && !destroyed) {
//If this is the last tab, select the previous tab. else, the next tab.
var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1;
ctrl.select(tabs[newActiveIndex]);
}
tabs.splice(index, 1);
};
var destroyed;
$scope.$on('$destroy', function() {
destroyed = true;
});
}])
/**
* @ngdoc directive
* @name ui.bootstrap.tabs.directive:tabset
* @restrict EA
*
* @description
* Tabset is the outer container for the tabs directive
*
* @param {boolean=} vertical Whether or not to use vertical styling for the tabs.
* @param {boolean=} justified Whether or not to use justified styling for the tabs.
*
* @example
<example module="ui.bootstrap">
<file name="index.html">
<tabset>
<tab heading="Tab 1"><b>First</b> Content!</tab>
<tab heading="Tab 2"><i>Second</i> Content!</tab>
</tabset>
<hr />
<tabset vertical="true">
<tab heading="Vertical Tab 1"><b>First</b> Vertical Content!</tab>
<tab heading="Vertical Tab 2"><i>Second</i> Vertical Content!</tab>
</tabset>
<tabset justified="true">
<tab heading="Justified Tab 1"><b>First</b> Justified Content!</tab>
<tab heading="Justified Tab 2"><i>Second</i> Justified Content!</tab>
</tabset>
</file>
</example>
*/
.directive('tabset', function() {
return {
restrict: 'EA',
transclude: true,
replace: true,
scope: {
type: '@'
},
controller: 'TabsetController',
templateUrl: 'app/partials/bootstrap/tabset.html',
link: function(scope, element, attrs) {
scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false;
scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false;
}
};
})
/**
* @ngdoc directive
* @name ui.bootstrap.tabs.directive:tab
* @restrict EA
*
* @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}.
* @param {string=} select An expression to evaluate when the tab is selected.
* @param {boolean=} active A binding, telling whether or not this tab is selected.
* @param {boolean=} disabled A binding, telling whether or not this tab is disabled.
*
* @description
* Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}.
*
* @example
<example module="ui.bootstrap">
<file name="index.html">
<div ng-controller="TabsDemoCtrl">
<button class="btn btn-small" ng-click="items[0].active = true">
Select item 1, using active binding
</button>
<button class="btn btn-small" ng-click="items[1].disabled = !items[1].disabled">
Enable/disable item 2, using disabled binding
</button>
<br />
<tabset>
<tab heading="Tab 1">First Tab</tab>
<tab select="alertMe()">
<tab-heading><i class="icon-bell"></i> Alert me!</tab-heading>
Second Tab, with alert callback and html heading!
</tab>
<tab ng-repeat="item in items"
heading="{{item.title}}"
disabled="item.disabled"
active="item.active">
{{item.content}}
</tab>
</tabset>
</div>
</file>
<file name="script.js">
function TabsDemoCtrl($scope) {
$scope.items = [
{ title:"Dynamic Title 1", content:"Dynamic Item 0" },
{ title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true }
];
$scope.alertMe = function() {
setTimeout(function() {
alert("You've selected the alert tab!");
});
};
};
</file>
</example>
*/
/**
* @ngdoc directive
* @name ui.bootstrap.tabs.directive:tabHeading
* @restrict EA
*
* @description
* Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element.
*
* @example
<example module="ui.bootstrap">
<file name="index.html">
<tabset>
<tab>
<tab-heading><b>HTML</b> in my titles?!</tab-heading>
And some content, too!
</tab>
<tab>
<tab-heading><i class="icon-heart"></i> Icon heading?!?</tab-heading>
That's right.
</tab>
</tabset>
</file>
</example>
*/
.directive('tab', ['$parse', '$log', function($parse, $log) {
return {
require: '^tabset',
restrict: 'EA',
replace: true,
templateUrl: 'app/partials/bootstrap/tab.html',
transclude: true,
scope: {
active: '=?',
heading: '@',
onSelect: '&select', //This callback is called in contentHeadingTransclude
//once it inserts the tab's content into the dom
onDeselect: '&deselect'
},
controller: function() {
//Empty controller so other directives can require being 'under' a tab
},
compile: function(elm, attrs, transclude) {
return function postLink(scope, elm, attrs, tabsetCtrl) {
scope.$watch('active', function(active) {
if (active) {
tabsetCtrl.select(scope);
}
});
scope.disabled = false;
if ( attrs.disable ) {
scope.$parent.$watch($parse(attrs.disable), function(value) {
scope.disabled = !! value;
});
}
// Deprecation support of "disabled" parameter
// fix(tab): IE9 disabled attr renders grey text on enabled tab #2677
// This code is duplicated from the lines above to make it easy to remove once
// the feature has been completely deprecated
if ( attrs.disabled ) {
$log.warn('Use of "disabled" attribute has been deprecated, please use "disable"');
scope.$parent.$watch($parse(attrs.disabled), function(value) {
scope.disabled = !! value;
});
}
scope.select = function() {
if ( !scope.disabled ) {
scope.active = true;
}
};
tabsetCtrl.addTab(scope);
scope.$on('$destroy', function() {
tabsetCtrl.removeTab(scope);
});
//We need to transclude later, once the content container is ready.
//when this link happens, we're inside a tab heading.
scope.$transcludeFn = transclude;
};
}
};
}])
.directive('tabHeadingTransclude', [function() {
return {
restrict: 'A',
require: '^tab',
link: function(scope, elm, attrs, tabCtrl) {
scope.$watch('headingElement', function updateHeadingElement(heading) {
if (heading) {
elm.html('');
elm.append(heading);
}
});
}
};
}])
.directive('tabContentTransclude', function() {
return {
restrict: 'A',
require: '^tabset',
link: function(scope, elm, attrs) {
var tab = scope.$eval(attrs.tabContentTransclude);
//Now our tab is ready to be transcluded: both the tab heading area
//and the tab content area are loaded. Transclude 'em both.
tab.$transcludeFn(tab.$parent, function(contents) {
angular.forEach(contents, function(node) {
if (isTabHeading(node)) {
//Let tabHeadingTransclude know.
tab.headingElement = node;
} else {
elm.append(node);
}
});
});
}
};
function isTabHeading(node) {
return node.tagName && (
node.hasAttribute('tab-heading') ||
node.hasAttribute('data-tab-heading') ||
node.tagName.toLowerCase() === 'tab-heading' ||
node.tagName.toLowerCase() === 'data-tab-heading'
);
}
})
;

File diff suppressed because it is too large Load Diff