Last active
November 28, 2015 04:24
-
-
Save davious/fc20472c94c414422b01 to your computer and use it in GitHub Desktop.
uib-parse-min-date
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* angular-ui-bootstrap | |
* http://angular-ui.github.io/bootstrap/ | |
* Version: 1.0.0-SNAPSHOT - 2015-11-27 | |
* License: MIT | |
*/ | |
angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.debounce","ui.bootstrap.dropdown","ui.bootstrap.stackedMap","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.pager","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]); | |
angular.module("ui.bootstrap.tpls", ["uib/template/accordion/accordion-group.html","uib/template/accordion/accordion.html","uib/template/alert/alert.html","uib/template/carousel/carousel.html","uib/template/carousel/slide.html","uib/template/datepicker/datepicker.html","uib/template/datepicker/day.html","uib/template/datepicker/month.html","uib/template/datepicker/popup.html","uib/template/datepicker/year.html","uib/template/modal/backdrop.html","uib/template/modal/window.html","uib/template/pagination/pagination.html","uib/template/pager/pager.html","uib/template/tooltip/tooltip-html-popup.html","uib/template/tooltip/tooltip-popup.html","uib/template/tooltip/tooltip-template-popup.html","uib/template/popover/popover-html.html","uib/template/popover/popover-template.html","uib/template/popover/popover.html","uib/template/progressbar/bar.html","uib/template/progressbar/progress.html","uib/template/progressbar/progressbar.html","uib/template/rating/rating.html","uib/template/tabs/tab.html","uib/template/tabs/tabset.html","uib/template/timepicker/timepicker.html","uib/template/typeahead/typeahead-match.html","uib/template/typeahead/typeahead-popup.html"]); | |
angular.module('ui.bootstrap.collapse', []) | |
.directive('uibCollapse', ['$animate', '$injector', function($animate, $injector) { | |
var $animateCss = $injector.has('$animateCss') ? $injector.get('$animateCss') : null; | |
return { | |
link: function(scope, element, attrs) { | |
function expand() { | |
element.removeClass('collapse') | |
.addClass('collapsing') | |
.attr('aria-expanded', true) | |
.attr('aria-hidden', false); | |
if ($animateCss) { | |
$animateCss(element, { | |
addClass: 'in', | |
easing: 'ease', | |
to: { height: element[0].scrollHeight + 'px' } | |
}).start()['finally'](expandDone); | |
} else { | |
$animate.addClass(element, 'in', { | |
to: { height: element[0].scrollHeight + 'px' } | |
}).then(expandDone); | |
} | |
} | |
function expandDone() { | |
element.removeClass('collapsing') | |
.addClass('collapse') | |
.css({height: 'auto'}); | |
} | |
function collapse() { | |
if (!element.hasClass('collapse') && !element.hasClass('in')) { | |
return collapseDone(); | |
} | |
element | |
// IMPORTANT: The height must be set before adding "collapsing" class. | |
// Otherwise, the browser attempts to animate from height 0 (in | |
// collapsing class) to the given height here. | |
.css({height: element[0].scrollHeight + 'px'}) | |
// initially all panel collapse have the collapse class, this removal | |
// prevents the animation from jumping to collapsed state | |
.removeClass('collapse') | |
.addClass('collapsing') | |
.attr('aria-expanded', false) | |
.attr('aria-hidden', true); | |
if ($animateCss) { | |
$animateCss(element, { | |
removeClass: 'in', | |
to: {height: '0'} | |
}).start()['finally'](collapseDone); | |
} else { | |
$animate.removeClass(element, 'in', { | |
to: {height: '0'} | |
}).then(collapseDone); | |
} | |
} | |
function collapseDone() { | |
element.css({height: '0'}); // Required so that collapse works when animation is disabled | |
element.removeClass('collapsing') | |
.addClass('collapse'); | |
} | |
scope.$watch(attrs.uibCollapse, function(shouldCollapse) { | |
if (shouldCollapse) { | |
collapse(); | |
} else { | |
expand(); | |
} | |
}); | |
} | |
}; | |
}]); | |
angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) | |
.constant('uibAccordionConfig', { | |
closeOthers: true | |
}) | |
.controller('UibAccordionController', ['$scope', '$attrs', 'uibAccordionConfig', function($scope, $attrs, accordionConfig) { | |
// This array keeps track of the accordion groups | |
this.groups = []; | |
// Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to | |
this.closeOthers = function(openGroup) { | |
var closeOthers = angular.isDefined($attrs.closeOthers) ? | |
$scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; | |
if (closeOthers) { | |
angular.forEach(this.groups, function(group) { | |
if (group !== openGroup) { | |
group.isOpen = false; | |
} | |
}); | |
} | |
}; | |
// This is called from the accordion-group directive to add itself to the accordion | |
this.addGroup = function(groupScope) { | |
var that = this; | |
this.groups.push(groupScope); | |
groupScope.$on('$destroy', function(event) { | |
that.removeGroup(groupScope); | |
}); | |
}; | |
// This is called from the accordion-group directive when to remove itself | |
this.removeGroup = function(group) { | |
var index = this.groups.indexOf(group); | |
if (index !== -1) { | |
this.groups.splice(index, 1); | |
} | |
}; | |
}]) | |
// The accordion directive simply sets up the directive controller | |
// and adds an accordion CSS class to itself element. | |
.directive('uibAccordion', function() { | |
return { | |
controller: 'UibAccordionController', | |
controllerAs: 'accordion', | |
transclude: true, | |
templateUrl: function(element, attrs) { | |
return attrs.templateUrl || 'uib/template/accordion/accordion.html'; | |
} | |
}; | |
}) | |
// The accordion-group directive indicates a block of html that will expand and collapse in an accordion | |
.directive('uibAccordionGroup', function() { | |
return { | |
require: '^uibAccordion', // We need this directive to be inside an accordion | |
transclude: true, // It transcludes the contents of the directive into the template | |
replace: true, // The element containing the directive will be replaced with the template | |
templateUrl: function(element, attrs) { | |
return attrs.templateUrl || 'uib/template/accordion/accordion-group.html'; | |
}, | |
scope: { | |
heading: '@', // Interpolate the heading attribute onto this scope | |
isOpen: '=?', | |
isDisabled: '=?' | |
}, | |
controller: function() { | |
this.setHeading = function(element) { | |
this.heading = element; | |
}; | |
}, | |
link: function(scope, element, attrs, accordionCtrl) { | |
accordionCtrl.addGroup(scope); | |
scope.openClass = attrs.openClass || 'panel-open'; | |
scope.panelClass = attrs.panelClass || 'panel-default'; | |
scope.$watch('isOpen', function(value) { | |
element.toggleClass(scope.openClass, !!value); | |
if (value) { | |
accordionCtrl.closeOthers(scope); | |
} | |
}); | |
scope.toggleOpen = function($event) { | |
if (!scope.isDisabled) { | |
if (!$event || $event.which === 32) { | |
scope.isOpen = !scope.isOpen; | |
} | |
} | |
}; | |
} | |
}; | |
}) | |
// Use accordion-heading below an accordion-group to provide a heading containing HTML | |
.directive('uibAccordionHeading', function() { | |
return { | |
transclude: true, // Grab the contents to be used as the heading | |
template: '', // In effect remove this element! | |
replace: true, | |
require: '^uibAccordionGroup', | |
link: function(scope, element, attrs, accordionGroupCtrl, transclude) { | |
// Pass the heading to the accordion-group controller | |
// so that it can be transcluded into the right place in the template | |
// [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat] | |
accordionGroupCtrl.setHeading(transclude(scope, angular.noop)); | |
} | |
}; | |
}) | |
// Use in the accordion-group template to indicate where you want the heading to be transcluded | |
// You must provide the property on the accordion-group controller that will hold the transcluded element | |
.directive('uibAccordionTransclude', function() { | |
return { | |
require: '^uibAccordionGroup', | |
link: function(scope, element, attrs, controller) { | |
scope.$watch(function() { return controller[attrs.uibAccordionTransclude]; }, function(heading) { | |
if (heading) { | |
element.find('span').html(''); | |
element.find('span').append(heading); | |
} | |
}); | |
} | |
}; | |
}); | |
angular.module('ui.bootstrap.alert', []) | |
.controller('UibAlertController', ['$scope', '$attrs', '$interpolate', '$timeout', function($scope, $attrs, $interpolate, $timeout) { | |
$scope.closeable = !!$attrs.close; | |
var dismissOnTimeout = angular.isDefined($attrs.dismissOnTimeout) ? | |
$interpolate($attrs.dismissOnTimeout)($scope.$parent) : null; | |
if (dismissOnTimeout) { | |
$timeout(function() { | |
$scope.close(); | |
}, parseInt(dismissOnTimeout, 10)); | |
} | |
}]) | |
.directive('uibAlert', function() { | |
return { | |
controller: 'UibAlertController', | |
controllerAs: 'alert', | |
templateUrl: function(element, attrs) { | |
return attrs.templateUrl || 'uib/template/alert/alert.html'; | |
}, | |
transclude: true, | |
replace: true, | |
scope: { | |
type: '@', | |
close: '&' | |
} | |
}; | |
}); | |
angular.module('ui.bootstrap.buttons', []) | |
.constant('uibButtonConfig', { | |
activeClass: 'active', | |
toggleEvent: 'click' | |
}) | |
.controller('UibButtonsController', ['uibButtonConfig', function(buttonConfig) { | |
this.activeClass = buttonConfig.activeClass || 'active'; | |
this.toggleEvent = buttonConfig.toggleEvent || 'click'; | |
}]) | |
.directive('uibBtnRadio', ['$parse', function($parse) { | |
return { | |
require: ['uibBtnRadio', 'ngModel'], | |
controller: 'UibButtonsController', | |
controllerAs: 'buttons', | |
link: function(scope, element, attrs, ctrls) { | |
var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; | |
var uncheckableExpr = $parse(attrs.uibUncheckable); | |
element.find('input').css({display: 'none'}); | |
//model -> UI | |
ngModelCtrl.$render = function() { | |
element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.uibBtnRadio))); | |
}; | |
//ui->model | |
element.on(buttonsCtrl.toggleEvent, function() { | |
if (attrs.disabled) { | |
return; | |
} | |
var isActive = element.hasClass(buttonsCtrl.activeClass); | |
if (!isActive || angular.isDefined(attrs.uncheckable)) { | |
scope.$apply(function() { | |
ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.uibBtnRadio)); | |
ngModelCtrl.$render(); | |
}); | |
} | |
}); | |
if (attrs.uibUncheckable) { | |
scope.$watch(uncheckableExpr, function(uncheckable) { | |
attrs.$set('uncheckable', uncheckable ? '' : null); | |
}); | |
} | |
} | |
}; | |
}]) | |
.directive('uibBtnCheckbox', function() { | |
return { | |
require: ['uibBtnCheckbox', 'ngModel'], | |
controller: 'UibButtonsController', | |
controllerAs: 'button', | |
link: function(scope, element, attrs, ctrls) { | |
var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; | |
element.find('input').css({display: 'none'}); | |
function getTrueValue() { | |
return getCheckboxValue(attrs.btnCheckboxTrue, true); | |
} | |
function getFalseValue() { | |
return getCheckboxValue(attrs.btnCheckboxFalse, false); | |
} | |
function getCheckboxValue(attribute, defaultValue) { | |
return angular.isDefined(attribute) ? scope.$eval(attribute) : defaultValue; | |
} | |
//model -> UI | |
ngModelCtrl.$render = function() { | |
element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue())); | |
}; | |
//ui->model | |
element.on(buttonsCtrl.toggleEvent, function() { | |
if (attrs.disabled) { | |
return; | |
} | |
scope.$apply(function() { | |
ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue()); | |
ngModelCtrl.$render(); | |
}); | |
}); | |
} | |
}; | |
}); | |
angular.module('ui.bootstrap.carousel', []) | |
.controller('UibCarouselController', ['$scope', '$element', '$interval', '$animate', function($scope, $element, $interval, $animate) { | |
var self = this, | |
slides = self.slides = $scope.slides = [], | |
SLIDE_DIRECTION = 'uib-slideDirection', | |
currentIndex = -1, | |
currentInterval, isPlaying; | |
self.currentSlide = null; | |
var destroyed = false; | |
/* direction: "prev" or "next" */ | |
self.select = $scope.select = function(nextSlide, direction) { | |
var nextIndex = $scope.indexOfSlide(nextSlide); | |
//Decide direction if it's not given | |
if (direction === undefined) { | |
direction = nextIndex > self.getCurrentIndex() ? 'next' : 'prev'; | |
} | |
//Prevent this user-triggered transition from occurring if there is already one in progress | |
if (nextSlide && nextSlide !== self.currentSlide && !$scope.$currentTransition) { | |
goNext(nextSlide, nextIndex, direction); | |
} | |
}; | |
function goNext(slide, index, direction) { | |
// Scope has been destroyed, stop here. | |
if (destroyed) { return; } | |
angular.extend(slide, {direction: direction, active: true}); | |
angular.extend(self.currentSlide || {}, {direction: direction, active: false}); | |
if ($animate.enabled() && !$scope.noTransition && !$scope.$currentTransition && | |
slide.$element && self.slides.length > 1) { | |
slide.$element.data(SLIDE_DIRECTION, slide.direction); | |
if (self.currentSlide && self.currentSlide.$element) { | |
self.currentSlide.$element.data(SLIDE_DIRECTION, slide.direction); | |
} | |
$scope.$currentTransition = true; | |
$animate.on('addClass', slide.$element, function(element, phase) { | |
if (phase === 'close') { | |
$scope.$currentTransition = null; | |
$animate.off('addClass', element); | |
} | |
}); | |
} | |
self.currentSlide = slide; | |
currentIndex = index; | |
//every time you change slides, reset the timer | |
restartTimer(); | |
} | |
function getSlideByIndex(index) { | |
if (angular.isUndefined(slides[index].index)) { | |
return slides[index]; | |
} | |
for (var i = 0, l = slides.length; i < l; ++i) { | |
if (slides[i].index === index) { | |
return slides[i]; | |
} | |
} | |
} | |
self.getCurrentIndex = function() { | |
if (self.currentSlide && angular.isDefined(self.currentSlide.index)) { | |
return +self.currentSlide.index; | |
} | |
return currentIndex; | |
}; | |
/* Allow outside people to call indexOf on slides array */ | |
$scope.indexOfSlide = function(slide) { | |
return angular.isDefined(slide.index) ? +slide.index : slides.indexOf(slide); | |
}; | |
self.next = $scope.next = function() { | |
var newIndex = (self.getCurrentIndex() + 1) % slides.length; | |
if (newIndex === 0 && $scope.noWrap()) { | |
$scope.pause(); | |
return; | |
} | |
return self.select(getSlideByIndex(newIndex), 'next'); | |
}; | |
self.prev = $scope.prev = function() { | |
var newIndex = self.getCurrentIndex() - 1 < 0 ? slides.length - 1 : self.getCurrentIndex() - 1; | |
if ($scope.noWrap() && newIndex === slides.length - 1) { | |
$scope.pause(); | |
return; | |
} | |
return self.select(getSlideByIndex(newIndex), 'prev'); | |
}; | |
$scope.isActive = function(slide) { | |
return self.currentSlide === slide; | |
}; | |
$scope.$watch('interval', restartTimer); | |
$scope.$watchCollection('slides', resetTransition); | |
$scope.$on('$destroy', function() { | |
destroyed = true; | |
resetTimer(); | |
}); | |
function restartTimer() { | |
resetTimer(); | |
var interval = +$scope.interval; | |
if (!isNaN(interval) && interval > 0) { | |
currentInterval = $interval(timerFn, interval); | |
} | |
} | |
function resetTimer() { | |
if (currentInterval) { | |
$interval.cancel(currentInterval); | |
currentInterval = null; | |
} | |
} | |
function timerFn() { | |
var interval = +$scope.interval; | |
if (isPlaying && !isNaN(interval) && interval > 0 && slides.length) { | |
$scope.next(); | |
} else { | |
$scope.pause(); | |
} | |
} | |
function resetTransition(slides) { | |
if (!slides.length) { | |
$scope.$currentTransition = null; | |
} | |
} | |
$scope.play = function() { | |
if (!isPlaying) { | |
isPlaying = true; | |
restartTimer(); | |
} | |
}; | |
$scope.pause = function() { | |
if (!$scope.noPause) { | |
isPlaying = false; | |
resetTimer(); | |
} | |
}; | |
self.addSlide = function(slide, element) { | |
slide.$element = element; | |
slides.push(slide); | |
//if this is the first slide or the slide is set to active, select it | |
if (slides.length === 1 || slide.active) { | |
self.select(slides[slides.length - 1]); | |
if (slides.length === 1) { | |
$scope.play(); | |
} | |
} else { | |
slide.active = false; | |
} | |
}; | |
self.removeSlide = function(slide) { | |
if (angular.isDefined(slide.index)) { | |
slides.sort(function(a, b) { | |
return +a.index > +b.index; | |
}); | |
} | |
//get the index of the slide inside the carousel | |
var index = slides.indexOf(slide); | |
slides.splice(index, 1); | |
if (slides.length > 0 && slide.active) { | |
if (index >= slides.length) { | |
self.select(slides[index - 1]); | |
} else { | |
self.select(slides[index]); | |
} | |
} else if (currentIndex > index) { | |
currentIndex--; | |
} | |
//clean the currentSlide when no more slide | |
if (slides.length === 0) { | |
self.currentSlide = null; | |
} | |
}; | |
$scope.$watch('noTransition', function(noTransition) { | |
$animate.enabled($element, noTransition); | |
}); | |
}]) | |
.directive('uibCarousel', function() { | |
return { | |
transclude: true, | |
replace: true, | |
controller: 'UibCarouselController', | |
controllerAs: 'carousel', | |
templateUrl: function(element, attrs) { | |
return attrs.templateUrl || 'uib/template/carousel/carousel.html'; | |
}, | |
scope: { | |
interval: '=', | |
noTransition: '=', | |
noPause: '=', | |
noWrap: '&' | |
} | |
}; | |
}) | |
.directive('uibSlide', function() { | |
return { | |
require: '^uibCarousel', | |
transclude: true, | |
replace: true, | |
templateUrl: function(element, attrs) { | |
return attrs.templateUrl || 'uib/template/carousel/slide.html'; | |
}, | |
scope: { | |
active: '=?', | |
actual: '=?', | |
index: '=?' | |
}, | |
link: function (scope, element, attrs, carouselCtrl) { | |
carouselCtrl.addSlide(scope, element); | |
//when the scope is destroyed then remove the slide from the current slides array | |
scope.$on('$destroy', function() { | |
carouselCtrl.removeSlide(scope); | |
}); | |
scope.$watch('active', function(active) { | |
if (active) { | |
carouselCtrl.select(scope); | |
} | |
}); | |
} | |
}; | |
}) | |
.animation('.item', ['$animate', '$animateCss', | |
function($animate, $animateCss) { | |
var SLIDE_DIRECTION = 'uib-slideDirection'; | |
function removeClass(element, className, callback) { | |
element.removeClass(className); | |
if (callback) { | |
callback(); | |
} | |
} | |
return { | |
beforeAddClass: function(element, className, done) { | |
// Due to transclusion, noTransition property is on parent's scope | |
if (className === 'active' && !$animate.enabled(element)) { | |
var stopped = false; | |
var direction = element.data(SLIDE_DIRECTION); | |
var directionClass = direction === 'next' ? 'left' : 'right'; | |
var removeClassFn = removeClass.bind(this, element, | |
directionClass + ' ' + direction, done); | |
element.addClass(direction); | |
$animateCss(element, {addClass: directionClass}) | |
.start() | |
.done(removeClassFn); | |
return function() { | |
stopped = true; | |
}; | |
} | |
done(); | |
}, | |
beforeRemoveClass: function (element, className, done) { | |
// Due to transclusion, noTransition property is on parent's scope | |
if (className === 'active' && !$animate.enabled(element)) { | |
var stopped = false; | |
var direction = element.data(SLIDE_DIRECTION); | |
var directionClass = direction === 'next' ? 'left' : 'right'; | |
var removeClassFn = removeClass.bind(this, element, directionClass, done); | |
$animateCss(element, {addClass: directionClass}) | |
.start() | |
.done(removeClassFn); | |
return function() { | |
stopped = true; | |
}; | |
} | |
done(); | |
} | |
}; | |
}]); | |
angular.module('ui.bootstrap.dateparser', []) | |
.service('uibDateParser', ['$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 = [ | |
{ | |
key: 'yyyy', | |
regex: '\\d{4}', | |
apply: function(value) { this.year = +value; } | |
}, | |
{ | |
key: 'yy', | |
regex: '\\d{2}', | |
apply: function(value) { this.year = +value + 2000; } | |
}, | |
{ | |
key: 'y', | |
regex: '\\d{1,4}', | |
apply: function(value) { this.year = +value; } | |
}, | |
{ | |
key: 'M!', | |
regex: '0?[1-9]|1[0-2]', | |
apply: function(value) { this.month = value - 1; } | |
}, | |
{ | |
key: 'MMMM', | |
regex: $locale.DATETIME_FORMATS.MONTH.join('|'), | |
apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); } | |
}, | |
{ | |
key: 'MMM', | |
regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), | |
apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); } | |
}, | |
{ | |
key: 'MM', | |
regex: '0[1-9]|1[0-2]', | |
apply: function(value) { this.month = value - 1; } | |
}, | |
{ | |
key: 'M', | |
regex: '[1-9]|1[0-2]', | |
apply: function(value) { this.month = value - 1; } | |
}, | |
{ | |
key: 'd!', | |
regex: '[0-2]?[0-9]{1}|3[0-1]{1}', | |
apply: function(value) { this.date = +value; } | |
}, | |
{ | |
key: 'dd', | |
regex: '[0-2][0-9]{1}|3[0-1]{1}', | |
apply: function(value) { this.date = +value; } | |
}, | |
{ | |
key: 'd', | |
regex: '[1-2]?[0-9]{1}|3[0-1]{1}', | |
apply: function(value) { this.date = +value; } | |
}, | |
{ | |
key: 'EEEE', | |
regex: $locale.DATETIME_FORMATS.DAY.join('|') | |
}, | |
{ | |
key: 'EEE', | |
regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|') | |
}, | |
{ | |
key: 'HH', | |
regex: '(?:0|1)[0-9]|2[0-3]', | |
apply: function(value) { this.hours = +value; } | |
}, | |
{ | |
key: 'hh', | |
regex: '0[0-9]|1[0-2]', | |
apply: function(value) { this.hours = +value; } | |
}, | |
{ | |
key: 'H', | |
regex: '1?[0-9]|2[0-3]', | |
apply: function(value) { this.hours = +value; } | |
}, | |
{ | |
key: 'h', | |
regex: '[0-9]|1[0-2]', | |
apply: function(value) { this.hours = +value; } | |
}, | |
{ | |
key: 'mm', | |
regex: '[0-5][0-9]', | |
apply: function(value) { this.minutes = +value; } | |
}, | |
{ | |
key: 'm', | |
regex: '[0-9]|[1-5][0-9]', | |
apply: function(value) { this.minutes = +value; } | |
}, | |
{ | |
key: 'sss', | |
regex: '[0-9][0-9][0-9]', | |
apply: function(value) { this.milliseconds = +value; } | |
}, | |
{ | |
key: 'ss', | |
regex: '[0-5][0-9]', | |
apply: function(value) { this.seconds = +value; } | |
}, | |
{ | |
key: 's', | |
regex: '[0-9]|[1-5][0-9]', | |
apply: function(value) { this.seconds = +value; } | |
}, | |
{ | |
key: 'a', | |
regex: $locale.DATETIME_FORMATS.AMPMS.join('|'), | |
apply: function(value) { | |
if (this.hours === 12) { | |
this.hours = 0; | |
} | |
if (value === 'PM') { | |
this.hours += 12; | |
} | |
} | |
}, | |
{ | |
key: 'Z', | |
regex: '[+-]\\d{4}', | |
apply: function(value) { | |
var matches = value.match(/([+-])(\d{2})(\d{2})/), | |
sign = matches[1], | |
hours = matches[2], | |
minutes = matches[3]; | |
this.hours += toInt(sign + hours); | |
this.minutes += toInt(sign + minutes); | |
} | |
}, | |
{ | |
key: 'ww', | |
regex: '[0-4][0-9]|5[0-3]' | |
}, | |
{ | |
key: 'w', | |
regex: '[0-9]|[1-4][0-9]|5[0-3]' | |
}, | |
{ | |
key: 'GGGG', | |
regex: $locale.DATETIME_FORMATS.ERANAMES.join('|').replace(/\s/g, '\\s') | |
}, | |
{ | |
key: 'GGG', | |
regex: $locale.DATETIME_FORMATS.ERAS.join('|') | |
}, | |
{ | |
key: 'GG', | |
regex: $locale.DATETIME_FORMATS.ERAS.join('|') | |
}, | |
{ | |
key: 'G', | |
regex: $locale.DATETIME_FORMATS.ERAS.join('|') | |
} | |
]; | |
}; | |
this.init(); | |
function createParser(format) { | |
var map = [], regex = format.split(''); | |
// check for literal values | |
var quoteIndex = format.indexOf('`'); | |
if (quoteIndex > -1) { | |
var inLiteral = false; | |
format = format.split(''); | |
for (var i = quoteIndex; i < format.length; i++) { | |
if (inLiteral) { | |
if (format[i] === '`') { | |
if (i + 1 < format.length && format[i + 1] === '\`') { // escaped backtick | |
format[i + 1] = '$'; | |
regex[i + 1] = ''; | |
} else { // end of literal | |
regex[i] = ''; | |
inLiteral = false; | |
} | |
} | |
format[i] = '$'; | |
} else { | |
if (format[i] === '`') { // start of literal | |
format[i] = '$'; | |
regex[i] = ''; | |
inLiteral = true; | |
} | |
} | |
} | |
format = format.join(''); | |
} | |
angular.forEach(formatCodeToRegex, function(data) { | |
var index = format.indexOf(data.key); | |
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 + data.key.length; i < n; i++) { | |
regex[i] = ''; | |
format[i] = '$'; | |
} | |
format = format.join(''); | |
map.push({ | |
index: index, | |
apply: data.apply, | |
matcher: data.regex | |
}); | |
} | |
}); | |
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), | |
tzOffset = false; | |
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.matcher === 'Z') { | |
tzOffset = true; | |
} | |
if (mapper.apply) { | |
mapper.apply.call(fields, results[i]); | |
} | |
} | |
var datesetter = tzOffset ? Date.prototype.setUTCFullYear : | |
Date.prototype.setFullYear; | |
var timesetter = tzOffset ? Date.prototype.setUTCHours : | |
Date.prototype.setHours; | |
if (isValid(fields.year, fields.month, fields.date)) { | |
if (angular.isDate(baseDate) && !isNaN(baseDate.getTime()) && !tzOffset) { | |
dt = new Date(baseDate); | |
datesetter.call(dt, fields.year, fields.month, fields.date); | |
timesetter.call(dt, fields.hours, fields.minutes, | |
fields.seconds, fields.milliseconds); | |
} else { | |
dt = new Date(0); | |
datesetter.call(dt, fields.year, fields.month, fields.date); | |
timesetter.call(dt, fields.hours || 0, fields.minutes || 0, | |
fields.seconds || 0, 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; | |
} | |
function toInt(str) { | |
return parseInt(str, 10); | |
} | |
}]); | |
angular.module('ui.bootstrap.position', []) | |
/** | |
* A set of utility methods for working with the DOM. | |
* It is meant to be used where we need to absolute-position elements in | |
* relation to another element (this is the case for tooltips, popovers, | |
* typeahead suggestions etc.). | |
*/ | |
.factory('$uibPosition', ['$document', '$window', function($document, $window) { | |
/** | |
* Used by scrollbarWidth() function to cache scrollbar's width. | |
* Do not access this variable directly, use scrollbarWidth() instead. | |
*/ | |
var SCROLLBAR_WIDTH; | |
var OVERFLOW_REGEX = { | |
normal: /(auto|scroll)/, | |
hidden: /(auto|scroll|hidden)/ | |
}; | |
var PLACEMENT_REGEX = { | |
auto: /\s?auto?\s?/i, | |
primary: /^(top|bottom|left|right)$/, | |
secondary: /^(top|bottom|left|right|center)$/, | |
vertical: /^(top|bottom)$/ | |
}; | |
return { | |
/** | |
* Provides a raw DOM element from a jQuery/jQLite element. | |
* | |
* @param {element} elem - The element to convert. | |
* | |
* @returns {element} A HTML element. | |
*/ | |
getRawNode: function(elem) { | |
return elem[0] || elem; | |
}, | |
/** | |
* Provides a parsed number for a style property. Strips | |
* units and casts invalid numbers to 0. | |
* | |
* @param {string} value - The style value to parse. | |
* | |
* @returns {number} A valid number. | |
*/ | |
parseStyle: function(value) { | |
value = parseFloat(value); | |
return isFinite(value) ? value : 0; | |
}, | |
/** | |
* Provides the closest positioned ancestor. | |
* | |
* @param {element} element - The element to get the offest parent for. | |
* | |
* @returns {element} The closest positioned ancestor. | |
*/ | |
offsetParent: function(elem) { | |
elem = this.getRawNode(elem); | |
var offsetParent = elem.offsetParent || $document[0].documentElement; | |
function isStaticPositioned(el) { | |
return ($window.getComputedStyle(el).position || 'static') === 'static'; | |
} | |
while (offsetParent && offsetParent !== $document[0].documentElement && isStaticPositioned(offsetParent)) { | |
offsetParent = offsetParent.offsetParent; | |
} | |
return offsetParent || $document[0].documentElement; | |
}, | |
/** | |
* Provides the scrollbar width, concept from TWBS measureScrollbar() | |
* function in https://github.com/twbs/bootstrap/blob/master/js/modal.js | |
* | |
* @returns {number} The width of the browser scollbar. | |
*/ | |
scrollbarWidth: function() { | |
if (angular.isUndefined(SCROLLBAR_WIDTH)) { | |
var scrollElem = angular.element('<div style="position: absolute; top: -9999px; width: 50px; height: 50px; overflow: scroll;"></div>'); | |
$document.find('body').append(scrollElem); | |
SCROLLBAR_WIDTH = scrollElem[0].offsetWidth - scrollElem[0].clientWidth; | |
SCROLLBAR_WIDTH = isFinite(SCROLLBAR_WIDTH) ? SCROLLBAR_WIDTH : 0; | |
scrollElem.remove(); | |
} | |
return SCROLLBAR_WIDTH; | |
}, | |
/** | |
* Provides the closest scrollable ancestor. | |
* A port of the jQuery UI scrollParent method: | |
* https://github.com/jquery/jquery-ui/blob/master/ui/scroll-parent.js | |
* | |
* @param {element} elem - The element to find the scroll parent of. | |
* @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered, | |
* default is false. | |
* | |
* @returns {element} A HTML element. | |
*/ | |
scrollParent: function(elem, includeHidden) { | |
elem = this.getRawNode(elem); | |
var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal; | |
var documentEl = $document[0].documentElement; | |
var elemStyle = $window.getComputedStyle(elem); | |
var excludeStatic = elemStyle.position === 'absolute'; | |
var scrollParent = elem.parentElement || documentEl; | |
if (scrollParent === documentEl || elemStyle.position === 'fixed') { | |
return documentEl; | |
} | |
while (scrollParent.parentElement && scrollParent !== documentEl) { | |
var spStyle = $window.getComputedStyle(scrollParent); | |
if (excludeStatic && spStyle.position !== 'static') { | |
excludeStatic = false; | |
} | |
if (!excludeStatic && overflowRegex.test(spStyle.overflow + spStyle.overflowY + spStyle.overflowX)) { | |
break; | |
} | |
scrollParent = scrollParent.parentElement; | |
} | |
return scrollParent; | |
}, | |
/** | |
* Provides read-only equivalent of jQuery's position function: | |
* http://api.jquery.com/position/ - distance to closest positioned | |
* ancestor. Does not account for margins by default like jQuery position. | |
* | |
* @param {element} elem - The element to caclulate the position on. | |
* @param {boolean=} [includeMargins=false] - Should margins be accounted | |
* for, default is false. | |
* | |
* @returns {object} An object with the following properties: | |
* <ul> | |
* <li>**width**: the width of the element</li> | |
* <li>**height**: the height of the element</li> | |
* <li>**top**: distance to top edge of offset parent</li> | |
* <li>**left**: distance to left edge of offset parent</li> | |
* </ul> | |
*/ | |
position: function(elem, includeMagins) { | |
elem = this.getRawNode(elem); | |
var elemOffset = this.offset(elem); | |
if (includeMagins) { | |
var elemStyle = $window.getComputedStyle(elem); | |
elemOffset.top -= this.parseStyle(elemStyle.marginTop); | |
elemOffset.left -= this.parseStyle(elemStyle.marginLeft); | |
} | |
var parent = this.offsetParent(elem); | |
var parentOffset = {top: 0, left: 0}; | |
if (parent !== $document[0].documentElement) { | |
parentOffset = this.offset(parent); | |
parentOffset.top += parent.clientTop - parent.scrollTop; | |
parentOffset.left += parent.clientLeft - parent.scrollLeft; | |
} | |
return { | |
width: Math.round(angular.isNumber(elemOffset.width) ? elemOffset.width : elem.offsetWidth), | |
height: Math.round(angular.isNumber(elemOffset.height) ? elemOffset.height : elem.offsetHeight), | |
top: Math.round(elemOffset.top - parentOffset.top), | |
left: Math.round(elemOffset.left - parentOffset.left) | |
}; | |
}, | |
/** | |
* Provides read-only equivalent of jQuery's offset function: | |
* http://api.jquery.com/offset/ - distance to viewport. Does | |
* not account for borders, margins, or padding on the body | |
* element. | |
* | |
* @param {element} elem - The element to calculate the offset on. | |
* | |
* @returns {object} An object with the following properties: | |
* <ul> | |
* <li>**width**: the width of the element</li> | |
* <li>**height**: the height of the element</li> | |
* <li>**top**: distance to top edge of viewport</li> | |
* <li>**right**: distance to bottom edge of viewport</li> | |
* </ul> | |
*/ | |
offset: function(elem) { | |
elem = this.getRawNode(elem); | |
var elemBCR = elem.getBoundingClientRect(); | |
return { | |
width: Math.round(angular.isNumber(elemBCR.width) ? elemBCR.width : elem.offsetWidth), | |
height: Math.round(angular.isNumber(elemBCR.height) ? elemBCR.height : elem.offsetHeight), | |
top: Math.round(elemBCR.top + ($window.pageYOffset || $document[0].documentElement.scrollTop)), | |
left: Math.round(elemBCR.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)) | |
}; | |
}, | |
/** | |
* Provides offset distance to the closest scrollable ancestor | |
* or viewport. Accounts for border and scrollbar width. | |
* | |
* Right and bottom dimensions represent the distance to the | |
* respective edge of the viewport element. If the element | |
* edge extends beyond the viewport, a negative value will be | |
* reported. | |
* | |
* @param {element} elem - The element to get the viewport offset for. | |
* @param {boolean=} [useDocument=false] - Should the viewport be the document element instead | |
* of the first scrollable element, default is false. | |
* @param {boolean=} [includePadding=true] - Should the padding on the offset parent element | |
* be accounted for, default is true. | |
* | |
* @returns {object} An object with the following properties: | |
* <ul> | |
* <li>**top**: distance to the top content edge of viewport element</li> | |
* <li>**bottom**: distance to the bottom content edge of viewport element</li> | |
* <li>**left**: distance to the left content edge of viewport element</li> | |
* <li>**right**: distance to the right content edge of viewport element</li> | |
* </ul> | |
*/ | |
viewportOffset: function(elem, useDocument, includePadding) { | |
elem = this.getRawNode(elem); | |
includePadding = includePadding !== false ? true : false; | |
var elemBCR = elem.getBoundingClientRect(); | |
var offsetBCR = {top: 0, left: 0, bottom: 0, right: 0}; | |
var offsetParent = useDocument ? $document[0].documentElement : this.scrollParent(elem); | |
var offsetParentBCR = offsetParent.getBoundingClientRect(); | |
offsetBCR.top = offsetParentBCR.top + offsetParent.clientTop; | |
offsetBCR.left = offsetParentBCR.left + offsetParent.clientLeft; | |
if (offsetParent === $document[0].documentElement) { | |
offsetBCR.top += $window.pageYOffset; | |
offsetBCR.left += $window.pageXOffset; | |
} | |
offsetBCR.bottom = offsetBCR.top + offsetParent.clientHeight; | |
offsetBCR.right = offsetBCR.left + offsetParent.clientWidth; | |
if (includePadding) { | |
var offsetParentStyle = $window.getComputedStyle(offsetParent); | |
offsetBCR.top += this.parseStyle(offsetParentStyle.paddingTop); | |
offsetBCR.bottom -= this.parseStyle(offsetParentStyle.paddingBottom); | |
offsetBCR.left += this.parseStyle(offsetParentStyle.paddingLeft); | |
offsetBCR.right -= this.parseStyle(offsetParentStyle.paddingRight); | |
} | |
return { | |
top: Math.round(elemBCR.top - offsetBCR.top), | |
bottom: Math.round(offsetBCR.bottom - elemBCR.bottom), | |
left: Math.round(elemBCR.left - offsetBCR.left), | |
right: Math.round(offsetBCR.right - elemBCR.right) | |
}; | |
}, | |
/** | |
* Provides an array of placement values parsed from a placement string. | |
* Along with the 'auto' indicator, supported placement strings are: | |
* <ul> | |
* <li>top: element on top, horizontally centered on host element.</li> | |
* <li>top-left: element on top, left edge aligned with host element left edge.</li> | |
* <li>top-right: element on top, lerightft edge aligned with host element right edge.</li> | |
* <li>bottom: element on bottom, horizontally centered on host element.</li> | |
* <li>bottom-left: element on bottom, left edge aligned with host element left edge.</li> | |
* <li>bottom-right: element on bottom, right edge aligned with host element right edge.</li> | |
* <li>left: element on left, vertically centered on host element.</li> | |
* <li>left-top: element on left, top edge aligned with host element top edge.</li> | |
* <li>left-bottom: element on left, bottom edge aligned with host element bottom edge.</li> | |
* <li>right: element on right, vertically centered on host element.</li> | |
* <li>right-top: element on right, top edge aligned with host element top edge.</li> | |
* <li>right-bottom: element on right, bottom edge aligned with host element bottom edge.</li> | |
* </ul> | |
* A placement string with an 'auto' indicator is expected to be | |
* space separated from the placement, i.e: 'auto bottom-left' If | |
* the primary and secondary placement values do not match 'top, | |
* bottom, left, right' then 'top' will be the primary placement and | |
* 'center' will be the secondary placement. If 'auto' is passed, true | |
* will be returned as the 3rd value of the array. | |
* | |
* @param {string} placement - The placement string to parse. | |
* | |
* @returns {array} An array with the following values | |
* <ul> | |
* <li>**[0]**: The primary placement.</li> | |
* <li>**[1]**: The secondary placement.</li> | |
* <li>**[2]**: If auto is passed: true, else undefined.</li> | |
* </ul> | |
*/ | |
parsePlacement: function(placement) { | |
var autoPlace = PLACEMENT_REGEX.auto.test(placement); | |
if (autoPlace) { | |
placement = placement.replace(PLACEMENT_REGEX.auto, ''); | |
} | |
placement = placement.split('-'); | |
placement[0] = placement[0] || 'top'; | |
if (!PLACEMENT_REGEX.primary.test(placement[0])) { | |
placement[0] = 'top'; | |
} | |
placement[1] = placement[1] || 'center'; | |
if (!PLACEMENT_REGEX.secondary.test(placement[1])) { | |
placement[1] = 'center'; | |
} | |
if (autoPlace) { | |
placement[2] = true; | |
} else { | |
placement[2] = false; | |
} | |
return placement; | |
}, | |
/** | |
* Provides coordinates for an element to be positioned relative to | |
* another element. Passing 'auto' as part of the placement parameter | |
* will enable smart placement - where the element fits. i.e: | |
* 'auto left-top' will check to see if there is enough space to the left | |
* of the hostElem to fit the targetElem, if not place right (same for secondary | |
* top placement). Available space is calculated using the viewportOffset | |
* function. | |
* | |
* @param {element} hostElem - The element to position against. | |
* @param {element} targetElem - The element to position. | |
* @param {string=} [placement=top] - The placement for the targetElem, | |
* default is 'top'. 'center' is assumed as secondary placement for | |
* 'top', 'left', 'right', and 'bottom' placements. Available placements are: | |
* <ul> | |
* <li>top</li> | |
* <li>top-right</li> | |
* <li>top-left</li> | |
* <li>bottom</li> | |
* <li>bottom-left</li> | |
* <li>bottom-right</li> | |
* <li>left</li> | |
* <li>left-top</li> | |
* <li>left-bottom</li> | |
* <li>right</li> | |
* <li>right-top</li> | |
* <li>right-bottom</li> | |
* </ul> | |
* @param {boolean=} [appendToBody=false] - Should the top and left values returned | |
* be calculated from the body element, default is false. | |
* | |
* @returns {object} An object with the following properties: | |
* <ul> | |
* <li>**top**: Value for targetElem top.</li> | |
* <li>**left**: Value for targetElem left.</li> | |
* <li>**placement**: The resolved placement.</li> | |
* </ul> | |
*/ | |
positionElements: function(hostElem, targetElem, placement, appendToBody) { | |
hostElem = this.getRawNode(hostElem); | |
targetElem = this.getRawNode(targetElem); | |
// need to read from prop to support tests. | |
var targetWidth = angular.isDefined(targetElem.offsetWidth) ? targetElem.offsetWidth : targetElem.prop('offsetWidth'); | |
var targetHeight = angular.isDefined(targetElem.offsetHeight) ? targetElem.offsetHeight : targetElem.prop('offsetHeight'); | |
placement = this.parsePlacement(placement); | |
var hostElemPos = appendToBody ? this.offset(hostElem) : this.position(hostElem); | |
var targetElemPos = {top: 0, left: 0, placement: ''}; | |
if (placement[2]) { | |
var viewportOffset = this.viewportOffset(hostElem); | |
var targetElemStyle = $window.getComputedStyle(targetElem); | |
var adjustedSize = { | |
width: targetWidth + Math.round(Math.abs(this.parseStyle(targetElemStyle.marginLeft) + this.parseStyle(targetElemStyle.marginRight))), | |
height: targetHeight + Math.round(Math.abs(this.parseStyle(targetElemStyle.marginTop) + this.parseStyle(targetElemStyle.marginBottom))) | |
}; | |
placement[0] = placement[0] === 'top' && adjustedSize.height > viewportOffset.top && adjustedSize.height <= viewportOffset.bottom ? 'bottom' : | |
placement[0] === 'bottom' && adjustedSize.height > viewportOffset.bottom && adjustedSize.height <= viewportOffset.top ? 'top' : | |
placement[0] === 'left' && adjustedSize.width > viewportOffset.left && adjustedSize.width <= viewportOffset.right ? 'right' : | |
placement[0] === 'right' && adjustedSize.width > viewportOffset.right && adjustedSize.width <= viewportOffset.left ? 'left' : | |
placement[0]; | |
placement[1] = placement[1] === 'top' && adjustedSize.height - hostElemPos.height > viewportOffset.bottom && adjustedSize.height - hostElemPos.height <= viewportOffset.top ? 'bottom' : | |
placement[1] === 'bottom' && adjustedSize.height - hostElemPos.height > viewportOffset.top && adjustedSize.height - hostElemPos.height <= viewportOffset.bottom ? 'top' : | |
placement[1] === 'left' && adjustedSize.width - hostElemPos.width > viewportOffset.right && adjustedSize.width - hostElemPos.width <= viewportOffset.left ? 'right' : | |
placement[1] === 'right' && adjustedSize.width - hostElemPos.width > viewportOffset.left && adjustedSize.width - hostElemPos.width <= viewportOffset.right ? 'left' : | |
placement[1]; | |
if (placement[1] === 'center') { | |
if (PLACEMENT_REGEX.vertical.test(placement[0])) { | |
var xOverflow = hostElemPos.width / 2 - targetWidth / 2; | |
if (viewportOffset.left + xOverflow < 0 && adjustedSize.width - hostElemPos.width <= viewportOffset.right) { | |
placement[1] = 'left'; | |
} else if (viewportOffset.right + xOverflow < 0 && adjustedSize.width - hostElemPos.width <= viewportOffset.left) { | |
placement[1] = 'right'; | |
} | |
} else { | |
var yOverflow = hostElemPos.height / 2 - adjustedSize.height / 2; | |
if (viewportOffset.top + yOverflow < 0 && adjustedSize.height - hostElemPos.height <= viewportOffset.bottom) { | |
placement[1] = 'top'; | |
} else if (viewportOffset.bottom + yOverflow < 0 && adjustedSize.height - hostElemPos.height <= viewportOffset.top) { | |
placement[1] = 'bottom'; | |
} | |
} | |
} | |
} | |
switch (placement[0]) { | |
case 'top': | |
targetElemPos.top = hostElemPos.top - targetHeight; | |
break; | |
case 'bottom': | |
targetElemPos.top = hostElemPos.top + hostElemPos.height; | |
break; | |
case 'left': | |
targetElemPos.left = hostElemPos.left - targetWidth; | |
break; | |
case 'right': | |
targetElemPos.left = hostElemPos.left + hostElemPos.width; | |
break; | |
} | |
switch (placement[1]) { | |
case 'top': | |
targetElemPos.top = hostElemPos.top; | |
break; | |
case 'bottom': | |
targetElemPos.top = hostElemPos.top + hostElemPos.height - targetHeight; | |
break; | |
case 'left': | |
targetElemPos.left = hostElemPos.left; | |
break; | |
case 'right': | |
targetElemPos.left = hostElemPos.left + hostElemPos.width - targetWidth; | |
break; | |
case 'center': | |
if (PLACEMENT_REGEX.vertical.test(placement[0])) { | |
targetElemPos.left = hostElemPos.left + hostElemPos.width / 2 - targetWidth / 2; | |
} else { | |
targetElemPos.top = hostElemPos.top + hostElemPos.height / 2 - targetHeight / 2; | |
} | |
break; | |
} | |
targetElemPos.top = Math.round(targetElemPos.top); | |
targetElemPos.left = Math.round(targetElemPos.left); | |
targetElemPos.placement = placement[1] === 'center' ? placement[0] : placement[0] + '-' + placement[1]; | |
return targetElemPos; | |
}, | |
/** | |
* Provides a way for positioning tooltip & dropdown | |
* arrows when using placement options beyond the standard | |
* left, right, top, or bottom. | |
* | |
* @param {element} elem - The tooltip/dropdown element. | |
* @param {string} placement - The placement for the elem. | |
*/ | |
positionArrow: function(elem, placement) { | |
elem = this.getRawNode(elem); | |
var isTooltip = true; | |
var innerElem = elem.querySelector('.tooltip-inner'); | |
if (!innerElem) { | |
isTooltip = false; | |
innerElem = elem.querySelector('.popover-inner'); | |
} | |
if (!innerElem) { | |
return; | |
} | |
var arrowElem = isTooltip ? elem.querySelector('.tooltip-arrow') : elem.querySelector('.arrow'); | |
if (!arrowElem) { | |
return; | |
} | |
placement = this.parsePlacement(placement); | |
if (placement[1] === 'center') { | |
// no adjustment necessary - just reset styles | |
angular.element(arrowElem).css({top: '', bottom: '', right: '', left: '', margin: ''}); | |
return; | |
} | |
var borderProp = 'border-' + placement[0] + '-width'; | |
var borderWidth = $window.getComputedStyle(arrowElem)[borderProp]; | |
var borderRadiusProp = 'border-'; | |
if (PLACEMENT_REGEX.vertical.test(placement[0])) { | |
borderRadiusProp += placement[0] + '-' + placement[1]; | |
} else { | |
borderRadiusProp += placement[1] + '-' + placement[0]; | |
} | |
borderRadiusProp += '-radius'; | |
var borderRadius = $window.getComputedStyle(isTooltip ? innerElem : elem)[borderRadiusProp]; | |
var arrowCss = { | |
top: 'auto', | |
bottom: 'auto', | |
left: 'auto', | |
right: 'auto', | |
margin: 0 | |
}; | |
switch (placement[0]) { | |
case 'top': | |
arrowCss.bottom = isTooltip ? '0' : '-' + borderWidth; | |
break; | |
case 'bottom': | |
arrowCss.top = isTooltip ? '0' : '-' + borderWidth; | |
break; | |
case 'left': | |
arrowCss.right = isTooltip ? '0' : '-' + borderWidth; | |
break; | |
case 'right': | |
arrowCss.left = isTooltip ? '0' : '-' + borderWidth; | |
break; | |
} | |
arrowCss[placement[1]] = borderRadius; | |
angular.element(arrowElem).css(arrowCss); | |
} | |
}; | |
}]); | |
angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position']) | |
.value('$datepickerSuppressError', false) | |
.constant('uibDatepickerConfig', { | |
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('UibDatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$log', 'dateFilter', 'uibDatepickerConfig', '$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']; | |
// Interpolated configuration attributes | |
angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle'], function(key) { | |
self[key] = angular.isDefined($attrs[key]) ? $interpolate($attrs[key])($scope.$parent) : datepickerConfig[key]; | |
}); | |
// Evaled configuration attributes | |
angular.forEach(['showWeeks', 'startingDay', 'yearRange', 'shortcutPropagation'], function(key) { | |
self[key] = angular.isDefined($attrs[key]) ? $scope.$parent.$eval($attrs[key]) : datepickerConfig[key]; | |
}); | |
// Watchable date attributes | |
angular.forEach(['minDate', 'maxDate'], function(key) { | |
if ($attrs[key]) { | |
$scope.$parent.$watch($attrs[key], function(value) { | |
self[key] = value ? angular.isDate(value) ? new Date(value) : new Date(dateFilter(value, 'medium')) : 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($attrs[key], function(value) { | |
self[key] = $scope[key] = angular.isDefined(value) ? value : $attrs[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] = $scope[key] = datepickerConfig[key] || null; | |
} | |
}); | |
$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.disabled = angular.isDefined($attrs.disabled) || false; | |
if (angular.isDefined($attrs.ngDisabled)) { | |
$scope.$parent.$watch($attrs.ngDisabled, function(disabled) { | |
$scope.disabled = disabled; | |
self.refreshView(); | |
}); | |
} | |
$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 $scope.disabled || | |
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; | |
}; | |
$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('uib:datepicker.focus', focusElement); | |
$scope.keydown = function(evt) { | |
var key = $scope.keys[evt.which]; | |
if (!key || evt.shiftKey || evt.altKey || $scope.disabled) { | |
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(); | |
} | |
}; | |
}]) | |
.controller('UibDaypickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) { | |
var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; | |
this.step = { months: 1 }; | |
this.element = $element; | |
function getDaysInMonth(year, month) { | |
return month === 1 && year % 4 === 0 && | |
(year % 100 !== 0 || year % 400 === 0) ? 29 : DAYS_IN_MONTH[month]; | |
} | |
this.init = function(ctrl) { | |
angular.extend(ctrl, this); | |
scope.showWeeks = ctrl.showWeeks; | |
ctrl.refreshView(); | |
}; | |
this.getDates = function(startDate, n) { | |
var dates = new Array(n), current = new Date(startDate), i = 0, date; | |
while (i < n) { | |
date = new Date(current); | |
dates[i++] = date; | |
current.setDate(current.getDate() + 1); | |
} | |
return dates; | |
}; | |
this._refreshView = function() { | |
var year = this.activeDate.getFullYear(), | |
month = this.activeDate.getMonth(), | |
firstDayOfMonth = new Date(this.activeDate); | |
firstDayOfMonth.setFullYear(year, month, 1); | |
var difference = this.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-week calendar | |
var days = this.getDates(firstDate, 42); | |
for (var i = 0; i < 42; i ++) { | |
days[i] = angular.extend(this.createDateObject(days[i], this.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, this.formatDayHeader), | |
full: dateFilter(days[j].date, 'EEEE') | |
}; | |
} | |
scope.title = dateFilter(this.activeDate, this.formatDayTitle); | |
scope.rows = this.split(days, 7); | |
if (scope.showWeeks) { | |
scope.weekNumbers = []; | |
var thursdayIndex = (4 + 7 - this.startingDay) % 7, | |
numWeeks = scope.rows.length; | |
for (var curWeek = 0; curWeek < numWeeks; curWeek++) { | |
scope.weekNumbers.push( | |
getISO8601WeekNumber(scope.rows[curWeek][thursdayIndex].date)); | |
} | |
} | |
}; | |
this.compare = function(date1, date2) { | |
var _date1 = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()); | |
var _date2 = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()); | |
_date1.setFullYear(date1.getFullYear()); | |
_date2.setFullYear(date2.getFullYear()); | |
return _date1 - _date2; | |
}; | |
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; | |
} | |
this.handleKeyDown = function(key, evt) { | |
var date = this.activeDate.getDate(); | |
if (key === 'left') { | |
date = date - 1; | |
} else if (key === 'up') { | |
date = date - 7; | |
} else if (key === 'right') { | |
date = date + 1; | |
} else if (key === 'down') { | |
date = date + 7; | |
} else if (key === 'pageup' || key === 'pagedown') { | |
var month = this.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1); | |
this.activeDate.setMonth(month, 1); | |
date = Math.min(getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth()), date); | |
} else if (key === 'home') { | |
date = 1; | |
} else if (key === 'end') { | |
date = getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth()); | |
} | |
this.activeDate.setDate(date); | |
}; | |
}]) | |
.controller('UibMonthpickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) { | |
this.step = { years: 1 }; | |
this.element = $element; | |
this.init = function(ctrl) { | |
angular.extend(ctrl, this); | |
ctrl.refreshView(); | |
}; | |
this._refreshView = function() { | |
var months = new Array(12), | |
year = this.activeDate.getFullYear(), | |
date; | |
for (var i = 0; i < 12; i++) { | |
date = new Date(this.activeDate); | |
date.setFullYear(year, i, 1); | |
months[i] = angular.extend(this.createDateObject(date, this.formatMonth), { | |
uid: scope.uniqueId + '-' + i | |
}); | |
} | |
scope.title = dateFilter(this.activeDate, this.formatMonthTitle); | |
scope.rows = this.split(months, 3); | |
}; | |
this.compare = function(date1, date2) { | |
var _date1 = new Date(date1.getFullYear(), date1.getMonth()); | |
var _date2 = new Date(date2.getFullYear(), date2.getMonth()); | |
_date1.setFullYear(date1.getFullYear()); | |
_date2.setFullYear(date2.getFullYear()); | |
return _date1 - _date2; | |
}; | |
this.handleKeyDown = function(key, evt) { | |
var date = this.activeDate.getMonth(); | |
if (key === 'left') { | |
date = date - 1; | |
} else if (key === 'up') { | |
date = date - 3; | |
} else if (key === 'right') { | |
date = date + 1; | |
} else if (key === 'down') { | |
date = date + 3; | |
} else if (key === 'pageup' || key === 'pagedown') { | |
var year = this.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); | |
this.activeDate.setFullYear(year); | |
} else if (key === 'home') { | |
date = 0; | |
} else if (key === 'end') { | |
date = 11; | |
} | |
this.activeDate.setMonth(date); | |
}; | |
}]) | |
.controller('UibYearpickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) { | |
var range; | |
this.element = $element; | |
function getStartingYear(year) { | |
return parseInt((year - 1) / range, 10) * range + 1; | |
} | |
this.yearpickerInit = function() { | |
range = this.yearRange; | |
this.step = { years: range }; | |
}; | |
this._refreshView = function() { | |
var years = new Array(range), date; | |
for (var i = 0, start = getStartingYear(this.activeDate.getFullYear()); i < range; i++) { | |
date = new Date(this.activeDate); | |
date.setFullYear(start + i, 0, 1); | |
years[i] = angular.extend(this.createDateObject(date, this.formatYear), { | |
uid: scope.uniqueId + '-' + i | |
}); | |
} | |
scope.title = [years[0].label, years[range - 1].label].join(' - '); | |
scope.rows = this.split(years, 5); | |
}; | |
this.compare = function(date1, date2) { | |
return date1.getFullYear() - date2.getFullYear(); | |
}; | |
this.handleKeyDown = function(key, evt) { | |
var date = this.activeDate.getFullYear(); | |
if (key === 'left') { | |
date = date - 1; | |
} else if (key === 'up') { | |
date = date - 5; | |
} else if (key === 'right') { | |
date = date + 1; | |
} else if (key === 'down') { | |
date = date + 5; | |
} else if (key === 'pageup' || key === 'pagedown') { | |
date += (key === 'pageup' ? - 1 : 1) * this.step.years; | |
} else if (key === 'home') { | |
date = getStartingYear(this.activeDate.getFullYear()); | |
} else if (key === 'end') { | |
date = getStartingYear(this.activeDate.getFullYear()) + range - 1; | |
} | |
this.activeDate.setFullYear(date); | |
}; | |
}]) | |
.directive('uibDatepicker', function() { | |
return { | |
replace: true, | |
templateUrl: function(element, attrs) { | |
return attrs.templateUrl || 'uib/template/datepicker/datepicker.html'; | |
}, | |
scope: { | |
datepickerMode: '=?', | |
dateDisabled: '&', | |
customClass: '&', | |
shortcutPropagation: '&?' | |
}, | |
require: ['uibDatepicker', '^ngModel'], | |
controller: 'UibDatepickerController', | |
controllerAs: 'datepicker', | |
link: function(scope, element, attrs, ctrls) { | |
var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; | |
datepickerCtrl.init(ngModelCtrl); | |
} | |
}; | |
}) | |
.directive('uibDaypicker', function() { | |
return { | |
replace: true, | |
templateUrl: function(element, attrs) { | |
return attrs.templateUrl || 'uib/template/datepicker/day.html'; | |
}, | |
require: ['^uibDatepicker', 'uibDaypicker'], | |
controller: 'UibDaypickerController', | |
link: function(scope, element, attrs, ctrls) { | |
var datepickerCtrl = ctrls[0], | |
daypickerCtrl = ctrls[1]; | |
daypickerCtrl.init(datepickerCtrl); | |
} | |
}; | |
}) | |
.directive('uibMonthpicker', function() { | |
return { | |
replace: true, | |
templateUrl: function(element, attrs) { | |
return attrs.templateUrl || 'uib/template/datepicker/month.html'; | |
}, | |
require: ['^uibDatepicker', 'uibMonthpicker'], | |
controller: 'UibMonthpickerController', | |
link: function(scope, element, attrs, ctrls) { | |
var datepickerCtrl = ctrls[0], | |
monthpickerCtrl = ctrls[1]; | |
monthpickerCtrl.init(datepickerCtrl); | |
} | |
}; | |
}) | |
.directive('uibYearpicker', function() { | |
return { | |
replace: true, | |
templateUrl: function(element, attrs) { | |
return attrs.templateUrl || 'uib/template/datepicker/year.html'; | |
}, | |
require: ['^uibDatepicker', 'uibYearpicker'], | |
controller: 'UibYearpickerController', | |
link: function(scope, element, attrs, ctrls) { | |
var ctrl = ctrls[0]; | |
angular.extend(ctrl, ctrls[1]); | |
ctrl.yearpickerInit(); | |
ctrl.refreshView(); | |
} | |
}; | |
}) | |
.constant('uibDatepickerPopupConfig', { | |
datepickerPopup: 'yyyy-MM-dd', | |
datepickerPopupTemplateUrl: 'uib/template/datepicker/popup.html', | |
datepickerTemplateUrl: 'uib/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, | |
altInputFormats: [] | |
}) | |
.controller('UibDatepickerPopupController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$document', '$rootScope', '$uibPosition', 'dateFilter', 'uibDateParser', 'uibDatepickerPopupConfig', '$timeout', | |
function(scope, element, attrs, $compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout) { | |
var self = this; | |
var cache = {}, | |
isHtml5DateInput = false; | |
var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus, | |
datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl, | |
ngModel, $popup, altInputFormats; | |
scope.watchData = {}; | |
this.init = function(_ngModel_) { | |
ngModel = _ngModel_; | |
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; | |
altInputFormats = angular.isDefined(attrs.altInputFormats) ? scope.$parent.$eval(attrs.altInputFormats) : datepickerPopupConfig.altInputFormats; | |
scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar; | |
if (datepickerPopupConfig.html5Types[attrs.type]) { | |
dateFormat = datepickerPopupConfig.html5Types[attrs.type]; | |
isHtml5DateInput = true; | |
} else { | |
dateFormat = attrs.uibDatepickerPopup || datepickerPopupConfig.datepickerPopup; | |
attrs.$observe('uibDatepickerPopup', 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('uibDatepickerPopup must have a date format specified.'); | |
} | |
} | |
}); | |
} | |
if (!dateFormat) { | |
throw new Error('uibDatepickerPopup must have a date format specified.'); | |
} | |
if (isHtml5DateInput && attrs.uibDatepickerPopup) { | |
throw new Error('HTML5 date input types do not support custom formats.'); | |
} | |
// popup element used to display calendar | |
popupEl = angular.element('<div uib-datepicker-popup-wrap><div uib-datepicker></div></div>'); | |
popupEl.attr({ | |
'ng-model': 'date', | |
'ng-change': 'dateSelection(date)', | |
'template-url': datepickerPopupTemplateUrl | |
}); | |
// datepicker element | |
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); | |
}); | |
} | |
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) { | |
if (key === 'minDate' || key === 'maxDate') { | |
cache[key] = angular.isDate(value) ? new Date(value) : new Date(dateFilter(value, 'medium')); | |
} | |
scope.watchData[key] = cache[key] || 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 })'); | |
} | |
angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', 'showWeeks', 'startingDay', 'yearRange'], function(key) { | |
if (angular.isDefined(attrs[key])) { | |
datepickerEl.attr(cameltoDash(key), attrs[key]); | |
} | |
}); | |
if (attrs.customClass) { | |
datepickerEl.attr('custom-class', 'customClass({ date: date, mode: mode })'); | |
} | |
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; | |
}); | |
} | |
// Detect changes in the view from the text box | |
ngModel.$viewChangeListeners.push(function() { | |
scope.date = dateParser.parse(ngModel.$viewValue, dateFormat, scope.date); | |
}); | |
element.bind('keydown', inputKeydownBind); | |
$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); | |
}); | |
}; | |
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()); | |
}; | |
// 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(); | |
} | |
}; | |
scope.keydown = function(evt) { | |
if (evt.which === 27) { | |
scope.isOpen = false; | |
element[0].focus(); | |
} | |
}; | |
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(); | |
}; | |
scope.disabled = angular.isDefined(attrs.disabled) || false; | |
if (attrs.ngDisabled) { | |
scope.$parent.$watch($parse(attrs.ngDisabled), function(disabled) { | |
scope.disabled = disabled; | |
}); | |
} | |
scope.$watch('isOpen', function(value) { | |
if (value) { | |
if (!scope.disabled) { | |
scope.position = appendToBody ? $position.offset(element) : $position.position(element); | |
scope.position.top = scope.position.top + element.prop('offsetHeight'); | |
$timeout(function() { | |
if (onOpenFocus) { | |
scope.$broadcast('uib:datepicker.focus'); | |
} | |
$document.bind('click', documentClickBind); | |
}, 0, false); | |
} else { | |
scope.isOpen = false; | |
} | |
} else { | |
$document.unbind('click', documentClickBind); | |
} | |
}); | |
function cameltoDash(string) { | |
return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); | |
} | |
function parseDate(viewValue) { | |
if (angular.isNumber(viewValue)) { | |
// presumably timestamp to date object | |
viewValue = new Date(viewValue); | |
} | |
if (!viewValue) { | |
return null; | |
} | |
if (angular.isDate(viewValue) && !isNaN(viewValue)) { | |
return viewValue; | |
} | |
if (angular.isString(viewValue)) { | |
var date = dateParser.parse(viewValue, dateFormat, scope.date); | |
if (isNaN(date)) { | |
for (var i = 0; i < altInputFormats.length; i++) { | |
date = dateParser.parse(viewValue, altInputFormats[i], scope.date); | |
if (!isNaN(date)) { | |
break; | |
} | |
} | |
} | |
if (isNaN(date)) { | |
return undefined; | |
} | |
return date; | |
} | |
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; | |
} | |
if (angular.isDate(value) && !isNaN(value)) { | |
return true; | |
} | |
if (angular.isString(value)) { | |
var date = dateParser.parse(value, dateFormat); | |
if (isNaN(date)) { | |
for (var i = 0; i < altInputFormats.length; i++) { | |
date = dateParser.parse(value, altInputFormats[i]); | |
if (!isNaN(date)) { | |
break; | |
} | |
} | |
} | |
return !isNaN(date); | |
} | |
return false; | |
} | |
function documentClickBind(event) { | |
if (!scope.isOpen && scope.disabled) { | |
return; | |
} | |
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; | |
}); | |
} | |
} | |
function inputKeydownBind(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; | |
}); | |
} | |
} | |
}]) | |
.directive('uibDatepickerPopup', function() { | |
return { | |
require: ['ngModel', 'uibDatepickerPopup'], | |
controller: 'UibDatepickerPopupController', | |
scope: { | |
isOpen: '=?', | |
currentText: '@', | |
clearText: '@', | |
closeText: '@', | |
dateDisabled: '&', | |
customClass: '&' | |
}, | |
link: function(scope, element, attrs, ctrls) { | |
var ngModel = ctrls[0], | |
ctrl = ctrls[1]; | |
ctrl.init(ngModel); | |
} | |
}; | |
}) | |
.directive('uibDatepickerPopupWrap', function() { | |
return { | |
replace: true, | |
transclude: true, | |
templateUrl: function(element, attrs) { | |
return attrs.templateUrl || 'uib/template/datepicker/popup.html'; | |
} | |
}; | |
}); | |
angular.module('ui.bootstrap.debounce', []) | |
/** | |
* A helper, internal service that debounces a function | |
*/ | |
.factory('$$debounce', ['$timeout', function($timeout) { | |
return function(callback, debounceTime) { | |
var timeoutPromise; | |
return function() { | |
var self = this; | |
var args = Array.prototype.slice.call(arguments); | |
if (timeoutPromise) { | |
$timeout.cancel(timeoutPromise); | |
} | |
timeoutPromise = $timeout(function() { | |
callback.apply(self, args); | |
}, debounceTime); | |
}; | |
}; | |
}]); | |
angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) | |
.constant('uibDropdownConfig', { | |
appendToOpenClass: 'uib-dropdown-open', | |
openClass: 'open' | |
}) | |
.service('uibDropdownService', ['$document', '$rootScope', function($document, $rootScope) { | |
var openScope = null; | |
this.open = function(dropdownScope) { | |
if (!openScope) { | |
$document.on('click', closeDropdown); | |
$document.on('keydown', keybindFilter); | |
} | |
if (openScope && openScope !== dropdownScope) { | |
openScope.isOpen = false; | |
} | |
openScope = dropdownScope; | |
}; | |
this.close = function(dropdownScope) { | |
if (openScope === dropdownScope) { | |
openScope = null; | |
$document.off('click', closeDropdown); | |
$document.off('keydown', keybindFilter); | |
} | |
}; | |
var closeDropdown = function(evt) { | |
// This method may still be called during the same mouse event that | |
// unbound this event handler. So check openScope before proceeding. | |
if (!openScope) { return; } | |
if (evt && openScope.getAutoClose() === 'disabled') { return ; } | |
var toggleElement = openScope.getToggleElement(); | |
if (evt && toggleElement && toggleElement[0].contains(evt.target)) { | |
return; | |
} | |
var dropdownElement = openScope.getDropdownElement(); | |
if (evt && openScope.getAutoClose() === 'outsideClick' && | |
dropdownElement && dropdownElement[0].contains(evt.target)) { | |
return; | |
} | |
openScope.isOpen = false; | |
if (!$rootScope.$$phase) { | |
openScope.$apply(); | |
} | |
}; | |
var keybindFilter = function(evt) { | |
if (evt.which === 27) { | |
openScope.focusToggleElement(); | |
closeDropdown(); | |
} else if (openScope.isKeynavEnabled() && [38, 40].indexOf(evt.which) !== -1 && openScope.isOpen) { | |
evt.preventDefault(); | |
evt.stopPropagation(); | |
openScope.focusDropdownEntry(evt.which); | |
} | |
}; | |
}]) | |
.controller('UibDropdownController', ['$scope', '$element', '$attrs', '$parse', 'uibDropdownConfig', 'uibDropdownService', '$animate', '$uibPosition', '$document', '$compile', '$templateRequest', function($scope, $element, $attrs, $parse, dropdownConfig, uibDropdownService, $animate, $position, $document, $compile, $templateRequest) { | |
var self = this, | |
scope = $scope.$new(), // create a child scope so we are not polluting original one | |
templateScope, | |
appendToOpenClass = dropdownConfig.appendToOpenClass, | |
openClass = dropdownConfig.openClass, | |
getIsOpen, | |
setIsOpen = angular.noop, | |
toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop, | |
appendToBody = false, | |
appendTo = null, | |
keynavEnabled = false, | |
selectedOption = null, | |
body = $document.find('body'); | |
$element.addClass('dropdown'); | |
this.init = function() { | |
if ($attrs.isOpen) { | |
getIsOpen = $parse($attrs.isOpen); | |
setIsOpen = getIsOpen.assign; | |
$scope.$watch(getIsOpen, function(value) { | |
scope.isOpen = !!value; | |
}); | |
} | |
if (angular.isDefined($attrs.dropdownAppendTo)) { | |
var appendToEl = $parse($attrs.dropdownAppendTo)(scope); | |
if (appendToEl) { | |
appendTo = angular.element(appendToEl); | |
} | |
} | |
appendToBody = angular.isDefined($attrs.dropdownAppendToBody); | |
keynavEnabled = angular.isDefined($attrs.keyboardNav); | |
if (appendToBody && !appendTo) { | |
appendTo = body; | |
} | |
if (appendTo && self.dropdownMenu) { | |
appendTo.append(self.dropdownMenu); | |
$element.on('$destroy', function handleDestroyEvent() { | |
self.dropdownMenu.remove(); | |
}); | |
} | |
}; | |
this.toggle = function(open) { | |
return scope.isOpen = arguments.length ? !!open : !scope.isOpen; | |
}; | |
// Allow other directives to watch status | |
this.isOpen = function() { | |
return scope.isOpen; | |
}; | |
scope.getToggleElement = function() { | |
return self.toggleElement; | |
}; | |
scope.getAutoClose = function() { | |
return $attrs.autoClose || 'always'; //or 'outsideClick' or 'disabled' | |
}; | |
scope.getElement = function() { | |
return $element; | |
}; | |
scope.isKeynavEnabled = function() { | |
return keynavEnabled; | |
}; | |
scope.focusDropdownEntry = function(keyCode) { | |
var elems = self.dropdownMenu ? //If append to body is used. | |
angular.element(self.dropdownMenu).find('a') : | |
$element.find('ul').eq(0).find('a'); | |
switch (keyCode) { | |
case 40: { | |
if (!angular.isNumber(self.selectedOption)) { | |
self.selectedOption = 0; | |
} else { | |
self.selectedOption = self.selectedOption === elems.length - 1 ? | |
self.selectedOption : | |
self.selectedOption + 1; | |
} | |
break; | |
} | |
case 38: { | |
if (!angular.isNumber(self.selectedOption)) { | |
self.selectedOption = elems.length - 1; | |
} else { | |
self.selectedOption = self.selectedOption === 0 ? | |
0 : self.selectedOption - 1; | |
} | |
break; | |
} | |
} | |
elems[self.selectedOption].focus(); | |
}; | |
scope.getDropdownElement = function() { | |
return self.dropdownMenu; | |
}; | |
scope.focusToggleElement = function() { | |
if (self.toggleElement) { | |
self.toggleElement[0].focus(); | |
} | |
}; | |
scope.$watch('isOpen', function(isOpen, wasOpen) { | |
if (appendTo && self.dropdownMenu) { | |
var pos = $position.positionElements($element, self.dropdownMenu, 'bottom-left', true), | |
css, | |
rightalign; | |
css = { | |
top: pos.top + 'px', | |
display: isOpen ? 'block' : 'none' | |
}; | |
rightalign = self.dropdownMenu.hasClass('dropdown-menu-right'); | |
if (!rightalign) { | |
css.left = pos.left + 'px'; | |
css.right = 'auto'; | |
} else { | |
css.left = 'auto'; | |
css.right = window.innerWidth - | |
(pos.left + $element.prop('offsetWidth')) + 'px'; | |
} | |
// Need to adjust our positioning to be relative to the appendTo container | |
// if it's not the body element | |
if (!appendToBody) { | |
var appendOffset = $position.offset(appendTo); | |
css.top = pos.top - appendOffset.top + 'px'; | |
if (!rightalign) { | |
css.left = pos.left - appendOffset.left + 'px'; | |
} else { | |
css.right = window.innerWidth - | |
(pos.left - appendOffset.left + $element.prop('offsetWidth')) + 'px'; | |
} | |
} | |
self.dropdownMenu.css(css); | |
} | |
var openContainer = appendTo ? appendTo : $element; | |
$animate[isOpen ? 'addClass' : 'removeClass'](openContainer, appendTo ? appendToOpenClass : openClass).then(function() { | |
if (angular.isDefined(isOpen) && isOpen !== wasOpen) { | |
toggleInvoker($scope, { open: !!isOpen }); | |
} | |
}); | |
if (isOpen) { | |
if (self.dropdownMenuTemplateUrl) { | |
$templateRequest(self.dropdownMenuTemplateUrl).then(function(tplContent) { | |
templateScope = scope.$new(); | |
$compile(tplContent.trim())(templateScope, function(dropdownElement) { | |
var newEl = dropdownElement; | |
self.dropdownMenu.replaceWith(newEl); | |
self.dropdownMenu = newEl; | |
}); | |
}); | |
} | |
scope.focusToggleElement(); | |
uibDropdownService.open(scope); | |
} else { | |
if (self.dropdownMenuTemplateUrl) { | |
if (templateScope) { | |
templateScope.$destroy(); | |
} | |
var newEl = angular.element('<ul class="dropdown-menu"></ul>'); | |
self.dropdownMenu.replaceWith(newEl); | |
self.dropdownMenu = newEl; | |
} | |
uibDropdownService.close(scope); | |
self.selectedOption = null; | |
} | |
if (angular.isFunction(setIsOpen)) { | |
setIsOpen($scope, isOpen); | |
} | |
}); | |
$scope.$on('$locationChangeSuccess', function() { | |
if (scope.getAutoClose() !== 'disabled') { | |
scope.isOpen = false; | |
} | |
}); | |
}]) | |
.directive('uibDropdown', function() { | |
return { | |
controller: 'UibDropdownController', | |
link: function(scope, element, attrs, dropdownCtrl) { | |
dropdownCtrl.init(); | |
} | |
}; | |
}) | |
.directive('uibDropdownMenu', function() { | |
return { | |
restrict: 'AC', | |
require: '?^uibDropdown', | |
link: function(scope, element, attrs, dropdownCtrl) { | |
if (!dropdownCtrl || angular.isDefined(attrs.dropdownNested)) { | |
return; | |
} | |
element.addClass('dropdown-menu'); | |
var tplUrl = attrs.templateUrl; | |
if (tplUrl) { | |
dropdownCtrl.dropdownMenuTemplateUrl = tplUrl; | |
} | |
if (!dropdownCtrl.dropdownMenu) { | |
dropdownCtrl.dropdownMenu = element; | |
} | |
} | |
}; | |
}) | |
.directive('uibDropdownToggle', function() { | |
return { | |
require: '?^uibDropdown', | |
link: function(scope, element, attrs, dropdownCtrl) { | |
if (!dropdownCtrl) { | |
return; | |
} | |
element.addClass('dropdown-toggle'); | |
dropdownCtrl.toggleElement = element; | |
var toggleDropdown = function(event) { | |
event.preventDefault(); | |
if (!element.hasClass('disabled') && !attrs.disabled) { | |
scope.$apply(function() { | |
dropdownCtrl.toggle(); | |
}); | |
} | |
}; | |
element.bind('click', toggleDropdown); | |
// WAI-ARIA | |
element.attr({ 'aria-haspopup': true, 'aria-expanded': false }); | |
scope.$watch(dropdownCtrl.isOpen, function(isOpen) { | |
element.attr('aria-expanded', !!isOpen); | |
}); | |
scope.$on('$destroy', function() { | |
element.unbind('click', toggleDropdown); | |
}); | |
} | |
}; | |
}); | |
angular.module('ui.bootstrap.stackedMap', []) | |
/** | |
* A helper, internal data structure that acts as a map but also allows getting / removing | |
* elements in the LIFO order | |
*/ | |
.factory('$$stackedMap', function() { | |
return { | |
createNew: function() { | |
var stack = []; | |
return { | |
add: function(key, value) { | |
stack.push({ | |
key: key, | |
value: value | |
}); | |
}, | |
get: function(key) { | |
for (var i = 0; i < stack.length; i++) { | |
if (key === stack[i].key) { | |
return stack[i]; | |
} | |
} | |
}, | |
keys: function() { | |
var keys = []; | |
for (var i = 0; i < stack.length; i++) { | |
keys.push(stack[i].key); | |
} | |
return keys; | |
}, | |
top: function() { | |
return stack[stack.length - 1]; | |
}, | |
remove: function(key) { | |
var idx = -1; | |
for (var i = 0; i < stack.length; i++) { | |
if (key === stack[i].key) { | |
idx = i; | |
break; | |
} | |
} | |
return stack.splice(idx, 1)[0]; | |
}, | |
removeTop: function() { | |
return stack.splice(stack.length - 1, 1)[0]; | |
}, | |
length: function() { | |
return stack.length; | |
} | |
}; | |
} | |
}; | |
}); | |
angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap']) | |
/** | |
* A helper, internal data structure that stores all references attached to key | |
*/ | |
.factory('$$multiMap', function() { | |
return { | |
createNew: function() { | |
var map = {}; | |
return { | |
entries: function() { | |
return Object.keys(map).map(function(key) { | |
return { | |
key: key, | |
value: map[key] | |
}; | |
}); | |
}, | |
get: function(key) { | |
return map[key]; | |
}, | |
hasKey: function(key) { | |
return !!map[key]; | |
}, | |
keys: function() { | |
return Object.keys(map); | |
}, | |
put: function(key, value) { | |
if (!map[key]) { | |
map[key] = []; | |
} | |
map[key].push(value); | |
}, | |
remove: function(key, value) { | |
var values = map[key]; | |
if (!values) { | |
return; | |
} | |
var idx = values.indexOf(value); | |
if (idx !== -1) { | |
values.splice(idx, 1); | |
} | |
if (!values.length) { | |
delete map[key]; | |
} | |
} | |
}; | |
} | |
}; | |
}) | |
/** | |
* A helper directive for the $modal service. It creates a backdrop element. | |
*/ | |
.directive('uibModalBackdrop', ['$animate', '$injector', '$uibModalStack', | |
function($animate, $injector, $modalStack) { | |
var $animateCss = null; | |
if ($injector.has('$animateCss')) { | |
$animateCss = $injector.get('$animateCss'); | |
} | |
return { | |
replace: true, | |
templateUrl: 'uib/template/modal/backdrop.html', | |
compile: function(tElement, tAttrs) { | |
tElement.addClass(tAttrs.backdropClass); | |
return linkFn; | |
} | |
}; | |
function linkFn(scope, element, attrs) { | |
// Temporary fix for prefixing | |
element.addClass('modal-backdrop'); | |
if (attrs.modalInClass) { | |
if ($animateCss) { | |
$animateCss(element, { | |
addClass: attrs.modalInClass | |
}).start(); | |
} else { | |
$animate.addClass(element, attrs.modalInClass); | |
} | |
scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { | |
var done = setIsAsync(); | |
if (scope.modalOptions.animation) { | |
if ($animateCss) { | |
$animateCss(element, { | |
removeClass: attrs.modalInClass | |
}).start().then(done); | |
} else { | |
$animate.removeClass(element, attrs.modalInClass).then(done); | |
} | |
} else { | |
done(); | |
} | |
}); | |
} | |
} | |
}]) | |
.directive('uibModalWindow', ['$uibModalStack', '$q', '$animate', '$animateCss', '$document', | |
function($modalStack, $q, $animate, $animateCss, $document) { | |
return { | |
scope: { | |
index: '@' | |
}, | |
replace: true, | |
transclude: true, | |
templateUrl: function(tElement, tAttrs) { | |
return tAttrs.templateUrl || 'uib/template/modal/window.html'; | |
}, | |
link: function(scope, element, attrs) { | |
element.addClass(attrs.windowClass || ''); | |
element.addClass(attrs.windowTopClass || ''); | |
scope.size = attrs.size; | |
scope.close = function(evt) { | |
var modal = $modalStack.getTop(); | |
if (modal && modal.value.backdrop && | |
modal.value.backdrop !== 'static' && | |
evt.target === evt.currentTarget) { | |
evt.preventDefault(); | |
evt.stopPropagation(); | |
$modalStack.dismiss(modal.key, 'backdrop click'); | |
} | |
}; | |
// moved from template to fix issue #2280 | |
element.on('click', scope.close); | |
// This property is only added to the scope for the purpose of detecting when this directive is rendered. | |
// We can detect that by using this property in the template associated with this directive and then use | |
// {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}. | |
scope.$isRendered = true; | |
// Deferred object that will be resolved when this modal is render. | |
var modalRenderDeferObj = $q.defer(); | |
// Observe function will be called on next digest cycle after compilation, ensuring that the DOM is ready. | |
// In order to use this way of finding whether DOM is ready, we need to observe a scope property used in modal's template. | |
attrs.$observe('modalRender', function(value) { | |
if (value === 'true') { | |
modalRenderDeferObj.resolve(); | |
} | |
}); | |
modalRenderDeferObj.promise.then(function() { | |
var animationPromise = null; | |
if (attrs.modalInClass) { | |
animationPromise = $animateCss(element, { | |
addClass: attrs.modalInClass | |
}).start(); | |
scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { | |
var done = setIsAsync(); | |
if ($animateCss) { | |
$animateCss(element, { | |
removeClass: attrs.modalInClass | |
}).start().then(done); | |
} else { | |
$animate.removeClass(element, attrs.modalInClass).then(done); | |
} | |
}); | |
} | |
$q.when(animationPromise).then(function() { | |
/** | |
* If something within the freshly-opened modal already has focus (perhaps via a | |
* directive that causes focus). then no need to try and focus anything. | |
*/ | |
if (!($document[0].activeElement && element[0].contains($document[0].activeElement))) { | |
var inputWithAutofocus = element[0].querySelector('[autofocus]'); | |
/** | |
* Auto-focusing of a freshly-opened modal element causes any child elements | |
* with the autofocus attribute to lose focus. This is an issue on touch | |
* based devices which will show and then hide the onscreen keyboard. | |
* Attempts to refocus the autofocus element via JavaScript will not reopen | |
* the onscreen keyboard. Fixed by updated the focusing logic to only autofocus | |
* the modal element if the modal does not contain an autofocus element. | |
*/ | |
if (inputWithAutofocus) { | |
inputWithAutofocus.focus(); | |
} else { | |
element[0].focus(); | |
} | |
} | |
}); | |
// Notify {@link $modalStack} that modal is rendered. | |
var modal = $modalStack.getTop(); | |
if (modal) { | |
$modalStack.modalRendered(modal.key); | |
} | |
}); | |
} | |
}; | |
}]) | |
.directive('uibModalAnimationClass', function() { | |
return { | |
compile: function(tElement, tAttrs) { | |
if (tAttrs.modalAnimation) { | |
tElement.addClass(tAttrs.uibModalAnimationClass); | |
} | |
} | |
}; | |
}) | |
.directive('uibModalTransclude', function() { | |
return { | |
link: function($scope, $element, $attrs, controller, $transclude) { | |
$transclude($scope.$parent, function(clone) { | |
$element.empty(); | |
$element.append(clone); | |
}); | |
} | |
}; | |
}) | |
.factory('$uibModalStack', ['$animate', '$animateCss', '$document', | |
'$compile', '$rootScope', '$q', '$$multiMap', '$$stackedMap', | |
function($animate, $animateCss, $document, $compile, $rootScope, $q, $$multiMap, $$stackedMap) { | |
var OPENED_MODAL_CLASS = 'modal-open'; | |
var backdropDomEl, backdropScope; | |
var openedWindows = $$stackedMap.createNew(); | |
var openedClasses = $$multiMap.createNew(); | |
var $modalStack = { | |
NOW_CLOSING_EVENT: 'modal.stack.now-closing' | |
}; | |
//Modal focus behavior | |
var focusableElementList; | |
var focusIndex = 0; | |
var tababbleSelector = 'a[href], area[href], input:not([disabled]), ' + | |
'button:not([disabled]),select:not([disabled]), textarea:not([disabled]), ' + | |
'iframe, object, embed, *[tabindex], *[contenteditable=true]'; | |
function backdropIndex() { | |
var topBackdropIndex = -1; | |
var opened = openedWindows.keys(); | |
for (var i = 0; i < opened.length; i++) { | |
if (openedWindows.get(opened[i]).value.backdrop) { | |
topBackdropIndex = i; | |
} | |
} | |
return topBackdropIndex; | |
} | |
$rootScope.$watch(backdropIndex, function(newBackdropIndex) { | |
if (backdropScope) { | |
backdropScope.index = newBackdropIndex; | |
} | |
}); | |
function removeModalWindow(modalInstance, elementToReceiveFocus) { | |
var modalWindow = openedWindows.get(modalInstance).value; | |
var appendToElement = modalWindow.appendTo; | |
//clean up the stack | |
openedWindows.remove(modalInstance); | |
removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() { | |
var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS; | |
openedClasses.remove(modalBodyClass, modalInstance); | |
appendToElement.toggleClass(modalBodyClass, openedClasses.hasKey(modalBodyClass)); | |
toggleTopWindowClass(true); | |
}); | |
checkRemoveBackdrop(); | |
//move focus to specified element if available, or else to body | |
if (elementToReceiveFocus && elementToReceiveFocus.focus) { | |
elementToReceiveFocus.focus(); | |
} else { | |
appendToElement.focus(); | |
} | |
} | |
// Add or remove "windowTopClass" from the top window in the stack | |
function toggleTopWindowClass(toggleSwitch) { | |
var modalWindow; | |
if (openedWindows.length() > 0) { | |
modalWindow = openedWindows.top().value; | |
modalWindow.modalDomEl.toggleClass(modalWindow.windowTopClass || '', toggleSwitch); | |
} | |
} | |
function checkRemoveBackdrop() { | |
//remove backdrop if no longer needed | |
if (backdropDomEl && backdropIndex() === -1) { | |
var backdropScopeRef = backdropScope; | |
removeAfterAnimate(backdropDomEl, backdropScope, function() { | |
backdropScopeRef = null; | |
}); | |
backdropDomEl = undefined; | |
backdropScope = undefined; | |
} | |
} | |
function removeAfterAnimate(domEl, scope, done) { | |
var asyncDeferred; | |
var asyncPromise = null; | |
var setIsAsync = function() { | |
if (!asyncDeferred) { | |
asyncDeferred = $q.defer(); | |
asyncPromise = asyncDeferred.promise; | |
} | |
return function asyncDone() { | |
asyncDeferred.resolve(); | |
}; | |
}; | |
scope.$broadcast($modalStack.NOW_CLOSING_EVENT, setIsAsync); | |
// Note that it's intentional that asyncPromise might be null. | |
// That's when setIsAsync has not been called during the | |
// NOW_CLOSING_EVENT broadcast. | |
return $q.when(asyncPromise).then(afterAnimating); | |
function afterAnimating() { | |
if (afterAnimating.done) { | |
return; | |
} | |
afterAnimating.done = true; | |
$animateCss(domEl, { | |
event: 'leave' | |
}).start().then(function() { | |
domEl.remove(); | |
}); | |
scope.$destroy(); | |
if (done) { | |
done(); | |
} | |
} | |
} | |
$document.bind('keydown', function(evt) { | |
if (evt.isDefaultPrevented()) { | |
return evt; | |
} | |
var modal = openedWindows.top(); | |
if (modal && modal.value.keyboard) { | |
switch (evt.which) { | |
case 27: { | |
evt.preventDefault(); | |
$rootScope.$apply(function() { | |
$modalStack.dismiss(modal.key, 'escape key press'); | |
}); | |
break; | |
} | |
case 9: { | |
$modalStack.loadFocusElementList(modal); | |
var focusChanged = false; | |
if (evt.shiftKey) { | |
if ($modalStack.isFocusInFirstItem(evt)) { | |
focusChanged = $modalStack.focusLastFocusableElement(); | |
} | |
} else { | |
if ($modalStack.isFocusInLastItem(evt)) { | |
focusChanged = $modalStack.focusFirstFocusableElement(); | |
} | |
} | |
if (focusChanged) { | |
evt.preventDefault(); | |
evt.stopPropagation(); | |
} | |
break; | |
} | |
} | |
} | |
}); | |
$modalStack.open = function(modalInstance, modal) { | |
var modalOpener = $document[0].activeElement, | |
modalBodyClass = modal.openedClass || OPENED_MODAL_CLASS; | |
toggleTopWindowClass(false); | |
openedWindows.add(modalInstance, { | |
deferred: modal.deferred, | |
renderDeferred: modal.renderDeferred, | |
modalScope: modal.scope, | |
backdrop: modal.backdrop, | |
keyboard: modal.keyboard, | |
openedClass: modal.openedClass, | |
windowTopClass: modal.windowTopClass, | |
animation: modal.animation, | |
appendTo: modal.appendTo | |
}); | |
openedClasses.put(modalBodyClass, modalInstance); | |
var appendToElement = modal.appendTo, | |
currBackdropIndex = backdropIndex(); | |
if (!appendToElement.length) { | |
throw new Error('appendTo element not found. Make sure that the element passed is in DOM.'); | |
} | |
if (currBackdropIndex >= 0 && !backdropDomEl) { | |
backdropScope = $rootScope.$new(true); | |
backdropScope.modalOptions = modal; | |
backdropScope.index = currBackdropIndex; | |
var angularBackgroundDomEl = angular.element('<div uib-modal-backdrop="modal-backdrop"></div>'); | |
angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass); | |
if (modal.animation) { | |
angularBackgroundDomEl.attr('modal-animation', 'true'); | |
} | |
backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope); | |
$animate.enter(backdropDomEl, appendToElement); | |
} | |
var angularDomEl = angular.element('<div uib-modal-window="modal-window"></div>'); | |
angularDomEl.attr({ | |
'template-url': modal.windowTemplateUrl, | |
'window-class': modal.windowClass, | |
'window-top-class': modal.windowTopClass, | |
'size': modal.size, | |
'index': openedWindows.length() - 1, | |
'animate': 'animate' | |
}).html(modal.content); | |
if (modal.animation) { | |
angularDomEl.attr('modal-animation', 'true'); | |
} | |
$animate.enter(angularDomEl, appendToElement) | |
.then(function() { | |
$compile(angularDomEl)(modal.scope); | |
$animate.addClass(appendToElement, modalBodyClass); | |
}); | |
openedWindows.top().value.modalDomEl = angularDomEl; | |
openedWindows.top().value.modalOpener = modalOpener; | |
$modalStack.clearFocusListCache(); | |
}; | |
function broadcastClosing(modalWindow, resultOrReason, closing) { | |
return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented; | |
} | |
$modalStack.close = function(modalInstance, result) { | |
var modalWindow = openedWindows.get(modalInstance); | |
if (modalWindow && broadcastClosing(modalWindow, result, true)) { | |
modalWindow.value.modalScope.$$uibDestructionScheduled = true; | |
modalWindow.value.deferred.resolve(result); | |
removeModalWindow(modalInstance, modalWindow.value.modalOpener); | |
return true; | |
} | |
return !modalWindow; | |
}; | |
$modalStack.dismiss = function(modalInstance, reason) { | |
var modalWindow = openedWindows.get(modalInstance); | |
if (modalWindow && broadcastClosing(modalWindow, reason, false)) { | |
modalWindow.value.modalScope.$$uibDestructionScheduled = true; | |
modalWindow.value.deferred.reject(reason); | |
removeModalWindow(modalInstance, modalWindow.value.modalOpener); | |
return true; | |
} | |
return !modalWindow; | |
}; | |
$modalStack.dismissAll = function(reason) { | |
var topModal = this.getTop(); | |
while (topModal && this.dismiss(topModal.key, reason)) { | |
topModal = this.getTop(); | |
} | |
}; | |
$modalStack.getTop = function() { | |
return openedWindows.top(); | |
}; | |
$modalStack.modalRendered = function(modalInstance) { | |
var modalWindow = openedWindows.get(modalInstance); | |
if (modalWindow) { | |
modalWindow.value.renderDeferred.resolve(); | |
} | |
}; | |
$modalStack.focusFirstFocusableElement = function() { | |
if (focusableElementList.length > 0) { | |
focusableElementList[0].focus(); | |
return true; | |
} | |
return false; | |
}; | |
$modalStack.focusLastFocusableElement = function() { | |
if (focusableElementList.length > 0) { | |
focusableElementList[focusableElementList.length - 1].focus(); | |
return true; | |
} | |
return false; | |
}; | |
$modalStack.isFocusInFirstItem = function(evt) { | |
if (focusableElementList.length > 0) { | |
return (evt.target || evt.srcElement) === focusableElementList[0]; | |
} | |
return false; | |
}; | |
$modalStack.isFocusInLastItem = function(evt) { | |
if (focusableElementList.length > 0) { | |
return (evt.target || evt.srcElement) === focusableElementList[focusableElementList.length - 1]; | |
} | |
return false; | |
}; | |
$modalStack.clearFocusListCache = function() { | |
focusableElementList = []; | |
focusIndex = 0; | |
}; | |
$modalStack.loadFocusElementList = function(modalWindow) { | |
if (focusableElementList === undefined || !focusableElementList.length) { | |
if (modalWindow) { | |
var modalDomE1 = modalWindow.value.modalDomEl; | |
if (modalDomE1 && modalDomE1.length) { | |
focusableElementList = modalDomE1[0].querySelectorAll(tababbleSelector); | |
} | |
} | |
} | |
}; | |
return $modalStack; | |
}]) | |
.provider('$uibModal', function() { | |
var $modalProvider = { | |
options: { | |
animation: true, | |
backdrop: true, //can also be false or 'static' | |
keyboard: true | |
}, | |
$get: ['$injector', '$rootScope', '$q', '$document', '$templateRequest', '$controller', '$uibModalStack', | |
function ($injector, $rootScope, $q, $document, $templateRequest, $controller, $modalStack) { | |
var $modal = {}; | |
function getTemplatePromise(options) { | |
return options.template ? $q.when(options.template) : | |
$templateRequest(angular.isFunction(options.templateUrl) ? | |
options.templateUrl() : options.templateUrl); | |
} | |
function getResolvePromises(resolves) { | |
var promisesArr = []; | |
angular.forEach(resolves, function(value) { | |
if (angular.isFunction(value) || angular.isArray(value)) { | |
promisesArr.push($q.when($injector.invoke(value))); | |
} else if (angular.isString(value)) { | |
promisesArr.push($q.when($injector.get(value))); | |
} else { | |
promisesArr.push($q.when(value)); | |
} | |
}); | |
return promisesArr; | |
} | |
var promiseChain = null; | |
$modal.getPromiseChain = function() { | |
return promiseChain; | |
}; | |
$modal.open = function(modalOptions) { | |
var modalResultDeferred = $q.defer(); | |
var modalOpenedDeferred = $q.defer(); | |
var modalRenderDeferred = $q.defer(); | |
//prepare an instance of a modal to be injected into controllers and returned to a caller | |
var modalInstance = { | |
result: modalResultDeferred.promise, | |
opened: modalOpenedDeferred.promise, | |
rendered: modalRenderDeferred.promise, | |
close: function (result) { | |
return $modalStack.close(modalInstance, result); | |
}, | |
dismiss: function (reason) { | |
return $modalStack.dismiss(modalInstance, reason); | |
} | |
}; | |
//merge and clean up options | |
modalOptions = angular.extend({}, $modalProvider.options, modalOptions); | |
modalOptions.resolve = modalOptions.resolve || {}; | |
modalOptions.appendTo = modalOptions.appendTo || $document.find('body').eq(0); | |
//verify options | |
if (!modalOptions.template && !modalOptions.templateUrl) { | |
throw new Error('One of template or templateUrl options is required.'); | |
} | |
var templateAndResolvePromise = | |
$q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve))); | |
function resolveWithTemplate() { | |
return templateAndResolvePromise; | |
} | |
// Wait for the resolution of the existing promise chain. | |
// Then switch to our own combined promise dependency (regardless of how the previous modal fared). | |
// Then add to $modalStack and resolve opened. | |
// Finally clean up the chain variable if no subsequent modal has overwritten it. | |
var samePromise; | |
samePromise = promiseChain = $q.all([promiseChain]) | |
.then(resolveWithTemplate, resolveWithTemplate) | |
.then(function resolveSuccess(tplAndVars) { | |
var modalScope = (modalOptions.scope || $rootScope).$new(); | |
modalScope.$close = modalInstance.close; | |
modalScope.$dismiss = modalInstance.dismiss; | |
modalScope.$on('$destroy', function() { | |
if (!modalScope.$$uibDestructionScheduled) { | |
modalScope.$dismiss('$uibUnscheduledDestruction'); | |
} | |
}); | |
var ctrlInstance, ctrlLocals = {}; | |
var resolveIter = 1; | |
//controllers | |
if (modalOptions.controller) { | |
ctrlLocals.$scope = modalScope; | |
ctrlLocals.$uibModalInstance = modalInstance; | |
angular.forEach(modalOptions.resolve, function(value, key) { | |
ctrlLocals[key] = tplAndVars[resolveIter++]; | |
}); | |
ctrlInstance = $controller(modalOptions.controller, ctrlLocals); | |
if (modalOptions.controllerAs) { | |
if (modalOptions.bindToController) { | |
angular.extend(ctrlInstance, modalScope); | |
} | |
modalScope[modalOptions.controllerAs] = ctrlInstance; | |
} | |
} | |
$modalStack.open(modalInstance, { | |
scope: modalScope, | |
deferred: modalResultDeferred, | |
renderDeferred: modalRenderDeferred, | |
content: tplAndVars[0], | |
animation: modalOptions.animation, | |
backdrop: modalOptions.backdrop, | |
keyboard: modalOptions.keyboard, | |
backdropClass: modalOptions.backdropClass, | |
windowTopClass: modalOptions.windowTopClass, | |
windowClass: modalOptions.windowClass, | |
windowTemplateUrl: modalOptions.windowTemplateUrl, | |
size: modalOptions.size, | |
openedClass: modalOptions.openedClass, | |
appendTo: modalOptions.appendTo | |
}); | |
modalOpenedDeferred.resolve(true); | |
}, function resolveError(reason) { | |
modalOpenedDeferred.reject(reason); | |
modalResultDeferred.reject(reason); | |
})['finally'](function() { | |
if (promiseChain === samePromise) { | |
promiseChain = null; | |
} | |
}); | |
return modalInstance; | |
}; | |
return $modal; | |
} | |
] | |
}; | |
return $modalProvider; | |
}); | |
angular.module('ui.bootstrap.pagination', []) | |
.controller('UibPaginationController', ['$scope', '$attrs', '$parse', function($scope, $attrs, $parse) { | |
var self = this, | |
ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl | |
setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; | |
this.init = function(ngModelCtrl_, config) { | |
ngModelCtrl = ngModelCtrl_; | |
this.config = config; | |
ngModelCtrl.$render = function() { | |
self.render(); | |
}; | |
if ($attrs.itemsPerPage) { | |
$scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) { | |
self.itemsPerPage = parseInt(value, 10); | |
$scope.totalPages = self.calculateTotalPages(); | |
updatePage(); | |
}); | |
} else { | |
this.itemsPerPage = config.itemsPerPage; | |
} | |
$scope.$watch('totalItems', function(newTotal, oldTotal) { | |
if (angular.isDefined(newTotal) || newTotal !== oldTotal) { | |
$scope.totalPages = self.calculateTotalPages(); | |
updatePage(); | |
} | |
}); | |
}; | |
this.calculateTotalPages = function() { | |
var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage); | |
return Math.max(totalPages || 0, 1); | |
}; | |
this.render = function() { | |
$scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1; | |
}; | |
$scope.selectPage = function(page, evt) { | |
if (evt) { | |
evt.preventDefault(); | |
} | |
var clickAllowed = !$scope.ngDisabled || !evt; | |
if (clickAllowed && $scope.page !== page && page > 0 && page <= $scope.totalPages) { | |
if (evt && evt.target) { | |
evt.target.blur(); | |
} | |
ngModelCtrl.$setViewValue(page); | |
ngModelCtrl.$render(); | |
} | |
}; | |
$scope.getText = function(key) { | |
return $scope[key + 'Text'] || self.config[key + 'Text']; | |
}; | |
$scope.noPrevious = function() { | |
return $scope.page === 1; | |
}; | |
$scope.noNext = function() { | |
return $scope.page === $scope.totalPages; | |
}; | |
function updatePage() { | |
setNumPages($scope.$parent, $scope.totalPages); // Readonly variable | |
if ($scope.page > $scope.totalPages) { | |
$scope.selectPage($scope.totalPages); | |
} else { | |
ngModelCtrl.$render(); | |
} | |
} | |
}]) | |
.constant('uibPaginationConfig', { | |
itemsPerPage: 10, | |
boundaryLinks: false, | |
boundaryLinkNumbers: false, | |
directionLinks: true, | |
firstText: 'First', | |
previousText: 'Previous', | |
nextText: 'Next', | |
lastText: 'Last', | |
rotate: true, | |
forceEllipses: false | |
}) | |
.directive('uibPagination', ['$parse', 'uibPaginationConfig', function($parse, paginationConfig) { | |
return { | |
restrict: 'EA', | |
scope: { | |
totalItems: '=', | |
firstText: '@', | |
previousText: '@', | |
nextText: '@', | |
lastText: '@', | |
ngDisabled:'=' | |
}, | |
require: ['uibPagination', '?ngModel'], | |
controller: 'UibPaginationController', | |
controllerAs: 'pagination', | |
templateUrl: function(element, attrs) { | |
return attrs.templateUrl || 'uib/template/pagination/pagination.html'; | |
}, | |
replace: true, | |
link: function(scope, element, attrs, ctrls) { | |
var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; | |
if (!ngModelCtrl) { | |
return; // do nothing if no ng-model | |
} | |
// Setup configuration parameters | |
var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize, | |
rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate, | |
forceEllipses = angular.isDefined(attrs.forceEllipses) ? scope.$parent.$eval(attrs.forceEllipses) : paginationConfig.forceEllipses, | |
boundaryLinkNumbers = angular.isDefined(attrs.boundaryLinkNumbers) ? scope.$parent.$eval(attrs.boundaryLinkNumbers) : paginationConfig.boundaryLinkNumbers; | |
scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks; | |
scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks; | |
paginationCtrl.init(ngModelCtrl, paginationConfig); | |
if (attrs.maxSize) { | |
scope.$parent.$watch($parse(attrs.maxSize), function(value) { | |
maxSize = parseInt(value, 10); | |
paginationCtrl.render(); | |
}); | |
} | |
// Create page object used in template | |
function makePage(number, text, isActive) { | |
return { | |
number: number, | |
text: text, | |
active: isActive | |
}; | |
} | |
function getPages(currentPage, totalPages) { | |
var pages = []; | |
// Default page limits | |
var startPage = 1, endPage = totalPages; | |
var isMaxSized = angular.isDefined(maxSize) && maxSize < totalPages; | |
// recompute if maxSize | |
if (isMaxSized) { | |
if (rotate) { | |
// Current page is displayed in the middle of the visible ones | |
startPage = Math.max(currentPage - Math.floor(maxSize / 2), 1); | |
endPage = startPage + maxSize - 1; | |
// Adjust if limit is exceeded | |
if (endPage > totalPages) { | |
endPage = totalPages; | |
startPage = endPage - maxSize + 1; | |
} | |
} else { | |
// Visible pages are paginated with maxSize | |
startPage = (Math.ceil(currentPage / maxSize) - 1) * maxSize + 1; | |
// Adjust last page if limit is exceeded | |
endPage = Math.min(startPage + maxSize - 1, totalPages); | |
} | |
} | |
// Add page number links | |
for (var number = startPage; number <= endPage; number++) { | |
var page = makePage(number, number, number === currentPage); | |
pages.push(page); | |
} | |
// Add links to move between page sets | |
if (isMaxSized && maxSize > 0 && (!rotate || forceEllipses || boundaryLinkNumbers)) { | |
if (startPage > 1) { | |
if (!boundaryLinkNumbers || startPage > 3) { //need ellipsis for all options unless range is too close to beginning | |
var previousPageSet = makePage(startPage - 1, '...', false); | |
pages.unshift(previousPageSet); | |
} | |
if (boundaryLinkNumbers) { | |
if (startPage === 3) { //need to replace ellipsis when the buttons would be sequential | |
var secondPageLink = makePage(2, '2', false); | |
pages.unshift(secondPageLink); | |
} | |
//add the first page | |
var firstPageLink = makePage(1, '1', false); | |
pages.unshift(firstPageLink); | |
} | |
} | |
if (endPage < totalPages) { | |
if (!boundaryLinkNumbers || endPage < totalPages - 2) { //need ellipsis for all options unless range is too close to end | |
var nextPageSet = makePage(endPage + 1, '...', false); | |
pages.push(nextPageSet); | |
} | |
if (boundaryLinkNumbers) { | |
if (endPage === totalPages - 2) { //need to replace ellipsis when the buttons would be sequential | |
var secondToLastPageLink = makePage(totalPages - 1, totalPages - 1, false); | |
pages.push(secondToLastPageLink); | |
} | |
//add the last page | |
var lastPageLink = makePage(totalPages, totalPages, false); | |
pages.push(lastPageLink); | |
} | |
} | |
} | |
return pages; | |
} | |
var originalRender = paginationCtrl.render; | |
paginationCtrl.render = function() { | |
originalRender(); | |
if (scope.page > 0 && scope.page <= scope.totalPages) { | |
scope.pages = getPages(scope.page, scope.totalPages); | |
} | |
}; | |
} | |
}; | |
}]); | |
angular.module('ui.bootstrap.pager', ['ui.bootstrap.pagination']) | |
.constant('uibPagerConfig', { | |
itemsPerPage: 10, | |
previousText: '« Previous', | |
nextText: 'Next »', | |
align: true | |
}) | |
.directive('uibPager', ['uibPagerConfig', function(pagerConfig) { | |
return { | |
scope: { | |
totalItems: '=', | |
previousText: '@', | |
nextText: '@', | |
ngDisabled: '=' | |
}, | |
require: ['uibPager', '?ngModel'], | |
controller: 'UibPaginationController', | |
controllerAs: 'pager', | |
templateUrl: function(element, attrs) { | |
return attrs.templateUrl || 'uib/template/pager/pager.html'; | |
}, | |
replace: true, | |
link: function(scope, element, attrs, ctrls) { | |
var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; | |
if (!ngModelCtrl) { | |
return; // do nothing if no ng-model | |
} | |
scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align; | |
paginationCtrl.init(ngModelCtrl, pagerConfig); | |
} | |
}; | |
}]); | |
/** | |
* The following features are still outstanding: animation as a | |
* function, placement as a function, inside, support for more triggers than | |
* just mouse enter/leave, html tooltips, and selector delegation. | |
*/ | |
angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.stackedMap']) | |
/** | |
* The $tooltip service creates tooltip- and popover-like directives as well as | |
* houses global options for them. | |
*/ | |
.provider('$uibTooltip', function() { | |
// The default options tooltip and popover. | |
var defaultOptions = { | |
placement: 'top', | |
placementClassPrefix: '', | |
animation: true, | |
popupDelay: 0, | |
popupCloseDelay: 0, | |
useContentExp: false | |
}; | |
// Default hide triggers for each show trigger | |
var triggerMap = { | |
'mouseenter': 'mouseleave', | |
'click': 'click', | |
'outsideClick': 'outsideClick', | |
'focus': 'blur', | |
'none': '' | |
}; | |
// The options specified to the provider globally. | |
var globalOptions = {}; | |
/** | |
* `options({})` allows global configuration of all tooltips in the | |
* application. | |
* | |
* var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) { | |
* // place tooltips left instead of top by default | |
* $tooltipProvider.options( { placement: 'left' } ); | |
* }); | |
*/ | |
this.options = function(value) { | |
angular.extend(globalOptions, value); | |
}; | |
/** | |
* This allows you to extend the set of trigger mappings available. E.g.: | |
* | |
* $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ); | |
*/ | |
this.setTriggers = function setTriggers(triggers) { | |
angular.extend(triggerMap, triggers); | |
}; | |
/** | |
* This is a helper function for translating camel-case to snake-case. | |
*/ | |
function snake_case(name) { | |
var regexp = /[A-Z]/g; | |
var separator = '-'; | |
return name.replace(regexp, function(letter, pos) { | |
return (pos ? separator : '') + letter.toLowerCase(); | |
}); | |
} | |
/** | |
* Returns the actual instance of the $tooltip service. | |
* TODO support multiple triggers | |
*/ | |
this.$get = ['$window', '$compile', '$timeout', '$document', '$uibPosition', '$interpolate', '$rootScope', '$parse', '$$stackedMap', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse, $$stackedMap) { | |
var openedTooltips = $$stackedMap.createNew(); | |
$document.on('keypress', function(e) { | |
if (e.which === 27) { | |
var last = openedTooltips.top(); | |
if (last) { | |
last.value.close(); | |
openedTooltips.removeTop(); | |
last = null; | |
} | |
} | |
}); | |
return function $tooltip(ttType, prefix, defaultTriggerShow, options) { | |
options = angular.extend({}, defaultOptions, globalOptions, options); | |
/** | |
* Returns an object of show and hide triggers. | |
* | |
* If a trigger is supplied, | |
* it is used to show the tooltip; otherwise, it will use the `trigger` | |
* option passed to the `$tooltipProvider.options` method; else it will | |
* default to the trigger supplied to this directive factory. | |
* | |
* The hide trigger is based on the show trigger. If the `trigger` option | |
* was passed to the `$tooltipProvider.options` method, it will use the | |
* mapped trigger from `triggerMap` or the passed trigger if the map is | |
* undefined; otherwise, it uses the `triggerMap` value of the show | |
* trigger; else it will just use the show trigger. | |
*/ | |
function getTriggers(trigger) { | |
var show = (trigger || options.trigger || defaultTriggerShow).split(' '); | |
var hide = show.map(function(trigger) { | |
return triggerMap[trigger] || trigger; | |
}); | |
return { | |
show: show, | |
hide: hide | |
}; | |
} | |
var directiveName = snake_case(ttType); | |
var startSym = $interpolate.startSymbol(); | |
var endSym = $interpolate.endSymbol(); | |
var template = | |
'<div '+ directiveName + '-popup '+ | |
'title="' + startSym + 'title' + endSym + '" '+ | |
(options.useContentExp ? | |
'content-exp="contentExp()" ' : | |
'content="' + startSym + 'content' + endSym + '" ') + | |
'placement="' + startSym + 'placement' + endSym + '" '+ | |
'popup-class="' + startSym + 'popupClass' + endSym + '" '+ | |
'animation="animation" ' + | |
'is-open="isOpen"' + | |
'origin-scope="origScope" ' + | |
'style="visibility: hidden; display: block; top: -9999px; left: -9999px;"' + | |
'>' + | |
'</div>'; | |
return { | |
compile: function(tElem, tAttrs) { | |
var tooltipLinker = $compile(template); | |
return function link(scope, element, attrs, tooltipCtrl) { | |
var tooltip; | |
var tooltipLinkedScope; | |
var transitionTimeout; | |
var showTimeout; | |
var hideTimeout; | |
var positionTimeout; | |
var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false; | |
var triggers = getTriggers(undefined); | |
var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']); | |
var ttScope = scope.$new(true); | |
var repositionScheduled = false; | |
var isOpenParse = angular.isDefined(attrs[prefix + 'IsOpen']) ? $parse(attrs[prefix + 'IsOpen']) : false; | |
var contentParse = options.useContentExp ? $parse(attrs[ttType]) : false; | |
var observers = []; | |
var positionTooltip = function() { | |
// check if tooltip exists and is not empty | |
if (!tooltip || !tooltip.html()) { return; } | |
if (!positionTimeout) { | |
positionTimeout = $timeout(function() { | |
// Reset the positioning. | |
tooltip.css({ top: 0, left: 0 }); | |
// Now set the calculated positioning. | |
var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody); | |
tooltip.css({ top: ttPosition.top + 'px', left: ttPosition.left + 'px', visibility: 'visible' }); | |
// If the placement class is prefixed, still need | |
// to remove the TWBS standard class. | |
if (options.placementClassPrefix) { | |
tooltip.removeClass('top bottom left right'); | |
} | |
tooltip.removeClass( | |
options.placementClassPrefix + 'top ' + | |
options.placementClassPrefix + 'top-left ' + | |
options.placementClassPrefix + 'top-right ' + | |
options.placementClassPrefix + 'bottom ' + | |
options.placementClassPrefix + 'bottom-left ' + | |
options.placementClassPrefix + 'bottom-right ' + | |
options.placementClassPrefix + 'left ' + | |
options.placementClassPrefix + 'left-top ' + | |
options.placementClassPrefix + 'left-bottom ' + | |
options.placementClassPrefix + 'right ' + | |
options.placementClassPrefix + 'right-top ' + | |
options.placementClassPrefix + 'right-bottom'); | |
var placement = ttPosition.placement.split('-'); | |
tooltip.addClass(placement[0], options.placementClassPrefix + ttPosition.placement); | |
$position.positionArrow(tooltip, ttPosition.placement); | |
positionTimeout = null; | |
}, 0, false); | |
} | |
}; | |
// Set up the correct scope to allow transclusion later | |
ttScope.origScope = scope; | |
// By default, the tooltip is not open. | |
// TODO add ability to start tooltip opened | |
ttScope.isOpen = false; | |
openedTooltips.add(ttScope, { | |
close: hide | |
}); | |
function toggleTooltipBind() { | |
if (!ttScope.isOpen) { | |
showTooltipBind(); | |
} else { | |
hideTooltipBind(); | |
} | |
} | |
// Show the tooltip with delay if specified, otherwise show it immediately | |
function showTooltipBind() { | |
if (hasEnableExp && !scope.$eval(attrs[prefix + 'Enable'])) { | |
return; | |
} | |
cancelHide(); | |
prepareTooltip(); | |
if (ttScope.popupDelay) { | |
// Do nothing if the tooltip was already scheduled to pop-up. | |
// This happens if show is triggered multiple times before any hide is triggered. | |
if (!showTimeout) { | |
showTimeout = $timeout(show, ttScope.popupDelay, false); | |
} | |
} else { | |
show(); | |
} | |
} | |
function hideTooltipBind() { | |
cancelShow(); | |
if (ttScope.popupCloseDelay) { | |
if (!hideTimeout) { | |
hideTimeout = $timeout(hide, ttScope.popupCloseDelay, false); | |
} | |
} else { | |
hide(); | |
} | |
} | |
// Show the tooltip popup element. | |
function show() { | |
cancelShow(); | |
cancelHide(); | |
// Don't show empty tooltips. | |
if (!ttScope.content) { | |
return angular.noop; | |
} | |
createTooltip(); | |
// And show the tooltip. | |
ttScope.$evalAsync(function() { | |
ttScope.isOpen = true; | |
assignIsOpen(true); | |
positionTooltip(); | |
}); | |
} | |
function cancelShow() { | |
if (showTimeout) { | |
$timeout.cancel(showTimeout); | |
showTimeout = null; | |
} | |
if (positionTimeout) { | |
$timeout.cancel(positionTimeout); | |
positionTimeout = null; | |
} | |
} | |
// Hide the tooltip popup element. | |
function hide() { | |
if (!ttScope) { | |
return; | |
} | |
// First things first: we don't show it anymore. | |
ttScope.$evalAsync(function() { | |
ttScope.isOpen = false; | |
assignIsOpen(false); | |
// And now we remove it from the DOM. However, if we have animation, we | |
// need to wait for it to expire beforehand. | |
// FIXME: this is a placeholder for a port of the transitions library. | |
// The fade transition in TWBS is 150ms. | |
if (ttScope.animation) { | |
if (!transitionTimeout) { | |
transitionTimeout = $timeout(removeTooltip, 150, false); | |
} | |
} else { | |
removeTooltip(); | |
} | |
}); | |
} | |
function cancelHide() { | |
if (hideTimeout) { | |
$timeout.cancel(hideTimeout); | |
hideTimeout = null; | |
} | |
if (transitionTimeout) { | |
$timeout.cancel(transitionTimeout); | |
transitionTimeout = null; | |
} | |
} | |
function createTooltip() { | |
// There can only be one tooltip element per directive shown at once. | |
if (tooltip) { | |
return; | |
} | |
tooltipLinkedScope = ttScope.$new(); | |
tooltip = tooltipLinker(tooltipLinkedScope, function(tooltip) { | |
if (appendToBody) { | |
$document.find('body').append(tooltip); | |
} else { | |
element.after(tooltip); | |
} | |
}); | |
prepObservers(); | |
} | |
function removeTooltip() { | |
cancelShow(); | |
cancelHide(); | |
unregisterObservers(); | |
if (tooltip) { | |
tooltip.remove(); | |
tooltip = null; | |
} | |
if (tooltipLinkedScope) { | |
tooltipLinkedScope.$destroy(); | |
tooltipLinkedScope = null; | |
} | |
} | |
/** | |
* Set the inital scope values. Once | |
* the tooltip is created, the observers | |
* will be added to keep things in synch. | |
*/ | |
function prepareTooltip() { | |
ttScope.title = attrs[prefix + 'Title']; | |
if (contentParse) { | |
ttScope.content = contentParse(scope); | |
} else { | |
ttScope.content = attrs[ttType]; | |
} | |
ttScope.popupClass = attrs[prefix + 'Class']; | |
ttScope.placement = angular.isDefined(attrs[prefix + 'Placement']) ? attrs[prefix + 'Placement'] : options.placement; | |
var delay = parseInt(attrs[prefix + 'PopupDelay'], 10); | |
var closeDelay = parseInt(attrs[prefix + 'PopupCloseDelay'], 10); | |
ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay; | |
ttScope.popupCloseDelay = !isNaN(closeDelay) ? closeDelay : options.popupCloseDelay; | |
} | |
function assignIsOpen(isOpen) { | |
if (isOpenParse && angular.isFunction(isOpenParse.assign)) { | |
isOpenParse.assign(scope, isOpen); | |
} | |
} | |
ttScope.contentExp = function() { | |
return ttScope.content; | |
}; | |
/** | |
* Observe the relevant attributes. | |
*/ | |
attrs.$observe('disabled', function(val) { | |
if (val) { | |
cancelShow(); | |
} | |
if (val && ttScope.isOpen) { | |
hide(); | |
} | |
}); | |
if (isOpenParse) { | |
scope.$watch(isOpenParse, function(val) { | |
if (ttScope && !val === ttScope.isOpen) { | |
toggleTooltipBind(); | |
} | |
}); | |
} | |
function prepObservers() { | |
observers.length = 0; | |
if (contentParse) { | |
observers.push( | |
scope.$watch(contentParse, function(val) { | |
ttScope.content = val; | |
if (!val && ttScope.isOpen) { | |
hide(); | |
} | |
}) | |
); | |
observers.push( | |
tooltipLinkedScope.$watch(function() { | |
if (!repositionScheduled) { | |
repositionScheduled = true; | |
tooltipLinkedScope.$$postDigest(function() { | |
repositionScheduled = false; | |
if (ttScope && ttScope.isOpen) { | |
positionTooltip(); | |
} | |
}); | |
} | |
}) | |
); | |
} else { | |
observers.push( | |
attrs.$observe(ttType, function(val) { | |
ttScope.content = val; | |
if (!val && ttScope.isOpen) { | |
hide(); | |
} else { | |
positionTooltip(); | |
} | |
}) | |
); | |
} | |
observers.push( | |
attrs.$observe(prefix + 'Title', function(val) { | |
ttScope.title = val; | |
if (ttScope.isOpen) { | |
positionTooltip(); | |
} | |
}) | |
); | |
observers.push( | |
attrs.$observe(prefix + 'Placement', function(val) { | |
ttScope.placement = val ? val : options.placement; | |
if (ttScope.isOpen) { | |
positionTooltip(); | |
} | |
}) | |
); | |
} | |
function unregisterObservers() { | |
if (observers.length) { | |
angular.forEach(observers, function(observer) { | |
observer(); | |
}); | |
observers.length = 0; | |
} | |
} | |
// hide tooltips/popovers for outsideClick trigger | |
function bodyHideTooltipBind(e) { | |
if (!ttScope || !ttScope.isOpen || !tooltip) { | |
return; | |
} | |
// make sure the tooltip/popover link or tool tooltip/popover itself were not clicked | |
if (!element[0].contains(e.target) && !tooltip[0].contains(e.target)) { | |
hideTooltipBind(); | |
} | |
} | |
var unregisterTriggers = function() { | |
triggers.show.forEach(function(trigger) { | |
if (trigger === 'outsideClick') { | |
element[0].removeEventListener('click', toggleTooltipBind); | |
} else { | |
element[0].removeEventListener(trigger, showTooltipBind); | |
element[0].removeEventListener(trigger, toggleTooltipBind); | |
} | |
}); | |
triggers.hide.forEach(function(trigger) { | |
trigger.split(' ').forEach(function(hideTrigger) { | |
if (trigger === 'outsideClick') { | |
$document[0].removeEventListener('click', bodyHideTooltipBind); | |
} else { | |
element[0].removeEventListener(hideTrigger, hideTooltipBind); | |
} | |
}); | |
}); | |
}; | |
function prepTriggers() { | |
var val = attrs[prefix + 'Trigger']; | |
unregisterTriggers(); | |
triggers = getTriggers(val); | |
if (triggers.show !== 'none') { | |
triggers.show.forEach(function(trigger, idx) { | |
// Using raw addEventListener due to jqLite/jQuery bug - #4060 | |
if (trigger === 'outsideClick') { | |
element[0].addEventListener('click', toggleTooltipBind); | |
$document[0].addEventListener('click', bodyHideTooltipBind); | |
} else if (trigger === triggers.hide[idx]) { | |
element[0].addEventListener(trigger, toggleTooltipBind); | |
} else if (trigger) { | |
element[0].addEventListener(trigger, showTooltipBind); | |
triggers.hide[idx].split(' ').forEach(function(trigger) { | |
element[0].addEventListener(trigger, hideTooltipBind); | |
}); | |
} | |
element.on('keypress', function(e) { | |
if (e.which === 27) { | |
hideTooltipBind(); | |
} | |
}); | |
}); | |
} | |
} | |
prepTriggers(); | |
var animation = scope.$eval(attrs[prefix + 'Animation']); | |
ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation; | |
var appendToBodyVal = scope.$eval(attrs[prefix + 'AppendToBody']); | |
appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody; | |
// if a tooltip is attached to <body> we need to remove it on | |
// location change as its parent scope will probably not be destroyed | |
// by the change. | |
if (appendToBody) { | |
scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess() { | |
if (ttScope.isOpen) { | |
hide(); | |
} | |
}); | |
} | |
// Make sure tooltip is destroyed and removed. | |
scope.$on('$destroy', function onDestroyTooltip() { | |
unregisterTriggers(); | |
removeTooltip(); | |
openedTooltips.remove(ttScope); | |
ttScope = null; | |
}); | |
}; | |
} | |
}; | |
}; | |
}]; | |
}) | |
// This is mostly ngInclude code but with a custom scope | |
.directive('uibTooltipTemplateTransclude', [ | |
'$animate', '$sce', '$compile', '$templateRequest', | |
function ($animate, $sce, $compile, $templateRequest) { | |
return { | |
link: function(scope, elem, attrs) { | |
var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope); | |
var changeCounter = 0, | |
currentScope, | |
previousElement, | |
currentElement; | |
var cleanupLastIncludeContent = function() { | |
if (previousElement) { | |
previousElement.remove(); | |
previousElement = null; | |
} | |
if (currentScope) { | |
currentScope.$destroy(); | |
currentScope = null; | |
} | |
if (currentElement) { | |
$animate.leave(currentElement).then(function() { | |
previousElement = null; | |
}); | |
previousElement = currentElement; | |
currentElement = null; | |
} | |
}; | |
scope.$watch($sce.parseAsResourceUrl(attrs.uibTooltipTemplateTransclude), function(src) { | |
var thisChangeId = ++changeCounter; | |
if (src) { | |
//set the 2nd param to true to ignore the template request error so that the inner | |
//contents and scope can be cleaned up. | |
$templateRequest(src, true).then(function(response) { | |
if (thisChangeId !== changeCounter) { return; } | |
var newScope = origScope.$new(); | |
var template = response; | |
var clone = $compile(template)(newScope, function(clone) { | |
cleanupLastIncludeContent(); | |
$animate.enter(clone, elem); | |
}); | |
currentScope = newScope; | |
currentElement = clone; | |
currentScope.$emit('$includeContentLoaded', src); | |
}, function() { | |
if (thisChangeId === changeCounter) { | |
cleanupLastIncludeContent(); | |
scope.$emit('$includeContentError', src); | |
} | |
}); | |
scope.$emit('$includeContentRequested', src); | |
} else { | |
cleanupLastIncludeContent(); | |
} | |
}); | |
scope.$on('$destroy', cleanupLastIncludeContent); | |
} | |
}; | |
}]) | |
/** | |
* Note that it's intentional that these classes are *not* applied through $animate. | |
* They must not be animated as they're expected to be present on the tooltip on | |
* initialization. | |
*/ | |
.directive('uibTooltipClasses', ['$uibPosition', function($uibPosition) { | |
return { | |
restrict: 'A', | |
link: function(scope, element, attrs) { | |
// need to set the primary position so the | |
// arrow has space during position measure. | |
// tooltip.positionTooltip() | |
if (scope.placement) { | |
// // There are no top-left etc... classes | |
// // in TWBS, so we need the primary position. | |
var position = $uibPosition.parsePlacement(scope.placement); | |
element.addClass(position[0]); | |
} else { | |
element.addClass('top'); | |
} | |
if (scope.popupClass) { | |
element.addClass(scope.popupClass); | |
} | |
if (scope.animation()) { | |
element.addClass(attrs.tooltipAnimationClass); | |
} | |
} | |
}; | |
}]) | |
.directive('uibTooltipPopup', function() { | |
return { | |
replace: true, | |
scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, | |
templateUrl: 'uib/template/tooltip/tooltip-popup.html' | |
}; | |
}) | |
.directive('uibTooltip', [ '$uibTooltip', function($uibTooltip) { | |
return $uibTooltip('uibTooltip', 'tooltip', 'mouseenter'); | |
}]) | |
.directive('uibTooltipTemplatePopup', function() { | |
return { | |
replace: true, | |
scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&', | |
originScope: '&' }, | |
templateUrl: 'uib/template/tooltip/tooltip-template-popup.html' | |
}; | |
}) | |
.directive('uibTooltipTemplate', ['$uibTooltip', function($uibTooltip) { | |
return $uibTooltip('uibTooltipTemplate', 'tooltip', 'mouseenter', { | |
useContentExp: true | |
}); | |
}]) | |
.directive('uibTooltipHtmlPopup', function() { | |
return { | |
replace: true, | |
scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, | |
templateUrl: 'uib/template/tooltip/tooltip-html-popup.html' | |
}; | |
}) | |
.directive('uibTooltipHtml', ['$uibTooltip', function($uibTooltip) { | |
return $uibTooltip('uibTooltipHtml', 'tooltip', 'mouseenter', { | |
useContentExp: true | |
}); | |
}]); | |
/** | |
* The following features are still outstanding: popup delay, animation as a | |
* function, placement as a function, inside, support for more triggers than | |
* just mouse enter/leave, and selector delegatation. | |
*/ | |
angular.module('ui.bootstrap.popover', ['ui.bootstrap.tooltip']) | |
.directive('uibPopoverTemplatePopup', function() { | |
return { | |
replace: true, | |
scope: { title: '@', contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&', | |
originScope: '&' }, | |
templateUrl: 'uib/template/popover/popover-template.html' | |
}; | |
}) | |
.directive('uibPopoverTemplate', ['$uibTooltip', function($uibTooltip) { | |
return $uibTooltip('uibPopoverTemplate', 'popover', 'click', { | |
useContentExp: true | |
}); | |
}]) | |
.directive('uibPopoverHtmlPopup', function() { | |
return { | |
replace: true, | |
scope: { contentExp: '&', title: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, | |
templateUrl: 'uib/template/popover/popover-html.html' | |
}; | |
}) | |
.directive('uibPopoverHtml', ['$uibTooltip', function($uibTooltip) { | |
return $uibTooltip('uibPopoverHtml', 'popover', 'click', { | |
useContentExp: true | |
}); | |
}]) | |
.directive('uibPopoverPopup', function() { | |
return { | |
replace: true, | |
scope: { title: '@', content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, | |
templateUrl: 'uib/template/popover/popover.html' | |
}; | |
}) | |
.directive('uibPopover', ['$uibTooltip', function($uibTooltip) { | |
return $uibTooltip('uibPopover', 'popover', 'click'); | |
}]); | |
angular.module('ui.bootstrap.progressbar', []) | |
.constant('uibProgressConfig', { | |
animate: true, | |
max: 100 | |
}) | |
.controller('UibProgressController', ['$scope', '$attrs', 'uibProgressConfig', function($scope, $attrs, progressConfig) { | |
var self = this, | |
animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate; | |
this.bars = []; | |
$scope.max = angular.isDefined($scope.max) ? $scope.max : progressConfig.max; | |
this.addBar = function(bar, element, attrs) { | |
if (!animate) { | |
element.css({'transition': 'none'}); | |
} | |
this.bars.push(bar); | |
bar.max = $scope.max; | |
bar.title = attrs && angular.isDefined(attrs.title) ? attrs.title : 'progressbar'; | |
bar.$watch('value', function(value) { | |
bar.recalculatePercentage(); | |
}); | |
bar.recalculatePercentage = function() { | |
var totalPercentage = self.bars.reduce(function(total, bar) { | |
bar.percent = +(100 * bar.value / bar.max).toFixed(2); | |
return total + bar.percent; | |
}, 0); | |
if (totalPercentage > 100) { | |
bar.percent -= totalPercentage - 100; | |
} | |
}; | |
bar.$on('$destroy', function() { | |
element = null; | |
self.removeBar(bar); | |
}); | |
}; | |
this.removeBar = function(bar) { | |
this.bars.splice(this.bars.indexOf(bar), 1); | |
this.bars.forEach(function (bar) { | |
bar.recalculatePercentage(); | |
}); | |
}; | |
$scope.$watch('max', function(max) { | |
self.bars.forEach(function(bar) { | |
bar.max = $scope.max; | |
bar.recalculatePercentage(); | |
}); | |
}); | |
}]) | |
.directive('uibProgress', function() { | |
return { | |
replace: true, | |
transclude: true, | |
controller: 'UibProgressController', | |
require: 'uibProgress', | |
scope: { | |
max: '=?' | |
}, | |
templateUrl: 'uib/template/progressbar/progress.html' | |
}; | |
}) | |
.directive('uibBar', function() { | |
return { | |
replace: true, | |
transclude: true, | |
require: '^uibProgress', | |
scope: { | |
value: '=', | |
type: '@' | |
}, | |
templateUrl: 'uib/template/progressbar/bar.html', | |
link: function(scope, element, attrs, progressCtrl) { | |
progressCtrl.addBar(scope, element, attrs); | |
} | |
}; | |
}) | |
.directive('uibProgressbar', function() { | |
return { | |
replace: true, | |
transclude: true, | |
controller: 'UibProgressController', | |
scope: { | |
value: '=', | |
max: '=?', | |
type: '@' | |
}, | |
templateUrl: 'uib/template/progressbar/progressbar.html', | |
link: function(scope, element, attrs, progressCtrl) { | |
progressCtrl.addBar(scope, angular.element(element.children()[0]), {title: attrs.title}); | |
} | |
}; | |
}); | |
angular.module('ui.bootstrap.rating', []) | |
.constant('uibRatingConfig', { | |
max: 5, | |
stateOn: null, | |
stateOff: null, | |
titles : ['one', 'two', 'three', 'four', 'five'] | |
}) | |
.controller('UibRatingController', ['$scope', '$attrs', 'uibRatingConfig', function($scope, $attrs, ratingConfig) { | |
var ngModelCtrl = { $setViewValue: angular.noop }; | |
this.init = function(ngModelCtrl_) { | |
ngModelCtrl = ngModelCtrl_; | |
ngModelCtrl.$render = this.render; | |
ngModelCtrl.$formatters.push(function(value) { | |
if (angular.isNumber(value) && value << 0 !== value) { | |
value = Math.round(value); | |
} | |
return value; | |
}); | |
this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn; | |
this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff; | |
var tmpTitles = angular.isDefined($attrs.titles) ? $scope.$parent.$eval($attrs.titles) : ratingConfig.titles ; | |
this.titles = angular.isArray(tmpTitles) && tmpTitles.length > 0 ? | |
tmpTitles : ratingConfig.titles; | |
var ratingStates = angular.isDefined($attrs.ratingStates) ? | |
$scope.$parent.$eval($attrs.ratingStates) : | |
new Array(angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max); | |
$scope.range = this.buildTemplateObjects(ratingStates); | |
}; | |
this.buildTemplateObjects = function(states) { | |
for (var i = 0, n = states.length; i < n; i++) { | |
states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff, title: this.getTitle(i) }, states[i]); | |
} | |
return states; | |
}; | |
this.getTitle = function(index) { | |
if (index >= this.titles.length) { | |
return index + 1; | |
} | |
return this.titles[index]; | |
}; | |
$scope.rate = function(value) { | |
if (!$scope.readonly && value >= 0 && value <= $scope.range.length) { | |
ngModelCtrl.$setViewValue(ngModelCtrl.$viewValue === value ? 0 : value); | |
ngModelCtrl.$render(); | |
} | |
}; | |
$scope.enter = function(value) { | |
if (!$scope.readonly) { | |
$scope.value = value; | |
} | |
$scope.onHover({value: value}); | |
}; | |
$scope.reset = function() { | |
$scope.value = ngModelCtrl.$viewValue; | |
$scope.onLeave(); | |
}; | |
$scope.onKeydown = function(evt) { | |
if (/(37|38|39|40)/.test(evt.which)) { | |
evt.preventDefault(); | |
evt.stopPropagation(); | |
$scope.rate($scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1)); | |
} | |
}; | |
this.render = function() { | |
$scope.value = ngModelCtrl.$viewValue; | |
}; | |
}]) | |
.directive('uibRating', function() { | |
return { | |
require: ['uibRating', 'ngModel'], | |
scope: { | |
readonly: '=?', | |
onHover: '&', | |
onLeave: '&' | |
}, | |
controller: 'UibRatingController', | |
templateUrl: 'uib/template/rating/rating.html', | |
replace: true, | |
link: function(scope, element, attrs, ctrls) { | |
var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1]; | |
ratingCtrl.init(ngModelCtrl); | |
} | |
}; | |
}); | |
/** | |
* @ngdoc overview | |
* @name ui.bootstrap.tabs | |
* | |
* @description | |
* AngularJS version of the tabs directive. | |
*/ | |
angular.module('ui.bootstrap.tabs', []) | |
.controller('UibTabsetController', ['$scope', function ($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 | |
<example module="ui.bootstrap"> | |
<file name="index.html"> | |
<uib-tabset> | |
<uib-tab heading="Tab 1"><b>First</b> Content!</uib-tab> | |
<uib-tab heading="Tab 2"><i>Second</i> Content!</uib-tab> | |
</uib-tabset> | |
<hr /> | |
<uib-tabset vertical="true"> | |
<uib-tab heading="Vertical Tab 1"><b>First</b> Vertical Content!</uib-tab> | |
<uib-tab heading="Vertical Tab 2"><i>Second</i> Vertical Content!</uib-tab> | |
</uib-tabset> | |
<uib-tabset justified="true"> | |
<uib-tab heading="Justified Tab 1"><b>First</b> Justified Content!</uib-tab> | |
<uib-tab heading="Justified Tab 2"><i>Second</i> Justified Content!</uib-tab> | |
</uib-tabset> | |
</file> | |
</example> | |
*/ | |
.directive('uibTabset', function() { | |
return { | |
transclude: true, | |
replace: true, | |
scope: { | |
type: '@' | |
}, | |
controller: 'UibTabsetController', | |
templateUrl: 'uib/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 | |
<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 /> | |
<uib-tabset> | |
<uib-tab heading="Tab 1">First Tab</uib-tab> | |
<uib-tab select="alertMe()"> | |
<uib-tab-heading><i class="icon-bell"></i> Alert me!</tab-heading> | |
Second Tab, with alert callback and html heading! | |
</uib-tab> | |
<uib-tab ng-repeat="item in items" | |
heading="{{item.title}}" | |
disabled="item.disabled" | |
active="item.active"> | |
{{item.content}} | |
</uib-tab> | |
</uib-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"> | |
<uib-tabset> | |
<uib-tab> | |
<uib-tab-heading><b>HTML</b> in my titles?!</tab-heading> | |
And some content, too! | |
</uib-tab> | |
<uib-tab> | |
<uib-tab-heading><i class="icon-heart"></i> Icon heading?!?</tab-heading> | |
That's right. | |
</uib-tab> | |
</uib-tabset> | |
</file> | |
</example> | |
*/ | |
.directive('uibTab', ['$parse', function($parse) { | |
return { | |
require: '^uibTabset', | |
replace: true, | |
templateUrl: 'uib/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; | |
}); | |
} | |
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('uibTabHeadingTransclude', function() { | |
return { | |
restrict: 'A', | |
require: '^uibTab', | |
link: function(scope, elm) { | |
scope.$watch('headingElement', function updateHeadingElement(heading) { | |
if (heading) { | |
elm.html(''); | |
elm.append(heading); | |
} | |
}); | |
} | |
}; | |
}) | |
.directive('uibTabContentTransclude', function() { | |
return { | |
restrict: 'A', | |
require: '^uibTabset', | |
link: function(scope, elm, attrs) { | |
var tab = scope.$eval(attrs.uibTabContentTransclude); | |
//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('uib-tab-heading') || | |
node.hasAttribute('data-uib-tab-heading') || | |
node.hasAttribute('x-uib-tab-heading') || | |
node.tagName.toLowerCase() === 'uib-tab-heading' || | |
node.tagName.toLowerCase() === 'data-uib-tab-heading' || | |
node.tagName.toLowerCase() === 'x-uib-tab-heading' | |
); | |
} | |
}); | |
angular.module('ui.bootstrap.timepicker', []) | |
.constant('uibTimepickerConfig', { | |
hourStep: 1, | |
minuteStep: 1, | |
secondStep: 1, | |
showMeridian: true, | |
showSeconds: false, | |
meridians: null, | |
readonlyInput: false, | |
mousewheel: true, | |
arrowkeys: true, | |
showSpinners: true | |
}) | |
.controller('UibTimepickerController', ['$scope', '$element', '$attrs', '$parse', '$log', '$locale', 'uibTimepickerConfig', function($scope, $element, $attrs, $parse, $log, $locale, timepickerConfig) { | |
var selected = new Date(), | |
ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl | |
meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS; | |
$scope.tabindex = angular.isDefined($attrs.tabindex) ? $attrs.tabindex : 0; | |
$element.removeAttr('tabindex'); | |
this.init = function(ngModelCtrl_, inputs) { | |
ngModelCtrl = ngModelCtrl_; | |
ngModelCtrl.$render = this.render; | |
ngModelCtrl.$formatters.unshift(function(modelValue) { | |
return modelValue ? new Date(modelValue) : null; | |
}); | |
var hoursInputEl = inputs.eq(0), | |
minutesInputEl = inputs.eq(1), | |
secondsInputEl = inputs.eq(2); | |
var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel; | |
if (mousewheel) { | |
this.setupMousewheelEvents(hoursInputEl, minutesInputEl, secondsInputEl); | |
} | |
var arrowkeys = angular.isDefined($attrs.arrowkeys) ? $scope.$parent.$eval($attrs.arrowkeys) : timepickerConfig.arrowkeys; | |
if (arrowkeys) { | |
this.setupArrowkeyEvents(hoursInputEl, minutesInputEl, secondsInputEl); | |
} | |
$scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput; | |
this.setupInputEvents(hoursInputEl, minutesInputEl, secondsInputEl); | |
}; | |
var hourStep = timepickerConfig.hourStep; | |
if ($attrs.hourStep) { | |
$scope.$parent.$watch($parse($attrs.hourStep), function(value) { | |
hourStep = parseInt(value, 10); | |
}); | |
} | |
var minuteStep = timepickerConfig.minuteStep; | |
if ($attrs.minuteStep) { | |
$scope.$parent.$watch($parse($attrs.minuteStep), function(value) { | |
minuteStep = parseInt(value, 10); | |
}); | |
} | |
var min; | |
$scope.$parent.$watch($parse($attrs.min), function(value) { | |
var dt = new Date(value); | |
min = isNaN(dt) ? undefined : dt; | |
}); | |
var max; | |
$scope.$parent.$watch($parse($attrs.max), function(value) { | |
var dt = new Date(value); | |
max = isNaN(dt) ? undefined : dt; | |
}); | |
var disabled = false; | |
if ($attrs.ngDisabled) { | |
$scope.$parent.$watch($parse($attrs.ngDisabled), function(value) { | |
disabled = value; | |
}); | |
} | |
$scope.noIncrementHours = function() { | |
var incrementedSelected = addMinutes(selected, hourStep * 60); | |
return disabled || incrementedSelected > max || | |
incrementedSelected < selected && incrementedSelected < min; | |
}; | |
$scope.noDecrementHours = function() { | |
var decrementedSelected = addMinutes(selected, -hourStep * 60); | |
return disabled || decrementedSelected < min || | |
decrementedSelected > selected && decrementedSelected > max; | |
}; | |
$scope.noIncrementMinutes = function() { | |
var incrementedSelected = addMinutes(selected, minuteStep); | |
return disabled || incrementedSelected > max || | |
incrementedSelected < selected && incrementedSelected < min; | |
}; | |
$scope.noDecrementMinutes = function() { | |
var decrementedSelected = addMinutes(selected, -minuteStep); | |
return disabled || decrementedSelected < min || | |
decrementedSelected > selected && decrementedSelected > max; | |
}; | |
$scope.noIncrementSeconds = function() { | |
var incrementedSelected = addSeconds(selected, secondStep); | |
return disabled || incrementedSelected > max || | |
incrementedSelected < selected && incrementedSelected < min; | |
}; | |
$scope.noDecrementSeconds = function() { | |
var decrementedSelected = addSeconds(selected, -secondStep); | |
return disabled || decrementedSelected < min || | |
decrementedSelected > selected && decrementedSelected > max; | |
}; | |
$scope.noToggleMeridian = function() { | |
if (selected.getHours() < 12) { | |
return disabled || addMinutes(selected, 12 * 60) > max; | |
} | |
return disabled || addMinutes(selected, -12 * 60) < min; | |
}; | |
var secondStep = timepickerConfig.secondStep; | |
if ($attrs.secondStep) { | |
$scope.$parent.$watch($parse($attrs.secondStep), function(value) { | |
secondStep = parseInt(value, 10); | |
}); | |
} | |
$scope.showSeconds = timepickerConfig.showSeconds; | |
if ($attrs.showSeconds) { | |
$scope.$parent.$watch($parse($attrs.showSeconds), function(value) { | |
$scope.showSeconds = !!value; | |
}); | |
} | |
// 12H / 24H mode | |
$scope.showMeridian = timepickerConfig.showMeridian; | |
if ($attrs.showMeridian) { | |
$scope.$parent.$watch($parse($attrs.showMeridian), function(value) { | |
$scope.showMeridian = !!value; | |
if (ngModelCtrl.$error.time) { | |
// Evaluate from template | |
var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate(); | |
if (angular.isDefined(hours) && angular.isDefined(minutes)) { | |
selected.setHours(hours); | |
refresh(); | |
} | |
} else { | |
updateTemplate(); | |
} | |
}); | |
} | |
// Get $scope.hours in 24H mode if valid | |
function getHoursFromTemplate() { | |
var hours = parseInt($scope.hours, 10); | |
var valid = $scope.showMeridian ? hours > 0 && hours < 13 : | |
hours >= 0 && hours < 24; | |
if (!valid) { | |
return undefined; | |
} | |
if ($scope.showMeridian) { | |
if (hours === 12) { | |
hours = 0; | |
} | |
if ($scope.meridian === meridians[1]) { | |
hours = hours + 12; | |
} | |
} | |
return hours; | |
} | |
function getMinutesFromTemplate() { | |
var minutes = parseInt($scope.minutes, 10); | |
return minutes >= 0 && minutes < 60 ? minutes : undefined; | |
} | |
function getSecondsFromTemplate() { | |
var seconds = parseInt($scope.seconds, 10); | |
return seconds >= 0 && seconds < 60 ? seconds : undefined; | |
} | |
function pad(value) { | |
if (value === null) { | |
return ''; | |
} | |
return angular.isDefined(value) && value.toString().length < 2 ? | |
'0' + value : value.toString(); | |
} | |
// Respond on mousewheel spin | |
this.setupMousewheelEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) { | |
var isScrollingUp = function(e) { | |
if (e.originalEvent) { | |
e = e.originalEvent; | |
} | |
//pick correct delta variable depending on event | |
var delta = e.wheelDelta ? e.wheelDelta : -e.deltaY; | |
return e.detail || delta > 0; | |
}; | |
hoursInputEl.bind('mousewheel wheel', function(e) { | |
if (!disabled) { | |
$scope.$apply(isScrollingUp(e) ? $scope.incrementHours() : $scope.decrementHours()); | |
} | |
e.preventDefault(); | |
}); | |
minutesInputEl.bind('mousewheel wheel', function(e) { | |
if (!disabled) { | |
$scope.$apply(isScrollingUp(e) ? $scope.incrementMinutes() : $scope.decrementMinutes()); | |
} | |
e.preventDefault(); | |
}); | |
secondsInputEl.bind('mousewheel wheel', function(e) { | |
if (!disabled) { | |
$scope.$apply(isScrollingUp(e) ? $scope.incrementSeconds() : $scope.decrementSeconds()); | |
} | |
e.preventDefault(); | |
}); | |
}; | |
// Respond on up/down arrowkeys | |
this.setupArrowkeyEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) { | |
hoursInputEl.bind('keydown', function(e) { | |
if (!disabled) { | |
if (e.which === 38) { // up | |
e.preventDefault(); | |
$scope.incrementHours(); | |
$scope.$apply(); | |
} else if (e.which === 40) { // down | |
e.preventDefault(); | |
$scope.decrementHours(); | |
$scope.$apply(); | |
} | |
} | |
}); | |
minutesInputEl.bind('keydown', function(e) { | |
if (!disabled) { | |
if (e.which === 38) { // up | |
e.preventDefault(); | |
$scope.incrementMinutes(); | |
$scope.$apply(); | |
} else if (e.which === 40) { // down | |
e.preventDefault(); | |
$scope.decrementMinutes(); | |
$scope.$apply(); | |
} | |
} | |
}); | |
secondsInputEl.bind('keydown', function(e) { | |
if (!disabled) { | |
if (e.which === 38) { // up | |
e.preventDefault(); | |
$scope.incrementSeconds(); | |
$scope.$apply(); | |
} else if (e.which === 40) { // down | |
e.preventDefault(); | |
$scope.decrementSeconds(); | |
$scope.$apply(); | |
} | |
} | |
}); | |
}; | |
this.setupInputEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) { | |
if ($scope.readonlyInput) { | |
$scope.updateHours = angular.noop; | |
$scope.updateMinutes = angular.noop; | |
$scope.updateSeconds = angular.noop; | |
return; | |
} | |
var invalidate = function(invalidHours, invalidMinutes, invalidSeconds) { | |
ngModelCtrl.$setViewValue(null); | |
ngModelCtrl.$setValidity('time', false); | |
if (angular.isDefined(invalidHours)) { | |
$scope.invalidHours = invalidHours; | |
} | |
if (angular.isDefined(invalidMinutes)) { | |
$scope.invalidMinutes = invalidMinutes; | |
} | |
if (angular.isDefined(invalidSeconds)) { | |
$scope.invalidSeconds = invalidSeconds; | |
} | |
}; | |
$scope.updateHours = function() { | |
var hours = getHoursFromTemplate(), | |
minutes = getMinutesFromTemplate(); | |
ngModelCtrl.$setDirty(); | |
if (angular.isDefined(hours) && angular.isDefined(minutes)) { | |
selected.setHours(hours); | |
selected.setMinutes(minutes); | |
if (selected < min || selected > max) { | |
invalidate(true); | |
} else { | |
refresh('h'); | |
} | |
} else { | |
invalidate(true); | |
} | |
}; | |
hoursInputEl.bind('blur', function(e) { | |
ngModelCtrl.$setTouched(); | |
if ($scope.hours === null || $scope.hours === '') { | |
invalidate(true); | |
} else if (!$scope.invalidHours && $scope.hours < 10) { | |
$scope.$apply(function() { | |
$scope.hours = pad($scope.hours); | |
}); | |
} | |
}); | |
$scope.updateMinutes = function() { | |
var minutes = getMinutesFromTemplate(), | |
hours = getHoursFromTemplate(); | |
ngModelCtrl.$setDirty(); | |
if (angular.isDefined(minutes) && angular.isDefined(hours)) { | |
selected.setHours(hours); | |
selected.setMinutes(minutes); | |
if (selected < min || selected > max) { | |
invalidate(undefined, true); | |
} else { | |
refresh('m'); | |
} | |
} else { | |
invalidate(undefined, true); | |
} | |
}; | |
minutesInputEl.bind('blur', function(e) { | |
ngModelCtrl.$setTouched(); | |
if ($scope.minutes === null) { | |
invalidate(undefined, true); | |
} else if (!$scope.invalidMinutes && $scope.minutes < 10) { | |
$scope.$apply(function() { | |
$scope.minutes = pad($scope.minutes); | |
}); | |
} | |
}); | |
$scope.updateSeconds = function() { | |
var seconds = getSecondsFromTemplate(); | |
ngModelCtrl.$setDirty(); | |
if (angular.isDefined(seconds)) { | |
selected.setSeconds(seconds); | |
refresh('s'); | |
} else { | |
invalidate(undefined, undefined, true); | |
} | |
}; | |
secondsInputEl.bind('blur', function(e) { | |
if (!$scope.invalidSeconds && $scope.seconds < 10) { | |
$scope.$apply( function() { | |
$scope.seconds = pad($scope.seconds); | |
}); | |
} | |
}); | |
}; | |
this.render = function() { | |
var date = ngModelCtrl.$viewValue; | |
if (isNaN(date)) { | |
ngModelCtrl.$setValidity('time', false); | |
$log.error('Timepicker 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.'); | |
} else { | |
if (date) { | |
selected = date; | |
} | |
if (selected < min || selected > max) { | |
ngModelCtrl.$setValidity('time', false); | |
$scope.invalidHours = true; | |
$scope.invalidMinutes = true; | |
} else { | |
makeValid(); | |
} | |
updateTemplate(); | |
} | |
}; | |
// Call internally when we know that model is valid. | |
function refresh(keyboardChange) { | |
makeValid(); | |
ngModelCtrl.$setViewValue(new Date(selected)); | |
updateTemplate(keyboardChange); | |
} | |
function makeValid() { | |
ngModelCtrl.$setValidity('time', true); | |
$scope.invalidHours = false; | |
$scope.invalidMinutes = false; | |
$scope.invalidSeconds = false; | |
} | |
function updateTemplate(keyboardChange) { | |
if (!ngModelCtrl.$modelValue) { | |
$scope.hours = null; | |
$scope.minutes = null; | |
$scope.seconds = null; | |
$scope.meridian = meridians[0]; | |
} else { | |
var hours = selected.getHours(), | |
minutes = selected.getMinutes(), | |
seconds = selected.getSeconds(); | |
if ($scope.showMeridian) { | |
hours = hours === 0 || hours === 12 ? 12 : hours % 12; // Convert 24 to 12 hour system | |
} | |
$scope.hours = keyboardChange === 'h' ? hours : pad(hours); | |
if (keyboardChange !== 'm') { | |
$scope.minutes = pad(minutes); | |
} | |
$scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; | |
if (keyboardChange !== 's') { | |
$scope.seconds = pad(seconds); | |
} | |
$scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; | |
} | |
} | |
function addSecondsToSelected(seconds) { | |
selected = addSeconds(selected, seconds); | |
refresh(); | |
} | |
function addMinutes(selected, minutes) { | |
return addSeconds(selected, minutes*60); | |
} | |
function addSeconds(date, seconds) { | |
var dt = new Date(date.getTime() + seconds * 1000); | |
var newDate = new Date(date); | |
newDate.setHours(dt.getHours(), dt.getMinutes(), dt.getSeconds()); | |
return newDate; | |
} | |
$scope.showSpinners = angular.isDefined($attrs.showSpinners) ? | |
$scope.$parent.$eval($attrs.showSpinners) : timepickerConfig.showSpinners; | |
$scope.incrementHours = function() { | |
if (!$scope.noIncrementHours()) { | |
addSecondsToSelected(hourStep * 60 * 60); | |
} | |
}; | |
$scope.decrementHours = function() { | |
if (!$scope.noDecrementHours()) { | |
addSecondsToSelected(-hourStep * 60 * 60); | |
} | |
}; | |
$scope.incrementMinutes = function() { | |
if (!$scope.noIncrementMinutes()) { | |
addSecondsToSelected(minuteStep * 60); | |
} | |
}; | |
$scope.decrementMinutes = function() { | |
if (!$scope.noDecrementMinutes()) { | |
addSecondsToSelected(-minuteStep * 60); | |
} | |
}; | |
$scope.incrementSeconds = function() { | |
if (!$scope.noIncrementSeconds()) { | |
addSecondsToSelected(secondStep); | |
} | |
}; | |
$scope.decrementSeconds = function() { | |
if (!$scope.noDecrementSeconds()) { | |
addSecondsToSelected(-secondStep); | |
} | |
}; | |
$scope.toggleMeridian = function() { | |
var minutes = getMinutesFromTemplate(), | |
hours = getHoursFromTemplate(); | |
if (!$scope.noToggleMeridian()) { | |
if (angular.isDefined(minutes) && angular.isDefined(hours)) { | |
addSecondsToSelected(12 * 60 * (selected.getHours() < 12 ? 60 : -60)); | |
} else { | |
$scope.meridian = $scope.meridian === meridians[0] ? meridians[1] : meridians[0]; | |
} | |
} | |
}; | |
$scope.blur = function() { | |
ngModelCtrl.$setTouched(); | |
}; | |
}]) | |
.directive('uibTimepicker', function() { | |
return { | |
require: ['uibTimepicker', '?^ngModel'], | |
controller: 'UibTimepickerController', | |
controllerAs: 'timepicker', | |
replace: true, | |
scope: {}, | |
templateUrl: function(element, attrs) { | |
return attrs.templateUrl || 'uib/template/timepicker/timepicker.html'; | |
}, | |
link: function(scope, element, attrs, ctrls) { | |
var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; | |
if (ngModelCtrl) { | |
timepickerCtrl.init(ngModelCtrl, element.find('input')); | |
} | |
} | |
}; | |
}); | |
angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap.position']) | |
/** | |
* A helper service that can parse typeahead's syntax (string provided by users) | |
* Extracted to a separate service for ease of unit testing | |
*/ | |
.factory('uibTypeaheadParser', ['$parse', function($parse) { | |
// 00000111000000000000022200000000000000003333333333333330000000000044000 | |
var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/; | |
return { | |
parse: function(input) { | |
var match = input.match(TYPEAHEAD_REGEXP); | |
if (!match) { | |
throw new Error( | |
'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' + | |
' but got "' + input + '".'); | |
} | |
return { | |
itemName: match[3], | |
source: $parse(match[4]), | |
viewMapper: $parse(match[2] || match[1]), | |
modelMapper: $parse(match[1]) | |
}; | |
} | |
}; | |
}]) | |
.controller('UibTypeaheadController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$$debounce', '$uibPosition', 'uibTypeaheadParser', | |
function(originalScope, element, attrs, $compile, $parse, $q, $timeout, $document, $window, $rootScope, $$debounce, $position, typeaheadParser) { | |
var HOT_KEYS = [9, 13, 27, 38, 40]; | |
var eventDebounceTime = 200; | |
var modelCtrl, ngModelOptions; | |
//SUPPORTED ATTRIBUTES (OPTIONS) | |
//minimal no of characters that needs to be entered before typeahead kicks-in | |
var minLength = originalScope.$eval(attrs.typeaheadMinLength); | |
if (!minLength && minLength !== 0) { | |
minLength = 1; | |
} | |
//minimal wait time after last character typed before typeahead kicks-in | |
var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; | |
//should it restrict model values to the ones selected from the popup only? | |
var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; | |
originalScope.$watch(attrs.typeaheadEditable, function (newVal) { | |
isEditable = newVal !== false; | |
}); | |
//binding to a variable that indicates if matches are being retrieved asynchronously | |
var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; | |
//a callback executed when a match is selected | |
var onSelectCallback = $parse(attrs.typeaheadOnSelect); | |
//should it select highlighted popup value when losing focus? | |
var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false; | |
//binding to a variable that indicates if there were no results after the query is completed | |
var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop; | |
var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; | |
var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; | |
var appendTo = attrs.typeaheadAppendTo ? | |
originalScope.$eval(attrs.typeaheadAppendTo) : null; | |
var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false; | |
//If input matches an item of the list exactly, select it automatically | |
var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false; | |
//binding to a variable that indicates if dropdown is open | |
var isOpenSetter = $parse(attrs.typeaheadIsOpen).assign || angular.noop; | |
var showHint = originalScope.$eval(attrs.typeaheadShowHint) || false; | |
//INTERNAL VARIABLES | |
//model setter executed upon match selection | |
var parsedModel = $parse(attrs.ngModel); | |
var invokeModelSetter = $parse(attrs.ngModel + '($$$p)'); | |
var $setModelValue = function(scope, newValue) { | |
if (angular.isFunction(parsedModel(originalScope)) && | |
ngModelOptions && ngModelOptions.$options && ngModelOptions.$options.getterSetter) { | |
return invokeModelSetter(scope, {$$$p: newValue}); | |
} | |
return parsedModel.assign(scope, newValue); | |
}; | |
//expressions used by typeahead | |
var parserResult = typeaheadParser.parse(attrs.uibTypeahead); | |
var hasFocus; | |
//Used to avoid bug in iOS webview where iOS keyboard does not fire | |
//mousedown & mouseup events | |
//Issue #3699 | |
var selected; | |
//create a child scope for the typeahead directive so we are not polluting original scope | |
//with typeahead-specific data (matches, query etc.) | |
var scope = originalScope.$new(); | |
var offDestroy = originalScope.$on('$destroy', function() { | |
scope.$destroy(); | |
}); | |
scope.$on('$destroy', offDestroy); | |
// WAI-ARIA | |
var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); | |
element.attr({ | |
'aria-autocomplete': 'list', | |
'aria-expanded': false, | |
'aria-owns': popupId | |
}); | |
var inputsContainer, hintInputElem; | |
//add read-only input to show hint | |
if (showHint) { | |
inputsContainer = angular.element('<div></div>'); | |
inputsContainer.css('position', 'relative'); | |
element.after(inputsContainer); | |
hintInputElem = element.clone(); | |
hintInputElem.attr('placeholder', ''); | |
hintInputElem.val(''); | |
hintInputElem.css({ | |
'position': 'absolute', | |
'top': '0px', | |
'left': '0px', | |
'border-color': 'transparent', | |
'box-shadow': 'none', | |
'opacity': 1, | |
'background': 'none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)', | |
'color': '#999' | |
}); | |
element.css({ | |
'position': 'relative', | |
'vertical-align': 'top', | |
'background-color': 'transparent' | |
}); | |
inputsContainer.append(hintInputElem); | |
hintInputElem.after(element); | |
} | |
//pop-up element used to display matches | |
var popUpEl = angular.element('<div uib-typeahead-popup></div>'); | |
popUpEl.attr({ | |
id: popupId, | |
matches: 'matches', | |
active: 'activeIdx', | |
select: 'select(activeIdx)', | |
'move-in-progress': 'moveInProgress', | |
query: 'query', | |
position: 'position', | |
'assign-is-open': 'assignIsOpen(isOpen)' | |
}); | |
//custom item template | |
if (angular.isDefined(attrs.typeaheadTemplateUrl)) { | |
popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); | |
} | |
if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) { | |
popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl); | |
} | |
var resetHint = function() { | |
if (showHint) { | |
hintInputElem.val(''); | |
} | |
}; | |
var resetMatches = function() { | |
scope.matches = []; | |
scope.activeIdx = -1; | |
element.attr('aria-expanded', false); | |
resetHint(); | |
}; | |
var getMatchId = function(index) { | |
return popupId + '-option-' + index; | |
}; | |
// Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead. | |
// This attribute is added or removed automatically when the `activeIdx` changes. | |
scope.$watch('activeIdx', function(index) { | |
if (index < 0) { | |
element.removeAttr('aria-activedescendant'); | |
} else { | |
element.attr('aria-activedescendant', getMatchId(index)); | |
} | |
}); | |
var inputIsExactMatch = function(inputValue, index) { | |
if (scope.matches.length > index && inputValue) { | |
return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase(); | |
} | |
return false; | |
}; | |
var getMatchesAsync = function(inputValue) { | |
var locals = {$viewValue: inputValue}; | |
isLoadingSetter(originalScope, true); | |
isNoResultsSetter(originalScope, false); | |
$q.when(parserResult.source(originalScope, locals)).then(function(matches) { | |
//it might happen that several async queries were in progress if a user were typing fast | |
//but we are interested only in responses that correspond to the current view value | |
var onCurrentRequest = inputValue === modelCtrl.$viewValue; | |
if (onCurrentRequest && hasFocus) { | |
if (matches && matches.length > 0) { | |
scope.activeIdx = focusFirst ? 0 : -1; | |
isNoResultsSetter(originalScope, false); | |
scope.matches.length = 0; | |
//transform labels | |
for (var i = 0; i < matches.length; i++) { | |
locals[parserResult.itemName] = matches[i]; | |
scope.matches.push({ | |
id: getMatchId(i), | |
label: parserResult.viewMapper(scope, locals), | |
model: matches[i] | |
}); | |
} | |
scope.query = inputValue; | |
//position pop-up with matches - we need to re-calculate its position each time we are opening a window | |
//with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page | |
//due to other elements being rendered | |
recalculatePosition(); | |
element.attr('aria-expanded', true); | |
//Select the single remaining option if user input matches | |
if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) { | |
scope.select(0); | |
} | |
if (showHint) { | |
var firstLabel = scope.matches[0].label; | |
if (inputValue.length > 0 && firstLabel.slice(0, inputValue.length).toUpperCase() === inputValue.toUpperCase()) { | |
hintInputElem.val(inputValue + firstLabel.slice(inputValue.length)); | |
} | |
else { | |
hintInputElem.val(''); | |
} | |
} | |
} else { | |
resetMatches(); | |
isNoResultsSetter(originalScope, true); | |
} | |
} | |
if (onCurrentRequest) { | |
isLoadingSetter(originalScope, false); | |
} | |
}, function() { | |
resetMatches(); | |
isLoadingSetter(originalScope, false); | |
isNoResultsSetter(originalScope, true); | |
}); | |
}; | |
// bind events only if appendToBody params exist - performance feature | |
if (appendToBody) { | |
angular.element($window).bind('resize', fireRecalculating); | |
$document.find('body').bind('scroll', fireRecalculating); | |
} | |
// Declare the debounced function outside recalculating for | |
// proper debouncing | |
var debouncedRecalculate = $$debounce(function() { | |
// if popup is visible | |
if (scope.matches.length) { | |
recalculatePosition(); | |
} | |
scope.moveInProgress = false; | |
}, eventDebounceTime); | |
// Default progress type | |
scope.moveInProgress = false; | |
function fireRecalculating() { | |
if (!scope.moveInProgress) { | |
scope.moveInProgress = true; | |
scope.$digest(); | |
} | |
debouncedRecalculate(); | |
} | |
// recalculate actual position and set new values to scope | |
// after digest loop is popup in right position | |
function recalculatePosition() { | |
scope.position = appendToBody ? $position.offset(element) : $position.position(element); | |
scope.position.top += element.prop('offsetHeight'); | |
} | |
//we need to propagate user's query so we can higlight matches | |
scope.query = undefined; | |
//Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later | |
var timeoutPromise; | |
var scheduleSearchWithTimeout = function(inputValue) { | |
timeoutPromise = $timeout(function() { | |
getMatchesAsync(inputValue); | |
}, waitTime); | |
}; | |
var cancelPreviousTimeout = function() { | |
if (timeoutPromise) { | |
$timeout.cancel(timeoutPromise); | |
} | |
}; | |
resetMatches(); | |
scope.assignIsOpen = function (isOpen) { | |
isOpenSetter(originalScope, isOpen); | |
}; | |
scope.select = function(activeIdx) { | |
//called from within the $digest() cycle | |
var locals = {}; | |
var model, item; | |
selected = true; | |
locals[parserResult.itemName] = item = scope.matches[activeIdx].model; | |
model = parserResult.modelMapper(originalScope, locals); | |
$setModelValue(originalScope, model); | |
modelCtrl.$setValidity('editable', true); | |
modelCtrl.$setValidity('parse', true); | |
onSelectCallback(originalScope, { | |
$item: item, | |
$model: model, | |
$label: parserResult.viewMapper(originalScope, locals) | |
}); | |
resetMatches(); | |
//return focus to the input element if a match was selected via a mouse click event | |
// use timeout to avoid $rootScope:inprog error | |
if (scope.$eval(attrs.typeaheadFocusOnSelect) !== false) { | |
$timeout(function() { element[0].focus(); }, 0, false); | |
} | |
}; | |
//bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) | |
element.bind('keydown', function(evt) { | |
//typeahead is open and an "interesting" key was pressed | |
if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { | |
return; | |
} | |
// if there's nothing selected (i.e. focusFirst) and enter or tab is hit, clear the results | |
if (scope.activeIdx === -1 && (evt.which === 9 || evt.which === 13)) { | |
resetMatches(); | |
scope.$digest(); | |
return; | |
} | |
evt.preventDefault(); | |
if (evt.which === 40) { | |
scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; | |
scope.$digest(); | |
popUpEl.children()[scope.activeIdx].scrollIntoView(false); | |
} else if (evt.which === 38) { | |
scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1; | |
scope.$digest(); | |
popUpEl.children()[scope.activeIdx].scrollIntoView(false); | |
} else if (evt.which === 13 || evt.which === 9) { | |
scope.$apply(function () { | |
scope.select(scope.activeIdx); | |
}); | |
} else if (evt.which === 27) { | |
evt.stopPropagation(); | |
resetMatches(); | |
scope.$digest(); | |
} | |
}); | |
element.bind('focus', function () { | |
hasFocus = true; | |
if (minLength === 0 && !modelCtrl.$viewValue) { | |
getMatchesAsync(modelCtrl.$viewValue); | |
} | |
}); | |
element.bind('blur', function() { | |
if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) { | |
selected = true; | |
scope.$apply(function() { | |
scope.select(scope.activeIdx); | |
}); | |
} | |
if (!isEditable && modelCtrl.$error.editable) { | |
modelCtrl.$viewValue = ''; | |
element.val(''); | |
} | |
hasFocus = false; | |
selected = false; | |
}); | |
// Keep reference to click handler to unbind it. | |
var dismissClickHandler = function(evt) { | |
// Issue #3973 | |
// Firefox treats right click as a click on document | |
if (element[0] !== evt.target && evt.which !== 3 && scope.matches.length !== 0) { | |
resetMatches(); | |
if (!$rootScope.$$phase) { | |
scope.$digest(); | |
} | |
} | |
}; | |
$document.bind('click', dismissClickHandler); | |
originalScope.$on('$destroy', function() { | |
$document.unbind('click', dismissClickHandler); | |
if (appendToBody || appendTo) { | |
$popup.remove(); | |
} | |
if (appendToBody) { | |
angular.element($window).unbind('resize', fireRecalculating); | |
$document.find('body').unbind('scroll', fireRecalculating); | |
} | |
// Prevent jQuery cache memory leak | |
popUpEl.remove(); | |
if (showHint) { | |
inputsContainer.remove(); | |
} | |
}); | |
var $popup = $compile(popUpEl)(scope); | |
if (appendToBody) { | |
$document.find('body').append($popup); | |
} else if (appendTo) { | |
angular.element(appendTo).eq(0).append($popup); | |
} else { | |
element.after($popup); | |
} | |
this.init = function(_modelCtrl, _ngModelOptions) { | |
modelCtrl = _modelCtrl; | |
ngModelOptions = _ngModelOptions; | |
//plug into $parsers pipeline to open a typeahead on view changes initiated from DOM | |
//$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue | |
modelCtrl.$parsers.unshift(function(inputValue) { | |
hasFocus = true; | |
if (minLength === 0 || inputValue && inputValue.length >= minLength) { | |
if (waitTime > 0) { | |
cancelPreviousTimeout(); | |
scheduleSearchWithTimeout(inputValue); | |
} else { | |
getMatchesAsync(inputValue); | |
} | |
} else { | |
isLoadingSetter(originalScope, false); | |
cancelPreviousTimeout(); | |
resetMatches(); | |
} | |
if (isEditable) { | |
return inputValue; | |
} | |
if (!inputValue) { | |
// Reset in case user had typed something previously. | |
modelCtrl.$setValidity('editable', true); | |
return null; | |
} | |
modelCtrl.$setValidity('editable', false); | |
return undefined; | |
}); | |
modelCtrl.$formatters.push(function(modelValue) { | |
var candidateViewValue, emptyViewValue; | |
var locals = {}; | |
// The validity may be set to false via $parsers (see above) if | |
// the model is restricted to selected values. If the model | |
// is set manually it is considered to be valid. | |
if (!isEditable) { | |
modelCtrl.$setValidity('editable', true); | |
} | |
if (inputFormatter) { | |
locals.$model = modelValue; | |
return inputFormatter(originalScope, locals); | |
} | |
//it might happen that we don't have enough info to properly render input value | |
//we need to check for this situation and simply return model value if we can't apply custom formatting | |
locals[parserResult.itemName] = modelValue; | |
candidateViewValue = parserResult.viewMapper(originalScope, locals); | |
locals[parserResult.itemName] = undefined; | |
emptyViewValue = parserResult.viewMapper(originalScope, locals); | |
return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue; | |
}); | |
}; | |
}]) | |
.directive('uibTypeahead', function() { | |
return { | |
controller: 'UibTypeaheadController', | |
require: ['ngModel', '^?ngModelOptions', 'uibTypeahead'], | |
link: function(originalScope, element, attrs, ctrls) { | |
ctrls[2].init(ctrls[0], ctrls[1]); | |
} | |
}; | |
}) | |
.directive('uibTypeaheadPopup', function() { | |
return { | |
scope: { | |
matches: '=', | |
query: '=', | |
active: '=', | |
position: '&', | |
moveInProgress: '=', | |
select: '&', | |
assignIsOpen: '&' | |
}, | |
replace: true, | |
templateUrl: function(element, attrs) { | |
return attrs.popupTemplateUrl || 'uib/template/typeahead/typeahead-popup.html'; | |
}, | |
link: function(scope, element, attrs) { | |
scope.templateUrl = attrs.templateUrl; | |
scope.isOpen = function() { | |
var isDropdownOpen = scope.matches.length > 0; | |
scope.assignIsOpen({ isOpen: isDropdownOpen }); | |
return isDropdownOpen; | |
}; | |
scope.isActive = function(matchIdx) { | |
return scope.active === matchIdx; | |
}; | |
scope.selectActive = function(matchIdx) { | |
scope.active = matchIdx; | |
}; | |
scope.selectMatch = function(activeIdx) { | |
scope.select({activeIdx: activeIdx}); | |
}; | |
} | |
}; | |
}) | |
.directive('uibTypeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) { | |
return { | |
scope: { | |
index: '=', | |
match: '=', | |
query: '=' | |
}, | |
link: function(scope, element, attrs) { | |
var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'uib/template/typeahead/typeahead-match.html'; | |
$templateRequest(tplUrl).then(function(tplContent) { | |
var tplEl = angular.element(tplContent.trim()); | |
element.replaceWith(tplEl); | |
$compile(tplEl)(scope); | |
}); | |
} | |
}; | |
}]) | |
.filter('uibTypeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) { | |
var isSanitizePresent; | |
isSanitizePresent = $injector.has('$sanitize'); | |
function escapeRegexp(queryToEscape) { | |
// Regex: capture the whole query string and replace it with the string that will be used to match | |
// the results, for example if the capture is "a" the result will be \a | |
return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); | |
} | |
function containsHtml(matchItem) { | |
return /<.*>/g.test(matchItem); | |
} | |
return function(matchItem, query) { | |
if (!isSanitizePresent && containsHtml(matchItem)) { | |
$log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger | |
} | |
matchItem = query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag | |
if (!isSanitizePresent) { | |
matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive | |
} | |
return matchItem; | |
}; | |
}]); | |
angular.module("uib/template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/accordion/accordion-group.html", | |
"<div class=\"panel\" ng-class=\"panelClass || 'panel-default'\">\n" + | |
" <div class=\"panel-heading\" ng-keypress=\"toggleOpen($event)\">\n" + | |
" <h4 class=\"panel-title\">\n" + | |
" <div tabindex=\"0\" class=\"accordion-toggle\" ng-click=\"toggleOpen()\" uib-accordion-transclude=\"heading\"><span ng-class=\"{'text-muted': isDisabled}\">{{heading}}</span></div>\n" + | |
" </h4>\n" + | |
" </div>\n" + | |
" <div class=\"panel-collapse collapse\" uib-collapse=\"!isOpen\">\n" + | |
" <div class=\"panel-body\" ng-transclude></div>\n" + | |
" </div>\n" + | |
"</div>\n" + | |
""); | |
}]); | |
angular.module("uib/template/accordion/accordion.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/accordion/accordion.html", | |
"<div class=\"panel-group\" ng-transclude></div>"); | |
}]); | |
angular.module("uib/template/alert/alert.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/alert/alert.html", | |
"<div class=\"alert\" ng-class=\"['alert-' + (type || 'warning'), closeable ? 'alert-dismissible' : null]\" role=\"alert\">\n" + | |
" <button ng-show=\"closeable\" type=\"button\" class=\"close\" ng-click=\"close({$event: $event})\">\n" + | |
" <span aria-hidden=\"true\">×</span>\n" + | |
" <span class=\"sr-only\">Close</span>\n" + | |
" </button>\n" + | |
" <div ng-transclude></div>\n" + | |
"</div>\n" + | |
""); | |
}]); | |
angular.module("uib/template/carousel/carousel.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/carousel/carousel.html", | |
"<div ng-mouseenter=\"pause()\" ng-mouseleave=\"play()\" class=\"carousel\" ng-swipe-right=\"prev()\" ng-swipe-left=\"next()\">\n" + | |
" <div class=\"carousel-inner\" ng-transclude></div>\n" + | |
" <a role=\"button\" href class=\"left carousel-control\" ng-click=\"prev()\" ng-show=\"slides.length > 1\">\n" + | |
" <span aria-hidden=\"true\" class=\"glyphicon glyphicon-chevron-left\"></span>\n" + | |
" <span class=\"sr-only\">previous</span>\n" + | |
" </a>\n" + | |
" <a role=\"button\" href class=\"right carousel-control\" ng-click=\"next()\" ng-show=\"slides.length > 1\">\n" + | |
" <span aria-hidden=\"true\" class=\"glyphicon glyphicon-chevron-right\"></span>\n" + | |
" <span class=\"sr-only\">next</span>\n" + | |
" </a>\n" + | |
" <ol class=\"carousel-indicators\" ng-show=\"slides.length > 1\">\n" + | |
" <li ng-repeat=\"slide in slides | orderBy:indexOfSlide track by $index\" ng-class=\"{ active: isActive(slide) }\" ng-click=\"select(slide)\">\n" + | |
" <span class=\"sr-only\">slide {{ $index + 1 }} of {{ slides.length }}<span ng-if=\"isActive(slide)\">, currently active</span></span>\n" + | |
" </li>\n" + | |
" </ol>\n" + | |
"</div>"); | |
}]); | |
angular.module("uib/template/carousel/slide.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/carousel/slide.html", | |
"<div ng-class=\"{\n" + | |
" 'active': active\n" + | |
" }\" class=\"item text-center\" ng-transclude></div>\n" + | |
""); | |
}]); | |
angular.module("uib/template/datepicker/datepicker.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/datepicker/datepicker.html", | |
"<div class=\"uib-datepicker\" ng-switch=\"datepickerMode\" role=\"application\" ng-keydown=\"keydown($event)\">\n" + | |
" <uib-daypicker ng-switch-when=\"day\" tabindex=\"0\"></uib-daypicker>\n" + | |
" <uib-monthpicker ng-switch-when=\"month\" tabindex=\"0\"></uib-monthpicker>\n" + | |
" <uib-yearpicker ng-switch-when=\"year\" tabindex=\"0\"></uib-yearpicker>\n" + | |
"</div>"); | |
}]); | |
angular.module("uib/template/datepicker/day.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/datepicker/day.html", | |
"<table class=\"uib-daypicker\" role=\"grid\" aria-labelledby=\"{{::uniqueId}}-title\" aria-activedescendant=\"{{activeDateId}}\">\n" + | |
" <thead>\n" + | |
" <tr>\n" + | |
" <th><button type=\"button\" class=\"btn btn-default btn-sm pull-left uib-left\" ng-click=\"move(-1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-left\"></i></button></th>\n" + | |
" <th colspan=\"{{::5 + showWeeks}}\"><button id=\"{{::uniqueId}}-title\" role=\"heading\" aria-live=\"assertive\" aria-atomic=\"true\" type=\"button\" class=\"btn btn-default btn-sm uib-title\" ng-click=\"toggleMode()\" ng-disabled=\"datepickerMode === maxMode\" tabindex=\"-1\" style=\"width:100%;\"><strong>{{title}}</strong></button></th>\n" + | |
" <th><button type=\"button\" class=\"btn btn-default btn-sm pull-right uib-right\" ng-click=\"move(1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-right\"></i></button></th>\n" + | |
" </tr>\n" + | |
" <tr>\n" + | |
" <th ng-if=\"showWeeks\" class=\"text-center\"></th>\n" + | |
" <th ng-repeat=\"label in ::labels track by $index\" class=\"text-center\"><small aria-label=\"{{::label.full}}\">{{::label.abbr}}</small></th>\n" + | |
" </tr>\n" + | |
" </thead>\n" + | |
" <tbody>\n" + | |
" <tr class=\"uib-weeks\" ng-repeat=\"row in rows track by $index\">\n" + | |
" <td ng-if=\"showWeeks\" class=\"text-center h6\"><em>{{ weekNumbers[$index] }}</em></td>\n" + | |
" <td ng-repeat=\"dt in row track by dt.date\" class=\"uib-day text-center\" role=\"gridcell\" id=\"{{::dt.uid}}\" ng-class=\"::dt.customClass\">\n" + | |
" <button type=\"button\" style=\"min-width:100%;\" class=\"btn btn-default btn-sm\" ng-class=\"{'btn-info': dt.selected, active: isActive(dt)}\" ng-click=\"select(dt.date)\" ng-disabled=\"dt.disabled\" tabindex=\"-1\"><span ng-class=\"::{'text-muted': dt.secondary, 'text-info': dt.current}\">{{::dt.label}}</span></button>\n" + | |
" </td>\n" + | |
" </tr>\n" + | |
" </tbody>\n" + | |
"</table>\n" + | |
""); | |
}]); | |
angular.module("uib/template/datepicker/month.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/datepicker/month.html", | |
"<table class=\"uib-monthpicker\" role=\"grid\" aria-labelledby=\"{{::uniqueId}}-title\" aria-activedescendant=\"{{activeDateId}}\">\n" + | |
" <thead>\n" + | |
" <tr>\n" + | |
" <th><button type=\"button\" class=\"btn btn-default btn-sm pull-left uib-left\" ng-click=\"move(-1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-left\"></i></button></th>\n" + | |
" <th><button id=\"{{::uniqueId}}-title\" role=\"heading\" aria-live=\"assertive\" aria-atomic=\"true\" type=\"button\" class=\"btn btn-default btn-sm uib-title\" ng-click=\"toggleMode()\" ng-disabled=\"datepickerMode === maxMode\" tabindex=\"-1\" style=\"width:100%;\"><strong>{{title}}</strong></button></th>\n" + | |
" <th><button type=\"button\" class=\"btn btn-default btn-sm pull-right uib-right\" ng-click=\"move(1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-right\"></i></button></th>\n" + | |
" </tr>\n" + | |
" </thead>\n" + | |
" <tbody>\n" + | |
" <tr class=\"uib-months\" ng-repeat=\"row in rows track by $index\">\n" + | |
" <td ng-repeat=\"dt in row track by dt.date\" class=\"uib-month text-center\" role=\"gridcell\" id=\"{{::dt.uid}}\" ng-class=\"::dt.customClass\">\n" + | |
" <button type=\"button\" style=\"min-width:100%;\" class=\"btn btn-default\" ng-class=\"{'btn-info': dt.selected, active: isActive(dt)}\" ng-click=\"select(dt.date)\" ng-disabled=\"dt.disabled\" tabindex=\"-1\"><span ng-class=\"::{'text-info': dt.current}\">{{::dt.label}}</span></button>\n" + | |
" </td>\n" + | |
" </tr>\n" + | |
" </tbody>\n" + | |
"</table>\n" + | |
""); | |
}]); | |
angular.module("uib/template/datepicker/popup.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/datepicker/popup.html", | |
"<ul class=\"uib-datepicker-popup dropdown-menu\" dropdown-nested ng-if=\"isOpen\" style=\"display: block\" ng-style=\"{top: position.top+'px', left: position.left+'px'}\" ng-keydown=\"keydown($event)\" ng-click=\"$event.stopPropagation()\">\n" + | |
" <li ng-transclude></li>\n" + | |
" <li ng-if=\"showButtonBar\" style=\"padding:10px 9px 2px\" class=\"uib-button-bar\">\n" + | |
" <span class=\"btn-group pull-left\">\n" + | |
" <button type=\"button\" class=\"btn btn-sm btn-info uib-datepicker-current\" ng-click=\"select('today')\" ng-disabled=\"isDisabled('today')\">{{ getText('current') }}</button>\n" + | |
" <button type=\"button\" class=\"btn btn-sm btn-danger uib-clear\" ng-click=\"select(null)\">{{ getText('clear') }}</button>\n" + | |
" </span>\n" + | |
" <button type=\"button\" class=\"btn btn-sm btn-success pull-right uib-close\" ng-click=\"close()\">{{ getText('close') }}</button>\n" + | |
" </li>\n" + | |
"</ul>\n" + | |
""); | |
}]); | |
angular.module("uib/template/datepicker/year.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/datepicker/year.html", | |
"<table class=\"uib-yearpicker\" role=\"grid\" aria-labelledby=\"{{::uniqueId}}-title\" aria-activedescendant=\"{{activeDateId}}\">\n" + | |
" <thead>\n" + | |
" <tr>\n" + | |
" <th><button type=\"button\" class=\"btn btn-default btn-sm pull-left uib-left\" ng-click=\"move(-1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-left\"></i></button></th>\n" + | |
" <th colspan=\"3\"><button id=\"{{::uniqueId}}-title\" role=\"heading\" aria-live=\"assertive\" aria-atomic=\"true\" type=\"button\" class=\"btn btn-default btn-sm uib-title\" ng-click=\"toggleMode()\" ng-disabled=\"datepickerMode === maxMode\" tabindex=\"-1\" style=\"width:100%;\"><strong>{{title}}</strong></button></th>\n" + | |
" <th><button type=\"button\" class=\"btn btn-default btn-sm pull-right uib-right\" ng-click=\"move(1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-right\"></i></button></th>\n" + | |
" </tr>\n" + | |
" </thead>\n" + | |
" <tbody>\n" + | |
" <tr class=\"uib-years\" ng-repeat=\"row in rows track by $index\">\n" + | |
" <td ng-repeat=\"dt in row track by dt.date\" class=\"uib-year text-center\" role=\"gridcell\" id=\"{{::dt.uid}}\" ng-class=\"::dt.customClass\">\n" + | |
" <button type=\"button\" style=\"min-width:100%;\" class=\"btn btn-default\" ng-class=\"{'btn-info': dt.selected, active: isActive(dt)}\" ng-click=\"select(dt.date)\" ng-disabled=\"dt.disabled\" tabindex=\"-1\"><span ng-class=\"::{'text-info': dt.current}\">{{::dt.label}}</span></button>\n" + | |
" </td>\n" + | |
" </tr>\n" + | |
" </tbody>\n" + | |
"</table>\n" + | |
""); | |
}]); | |
angular.module("uib/template/modal/backdrop.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/modal/backdrop.html", | |
"<div uib-modal-animation-class=\"fade\"\n" + | |
" modal-in-class=\"in\"\n" + | |
" ng-style=\"{'z-index': 1040 + (index && 1 || 0) + index*10}\"\n" + | |
"></div>\n" + | |
""); | |
}]); | |
angular.module("uib/template/modal/window.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/modal/window.html", | |
"<div modal-render=\"{{$isRendered}}\" tabindex=\"-1\" role=\"dialog\" class=\"modal\"\n" + | |
" uib-modal-animation-class=\"fade\"\n" + | |
" modal-in-class=\"in\"\n" + | |
" ng-style=\"{'z-index': 1050 + index*10, display: 'block'}\">\n" + | |
" <div class=\"modal-dialog\" ng-class=\"size ? 'modal-' + size : ''\"><div class=\"modal-content\" uib-modal-transclude></div></div>\n" + | |
"</div>\n" + | |
""); | |
}]); | |
angular.module("uib/template/pagination/pager.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/pagination/pager.html", | |
"<ul class=\"pager\">\n" + | |
" <li ng-class=\"{disabled: noPrevious()||ngDisabled, previous: align}\"><a href ng-click=\"selectPage(page - 1, $event)\">{{::getText('previous')}}</a></li>\n" + | |
" <li ng-class=\"{disabled: noNext()||ngDisabled, next: align}\"><a href ng-click=\"selectPage(page + 1, $event)\">{{::getText('next')}}</a></li>\n" + | |
"</ul>\n" + | |
""); | |
}]); | |
angular.module("uib/template/pagination/pagination.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/pagination/pagination.html", | |
"<ul class=\"pagination\">\n" + | |
" <li ng-if=\"::boundaryLinks\" ng-class=\"{disabled: noPrevious()||ngDisabled}\" class=\"pagination-first\"><a href ng-click=\"selectPage(1, $event)\">{{::getText('first')}}</a></li>\n" + | |
" <li ng-if=\"::directionLinks\" ng-class=\"{disabled: noPrevious()||ngDisabled}\" class=\"pagination-prev\"><a href ng-click=\"selectPage(page - 1, $event)\">{{::getText('previous')}}</a></li>\n" + | |
" <li ng-repeat=\"page in pages track by $index\" ng-class=\"{active: page.active,disabled: ngDisabled&&!page.active}\" class=\"pagination-page\"><a href ng-click=\"selectPage(page.number, $event)\">{{page.text}}</a></li>\n" + | |
" <li ng-if=\"::directionLinks\" ng-class=\"{disabled: noNext()||ngDisabled}\" class=\"pagination-next\"><a href ng-click=\"selectPage(page + 1, $event)\">{{::getText('next')}}</a></li>\n" + | |
" <li ng-if=\"::boundaryLinks\" ng-class=\"{disabled: noNext()||ngDisabled}\" class=\"pagination-last\"><a href ng-click=\"selectPage(totalPages, $event)\">{{::getText('last')}}</a></li>\n" + | |
"</ul>\n" + | |
""); | |
}]); | |
angular.module("uib/template/pager/pager.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/pager/pager.html", | |
"<ul class=\"pager\">\n" + | |
" <li ng-class=\"{disabled: noPrevious()||ngDisabled, previous: align}\"><a href ng-click=\"selectPage(page - 1, $event)\">{{::getText('previous')}}</a></li>\n" + | |
" <li ng-class=\"{disabled: noNext()||ngDisabled, next: align}\"><a href ng-click=\"selectPage(page + 1, $event)\">{{::getText('next')}}</a></li>\n" + | |
"</ul>\n" + | |
""); | |
}]); | |
angular.module("uib/template/tooltip/tooltip-html-popup.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/tooltip/tooltip-html-popup.html", | |
"<div class=\"tooltip\"\n" + | |
" tooltip-animation-class=\"fade\"\n" + | |
" uib-tooltip-classes\n" + | |
" ng-class=\"{ in: isOpen() }\">\n" + | |
" <div class=\"tooltip-arrow\"></div>\n" + | |
" <div class=\"tooltip-inner\" ng-bind-html=\"contentExp()\"></div>\n" + | |
"</div>\n" + | |
""); | |
}]); | |
angular.module("template/tooltip/tooltip-html-unsafe-popup.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("template/tooltip/tooltip-html-unsafe-popup.html", | |
"<div class=\"tooltip\"\n" + | |
" tooltip-animation-class=\"fade\"\n" + | |
" tooltip-classes\n" + | |
" ng-class=\"{ in: isOpen() }\">\n" + | |
" <div class=\"tooltip-arrow\"></div>\n" + | |
" <div class=\"tooltip-inner\" bind-html-unsafe=\"content\"></div>\n" + | |
"</div>\n" + | |
""); | |
}]); | |
angular.module("uib/template/tooltip/tooltip-popup.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/tooltip/tooltip-popup.html", | |
"<div class=\"tooltip\"\n" + | |
" tooltip-animation-class=\"fade\"\n" + | |
" uib-tooltip-classes\n" + | |
" ng-class=\"{ in: isOpen() }\">\n" + | |
" <div class=\"tooltip-arrow\"></div>\n" + | |
" <div class=\"tooltip-inner\" ng-bind=\"content\"></div>\n" + | |
"</div>\n" + | |
""); | |
}]); | |
angular.module("uib/template/tooltip/tooltip-template-popup.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/tooltip/tooltip-template-popup.html", | |
"<div class=\"tooltip\"\n" + | |
" tooltip-animation-class=\"fade\"\n" + | |
" uib-tooltip-classes\n" + | |
" ng-class=\"{ in: isOpen() }\">\n" + | |
" <div class=\"tooltip-arrow\"></div>\n" + | |
" <div class=\"tooltip-inner\"\n" + | |
" uib-tooltip-template-transclude=\"contentExp()\"\n" + | |
" tooltip-template-transclude-scope=\"originScope()\"></div>\n" + | |
"</div>\n" + | |
""); | |
}]); | |
angular.module("uib/template/popover/popover-html.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/popover/popover-html.html", | |
"<div class=\"popover\"\n" + | |
" tooltip-animation-class=\"fade\"\n" + | |
" uib-tooltip-classes\n" + | |
" ng-class=\"{ in: isOpen() }\">\n" + | |
" <div class=\"arrow\"></div>\n" + | |
"\n" + | |
" <div class=\"popover-inner\">\n" + | |
" <h3 class=\"popover-title\" ng-bind=\"title\" ng-if=\"title\"></h3>\n" + | |
" <div class=\"popover-content\" ng-bind-html=\"contentExp()\"></div>\n" + | |
" </div>\n" + | |
"</div>\n" + | |
""); | |
}]); | |
angular.module("uib/template/popover/popover-template.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/popover/popover-template.html", | |
"<div class=\"popover\"\n" + | |
" tooltip-animation-class=\"fade\"\n" + | |
" uib-tooltip-classes\n" + | |
" ng-class=\"{ in: isOpen() }\">\n" + | |
" <div class=\"arrow\"></div>\n" + | |
"\n" + | |
" <div class=\"popover-inner\">\n" + | |
" <h3 class=\"popover-title\" ng-bind=\"title\" ng-if=\"title\"></h3>\n" + | |
" <div class=\"popover-content\"\n" + | |
" uib-tooltip-template-transclude=\"contentExp()\"\n" + | |
" tooltip-template-transclude-scope=\"originScope()\"></div>\n" + | |
" </div>\n" + | |
"</div>\n" + | |
""); | |
}]); | |
angular.module("uib/template/popover/popover.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/popover/popover.html", | |
"<div class=\"popover\"\n" + | |
" tooltip-animation-class=\"fade\"\n" + | |
" uib-tooltip-classes\n" + | |
" ng-class=\"{ in: isOpen() }\">\n" + | |
" <div class=\"arrow\"></div>\n" + | |
"\n" + | |
" <div class=\"popover-inner\">\n" + | |
" <h3 class=\"popover-title\" ng-bind=\"title\" ng-if=\"title\"></h3>\n" + | |
" <div class=\"popover-content\" ng-bind=\"content\"></div>\n" + | |
" </div>\n" + | |
"</div>\n" + | |
""); | |
}]); | |
angular.module("uib/template/progressbar/bar.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/progressbar/bar.html", | |
"<div class=\"progress-bar\" ng-class=\"type && 'progress-bar-' + type\" role=\"progressbar\" aria-valuenow=\"{{value}}\" aria-valuemin=\"0\" aria-valuemax=\"{{max}}\" ng-style=\"{width: (percent < 100 ? percent : 100) + '%'}\" aria-valuetext=\"{{percent | number:0}}%\" aria-labelledby=\"{{::title}}\" style=\"min-width: 0;\" ng-transclude></div>\n" + | |
""); | |
}]); | |
angular.module("uib/template/progressbar/progress.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/progressbar/progress.html", | |
"<div class=\"progress\" ng-transclude aria-labelledby=\"{{::title}}\"></div>"); | |
}]); | |
angular.module("uib/template/progressbar/progressbar.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/progressbar/progressbar.html", | |
"<div class=\"progress\">\n" + | |
" <div class=\"progress-bar\" ng-class=\"type && 'progress-bar-' + type\" role=\"progressbar\" aria-valuenow=\"{{value}}\" aria-valuemin=\"0\" aria-valuemax=\"{{max}}\" ng-style=\"{width: (percent < 100 ? percent : 100) + '%'}\" aria-valuetext=\"{{percent | number:0}}%\" aria-labelledby=\"{{::title}}\" style=\"min-width: 0;\" ng-transclude></div>\n" + | |
"</div>\n" + | |
""); | |
}]); | |
angular.module("uib/template/rating/rating.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/rating/rating.html", | |
"<span ng-mouseleave=\"reset()\" ng-keydown=\"onKeydown($event)\" tabindex=\"0\" role=\"slider\" aria-valuemin=\"0\" aria-valuemax=\"{{range.length}}\" aria-valuenow=\"{{value}}\">\n" + | |
" <span ng-repeat-start=\"r in range track by $index\" class=\"sr-only\">({{ $index < value ? '*' : ' ' }})</span>\n" + | |
" <i ng-repeat-end ng-mouseenter=\"enter($index + 1)\" ng-click=\"rate($index + 1)\" class=\"glyphicon\" ng-class=\"$index < value && (r.stateOn || 'glyphicon-star') || (r.stateOff || 'glyphicon-star-empty')\" ng-attr-title=\"{{r.title}}\" aria-valuetext=\"{{r.title}}\"></i>\n" + | |
"</span>\n" + | |
""); | |
}]); | |
angular.module("uib/template/tabs/tab.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/tabs/tab.html", | |
"<li ng-class=\"{active: active, disabled: disabled}\" class=\"uib-tab\">\n" + | |
" <div ng-click=\"select()\" uib-tab-heading-transclude>{{heading}}</div>\n" + | |
"</li>\n" + | |
""); | |
}]); | |
angular.module("uib/template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/tabs/tabset.html", | |
"<div>\n" + | |
" <ul class=\"nav nav-{{type || 'tabs'}}\" ng-class=\"{'nav-stacked': vertical, 'nav-justified': justified}\" ng-transclude></ul>\n" + | |
" <div class=\"tab-content\">\n" + | |
" <div class=\"tab-pane\" \n" + | |
" ng-repeat=\"tab in tabs\" \n" + | |
" ng-class=\"{active: tab.active}\"\n" + | |
" uib-tab-content-transclude=\"tab\">\n" + | |
" </div>\n" + | |
" </div>\n" + | |
"</div>\n" + | |
""); | |
}]); | |
angular.module("uib/template/timepicker/timepicker.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/timepicker/timepicker.html", | |
"<table>\n" + | |
" <tbody>\n" + | |
" <tr class=\"text-center\" ng-show=\"::showSpinners\">\n" + | |
" <td><a ng-click=\"incrementHours()\" ng-class=\"{disabled: noIncrementHours()}\" class=\"btn btn-link\" ng-disabled=\"noIncrementHours()\" tabindex=\"{{::tabindex}}\"><span class=\"glyphicon glyphicon-chevron-up\"></span></a></td>\n" + | |
" <td> </td>\n" + | |
" <td><a ng-click=\"incrementMinutes()\" ng-class=\"{disabled: noIncrementMinutes()}\" class=\"btn btn-link\" ng-disabled=\"noIncrementMinutes()\" tabindex=\"{{::tabindex}}\"><span class=\"glyphicon glyphicon-chevron-up\"></span></a></td>\n" + | |
" <td ng-show=\"showSeconds\"> </td>\n" + | |
" <td ng-show=\"showSeconds\"><a ng-click=\"incrementSeconds()\" ng-class=\"{disabled: noIncrementSeconds()}\" class=\"btn btn-link\" ng-disabled=\"noIncrementSeconds()\" tabindex=\"{{::tabindex}}\"><span class=\"glyphicon glyphicon-chevron-up\"></span></a></td>\n" + | |
" <td ng-show=\"showMeridian\"></td>\n" + | |
" </tr>\n" + | |
" <tr>\n" + | |
" <td class=\"form-group\" ng-class=\"{'has-error': invalidHours}\">\n" + | |
" <input style=\"width:50px;\" type=\"text\" placeholder=\"HH\" ng-model=\"hours\" ng-change=\"updateHours()\" class=\"form-control text-center\" ng-readonly=\"::readonlyInput\" maxlength=\"2\" tabindex=\"{{::tabindex}}\" ng-disabled=\"disabled\" ng-blur=\"blur()\">\n" + | |
" </td>\n" + | |
" <td>:</td>\n" + | |
" <td class=\"form-group\" ng-class=\"{'has-error': invalidMinutes}\">\n" + | |
" <input style=\"width:50px;\" type=\"text\" placeholder=\"MM\" ng-model=\"minutes\" ng-change=\"updateMinutes()\" class=\"form-control text-center\" ng-readonly=\"::readonlyInput\" maxlength=\"2\" tabindex=\"{{::tabindex}}\" ng-disabled=\"disabled\" ng-blur=\"blur()\">\n" + | |
" </td>\n" + | |
" <td ng-show=\"showSeconds\">:</td>\n" + | |
" <td class=\"form-group\" ng-class=\"{'has-error': invalidSeconds}\" ng-show=\"showSeconds\">\n" + | |
" <input style=\"width:50px;\" type=\"text\" ng-model=\"seconds\" ng-change=\"updateSeconds()\" class=\"form-control text-center\" ng-readonly=\"readonlyInput\" maxlength=\"2\" tabindex=\"{{::tabindex}}\" ng-disabled=\"disabled\" ng-blur=\"blur()\">\n" + | |
" </td>\n" + | |
" <td ng-show=\"showMeridian\"><button type=\"button\" ng-class=\"{disabled: noToggleMeridian()}\" class=\"btn btn-default text-center\" ng-click=\"toggleMeridian()\" ng-disabled=\"noToggleMeridian()\" tabindex=\"{{::tabindex}}\">{{meridian}}</button></td>\n" + | |
" </tr>\n" + | |
" <tr class=\"text-center\" ng-show=\"::showSpinners\">\n" + | |
" <td><a ng-click=\"decrementHours()\" ng-class=\"{disabled: noDecrementHours()}\" class=\"btn btn-link\" ng-disabled=\"noDecrementHours()\" tabindex=\"{{::tabindex}}\"><span class=\"glyphicon glyphicon-chevron-down\"></span></a></td>\n" + | |
" <td> </td>\n" + | |
" <td><a ng-click=\"decrementMinutes()\" ng-class=\"{disabled: noDecrementMinutes()}\" class=\"btn btn-link\" ng-disabled=\"noDecrementMinutes()\" tabindex=\"{{::tabindex}}\"><span class=\"glyphicon glyphicon-chevron-down\"></span></a></td>\n" + | |
" <td ng-show=\"showSeconds\"> </td>\n" + | |
" <td ng-show=\"showSeconds\"><a ng-click=\"decrementSeconds()\" ng-class=\"{disabled: noDecrementSeconds()}\" class=\"btn btn-link\" ng-disabled=\"noDecrementSeconds()\" tabindex=\"{{::tabindex}}\"><span class=\"glyphicon glyphicon-chevron-down\"></span></a></td>\n" + | |
" <td ng-show=\"showMeridian\"></td>\n" + | |
" </tr>\n" + | |
" </tbody>\n" + | |
"</table>\n" + | |
""); | |
}]); | |
angular.module("uib/template/typeahead/typeahead-match.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/typeahead/typeahead-match.html", | |
"<a href tabindex=\"-1\" ng-bind-html=\"match.label | uibTypeaheadHighlight:query\"></a>\n" + | |
""); | |
}]); | |
angular.module("uib/template/typeahead/typeahead-popup.html", []).run(["$templateCache", function($templateCache) { | |
$templateCache.put("uib/template/typeahead/typeahead-popup.html", | |
"<ul class=\"dropdown-menu\" ng-show=\"isOpen() && !moveInProgress\" ng-style=\"{top: position().top+'px', left: position().left+'px'}\" style=\"display: block;\" role=\"listbox\" aria-hidden=\"{{!isOpen()}}\">\n" + | |
" <li ng-repeat=\"match in matches track by $index\" ng-class=\"{active: isActive($index) }\" ng-mouseenter=\"selectActive($index)\" ng-click=\"selectMatch($index)\" role=\"option\" id=\"{{::match.id}}\">\n" + | |
" <div uib-typeahead-match index=\"$index\" match=\"match\" query=\"query\" template-url=\"templateUrl\"></div>\n" + | |
" </li>\n" + | |
"</ul>\n" + | |
""); | |
}]); | |
angular.module('ui.bootstrap.carousel').run(function() {!angular.$$csp() && angular.element(document).find('head').prepend('<style type="text/css">.ng-animate.item:not(.left):not(.right){-webkit-transition:0s ease-in-out left;transition:0s ease-in-out left}</style>'); }) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment