/* * angular-ui-bootstrap * http://angular-ui.github.io/bootstrap/ * Version: 0.13.4 - 2015-09-03 * License: MIT */ angular.module("ui.bootstrap", ["ui.bootstrap.tpls","ui.bootstrap.position","ui.bootstrap.dateparser","ui.bootstrap.datepicker","ui.bootstrap.tabs"]); angular.module("ui.bootstrap.tpls", ["template/datepicker/datepicker.html","template/datepicker/day.html","template/datepicker/month.html","template/datepicker/popup.html","template/datepicker/year.html","template/tabs/tab.html","template/tabs/tabset.html"]); 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; } }; }]); 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; this.parsers = {}; var 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; } } } }; 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 (!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; } }]); 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); focusElement(); } else if (evt.ctrlKey && (key === 'up' || key === 'down')) { $scope.toggleMode(key === 'up' ? 1 : -1); focusElement(); } 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('
'); 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) { if (scope.isOpen && !(element[0].contains(event.target) || popupEl[0].contains(event.target))) { 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'; } }; }); /** * @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.selectCalled = false; } }); selectedTab.active = true; // only call select if it has not already been called if (!selectedTab.selectCalled) { selectedTab.onSelect(); selectedTab.selectCalled = true; } }; 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 First Content! Second Content!
First Vertical Content! Second Vertical Content! First Justified Content! Second Justified Content!
*/ .directive('tabset', function() { return { restrict: 'EA', transclude: true, replace: true, scope: { type: '@' }, controller: 'TabsetController', templateUrl: 'template/tabs/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

First Tab Alert me! Second Tab, with alert callback and html heading! {{item.content}}
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!"); }); }; };
*/ /** * @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 HTML in my titles?! And some content, too! Icon heading?!? That's right. */ .directive('tab', ['$parse', '$log', function($parse, $log) { return { require: '^tabset', restrict: 'EA', replace: true, templateUrl: 'template/tabs/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 }, link: function(scope, elm, attrs, tabsetCtrl, transclude) { 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.hasAttribute('x-tab-heading') || node.tagName.toLowerCase() === 'tab-heading' || node.tagName.toLowerCase() === 'data-tab-heading' || node.tagName.toLowerCase() === 'x-tab-heading' ); } }); angular.module("template/datepicker/datepicker.html", []).run(["$templateCache", function($templateCache) { $templateCache.put("template/datepicker/datepicker.html", "
\n" + " \n" + " \n" + " \n" + "
"); }]); angular.module("template/datepicker/day.html", []).run(["$templateCache", function($templateCache) { $templateCache.put("template/datepicker/day.html", "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
{{::label.abbr}}
{{ weekNumbers[$index] }}\n" + " \n" + "
\n" + ""); }]); angular.module("template/datepicker/month.html", []).run(["$templateCache", function($templateCache) { $templateCache.put("template/datepicker/month.html", "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
\n" + " \n" + "
\n" + ""); }]); angular.module("template/datepicker/popup.html", []).run(["$templateCache", function($templateCache) { $templateCache.put("template/datepicker/popup.html", "\n" + ""); }]); angular.module("template/datepicker/year.html", []).run(["$templateCache", function($templateCache) { $templateCache.put("template/datepicker/year.html", "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
\n" + " \n" + "
\n" + ""); }]); angular.module("template/tabs/tab.html", []).run(["$templateCache", function($templateCache) { $templateCache.put("template/tabs/tab.html", "
  • \n" + " {{heading}}\n" + "
  • \n" + ""); }]); angular.module("template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) { $templateCache.put("template/tabs/tabset.html", "
    \n" + " \n" + "
    \n" + "
    \n" + "
    \n" + "
    \n" + "
    \n" + ""); }]);