Snippet:
Filter
Source
Rendered
linky filter
<div ng-bind-html="snippet | linky"> </div>
linky target
<div ng-bind-html="snippetWithSingleURL | linky:'_blank'"> </div>
linky custom attributes
<div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"> </div>
no filter
<div ng-bind="snippet"> </div>
angular.module('linkyExample', ['ngSanitize'])
.controller('ExampleController', ['$scope', function($scope) {
$scope.snippet =
'Pretty text with some links:\n' +
'http://angularjs.org/,\n' +
'mailto:us@somewhere.org,\n' +
'another@somewhere.org,\n' +
'and one more: ftp://127.0.0.1/.';
$scope.snippetWithSingleURL = 'http://angularjs.org/';
}]);
it('should linkify the snippet with urls', function() {
expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
'another@somewhere.org, and one more: ftp://127.0.0.1/.');
expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
});
it('should not linkify snippet without the linky filter', function() {
expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' +
'another@somewhere.org, and one more: ftp://127.0.0.1/.');
expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
});
it('should update', function() {
element(by.model('snippet')).clear();
element(by.model('snippet')).sendKeys('new http://link.');
expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
toBe('new http://link.');
expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
.toBe('new http://link.');
});
it('should work with the target property', function() {
expect(element(by.id('linky-target')).
element(by.binding("snippetWithSingleURL | linky:'_blank'")).getText()).
toBe('http://angularjs.org/');
expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
});
it('should optionally add custom attributes', function() {
expect(element(by.id('linky-custom-attributes')).
element(by.binding("snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}")).getText()).
toBe('http://angularjs.org/');
expect(element(by.css('#linky-custom-attributes a')).getAttribute('rel')).toEqual('nofollow');
});
*/
angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
var LINKY_URL_REGEXP =
/((s?ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
MAILTO_REGEXP = /^mailto:/i;
var linkyMinErr = angular.$$minErr('linky');
var isDefined = angular.isDefined;
var isFunction = angular.isFunction;
var isObject = angular.isObject;
var isString = angular.isString;
return function(text, target, attributes) {
if (text == null || text === '') return text;
if (!isString(text)) throw linkyMinErr('notstring', 'Expected string but received: {0}', text);
var attributesFn =
isFunction(attributes) ? attributes :
isObject(attributes) ? function getAttributesObject() {return attributes;} :
function getEmptyAttributesObject() {return {};};
var match;
var raw = text;
var html = [];
var url;
var i;
while ((match = raw.match(LINKY_URL_REGEXP))) {
// We can not end in these as they are sometimes found at the end of the sentence
url = match[0];
// if we did not match ftp/http/www/mailto then assume mailto
if (!match[2] && !match[4]) {
url = (match[3] ? 'http://' : 'mailto:') + url;
}
i = match.index;
addText(raw.substr(0, i));
addLink(url, match[0].replace(MAILTO_REGEXP, ''));
raw = raw.substring(i + match[0].length);
}
addText(raw);
return $sanitize(html.join(''));
function addText(text) {
if (!text) {
return;
}
html.push(sanitizeText(text));
}
function addLink(url, text) {
var key, linkAttributes = attributesFn(url);
html.push('
');
addText(text);
html.push(' ');
}
};
}]);
})(window, window.angular);
/*
* angular-ui-bootstrap
* http://angular-ui.github.io/bootstrap/
* Version: 2.5.6 - 2017-10-14
* License: MIT
*/angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.collapse","ui.bootstrap.tabindex","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.isClass","ui.bootstrap.datepicker","ui.bootstrap.position","ui.bootstrap.datepickerPopup","ui.bootstrap.debounce","ui.bootstrap.multiMap","ui.bootstrap.dropdown","ui.bootstrap.stackedMap","ui.bootstrap.modal","ui.bootstrap.paging","ui.bootstrap.pager","ui.bootstrap.pagination","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/year.html","uib/template/datepickerPopup/popup.html","uib/template/modal/window.html","uib/template/pager/pager.html","uib/template/pagination/pagination.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', '$q', '$parse', '$injector', function($animate, $q, $parse, $injector) {
var $animateCss = $injector.has('$animateCss') ? $injector.get('$animateCss') : null;
return {
link: function(scope, element, attrs) {
var expandingExpr = $parse(attrs.expanding),
expandedExpr = $parse(attrs.expanded),
collapsingExpr = $parse(attrs.collapsing),
collapsedExpr = $parse(attrs.collapsed),
horizontal = false,
css = {},
cssTo = {};
init();
function init() {
horizontal = !!('horizontal' in attrs);
if (horizontal) {
css = {
width: ''
};
cssTo = {width: '0'};
} else {
css = {
height: ''
};
cssTo = {height: '0'};
}
if (!scope.$eval(attrs.uibCollapse)) {
element.addClass('in')
.addClass('collapse')
.attr('aria-expanded', true)
.attr('aria-hidden', false)
.css(css);
}
}
function getScrollFromElement(element) {
if (horizontal) {
return {width: element.scrollWidth + 'px'};
}
return {height: element.scrollHeight + 'px'};
}
function expand() {
if (element.hasClass('collapse') && element.hasClass('in')) {
return;
}
$q.resolve(expandingExpr(scope))
.then(function() {
element.removeClass('collapse')
.addClass('collapsing')
.attr('aria-expanded', true)
.attr('aria-hidden', false);
if ($animateCss) {
$animateCss(element, {
addClass: 'in',
easing: 'ease',
css: {
overflow: 'hidden'
},
to: getScrollFromElement(element[0])
}).start()['finally'](expandDone);
} else {
$animate.addClass(element, 'in', {
css: {
overflow: 'hidden'
},
to: getScrollFromElement(element[0])
}).then(expandDone);
}
}, angular.noop);
}
function expandDone() {
element.removeClass('collapsing')
.addClass('collapse')
.css(css);
expandedExpr(scope);
}
function collapse() {
if (!element.hasClass('collapse') && !element.hasClass('in')) {
return collapseDone();
}
$q.resolve(collapsingExpr(scope))
.then(function() {
element
// IMPORTANT: The width must be set before adding "collapsing" class.
// Otherwise, the browser attempts to animate from width 0 (in
// collapsing class) to the given width here.
.css(getScrollFromElement(element[0]))
// 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: cssTo
}).start()['finally'](collapseDone);
} else {
$animate.removeClass(element, 'in', {
to: cssTo
}).then(collapseDone);
}
}, angular.noop);
}
function collapseDone() {
element.css(cssTo); // Required so that collapse works when animation is disabled
element.removeClass('collapsing')
.addClass('collapse');
collapsedExpr(scope);
}
scope.$watch(attrs.uibCollapse, function(shouldCollapse) {
if (shouldCollapse) {
collapse();
} else {
expand();
}
});
}
};
}]);
angular.module('ui.bootstrap.tabindex', [])
.directive('uibTabindexToggle', function() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
attrs.$observe('disabled', function(disabled) {
attrs.$set('tabindex', disabled ? -1 : null);
});
}
};
});
angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse', 'ui.bootstrap.tabindex'])
.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
restrict: 'A',
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/accordion/accordion-group.html';
},
scope: {
heading: '@', // Interpolate the heading attribute onto this scope
panelClass: '@?', // Ditto with panelClass
isOpen: '=?',
isDisabled: '=?'
},
controller: function() {
this.setHeading = function(element) {
this.heading = element;
};
},
link: function(scope, element, attrs, accordionCtrl) {
element.addClass('panel');
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;
}
}
};
var id = 'accordiongroup-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
scope.headingId = id + '-tab';
scope.panelId = id + '-panel';
}
};
})
// 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) {
var elem = angular.element(element[0].querySelector(getHeaderSelectors()));
elem.html('');
elem.append(heading);
}
});
}
};
function getHeaderSelectors() {
return 'uib-accordion-header,' +
'data-uib-accordion-header,' +
'x-uib-accordion-header,' +
'uib\\:accordion-header,' +
'[uib-accordion-header],' +
'[data-uib-accordion-header],' +
'[x-uib-accordion-header]';
}
});
angular.module('ui.bootstrap.alert', [])
.controller('UibAlertController', ['$scope', '$element', '$attrs', '$interpolate', '$timeout', function($scope, $element, $attrs, $interpolate, $timeout) {
$scope.closeable = !!$attrs.close;
$element.addClass('alert');
$attrs.$set('role', 'alert');
if ($scope.closeable) {
$element.addClass('alert-dismissible');
}
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',
restrict: 'A',
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/alert/alert.html';
},
transclude: true,
scope: {
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 ? '' : undefined);
});
}
}
};
}])
.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', '$timeout', '$animate', function($scope, $element, $interval, $timeout, $animate) {
var self = this,
slides = self.slides = $scope.slides = [],
SLIDE_DIRECTION = 'uib-slideDirection',
currentIndex = $scope.active,
currentInterval, isPlaying;
var destroyed = false;
$element.addClass('carousel');
self.addSlide = function(slide, element) {
slides.push({
slide: slide,
element: element
});
slides.sort(function(a, b) {
return +a.slide.index - +b.slide.index;
});
//if this is the first slide or the slide is set to active, select it
if (slide.index === $scope.active || slides.length === 1 && !angular.isNumber($scope.active)) {
if ($scope.$currentTransition) {
$scope.$currentTransition = null;
}
currentIndex = slide.index;
$scope.active = slide.index;
setActive(currentIndex);
self.select(slides[findSlideIndex(slide)]);
if (slides.length === 1) {
$scope.play();
}
}
};
self.getCurrentIndex = function() {
for (var i = 0; i < slides.length; i++) {
if (slides[i].slide.index === currentIndex) {
return i;
}
}
};
self.next = $scope.next = function() {
var newIndex = (self.getCurrentIndex() + 1) % slides.length;
if (newIndex === 0 && $scope.noWrap()) {
$scope.pause();
return;
}
return self.select(slides[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(slides[newIndex], 'prev');
};
self.removeSlide = function(slide) {
var index = findSlideIndex(slide);
//get the index of the slide inside the carousel
slides.splice(index, 1);
if (slides.length > 0 && currentIndex === index) {
if (index >= slides.length) {
currentIndex = slides.length - 1;
$scope.active = currentIndex;
setActive(currentIndex);
self.select(slides[slides.length - 1]);
} else {
currentIndex = index;
$scope.active = currentIndex;
setActive(currentIndex);
self.select(slides[index]);
}
} else if (currentIndex > index) {
currentIndex--;
$scope.active = currentIndex;
}
//clean the active value when no more slide
if (slides.length === 0) {
currentIndex = null;
$scope.active = null;
}
};
/* direction: "prev" or "next" */
self.select = $scope.select = function(nextSlide, direction) {
var nextIndex = findSlideIndex(nextSlide.slide);
//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.slide.index !== currentIndex &&
!$scope.$currentTransition) {
goNext(nextSlide.slide, nextIndex, direction);
}
};
/* Allow outside people to call indexOf on slides array */
$scope.indexOfSlide = function(slide) {
return +slide.slide.index;
};
$scope.isActive = function(slide) {
return $scope.active === slide.slide.index;
};
$scope.isPrevDisabled = function() {
return $scope.active === 0 && $scope.noWrap();
};
$scope.isNextDisabled = function() {
return $scope.active === slides.length - 1 && $scope.noWrap();
};
$scope.pause = function() {
if (!$scope.noPause) {
isPlaying = false;
resetTimer();
}
};
$scope.play = function() {
if (!isPlaying) {
isPlaying = true;
restartTimer();
}
};
$element.on('mouseenter', $scope.pause);
$element.on('mouseleave', $scope.play);
$scope.$on('$destroy', function() {
destroyed = true;
resetTimer();
});
$scope.$watch('noTransition', function(noTransition) {
$animate.enabled($element, !noTransition);
});
$scope.$watch('interval', restartTimer);
$scope.$watchCollection('slides', resetTransition);
$scope.$watch('active', function(index) {
if (angular.isNumber(index) && currentIndex !== index) {
for (var i = 0; i < slides.length; i++) {
if (slides[i].slide.index === index) {
index = i;
break;
}
}
var slide = slides[index];
if (slide) {
setActive(index);
self.select(slides[index]);
currentIndex = index;
}
}
});
function getSlideByIndex(index) {
for (var i = 0, l = slides.length; i < l; ++i) {
if (slides[i].index === index) {
return slides[i];
}
}
}
function setActive(index) {
for (var i = 0; i < slides.length; i++) {
slides[i].slide.active = i === index;
}
}
function goNext(slide, index, direction) {
if (destroyed) {
return;
}
angular.extend(slide, {direction: direction});
angular.extend(slides[currentIndex].slide || {}, {direction: direction});
if ($animate.enabled($element) && !$scope.$currentTransition &&
slides[index].element && self.slides.length > 1) {
slides[index].element.data(SLIDE_DIRECTION, slide.direction);
var currentIdx = self.getCurrentIndex();
if (angular.isNumber(currentIdx) && slides[currentIdx].element) {
slides[currentIdx].element.data(SLIDE_DIRECTION, slide.direction);
}
$scope.$currentTransition = true;
$animate.on('addClass', slides[index].element, function(element, phase) {
if (phase === 'close') {
$scope.$currentTransition = null;
$animate.off('addClass', element);
}
});
}
$scope.active = slide.index;
currentIndex = slide.index;
setActive(index);
//every time you change slides, reset the timer
restartTimer();
}
function findSlideIndex(slide) {
for (var i = 0; i < slides.length; i++) {
if (slides[i].slide === slide) {
return i;
}
}
}
function resetTimer() {
if (currentInterval) {
$interval.cancel(currentInterval);
currentInterval = null;
}
}
function resetTransition(slides) {
if (!slides.length) {
$scope.$currentTransition = null;
}
}
function restartTimer() {
resetTimer();
var interval = +$scope.interval;
if (!isNaN(interval) && interval > 0) {
currentInterval = $interval(timerFn, interval);
}
}
function timerFn() {
var interval = +$scope.interval;
if (isPlaying && !isNaN(interval) && interval > 0 && slides.length) {
$scope.next();
} else {
$scope.pause();
}
}
}])
.directive('uibCarousel', function() {
return {
transclude: true,
controller: 'UibCarouselController',
controllerAs: 'carousel',
restrict: 'A',
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/carousel/carousel.html';
},
scope: {
active: '=',
interval: '=',
noTransition: '=',
noPause: '=',
noWrap: '&'
}
};
})
.directive('uibSlide', ['$animate', function($animate) {
return {
require: '^uibCarousel',
restrict: 'A',
transclude: true,
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/carousel/slide.html';
},
scope: {
actual: '=?',
index: '=?'
},
link: function (scope, element, attrs, carouselCtrl) {
element.addClass('item');
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) {
$animate[active ? 'addClass' : 'removeClass'](element, 'active');
});
}
};
}])
.animation('.item', ['$animateCss',
function($animateCss) {
var SLIDE_DIRECTION = 'uib-slideDirection';
function removeClass(element, className, callback) {
element.removeClass(className);
if (callback) {
callback();
}
}
return {
beforeAddClass: function(element, className, done) {
if (className === 'active') {
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) {
if (className === 'active') {
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', 'dateFilter', 'orderByFilter', 'filterFilter', function($log, $locale, dateFilter, orderByFilter, filterFilter) {
// 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 = {};
this.formatters = {};
formatCodeToRegex = [
{
key: 'yyyy',
regex: '\\d{4}',
apply: function(value) { this.year = +value; },
formatter: function(date) {
var _date = new Date();
_date.setFullYear(Math.abs(date.getFullYear()));
return dateFilter(_date, 'yyyy');
}
},
{
key: 'yy',
regex: '\\d{2}',
apply: function(value) { value = +value; this.year = value < 69 ? value + 2000 : value + 1900; },
formatter: function(date) {
var _date = new Date();
_date.setFullYear(Math.abs(date.getFullYear()));
return dateFilter(_date, 'yy');
}
},
{
key: 'y',
regex: '\\d{1,4}',
apply: function(value) { this.year = +value; },
formatter: function(date) {
var _date = new Date();
_date.setFullYear(Math.abs(date.getFullYear()));
return dateFilter(_date, 'y');
}
},
{
key: 'M!',
regex: '0?[1-9]|1[0-2]',
apply: function(value) { this.month = value - 1; },
formatter: function(date) {
var value = date.getMonth();
if (/^[0-9]$/.test(value)) {
return dateFilter(date, 'MM');
}
return dateFilter(date, 'M');
}
},
{
key: 'MMMM',
regex: $locale.DATETIME_FORMATS.MONTH.join('|'),
apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); },
formatter: function(date) { return dateFilter(date, 'MMMM'); }
},
{
key: 'MMM',
regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'),
apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); },
formatter: function(date) { return dateFilter(date, 'MMM'); }
},
{
key: 'MM',
regex: '0[1-9]|1[0-2]',
apply: function(value) { this.month = value - 1; },
formatter: function(date) { return dateFilter(date, 'MM'); }
},
{
key: 'M',
regex: '[1-9]|1[0-2]',
apply: function(value) { this.month = value - 1; },
formatter: function(date) { return dateFilter(date, 'M'); }
},
{
key: 'd!',
regex: '[0-2]?[0-9]{1}|3[0-1]{1}',
apply: function(value) { this.date = +value; },
formatter: function(date) {
var value = date.getDate();
if (/^[1-9]$/.test(value)) {
return dateFilter(date, 'dd');
}
return dateFilter(date, 'd');
}
},
{
key: 'dd',
regex: '[0-2][0-9]{1}|3[0-1]{1}',
apply: function(value) { this.date = +value; },
formatter: function(date) { return dateFilter(date, 'dd'); }
},
{
key: 'd',
regex: '[1-2]?[0-9]{1}|3[0-1]{1}',
apply: function(value) { this.date = +value; },
formatter: function(date) { return dateFilter(date, 'd'); }
},
{
key: 'EEEE',
regex: $locale.DATETIME_FORMATS.DAY.join('|'),
formatter: function(date) { return dateFilter(date, 'EEEE'); }
},
{
key: 'EEE',
regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|'),
formatter: function(date) { return dateFilter(date, 'EEE'); }
},
{
key: 'HH',
regex: '(?:0|1)[0-9]|2[0-3]',
apply: function(value) { this.hours = +value; },
formatter: function(date) { return dateFilter(date, 'HH'); }
},
{
key: 'hh',
regex: '0[0-9]|1[0-2]',
apply: function(value) { this.hours = +value; },
formatter: function(date) { return dateFilter(date, 'hh'); }
},
{
key: 'H',
regex: '1?[0-9]|2[0-3]',
apply: function(value) { this.hours = +value; },
formatter: function(date) { return dateFilter(date, 'H'); }
},
{
key: 'h',
regex: '[0-9]|1[0-2]',
apply: function(value) { this.hours = +value; },
formatter: function(date) { return dateFilter(date, 'h'); }
},
{
key: 'mm',
regex: '[0-5][0-9]',
apply: function(value) { this.minutes = +value; },
formatter: function(date) { return dateFilter(date, 'mm'); }
},
{
key: 'm',
regex: '[0-9]|[1-5][0-9]',
apply: function(value) { this.minutes = +value; },
formatter: function(date) { return dateFilter(date, 'm'); }
},
{
key: 'sss',
regex: '[0-9][0-9][0-9]',
apply: function(value) { this.milliseconds = +value; },
formatter: function(date) { return dateFilter(date, 'sss'); }
},
{
key: 'ss',
regex: '[0-5][0-9]',
apply: function(value) { this.seconds = +value; },
formatter: function(date) { return dateFilter(date, 'ss'); }
},
{
key: 's',
regex: '[0-9]|[1-5][0-9]',
apply: function(value) { this.seconds = +value; },
formatter: function(date) { return dateFilter(date, 's'); }
},
{
key: 'a',
regex: $locale.DATETIME_FORMATS.AMPMS.join('|'),
apply: function(value) {
if (this.hours === 12) {
this.hours = 0;
}
if (value === 'PM') {
this.hours += 12;
}
},
formatter: function(date) { return dateFilter(date, 'a'); }
},
{
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);
},
formatter: function(date) {
return dateFilter(date, 'Z');
}
},
{
key: 'ww',
regex: '[0-4][0-9]|5[0-3]',
formatter: function(date) { return dateFilter(date, 'ww'); }
},
{
key: 'w',
regex: '[0-9]|[1-4][0-9]|5[0-3]',
formatter: function(date) { return dateFilter(date, 'w'); }
},
{
key: 'GGGG',
regex: $locale.DATETIME_FORMATS.ERANAMES.join('|').replace(/\s/g, '\\s'),
formatter: function(date) { return dateFilter(date, 'GGGG'); }
},
{
key: 'GGG',
regex: $locale.DATETIME_FORMATS.ERAS.join('|'),
formatter: function(date) { return dateFilter(date, 'GGG'); }
},
{
key: 'GG',
regex: $locale.DATETIME_FORMATS.ERAS.join('|'),
formatter: function(date) { return dateFilter(date, 'GG'); }
},
{
key: 'G',
regex: $locale.DATETIME_FORMATS.ERAS.join('|'),
formatter: function(date) { return dateFilter(date, 'G'); }
}
];
if (angular.version.major >= 1 && angular.version.minor > 4) {
formatCodeToRegex.push({
key: 'LLLL',
regex: $locale.DATETIME_FORMATS.STANDALONEMONTH.join('|'),
apply: function(value) { this.month = $locale.DATETIME_FORMATS.STANDALONEMONTH.indexOf(value); },
formatter: function(date) { return dateFilter(date, 'LLLL'); }
});
}
};
this.init();
function getFormatCodeToRegex(key) {
return filterFilter(formatCodeToRegex, {key: key}, true)[0];
}
this.getParser = function (key) {
var f = getFormatCodeToRegex(key);
return f && f.apply || null;
};
this.overrideParser = function (key, parser) {
var f = getFormatCodeToRegex(key);
if (f && angular.isFunction(parser)) {
this.parsers = {};
f.apply = parser;
}
}.bind(this);
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 single quote
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,
key: data.key,
apply: data.apply,
matcher: data.regex
});
}
});
return {
regex: new RegExp('^' + regex.join('') + '$'),
map: orderByFilter(map, 'index')
};
}
function createFormatter(format) {
var formatters = [];
var i = 0;
var formatter, literalIdx;
while (i < format.length) {
if (angular.isNumber(literalIdx)) {
if (format.charAt(i) === '\'') {
if (i + 1 >= format.length || format.charAt(i + 1) !== '\'') {
formatters.push(constructLiteralFormatter(format, literalIdx, i));
literalIdx = null;
}
} else if (i === format.length) {
while (literalIdx < format.length) {
formatter = constructFormatterFromIdx(format, literalIdx);
formatters.push(formatter);
literalIdx = formatter.endIdx;
}
}
i++;
continue;
}
if (format.charAt(i) === '\'') {
literalIdx = i;
i++;
continue;
}
formatter = constructFormatterFromIdx(format, i);
formatters.push(formatter.parser);
i = formatter.endIdx;
}
return formatters;
}
function constructLiteralFormatter(format, literalIdx, endIdx) {
return function() {
return format.substr(literalIdx + 1, endIdx - literalIdx - 1);
};
}
function constructFormatterFromIdx(format, i) {
var currentPosStr = format.substr(i);
for (var j = 0; j < formatCodeToRegex.length; j++) {
if (new RegExp('^' + formatCodeToRegex[j].key).test(currentPosStr)) {
var data = formatCodeToRegex[j];
return {
endIdx: i + data.key.length,
parser: data.formatter
};
}
}
return {
endIdx: i + 1,
parser: function() {
return currentPosStr.charAt(0);
}
};
}
this.filter = function(date, format) {
if (!angular.isDate(date) || isNaN(date) || !format) {
return '';
}
format = $locale.DATETIME_FORMATS[format] || format;
if ($locale.id !== localeId) {
this.init();
}
if (!this.formatters[format]) {
this.formatters[format] = createFormatter(format);
}
var formatters = this.formatters[format];
return formatters.reduce(function(str, formatter) {
return str + formatter(date);
}, '');
};
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, 'apply');
}
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);
}
this.toTimezone = toTimezone;
this.fromTimezone = fromTimezone;
this.timezoneToOffset = timezoneToOffset;
this.addDateMinutes = addDateMinutes;
this.convertTimezoneToLocal = convertTimezoneToLocal;
function toTimezone(date, timezone) {
return date && timezone ? convertTimezoneToLocal(date, timezone) : date;
}
function fromTimezone(date, timezone) {
return date && timezone ? convertTimezoneToLocal(date, timezone, true) : date;
}
//https://github.com/angular/angular.js/blob/622c42169699ec07fc6daaa19fe6d224e5d2f70e/src/Angular.js#L1207
function timezoneToOffset(timezone, fallback) {
timezone = timezone.replace(/:/g, '');
var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000;
return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset;
}
function addDateMinutes(date, minutes) {
date = new Date(date.getTime());
date.setMinutes(date.getMinutes() + minutes);
return date;
}
function convertTimezoneToLocal(date, timezone, reverse) {
reverse = reverse ? -1 : 1;
var dateTimezoneOffset = date.getTimezoneOffset();
var timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset);
return addDateMinutes(date, reverse * (timezoneOffset - dateTimezoneOffset));
}
}]);
// Avoiding use of ng-class as it creates a lot of watchers when a class is to be applied to
// at most one element.
angular.module('ui.bootstrap.isClass', [])
.directive('uibIsClass', [
'$animate',
function ($animate) {
// 11111111 22222222
var ON_REGEXP = /^\s*([\s\S]+?)\s+on\s+([\s\S]+?)\s*$/;
// 11111111 22222222
var IS_REGEXP = /^\s*([\s\S]+?)\s+for\s+([\s\S]+?)\s*$/;
var dataPerTracked = {};
return {
restrict: 'A',
compile: function(tElement, tAttrs) {
var linkedScopes = [];
var instances = [];
var expToData = {};
var lastActivated = null;
var onExpMatches = tAttrs.uibIsClass.match(ON_REGEXP);
var onExp = onExpMatches[2];
var expsStr = onExpMatches[1];
var exps = expsStr.split(',');
return linkFn;
function linkFn(scope, element, attrs) {
linkedScopes.push(scope);
instances.push({
scope: scope,
element: element
});
exps.forEach(function(exp, k) {
addForExp(exp, scope);
});
scope.$on('$destroy', removeScope);
}
function addForExp(exp, scope) {
var matches = exp.match(IS_REGEXP);
var clazz = scope.$eval(matches[1]);
var compareWithExp = matches[2];
var data = expToData[exp];
if (!data) {
var watchFn = function(compareWithVal) {
var newActivated = null;
instances.some(function(instance) {
var thisVal = instance.scope.$eval(onExp);
if (thisVal === compareWithVal) {
newActivated = instance;
return true;
}
});
if (data.lastActivated !== newActivated) {
if (data.lastActivated) {
$animate.removeClass(data.lastActivated.element, clazz);
}
if (newActivated) {
$animate.addClass(newActivated.element, clazz);
}
data.lastActivated = newActivated;
}
};
expToData[exp] = data = {
lastActivated: null,
scope: scope,
watchFn: watchFn,
compareWithExp: compareWithExp,
watcher: scope.$watch(compareWithExp, watchFn)
};
}
data.watchFn(scope.$eval(compareWithExp));
}
function removeScope(e) {
var removedScope = e.targetScope;
var index = linkedScopes.indexOf(removedScope);
linkedScopes.splice(index, 1);
instances.splice(index, 1);
if (linkedScopes.length) {
var newWatchScope = linkedScopes[0];
angular.forEach(expToData, function(data) {
if (data.scope === removedScope) {
data.watcher = newWatchScope.$watch(data.compareWithExp, data.watchFn);
data.scope = newWatchScope;
}
});
} else {
expToData = {};
}
}
}
};
}]);
angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.isClass'])
.value('$datepickerSuppressError', false)
.value('$datepickerLiteralWarning', true)
.constant('uibDatepickerConfig', {
datepickerMode: 'day',
formatDay: 'dd',
formatMonth: 'MMMM',
formatYear: 'yyyy',
formatDayHeader: 'EEE',
formatDayTitle: 'MMMM yyyy',
formatMonthTitle: 'yyyy',
maxDate: null,
maxMode: 'year',
minDate: null,
minMode: 'day',
monthColumns: 3,
ngModelOptions: {},
shortcutPropagation: false,
showWeeks: true,
yearColumns: 5,
yearRows: 4
})
.controller('UibDatepickerController', ['$scope', '$element', '$attrs', '$parse', '$interpolate', '$locale', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerLiteralWarning', '$datepickerSuppressError', 'uibDateParser',
function($scope, $element, $attrs, $parse, $interpolate, $locale, $log, dateFilter, datepickerConfig, $datepickerLiteralWarning, $datepickerSuppressError, dateParser) {
var self = this,
ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl;
ngModelOptions = {},
watchListeners = [];
$element.addClass('uib-datepicker');
$attrs.$set('role', 'application');
if (!$scope.datepickerOptions) {
$scope.datepickerOptions = {};
}
// Modes chain
this.modes = ['day', 'month', 'year'];
[
'customClass',
'dateDisabled',
'datepickerMode',
'formatDay',
'formatDayHeader',
'formatDayTitle',
'formatMonth',
'formatMonthTitle',
'formatYear',
'maxDate',
'maxMode',
'minDate',
'minMode',
'monthColumns',
'showWeeks',
'shortcutPropagation',
'startingDay',
'yearColumns',
'yearRows'
].forEach(function(key) {
switch (key) {
case 'customClass':
case 'dateDisabled':
$scope[key] = $scope.datepickerOptions[key] || angular.noop;
break;
case 'datepickerMode':
$scope.datepickerMode = angular.isDefined($scope.datepickerOptions.datepickerMode) ?
$scope.datepickerOptions.datepickerMode : datepickerConfig.datepickerMode;
break;
case 'formatDay':
case 'formatDayHeader':
case 'formatDayTitle':
case 'formatMonth':
case 'formatMonthTitle':
case 'formatYear':
self[key] = angular.isDefined($scope.datepickerOptions[key]) ?
$interpolate($scope.datepickerOptions[key])($scope.$parent) :
datepickerConfig[key];
break;
case 'monthColumns':
case 'showWeeks':
case 'shortcutPropagation':
case 'yearColumns':
case 'yearRows':
self[key] = angular.isDefined($scope.datepickerOptions[key]) ?
$scope.datepickerOptions[key] : datepickerConfig[key];
break;
case 'startingDay':
if (angular.isDefined($scope.datepickerOptions.startingDay)) {
self.startingDay = $scope.datepickerOptions.startingDay;
} else if (angular.isNumber(datepickerConfig.startingDay)) {
self.startingDay = datepickerConfig.startingDay;
} else {
self.startingDay = ($locale.DATETIME_FORMATS.FIRSTDAYOFWEEK + 8) % 7;
}
break;
case 'maxDate':
case 'minDate':
$scope.$watch('datepickerOptions.' + key, function(value) {
if (value) {
if (angular.isDate(value)) {
self[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.getOption('timezone'));
} else {
if ($datepickerLiteralWarning) {
$log.warn('Literal date support has been deprecated, please switch to date object usage');
}
self[key] = new Date(dateFilter(value, 'medium'));
}
} else {
self[key] = datepickerConfig[key] ?
dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.getOption('timezone')) :
null;
}
self.refreshView();
});
break;
case 'maxMode':
case 'minMode':
if ($scope.datepickerOptions[key]) {
$scope.$watch(function() { return $scope.datepickerOptions[key]; }, function(value) {
self[key] = $scope[key] = angular.isDefined(value) ? value : $scope.datepickerOptions[key];
if (key === 'minMode' && self.modes.indexOf($scope.datepickerOptions.datepickerMode) < self.modes.indexOf(self[key]) ||
key === 'maxMode' && self.modes.indexOf($scope.datepickerOptions.datepickerMode) > self.modes.indexOf(self[key])) {
$scope.datepickerMode = self[key];
$scope.datepickerOptions.datepickerMode = self[key];
}
});
} else {
self[key] = $scope[key] = datepickerConfig[key] || null;
}
break;
}
});
$scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000);
$scope.disabled = angular.isDefined($attrs.disabled) || false;
if (angular.isDefined($attrs.ngDisabled)) {
watchListeners.push($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_;
ngModelOptions = extractOptions(ngModelCtrl);
if ($scope.datepickerOptions.initDate) {
self.activeDate = dateParser.fromTimezone($scope.datepickerOptions.initDate, ngModelOptions.getOption('timezone')) || new Date();
$scope.$watch('datepickerOptions.initDate', function(initDate) {
if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) {
self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.getOption('timezone'));
self.refreshView();
}
});
} else {
self.activeDate = new Date();
}
var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : new Date();
this.activeDate = !isNaN(date) ?
dateParser.fromTimezone(date, ngModelOptions.getOption('timezone')) :
dateParser.fromTimezone(new Date(), ngModelOptions.getOption('timezone'));
ngModelCtrl.$render = function() {
self.render();
};
};
this.render = function() {
if (ngModelCtrl.$viewValue) {
var date = new Date(ngModelCtrl.$viewValue),
isValid = !isNaN(date);
if (isValid) {
this.activeDate = dateParser.fromTimezone(date, ngModelOptions.getOption('timezone'));
} else if (!$datepickerSuppressError) {
$log.error('Datepicker directive: "ng-model" value must be a Date object');
}
}
this.refreshView();
};
this.refreshView = function() {
if (this.element) {
$scope.selectedDt = null;
this._refreshView();
if ($scope.activeDt) {
$scope.activeDateId = $scope.activeDt.uid;
}
var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
date = dateParser.fromTimezone(date, ngModelOptions.getOption('timezone'));
ngModelCtrl.$setValidity('dateDisabled', !date ||
this.element && !this.isDisabled(date));
}
};
this.createDateObject = function(date, format) {
var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
model = dateParser.fromTimezone(model, ngModelOptions.getOption('timezone'));
var today = new Date();
today = dateParser.fromTimezone(today, ngModelOptions.getOption('timezone'));
var time = this.compare(date, today);
var dt = {
date: date,
label: dateParser.filter(date, format),
selected: model && this.compare(date, model) === 0,
disabled: this.isDisabled(date),
past: time < 0,
current: time === 0,
future: time > 0,
customClass: this.customClass(date) || null
};
if (model && this.compare(date, model) === 0) {
$scope.selectedDt = dt;
}
if (self.activeDate && this.compare(dt.date, self.activeDate) === 0) {
$scope.activeDt = dt;
}
return dt;
};
this.isDisabled = function(date) {
return $scope.disabled ||
this.minDate && this.compare(date, this.minDate) < 0 ||
this.maxDate && this.compare(date, this.maxDate) > 0 ||
$scope.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 ? dateParser.fromTimezone(new Date(ngModelCtrl.$viewValue), ngModelOptions.getOption('timezone')) : new Date(0, 0, 0, 0, 0, 0, 0);
dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
dt = dateParser.toTimezone(dt, ngModelOptions.getOption('timezone'));
ngModelCtrl.$setViewValue(dt);
ngModelCtrl.$render();
} else {
self.activeDate = date;
setMode(self.modes[self.modes.indexOf($scope.datepickerMode) - 1]);
$scope.$emit('uib:datepicker.mode');
}
$scope.$broadcast('uib:datepicker.focus');
};
$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;
}
setMode(self.modes[self.modes.indexOf($scope.datepickerMode) + direction]);
$scope.$emit('uib:datepicker.mode');
};
// 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();
}
};
$element.on('keydown', function(evt) {
$scope.$apply(function() {
$scope.keydown(evt);
});
});
$scope.$on('$destroy', function() {
//Clear all watch listeners on destroy
while (watchListeners.length) {
watchListeners.shift()();
}
});
function setMode(mode) {
$scope.datepickerMode = mode;
$scope.datepickerOptions.datepickerMode = mode;
}
function extractOptions(ngModelCtrl) {
var ngModelOptions;
if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing
// guarantee a value
ngModelOptions = ngModelCtrl.$options ||
$scope.datepickerOptions.ngModelOptions ||
datepickerConfig.ngModelOptions ||
{};
// mimic 1.6+ api
ngModelOptions.getOption = function (key) {
return ngModelOptions[key];
};
} else { // in angular >=1.6 $options is always present
// ng-model-options defaults timezone to null; don't let its precedence squash a non-null value
var timezone = ngModelCtrl.$options.getOption('timezone') ||
($scope.datepickerOptions.ngModelOptions ? $scope.datepickerOptions.ngModelOptions.timezone : null) ||
(datepickerConfig.ngModelOptions ? datepickerConfig.ngModelOptions.timezone : null);
// values passed to createChild override existing values
ngModelOptions = ngModelCtrl.$options // start with a ModelOptions instance
.createChild(datepickerConfig.ngModelOptions) // lowest precedence
.createChild($scope.datepickerOptions.ngModelOptions)
.createChild(ngModelCtrl.$options) // highest precedence
.createChild({timezone: timezone}); // to keep from squashing a non-null value
}
return ngModelOptions;
}
}])
.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, this.monthColumns);
scope.yearHeaderColspan = this.monthColumns > 3 ? this.monthColumns - 2 : 1;
};
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 - this.monthColumns;
} else if (key === 'right') {
date = date + 1;
} else if (key === 'down') {
date = date + this.monthColumns;
} 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 columns, range;
this.element = $element;
function getStartingYear(year) {
return parseInt((year - 1) / range, 10) * range + 1;
}
this.yearpickerInit = function() {
columns = this.yearColumns;
range = this.yearRows * columns;
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, columns);
scope.columns = columns;
};
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 - columns;
} else if (key === 'right') {
date = date + 1;
} else if (key === 'down') {
date = date + columns;
} else if (key === 'pageup' || key === 'pagedown') {
date += (key === 'pageup' ? - 1 : 1) * range;
} 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 {
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/datepicker/datepicker.html';
},
scope: {
datepickerOptions: '=?'
},
require: ['uibDatepicker', '^ngModel'],
restrict: 'A',
controller: 'UibDatepickerController',
controllerAs: 'datepicker',
link: function(scope, element, attrs, ctrls) {
var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
datepickerCtrl.init(ngModelCtrl);
}
};
})
.directive('uibDaypicker', function() {
return {
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/datepicker/day.html';
},
require: ['^uibDatepicker', 'uibDaypicker'],
restrict: 'A',
controller: 'UibDaypickerController',
link: function(scope, element, attrs, ctrls) {
var datepickerCtrl = ctrls[0],
daypickerCtrl = ctrls[1];
daypickerCtrl.init(datepickerCtrl);
}
};
})
.directive('uibMonthpicker', function() {
return {
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/datepicker/month.html';
},
require: ['^uibDatepicker', 'uibMonthpicker'],
restrict: 'A',
controller: 'UibMonthpickerController',
link: function(scope, element, attrs, ctrls) {
var datepickerCtrl = ctrls[0],
monthpickerCtrl = ctrls[1];
monthpickerCtrl.init(datepickerCtrl);
}
};
})
.directive('uibYearpicker', function() {
return {
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/datepicker/year.html';
},
require: ['^uibDatepicker', 'uibYearpicker'],
restrict: 'A',
controller: 'UibYearpickerController',
link: function(scope, element, attrs, ctrls) {
var ctrl = ctrls[0];
angular.extend(ctrl, ctrls[1]);
ctrl.yearpickerInit();
ctrl.refreshView();
}
};
});
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;
/**
* scrollbar on body and html element in IE and Edge overlay
* content and should be considered 0 width.
*/
var BODY_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)$/
};
var BODY_REGEX = /(HTML|BODY)/;
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.nodeName ? elem : 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
* In IE and Edge, scollbar on body and html element overlay and should
* return a width of 0.
*
* @returns {number} The width of the browser scollbar.
*/
scrollbarWidth: function(isBody) {
if (isBody) {
if (angular.isUndefined(BODY_SCROLLBAR_WIDTH)) {
var bodyElem = $document.find('body');
bodyElem.addClass('uib-position-body-scrollbar-measure');
BODY_SCROLLBAR_WIDTH = $window.innerWidth - bodyElem[0].clientWidth;
BODY_SCROLLBAR_WIDTH = isFinite(BODY_SCROLLBAR_WIDTH) ? BODY_SCROLLBAR_WIDTH : 0;
bodyElem.removeClass('uib-position-body-scrollbar-measure');
}
return BODY_SCROLLBAR_WIDTH;
}
if (angular.isUndefined(SCROLLBAR_WIDTH)) {
var scrollElem = angular.element('
');
$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 padding required on an element to replace the scrollbar.
*
* @returns {object} An object with the following properties:
*
* **scrollbarWidth**: the width of the scrollbar
* **widthOverflow**: whether the the width is overflowing
* **right**: the amount of right padding on the element needed to replace the scrollbar
* **rightOriginal**: the amount of right padding currently on the element
* **heightOverflow**: whether the the height is overflowing
* **bottom**: the amount of bottom padding on the element needed to replace the scrollbar
* **bottomOriginal**: the amount of bottom padding currently on the element
*
*/
scrollbarPadding: function(elem) {
elem = this.getRawNode(elem);
var elemStyle = $window.getComputedStyle(elem);
var paddingRight = this.parseStyle(elemStyle.paddingRight);
var paddingBottom = this.parseStyle(elemStyle.paddingBottom);
var scrollParent = this.scrollParent(elem, false, true);
var scrollbarWidth = this.scrollbarWidth(BODY_REGEX.test(scrollParent.tagName));
return {
scrollbarWidth: scrollbarWidth,
widthOverflow: scrollParent.scrollWidth > scrollParent.clientWidth,
right: paddingRight + scrollbarWidth,
originalRight: paddingRight,
heightOverflow: scrollParent.scrollHeight > scrollParent.clientHeight,
bottom: paddingBottom + scrollbarWidth,
originalBottom: paddingBottom
};
},
/**
* Checks to see if the element is scrollable.
*
* @param {element} elem - The element to check.
* @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered,
* default is false.
*
* @returns {boolean} Whether the element is scrollable.
*/
isScrollable: function(elem, includeHidden) {
elem = this.getRawNode(elem);
var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal;
var elemStyle = $window.getComputedStyle(elem);
return overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX);
},
/**
* 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.
* @param {boolean=} [includeSelf=false] - Should the element being passed be
* included in the scrollable llokup.
*
* @returns {element} A HTML element.
*/
scrollParent: function(elem, includeHidden, includeSelf) {
elem = this.getRawNode(elem);
var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal;
var documentEl = $document[0].documentElement;
var elemStyle = $window.getComputedStyle(elem);
if (includeSelf && overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX)) {
return 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:
*
* **width**: the width of the element
* **height**: the height of the element
* **top**: distance to top edge of offset parent
* **left**: distance to left edge of offset parent
*
*/
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:
*
* **width**: the width of the element
* **height**: the height of the element
* **top**: distance to top edge of viewport
* **right**: distance to bottom edge of viewport
*
*/
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:
*
* **top**: distance to the top content edge of viewport element
* **bottom**: distance to the bottom content edge of viewport element
* **left**: distance to the left content edge of viewport element
* **right**: distance to the right content edge of viewport element
*
*/
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:
*
* top: element on top, horizontally centered on host element.
* top-left: element on top, left edge aligned with host element left edge.
* top-right: element on top, lerightft edge aligned with host element right edge.
* bottom: element on bottom, horizontally centered on host element.
* bottom-left: element on bottom, left edge aligned with host element left edge.
* bottom-right: element on bottom, right edge aligned with host element right edge.
* left: element on left, vertically centered on host element.
* left-top: element on left, top edge aligned with host element top edge.
* left-bottom: element on left, bottom edge aligned with host element bottom edge.
* right: element on right, vertically centered on host element.
* right-top: element on right, top edge aligned with host element top edge.
* right-bottom: element on right, bottom edge aligned with host element bottom edge.
*
* 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
*
* **[0]**: The primary placement.
* **[1]**: The secondary placement.
* **[2]**: If auto is passed: true, else undefined.
*
*/
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:
*
* top
* top-right
* top-left
* bottom
* bottom-left
* bottom-right
* left
* left-top
* left-bottom
* right
* right-top
* right-bottom
*
* @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:
*
* **top**: Value for targetElem top.
* **left**: Value for targetElem left.
* **placement**: The resolved placement.
*
*/
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, appendToBody);
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 to adjust the top positioning after first
* render to correctly align element to top after content
* rendering causes resized element height
*
* @param {array} placementClasses - The array of strings of classes
* element should have.
* @param {object} containerPosition - The object with container
* position information
* @param {number} initialHeight - The initial height for the elem.
* @param {number} currentHeight - The current height for the elem.
*/
adjustTop: function(placementClasses, containerPosition, initialHeight, currentHeight) {
if (placementClasses.indexOf('top') !== -1 && initialHeight !== currentHeight) {
return {
top: containerPosition.top - currentHeight + 'px'
};
}
},
/**
* 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 innerElem = elem.querySelector('.tooltip-inner, .popover-inner');
if (!innerElem) {
return;
}
var isTooltip = angular.element(innerElem).hasClass('tooltip-inner');
var arrowElem = isTooltip ? elem.querySelector('.tooltip-arrow') : elem.querySelector('.arrow');
if (!arrowElem) {
return;
}
var arrowCss = {
top: '',
bottom: '',
left: '',
right: ''
};
placement = this.parsePlacement(placement);
if (placement[1] === 'center') {
// no adjustment necessary - just reset styles
angular.element(arrowElem).css(arrowCss);
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];
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.datepickerPopup', ['ui.bootstrap.datepicker', 'ui.bootstrap.position'])
.value('$datepickerPopupLiteralWarning', true)
.constant('uibDatepickerPopupConfig', {
altInputFormats: [],
appendToBody: false,
clearText: 'Clear',
closeOnDateSelection: true,
closeText: 'Done',
currentText: 'Today',
datepickerPopup: 'yyyy-MM-dd',
datepickerPopupTemplateUrl: 'uib/template/datepickerPopup/popup.html',
datepickerTemplateUrl: 'uib/template/datepicker/datepicker.html',
html5Types: {
date: 'yyyy-MM-dd',
'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss',
'month': 'yyyy-MM'
},
onOpenFocus: true,
showButtonBar: true,
placement: 'auto bottom-left'
})
.controller('UibDatepickerPopupController', ['$scope', '$element', '$attrs', '$compile', '$log', '$parse', '$window', '$document', '$rootScope', '$uibPosition', 'dateFilter', 'uibDateParser', 'uibDatepickerPopupConfig', '$timeout', 'uibDatepickerConfig', '$datepickerPopupLiteralWarning',
function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout, datepickerConfig, $datepickerPopupLiteralWarning) {
var cache = {},
isHtml5DateInput = false;
var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus,
datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl, scrollParentEl,
ngModel, ngModelOptions, $popup, altInputFormats, watchListeners = [];
this.init = function(_ngModel_) {
ngModel = _ngModel_;
ngModelOptions = extractOptions(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('
');
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 (!$scope.datepickerOptions) {
$scope.datepickerOptions = {};
}
if (isHtml5DateInput) {
if ($attrs.type === 'month') {
$scope.datepickerOptions.datepickerMode = 'month';
$scope.datepickerOptions.minMode = 'month';
}
}
datepickerEl.attr('datepicker-options', 'datepickerOptions');
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) {
if (ngModel.$isEmpty(value)) {
$scope.date = value;
return value;
}
if (angular.isNumber(value)) {
value = new Date(value);
}
$scope.date = dateParser.fromTimezone(value, ngModelOptions.getOption('timezone'));
return dateParser.filter($scope.date, dateFormat);
});
} else {
ngModel.$formatters.push(function(value) {
$scope.date = dateParser.fromTimezone(value, ngModelOptions.getOption('timezone'));
return value;
});
}
// Detect changes in the view from the text box
ngModel.$viewChangeListeners.push(function() {
$scope.date = parseDateString(ngModel.$viewValue);
});
$element.on('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.off('keydown', inputKeydownBind);
$document.off('click', documentClickBind);
if (scrollParentEl) {
scrollParentEl.off('scroll', positionPopup);
}
angular.element($window).off('resize', positionPopup);
//Clear all watch listeners on destroy
while (watchListeners.length) {
watchListeners.shift()();
}
});
};
$scope.getText = function(key) {
return $scope[key + 'Text'] || datepickerPopupConfig[key + 'Text'];
};
$scope.isDisabled = function(date) {
if (date === 'today') {
date = dateParser.fromTimezone(new Date(), ngModelOptions.getOption('timezone'));
}
var dates = {};
angular.forEach(['minDate', 'maxDate'], function(key) {
if (!$scope.datepickerOptions[key]) {
dates[key] = null;
} else if (angular.isDate($scope.datepickerOptions[key])) {
dates[key] = new Date($scope.datepickerOptions[key]);
} else {
if ($datepickerPopupLiteralWarning) {
$log.warn('Literal date support has been deprecated, please switch to date object usage');
}
dates[key] = new Date(dateFilter($scope.datepickerOptions[key], 'medium'));
}
});
return $scope.datepickerOptions &&
dates.minDate && $scope.compare(date, dates.minDate) < 0 ||
dates.maxDate && $scope.compare(date, dates.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) {
$scope.date = dt;
var date = $scope.date ? dateParser.filter($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) {
evt.stopPropagation();
$scope.isOpen = false;
$element[0].focus();
}
};
$scope.select = function(date, evt) {
evt.stopPropagation();
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 = dateParser.fromTimezone(today, ngModelOptions.getOption('timezone'));
date.setHours(0, 0, 0, 0);
}
}
$scope.dateSelection(date);
};
$scope.close = function(evt) {
evt.stopPropagation();
$scope.isOpen = false;
$element[0].focus();
};
$scope.disabled = angular.isDefined($attrs.disabled) || false;
if ($attrs.ngDisabled) {
watchListeners.push($scope.$parent.$watch($parse($attrs.ngDisabled), function(disabled) {
$scope.disabled = disabled;
}));
}
$scope.$watch('isOpen', function(value) {
if (value) {
if (!$scope.disabled) {
$timeout(function() {
positionPopup();
if (onOpenFocus) {
$scope.$broadcast('uib:datepicker.focus');
}
$document.on('click', documentClickBind);
var placement = $attrs.popupPlacement ? $attrs.popupPlacement : datepickerPopupConfig.placement;
if (appendToBody || $position.parsePlacement(placement)[2]) {
scrollParentEl = scrollParentEl || angular.element($position.scrollParent($element));
if (scrollParentEl) {
scrollParentEl.on('scroll', positionPopup);
}
} else {
scrollParentEl = null;
}
angular.element($window).on('resize', positionPopup);
}, 0, false);
} else {
$scope.isOpen = false;
}
} else {
$document.off('click', documentClickBind);
if (scrollParentEl) {
scrollParentEl.off('scroll', positionPopup);
}
angular.element($window).off('resize', positionPopup);
}
});
function cameltoDash(string) {
return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); });
}
function parseDateString(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)) {
return date;
}
}
}
return date;
}
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 = parseDateString(viewValue);
if (!isNaN(date)) {
return dateParser.toTimezone(date, ngModelOptions.getOption('timezone'));
}
}
return ngModelOptions.getOption('allowInvalid') ? viewValue : 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)) {
return !isNaN(parseDateString(value));
}
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;
});
}
}
function positionPopup() {
if ($scope.isOpen) {
var dpElement = angular.element($popup[0].querySelector('.uib-datepicker-popup'));
var placement = $attrs.popupPlacement ? $attrs.popupPlacement : datepickerPopupConfig.placement;
var position = $position.positionElements($element, dpElement, placement, appendToBody);
dpElement.css({top: position.top + 'px', left: position.left + 'px'});
if (dpElement.hasClass('uib-position-measure')) {
dpElement.removeClass('uib-position-measure');
}
}
}
function extractOptions(ngModelCtrl) {
var ngModelOptions;
if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing
// guarantee a value
ngModelOptions = angular.isObject(ngModelCtrl.$options) ?
ngModelCtrl.$options :
{
timezone: null
};
// mimic 1.6+ api
ngModelOptions.getOption = function (key) {
return ngModelOptions[key];
};
} else { // in angular >=1.6 $options is always present
ngModelOptions = ngModelCtrl.$options;
}
return ngModelOptions;
}
$scope.$on('uib:datepicker.mode', function() {
$timeout(positionPopup, 0, false);
});
}])
.directive('uibDatepickerPopup', function() {
return {
require: ['ngModel', 'uibDatepickerPopup'],
controller: 'UibDatepickerPopupController',
scope: {
datepickerOptions: '=?',
isOpen: '=?',
currentText: '@',
clearText: '@',
closeText: '@'
},
link: function(scope, element, attrs, ctrls) {
var ngModel = ctrls[0],
ctrl = ctrls[1];
ctrl.init(ngModel);
}
};
})
.directive('uibDatepickerPopupWrap', function() {
return {
restrict: 'A',
transclude: true,
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/datepickerPopup/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.multiMap', [])
/**
* 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];
}
}
};
}
};
});
angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.multiMap', 'ui.bootstrap.position'])
.constant('uibDropdownConfig', {
appendToOpenClass: 'uib-dropdown-open',
openClass: 'open'
})
.service('uibDropdownService', ['$document', '$rootScope', '$$multiMap', function($document, $rootScope, $$multiMap) {
var openScope = null;
var openedContainers = $$multiMap.createNew();
this.isOnlyOpen = function(dropdownScope, appendTo) {
var openedDropdowns = openedContainers.get(appendTo);
if (openedDropdowns) {
var openDropdown = openedDropdowns.reduce(function(toClose, dropdown) {
if (dropdown.scope === dropdownScope) {
return dropdown;
}
return toClose;
}, {});
if (openDropdown) {
return openedDropdowns.length === 1;
}
}
return false;
};
this.open = function(dropdownScope, element, appendTo) {
if (!openScope) {
$document.on('click', closeDropdown);
}
if (openScope && openScope !== dropdownScope) {
openScope.isOpen = false;
}
openScope = dropdownScope;
if (!appendTo) {
return;
}
var openedDropdowns = openedContainers.get(appendTo);
if (openedDropdowns) {
var openedScopes = openedDropdowns.map(function(dropdown) {
return dropdown.scope;
});
if (openedScopes.indexOf(dropdownScope) === -1) {
openedContainers.put(appendTo, {
scope: dropdownScope
});
}
} else {
openedContainers.put(appendTo, {
scope: dropdownScope
});
}
};
this.close = function(dropdownScope, element, appendTo) {
if (openScope === dropdownScope) {
$document.off('click', closeDropdown);
$document.off('keydown', this.keybindFilter);
openScope = null;
}
if (!appendTo) {
return;
}
var openedDropdowns = openedContainers.get(appendTo);
if (openedDropdowns) {
var dropdownToClose = openedDropdowns.reduce(function(toClose, dropdown) {
if (dropdown.scope === dropdownScope) {
return dropdown;
}
return toClose;
}, {});
if (dropdownToClose) {
openedContainers.remove(appendTo, dropdownToClose);
}
}
};
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 || !openScope.isOpen) { return; }
if (evt && openScope.getAutoClose() === 'disabled') { return; }
if (evt && evt.which === 3) { 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.focusToggleElement();
openScope.isOpen = false;
if (!$rootScope.$$phase) {
openScope.$apply();
}
};
this.keybindFilter = function(evt) {
if (!openScope) {
// see this.close as ESC could have been pressed which kills the scope so we can not proceed
return;
}
var dropdownElement = openScope.getDropdownElement();
var toggleElement = openScope.getToggleElement();
var dropdownElementTargeted = dropdownElement && dropdownElement[0].contains(evt.target);
var toggleElementTargeted = toggleElement && toggleElement[0].contains(evt.target);
if (evt.which === 27) {
evt.stopPropagation();
openScope.focusToggleElement();
closeDropdown();
} else if (openScope.isKeynavEnabled() && [38, 40].indexOf(evt.which) !== -1 && openScope.isOpen && (dropdownElementTargeted || toggleElementTargeted)) {
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,
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;
});
}
keynavEnabled = angular.isDefined($attrs.keyboardNav);
};
this.toggle = function(open) {
scope.isOpen = arguments.length ? !!open : !scope.isOpen;
if (angular.isFunction(setIsOpen)) {
setIsOpen(scope, scope.isOpen);
}
return 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();
}
};
function removeDropdownMenu() {
$element.append(self.dropdownMenu);
}
scope.$watch('isOpen', function(isOpen, wasOpen) {
var appendTo = null,
appendToBody = false;
if (angular.isDefined($attrs.dropdownAppendTo)) {
var appendToEl = $parse($attrs.dropdownAppendTo)(scope);
if (appendToEl) {
appendTo = angular.element(appendToEl);
}
}
if (angular.isDefined($attrs.dropdownAppendToBody)) {
var appendToBodyValue = $parse($attrs.dropdownAppendToBody)(scope);
if (appendToBodyValue !== false) {
appendToBody = true;
}
}
if (appendToBody && !appendTo) {
appendTo = body;
}
if (appendTo && self.dropdownMenu) {
if (isOpen) {
appendTo.append(self.dropdownMenu);
$element.on('$destroy', removeDropdownMenu);
} else {
$element.off('$destroy', removeDropdownMenu);
removeDropdownMenu();
}
}
if (appendTo && self.dropdownMenu) {
var pos = $position.positionElements($element, self.dropdownMenu, 'bottom-left', true),
css,
rightalign,
scrollbarPadding,
scrollbarWidth = 0;
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';
scrollbarPadding = $position.scrollbarPadding(appendTo);
if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) {
scrollbarWidth = scrollbarPadding.scrollbarWidth;
}
css.right = window.innerWidth - scrollbarWidth -
(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;
var dropdownOpenClass = appendTo ? appendToOpenClass : openClass;
var hasOpenClass = openContainer.hasClass(dropdownOpenClass);
var isOnlyOpen = uibDropdownService.isOnlyOpen($scope, appendTo);
if (hasOpenClass === !isOpen) {
var toggleClass;
if (appendTo) {
toggleClass = !isOnlyOpen ? 'addClass' : 'removeClass';
} else {
toggleClass = isOpen ? 'addClass' : 'removeClass';
}
$animate[toggleClass](openContainer, dropdownOpenClass).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;
$document.on('keydown', uibDropdownService.keybindFilter);
});
});
} else {
$document.on('keydown', uibDropdownService.keybindFilter);
}
scope.focusToggleElement();
uibDropdownService.open(scope, $element, appendTo);
} else {
uibDropdownService.close(scope, $element, appendTo);
if (self.dropdownMenuTemplateUrl) {
if (templateScope) {
templateScope.$destroy();
}
var newEl = angular.element('');
self.dropdownMenu.replaceWith(newEl);
self.dropdownMenu = newEl;
}
self.selectedOption = null;
}
if (angular.isFunction(setIsOpen)) {
setIsOpen($scope, isOpen);
}
});
}])
.directive('uibDropdown', function() {
return {
controller: 'UibDropdownController',
link: function(scope, element, attrs, dropdownCtrl) {
dropdownCtrl.init();
}
};
})
.directive('uibDropdownMenu', function() {
return {
restrict: 'A',
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.on('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.off('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.pop();
},
length: function() {
return stack.length;
}
};
}
};
});
angular.module('ui.bootstrap.modal', ['ui.bootstrap.multiMap', 'ui.bootstrap.stackedMap', 'ui.bootstrap.position'])
/**
* Pluggable resolve mechanism for the modal resolve resolution
* Supports UI Router's $resolve service
*/
.provider('$uibResolve', function() {
var resolve = this;
this.resolver = null;
this.setResolver = function(resolver) {
this.resolver = resolver;
};
this.$get = ['$injector', '$q', function($injector, $q) {
var resolver = resolve.resolver ? $injector.get(resolve.resolver) : null;
return {
resolve: function(invocables, locals, parent, self) {
if (resolver) {
return resolver.resolve(invocables, locals, parent, self);
}
var promises = [];
angular.forEach(invocables, function(value) {
if (angular.isFunction(value) || angular.isArray(value)) {
promises.push($q.resolve($injector.invoke(value)));
} else if (angular.isString(value)) {
promises.push($q.resolve($injector.get(value)));
} else {
promises.push($q.resolve(value));
}
});
return $q.all(promises).then(function(resolves) {
var resolveObj = {};
var resolveIter = 0;
angular.forEach(invocables, function(value, key) {
resolveObj[key] = resolves[resolveIter++];
});
return resolveObj;
});
}
};
}];
})
/**
* A helper directive for the $modal service. It creates a backdrop element.
*/
.directive('uibModalBackdrop', ['$animate', '$injector', '$uibModalStack',
function($animate, $injector, $modalStack) {
return {
restrict: 'A',
compile: function(tElement, tAttrs) {
tElement.addClass(tAttrs.backdropClass);
return linkFn;
}
};
function linkFn(scope, element, attrs) {
if (attrs.modalInClass) {
$animate.addClass(element, attrs.modalInClass);
scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {
var done = setIsAsync();
if (scope.modalOptions.animation) {
$animate.removeClass(element, attrs.modalInClass).then(done);
} else {
done();
}
});
}
}
}])
.directive('uibModalWindow', ['$uibModalStack', '$q', '$animateCss', '$document',
function($modalStack, $q, $animateCss, $document) {
return {
scope: {
index: '@'
},
restrict: 'A',
transclude: true,
templateUrl: function(tElement, tAttrs) {
return tAttrs.templateUrl || 'uib/template/modal/window.html';
},
link: function(scope, element, attrs) {
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 rendered.
var modalRenderDeferObj = $q.defer();
// Resolve render promise post-digest
scope.$$postDigest(function() {
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();
$animateCss(element, {
removeClass: attrs.modalInClass
}).start().then(done);
});
}
$q.when(animationPromise).then(function() {
// Notify {@link $modalStack} that modal is rendered.
var modal = $modalStack.getTop();
if (modal) {
$modalStack.modalRendered(modal.key);
}
/**
* If something within the freshly-opened modal already has focus (perhaps via a
* directive that causes focus) then there's no need to try to 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();
}
}
});
});
}
};
}])
.directive('uibModalAnimationClass', function() {
return {
compile: function(tElement, tAttrs) {
if (tAttrs.modalAnimation) {
tElement.addClass(tAttrs.uibModalAnimationClass);
}
}
};
})
.directive('uibModalTransclude', ['$animate', function($animate) {
return {
link: function(scope, element, attrs, controller, transclude) {
transclude(scope.$parent, function(clone) {
element.empty();
$animate.enter(clone, element);
});
}
};
}])
.factory('$uibModalStack', ['$animate', '$animateCss', '$document',
'$compile', '$rootScope', '$q', '$$multiMap', '$$stackedMap', '$uibPosition',
function($animate, $animateCss, $document, $compile, $rootScope, $q, $$multiMap, $$stackedMap, $uibPosition) {
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'
};
var topModalIndex = 0;
var previousTopOpenedModal = null;
var ARIA_HIDDEN_ATTRIBUTE_NAME = 'data-bootstrap-modal-aria-hidden-count';
//Modal focus behavior
var tabbableSelector = 'a[href], area[href], input:not([disabled]):not([tabindex=\'-1\']), ' +
'button:not([disabled]):not([tabindex=\'-1\']),select:not([disabled]):not([tabindex=\'-1\']), textarea:not([disabled]):not([tabindex=\'-1\']), ' +
'iframe, object, embed, *[tabindex]:not([tabindex=\'-1\']), *[contenteditable=true]';
var scrollbarPadding;
var SNAKE_CASE_REGEXP = /[A-Z]/g;
// TODO: extract into common dependency with tooltip
function snake_case(name) {
var separator = '-';
return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
return (pos ? separator : '') + letter.toLowerCase();
});
}
function isVisible(element) {
return !!(element.offsetWidth ||
element.offsetHeight ||
element.getClientRects().length);
}
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;
}
}
// If any backdrop exist, ensure that it's index is always
// right below the top modal
if (topBackdropIndex > -1 && topBackdropIndex < topModalIndex) {
topBackdropIndex = topModalIndex;
}
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);
previousTopOpenedModal = openedWindows.top();
if (previousTopOpenedModal) {
topModalIndex = parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10);
}
removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() {
var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS;
openedClasses.remove(modalBodyClass, modalInstance);
var areAnyOpen = openedClasses.hasKey(modalBodyClass);
appendToElement.toggleClass(modalBodyClass, areAnyOpen);
if (!areAnyOpen && scrollbarPadding && scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) {
if (scrollbarPadding.originalRight) {
appendToElement.css({paddingRight: scrollbarPadding.originalRight + 'px'});
} else {
appendToElement.css({paddingRight: ''});
}
scrollbarPadding = null;
}
toggleTopWindowClass(true);
}, modalWindow.closedDeferred);
checkRemoveBackdrop();
//move focus to specified element if available, or else to body
if (elementToReceiveFocus && elementToReceiveFocus.focus) {
elementToReceiveFocus.focus();
} else if (appendToElement.focus) {
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, closedDeferred) {
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;
$animate.leave(domEl).then(function() {
if (done) {
done();
}
domEl.remove();
if (closedDeferred) {
closedDeferred.resolve();
}
});
scope.$destroy();
}
}
$document.on('keydown', keydownListener);
$rootScope.$on('$destroy', function() {
$document.off('keydown', keydownListener);
});
function keydownListener(evt) {
if (evt.isDefaultPrevented()) {
return evt;
}
var modal = openedWindows.top();
if (modal) {
switch (evt.which) {
case 27: {
if (modal.value.keyboard) {
evt.preventDefault();
$rootScope.$apply(function() {
$modalStack.dismiss(modal.key, 'escape key press');
});
}
break;
}
case 9: {
var list = $modalStack.loadFocusElementList(modal);
var focusChanged = false;
if (evt.shiftKey) {
if ($modalStack.isFocusInFirstItem(evt, list) || $modalStack.isModalFocused(evt, modal)) {
focusChanged = $modalStack.focusLastFocusableElement(list);
}
} else {
if ($modalStack.isFocusInLastItem(evt, list)) {
focusChanged = $modalStack.focusFirstFocusableElement(list);
}
}
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);
// Store the current top first, to determine what index we ought to use
// for the current top modal
previousTopOpenedModal = openedWindows.top();
openedWindows.add(modalInstance, {
deferred: modal.deferred,
renderDeferred: modal.renderDeferred,
closedDeferred: modal.closedDeferred,
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 (currBackdropIndex >= 0 && !backdropDomEl) {
backdropScope = $rootScope.$new(true);
backdropScope.modalOptions = modal;
backdropScope.index = currBackdropIndex;
backdropDomEl = angular.element('
');
backdropDomEl.attr({
'class': 'modal-backdrop',
'ng-style': '{\'z-index\': 1040 + (index && 1 || 0) + index*10}',
'uib-modal-animation-class': 'fade',
'modal-in-class': 'in'
});
if (modal.backdropClass) {
backdropDomEl.addClass(modal.backdropClass);
}
if (modal.animation) {
backdropDomEl.attr('modal-animation', 'true');
}
$compile(backdropDomEl)(backdropScope);
$animate.enter(backdropDomEl, appendToElement);
if ($uibPosition.isScrollable(appendToElement)) {
scrollbarPadding = $uibPosition.scrollbarPadding(appendToElement);
if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) {
appendToElement.css({paddingRight: scrollbarPadding.right + 'px'});
}
}
}
var content;
if (modal.component) {
content = document.createElement(snake_case(modal.component.name));
content = angular.element(content);
content.attr({
resolve: '$resolve',
'modal-instance': '$uibModalInstance',
close: '$close($value)',
dismiss: '$dismiss($value)'
});
} else {
content = modal.content;
}
// Set the top modal index based on the index of the previous top modal
topModalIndex = previousTopOpenedModal ? parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10) + 1 : 0;
var angularDomEl = angular.element('
');
angularDomEl.attr({
'class': 'modal',
'template-url': modal.windowTemplateUrl,
'window-top-class': modal.windowTopClass,
'role': 'dialog',
'aria-labelledby': modal.ariaLabelledBy,
'aria-describedby': modal.ariaDescribedBy,
'size': modal.size,
'index': topModalIndex,
'animate': 'animate',
'ng-style': '{\'z-index\': 1050 + $$topModalIndex*10, display: \'block\'}',
'tabindex': -1,
'uib-modal-animation-class': 'fade',
'modal-in-class': 'in'
}).append(content);
if (modal.windowClass) {
angularDomEl.addClass(modal.windowClass);
}
if (modal.animation) {
angularDomEl.attr('modal-animation', 'true');
}
appendToElement.addClass(modalBodyClass);
if (modal.scope) {
// we need to explicitly add the modal index to the modal scope
// because it is needed by ngStyle to compute the zIndex property.
modal.scope.$$topModalIndex = topModalIndex;
}
$animate.enter($compile(angularDomEl)(modal.scope), appendToElement);
openedWindows.top().value.modalDomEl = angularDomEl;
openedWindows.top().value.modalOpener = modalOpener;
applyAriaHidden(angularDomEl);
function applyAriaHidden(el) {
if (!el || el[0].tagName === 'BODY') {
return;
}
getSiblings(el).forEach(function(sibling) {
var elemIsAlreadyHidden = sibling.getAttribute('aria-hidden') === 'true',
ariaHiddenCount = parseInt(sibling.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10);
if (!ariaHiddenCount) {
ariaHiddenCount = elemIsAlreadyHidden ? 1 : 0;
}
sibling.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, ariaHiddenCount + 1);
sibling.setAttribute('aria-hidden', 'true');
});
return applyAriaHidden(el.parent());
function getSiblings(el) {
var children = el.parent() ? el.parent().children() : [];
return Array.prototype.filter.call(children, function(child) {
return child !== el[0];
});
}
}
};
function broadcastClosing(modalWindow, resultOrReason, closing) {
return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented;
}
function unhideBackgroundElements() {
Array.prototype.forEach.call(
document.querySelectorAll('[' + ARIA_HIDDEN_ATTRIBUTE_NAME + ']'),
function(hiddenEl) {
var ariaHiddenCount = parseInt(hiddenEl.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10),
newHiddenCount = ariaHiddenCount - 1;
hiddenEl.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, newHiddenCount);
if (!newHiddenCount) {
hiddenEl.removeAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME);
hiddenEl.removeAttribute('aria-hidden');
}
}
);
}
$modalStack.close = function(modalInstance, result) {
var modalWindow = openedWindows.get(modalInstance);
unhideBackgroundElements();
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);
unhideBackgroundElements();
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(list) {
if (list.length > 0) {
list[0].focus();
return true;
}
return false;
};
$modalStack.focusLastFocusableElement = function(list) {
if (list.length > 0) {
list[list.length - 1].focus();
return true;
}
return false;
};
$modalStack.isModalFocused = function(evt, modalWindow) {
if (evt && modalWindow) {
var modalDomEl = modalWindow.value.modalDomEl;
if (modalDomEl && modalDomEl.length) {
return (evt.target || evt.srcElement) === modalDomEl[0];
}
}
return false;
};
$modalStack.isFocusInFirstItem = function(evt, list) {
if (list.length > 0) {
return (evt.target || evt.srcElement) === list[0];
}
return false;
};
$modalStack.isFocusInLastItem = function(evt, list) {
if (list.length > 0) {
return (evt.target || evt.srcElement) === list[list.length - 1];
}
return false;
};
$modalStack.loadFocusElementList = function(modalWindow) {
if (modalWindow) {
var modalDomE1 = modalWindow.value.modalDomEl;
if (modalDomE1 && modalDomE1.length) {
var elements = modalDomE1[0].querySelectorAll(tabbableSelector);
return elements ?
Array.prototype.filter.call(elements, function(element) {
return isVisible(element);
}) : elements;
}
}
};
return $modalStack;
}])
.provider('$uibModal', function() {
var $modalProvider = {
options: {
animation: true,
backdrop: true, //can also be false or 'static'
keyboard: true
},
$get: ['$rootScope', '$q', '$document', '$templateRequest', '$controller', '$uibResolve', '$uibModalStack',
function ($rootScope, $q, $document, $templateRequest, $controller, $uibResolve, $modalStack) {
var $modal = {};
function getTemplatePromise(options) {
return options.template ? $q.when(options.template) :
$templateRequest(angular.isFunction(options.templateUrl) ?
options.templateUrl() : options.templateUrl);
}
var promiseChain = null;
$modal.getPromiseChain = function() {
return promiseChain;
};
$modal.open = function(modalOptions) {
var modalResultDeferred = $q.defer();
var modalOpenedDeferred = $q.defer();
var modalClosedDeferred = $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,
closed: modalClosedDeferred.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);
if (!modalOptions.appendTo.length) {
throw new Error('appendTo element not found. Make sure that the element passed is in DOM.');
}
//verify options
if (!modalOptions.component && !modalOptions.template && !modalOptions.templateUrl) {
throw new Error('One of component or template or templateUrl options is required.');
}
var templateAndResolvePromise;
if (modalOptions.component) {
templateAndResolvePromise = $q.when($uibResolve.resolve(modalOptions.resolve, {}, null, null));
} else {
templateAndResolvePromise =
$q.all([getTemplatePromise(modalOptions), $uibResolve.resolve(modalOptions.resolve, {}, null, null)]);
}
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 providedScope = modalOptions.scope || $rootScope;
var modalScope = providedScope.$new();
modalScope.$close = modalInstance.close;
modalScope.$dismiss = modalInstance.dismiss;
modalScope.$on('$destroy', function() {
if (!modalScope.$$uibDestructionScheduled) {
modalScope.$dismiss('$uibUnscheduledDestruction');
}
});
var modal = {
scope: modalScope,
deferred: modalResultDeferred,
renderDeferred: modalRenderDeferred,
closedDeferred: modalClosedDeferred,
animation: modalOptions.animation,
backdrop: modalOptions.backdrop,
keyboard: modalOptions.keyboard,
backdropClass: modalOptions.backdropClass,
windowTopClass: modalOptions.windowTopClass,
windowClass: modalOptions.windowClass,
windowTemplateUrl: modalOptions.windowTemplateUrl,
ariaLabelledBy: modalOptions.ariaLabelledBy,
ariaDescribedBy: modalOptions.ariaDescribedBy,
size: modalOptions.size,
openedClass: modalOptions.openedClass,
appendTo: modalOptions.appendTo
};
var component = {};
var ctrlInstance, ctrlInstantiate, ctrlLocals = {};
if (modalOptions.component) {
constructLocals(component, false, true, false);
component.name = modalOptions.component;
modal.component = component;
} else if (modalOptions.controller) {
constructLocals(ctrlLocals, true, false, true);
// the third param will make the controller instantiate later,private api
// @see https://github.com/angular/angular.js/blob/master/src/ng/controller.js#L126
ctrlInstantiate = $controller(modalOptions.controller, ctrlLocals, true, modalOptions.controllerAs);
if (modalOptions.controllerAs && modalOptions.bindToController) {
ctrlInstance = ctrlInstantiate.instance;
ctrlInstance.$close = modalScope.$close;
ctrlInstance.$dismiss = modalScope.$dismiss;
angular.extend(ctrlInstance, {
$resolve: ctrlLocals.$scope.$resolve
}, providedScope);
}
ctrlInstance = ctrlInstantiate();
if (angular.isFunction(ctrlInstance.$onInit)) {
ctrlInstance.$onInit();
}
}
if (!modalOptions.component) {
modal.content = tplAndVars[0];
}
$modalStack.open(modalInstance, modal);
modalOpenedDeferred.resolve(true);
function constructLocals(obj, template, instanceOnScope, injectable) {
obj.$scope = modalScope;
obj.$scope.$resolve = {};
if (instanceOnScope) {
obj.$scope.$uibModalInstance = modalInstance;
} else {
obj.$uibModalInstance = modalInstance;
}
var resolves = template ? tplAndVars[1] : tplAndVars;
angular.forEach(resolves, function(value, key) {
if (injectable) {
obj[key] = value;
}
obj.$scope.$resolve[key] = value;
});
}
}, 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.paging', [])
/**
* Helper internal service for generating common controller code between the
* pager and pagination components
*/
.factory('uibPaging', ['$parse', function($parse) {
return {
create: function(ctrl, $scope, $attrs) {
ctrl.setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop;
ctrl.ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl
ctrl._watchers = [];
ctrl.init = function(ngModelCtrl, config) {
ctrl.ngModelCtrl = ngModelCtrl;
ctrl.config = config;
ngModelCtrl.$render = function() {
ctrl.render();
};
if ($attrs.itemsPerPage) {
ctrl._watchers.push($scope.$parent.$watch($attrs.itemsPerPage, function(value) {
ctrl.itemsPerPage = parseInt(value, 10);
$scope.totalPages = ctrl.calculateTotalPages();
ctrl.updatePage();
}));
} else {
ctrl.itemsPerPage = config.itemsPerPage;
}
$scope.$watch('totalItems', function(newTotal, oldTotal) {
if (angular.isDefined(newTotal) || newTotal !== oldTotal) {
$scope.totalPages = ctrl.calculateTotalPages();
ctrl.updatePage();
}
});
};
ctrl.calculateTotalPages = function() {
var totalPages = ctrl.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / ctrl.itemsPerPage);
return Math.max(totalPages || 0, 1);
};
ctrl.render = function() {
$scope.page = parseInt(ctrl.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();
}
ctrl.ngModelCtrl.$setViewValue(page);
ctrl.ngModelCtrl.$render();
}
};
$scope.getText = function(key) {
return $scope[key + 'Text'] || ctrl.config[key + 'Text'];
};
$scope.noPrevious = function() {
return $scope.page === 1;
};
$scope.noNext = function() {
return $scope.page === $scope.totalPages;
};
ctrl.updatePage = function() {
ctrl.setNumPages($scope.$parent, $scope.totalPages); // Readonly variable
if ($scope.page > $scope.totalPages) {
$scope.selectPage($scope.totalPages);
} else {
ctrl.ngModelCtrl.$render();
}
};
$scope.$on('$destroy', function() {
while (ctrl._watchers.length) {
ctrl._watchers.shift()();
}
});
}
};
}]);
angular.module('ui.bootstrap.pager', ['ui.bootstrap.paging', 'ui.bootstrap.tabindex'])
.controller('UibPagerController', ['$scope', '$attrs', 'uibPaging', 'uibPagerConfig', function($scope, $attrs, uibPaging, uibPagerConfig) {
$scope.align = angular.isDefined($attrs.align) ? $scope.$parent.$eval($attrs.align) : uibPagerConfig.align;
uibPaging.create(this, $scope, $attrs);
}])
.constant('uibPagerConfig', {
itemsPerPage: 10,
previousText: '« Previous',
nextText: 'Next »',
align: true
})
.directive('uibPager', ['uibPagerConfig', function(uibPagerConfig) {
return {
scope: {
totalItems: '=',
previousText: '@',
nextText: '@',
ngDisabled: '='
},
require: ['uibPager', '?ngModel'],
restrict: 'A',
controller: 'UibPagerController',
controllerAs: 'pager',
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/pager/pager.html';
},
link: function(scope, element, attrs, ctrls) {
element.addClass('pager');
var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
if (!ngModelCtrl) {
return; // do nothing if no ng-model
}
paginationCtrl.init(ngModelCtrl, uibPagerConfig);
}
};
}]);
angular.module('ui.bootstrap.pagination', ['ui.bootstrap.paging', 'ui.bootstrap.tabindex'])
.controller('UibPaginationController', ['$scope', '$attrs', '$parse', 'uibPaging', 'uibPaginationConfig', function($scope, $attrs, $parse, uibPaging, uibPaginationConfig) {
var ctrl = this;
// Setup configuration parameters
var maxSize = angular.isDefined($attrs.maxSize) ? $scope.$parent.$eval($attrs.maxSize) : uibPaginationConfig.maxSize,
rotate = angular.isDefined($attrs.rotate) ? $scope.$parent.$eval($attrs.rotate) : uibPaginationConfig.rotate,
forceEllipses = angular.isDefined($attrs.forceEllipses) ? $scope.$parent.$eval($attrs.forceEllipses) : uibPaginationConfig.forceEllipses,
boundaryLinkNumbers = angular.isDefined($attrs.boundaryLinkNumbers) ? $scope.$parent.$eval($attrs.boundaryLinkNumbers) : uibPaginationConfig.boundaryLinkNumbers,
pageLabel = angular.isDefined($attrs.pageLabel) ? function(idx) { return $scope.$parent.$eval($attrs.pageLabel, {$page: idx}); } : angular.identity;
$scope.boundaryLinks = angular.isDefined($attrs.boundaryLinks) ? $scope.$parent.$eval($attrs.boundaryLinks) : uibPaginationConfig.boundaryLinks;
$scope.directionLinks = angular.isDefined($attrs.directionLinks) ? $scope.$parent.$eval($attrs.directionLinks) : uibPaginationConfig.directionLinks;
$attrs.$set('role', 'menu');
uibPaging.create(this, $scope, $attrs);
if ($attrs.maxSize) {
ctrl._watchers.push($scope.$parent.$watch($parse($attrs.maxSize), function(value) {
maxSize = parseInt(value, 10);
ctrl.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, pageLabel(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 = this.render;
this.render = function() {
originalRender();
if ($scope.page > 0 && $scope.page <= $scope.totalPages) {
$scope.pages = getPages($scope.page, $scope.totalPages);
}
};
}])
.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, uibPaginationConfig) {
return {
scope: {
totalItems: '=',
firstText: '@',
previousText: '@',
nextText: '@',
lastText: '@',
ngDisabled:'='
},
require: ['uibPagination', '?ngModel'],
restrict: 'A',
controller: 'UibPaginationController',
controllerAs: 'pagination',
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/pagination/pagination.html';
},
link: function(scope, element, attrs, ctrls) {
element.addClass('pagination');
var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
if (!ngModelCtrl) {
return; // do nothing if no ng-model
}
paginationCtrl.init(ngModelCtrl, uibPaginationConfig);
}
};
}]);
/**
* 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('keyup', keypressListener);
$rootScope.$on('$destroy', function() {
$document.off('keyup', keypressListener);
});
function keypressListener(e) {
if (e.which === 27) {
var last = openedTooltips.top();
if (last) {
last.value.close();
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 =
'
' +
'
';
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 adjustmentTimeout;
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 lastPlacement;
var positionTooltip = function() {
// check if tooltip exists and is not empty
if (!tooltip || !tooltip.html()) { return; }
if (!positionTimeout) {
positionTimeout = $timeout(function() {
var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody);
var initialHeight = angular.isDefined(tooltip.offsetHeight) ? tooltip.offsetHeight : tooltip.prop('offsetHeight');
var elementPos = appendToBody ? $position.offset(element) : $position.position(element);
tooltip.css({ top: ttPosition.top + 'px', left: ttPosition.left + 'px' });
var placementClasses = ttPosition.placement.split('-');
if (!tooltip.hasClass(placementClasses[0])) {
tooltip.removeClass(lastPlacement.split('-')[0]);
tooltip.addClass(placementClasses[0]);
}
if (!tooltip.hasClass(options.placementClassPrefix + ttPosition.placement)) {
tooltip.removeClass(options.placementClassPrefix + lastPlacement);
tooltip.addClass(options.placementClassPrefix + ttPosition.placement);
}
adjustmentTimeout = $timeout(function() {
var currentHeight = angular.isDefined(tooltip.offsetHeight) ? tooltip.offsetHeight : tooltip.prop('offsetHeight');
var adjustment = $position.adjustTop(placementClasses, elementPos, initialHeight, currentHeight);
if (adjustment) {
tooltip.css(adjustment);
}
adjustmentTimeout = null;
}, 0, false);
// first time through tt element will have the
// uib-position-measure class or if the placement
// has changed we need to position the arrow.
if (tooltip.hasClass('uib-position-measure')) {
$position.positionArrow(tooltip, ttPosition.placement);
tooltip.removeClass('uib-position-measure');
} else if (lastPlacement !== ttPosition.placement) {
$position.positionArrow(tooltip, ttPosition.placement);
}
lastPlacement = 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;
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() {
if (ttScope) {
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);
}
});
openedTooltips.add(ttScope, {
close: hide
});
prepObservers();
}
function removeTooltip() {
cancelShow();
cancelHide();
unregisterObservers();
if (tooltip) {
tooltip.remove();
tooltip = null;
if (adjustmentTimeout) {
$timeout.cancel(adjustmentTimeout);
}
}
openedTooltips.remove(ttScope);
if (tooltipLinkedScope) {
tooltipLinkedScope.$destroy();
tooltipLinkedScope = null;
}
}
/**
* Set the initial scope values. Once
* the tooltip is created, the observers
* will be added to keep things in sync.
*/
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 placement = $position.parsePlacement(ttScope.placement);
lastPlacement = placement[1] ? placement[0] + '-' + placement[1] : placement[0];
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();
}
}
// KeyboardEvent handler to hide the tooltip on Escape key press
function hideOnEscapeKey(e) {
if (e.which === 27) {
hideTooltipBind();
}
}
var unregisterTriggers = function() {
triggers.show.forEach(function(trigger) {
if (trigger === 'outsideClick') {
element.off('click', toggleTooltipBind);
} else {
element.off(trigger, showTooltipBind);
element.off(trigger, toggleTooltipBind);
}
element.off('keypress', hideOnEscapeKey);
});
triggers.hide.forEach(function(trigger) {
if (trigger === 'outsideClick') {
$document.off('click', bodyHideTooltipBind);
} else {
element.off(trigger, hideTooltipBind);
}
});
};
function prepTriggers() {
var showTriggers = [], hideTriggers = [];
var val = scope.$eval(attrs[prefix + 'Trigger']);
unregisterTriggers();
if (angular.isObject(val)) {
Object.keys(val).forEach(function(key) {
showTriggers.push(key);
hideTriggers.push(val[key]);
});
triggers = {
show: showTriggers,
hide: hideTriggers
};
} else {
triggers = getTriggers(val);
}
if (triggers.show !== 'none') {
triggers.show.forEach(function(trigger, idx) {
if (trigger === 'outsideClick') {
element.on('click', toggleTooltipBind);
$document.on('click', bodyHideTooltipBind);
} else if (trigger === triggers.hide[idx]) {
element.on(trigger, toggleTooltipBind);
} else if (trigger) {
element.on(trigger, showTooltipBind);
element.on(triggers.hide[idx], hideTooltipBind);
}
element.on('keypress', hideOnEscapeKey);
});
}
}
prepTriggers();
var animation = scope.$eval(attrs[prefix + 'Animation']);
ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation;
var appendToBodyVal;
var appendKey = prefix + 'AppendToBody';
if (appendKey in attrs && attrs[appendKey] === undefined) {
appendToBodyVal = true;
} else {
appendToBodyVal = scope.$eval(attrs[appendKey]);
}
appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody;
// Make sure tooltip is destroyed and removed.
scope.$on('$destroy', function onDestroyTooltip() {
unregisterTriggers();
removeTooltip();
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]);
}
if (scope.popupClass) {
element.addClass(scope.popupClass);
}
if (scope.animation) {
element.addClass(attrs.tooltipAnimationClass);
}
}
};
}])
.directive('uibTooltipPopup', function() {
return {
restrict: 'A',
scope: { content: '@' },
templateUrl: 'uib/template/tooltip/tooltip-popup.html'
};
})
.directive('uibTooltip', [ '$uibTooltip', function($uibTooltip) {
return $uibTooltip('uibTooltip', 'tooltip', 'mouseenter');
}])
.directive('uibTooltipTemplatePopup', function() {
return {
restrict: 'A',
scope: { contentExp: '&', 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 {
restrict: 'A',
scope: { contentExp: '&' },
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 {
restrict: 'A',
scope: { uibTitle: '@', contentExp: '&', originScope: '&' },
templateUrl: 'uib/template/popover/popover-template.html'
};
})
.directive('uibPopoverTemplate', ['$uibTooltip', function($uibTooltip) {
return $uibTooltip('uibPopoverTemplate', 'popover', 'click', {
useContentExp: true
});
}])
.directive('uibPopoverHtmlPopup', function() {
return {
restrict: 'A',
scope: { contentExp: '&', uibTitle: '@' },
templateUrl: 'uib/template/popover/popover-html.html'
};
})
.directive('uibPopoverHtml', ['$uibTooltip', function($uibTooltip) {
return $uibTooltip('uibPopoverHtml', 'popover', 'click', {
useContentExp: true
});
}])
.directive('uibPopoverPopup', function() {
return {
restrict: 'A',
scope: { uibTitle: '@', content: '@' },
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 = getMaxOrDefault();
this.addBar = function(bar, element, attrs) {
if (!animate) {
element.css({'transition': 'none'});
}
this.bars.push(bar);
bar.max = getMaxOrDefault();
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();
});
};
//$attrs.$observe('maxParam', function(maxParam) {
$scope.$watch('maxParam', function(maxParam) {
self.bars.forEach(function(bar) {
bar.max = getMaxOrDefault();
bar.recalculatePercentage();
});
});
function getMaxOrDefault () {
return angular.isDefined($scope.maxParam) ? $scope.maxParam : progressConfig.max;
}
}])
.directive('uibProgress', function() {
return {
replace: true,
transclude: true,
controller: 'UibProgressController',
require: 'uibProgress',
scope: {
maxParam: '=?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: '=',
maxParam: '=?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,
enableReset: true,
titles: ['one', 'two', 'three', 'four', 'five']
})
.controller('UibRatingController', ['$scope', '$attrs', 'uibRatingConfig', function($scope, $attrs, ratingConfig) {
var ngModelCtrl = { $setViewValue: angular.noop },
self = this;
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;
this.enableReset = angular.isDefined($attrs.enableReset) ?
$scope.$parent.$eval($attrs.enableReset) : ratingConfig.enableReset;
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) {
var newViewValue = self.enableReset && ngModelCtrl.$viewValue === value ? 0 : value;
ngModelCtrl.$setViewValue(newViewValue);
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;
$scope.title = self.getTitle($scope.value - 1);
};
}])
.directive('uibRating', function() {
return {
require: ['uibRating', 'ngModel'],
restrict: 'A',
scope: {
readonly: '=?readOnly',
onHover: '&',
onLeave: '&'
},
controller: 'UibRatingController',
templateUrl: 'uib/template/rating/rating.html',
link: function(scope, element, attrs, ctrls) {
var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1];
ratingCtrl.init(ngModelCtrl);
}
};
});
angular.module('ui.bootstrap.tabs', [])
.controller('UibTabsetController', ['$scope', function ($scope) {
var ctrl = this,
oldIndex;
ctrl.tabs = [];
ctrl.select = function(index, evt) {
if (!destroyed) {
var previousIndex = findTabIndex(oldIndex);
var previousSelected = ctrl.tabs[previousIndex];
if (previousSelected) {
previousSelected.tab.onDeselect({
$event: evt,
$selectedIndex: index
});
if (evt && evt.isDefaultPrevented()) {
return;
}
previousSelected.tab.active = false;
}
var selected = ctrl.tabs[index];
if (selected) {
selected.tab.onSelect({
$event: evt
});
selected.tab.active = true;
ctrl.active = selected.index;
oldIndex = selected.index;
} else if (!selected && angular.isDefined(oldIndex)) {
ctrl.active = null;
oldIndex = null;
}
}
};
ctrl.addTab = function addTab(tab) {
ctrl.tabs.push({
tab: tab,
index: tab.index
});
ctrl.tabs.sort(function(t1, t2) {
if (t1.index > t2.index) {
return 1;
}
if (t1.index < t2.index) {
return -1;
}
return 0;
});
if (tab.index === ctrl.active || !angular.isDefined(ctrl.active) && ctrl.tabs.length === 1) {
var newActiveIndex = findTabIndex(tab.index);
ctrl.select(newActiveIndex);
}
};
ctrl.removeTab = function removeTab(tab) {
var index;
for (var i = 0; i < ctrl.tabs.length; i++) {
if (ctrl.tabs[i].tab === tab) {
index = i;
break;
}
}
if (ctrl.tabs[index].index === ctrl.active) {
var newActiveTabIndex = index === ctrl.tabs.length - 1 ?
index - 1 : index + 1 % ctrl.tabs.length;
ctrl.select(newActiveTabIndex);
}
ctrl.tabs.splice(index, 1);
};
$scope.$watch('tabset.active', function(val) {
if (angular.isDefined(val) && val !== oldIndex) {
ctrl.select(findTabIndex(val));
}
});
var destroyed;
$scope.$on('$destroy', function() {
destroyed = true;
});
function findTabIndex(index) {
for (var i = 0; i < ctrl.tabs.length; i++) {
if (ctrl.tabs[i].index === index) {
return i;
}
}
}
}])
.directive('uibTabset', function() {
return {
transclude: true,
replace: true,
scope: {},
bindToController: {
active: '=?',
type: '@'
},
controller: 'UibTabsetController',
controllerAs: 'tabset',
templateUrl: function(element, attrs) {
return attrs.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;
}
};
})
.directive('uibTab', ['$parse', function($parse) {
return {
require: '^uibTabset',
replace: true,
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/tabs/tab.html';
},
transclude: true,
scope: {
heading: '@',
index: '=?',
classes: '@?',
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
},
controllerAs: 'tab',
link: function(scope, elm, attrs, tabsetCtrl, transclude) {
scope.disabled = false;
if (attrs.disable) {
scope.$parent.$watch($parse(attrs.disable), function(value) {
scope.disabled = !! value;
});
}
if (angular.isUndefined(attrs.index)) {
if (tabsetCtrl.tabs && tabsetCtrl.tabs.length) {
scope.index = Math.max.apply(null, tabsetCtrl.tabs.map(function(t) { return t.index; })) + 1;
} else {
scope.index = 0;
}
}
if (angular.isUndefined(attrs.classes)) {
scope.classes = '';
}
scope.select = function(evt) {
if (!scope.disabled) {
var index;
for (var i = 0; i < tabsetCtrl.tabs.length; i++) {
if (tabsetCtrl.tabs[i].tab === scope) {
index = i;
break;
}
}
tabsetCtrl.select(index, evt);
}
};
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).tab;
//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' ||
node.tagName.toLowerCase() === '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,
templateUrl: 'uib/template/timepicker/timepicker.html'
})
.controller('UibTimepickerController', ['$scope', '$element', '$attrs', '$parse', '$log', '$locale', 'uibTimepickerConfig', function($scope, $element, $attrs, $parse, $log, $locale, timepickerConfig) {
var hoursModelCtrl, minutesModelCtrl, secondsModelCtrl;
var selected = new Date(),
watchers = [],
ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl
meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS,
padHours = angular.isDefined($attrs.padHours) ? $scope.$parent.$eval($attrs.padHours) : true;
$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);
hoursModelCtrl = hoursInputEl.controller('ngModel');
minutesModelCtrl = minutesInputEl.controller('ngModel');
secondsModelCtrl = secondsInputEl.controller('ngModel');
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) {
watchers.push($scope.$parent.$watch($parse($attrs.hourStep), function(value) {
hourStep = +value;
}));
}
var minuteStep = timepickerConfig.minuteStep;
if ($attrs.minuteStep) {
watchers.push($scope.$parent.$watch($parse($attrs.minuteStep), function(value) {
minuteStep = +value;
}));
}
var min;
watchers.push($scope.$parent.$watch($parse($attrs.min), function(value) {
var dt = new Date(value);
min = isNaN(dt) ? undefined : dt;
}));
var max;
watchers.push($scope.$parent.$watch($parse($attrs.max), function(value) {
var dt = new Date(value);
max = isNaN(dt) ? undefined : dt;
}));
var disabled = false;
if ($attrs.ngDisabled) {
watchers.push($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) {
watchers.push($scope.$parent.$watch($parse($attrs.secondStep), function(value) {
secondStep = +value;
}));
}
$scope.showSeconds = timepickerConfig.showSeconds;
if ($attrs.showSeconds) {
watchers.push($scope.$parent.$watch($parse($attrs.showSeconds), function(value) {
$scope.showSeconds = !!value;
}));
}
// 12H / 24H mode
$scope.showMeridian = timepickerConfig.showMeridian;
if ($attrs.showMeridian) {
watchers.push($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 = +$scope.hours;
var valid = $scope.showMeridian ? hours > 0 && hours < 13 :
hours >= 0 && hours < 24;
if (!valid || $scope.hours === '') {
return undefined;
}
if ($scope.showMeridian) {
if (hours === 12) {
hours = 0;
}
if ($scope.meridian === meridians[1]) {
hours = hours + 12;
}
}
return hours;
}
function getMinutesFromTemplate() {
var minutes = +$scope.minutes;
var valid = minutes >= 0 && minutes < 60;
if (!valid || $scope.minutes === '') {
return undefined;
}
return minutes;
}
function getSecondsFromTemplate() {
var seconds = +$scope.seconds;
return seconds >= 0 && seconds < 60 ? seconds : undefined;
}
function pad(value, noPad) {
if (value === null) {
return '';
}
return angular.isDefined(value) && value.toString().length < 2 && !noPad ?
'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.on('mousewheel wheel', function(e) {
if (!disabled) {
$scope.$apply(isScrollingUp(e) ? $scope.incrementHours() : $scope.decrementHours());
}
e.preventDefault();
});
minutesInputEl.on('mousewheel wheel', function(e) {
if (!disabled) {
$scope.$apply(isScrollingUp(e) ? $scope.incrementMinutes() : $scope.decrementMinutes());
}
e.preventDefault();
});
secondsInputEl.on('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.on('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.on('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.on('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 (hoursModelCtrl) {
hoursModelCtrl.$setValidity('hours', false);
}
}
if (angular.isDefined(invalidMinutes)) {
$scope.invalidMinutes = invalidMinutes;
if (minutesModelCtrl) {
minutesModelCtrl.$setValidity('minutes', false);
}
}
if (angular.isDefined(invalidSeconds)) {
$scope.invalidSeconds = invalidSeconds;
if (secondsModelCtrl) {
secondsModelCtrl.$setValidity('seconds', false);
}
}
};
$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.on('blur', function(e) {
ngModelCtrl.$setTouched();
if (modelIsEmpty()) {
makeValid();
} else if ($scope.hours === null || $scope.hours === '') {
invalidate(true);
} else if (!$scope.invalidHours && $scope.hours < 10) {
$scope.$apply(function() {
$scope.hours = pad($scope.hours, !padHours);
});
}
});
$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.on('blur', function(e) {
ngModelCtrl.$setTouched();
if (modelIsEmpty()) {
makeValid();
} else 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.on('blur', function(e) {
if (modelIsEmpty()) {
makeValid();
} else 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() {
if (hoursModelCtrl) {
hoursModelCtrl.$setValidity('hours', true);
}
if (minutesModelCtrl) {
minutesModelCtrl.$setValidity('minutes', true);
}
if (secondsModelCtrl) {
secondsModelCtrl.$setValidity('seconds', true);
}
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, !padHours);
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;
}
function modelIsEmpty() {
return ($scope.hours === null || $scope.hours === '') &&
($scope.minutes === null || $scope.minutes === '') &&
(!$scope.showSeconds || $scope.showSeconds && ($scope.seconds === null || $scope.seconds === ''));
}
$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();
};
$scope.$on('$destroy', function() {
while (watchers.length) {
watchers.shift()();
}
});
}])
.directive('uibTimepicker', ['uibTimepickerConfig', function(uibTimepickerConfig) {
return {
require: ['uibTimepicker', '?^ngModel'],
restrict: 'A',
controller: 'UibTimepickerController',
controllerAs: 'timepicker',
scope: {},
templateUrl: function(element, attrs) {
return attrs.templateUrl || uibTimepickerConfig.templateUrl;
},
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) {
// 000001111111100000000000002222222200000000000000003333333333333330000000000044444444000
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;
}
originalScope.$watch(attrs.typeaheadMinLength, function (newVal) {
minLength = !newVal && newVal !== 0 ? 1 : newVal;
});
//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 function to determine if an event should cause selection
var isSelectEvent = attrs.typeaheadShouldSelect ? $parse(attrs.typeaheadShouldSelect) : function(scope, vals) {
var evt = vals.$event;
return evt.which === 13 || evt.which === 9;
};
//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.getOption('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('
');
inputsContainer.css('position', 'relative');
element.after(inputsContainer);
hintInputElem = element.clone();
hintInputElem.attr('placeholder', '');
hintInputElem.attr('tabindex', '-1');
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'
});
if (hintInputElem.attr('id')) {
hintInputElem.removeAttr('id'); // remove duplicate id if present.
}
inputsContainer.append(hintInputElem);
hintInputElem.after(element);
}
//pop-up element used to display matches
var popUpEl = angular.element('
');
popUpEl.attr({
id: popupId,
matches: 'matches',
active: 'activeIdx',
select: 'select(activeIdx, evt)',
'move-in-progress': 'moveInProgress',
query: 'query',
position: 'position',
'assign-is-open': 'assignIsOpen(isOpen)',
debounce: 'debounceUpdate'
});
//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, evt) {
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)) {
if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) {
$$debounce(function() {
scope.select(0, evt);
}, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']);
} else {
scope.select(0, evt);
}
}
if (showHint) {
var firstLabel = scope.matches[0].label;
if (angular.isString(inputValue) &&
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).on('resize', fireRecalculating);
$document.find('body').on('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, evt) {
//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),
$event: evt
});
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.on('keydown', function(evt) {
//typeahead is open and an "interesting" key was pressed
if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
return;
}
var shouldSelect = isSelectEvent(originalScope, {$event: evt});
/**
* if there's nothing selected (i.e. focusFirst) and enter or tab is hit
* or
* shift + tab is pressed to bring focus to the previous element
* then clear the results
*/
if (scope.activeIdx === -1 && shouldSelect || evt.which === 9 && !!evt.shiftKey) {
resetMatches();
scope.$digest();
return;
}
evt.preventDefault();
var target;
switch (evt.which) {
case 27: // escape
evt.stopPropagation();
resetMatches();
originalScope.$digest();
break;
case 38: // up arrow
scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1;
scope.$digest();
target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx];
target.parentNode.scrollTop = target.offsetTop;
break;
case 40: // down arrow
scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
scope.$digest();
target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx];
target.parentNode.scrollTop = target.offsetTop;
break;
default:
if (shouldSelect) {
scope.$apply(function() {
if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) {
$$debounce(function() {
scope.select(scope.activeIdx, evt);
}, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']);
} else {
scope.select(scope.activeIdx, evt);
}
});
}
}
});
element.on('focus', function (evt) {
hasFocus = true;
if (minLength === 0 && !modelCtrl.$viewValue) {
$timeout(function() {
getMatchesAsync(modelCtrl.$viewValue, evt);
}, 0);
}
});
element.on('blur', function(evt) {
if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) {
selected = true;
scope.$apply(function() {
if (angular.isObject(scope.debounceUpdate) && angular.isNumber(scope.debounceUpdate.blur)) {
$$debounce(function() {
scope.select(scope.activeIdx, evt);
}, scope.debounceUpdate.blur);
} else {
scope.select(scope.activeIdx, evt);
}
});
}
if (!isEditable && modelCtrl.$error.editable) {
modelCtrl.$setViewValue();
scope.$apply(function() {
// Reset validity as we are clearing
modelCtrl.$setValidity('editable', true);
modelCtrl.$setValidity('parse', true);
});
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) {
originalScope.$digest();
}
}
};
$document.on('click', dismissClickHandler);
originalScope.$on('$destroy', function() {
$document.off('click', dismissClickHandler);
if (appendToBody || appendTo) {
$popup.remove();
}
if (appendToBody) {
angular.element($window).off('resize', fireRecalculating);
$document.find('body').off('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) {
modelCtrl = _modelCtrl;
ngModelOptions = extractOptions(modelCtrl);
scope.debounceUpdate = $parse(ngModelOptions.getOption('debounce'))(originalScope);
//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;
});
};
function extractOptions(ngModelCtrl) {
var ngModelOptions;
if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing
// guarantee a value
ngModelOptions = ngModelCtrl.$options || {};
// mimic 1.6+ api
ngModelOptions.getOption = function (key) {
return ngModelOptions[key];
};
} else { // in angular >=1.6 $options is always present
ngModelOptions = ngModelCtrl.$options;
}
return ngModelOptions;
}
}])
.directive('uibTypeahead', function() {
return {
controller: 'UibTypeaheadController',
require: ['ngModel', 'uibTypeahead'],
link: function(originalScope, element, attrs, ctrls) {
ctrls[1].init(ctrls[0]);
}
};
})
.directive('uibTypeaheadPopup', ['$$debounce', function($$debounce) {
return {
scope: {
matches: '=',
query: '=',
active: '=',
position: '&',
moveInProgress: '=',
select: '&',
assignIsOpen: '&',
debounce: '&'
},
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, evt) {
var debounce = scope.debounce();
if (angular.isNumber(debounce) || angular.isObject(debounce)) {
$$debounce(function() {
scope.select({activeIdx: activeIdx, evt: evt});
}, angular.isNumber(debounce) ? debounce : debounce['default']);
} else {
scope.select({activeIdx: activeIdx, evt: evt});
}
};
}
};
}])
.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'), '
$& ') : 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",
"
\n" +
"
\n" +
"");
}]);
angular.module("uib/template/accordion/accordion.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/accordion/accordion.html",
"
");
}]);
angular.module("uib/template/alert/alert.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/alert/alert.html",
"
\n" +
" × \n" +
" Close \n" +
" \n" +
"
\n" +
"");
}]);
angular.module("uib/template/carousel/carousel.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/carousel/carousel.html",
"
\n" +
"
1\">\n" +
" \n" +
" previous \n" +
" \n" +
"
1\">\n" +
" \n" +
" next \n" +
" \n" +
"
1\">\n" +
" \n" +
" slide {{ $index + 1 }} of {{ slides.length }}, currently active \n" +
" \n" +
" \n" +
"");
}]);
angular.module("uib/template/carousel/slide.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/carousel/slide.html",
"
\n" +
"");
}]);
angular.module("uib/template/datepicker/datepicker.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/datepicker/datepicker.html",
"
\n" +
"
\n" +
"
\n" +
"
\n" +
"
\n" +
"");
}]);
angular.module("uib/template/datepicker/day.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/datepicker/day.html",
"
\n" +
" \n" +
" \n" +
" previous \n" +
" {{title}} \n" +
" next \n" +
" \n" +
" \n" +
" \n" +
" {{::label.abbr}} \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" {{ weekNumbers[$index] }} \n" +
" \n" +
" {{::dt.label}} \n" +
" \n" +
" \n" +
" \n" +
"
\n" +
"");
}]);
angular.module("uib/template/datepicker/month.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/datepicker/month.html",
"
\n" +
" \n" +
" \n" +
" previous \n" +
" {{title}} \n" +
" next \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" {{::dt.label}} \n" +
" \n" +
" \n" +
" \n" +
"
\n" +
"");
}]);
angular.module("uib/template/datepicker/popup.html", []).run(["$templateCache", function($templateCache) {
$templateCache.put("uib/template/datepicker/popup.html",
"
\n" +
" \n" +
"
\n" +
"");
}]);
angular.module("uib/template/datepicker/year.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/datepicker/year.html",
"
\n" +
" \n" +
" \n" +
" previous \n" +
" {{title}} \n" +
" next \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" {{::dt.label}} \n" +
" \n" +
" \n" +
" \n" +
"
\n" +
"");
}]);
angular.module("uib/template/datepickerPopup/popup.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/datepickerPopup/popup.html",
"\n" +
"");
}]);
angular.module("uib/template/modal/backdrop.html", []).run(["$templateCache", function($templateCache) {
$templateCache.put("uib/template/modal/backdrop.html",
"
\n" +
"");
}]);
angular.module("uib/template/modal/window.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/modal/window.html",
"
\n" +
"");
}]);
angular.module("uib/template/pager/pager.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/pager/pager.html",
"
{{::getText('previous')}} \n" +
"
{{::getText('next')}} \n" +
"");
}]);
angular.module("uib/template/pagination/pagination.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/pagination/pagination.html",
"\n" +
"\n" +
"\n" +
"\n" +
"\n" +
"");
}]);
angular.module("uib/template/tooltip/tooltip-html-popup.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/tooltip/tooltip-html-popup.html",
"
\n" +
"
\n" +
"");
}]);
angular.module("uib/template/tooltip/tooltip-popup.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/tooltip/tooltip-popup.html",
"
\n" +
"
\n" +
"");
}]);
angular.module("uib/template/tooltip/tooltip-template-popup.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/tooltip/tooltip-template-popup.html",
"
\n" +
"
\n" +
"");
}]);
angular.module("uib/template/popover/popover-html.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/popover/popover-html.html",
"
\n" +
"\n" +
"
\n" +
"");
}]);
angular.module("uib/template/popover/popover-template.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/popover/popover-template.html",
"
\n" +
"\n" +
"
\n" +
"");
}]);
angular.module("uib/template/popover/popover.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/popover/popover.html",
"
\n" +
"\n" +
"
\n" +
"");
}]);
angular.module("uib/template/progressbar/bar.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/progressbar/bar.html",
"
\n" +
"");
}]);
angular.module("uib/template/progressbar/progress.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/progressbar/progress.html",
"
");
}]);
angular.module("uib/template/progressbar/progressbar.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/progressbar/progressbar.html",
"
\n" +
"");
}]);
angular.module("uib/template/rating/rating.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/rating/rating.html",
"
\n" +
" ({{ $index < value ? '*' : ' ' }}) \n" +
" \n" +
" \n" +
"");
}]);
angular.module("uib/template/tabs/tab.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/tabs/tab.html",
"
\n" +
" {{heading}} \n" +
" \n" +
"");
}]);
angular.module("uib/template/tabs/tabset.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/tabs/tabset.html",
"
\n" +
"");
}]);
angular.module("uib/template/timepicker/timepicker.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/timepicker/timepicker.html",
"
\n" +
"");
}]);
angular.module("uib/template/typeahead/typeahead-match.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/typeahead/typeahead-match.html",
"
\n" +
"");
}]);
angular.module("uib/template/typeahead/typeahead-popup.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("uib/template/typeahead/typeahead-popup.html",
"\n" +
"");
}]);
angular.module('ui.bootstrap.carousel').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibCarouselCss && angular.element(document).find('head').prepend(''); angular.$$uibCarouselCss = true; });
angular.module('ui.bootstrap.datepicker').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibDatepickerCss && angular.element(document).find('head').prepend(''); angular.$$uibDatepickerCss = true; });
angular.module('ui.bootstrap.position').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibPositionCss && angular.element(document).find('head').prepend(''); angular.$$uibPositionCss = true; });
angular.module('ui.bootstrap.datepickerPopup').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibDatepickerpopupCss && angular.element(document).find('head').prepend(''); angular.$$uibDatepickerpopupCss = true; });
angular.module('ui.bootstrap.tooltip').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibTooltipCss && angular.element(document).find('head').prepend(''); angular.$$uibTooltipCss = true; });
angular.module('ui.bootstrap.timepicker').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibTimepickerCss && angular.element(document).find('head').prepend(''); angular.$$uibTimepickerCss = true; });
angular.module('ui.bootstrap.typeahead').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibTypeaheadCss && angular.element(document).find('head').prepend(''); angular.$$uibTypeaheadCss = true; });
(function() {
'use strict';
angular.module('toastr', [])
.factory('toastr', toastr);
toastr.$inject = ['$animate', '$injector', '$document', '$rootScope', '$sce', 'toastrConfig', '$q'];
function toastr($animate, $injector, $document, $rootScope, $sce, toastrConfig, $q) {
var container;
var index = 0;
var toasts = [];
var previousToastMessage = '';
var openToasts = {};
var containerDefer = $q.defer();
var toast = {
active: active,
clear: clear,
error: error,
info: info,
remove: remove,
success: success,
warning: warning,
refreshTimer: refreshTimer
};
return toast;
/* Public API */
function active() {
return toasts.length;
}
function clear(toast) {
// Bit of a hack, I will remove this soon with a BC
if (arguments.length === 1 && !toast) { return; }
if (toast) {
remove(toast.toastId);
} else {
for (var i = 0; i < toasts.length; i++) {
remove(toasts[i].toastId);
}
}
}
function error(message, title, optionsOverride) {
var type = _getOptions().iconClasses.error;
return _buildNotification(type, message, title, optionsOverride);
}
function info(message, title, optionsOverride) {
var type = _getOptions().iconClasses.info;
return _buildNotification(type, message, title, optionsOverride);
}
function success(message, title, optionsOverride) {
var type = _getOptions().iconClasses.success;
return _buildNotification(type, message, title, optionsOverride);
}
function warning(message, title, optionsOverride) {
var type = _getOptions().iconClasses.warning;
return _buildNotification(type, message, title, optionsOverride);
}
function refreshTimer(toast, newTime) {
if (toast && toast.isOpened && toasts.indexOf(toast) >= 0) {
toast.scope.refreshTimer(newTime);
}
}
function remove(toastId, wasClicked) {
var toast = findToast(toastId);
if (toast && ! toast.deleting) { // Avoid clicking when fading out
toast.deleting = true;
toast.isOpened = false;
$animate.leave(toast.el).then(function() {
if (toast.scope.options.onHidden) {
toast.scope.options.onHidden(!!wasClicked, toast);
}
toast.scope.$destroy();
var index = toasts.indexOf(toast);
delete openToasts[toast.scope.message];
toasts.splice(index, 1);
var maxOpened = toastrConfig.maxOpened;
if (maxOpened && toasts.length >= maxOpened) {
toasts[maxOpened - 1].open.resolve();
}
if (lastToast()) {
container.remove();
container = null;
containerDefer = $q.defer();
}
});
}
function findToast(toastId) {
for (var i = 0; i < toasts.length; i++) {
if (toasts[i].toastId === toastId) {
return toasts[i];
}
}
}
function lastToast() {
return !toasts.length;
}
}
/* Internal functions */
function _buildNotification(type, message, title, optionsOverride) {
if (angular.isObject(title)) {
optionsOverride = title;
title = null;
}
return _notify({
iconClass: type,
message: message,
optionsOverride: optionsOverride,
title: title
});
}
function _getOptions() {
return angular.extend({}, toastrConfig);
}
function _createOrGetContainer(options) {
if(container) { return containerDefer.promise; }
container = angular.element('
');
container.attr('id', options.containerId);
container.addClass(options.positionClass);
container.css({'pointer-events': 'auto'});
var target = angular.element(document.querySelector(options.target));
if ( ! target || ! target.length) {
throw 'Target for toasts doesn\'t exist';
}
$animate.enter(container, target).then(function() {
containerDefer.resolve();
});
return containerDefer.promise;
}
function _notify(map) {
var options = _getOptions();
if (shouldExit()) { return; }
var newToast = createToast();
toasts.push(newToast);
if (ifMaxOpenedAndAutoDismiss()) {
var oldToasts = toasts.slice(0, (toasts.length - options.maxOpened));
for (var i = 0, len = oldToasts.length; i < len; i++) {
remove(oldToasts[i].toastId);
}
}
if (maxOpenedNotReached()) {
newToast.open.resolve();
}
newToast.open.promise.then(function() {
_createOrGetContainer(options).then(function() {
newToast.isOpened = true;
if (options.newestOnTop) {
$animate.enter(newToast.el, container).then(function() {
newToast.scope.init();
});
} else {
var sibling = container[0].lastChild ? angular.element(container[0].lastChild) : null;
$animate.enter(newToast.el, container, sibling).then(function() {
newToast.scope.init();
});
}
});
});
return newToast;
function ifMaxOpenedAndAutoDismiss() {
return options.autoDismiss && options.maxOpened && toasts.length > options.maxOpened;
}
function createScope(toast, map, options) {
if (options.allowHtml) {
toast.scope.allowHtml = true;
toast.scope.title = $sce.trustAsHtml(map.title);
toast.scope.message = $sce.trustAsHtml(map.message);
} else {
toast.scope.title = map.title;
toast.scope.message = map.message;
}
toast.scope.toastType = toast.iconClass;
toast.scope.toastId = toast.toastId;
toast.scope.extraData = options.extraData;
toast.scope.options = {
extendedTimeOut: options.extendedTimeOut,
messageClass: options.messageClass,
onHidden: options.onHidden,
onShown: generateEvent('onShown'),
onTap: generateEvent('onTap'),
progressBar: options.progressBar,
tapToDismiss: options.tapToDismiss,
timeOut: options.timeOut,
titleClass: options.titleClass,
toastClass: options.toastClass
};
if (options.closeButton) {
toast.scope.options.closeHtml = options.closeHtml;
}
function generateEvent(event) {
if (options[event]) {
return function() {
options[event](toast);
};
}
}
}
function createToast() {
var newToast = {
toastId: index++,
isOpened: false,
scope: $rootScope.$new(),
open: $q.defer()
};
newToast.iconClass = map.iconClass;
if (map.optionsOverride) {
angular.extend(options, cleanOptionsOverride(map.optionsOverride));
newToast.iconClass = map.optionsOverride.iconClass || newToast.iconClass;
}
createScope(newToast, map, options);
newToast.el = createToastEl(newToast.scope);
return newToast;
function cleanOptionsOverride(options) {
var badOptions = ['containerId', 'iconClasses', 'maxOpened', 'newestOnTop',
'positionClass', 'preventDuplicates', 'preventOpenDuplicates', 'templates'];
for (var i = 0, l = badOptions.length; i < l; i++) {
delete options[badOptions[i]];
}
return options;
}
}
function createToastEl(scope) {
var angularDomEl = angular.element('
'),
$compile = $injector.get('$compile');
return $compile(angularDomEl)(scope);
}
function maxOpenedNotReached() {
return options.maxOpened && toasts.length <= options.maxOpened || !options.maxOpened;
}
function shouldExit() {
var isDuplicateOfLast = options.preventDuplicates && map.message === previousToastMessage;
var isDuplicateOpen = options.preventOpenDuplicates && openToasts[map.message];
if (isDuplicateOfLast || isDuplicateOpen) {
return true;
}
previousToastMessage = map.message;
openToasts[map.message] = true;
return false;
}
}
}
}());
(function() {
'use strict';
angular.module('toastr')
.constant('toastrConfig', {
allowHtml: false,
autoDismiss: false,
closeButton: false,
closeHtml: '
× ',
containerId: 'toast-container',
extendedTimeOut: 1000,
iconClasses: {
error: 'toast-error',
info: 'toast-info',
success: 'toast-success',
warning: 'toast-warning'
},
maxOpened: 0,
messageClass: 'toast-message',
newestOnTop: true,
onHidden: null,
onShown: null,
onTap: null,
positionClass: 'toast-top-right',
preventDuplicates: false,
preventOpenDuplicates: false,
progressBar: false,
tapToDismiss: true,
target: 'body',
templates: {
toast: 'directives/toast/toast.html',
progressbar: 'directives/progressbar/progressbar.html'
},
timeOut: 5000,
titleClass: 'toast-title',
toastClass: 'toast'
});
}());
(function() {
'use strict';
angular.module('toastr')
.directive('progressBar', progressBar);
progressBar.$inject = ['toastrConfig'];
function progressBar(toastrConfig) {
return {
require: '^toast',
templateUrl: function() {
return toastrConfig.templates.progressbar;
},
link: linkFunction
};
function linkFunction(scope, element, attrs, toastCtrl) {
var intervalId, currentTimeOut, hideTime;
toastCtrl.progressBar = scope;
scope.start = function(duration) {
if (intervalId) {
clearInterval(intervalId);
}
currentTimeOut = parseFloat(duration);
hideTime = new Date().getTime() + currentTimeOut;
intervalId = setInterval(updateProgress, 10);
};
scope.stop = function() {
if (intervalId) {
clearInterval(intervalId);
}
};
function updateProgress() {
var percentage = ((hideTime - (new Date().getTime())) / currentTimeOut) * 100;
element.css('width', percentage + '%');
}
scope.$on('$destroy', function() {
// Failsafe stop
clearInterval(intervalId);
});
}
}
}());
(function() {
'use strict';
angular.module('toastr')
.controller('ToastController', ToastController);
function ToastController() {
this.progressBar = null;
this.startProgressBar = function(duration) {
if (this.progressBar) {
this.progressBar.start(duration);
}
};
this.stopProgressBar = function() {
if (this.progressBar) {
this.progressBar.stop();
}
};
}
}());
(function() {
'use strict';
angular.module('toastr')
.directive('toast', toast);
toast.$inject = ['$injector', '$interval', 'toastrConfig', 'toastr'];
function toast($injector, $interval, toastrConfig, toastr) {
return {
templateUrl: function() {
return toastrConfig.templates.toast;
},
controller: 'ToastController',
link: toastLinkFunction
};
function toastLinkFunction(scope, element, attrs, toastCtrl) {
var timeout;
scope.toastClass = scope.options.toastClass;
scope.titleClass = scope.options.titleClass;
scope.messageClass = scope.options.messageClass;
scope.progressBar = scope.options.progressBar;
if (wantsCloseButton()) {
var button = angular.element(scope.options.closeHtml),
$compile = $injector.get('$compile');
button.addClass('toast-close-button');
button.attr('ng-click', 'close(true, $event)');
$compile(button)(scope);
element.children().prepend(button);
}
scope.init = function() {
if (scope.options.timeOut) {
timeout = createTimeout(scope.options.timeOut);
}
if (scope.options.onShown) {
scope.options.onShown();
}
};
element.on('mouseenter', function() {
hideAndStopProgressBar();
if (timeout) {
$interval.cancel(timeout);
}
});
scope.tapToast = function () {
if (angular.isFunction(scope.options.onTap)) {
scope.options.onTap();
}
if (scope.options.tapToDismiss) {
scope.close(true);
}
};
scope.close = function (wasClicked, $event) {
if ($event && angular.isFunction($event.stopPropagation)) {
$event.stopPropagation();
}
toastr.remove(scope.toastId, wasClicked);
};
scope.refreshTimer = function(newTime) {
if (timeout) {
$interval.cancel(timeout);
timeout = createTimeout(newTime || scope.options.timeOut);
}
};
element.on('mouseleave', function() {
if (scope.options.timeOut === 0 && scope.options.extendedTimeOut === 0) { return; }
scope.$apply(function() {
scope.progressBar = scope.options.progressBar;
});
timeout = createTimeout(scope.options.extendedTimeOut);
});
function createTimeout(time) {
toastCtrl.startProgressBar(time);
return $interval(function() {
toastCtrl.stopProgressBar();
toastr.remove(scope.toastId);
}, time, 1);
}
function hideAndStopProgressBar() {
scope.progressBar = false;
toastCtrl.stopProgressBar();
}
function wantsCloseButton() {
return scope.options.closeHtml;
}
}
}
}());
(function() {
'use strict';
angular.module('toastr', [])
.factory('toastr', toastr);
toastr.$inject = ['$animate', '$injector', '$document', '$rootScope', '$sce', 'toastrConfig', '$q'];
function toastr($animate, $injector, $document, $rootScope, $sce, toastrConfig, $q) {
var container;
var index = 0;
var toasts = [];
var previousToastMessage = '';
var openToasts = {};
var containerDefer = $q.defer();
var toast = {
active: active,
clear: clear,
error: error,
info: info,
remove: remove,
success: success,
warning: warning,
refreshTimer: refreshTimer
};
return toast;
/* Public API */
function active() {
return toasts.length;
}
function clear(toast) {
// Bit of a hack, I will remove this soon with a BC
if (arguments.length === 1 && !toast) { return; }
if (toast) {
remove(toast.toastId);
} else {
for (var i = 0; i < toasts.length; i++) {
remove(toasts[i].toastId);
}
}
}
function error(message, title, optionsOverride) {
var type = _getOptions().iconClasses.error;
return _buildNotification(type, message, title, optionsOverride);
}
function info(message, title, optionsOverride) {
var type = _getOptions().iconClasses.info;
return _buildNotification(type, message, title, optionsOverride);
}
function success(message, title, optionsOverride) {
var type = _getOptions().iconClasses.success;
return _buildNotification(type, message, title, optionsOverride);
}
function warning(message, title, optionsOverride) {
var type = _getOptions().iconClasses.warning;
return _buildNotification(type, message, title, optionsOverride);
}
function refreshTimer(toast, newTime) {
if (toast && toast.isOpened && toasts.indexOf(toast) >= 0) {
toast.scope.refreshTimer(newTime);
}
}
function remove(toastId, wasClicked) {
var toast = findToast(toastId);
if (toast && ! toast.deleting) { // Avoid clicking when fading out
toast.deleting = true;
toast.isOpened = false;
$animate.leave(toast.el).then(function() {
if (toast.scope.options.onHidden) {
toast.scope.options.onHidden(!!wasClicked, toast);
}
toast.scope.$destroy();
var index = toasts.indexOf(toast);
delete openToasts[toast.scope.message];
toasts.splice(index, 1);
var maxOpened = toastrConfig.maxOpened;
if (maxOpened && toasts.length >= maxOpened) {
toasts[maxOpened - 1].open.resolve();
}
if (lastToast()) {
container.remove();
container = null;
containerDefer = $q.defer();
}
});
}
function findToast(toastId) {
for (var i = 0; i < toasts.length; i++) {
if (toasts[i].toastId === toastId) {
return toasts[i];
}
}
}
function lastToast() {
return !toasts.length;
}
}
/* Internal functions */
function _buildNotification(type, message, title, optionsOverride) {
if (angular.isObject(title)) {
optionsOverride = title;
title = null;
}
return _notify({
iconClass: type,
message: message,
optionsOverride: optionsOverride,
title: title
});
}
function _getOptions() {
return angular.extend({}, toastrConfig);
}
function _createOrGetContainer(options) {
if(container) { return containerDefer.promise; }
container = angular.element('
');
container.attr('id', options.containerId);
container.addClass(options.positionClass);
container.css({'pointer-events': 'auto'});
var target = angular.element(document.querySelector(options.target));
if ( ! target || ! target.length) {
throw 'Target for toasts doesn\'t exist';
}
$animate.enter(container, target).then(function() {
containerDefer.resolve();
});
return containerDefer.promise;
}
function _notify(map) {
var options = _getOptions();
if (shouldExit()) { return; }
var newToast = createToast();
toasts.push(newToast);
if (ifMaxOpenedAndAutoDismiss()) {
var oldToasts = toasts.slice(0, (toasts.length - options.maxOpened));
for (var i = 0, len = oldToasts.length; i < len; i++) {
remove(oldToasts[i].toastId);
}
}
if (maxOpenedNotReached()) {
newToast.open.resolve();
}
newToast.open.promise.then(function() {
_createOrGetContainer(options).then(function() {
newToast.isOpened = true;
if (options.newestOnTop) {
$animate.enter(newToast.el, container).then(function() {
newToast.scope.init();
});
} else {
var sibling = container[0].lastChild ? angular.element(container[0].lastChild) : null;
$animate.enter(newToast.el, container, sibling).then(function() {
newToast.scope.init();
});
}
});
});
return newToast;
function ifMaxOpenedAndAutoDismiss() {
return options.autoDismiss && options.maxOpened && toasts.length > options.maxOpened;
}
function createScope(toast, map, options) {
if (options.allowHtml) {
toast.scope.allowHtml = true;
toast.scope.title = $sce.trustAsHtml(map.title);
toast.scope.message = $sce.trustAsHtml(map.message);
} else {
toast.scope.title = map.title;
toast.scope.message = map.message;
}
toast.scope.toastType = toast.iconClass;
toast.scope.toastId = toast.toastId;
toast.scope.extraData = options.extraData;
toast.scope.options = {
extendedTimeOut: options.extendedTimeOut,
messageClass: options.messageClass,
onHidden: options.onHidden,
onShown: generateEvent('onShown'),
onTap: generateEvent('onTap'),
progressBar: options.progressBar,
tapToDismiss: options.tapToDismiss,
timeOut: options.timeOut,
titleClass: options.titleClass,
toastClass: options.toastClass
};
if (options.closeButton) {
toast.scope.options.closeHtml = options.closeHtml;
}
function generateEvent(event) {
if (options[event]) {
return function() {
options[event](toast);
};
}
}
}
function createToast() {
var newToast = {
toastId: index++,
isOpened: false,
scope: $rootScope.$new(),
open: $q.defer()
};
newToast.iconClass = map.iconClass;
if (map.optionsOverride) {
angular.extend(options, cleanOptionsOverride(map.optionsOverride));
newToast.iconClass = map.optionsOverride.iconClass || newToast.iconClass;
}
createScope(newToast, map, options);
newToast.el = createToastEl(newToast.scope);
return newToast;
function cleanOptionsOverride(options) {
var badOptions = ['containerId', 'iconClasses', 'maxOpened', 'newestOnTop',
'positionClass', 'preventDuplicates', 'preventOpenDuplicates', 'templates'];
for (var i = 0, l = badOptions.length; i < l; i++) {
delete options[badOptions[i]];
}
return options;
}
}
function createToastEl(scope) {
var angularDomEl = angular.element('
'),
$compile = $injector.get('$compile');
return $compile(angularDomEl)(scope);
}
function maxOpenedNotReached() {
return options.maxOpened && toasts.length <= options.maxOpened || !options.maxOpened;
}
function shouldExit() {
var isDuplicateOfLast = options.preventDuplicates && map.message === previousToastMessage;
var isDuplicateOpen = options.preventOpenDuplicates && openToasts[map.message];
if (isDuplicateOfLast || isDuplicateOpen) {
return true;
}
previousToastMessage = map.message;
openToasts[map.message] = true;
return false;
}
}
}
}());
(function() {
'use strict';
angular.module('toastr')
.constant('toastrConfig', {
allowHtml: false,
autoDismiss: false,
closeButton: false,
closeHtml: '
× ',
containerId: 'toast-container',
extendedTimeOut: 1000,
iconClasses: {
error: 'toast-error',
info: 'toast-info',
success: 'toast-success',
warning: 'toast-warning'
},
maxOpened: 0,
messageClass: 'toast-message',
newestOnTop: true,
onHidden: null,
onShown: null,
onTap: null,
positionClass: 'toast-top-right',
preventDuplicates: false,
preventOpenDuplicates: false,
progressBar: false,
tapToDismiss: true,
target: 'body',
templates: {
toast: 'directives/toast/toast.html',
progressbar: 'directives/progressbar/progressbar.html'
},
timeOut: 5000,
titleClass: 'toast-title',
toastClass: 'toast'
});
}());
(function() {
'use strict';
angular.module('toastr')
.directive('progressBar', progressBar);
progressBar.$inject = ['toastrConfig'];
function progressBar(toastrConfig) {
return {
require: '^toast',
templateUrl: function() {
return toastrConfig.templates.progressbar;
},
link: linkFunction
};
function linkFunction(scope, element, attrs, toastCtrl) {
var intervalId, currentTimeOut, hideTime;
toastCtrl.progressBar = scope;
scope.start = function(duration) {
if (intervalId) {
clearInterval(intervalId);
}
currentTimeOut = parseFloat(duration);
hideTime = new Date().getTime() + currentTimeOut;
intervalId = setInterval(updateProgress, 10);
};
scope.stop = function() {
if (intervalId) {
clearInterval(intervalId);
}
};
function updateProgress() {
var percentage = ((hideTime - (new Date().getTime())) / currentTimeOut) * 100;
element.css('width', percentage + '%');
}
scope.$on('$destroy', function() {
// Failsafe stop
clearInterval(intervalId);
});
}
}
}());
(function() {
'use strict';
angular.module('toastr')
.controller('ToastController', ToastController);
function ToastController() {
this.progressBar = null;
this.startProgressBar = function(duration) {
if (this.progressBar) {
this.progressBar.start(duration);
}
};
this.stopProgressBar = function() {
if (this.progressBar) {
this.progressBar.stop();
}
};
}
}());
(function() {
'use strict';
angular.module('toastr')
.directive('toast', toast);
toast.$inject = ['$injector', '$interval', 'toastrConfig', 'toastr'];
function toast($injector, $interval, toastrConfig, toastr) {
return {
templateUrl: function() {
return toastrConfig.templates.toast;
},
controller: 'ToastController',
link: toastLinkFunction
};
function toastLinkFunction(scope, element, attrs, toastCtrl) {
var timeout;
scope.toastClass = scope.options.toastClass;
scope.titleClass = scope.options.titleClass;
scope.messageClass = scope.options.messageClass;
scope.progressBar = scope.options.progressBar;
if (wantsCloseButton()) {
var button = angular.element(scope.options.closeHtml),
$compile = $injector.get('$compile');
button.addClass('toast-close-button');
button.attr('ng-click', 'close(true, $event)');
$compile(button)(scope);
element.children().prepend(button);
}
scope.init = function() {
if (scope.options.timeOut) {
timeout = createTimeout(scope.options.timeOut);
}
if (scope.options.onShown) {
scope.options.onShown();
}
};
element.on('mouseenter', function() {
hideAndStopProgressBar();
if (timeout) {
$interval.cancel(timeout);
}
});
scope.tapToast = function () {
if (angular.isFunction(scope.options.onTap)) {
scope.options.onTap();
}
if (scope.options.tapToDismiss) {
scope.close(true);
}
};
scope.close = function (wasClicked, $event) {
if ($event && angular.isFunction($event.stopPropagation)) {
$event.stopPropagation();
}
toastr.remove(scope.toastId, wasClicked);
};
scope.refreshTimer = function(newTime) {
if (timeout) {
$interval.cancel(timeout);
timeout = createTimeout(newTime || scope.options.timeOut);
}
};
element.on('mouseleave', function() {
if (scope.options.timeOut === 0 && scope.options.extendedTimeOut === 0) { return; }
scope.$apply(function() {
scope.progressBar = scope.options.progressBar;
});
timeout = createTimeout(scope.options.extendedTimeOut);
});
function createTimeout(time) {
toastCtrl.startProgressBar(time);
return $interval(function() {
toastCtrl.stopProgressBar();
toastr.remove(scope.toastId);
}, time, 1);
}
function hideAndStopProgressBar() {
scope.progressBar = false;
toastCtrl.stopProgressBar();
}
function wantsCloseButton() {
return scope.options.closeHtml;
}
}
}
}());
angular.module("toastr").run(["$templateCache", function($templateCache) {$templateCache.put("directives/progressbar/progressbar.html","
\n");
$templateCache.put("directives/toast/toast.html","
\n
\n
{{title}}
\n
{{message}}
\n
\n
\n
\n
\n
\n");}]);
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.angularStripe = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o
*
* Copyright (c) 2014 Jon Schlinkert, contributors.
* Licensed under the MIT license.
*/
var isNumber = _dereq_('is-number');
var slice = _dereq_('array-slice');
module.exports = function last(arr, num) {
if (!Array.isArray(arr)) {
throw new Error('array-last expects an array as the first argument.');
}
if (arr.length === 0) {
return null;
}
var res = slice(arr, arr.length - (isNumber(num) ? +num : 1));
if (+num === 1 || num == null) {
return res[0];
}
return res;
};
},{"array-slice":8,"is-number":20}],8:[function(_dereq_,module,exports){
/*!
* array-slice
*
* Copyright (c) 2014-2015, Jon Schlinkert.
* Licensed under the MIT License.
*/
'use strict';
module.exports = function slice(arr, start, end) {
var len = arr.length >>> 0;
var range = [];
start = idx(arr, start);
end = idx(arr, end, len);
while (start < end) {
range.push(arr[start++]);
}
return range;
};
function idx(arr, pos, end) {
var len = arr.length >>> 0;
if (pos == null) {
pos = end || 0;
} else if (pos < 0) {
pos = Math.max(len + pos, 0);
} else {
pos = Math.min(pos, len);
}
return pos;
}
},{}],9:[function(_dereq_,module,exports){
"use strict";
// rawAsap provides everything we need except exception management.
var rawAsap = _dereq_("./raw");
// RawTasks are recycled to reduce GC churn.
var freeTasks = [];
// We queue errors to ensure they are thrown in right order (FIFO).
// Array-as-queue is good enough here, since we are just dealing with exceptions.
var pendingErrors = [];
var requestErrorThrow = rawAsap.makeRequestCallFromTimer(throwFirstError);
function throwFirstError() {
if (pendingErrors.length) {
throw pendingErrors.shift();
}
}
/**
* Calls a task as soon as possible after returning, in its own event, with priority
* over other events like animation, reflow, and repaint. An error thrown from an
* event will not interrupt, nor even substantially slow down the processing of
* other events, but will be rather postponed to a lower priority event.
* @param {{call}} task A callable object, typically a function that takes no
* arguments.
*/
module.exports = asap;
function asap(task) {
var rawTask;
if (freeTasks.length) {
rawTask = freeTasks.pop();
} else {
rawTask = new RawTask();
}
rawTask.task = task;
rawAsap(rawTask);
}
// We wrap tasks with recyclable task objects. A task object implements
// `call`, just like a function.
function RawTask() {
this.task = null;
}
// The sole purpose of wrapping the task is to catch the exception and recycle
// the task object after its single use.
RawTask.prototype.call = function () {
try {
this.task.call();
} catch (error) {
if (asap.onerror) {
// This hook exists purely for testing purposes.
// Its name will be periodically randomized to break any code that
// depends on its existence.
asap.onerror(error);
} else {
// In a web browser, exceptions are not fatal. However, to avoid
// slowing down the queue of pending tasks, we rethrow the error in a
// lower priority turn.
pendingErrors.push(error);
requestErrorThrow();
}
} finally {
this.task = null;
freeTasks[freeTasks.length] = this;
}
};
},{"./raw":10}],10:[function(_dereq_,module,exports){
(function (global){
"use strict";
// Use the fastest means possible to execute a task in its own turn, with
// priority over other events including IO, animation, reflow, and redraw
// events in browsers.
//
// An exception thrown by a task will permanently interrupt the processing of
// subsequent tasks. The higher level `asap` function ensures that if an
// exception is thrown by a task, that the task queue will continue flushing as
// soon as possible, but if you use `rawAsap` directly, you are responsible to
// either ensure that no exceptions are thrown from your task, or to manually
// call `rawAsap.requestFlush` if an exception is thrown.
module.exports = rawAsap;
function rawAsap(task) {
if (!queue.length) {
requestFlush();
flushing = true;
}
// Equivalent to push, but avoids a function call.
queue[queue.length] = task;
}
var queue = [];
// Once a flush has been requested, no further calls to `requestFlush` are
// necessary until the next `flush` completes.
var flushing = false;
// `requestFlush` is an implementation-specific method that attempts to kick
// off a `flush` event as quickly as possible. `flush` will attempt to exhaust
// the event queue before yielding to the browser's own event loop.
var requestFlush;
// The position of the next task to execute in the task queue. This is
// preserved between calls to `flush` so that it can be resumed if
// a task throws an exception.
var index = 0;
// If a task schedules additional tasks recursively, the task queue can grow
// unbounded. To prevent memory exhaustion, the task queue will periodically
// truncate already-completed tasks.
var capacity = 1024;
// The flush function processes all tasks that have been scheduled with
// `rawAsap` unless and until one of those tasks throws an exception.
// If a task throws an exception, `flush` ensures that its state will remain
// consistent and will resume where it left off when called again.
// However, `flush` does not make any arrangements to be called again if an
// exception is thrown.
function flush() {
while (index < queue.length) {
var currentIndex = index;
// Advance the index before calling the task. This ensures that we will
// begin flushing on the next task the task throws an error.
index = index + 1;
queue[currentIndex].call();
// Prevent leaking memory for long chains of recursive calls to `asap`.
// If we call `asap` within tasks scheduled by `asap`, the queue will
// grow, but to avoid an O(n) walk for every task we execute, we don't
// shift tasks off the queue after they have been executed.
// Instead, we periodically shift 1024 tasks off the queue.
if (index > capacity) {
// Manually shift all values starting at the index back to the
// beginning of the queue.
for (var scan = 0, newLength = queue.length - index; scan < newLength; scan++) {
queue[scan] = queue[scan + index];
}
queue.length -= index;
index = 0;
}
}
queue.length = 0;
index = 0;
flushing = false;
}
// `requestFlush` is implemented using a strategy based on data collected from
// every available SauceLabs Selenium web driver worker at time of writing.
// https://docs.google.com/spreadsheets/d/1mG-5UYGup5qxGdEMWkhP6BWCz053NUb2E1QoUTU16uA/edit#gid=783724593
// Safari 6 and 6.1 for desktop, iPad, and iPhone are the only browsers that
// have WebKitMutationObserver but not un-prefixed MutationObserver.
// Must use `global` or `self` instead of `window` to work in both frames and web
// workers. `global` is a provision of Browserify, Mr, Mrs, or Mop.
/* globals self */
var scope = typeof global !== "undefined" ? global : self;
var BrowserMutationObserver = scope.MutationObserver || scope.WebKitMutationObserver;
// MutationObservers are desirable because they have high priority and work
// reliably everywhere they are implemented.
// They are implemented in all modern browsers.
//
// - Android 4-4.3
// - Chrome 26-34
// - Firefox 14-29
// - Internet Explorer 11
// - iPad Safari 6-7.1
// - iPhone Safari 7-7.1
// - Safari 6-7
if (typeof BrowserMutationObserver === "function") {
requestFlush = makeRequestCallFromMutationObserver(flush);
// MessageChannels are desirable because they give direct access to the HTML
// task queue, are implemented in Internet Explorer 10, Safari 5.0-1, and Opera
// 11-12, and in web workers in many engines.
// Although message channels yield to any queued rendering and IO tasks, they
// would be better than imposing the 4ms delay of timers.
// However, they do not work reliably in Internet Explorer or Safari.
// Internet Explorer 10 is the only browser that has setImmediate but does
// not have MutationObservers.
// Although setImmediate yields to the browser's renderer, it would be
// preferrable to falling back to setTimeout since it does not have
// the minimum 4ms penalty.
// Unfortunately there appears to be a bug in Internet Explorer 10 Mobile (and
// Desktop to a lesser extent) that renders both setImmediate and
// MessageChannel useless for the purposes of ASAP.
// https://github.com/kriskowal/q/issues/396
// Timers are implemented universally.
// We fall back to timers in workers in most engines, and in foreground
// contexts in the following browsers.
// However, note that even this simple case requires nuances to operate in a
// broad spectrum of browsers.
//
// - Firefox 3-13
// - Internet Explorer 6-9
// - iPad Safari 4.3
// - Lynx 2.8.7
} else {
requestFlush = makeRequestCallFromTimer(flush);
}
// `requestFlush` requests that the high priority event queue be flushed as
// soon as possible.
// This is useful to prevent an error thrown in a task from stalling the event
// queue if the exception handled by Node.js’s
// `process.on("uncaughtException")` or by a domain.
rawAsap.requestFlush = requestFlush;
// To request a high priority event, we induce a mutation observer by toggling
// the text of a text node between "1" and "-1".
function makeRequestCallFromMutationObserver(callback) {
var toggle = 1;
var observer = new BrowserMutationObserver(callback);
var node = document.createTextNode("");
observer.observe(node, {characterData: true});
return function requestCall() {
toggle = -toggle;
node.data = toggle;
};
}
// The message channel technique was discovered by Malte Ubl and was the
// original foundation for this library.
// http://www.nonblocking.io/2011/06/windownexttick.html
// Safari 6.0.5 (at least) intermittently fails to create message ports on a
// page's first load. Thankfully, this version of Safari supports
// MutationObservers, so we don't need to fall back in that case.
// function makeRequestCallFromMessageChannel(callback) {
// var channel = new MessageChannel();
// channel.port1.onmessage = callback;
// return function requestCall() {
// channel.port2.postMessage(0);
// };
// }
// For reasons explained above, we are also unable to use `setImmediate`
// under any circumstances.
// Even if we were, there is another bug in Internet Explorer 10.
// It is not sufficient to assign `setImmediate` to `requestFlush` because
// `setImmediate` must be called *by name* and therefore must be wrapped in a
// closure.
// Never forget.
// function makeRequestCallFromSetImmediate(callback) {
// return function requestCall() {
// setImmediate(callback);
// };
// }
// Safari 6.0 has a problem where timers will get lost while the user is
// scrolling. This problem does not impact ASAP because Safari 6.0 supports
// mutation observers, so that implementation is used instead.
// However, if we ever elect to use timers in Safari, the prevalent work-around
// is to add a scroll event listener that calls for a flush.
// `setTimeout` does not call the passed callback if the delay is less than
// approximately 7 in web workers in Firefox 8 through 18, and sometimes not
// even then.
function makeRequestCallFromTimer(callback) {
return function requestCall() {
// We dispatch a timeout with a specified delay of 0 for engines that
// can reliably accommodate that request. This will usually be snapped
// to a 4 milisecond delay, but once we're flushing, there's no delay
// between events.
var timeoutHandle = setTimeout(handleTimer, 0);
// However, since this timer gets frequently dropped in Firefox
// workers, we enlist an interval handle that will try to fire
// an event 20 times per second until it succeeds.
var intervalHandle = setInterval(handleTimer, 50);
function handleTimer() {
// Whichever timer succeeds will cancel both timers and
// execute the callback.
clearTimeout(timeoutHandle);
clearInterval(intervalHandle);
callback();
}
};
}
// This is for `asap.js` only.
// Its name will be periodically randomized to break any code that depends on
// its existence.
rawAsap.makeRequestCallFromTimer = makeRequestCallFromTimer;
// ASAP was originally a nextTick shim included in Q. This was factored out
// into this ASAP package. It was later adapted to RSVP which made further
// amendments. These decisions, particularly to marginalize MessageChannel and
// to capture the MutationObserver implementation in a closure, were integrated
// back into ASAP proper.
// https://github.com/tildeio/rsvp.js/blob/cddf7232546a9cf858524b75cde6f9edf72620a7/lib/rsvp/asap.js
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{}],11:[function(_dereq_,module,exports){
'use strict'
var assert = _dereq_('assert-ok')
var format = _dereq_('simple-format')
var print = _dereq_('print-value')
module.exports = function assertEqual (a, b) {
assert(a === b, format('expected `%s` to equal `%s`', print(a), print(b)))
}
},{"assert-ok":13,"print-value":30,"simple-format":32}],12:[function(_dereq_,module,exports){
'use strict'
module.exports = function assertFunction (value) {
if (typeof value !== 'function') {
throw new TypeError('Expected function, got: ' + value)
}
}
},{}],13:[function(_dereq_,module,exports){
'use strict'
module.exports = function assertOk (value, message) {
if (!value) {
throw new Error(message || 'Expected true, got ' + value)
}
}
},{}],14:[function(_dereq_,module,exports){
'use strict'
module.exports = CallAll
function CallAll (fns) {
fns = Array.isArray(fns) ? fns : arguments
return function callAll () {
var args = arguments
var ret = new Array(fns.length)
for (var i = 0, ii = fns.length; i < ii; i++) {
ret[i] = fns[i].apply(null, args)
}
return ret
}
}
},{}],15:[function(_dereq_,module,exports){
/**
* cuid.js
* Collision-resistant UID generator for browsers and node.
* Sequential for fast db lookups and recency sorting.
* Safe for element IDs and server-side lookups.
*
* Extracted from CLCTR
*
* Copyright (c) Eric Elliott 2012
* MIT License
*/
/*global window, navigator, document, require, process, module */
(function (app) {
'use strict';
var namespace = 'cuid',
c = 0,
blockSize = 4,
base = 36,
discreteValues = Math.pow(base, blockSize),
pad = function pad(num, size) {
var s = "000000000" + num;
return s.substr(s.length-size);
},
randomBlock = function randomBlock() {
return pad((Math.random() *
discreteValues << 0)
.toString(base), blockSize);
},
safeCounter = function () {
c = (c < discreteValues) ? c : 0;
c++; // this is not subliminal
return c - 1;
},
api = function cuid() {
// Starting with a lowercase letter makes
// it HTML element ID friendly.
var letter = 'c', // hard-coded allows for sequential access
// timestamp
// warning: this exposes the exact date and time
// that the uid was created.
timestamp = (new Date().getTime()).toString(base),
// Prevent same-machine collisions.
counter,
// A few chars to generate distinct ids for different
// clients (so different computers are far less
// likely to generate the same id)
fingerprint = api.fingerprint(),
// Grab some more chars from Math.random()
random = randomBlock() + randomBlock();
counter = pad(safeCounter().toString(base), blockSize);
return (letter + timestamp + counter + fingerprint + random);
};
api.slug = function slug() {
var date = new Date().getTime().toString(36),
counter,
print = api.fingerprint().slice(0,1) +
api.fingerprint().slice(-1),
random = randomBlock().slice(-2);
counter = safeCounter().toString(36).slice(-4);
return date.slice(-2) +
counter + print + random;
};
api.globalCount = function globalCount() {
// We want to cache the results of this
var cache = (function calc() {
var i,
count = 0;
for (i in window) {
count++;
}
return count;
}());
api.globalCount = function () { return cache; };
return cache;
};
api.fingerprint = function browserPrint() {
return pad((navigator.mimeTypes.length +
navigator.userAgent.length).toString(36) +
api.globalCount().toString(36), 4);
};
// don't change anything from here down.
if (app.register) {
app.register(namespace, api);
} else if (typeof module !== 'undefined') {
module.exports = api;
} else {
app[namespace] = api;
}
}(this.applitude || this));
},{}],16:[function(_dereq_,module,exports){
var wrappy = _dereq_('wrappy')
module.exports = wrappy(dezalgo)
var asap = _dereq_('asap')
function dezalgo (cb) {
var sync = true
asap(function () {
sync = false
})
return function zalgoSafe() {
var args = arguments
var me = this
if (sync)
asap(function() {
cb.apply(me, args)
})
else
cb.apply(me, args)
}
}
},{"asap":9,"wrappy":35}],17:[function(_dereq_,module,exports){
'use strict';
var isObj = _dereq_('is-obj');
module.exports.get = function (obj, path) {
if (!isObj(obj) || typeof path !== 'string') {
return obj;
}
var pathArr = getPathSegments(path);
for (var i = 0; i < pathArr.length; i++) {
var descriptor = Object.getOwnPropertyDescriptor(obj, pathArr[i]) || Object.getOwnPropertyDescriptor(Object.prototype, pathArr[i]);
if (descriptor && !descriptor.enumerable) {
return;
}
obj = obj[pathArr[i]];
if (obj === undefined || obj === null) {
// `obj` is either `undefined` or `null` so we want to stop the loop, and
// if this is not the last bit of the path, and
// if it did't return `undefined`
// it would return `null` if `obj` is `null`
// but we want `get({foo: null}, 'foo.bar')` to equal `undefined` not `null`
if (i !== pathArr.length - 1) {
return undefined;
}
break;
}
}
return obj;
};
module.exports.set = function (obj, path, value) {
if (!isObj(obj) || typeof path !== 'string') {
return;
}
var pathArr = getPathSegments(path);
for (var i = 0; i < pathArr.length; i++) {
var p = pathArr[i];
if (!isObj(obj[p])) {
obj[p] = {};
}
if (i === pathArr.length - 1) {
obj[p] = value;
}
obj = obj[p];
}
};
module.exports.delete = function (obj, path) {
if (!isObj(obj) || typeof path !== 'string') {
return;
}
var pathArr = getPathSegments(path);
for (var i = 0; i < pathArr.length; i++) {
var p = pathArr[i];
if (i === pathArr.length - 1) {
delete obj[p];
return;
}
obj = obj[p];
}
};
module.exports.has = function (obj, path) {
if (!isObj(obj) || typeof path !== 'string') {
return false;
}
var pathArr = getPathSegments(path);
for (var i = 0; i < pathArr.length; i++) {
obj = obj[pathArr[i]];
if (obj === undefined) {
return false;
}
}
return true;
};
function getPathSegments(path) {
var pathArr = path.split('.');
var parts = [];
for (var i = 0; i < pathArr.length; i++) {
var p = pathArr[i];
while (p[p.length - 1] === '\\' && pathArr[i + 1] !== undefined) {
p = p.slice(0, -1) + '.';
p += pathArr[++i];
}
parts.push(p);
}
return parts;
}
},{"is-obj":21}],18:[function(_dereq_,module,exports){
'use strict'
var assertFn = _dereq_('assert-function')
module.exports = Ear
function Ear () {
var callbacks = []
function listeners () {
var args = arguments
var i = 0
var length = callbacks.length
for (; i < length; i++) {
var callback = callbacks[i]
callback.apply(null, args)
}
}
listeners.add = function (listener) {
assertFn(listener)
callbacks.push(listener)
return function remove () {
var i = 0
var length = callbacks.length
for (; i < length; i++) {
if (callbacks[i] === listener) {
callbacks.splice(i, 1)
return
}
}
}
}
return listeners
}
},{"assert-function":12}],19:[function(_dereq_,module,exports){
(function (global){
var win;
if (typeof window !== "undefined") {
win = window;
} else if (typeof global !== "undefined") {
win = global;
} else if (typeof self !== "undefined"){
win = self;
} else {
win = {};
}
module.exports = win;
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{}],20:[function(_dereq_,module,exports){
/*!
* is-number
*
* Copyright (c) 2014 Jon Schlinkert, contributors.
* Licensed under the MIT License
*/
'use strict';
module.exports = function isNumber(n) {
return !!(+n) || n === 0 || n === '0';
};
},{}],21:[function(_dereq_,module,exports){
'use strict';
module.exports = function (x) {
var type = typeof x;
return x !== null && (type === 'object' || type === 'function');
};
},{}],22:[function(_dereq_,module,exports){
module.exports = Array.isArray || function (arr) {
return Object.prototype.toString.call(arr) == '[object Array]';
};
},{}],23:[function(_dereq_,module,exports){
/*!
* isobject
*
* Copyright (c) 2014-2015, Jon Schlinkert.
* Licensed under the MIT License.
*/
'use strict';
var isArray = _dereq_('isarray');
module.exports = function isObject(o) {
return o != null && typeof o === 'object' && !isArray(o);
};
},{"isarray":22}],24:[function(_dereq_,module,exports){
exports = module.exports = stringify
exports.getSerialize = serializer
function stringify(obj, replacer, spaces, cycleReplacer) {
return JSON.stringify(obj, serializer(replacer, cycleReplacer), spaces)
}
function serializer(replacer, cycleReplacer) {
var stack = [], keys = []
if (cycleReplacer == null) cycleReplacer = function(key, value) {
if (stack[0] === value) return "[Circular ~]"
return "[Circular ~." + keys.slice(0, stack.indexOf(value)).join(".") + "]"
}
return function(key, value) {
if (stack.length > 0) {
var thisPos = stack.indexOf(this)
~thisPos ? stack.splice(thisPos + 1) : stack.push(this)
~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key)
if (~stack.indexOf(value)) value = cycleReplacer.call(this, key, value)
}
else stack.push(value)
return replacer == null ? value : replacer.call(this, key, value)
}
}
},{}],25:[function(_dereq_,module,exports){
'use strict'
var assert = _dereq_('assert-ok')
var assertEqual = _dereq_('assert-equal')
var dot = _dereq_('dot-prop')
var toArray = _dereq_('to-array')
var last = _dereq_('array-last')
var dezalgo = _dereq_('dezalgo')
var all = _dereq_('call-all-fns')
module.exports = Lazy
function Lazy (methods, load) {
assert(Array.isArray(methods), 'methods are required')
assertEqual(typeof load, 'function', 'load fn is required')
var api = null
var error = null
var queue = []
load(function (err, lib) {
error = err
api = lib
all(queue)(err, lib)
queue = null
})
return methods.reduce(function (lazy, method) {
dot.set(lazy, method, Deferred(method))
return lazy
}, {})
function Deferred (method) {
return function deferred () {
var args = arguments
onReady(function (err, api) {
if (!err) return dot.get(api, method).apply(null, args)
var callback = last(toArray(args))
if (typeof callback === 'function') {
return callback(err)
}
})
}
}
function onReady (callback) {
callback = dezalgo(callback)
if (api || error) return callback(error, api)
queue.push(callback)
}
}
},{"array-last":7,"assert-equal":11,"assert-ok":13,"call-all-fns":14,"dezalgo":16,"dot-prop":26,"to-array":34}],26:[function(_dereq_,module,exports){
'use strict';
var isObj = _dereq_('is-obj');
module.exports.get = function (obj, path) {
if (!isObj(obj) || typeof path !== 'string') {
return obj;
}
var pathArr = getPathSegments(path);
for (var i = 0; i < pathArr.length; i++) {
obj = obj[pathArr[i]];
if (obj === undefined) {
break;
}
}
return obj;
};
module.exports.set = function (obj, path, value) {
if (!isObj(obj) || typeof path !== 'string') {
return;
}
var pathArr = getPathSegments(path);
for (var i = 0; i < pathArr.length; i++) {
var p = pathArr[i];
if (!isObj(obj[p])) {
obj[p] = {};
}
if (i === pathArr.length - 1) {
obj[p] = value;
}
obj = obj[p];
}
};
module.exports.delete = function (obj, path) {
if (!isObj(obj) || typeof path !== 'string') {
return;
}
var pathArr = getPathSegments(path);
for (var i = 0; i < pathArr.length; i++) {
var p = pathArr[i];
if (i === pathArr.length - 1) {
delete obj[p];
return;
}
obj = obj[p];
}
};
module.exports.has = function (obj, path) {
if (!isObj(obj) || typeof path !== 'string') {
return false;
}
var pathArr = getPathSegments(path);
for (var i = 0; i < pathArr.length; i++) {
obj = obj[pathArr[i]];
if (obj === undefined) {
return false;
}
}
return true;
};
function getPathSegments(path) {
var pathArr = path.split('.');
var parts = [];
for (var i = 0; i < pathArr.length; i++) {
var p = pathArr[i];
while (p[p.length - 1] === '\\') {
p = p.slice(0, -1) + '.';
p += pathArr[++i];
}
parts.push(p);
}
return parts;
}
},{"is-obj":21}],27:[function(_dereq_,module,exports){
'use strict'
var load = _dereq_('load-script')
var window = _dereq_('global/window')
var extend = _dereq_('xtend')
var assert = _dereq_('assert-ok')
var dezalgo = _dereq_('dezalgo')
var Listeners = _dereq_('ear')
var extendQuery = _dereq_('query-extend')
var cuid = _dereq_('cuid')
module.exports = loadGlobal
var listeners = {}
function loadGlobal (options, callback) {
assert(options, 'options required')
assert(options.url, 'url required')
assert(options.global, 'global required')
assert(callback, 'callback required')
options = extend(options)
callback = dezalgo(callback)
if (getGlobal(options)) {
return callback(null, getGlobal(options))
}
callback = cache(options, callback)
if (!callback) return
if (options.jsonp) {
var id = jsonpCallback(options, callback)
options.url = extendQuery(options.url, {callback: id})
}
load(options.url, options, function (err) {
if (err) return callback(err)
if (!options.jsonp) {
var library = getGlobal(options)
if (!library) return callback(new Error('expected: `window.' + options.global + '`, actual: `' + library + '`'))
callback(null, library)
}
})
}
function cache (options, callback) {
if (!get()) {
set(Listeners())
get().add(callback)
return function onComplete (err, lib) {
get()(err, lib)
set(Listeners())
}
}
get().add(callback)
return undefined
function get () {
return listeners[options.global]
}
function set (value) {
listeners[options.global] = value
}
}
function getGlobal (options) {
return window[options.global]
}
function jsonpCallback (options, callback) {
var id = cuid()
window[id] = function jsonpCallback () {
callback(null, getGlobal(options))
delete window[id]
}
return id
}
},{"assert-ok":13,"cuid":15,"dezalgo":16,"ear":18,"global/window":19,"load-script":28,"query-extend":31,"xtend":36}],28:[function(_dereq_,module,exports){
module.exports = function load (src, opts, cb) {
var head = document.head || document.getElementsByTagName('head')[0]
var script = document.createElement('script')
if (typeof opts === 'function') {
cb = opts
opts = {}
}
opts = opts || {}
cb = cb || function() {}
script.type = opts.type || 'text/javascript'
script.charset = opts.charset || 'utf8';
script.async = 'async' in opts ? !!opts.async : true
script.src = src
if (opts.attrs) {
setAttributes(script, opts.attrs)
}
if (opts.text) {
script.text = '' + opts.text
}
var onend = 'onload' in script ? stdOnEnd : ieOnEnd
onend(script, cb)
// some good legacy browsers (firefox) fail the 'in' detection above
// so as a fallback we always set onload
// old IE will ignore this and new IE will set onload
if (!script.onload) {
stdOnEnd(script, cb);
}
head.appendChild(script)
}
function setAttributes(script, attrs) {
for (var attr in attrs) {
script.setAttribute(attr, attrs[attr]);
}
}
function stdOnEnd (script, cb) {
script.onload = function () {
this.onerror = this.onload = null
cb(null, script)
}
script.onerror = function () {
// this.onload = null here is necessary
// because even IE9 works not like others
this.onerror = this.onload = null
cb(new Error('Failed to load ' + this.src), script)
}
}
function ieOnEnd (script, cb) {
script.onreadystatechange = function () {
if (this.readyState != 'complete' && this.readyState != 'loaded') return
this.onreadystatechange = null
cb(null, script) // there is no way to catch loading errors in IE8
}
}
},{}],29:[function(_dereq_,module,exports){
'use strict';
module.exports = function split(str) {
var a = 1,
res = '';
var parts = str.split('%'),
len = parts.length;
if (len > 0) { res += parts[0]; }
for (var i = 1; i < len; i++) {
if (parts[i][0] === 's' || parts[i][0] === 'd') {
var value = arguments[a++];
res += parts[i][0] === 'd' ? Math.floor(value) : value;
} else if (parts[i][0]) {
res += '%' + parts[i][0];
} else {
i++;
res += '%' + parts[i][0];
}
res += parts[i].substring(1);
}
return res;
};
},{}],30:[function(_dereq_,module,exports){
'use strict'
var isObject = _dereq_('isobject')
var safeStringify = _dereq_('json-stringify-safe')
module.exports = function print (value) {
var toString = isJson(value) ? stringify : String
return toString(value)
}
function isJson (value) {
return isObject(value) || Array.isArray(value)
}
function stringify (value) {
return safeStringify(value, null, '')
}
},{"isobject":23,"json-stringify-safe":24}],31:[function(_dereq_,module,exports){
!function(glob) {
var queryToObject = function(query) {
var obj = {};
if (!query) return obj;
each(query.split('&'), function(val) {
var pieces = val.split('=');
var key = parseKey(pieces[0]);
var keyDecoded = decodeURIComponent(key.val);
var valDecoded = pieces[1] && decodeURIComponent(pieces[1]);
if (key.type === 'array') {
if (!obj[keyDecoded]) obj[keyDecoded] = [];
obj[keyDecoded].push(valDecoded);
} else if (key.type === 'string') {
obj[keyDecoded] = valDecoded;
}
});
return obj;
};
var objectToQuery = function(obj) {
var pieces = [], encodedKey;
for (var k in obj) {
if (!obj.hasOwnProperty(k)) continue;
if (typeof obj[k] === 'undefined') {
pieces.push(encodeURIComponent(k));
continue;
}
encodedKey = encodeURIComponent(k);
if (isArray(obj[k])) {
each(obj[k], function(val) {
pieces.push(encodedKey + '[]=' + encodeURIComponent(val));
});
continue;
}
pieces.push(encodedKey + '=' + encodeURIComponent(obj[k]));
}
return pieces.length ? ('?' + pieces.join('&')) : '';
};
// for now we will only support string and arrays
var parseKey = function(key) {
var pos = key.indexOf('[');
if (pos === -1) return { type: 'string', val: key };
return { type: 'array', val: key.substr(0, pos) };
};
var isArray = function(val) {
return Object.prototype.toString.call(val) === '[object Array]';
};
var extract = function(url) {
var pos = url.lastIndexOf('?');
var hasQuery = pos !== -1;
var base = void 0;
if (hasQuery && pos > 0) {
base = url.substring(0, pos);
} else if (!hasQuery && (url && url.length > 0)) {
base = url;
}
return {
base: base,
query: hasQuery ? url.substring(pos+1) : void 0
};
};
// thanks raynos!
// https://github.com/Raynos/xtend
var extend = function() {
var target = {};
for (var i = 0; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
}
return target;
};
var queryExtend = function() {
var args = Array.prototype.slice.call(arguments, 0);
var asObject = args[args.length-1] === true;
var base = '';
if (!args.length) {
return base;
}
if (asObject) {
args.pop();
}
var normalized = map(args, function(param) {
if (typeof param === 'string') {
var extracted = extract(param);
if (extracted.base) base = extracted.base;
return queryToObject(extracted.query);
}
return param;
});
if (asObject) {
return extend.apply({}, normalized);
} else {
return base + objectToQuery(extend.apply({}, normalized));
}
};
var each = function(arr, fn) {
for (var i = 0, l = arr.length; i < l; i++) {
fn(arr[i], i);
}
};
var map = function(arr, fn) {
var res = [];
for (var i = 0, l = arr.length; i < l; i++) {
res.push( fn(arr[i], i) );
}
return res;
};
if (typeof module !== 'undefined' && module.exports) {
// Node.js / browserify
module.exports = queryExtend;
} else if (typeof define === 'function' && define.amd) {
// require.js / AMD
define(function() {
return queryExtend;
});
} else {
//
*
*
*
*
*
*
*
*
*/
angular.module('ui.router', ['ui.router.state']);
angular.module('ui.router.compat', ['ui.router']);
/**
* @ngdoc object
* @name ui.router.util.$resolve
*
* @requires $q
* @requires $injector
*
* @description
* Manages resolution of (acyclic) graphs of promises.
*/
$Resolve.$inject = ['$q', '$injector'];
function $Resolve( $q, $injector) {
var VISIT_IN_PROGRESS = 1,
VISIT_DONE = 2,
NOTHING = {},
NO_DEPENDENCIES = [],
NO_LOCALS = NOTHING,
NO_PARENT = extend($q.when(NOTHING), { $$promises: NOTHING, $$values: NOTHING });
/**
* @ngdoc function
* @name ui.router.util.$resolve#study
* @methodOf ui.router.util.$resolve
*
* @description
* Studies a set of invocables that are likely to be used multiple times.
*
* $resolve.study(invocables)(locals, parent, self)
*
* is equivalent to
*
* $resolve.resolve(invocables, locals, parent, self)
*
* but the former is more efficient (in fact `resolve` just calls `study`
* internally).
*
* @param {object} invocables Invocable objects
* @return {function} a function to pass in locals, parent and self
*/
this.study = function (invocables) {
if (!isObject(invocables)) throw new Error("'invocables' must be an object");
var invocableKeys = objectKeys(invocables || {});
// Perform a topological sort of invocables to build an ordered plan
var plan = [], cycle = [], visited = {};
function visit(value, key) {
if (visited[key] === VISIT_DONE) return;
cycle.push(key);
if (visited[key] === VISIT_IN_PROGRESS) {
cycle.splice(0, indexOf(cycle, key));
throw new Error("Cyclic dependency: " + cycle.join(" -> "));
}
visited[key] = VISIT_IN_PROGRESS;
if (isString(value)) {
plan.push(key, [ function() { return $injector.get(value); }], NO_DEPENDENCIES);
} else {
var params = $injector.annotate(value);
forEach(params, function (param) {
if (param !== key && invocables.hasOwnProperty(param)) visit(invocables[param], param);
});
plan.push(key, value, params);
}
cycle.pop();
visited[key] = VISIT_DONE;
}
forEach(invocables, visit);
invocables = cycle = visited = null; // plan is all that's required
function isResolve(value) {
return isObject(value) && value.then && value.$$promises;
}
return function (locals, parent, self) {
if (isResolve(locals) && self === undefined) {
self = parent; parent = locals; locals = null;
}
if (!locals) locals = NO_LOCALS;
else if (!isObject(locals)) {
throw new Error("'locals' must be an object");
}
if (!parent) parent = NO_PARENT;
else if (!isResolve(parent)) {
throw new Error("'parent' must be a promise returned by $resolve.resolve()");
}
// To complete the overall resolution, we have to wait for the parent
// promise and for the promise for each invokable in our plan.
var resolution = $q.defer(),
result = silenceUncaughtInPromise(resolution.promise),
promises = result.$$promises = {},
values = extend({}, locals),
wait = 1 + plan.length/3,
merged = false;
silenceUncaughtInPromise(result);
function done() {
// Merge parent values we haven't got yet and publish our own $$values
if (!--wait) {
if (!merged) merge(values, parent.$$values);
result.$$values = values;
result.$$promises = result.$$promises || true; // keep for isResolve()
delete result.$$inheritedValues;
resolution.resolve(values);
}
}
function fail(reason) {
result.$$failure = reason;
resolution.reject(reason);
}
// Short-circuit if parent has already failed
if (isDefined(parent.$$failure)) {
fail(parent.$$failure);
return result;
}
if (parent.$$inheritedValues) {
merge(values, omit(parent.$$inheritedValues, invocableKeys));
}
// Merge parent values if the parent has already resolved, or merge
// parent promises and wait if the parent resolve is still in progress.
extend(promises, parent.$$promises);
if (parent.$$values) {
merged = merge(values, omit(parent.$$values, invocableKeys));
result.$$inheritedValues = omit(parent.$$values, invocableKeys);
done();
} else {
if (parent.$$inheritedValues) {
result.$$inheritedValues = omit(parent.$$inheritedValues, invocableKeys);
}
parent.then(done, fail);
}
// Process each invocable in the plan, but ignore any where a local of the same name exists.
for (var i=0, ii=plan.length; i
* Impact on loading templates for more details about this mechanism.
*
* @param {boolean} value
*/
this.shouldUnsafelyUseHttp = function(value) {
shouldUnsafelyUseHttp = !!value;
};
/**
* @ngdoc object
* @name ui.router.util.$templateFactory
*
* @requires $http
* @requires $templateCache
* @requires $injector
*
* @description
* Service. Manages loading of templates.
*/
this.$get = ['$http', '$templateCache', '$injector', function($http, $templateCache, $injector){
return new TemplateFactory($http, $templateCache, $injector, shouldUnsafelyUseHttp);}];
}
/**
* @ngdoc object
* @name ui.router.util.$templateFactory
*
* @requires $http
* @requires $templateCache
* @requires $injector
*
* @description
* Service. Manages loading of templates.
*/
function TemplateFactory($http, $templateCache, $injector, shouldUnsafelyUseHttp) {
/**
* @ngdoc function
* @name ui.router.util.$templateFactory#fromConfig
* @methodOf ui.router.util.$templateFactory
*
* @description
* Creates a template from a configuration object.
*
* @param {object} config Configuration object for which to load a template.
* The following properties are search in the specified order, and the first one
* that is defined is used to create the template:
*
* @param {string|object} config.template html string template or function to
* load via {@link ui.router.util.$templateFactory#fromString fromString}.
* @param {string|object} config.templateUrl url to load or a function returning
* the url to load via {@link ui.router.util.$templateFactory#fromUrl fromUrl}.
* @param {Function} config.templateProvider function to invoke via
* {@link ui.router.util.$templateFactory#fromProvider fromProvider}.
* @param {object} params Parameters to pass to the template function.
* @param {object} locals Locals to pass to `invoke` if the template is loaded
* via a `templateProvider`. Defaults to `{ params: params }`.
*
* @return {string|object} The template html as a string, or a promise for
* that string,or `null` if no template is configured.
*/
this.fromConfig = function (config, params, locals) {
return (
isDefined(config.template) ? this.fromString(config.template, params) :
isDefined(config.templateUrl) ? this.fromUrl(config.templateUrl, params) :
isDefined(config.templateProvider) ? this.fromProvider(config.templateProvider, params, locals) :
null
);
};
/**
* @ngdoc function
* @name ui.router.util.$templateFactory#fromString
* @methodOf ui.router.util.$templateFactory
*
* @description
* Creates a template from a string or a function returning a string.
*
* @param {string|object} template html template as a string or function that
* returns an html template as a string.
* @param {object} params Parameters to pass to the template function.
*
* @return {string|object} The template html as a string, or a promise for that
* string.
*/
this.fromString = function (template, params) {
return isFunction(template) ? template(params) : template;
};
/**
* @ngdoc function
* @name ui.router.util.$templateFactory#fromUrl
* @methodOf ui.router.util.$templateFactory
*
* @description
* Loads a template from the a URL via `$http` and `$templateCache`.
*
* @param {string|Function} url url of the template to load, or a function
* that returns a url.
* @param {Object} params Parameters to pass to the url function.
* @return {string|Promise.} The template html as a string, or a promise
* for that string.
*/
this.fromUrl = function (url, params) {
if (isFunction(url)) url = url(params);
if (url == null) return null;
else {
if(!shouldUnsafelyUseHttp) {
return $injector.get('$templateRequest')(url);
} else {
return $http
.get(url, { cache: $templateCache, headers: { Accept: 'text/html' }})
.then(function(response) { return response.data; });
}
}
};
/**
* @ngdoc function
* @name ui.router.util.$templateFactory#fromProvider
* @methodOf ui.router.util.$templateFactory
*
* @description
* Creates a template by invoking an injectable provider function.
*
* @param {Function} provider Function to invoke via `$injector.invoke`
* @param {Object} params Parameters for the template.
* @param {Object} locals Locals to pass to `invoke`. Defaults to
* `{ params: params }`.
* @return {string|Promise.} The template html as a string, or a promise
* for that string.
*/
this.fromProvider = function (provider, params, locals) {
return $injector.invoke(provider, null, locals || { params: params });
};
}
angular.module('ui.router.util').provider('$templateFactory', TemplateFactoryProvider);
var $$UMFP; // reference to $UrlMatcherFactoryProvider
/**
* @ngdoc object
* @name ui.router.util.type:UrlMatcher
*
* @description
* Matches URLs against patterns and extracts named parameters from the path or the search
* part of the URL. A URL pattern consists of a path pattern, optionally followed by '?' and a list
* of search parameters. Multiple search parameter names are separated by '&'. Search parameters
* do not influence whether or not a URL is matched, but their values are passed through into
* the matched parameters returned by {@link ui.router.util.type:UrlMatcher#methods_exec exec}.
*
* Path parameter placeholders can be specified using simple colon/catch-all syntax or curly brace
* syntax, which optionally allows a regular expression for the parameter to be specified:
*
* * `':'` name - colon placeholder
* * `'*'` name - catch-all placeholder
* * `'{' name '}'` - curly placeholder
* * `'{' name ':' regexp|type '}'` - curly placeholder with regexp or type name. Should the
* regexp itself contain curly braces, they must be in matched pairs or escaped with a backslash.
*
* Parameter names may contain only word characters (latin letters, digits, and underscore) and
* must be unique within the pattern (across both path and search parameters). For colon
* placeholders or curly placeholders without an explicit regexp, a path parameter matches any
* number of characters other than '/'. For catch-all placeholders the path parameter matches
* any number of characters.
*
* Examples:
*
* * `'/hello/'` - Matches only if the path is exactly '/hello/'. There is no special treatment for
* trailing slashes, and patterns have to match the entire path, not just a prefix.
* * `'/user/:id'` - Matches '/user/bob' or '/user/1234!!!' or even '/user/' but not '/user' or
* '/user/bob/details'. The second path segment will be captured as the parameter 'id'.
* * `'/user/{id}'` - Same as the previous example, but using curly brace syntax.
* * `'/user/{id:[^/]*}'` - Same as the previous example.
* * `'/user/{id:[0-9a-fA-F]{1,8}}'` - Similar to the previous example, but only matches if the id
* parameter consists of 1 to 8 hex digits.
* * `'/files/{path:.*}'` - Matches any URL starting with '/files/' and captures the rest of the
* path into the parameter 'path'.
* * `'/files/*path'` - ditto.
* * `'/calendar/{start:date}'` - Matches "/calendar/2014-11-12" (because the pattern defined
* in the built-in `date` Type matches `2014-11-12`) and provides a Date object in $stateParams.start
*
* @param {string} pattern The pattern to compile into a matcher.
* @param {Object} config A configuration object hash:
* @param {Object=} parentMatcher Used to concatenate the pattern/config onto
* an existing UrlMatcher
*
* * `caseInsensitive` - `true` if URL matching should be case insensitive, otherwise `false`, the default value (for backward compatibility) is `false`.
* * `strict` - `false` if matching against a URL with a trailing slash should be treated as equivalent to a URL without a trailing slash, the default value is `true`.
*
* @property {string} prefix A static prefix of this pattern. The matcher guarantees that any
* URL matching this matcher (i.e. any string for which {@link ui.router.util.type:UrlMatcher#methods_exec exec()} returns
* non-null) will start with this prefix.
*
* @property {string} source The pattern that was passed into the constructor
*
* @property {string} sourcePath The path portion of the source property
*
* @property {string} sourceSearch The search portion of the source property
*
* @property {string} regex The constructed regex that will be used to match against the url when
* it is time to determine which url will match.
*
* @returns {Object} New `UrlMatcher` object
*/
function UrlMatcher(pattern, config, parentMatcher) {
config = extend({ params: {} }, isObject(config) ? config : {});
// Find all placeholders and create a compiled pattern, using either classic or curly syntax:
// '*' name
// ':' name
// '{' name '}'
// '{' name ':' regexp '}'
// The regular expression is somewhat complicated due to the need to allow curly braces
// inside the regular expression. The placeholder regexp breaks down as follows:
// ([:*])([\w\[\]]+) - classic placeholder ($1 / $2) (search version has - for snake-case)
// \{([\w\[\]]+)(?:\:\s*( ... ))?\} - curly brace placeholder ($3) with optional regexp/type ... ($4) (search version has - for snake-case
// (?: ... | ... | ... )+ - the regexp consists of any number of atoms, an atom being either
// [^{}\\]+ - anything other than curly braces or backslash
// \\. - a backslash escape
// \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms
var placeholder = /([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,
searchPlaceholder = /([:]?)([\w\[\].-]+)|\{([\w\[\].-]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,
compiled = '^', last = 0, m,
segments = this.segments = [],
parentParams = parentMatcher ? parentMatcher.params : {},
params = this.params = parentMatcher ? parentMatcher.params.$$new() : new $$UMFP.ParamSet(),
paramNames = [];
function addParameter(id, type, config, location) {
paramNames.push(id);
if (parentParams[id]) return parentParams[id];
if (!/^\w+([-.]+\w+)*(?:\[\])?$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'");
if (params[id]) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'");
params[id] = new $$UMFP.Param(id, type, config, location);
return params[id];
}
function quoteRegExp(string, pattern, squash, optional) {
var surroundPattern = ['',''], result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&");
if (!pattern) return result;
switch(squash) {
case false: surroundPattern = ['(', ')' + (optional ? "?" : "")]; break;
case true:
result = result.replace(/\/$/, '');
surroundPattern = ['(?:\/(', ')|\/)?'];
break;
default: surroundPattern = ['(' + squash + "|", ')?']; break;
}
return result + surroundPattern[0] + pattern + surroundPattern[1];
}
this.source = pattern;
// Split into static segments separated by path parameter placeholders.
// The number of segments is always 1 more than the number of parameters.
function matchDetails(m, isSearch) {
var id, regexp, segment, type, cfg, arrayMode;
id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null
cfg = config.params[id];
segment = pattern.substring(last, m.index);
regexp = isSearch ? m[4] : m[4] || (m[1] == '*' ? '.*' : null);
if (regexp) {
type = $$UMFP.type(regexp) || inherit($$UMFP.type("string"), { pattern: new RegExp(regexp, config.caseInsensitive ? 'i' : undefined) });
}
return {
id: id, regexp: regexp, segment: segment, type: type, cfg: cfg
};
}
var p, param, segment;
while ((m = placeholder.exec(pattern))) {
p = matchDetails(m, false);
if (p.segment.indexOf('?') >= 0) break; // we're into the search part
param = addParameter(p.id, p.type, p.cfg, "path");
compiled += quoteRegExp(p.segment, param.type.pattern.source, param.squash, param.isOptional);
segments.push(p.segment);
last = placeholder.lastIndex;
}
segment = pattern.substring(last);
// Find any search parameter names and remove them from the last segment
var i = segment.indexOf('?');
if (i >= 0) {
var search = this.sourceSearch = segment.substring(i);
segment = segment.substring(0, i);
this.sourcePath = pattern.substring(0, last + i);
if (search.length > 0) {
last = 0;
while ((m = searchPlaceholder.exec(search))) {
p = matchDetails(m, true);
param = addParameter(p.id, p.type, p.cfg, "search");
last = placeholder.lastIndex;
// check if ?&
}
}
} else {
this.sourcePath = pattern;
this.sourceSearch = '';
}
compiled += quoteRegExp(segment) + (config.strict === false ? '\/?' : '') + '$';
segments.push(segment);
this.regexp = new RegExp(compiled, config.caseInsensitive ? 'i' : undefined);
this.prefix = segments[0];
this.$$paramNames = paramNames;
}
/**
* @ngdoc function
* @name ui.router.util.type:UrlMatcher#concat
* @methodOf ui.router.util.type:UrlMatcher
*
* @description
* Returns a new matcher for a pattern constructed by appending the path part and adding the
* search parameters of the specified pattern to this pattern. The current pattern is not
* modified. This can be understood as creating a pattern for URLs that are relative to (or
* suffixes of) the current pattern.
*
* @example
* The following two matchers are equivalent:
*
* new UrlMatcher('/user/{id}?q').concat('/details?date');
* new UrlMatcher('/user/{id}/details?q&date');
*
*
* @param {string} pattern The pattern to append.
* @param {Object} config An object hash of the configuration for the matcher.
* @returns {UrlMatcher} A matcher for the concatenated pattern.
*/
UrlMatcher.prototype.concat = function (pattern, config) {
// Because order of search parameters is irrelevant, we can add our own search
// parameters to the end of the new pattern. Parse the new pattern by itself
// and then join the bits together, but it's much easier to do this on a string level.
var defaultConfig = {
caseInsensitive: $$UMFP.caseInsensitive(),
strict: $$UMFP.strictMode(),
squash: $$UMFP.defaultSquashPolicy()
};
return new UrlMatcher(this.sourcePath + pattern + this.sourceSearch, extend(defaultConfig, config), this);
};
UrlMatcher.prototype.toString = function () {
return this.source;
};
/**
* @ngdoc function
* @name ui.router.util.type:UrlMatcher#exec
* @methodOf ui.router.util.type:UrlMatcher
*
* @description
* Tests the specified path against this matcher, and returns an object containing the captured
* parameter values, or null if the path does not match. The returned object contains the values
* of any search parameters that are mentioned in the pattern, but their value may be null if
* they are not present in `searchParams`. This means that search parameters are always treated
* as optional.
*
* @example
*
* new UrlMatcher('/user/{id}?q&r').exec('/user/bob', {
* x: '1', q: 'hello'
* });
* // returns { id: 'bob', q: 'hello', r: null }
*
*
* @param {string} path The URL path to match, e.g. `$location.path()`.
* @param {Object} searchParams URL search parameters, e.g. `$location.search()`.
* @returns {Object} The captured parameter values.
*/
UrlMatcher.prototype.exec = function (path, searchParams) {
var m = this.regexp.exec(path);
if (!m) return null;
searchParams = searchParams || {};
var paramNames = this.parameters(), nTotal = paramNames.length,
nPath = this.segments.length - 1,
values = {}, i, j, cfg, paramName;
if (nPath !== m.length - 1) throw new Error("Unbalanced capture group in route '" + this.source + "'");
function decodePathArray(string) {
function reverseString(str) { return str.split("").reverse().join(""); }
function unquoteDashes(str) { return str.replace(/\\-/g, "-"); }
var split = reverseString(string).split(/-(?!\\)/);
var allReversed = map(split, reverseString);
return map(allReversed, unquoteDashes).reverse();
}
var param, paramVal;
for (i = 0; i < nPath; i++) {
paramName = paramNames[i];
param = this.params[paramName];
paramVal = m[i+1];
// if the param value matches a pre-replace pair, replace the value before decoding.
for (j = 0; j < param.replace.length; j++) {
if (param.replace[j].from === paramVal) paramVal = param.replace[j].to;
}
if (paramVal && param.array === true) paramVal = decodePathArray(paramVal);
if (isDefined(paramVal)) paramVal = param.type.decode(paramVal);
values[paramName] = param.value(paramVal);
}
for (/**/; i < nTotal; i++) {
paramName = paramNames[i];
values[paramName] = this.params[paramName].value(searchParams[paramName]);
param = this.params[paramName];
paramVal = searchParams[paramName];
for (j = 0; j < param.replace.length; j++) {
if (param.replace[j].from === paramVal) paramVal = param.replace[j].to;
}
if (isDefined(paramVal)) paramVal = param.type.decode(paramVal);
values[paramName] = param.value(paramVal);
}
return values;
};
/**
* @ngdoc function
* @name ui.router.util.type:UrlMatcher#parameters
* @methodOf ui.router.util.type:UrlMatcher
*
* @description
* Returns the names of all path and search parameters of this pattern in an unspecified order.
*
* @returns {Array.} An array of parameter names. Must be treated as read-only. If the
* pattern has no parameters, an empty array is returned.
*/
UrlMatcher.prototype.parameters = function (param) {
if (!isDefined(param)) return this.$$paramNames;
return this.params[param] || null;
};
/**
* @ngdoc function
* @name ui.router.util.type:UrlMatcher#validates
* @methodOf ui.router.util.type:UrlMatcher
*
* @description
* Checks an object hash of parameters to validate their correctness according to the parameter
* types of this `UrlMatcher`.
*
* @param {Object} params The object hash of parameters to validate.
* @returns {boolean} Returns `true` if `params` validates, otherwise `false`.
*/
UrlMatcher.prototype.validates = function (params) {
return this.params.$$validates(params);
};
/**
* @ngdoc function
* @name ui.router.util.type:UrlMatcher#format
* @methodOf ui.router.util.type:UrlMatcher
*
* @description
* Creates a URL that matches this pattern by substituting the specified values
* for the path and search parameters. Null values for path parameters are
* treated as empty strings.
*
* @example
*
* new UrlMatcher('/user/{id}?q').format({ id:'bob', q:'yes' });
* // returns '/user/bob?q=yes'
*
*
* @param {Object} values the values to substitute for the parameters in this pattern.
* @returns {string} the formatted URL (path and optionally search part).
*/
UrlMatcher.prototype.format = function (values) {
values = values || {};
var segments = this.segments, params = this.parameters(), paramset = this.params;
if (!this.validates(values)) return null;
var i, search = false, nPath = segments.length - 1, nTotal = params.length, result = segments[0];
function encodeDashes(str) { // Replace dashes with encoded "\-"
return encodeURIComponent(str).replace(/-/g, function(c) { return '%5C%' + c.charCodeAt(0).toString(16).toUpperCase(); });
}
for (i = 0; i < nTotal; i++) {
var isPathParam = i < nPath;
var name = params[i], param = paramset[name], value = param.value(values[name]);
var isDefaultValue = param.isOptional && param.type.equals(param.value(), value);
var squash = isDefaultValue ? param.squash : false;
var encoded = param.type.encode(value);
if (isPathParam) {
var nextSegment = segments[i + 1];
var isFinalPathParam = i + 1 === nPath;
if (squash === false) {
if (encoded != null) {
if (isArray(encoded)) {
result += map(encoded, encodeDashes).join("-");
} else {
result += encodeURIComponent(encoded);
}
}
result += nextSegment;
} else if (squash === true) {
var capture = result.match(/\/$/) ? /\/?(.*)/ : /(.*)/;
result += nextSegment.match(capture)[1];
} else if (isString(squash)) {
result += squash + nextSegment;
}
if (isFinalPathParam && param.squash === true && result.slice(-1) === '/') result = result.slice(0, -1);
} else {
if (encoded == null || (isDefaultValue && squash !== false)) continue;
if (!isArray(encoded)) encoded = [ encoded ];
if (encoded.length === 0) continue;
encoded = map(encoded, encodeURIComponent).join('&' + name + '=');
result += (search ? '&' : '?') + (name + '=' + encoded);
search = true;
}
}
return result;
};
/**
* @ngdoc object
* @name ui.router.util.type:Type
*
* @description
* Implements an interface to define custom parameter types that can be decoded from and encoded to
* string parameters matched in a URL. Used by {@link ui.router.util.type:UrlMatcher `UrlMatcher`}
* objects when matching or formatting URLs, or comparing or validating parameter values.
*
* See {@link ui.router.util.$urlMatcherFactory#methods_type `$urlMatcherFactory#type()`} for more
* information on registering custom types.
*
* @param {Object} config A configuration object which contains the custom type definition. The object's
* properties will override the default methods and/or pattern in `Type`'s public interface.
* @example
*
* {
* decode: function(val) { return parseInt(val, 10); },
* encode: function(val) { return val && val.toString(); },
* equals: function(a, b) { return this.is(a) && a === b; },
* is: function(val) { return angular.isNumber(val) isFinite(val) && val % 1 === 0; },
* pattern: /\d+/
* }
*
*
* @property {RegExp} pattern The regular expression pattern used to match values of this type when
* coming from a substring of a URL.
*
* @returns {Object} Returns a new `Type` object.
*/
function Type(config) {
extend(this, config);
}
/**
* @ngdoc function
* @name ui.router.util.type:Type#is
* @methodOf ui.router.util.type:Type
*
* @description
* Detects whether a value is of a particular type. Accepts a native (decoded) value
* and determines whether it matches the current `Type` object.
*
* @param {*} val The value to check.
* @param {string} key Optional. If the type check is happening in the context of a specific
* {@link ui.router.util.type:UrlMatcher `UrlMatcher`} object, this is the name of the
* parameter in which `val` is stored. Can be used for meta-programming of `Type` objects.
* @returns {Boolean} Returns `true` if the value matches the type, otherwise `false`.
*/
Type.prototype.is = function(val, key) {
return true;
};
/**
* @ngdoc function
* @name ui.router.util.type:Type#encode
* @methodOf ui.router.util.type:Type
*
* @description
* Encodes a custom/native type value to a string that can be embedded in a URL. Note that the
* return value does *not* need to be URL-safe (i.e. passed through `encodeURIComponent()`), it
* only needs to be a representation of `val` that has been coerced to a string.
*
* @param {*} val The value to encode.
* @param {string} key The name of the parameter in which `val` is stored. Can be used for
* meta-programming of `Type` objects.
* @returns {string} Returns a string representation of `val` that can be encoded in a URL.
*/
Type.prototype.encode = function(val, key) {
return val;
};
/**
* @ngdoc function
* @name ui.router.util.type:Type#decode
* @methodOf ui.router.util.type:Type
*
* @description
* Converts a parameter value (from URL string or transition param) to a custom/native value.
*
* @param {string} val The URL parameter value to decode.
* @param {string} key The name of the parameter in which `val` is stored. Can be used for
* meta-programming of `Type` objects.
* @returns {*} Returns a custom representation of the URL parameter value.
*/
Type.prototype.decode = function(val, key) {
return val;
};
/**
* @ngdoc function
* @name ui.router.util.type:Type#equals
* @methodOf ui.router.util.type:Type
*
* @description
* Determines whether two decoded values are equivalent.
*
* @param {*} a A value to compare against.
* @param {*} b A value to compare against.
* @returns {Boolean} Returns `true` if the values are equivalent/equal, otherwise `false`.
*/
Type.prototype.equals = function(a, b) {
return a == b;
};
Type.prototype.$subPattern = function() {
var sub = this.pattern.toString();
return sub.substr(1, sub.length - 2);
};
Type.prototype.pattern = /.*/;
Type.prototype.toString = function() { return "{Type:" + this.name + "}"; };
/** Given an encoded string, or a decoded object, returns a decoded object */
Type.prototype.$normalize = function(val) {
return this.is(val) ? val : this.decode(val);
};
/*
* Wraps an existing custom Type as an array of Type, depending on 'mode'.
* e.g.:
* - urlmatcher pattern "/path?{queryParam[]:int}"
* - url: "/path?queryParam=1&queryParam=2
* - $stateParams.queryParam will be [1, 2]
* if `mode` is "auto", then
* - url: "/path?queryParam=1 will create $stateParams.queryParam: 1
* - url: "/path?queryParam=1&queryParam=2 will create $stateParams.queryParam: [1, 2]
*/
Type.prototype.$asArray = function(mode, isSearch) {
if (!mode) return this;
if (mode === "auto" && !isSearch) throw new Error("'auto' array mode is for query parameters only");
function ArrayType(type, mode) {
function bindTo(type, callbackName) {
return function() {
return type[callbackName].apply(type, arguments);
};
}
// Wrap non-array value as array
function arrayWrap(val) { return isArray(val) ? val : (isDefined(val) ? [ val ] : []); }
// Unwrap array value for "auto" mode. Return undefined for empty array.
function arrayUnwrap(val) {
switch(val.length) {
case 0: return undefined;
case 1: return mode === "auto" ? val[0] : val;
default: return val;
}
}
function falsey(val) { return !val; }
// Wraps type (.is/.encode/.decode) functions to operate on each value of an array
function arrayHandler(callback, allTruthyMode) {
return function handleArray(val) {
if (isArray(val) && val.length === 0) return val;
val = arrayWrap(val);
var result = map(val, callback);
if (allTruthyMode === true)
return filter(result, falsey).length === 0;
return arrayUnwrap(result);
};
}
// Wraps type (.equals) functions to operate on each value of an array
function arrayEqualsHandler(callback) {
return function handleArray(val1, val2) {
var left = arrayWrap(val1), right = arrayWrap(val2);
if (left.length !== right.length) return false;
for (var i = 0; i < left.length; i++) {
if (!callback(left[i], right[i])) return false;
}
return true;
};
}
this.encode = arrayHandler(bindTo(type, 'encode'));
this.decode = arrayHandler(bindTo(type, 'decode'));
this.is = arrayHandler(bindTo(type, 'is'), true);
this.equals = arrayEqualsHandler(bindTo(type, 'equals'));
this.pattern = type.pattern;
this.$normalize = arrayHandler(bindTo(type, '$normalize'));
this.name = type.name;
this.$arrayMode = mode;
}
return new ArrayType(this, mode);
};
/**
* @ngdoc object
* @name ui.router.util.$urlMatcherFactory
*
* @description
* Factory for {@link ui.router.util.type:UrlMatcher `UrlMatcher`} instances. The factory
* is also available to providers under the name `$urlMatcherFactoryProvider`.
*/
function $UrlMatcherFactory() {
$$UMFP = this;
var isCaseInsensitive = false, isStrictMode = true, defaultSquashPolicy = false;
// Use tildes to pre-encode slashes.
// If the slashes are simply URLEncoded, the browser can choose to pre-decode them,
// and bidirectional encoding/decoding fails.
// Tilde was chosen because it's not a RFC 3986 section 2.2 Reserved Character
function valToString(val) { return val != null ? val.toString().replace(/(~|\/)/g, function (m) { return {'~':'~~', '/':'~2F'}[m]; }) : val; }
function valFromString(val) { return val != null ? val.toString().replace(/(~~|~2F)/g, function (m) { return {'~~':'~', '~2F':'/'}[m]; }) : val; }
var $types = {}, enqueue = true, typeQueue = [], injector, defaultTypes = {
"string": {
encode: valToString,
decode: valFromString,
// TODO: in 1.0, make string .is() return false if value is undefined/null by default.
// In 0.2.x, string params are optional by default for backwards compat
is: function(val) { return val == null || !isDefined(val) || typeof val === "string"; },
pattern: /[^/]*/
},
"int": {
encode: valToString,
decode: function(val) { return parseInt(val, 10); },
is: function(val) { return val !== undefined && val !== null && this.decode(val.toString()) === val; },
pattern: /\d+/
},
"bool": {
encode: function(val) { return val ? 1 : 0; },
decode: function(val) { return parseInt(val, 10) !== 0; },
is: function(val) { return val === true || val === false; },
pattern: /0|1/
},
"date": {
encode: function (val) {
if (!this.is(val))
return undefined;
return [ val.getFullYear(),
('0' + (val.getMonth() + 1)).slice(-2),
('0' + val.getDate()).slice(-2)
].join("-");
},
decode: function (val) {
if (this.is(val)) return val;
var match = this.capture.exec(val);
return match ? new Date(match[1], match[2] - 1, match[3]) : undefined;
},
is: function(val) { return val instanceof Date && !isNaN(val.valueOf()); },
equals: function (a, b) { return this.is(a) && this.is(b) && a.toISOString() === b.toISOString(); },
pattern: /[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/,
capture: /([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/
},
"json": {
encode: angular.toJson,
decode: angular.fromJson,
is: angular.isObject,
equals: angular.equals,
pattern: /[^/]*/
},
"any": { // does not encode/decode
encode: angular.identity,
decode: angular.identity,
equals: angular.equals,
pattern: /.*/
}
};
function getDefaultConfig() {
return {
strict: isStrictMode,
caseInsensitive: isCaseInsensitive
};
}
function isInjectable(value) {
return (isFunction(value) || (isArray(value) && isFunction(value[value.length - 1])));
}
/**
* [Internal] Get the default value of a parameter, which may be an injectable function.
*/
$UrlMatcherFactory.$$getDefaultValue = function(config) {
if (!isInjectable(config.value)) return config.value;
if (!injector) throw new Error("Injectable functions cannot be called at configuration time");
return injector.invoke(config.value);
};
/**
* @ngdoc function
* @name ui.router.util.$urlMatcherFactory#caseInsensitive
* @methodOf ui.router.util.$urlMatcherFactory
*
* @description
* Defines whether URL matching should be case sensitive (the default behavior), or not.
*
* @param {boolean} value `false` to match URL in a case sensitive manner; otherwise `true`;
* @returns {boolean} the current value of caseInsensitive
*/
this.caseInsensitive = function(value) {
if (isDefined(value))
isCaseInsensitive = value;
return isCaseInsensitive;
};
/**
* @ngdoc function
* @name ui.router.util.$urlMatcherFactory#strictMode
* @methodOf ui.router.util.$urlMatcherFactory
*
* @description
* Defines whether URLs should match trailing slashes, or not (the default behavior).
*
* @param {boolean=} value `false` to match trailing slashes in URLs, otherwise `true`.
* @returns {boolean} the current value of strictMode
*/
this.strictMode = function(value) {
if (isDefined(value))
isStrictMode = value;
return isStrictMode;
};
/**
* @ngdoc function
* @name ui.router.util.$urlMatcherFactory#defaultSquashPolicy
* @methodOf ui.router.util.$urlMatcherFactory
*
* @description
* Sets the default behavior when generating or matching URLs with default parameter values.
*
* @param {string} value A string that defines the default parameter URL squashing behavior.
* `nosquash`: When generating an href with a default parameter value, do not squash the parameter value from the URL
* `slash`: When generating an href with a default parameter value, squash (remove) the parameter value, and, if the
* parameter is surrounded by slashes, squash (remove) one slash from the URL
* any other string, e.g. "~": When generating an href with a default parameter value, squash (remove)
* the parameter value from the URL and replace it with this string.
*/
this.defaultSquashPolicy = function(value) {
if (!isDefined(value)) return defaultSquashPolicy;
if (value !== true && value !== false && !isString(value))
throw new Error("Invalid squash policy: " + value + ". Valid policies: false, true, arbitrary-string");
defaultSquashPolicy = value;
return value;
};
/**
* @ngdoc function
* @name ui.router.util.$urlMatcherFactory#compile
* @methodOf ui.router.util.$urlMatcherFactory
*
* @description
* Creates a {@link ui.router.util.type:UrlMatcher `UrlMatcher`} for the specified pattern.
*
* @param {string} pattern The URL pattern.
* @param {Object} config The config object hash.
* @returns {UrlMatcher} The UrlMatcher.
*/
this.compile = function (pattern, config) {
return new UrlMatcher(pattern, extend(getDefaultConfig(), config));
};
/**
* @ngdoc function
* @name ui.router.util.$urlMatcherFactory#isMatcher
* @methodOf ui.router.util.$urlMatcherFactory
*
* @description
* Returns true if the specified object is a `UrlMatcher`, or false otherwise.
*
* @param {Object} object The object to perform the type check against.
* @returns {Boolean} Returns `true` if the object matches the `UrlMatcher` interface, by
* implementing all the same methods.
*/
this.isMatcher = function (o) {
if (!isObject(o)) return false;
var result = true;
forEach(UrlMatcher.prototype, function(val, name) {
if (isFunction(val)) {
result = result && (isDefined(o[name]) && isFunction(o[name]));
}
});
return result;
};
/**
* @ngdoc function
* @name ui.router.util.$urlMatcherFactory#type
* @methodOf ui.router.util.$urlMatcherFactory
*
* @description
* Registers a custom {@link ui.router.util.type:Type `Type`} object that can be used to
* generate URLs with typed parameters.
*
* @param {string} name The type name.
* @param {Object|Function} definition The type definition. See
* {@link ui.router.util.type:Type `Type`} for information on the values accepted.
* @param {Object|Function} definitionFn (optional) A function that is injected before the app
* runtime starts. The result of this function is merged into the existing `definition`.
* See {@link ui.router.util.type:Type `Type`} for information on the values accepted.
*
* @returns {Object} Returns `$urlMatcherFactoryProvider`.
*
* @example
* This is a simple example of a custom type that encodes and decodes items from an
* array, using the array index as the URL-encoded value:
*
*
* var list = ['John', 'Paul', 'George', 'Ringo'];
*
* $urlMatcherFactoryProvider.type('listItem', {
* encode: function(item) {
* // Represent the list item in the URL using its corresponding index
* return list.indexOf(item);
* },
* decode: function(item) {
* // Look up the list item by index
* return list[parseInt(item, 10)];
* },
* is: function(item) {
* // Ensure the item is valid by checking to see that it appears
* // in the list
* return list.indexOf(item) > -1;
* }
* });
*
* $stateProvider.state('list', {
* url: "/list/{item:listItem}",
* controller: function($scope, $stateParams) {
* console.log($stateParams.item);
* }
* });
*
* // ...
*
* // Changes URL to '/list/3', logs "Ringo" to the console
* $state.go('list', { item: "Ringo" });
*
*
* This is a more complex example of a type that relies on dependency injection to
* interact with services, and uses the parameter name from the URL to infer how to
* handle encoding and decoding parameter values:
*
*
* // Defines a custom type that gets a value from a service,
* // where each service gets different types of values from
* // a backend API:
* $urlMatcherFactoryProvider.type('dbObject', {}, function(Users, Posts) {
*
* // Matches up services to URL parameter names
* var services = {
* user: Users,
* post: Posts
* };
*
* return {
* encode: function(object) {
* // Represent the object in the URL using its unique ID
* return object.id;
* },
* decode: function(value, key) {
* // Look up the object by ID, using the parameter
* // name (key) to call the correct service
* return services[key].findById(value);
* },
* is: function(object, key) {
* // Check that object is a valid dbObject
* return angular.isObject(object) && object.id && services[key];
* }
* equals: function(a, b) {
* // Check the equality of decoded objects by comparing
* // their unique IDs
* return a.id === b.id;
* }
* };
* });
*
* // In a config() block, you can then attach URLs with
* // type-annotated parameters:
* $stateProvider.state('users', {
* url: "/users",
* // ...
* }).state('users.item', {
* url: "/{user:dbObject}",
* controller: function($scope, $stateParams) {
* // $stateParams.user will now be an object returned from
* // the Users service
* },
* // ...
* });
*
*/
this.type = function (name, definition, definitionFn) {
if (!isDefined(definition)) return $types[name];
if ($types.hasOwnProperty(name)) throw new Error("A type named '" + name + "' has already been defined.");
$types[name] = new Type(extend({ name: name }, definition));
if (definitionFn) {
typeQueue.push({ name: name, def: definitionFn });
if (!enqueue) flushTypeQueue();
}
return this;
};
// `flushTypeQueue()` waits until `$urlMatcherFactory` is injected before invoking the queued `definitionFn`s
function flushTypeQueue() {
while(typeQueue.length) {
var type = typeQueue.shift();
if (type.pattern) throw new Error("You cannot override a type's .pattern at runtime.");
angular.extend($types[type.name], injector.invoke(type.def));
}
}
// Register default types. Store them in the prototype of $types.
forEach(defaultTypes, function(type, name) { $types[name] = new Type(extend({name: name}, type)); });
$types = inherit($types, {});
/* No need to document $get, since it returns this */
this.$get = ['$injector', function ($injector) {
injector = $injector;
enqueue = false;
flushTypeQueue();
forEach(defaultTypes, function(type, name) {
if (!$types[name]) $types[name] = new Type(type);
});
return this;
}];
this.Param = function Param(id, type, config, location) {
var self = this;
config = unwrapShorthand(config);
type = getType(config, type, location);
var arrayMode = getArrayMode();
type = arrayMode ? type.$asArray(arrayMode, location === "search") : type;
if (type.name === "string" && !arrayMode && location === "path" && config.value === undefined)
config.value = ""; // for 0.2.x; in 0.3.0+ do not automatically default to ""
var isOptional = config.value !== undefined;
var squash = getSquashPolicy(config, isOptional);
var replace = getReplace(config, arrayMode, isOptional, squash);
function unwrapShorthand(config) {
var keys = isObject(config) ? objectKeys(config) : [];
var isShorthand = indexOf(keys, "value") === -1 && indexOf(keys, "type") === -1 &&
indexOf(keys, "squash") === -1 && indexOf(keys, "array") === -1;
if (isShorthand) config = { value: config };
config.$$fn = isInjectable(config.value) ? config.value : function () { return config.value; };
return config;
}
function getType(config, urlType, location) {
if (config.type && urlType) throw new Error("Param '"+id+"' has two type configurations.");
if (urlType) return urlType;
if (!config.type) return (location === "config" ? $types.any : $types.string);
if (angular.isString(config.type))
return $types[config.type];
if (config.type instanceof Type)
return config.type;
return new Type(config.type);
}
// array config: param name (param[]) overrides default settings. explicit config overrides param name.
function getArrayMode() {
var arrayDefaults = { array: (location === "search" ? "auto" : false) };
var arrayParamNomenclature = id.match(/\[\]$/) ? { array: true } : {};
return extend(arrayDefaults, arrayParamNomenclature, config).array;
}
/**
* returns false, true, or the squash value to indicate the "default parameter url squash policy".
*/
function getSquashPolicy(config, isOptional) {
var squash = config.squash;
if (!isOptional || squash === false) return false;
if (!isDefined(squash) || squash == null) return defaultSquashPolicy;
if (squash === true || isString(squash)) return squash;
throw new Error("Invalid squash policy: '" + squash + "'. Valid policies: false, true, or arbitrary string");
}
function getReplace(config, arrayMode, isOptional, squash) {
var replace, configuredKeys, defaultPolicy = [
{ from: "", to: (isOptional || arrayMode ? undefined : "") },
{ from: null, to: (isOptional || arrayMode ? undefined : "") }
];
replace = isArray(config.replace) ? config.replace : [];
if (isString(squash))
replace.push({ from: squash, to: undefined });
configuredKeys = map(replace, function(item) { return item.from; } );
return filter(defaultPolicy, function(item) { return indexOf(configuredKeys, item.from) === -1; }).concat(replace);
}
/**
* [Internal] Get the default value of a parameter, which may be an injectable function.
*/
function $$getDefaultValue() {
if (!injector) throw new Error("Injectable functions cannot be called at configuration time");
var defaultValue = injector.invoke(config.$$fn);
if (defaultValue !== null && defaultValue !== undefined && !self.type.is(defaultValue))
throw new Error("Default value (" + defaultValue + ") for parameter '" + self.id + "' is not an instance of Type (" + self.type.name + ")");
return defaultValue;
}
/**
* [Internal] Gets the decoded representation of a value if the value is defined, otherwise, returns the
* default value, which may be the result of an injectable function.
*/
function $value(value) {
function hasReplaceVal(val) { return function(obj) { return obj.from === val; }; }
function $replace(value) {
var replacement = map(filter(self.replace, hasReplaceVal(value)), function(obj) { return obj.to; });
return replacement.length ? replacement[0] : value;
}
value = $replace(value);
return !isDefined(value) ? $$getDefaultValue() : self.type.$normalize(value);
}
function toString() { return "{Param:" + id + " " + type + " squash: '" + squash + "' optional: " + isOptional + "}"; }
extend(this, {
id: id,
type: type,
location: location,
array: arrayMode,
squash: squash,
replace: replace,
isOptional: isOptional,
value: $value,
dynamic: undefined,
config: config,
toString: toString
});
};
function ParamSet(params) {
extend(this, params || {});
}
ParamSet.prototype = {
$$new: function() {
return inherit(this, extend(new ParamSet(), { $$parent: this}));
},
$$keys: function () {
var keys = [], chain = [], parent = this,
ignore = objectKeys(ParamSet.prototype);
while (parent) { chain.push(parent); parent = parent.$$parent; }
chain.reverse();
forEach(chain, function(paramset) {
forEach(objectKeys(paramset), function(key) {
if (indexOf(keys, key) === -1 && indexOf(ignore, key) === -1) keys.push(key);
});
});
return keys;
},
$$values: function(paramValues) {
var values = {}, self = this;
forEach(self.$$keys(), function(key) {
values[key] = self[key].value(paramValues && paramValues[key]);
});
return values;
},
$$equals: function(paramValues1, paramValues2) {
var equal = true, self = this;
forEach(self.$$keys(), function(key) {
var left = paramValues1 && paramValues1[key], right = paramValues2 && paramValues2[key];
if (!self[key].type.equals(left, right)) equal = false;
});
return equal;
},
$$validates: function $$validate(paramValues) {
var keys = this.$$keys(), i, param, rawVal, normalized, encoded;
for (i = 0; i < keys.length; i++) {
param = this[keys[i]];
rawVal = paramValues[keys[i]];
if ((rawVal === undefined || rawVal === null) && param.isOptional)
break; // There was no parameter value, but the param is optional
normalized = param.type.$normalize(rawVal);
if (!param.type.is(normalized))
return false; // The value was not of the correct Type, and could not be decoded to the correct Type
encoded = param.type.encode(normalized);
if (angular.isString(encoded) && !param.type.pattern.exec(encoded))
return false; // The value was of the correct type, but when encoded, did not match the Type's regexp
}
return true;
},
$$parent: undefined
};
this.ParamSet = ParamSet;
}
// Register as a provider so it's available to other providers
angular.module('ui.router.util').provider('$urlMatcherFactory', $UrlMatcherFactory);
angular.module('ui.router.util').run(['$urlMatcherFactory', function($urlMatcherFactory) { }]);
/**
* @ngdoc object
* @name ui.router.router.$urlRouterProvider
*
* @requires ui.router.util.$urlMatcherFactoryProvider
* @requires $locationProvider
*
* @description
* `$urlRouterProvider` has the responsibility of watching `$location`.
* When `$location` changes it runs through a list of rules one by one until a
* match is found. `$urlRouterProvider` is used behind the scenes anytime you specify
* a url in a state configuration. All urls are compiled into a UrlMatcher object.
*
* There are several methods on `$urlRouterProvider` that make it useful to use directly
* in your module config.
*/
$UrlRouterProvider.$inject = ['$locationProvider', '$urlMatcherFactoryProvider'];
function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) {
var rules = [], otherwise = null, interceptDeferred = false, listener;
// Returns a string that is a prefix of all strings matching the RegExp
function regExpPrefix(re) {
var prefix = /^\^((?:\\[^a-zA-Z0-9]|[^\\\[\]\^$*+?.()|{}]+)*)/.exec(re.source);
return (prefix != null) ? prefix[1].replace(/\\(.)/g, "$1") : '';
}
// Interpolates matched values into a String.replace()-style pattern
function interpolate(pattern, match) {
return pattern.replace(/\$(\$|\d{1,2})/, function (m, what) {
return match[what === '$' ? 0 : Number(what)];
});
}
/**
* @ngdoc function
* @name ui.router.router.$urlRouterProvider#rule
* @methodOf ui.router.router.$urlRouterProvider
*
* @description
* Defines rules that are used by `$urlRouterProvider` to find matches for
* specific URLs.
*
* @example
*
* var app = angular.module('app', ['ui.router.router']);
*
* app.config(function ($urlRouterProvider) {
* // Here's an example of how you might allow case insensitive urls
* $urlRouterProvider.rule(function ($injector, $location) {
* var path = $location.path(),
* normalized = path.toLowerCase();
*
* if (path !== normalized) {
* return normalized;
* }
* });
* });
*
*
* @param {function} rule Handler function that takes `$injector` and `$location`
* services as arguments. You can use them to return a valid path as a string.
*
* @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance
*/
this.rule = function (rule) {
if (!isFunction(rule)) throw new Error("'rule' must be a function");
rules.push(rule);
return this;
};
/**
* @ngdoc object
* @name ui.router.router.$urlRouterProvider#otherwise
* @methodOf ui.router.router.$urlRouterProvider
*
* @description
* Defines a path that is used when an invalid route is requested.
*
* @example
*
* var app = angular.module('app', ['ui.router.router']);
*
* app.config(function ($urlRouterProvider) {
* // if the path doesn't match any of the urls you configured
* // otherwise will take care of routing the user to the
* // specified url
* $urlRouterProvider.otherwise('/index');
*
* // Example of using function rule as param
* $urlRouterProvider.otherwise(function ($injector, $location) {
* return '/a/valid/url';
* });
* });
*
*
* @param {string|function} rule The url path you want to redirect to or a function
* rule that returns the url path. The function version is passed two params:
* `$injector` and `$location` services, and must return a url string.
*
* @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance
*/
this.otherwise = function (rule) {
if (isString(rule)) {
var redirect = rule;
rule = function () { return redirect; };
}
else if (!isFunction(rule)) throw new Error("'rule' must be a function");
otherwise = rule;
return this;
};
function handleIfMatch($injector, handler, match) {
if (!match) return false;
var result = $injector.invoke(handler, handler, { $match: match });
return isDefined(result) ? result : true;
}
/**
* @ngdoc function
* @name ui.router.router.$urlRouterProvider#when
* @methodOf ui.router.router.$urlRouterProvider
*
* @description
* Registers a handler for a given url matching.
*
* If the handler is a string, it is
* treated as a redirect, and is interpolated according to the syntax of match
* (i.e. like `String.replace()` for `RegExp`, or like a `UrlMatcher` pattern otherwise).
*
* If the handler is a function, it is injectable. It gets invoked if `$location`
* matches. You have the option of inject the match object as `$match`.
*
* The handler can return
*
* - **falsy** to indicate that the rule didn't match after all, then `$urlRouter`
* will continue trying to find another one that matches.
* - **string** which is treated as a redirect and passed to `$location.url()`
* - **void** or any **truthy** value tells `$urlRouter` that the url was handled.
*
* @example
*
* var app = angular.module('app', ['ui.router.router']);
*
* app.config(function ($urlRouterProvider) {
* $urlRouterProvider.when($state.url, function ($match, $stateParams) {
* if ($state.$current.navigable !== state ||
* !equalForKeys($match, $stateParams) {
* $state.transitionTo(state, $match, false);
* }
* });
* });
*
*
* @param {string|object} what The incoming path that you want to redirect.
* @param {string|function} handler The path you want to redirect your user to.
*/
this.when = function (what, handler) {
var redirect, handlerIsString = isString(handler);
if (isString(what)) what = $urlMatcherFactory.compile(what);
if (!handlerIsString && !isFunction(handler) && !isArray(handler))
throw new Error("invalid 'handler' in when()");
var strategies = {
matcher: function (what, handler) {
if (handlerIsString) {
redirect = $urlMatcherFactory.compile(handler);
handler = ['$match', function ($match) { return redirect.format($match); }];
}
return extend(function ($injector, $location) {
return handleIfMatch($injector, handler, what.exec($location.path(), $location.search()));
}, {
prefix: isString(what.prefix) ? what.prefix : ''
});
},
regex: function (what, handler) {
if (what.global || what.sticky) throw new Error("when() RegExp must not be global or sticky");
if (handlerIsString) {
redirect = handler;
handler = ['$match', function ($match) { return interpolate(redirect, $match); }];
}
return extend(function ($injector, $location) {
return handleIfMatch($injector, handler, what.exec($location.path()));
}, {
prefix: regExpPrefix(what)
});
}
};
var check = { matcher: $urlMatcherFactory.isMatcher(what), regex: what instanceof RegExp };
for (var n in check) {
if (check[n]) return this.rule(strategies[n](what, handler));
}
throw new Error("invalid 'what' in when()");
};
/**
* @ngdoc function
* @name ui.router.router.$urlRouterProvider#deferIntercept
* @methodOf ui.router.router.$urlRouterProvider
*
* @description
* Disables (or enables) deferring location change interception.
*
* If you wish to customize the behavior of syncing the URL (for example, if you wish to
* defer a transition but maintain the current URL), call this method at configuration time.
* Then, at run time, call `$urlRouter.listen()` after you have configured your own
* `$locationChangeSuccess` event handler.
*
* @example
*
* var app = angular.module('app', ['ui.router.router']);
*
* app.config(function ($urlRouterProvider) {
*
* // Prevent $urlRouter from automatically intercepting URL changes;
* // this allows you to configure custom behavior in between
* // location changes and route synchronization:
* $urlRouterProvider.deferIntercept();
*
* }).run(function ($rootScope, $urlRouter, UserService) {
*
* $rootScope.$on('$locationChangeSuccess', function(e) {
* // UserService is an example service for managing user state
* if (UserService.isLoggedIn()) return;
*
* // Prevent $urlRouter's default handler from firing
* e.preventDefault();
*
* UserService.handleLogin().then(function() {
* // Once the user has logged in, sync the current URL
* // to the router:
* $urlRouter.sync();
* });
* });
*
* // Configures $urlRouter's listener *after* your custom listener
* $urlRouter.listen();
* });
*
*
* @param {boolean} defer Indicates whether to defer location change interception. Passing
no parameter is equivalent to `true`.
*/
this.deferIntercept = function (defer) {
if (defer === undefined) defer = true;
interceptDeferred = defer;
};
/**
* @ngdoc object
* @name ui.router.router.$urlRouter
*
* @requires $location
* @requires $rootScope
* @requires $injector
* @requires $browser
*
* @description
*
*/
this.$get = $get;
$get.$inject = ['$location', '$rootScope', '$injector', '$browser', '$sniffer'];
function $get( $location, $rootScope, $injector, $browser, $sniffer) {
var baseHref = $browser.baseHref(), location = $location.url(), lastPushedUrl;
function appendBasePath(url, isHtml5, absolute) {
if (baseHref === '/') return url;
if (isHtml5) return baseHref.slice(0, -1) + url;
if (absolute) return baseHref.slice(1) + url;
return url;
}
// TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree
function update(evt) {
if (evt && evt.defaultPrevented) return;
var ignoreUpdate = lastPushedUrl && $location.url() === lastPushedUrl;
lastPushedUrl = undefined;
// TODO: Re-implement this in 1.0 for https://github.com/angular-ui/ui-router/issues/1573
//if (ignoreUpdate) return true;
function check(rule) {
var handled = rule($injector, $location);
if (!handled) return false;
if (isString(handled)) $location.replace().url(handled);
return true;
}
var n = rules.length, i;
for (i = 0; i < n; i++) {
if (check(rules[i])) return;
}
// always check otherwise last to allow dynamic updates to the set of rules
if (otherwise) check(otherwise);
}
function listen() {
listener = listener || $rootScope.$on('$locationChangeSuccess', update);
return listener;
}
if (!interceptDeferred) listen();
return {
/**
* @ngdoc function
* @name ui.router.router.$urlRouter#sync
* @methodOf ui.router.router.$urlRouter
*
* @description
* Triggers an update; the same update that happens when the address bar url changes, aka `$locationChangeSuccess`.
* This method is useful when you need to use `preventDefault()` on the `$locationChangeSuccess` event,
* perform some custom logic (route protection, auth, config, redirection, etc) and then finally proceed
* with the transition by calling `$urlRouter.sync()`.
*
* @example
*
* angular.module('app', ['ui.router'])
* .run(function($rootScope, $urlRouter) {
* $rootScope.$on('$locationChangeSuccess', function(evt) {
* // Halt state change from even starting
* evt.preventDefault();
* // Perform custom logic
* var meetsRequirement = ...
* // Continue with the update and state transition if logic allows
* if (meetsRequirement) $urlRouter.sync();
* });
* });
*
*/
sync: function() {
update();
},
listen: function() {
return listen();
},
update: function(read) {
if (read) {
location = $location.url();
return;
}
if ($location.url() === location) return;
$location.url(location);
$location.replace();
},
push: function(urlMatcher, params, options) {
var url = urlMatcher.format(params || {});
// Handle the special hash param, if needed
if (url !== null && params && params['#']) {
url += '#' + params['#'];
}
$location.url(url);
lastPushedUrl = options && options.$$avoidResync ? $location.url() : undefined;
if (options && options.replace) $location.replace();
},
/**
* @ngdoc function
* @name ui.router.router.$urlRouter#href
* @methodOf ui.router.router.$urlRouter
*
* @description
* A URL generation method that returns the compiled URL for a given
* {@link ui.router.util.type:UrlMatcher `UrlMatcher`}, populated with the provided parameters.
*
* @example
*
* $bob = $urlRouter.href(new UrlMatcher("/about/:person"), {
* person: "bob"
* });
* // $bob == "/about/bob";
*
*
* @param {UrlMatcher} urlMatcher The `UrlMatcher` object which is used as the template of the URL to generate.
* @param {object=} params An object of parameter values to fill the matcher's required parameters.
* @param {object=} options Options object. The options are:
*
* - **`absolute`** - {boolean=false}, If true will generate an absolute url, e.g. "http://www.example.com/fullurl".
*
* @returns {string} Returns the fully compiled URL, or `null` if `params` fail validation against `urlMatcher`
*/
href: function(urlMatcher, params, options) {
if (!urlMatcher.validates(params)) return null;
var isHtml5 = $locationProvider.html5Mode();
if (angular.isObject(isHtml5)) {
isHtml5 = isHtml5.enabled;
}
isHtml5 = isHtml5 && $sniffer.history;
var url = urlMatcher.format(params);
options = options || {};
if (!isHtml5 && url !== null) {
url = "#" + $locationProvider.hashPrefix() + url;
}
// Handle special hash param, if needed
if (url !== null && params && params['#']) {
url += '#' + params['#'];
}
url = appendBasePath(url, isHtml5, options.absolute);
if (!options.absolute || !url) {
return url;
}
var slash = (!isHtml5 && url ? '/' : ''), port = $location.port();
port = (port === 80 || port === 443 ? '' : ':' + port);
return [$location.protocol(), '://', $location.host(), port, slash, url].join('');
}
};
}
}
angular.module('ui.router.router').provider('$urlRouter', $UrlRouterProvider);
/**
* @ngdoc object
* @name ui.router.state.$stateProvider
*
* @requires ui.router.router.$urlRouterProvider
* @requires ui.router.util.$urlMatcherFactoryProvider
*
* @description
* The new `$stateProvider` works similar to Angular's v1 router, but it focuses purely
* on state.
*
* A state corresponds to a "place" in the application in terms of the overall UI and
* navigation. A state describes (via the controller / template / view properties) what
* the UI looks like and does at that place.
*
* States often have things in common, and the primary way of factoring out these
* commonalities in this model is via the state hierarchy, i.e. parent/child states aka
* nested states.
*
* The `$stateProvider` provides interfaces to declare these states for your app.
*/
$StateProvider.$inject = ['$urlRouterProvider', '$urlMatcherFactoryProvider'];
function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
var root, states = {}, $state, queue = {}, abstractKey = 'abstract';
// Builds state properties from definition passed to registerState()
var stateBuilder = {
// Derive parent state from a hierarchical name only if 'parent' is not explicitly defined.
// state.children = [];
// if (parent) parent.children.push(state);
parent: function(state) {
if (isDefined(state.parent) && state.parent) return findState(state.parent);
// regex matches any valid composite state name
// would match "contact.list" but not "contacts"
var compositeName = /^(.+)\.[^.]+$/.exec(state.name);
return compositeName ? findState(compositeName[1]) : root;
},
// inherit 'data' from parent and override by own values (if any)
data: function(state) {
if (state.parent && state.parent.data) {
state.data = state.self.data = inherit(state.parent.data, state.data);
}
return state.data;
},
// Build a URLMatcher if necessary, either via a relative or absolute URL
url: function(state) {
var url = state.url, config = { params: state.params || {} };
if (isString(url)) {
if (url.charAt(0) == '^') return $urlMatcherFactory.compile(url.substring(1), config);
return (state.parent.navigable || root).url.concat(url, config);
}
if (!url || $urlMatcherFactory.isMatcher(url)) return url;
throw new Error("Invalid url '" + url + "' in state '" + state + "'");
},
// Keep track of the closest ancestor state that has a URL (i.e. is navigable)
navigable: function(state) {
return state.url ? state : (state.parent ? state.parent.navigable : null);
},
// Own parameters for this state. state.url.params is already built at this point. Create and add non-url params
ownParams: function(state) {
var params = state.url && state.url.params || new $$UMFP.ParamSet();
forEach(state.params || {}, function(config, id) {
if (!params[id]) params[id] = new $$UMFP.Param(id, null, config, "config");
});
return params;
},
// Derive parameters for this state and ensure they're a super-set of parent's parameters
params: function(state) {
var ownParams = pick(state.ownParams, state.ownParams.$$keys());
return state.parent && state.parent.params ? extend(state.parent.params.$$new(), ownParams) : new $$UMFP.ParamSet();
},
// If there is no explicit multi-view configuration, make one up so we don't have
// to handle both cases in the view directive later. Note that having an explicit
// 'views' property will mean the default unnamed view properties are ignored. This
// is also a good time to resolve view names to absolute names, so everything is a
// straight lookup at link time.
views: function(state) {
var views = {};
forEach(isDefined(state.views) ? state.views : { '': state }, function (view, name) {
if (name.indexOf('@') < 0) name += '@' + state.parent.name;
view.resolveAs = view.resolveAs || state.resolveAs || '$resolve';
views[name] = view;
});
return views;
},
// Keep a full path from the root down to this state as this is needed for state activation.
path: function(state) {
return state.parent ? state.parent.path.concat(state) : []; // exclude root from path
},
// Speed up $state.contains() as it's used a lot
includes: function(state) {
var includes = state.parent ? extend({}, state.parent.includes) : {};
includes[state.name] = true;
return includes;
},
$delegates: {}
};
function isRelative(stateName) {
return stateName.indexOf(".") === 0 || stateName.indexOf("^") === 0;
}
function findState(stateOrName, base) {
if (!stateOrName) return undefined;
var isStr = isString(stateOrName),
name = isStr ? stateOrName : stateOrName.name,
path = isRelative(name);
if (path) {
if (!base) throw new Error("No reference point given for path '" + name + "'");
base = findState(base);
var rel = name.split("."), i = 0, pathLength = rel.length, current = base;
for (; i < pathLength; i++) {
if (rel[i] === "" && i === 0) {
current = base;
continue;
}
if (rel[i] === "^") {
if (!current.parent) throw new Error("Path '" + name + "' not valid for state '" + base.name + "'");
current = current.parent;
continue;
}
break;
}
rel = rel.slice(i).join(".");
name = current.name + (current.name && rel ? "." : "") + rel;
}
var state = states[name];
if (state && (isStr || (!isStr && (state === stateOrName || state.self === stateOrName)))) {
return state;
}
return undefined;
}
function queueState(parentName, state) {
if (!queue[parentName]) {
queue[parentName] = [];
}
queue[parentName].push(state);
}
function flushQueuedChildren(parentName) {
var queued = queue[parentName] || [];
while(queued.length) {
registerState(queued.shift());
}
}
function registerState(state) {
// Wrap a new object around the state so we can store our private details easily.
state = inherit(state, {
self: state,
resolve: state.resolve || {},
toString: function() { return this.name; }
});
var name = state.name;
if (!isString(name) || name.indexOf('@') >= 0) throw new Error("State must have a valid name");
if (states.hasOwnProperty(name)) throw new Error("State '" + name + "' is already defined");
// Get parent name
var parentName = (name.indexOf('.') !== -1) ? name.substring(0, name.lastIndexOf('.'))
: (isString(state.parent)) ? state.parent
: (isObject(state.parent) && isString(state.parent.name)) ? state.parent.name
: '';
// If parent is not registered yet, add state to queue and register later
if (parentName && !states[parentName]) {
return queueState(parentName, state.self);
}
for (var key in stateBuilder) {
if (isFunction(stateBuilder[key])) state[key] = stateBuilder[key](state, stateBuilder.$delegates[key]);
}
states[name] = state;
// Register the state in the global state list and with $urlRouter if necessary.
if (!state[abstractKey] && state.url) {
$urlRouterProvider.when(state.url, ['$match', '$stateParams', function ($match, $stateParams) {
if ($state.$current.navigable != state || !equalForKeys($match, $stateParams)) {
$state.transitionTo(state, $match, { inherit: true, location: false });
}
}]);
}
// Register any queued children
flushQueuedChildren(name);
return state;
}
// Checks text to see if it looks like a glob.
function isGlob (text) {
return text.indexOf('*') > -1;
}
// Returns true if glob matches current $state name.
function doesStateMatchGlob (glob) {
var globSegments = glob.split('.'),
segments = $state.$current.name.split('.');
//match single stars
for (var i = 0, l = globSegments.length; i < l; i++) {
if (globSegments[i] === '*') {
segments[i] = '*';
}
}
//match greedy starts
if (globSegments[0] === '**') {
segments = segments.slice(indexOf(segments, globSegments[1]));
segments.unshift('**');
}
//match greedy ends
if (globSegments[globSegments.length - 1] === '**') {
segments.splice(indexOf(segments, globSegments[globSegments.length - 2]) + 1, Number.MAX_VALUE);
segments.push('**');
}
if (globSegments.length != segments.length) {
return false;
}
return segments.join('') === globSegments.join('');
}
// Implicit root state that is always active
root = registerState({
name: '',
url: '^',
views: null,
'abstract': true
});
root.navigable = null;
/**
* @ngdoc function
* @name ui.router.state.$stateProvider#decorator
* @methodOf ui.router.state.$stateProvider
*
* @description
* Allows you to extend (carefully) or override (at your own peril) the
* `stateBuilder` object used internally by `$stateProvider`. This can be used
* to add custom functionality to ui-router, for example inferring templateUrl
* based on the state name.
*
* When passing only a name, it returns the current (original or decorated) builder
* function that matches `name`.
*
* The builder functions that can be decorated are listed below. Though not all
* necessarily have a good use case for decoration, that is up to you to decide.
*
* In addition, users can attach custom decorators, which will generate new
* properties within the state's internal definition. There is currently no clear
* use-case for this beyond accessing internal states (i.e. $state.$current),
* however, expect this to become increasingly relevant as we introduce additional
* meta-programming features.
*
* **Warning**: Decorators should not be interdependent because the order of
* execution of the builder functions in non-deterministic. Builder functions
* should only be dependent on the state definition object and super function.
*
*
* Existing builder functions and current return values:
*
* - **parent** `{object}` - returns the parent state object.
* - **data** `{object}` - returns state data, including any inherited data that is not
* overridden by own values (if any).
* - **url** `{object}` - returns a {@link ui.router.util.type:UrlMatcher UrlMatcher}
* or `null`.
* - **navigable** `{object}` - returns closest ancestor state that has a URL (aka is
* navigable).
* - **params** `{object}` - returns an array of state params that are ensured to
* be a super-set of parent's params.
* - **views** `{object}` - returns a views object where each key is an absolute view
* name (i.e. "viewName@stateName") and each value is the config object
* (template, controller) for the view. Even when you don't use the views object
* explicitly on a state config, one is still created for you internally.
* So by decorating this builder function you have access to decorating template
* and controller properties.
* - **ownParams** `{object}` - returns an array of params that belong to the state,
* not including any params defined by ancestor states.
* - **path** `{string}` - returns the full path from the root down to this state.
* Needed for state activation.
* - **includes** `{object}` - returns an object that includes every state that
* would pass a `$state.includes()` test.
*
* @example
*
* // Override the internal 'views' builder with a function that takes the state
* // definition, and a reference to the internal function being overridden:
* $stateProvider.decorator('views', function (state, parent) {
* var result = {},
* views = parent(state);
*
* angular.forEach(views, function (config, name) {
* var autoName = (state.name + '.' + name).replace('.', '/');
* config.templateUrl = config.templateUrl || '/partials/' + autoName + '.html';
* result[name] = config;
* });
* return result;
* });
*
* $stateProvider.state('home', {
* views: {
* 'contact.list': { controller: 'ListController' },
* 'contact.item': { controller: 'ItemController' }
* }
* });
*
* // ...
*
* $state.go('home');
* // Auto-populates list and item views with /partials/home/contact/list.html,
* // and /partials/home/contact/item.html, respectively.
*
*
* @param {string} name The name of the builder function to decorate.
* @param {object} func A function that is responsible for decorating the original
* builder function. The function receives two parameters:
*
* - `{object}` - state - The state config object.
* - `{object}` - super - The original builder function.
*
* @return {object} $stateProvider - $stateProvider instance
*/
this.decorator = decorator;
function decorator(name, func) {
/*jshint validthis: true */
if (isString(name) && !isDefined(func)) {
return stateBuilder[name];
}
if (!isFunction(func) || !isString(name)) {
return this;
}
if (stateBuilder[name] && !stateBuilder.$delegates[name]) {
stateBuilder.$delegates[name] = stateBuilder[name];
}
stateBuilder[name] = func;
return this;
}
/**
* @ngdoc function
* @name ui.router.state.$stateProvider#state
* @methodOf ui.router.state.$stateProvider
*
* @description
* Registers a state configuration under a given state name. The stateConfig object
* has the following acceptable properties.
*
* @param {string} name A unique state name, e.g. "home", "about", "contacts".
* To create a parent/child state use a dot, e.g. "about.sales", "home.newest".
* @param {object} stateConfig State configuration object.
* @param {string|function=} stateConfig.template
*
* html template as a string or a function that returns
* an html template as a string which should be used by the uiView directives. This property
* takes precedence over templateUrl.
*
* If `template` is a function, it will be called with the following parameters:
*
* - {array.<object>} - state parameters extracted from the current $location.path() by
* applying the current state
*
* template:
* "inline template definition " +
* "
"
* template: function(params) {
* return "generated template "; }
*
*
* @param {string|function=} stateConfig.templateUrl
*