diff --git a/app/bower_components/angular-bootstrap/.bower.json b/app/bower_components/angular-bootstrap/.bower.json new file mode 100644 index 0000000..ba4deca --- /dev/null +++ b/app/bower_components/angular-bootstrap/.bower.json @@ -0,0 +1,24 @@ +{ + "author": { + "name": "https://github.com/angular-ui/bootstrap/graphs/contributors" + }, + "name": "angular-bootstrap", + "version": "0.11.0", + "main": [ + "./ui-bootstrap-tpls.js" + ], + "dependencies": { + "angular": ">=1" + }, + "homepage": "https://github.com/angular-ui/bootstrap-bower", + "_release": "0.11.0", + "_resolution": { + "type": "version", + "tag": "0.11.0", + "commit": "75b302f82c1a3b0647695a3dfeacab0a153ea8a0" + }, + "_source": "git://github.com/angular-ui/bootstrap-bower.git", + "_target": "~0.11.0", + "_originalSource": "angular-bootstrap", + "_direct": true +} \ No newline at end of file diff --git a/app/bower_components/angular-bootstrap/bower.json b/app/bower_components/angular-bootstrap/bower.json new file mode 100644 index 0000000..6793432 --- /dev/null +++ b/app/bower_components/angular-bootstrap/bower.json @@ -0,0 +1,11 @@ +{ + "author": { + "name": "https://github.com/angular-ui/bootstrap/graphs/contributors" + }, + "name": "angular-bootstrap", + "version": "0.11.0", + "main": ["./ui-bootstrap-tpls.js"], + "dependencies": { + "angular": ">=1" + } +} diff --git a/app/bower_components/angular-bootstrap/ui-bootstrap-tpls.js b/app/bower_components/angular-bootstrap/ui-bootstrap-tpls.js new file mode 100644 index 0000000..bcca1cd --- /dev/null +++ b/app/bower_components/angular-bootstrap/ui-bootstrap-tpls.js @@ -0,0 +1,4116 @@ +/* + * angular-ui-bootstrap + * http://angular-ui.github.io/bootstrap/ + + * Version: 0.11.0 - 2014-05-01 + * License: MIT + */ +angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","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", ["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/day.html","template/datepicker/month.html","template/datepicker/popup.html","template/datepicker/year.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]); +angular.module('ui.bootstrap.transition', []) + +/** + * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete. + * @param {DOMElement} element The DOMElement that will be animated. + * @param {string|object|function} trigger The thing that will cause the transition to start: + * - As a string, it represents the css class to be added to the element. + * - As an object, it represents a hash of style attributes to be applied to the element. + * - As a function, it represents a function to be called that will cause the transition to occur. + * @return {Promise} A promise that is resolved when the transition finishes. + */ +.factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) { + + var $transition = function(element, trigger, options) { + options = options || {}; + var deferred = $q.defer(); + var endEventName = $transition[options.animation ? 'animationEndEventName' : 'transitionEndEventName']; + + var transitionEndHandler = function(event) { + $rootScope.$apply(function() { + element.unbind(endEventName, transitionEndHandler); + deferred.resolve(element); + }); + }; + + if (endEventName) { + element.bind(endEventName, transitionEndHandler); + } + + // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur + $timeout(function() { + if ( angular.isString(trigger) ) { + element.addClass(trigger); + } else if ( angular.isFunction(trigger) ) { + trigger(element); + } else if ( angular.isObject(trigger) ) { + element.css(trigger); + } + //If browser does not support transitions, instantly resolve + if ( !endEventName ) { + deferred.resolve(element); + } + }); + + // Add our custom cancel function to the promise that is returned + // We can call this if we are about to run a new transition, which we know will prevent this transition from ending, + // i.e. it will therefore never raise a transitionEnd event for that transition + deferred.promise.cancel = function() { + if ( endEventName ) { + element.unbind(endEventName, transitionEndHandler); + } + deferred.reject('Transition cancelled'); + }; + + return deferred.promise; + }; + + // Work out the name of the transitionEnd event + var transElement = document.createElement('trans'); + var transitionEndEventNames = { + 'WebkitTransition': 'webkitTransitionEnd', + 'MozTransition': 'transitionend', + 'OTransition': 'oTransitionEnd', + 'transition': 'transitionend' + }; + var animationEndEventNames = { + 'WebkitTransition': 'webkitAnimationEnd', + 'MozTransition': 'animationend', + 'OTransition': 'oAnimationEnd', + 'transition': 'animationend' + }; + function findEndEventName(endEventNames) { + for (var name in endEventNames){ + if (transElement.style[name] !== undefined) { + return endEventNames[name]; + } + } + } + $transition.transitionEndEventName = findEndEventName(transitionEndEventNames); + $transition.animationEndEventName = findEndEventName(animationEndEventNames); + return $transition; +}]); + +angular.module('ui.bootstrap.collapse', ['ui.bootstrap.transition']) + + .directive('collapse', ['$transition', function ($transition) { + + return { + link: function (scope, element, attrs) { + + var initialAnimSkip = true; + var currentTransition; + + function doTransition(change) { + var newTransition = $transition(element, change); + if (currentTransition) { + currentTransition.cancel(); + } + currentTransition = newTransition; + newTransition.then(newTransitionDone, newTransitionDone); + return newTransition; + + function newTransitionDone() { + // Make sure it's this transition, otherwise, leave it alone. + if (currentTransition === newTransition) { + currentTransition = undefined; + } + } + } + + function expand() { + if (initialAnimSkip) { + initialAnimSkip = false; + expandDone(); + } else { + element.removeClass('collapse').addClass('collapsing'); + doTransition({ height: element[0].scrollHeight + 'px' }).then(expandDone); + } + } + + function expandDone() { + element.removeClass('collapsing'); + element.addClass('collapse in'); + element.css({height: 'auto'}); + } + + function collapse() { + if (initialAnimSkip) { + initialAnimSkip = false; + collapseDone(); + element.css({height: 0}); + } else { + // CSS transitions don't work with height: auto, so we have to manually change the height to a specific value + element.css({ height: element[0].scrollHeight + 'px' }); + //trigger reflow so a browser realizes that height was updated from auto to a specific value + var x = element[0].offsetWidth; + + element.removeClass('collapse in').addClass('collapsing'); + + doTransition({ height: 0 }).then(collapseDone); + } + } + + function collapseDone() { + element.removeClass('collapsing'); + element.addClass('collapse'); + } + + scope.$watch(attrs.collapse, function (shouldCollapse) { + if (shouldCollapse) { + collapse(); + } else { + expand(); + } + }); + } + }; + }]); + +angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) + +.constant('accordionConfig', { + closeOthers: true +}) + +.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', 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('accordion', function () { + return { + restrict:'EA', + controller:'AccordionController', + transclude: true, + replace: false, + templateUrl: 'template/accordion/accordion.html' + }; +}) + +// The accordion-group directive indicates a block of html that will expand and collapse in an accordion +.directive('accordionGroup', function() { + return { + require:'^accordion', // We need this directive to be inside an accordion + restrict:'EA', + transclude:true, // It transcludes the contents of the directive into the template + replace: true, // The element containing the directive will be replaced with the template + templateUrl:'template/accordion/accordion-group.html', + scope: { + heading: '@', // Interpolate the heading attribute onto this scope + isOpen: '=?', + isDisabled: '=?' + }, + controller: function() { + this.setHeading = function(element) { + this.heading = element; + }; + }, + link: function(scope, element, attrs, accordionCtrl) { + accordionCtrl.addGroup(scope); + + scope.$watch('isOpen', function(value) { + if ( value ) { + accordionCtrl.closeOthers(scope); + } + }); + + scope.toggleOpen = function() { + if ( !scope.isDisabled ) { + scope.isOpen = !scope.isOpen; + } + }; + } + }; +}) + +// Use accordion-heading below an accordion-group to provide a heading containing HTML +// +// Heading containing HTML - +// +.directive('accordionHeading', function() { + return { + restrict: 'EA', + transclude: true, // Grab the contents to be used as the heading + template: '', // In effect remove this element! + replace: true, + require: '^accordionGroup', + link: function(scope, element, attr, 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, function() {})); + } + }; +}) + +// 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('accordionTransclude', function() { + return { + require: '^accordionGroup', + link: function(scope, element, attr, controller) { + scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) { + if ( heading ) { + element.html(''); + element.append(heading); + } + }); + } + }; +}); + +angular.module('ui.bootstrap.alert', []) + +.controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) { + $scope.closeable = 'close' in $attrs; +}]) + +.directive('alert', function () { + return { + restrict:'EA', + controller:'AlertController', + templateUrl:'template/alert/alert.html', + transclude:true, + replace:true, + scope: { + type: '@', + close: '&' + } + }; +}); + +angular.module('ui.bootstrap.bindHtml', []) + + .directive('bindHtmlUnsafe', function () { + return function (scope, element, attr) { + element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe); + scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) { + element.html(value || ''); + }); + }; + }); +angular.module('ui.bootstrap.buttons', []) + +.constant('buttonConfig', { + activeClass: 'active', + toggleEvent: 'click' +}) + +.controller('ButtonsController', ['buttonConfig', function(buttonConfig) { + this.activeClass = buttonConfig.activeClass || 'active'; + this.toggleEvent = buttonConfig.toggleEvent || 'click'; +}]) + +.directive('btnRadio', function () { + return { + require: ['btnRadio', 'ngModel'], + controller: 'ButtonsController', + link: function (scope, element, attrs, ctrls) { + var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + //model -> UI + ngModelCtrl.$render = function () { + element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio))); + }; + + //ui->model + element.bind(buttonsCtrl.toggleEvent, function () { + var isActive = element.hasClass(buttonsCtrl.activeClass); + + if (!isActive || angular.isDefined(attrs.uncheckable)) { + scope.$apply(function () { + ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio)); + ngModelCtrl.$render(); + }); + } + }); + } + }; +}) + +.directive('btnCheckbox', function () { + return { + require: ['btnCheckbox', 'ngModel'], + controller: 'ButtonsController', + link: function (scope, element, attrs, ctrls) { + var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + function getTrueValue() { + return getCheckboxValue(attrs.btnCheckboxTrue, true); + } + + function getFalseValue() { + return getCheckboxValue(attrs.btnCheckboxFalse, false); + } + + function getCheckboxValue(attributeValue, defaultValue) { + var val = scope.$eval(attributeValue); + return angular.isDefined(val) ? val : defaultValue; + } + + //model -> UI + ngModelCtrl.$render = function () { + element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue())); + }; + + //ui->model + element.bind(buttonsCtrl.toggleEvent, function () { + scope.$apply(function () { + ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue()); + ngModelCtrl.$render(); + }); + }); + } + }; +}); + +/** +* @ngdoc overview +* @name ui.bootstrap.carousel +* +* @description +* AngularJS version of an image carousel. +* +*/ +angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition']) +.controller('CarouselController', ['$scope', '$timeout', '$transition', function ($scope, $timeout, $transition) { + var self = this, + slides = self.slides = $scope.slides = [], + currentIndex = -1, + currentTimeout, isPlaying; + self.currentSlide = null; + + var destroyed = false; + /* direction: "prev" or "next" */ + self.select = $scope.select = function(nextSlide, direction) { + var nextIndex = slides.indexOf(nextSlide); + //Decide direction if it's not given + if (direction === undefined) { + direction = nextIndex > currentIndex ? 'next' : 'prev'; + } + if (nextSlide && nextSlide !== self.currentSlide) { + if ($scope.$currentTransition) { + $scope.$currentTransition.cancel(); + //Timeout so ng-class in template has time to fix classes for finished slide + $timeout(goNext); + } else { + goNext(); + } + } + function goNext() { + // Scope has been destroyed, stop here. + if (destroyed) { return; } + //If we have a slide to transition from and we have a transition type and we're allowed, go + if (self.currentSlide && angular.isString(direction) && !$scope.noTransition && nextSlide.$element) { + //We shouldn't do class manip in here, but it's the same weird thing bootstrap does. need to fix sometime + nextSlide.$element.addClass(direction); + var reflow = nextSlide.$element[0].offsetWidth; //force reflow + + //Set all other slides to stop doing their stuff for the new transition + angular.forEach(slides, function(slide) { + angular.extend(slide, {direction: '', entering: false, leaving: false, active: false}); + }); + angular.extend(nextSlide, {direction: direction, active: true, entering: true}); + angular.extend(self.currentSlide||{}, {direction: direction, leaving: true}); + + $scope.$currentTransition = $transition(nextSlide.$element, {}); + //We have to create new pointers inside a closure since next & current will change + (function(next,current) { + $scope.$currentTransition.then( + function(){ transitionDone(next, current); }, + function(){ transitionDone(next, current); } + ); + }(nextSlide, self.currentSlide)); + } else { + transitionDone(nextSlide, self.currentSlide); + } + self.currentSlide = nextSlide; + currentIndex = nextIndex; + //every time you change slides, reset the timer + restartTimer(); + } + function transitionDone(next, current) { + angular.extend(next, {direction: '', active: true, leaving: false, entering: false}); + angular.extend(current||{}, {direction: '', active: false, leaving: false, entering: false}); + $scope.$currentTransition = null; + } + }; + $scope.$on('$destroy', function () { + destroyed = true; + }); + + /* Allow outside people to call indexOf on slides array */ + self.indexOfSlide = function(slide) { + return slides.indexOf(slide); + }; + + $scope.next = function() { + var newIndex = (currentIndex + 1) % slides.length; + + //Prevent this user-triggered transition from occurring if there is already one in progress + if (!$scope.$currentTransition) { + return self.select(slides[newIndex], 'next'); + } + }; + + $scope.prev = function() { + var newIndex = currentIndex - 1 < 0 ? slides.length - 1 : currentIndex - 1; + + //Prevent this user-triggered transition from occurring if there is already one in progress + if (!$scope.$currentTransition) { + return self.select(slides[newIndex], 'prev'); + } + }; + + $scope.isActive = function(slide) { + return self.currentSlide === slide; + }; + + $scope.$watch('interval', restartTimer); + $scope.$on('$destroy', resetTimer); + + function restartTimer() { + resetTimer(); + var interval = +$scope.interval; + if (!isNaN(interval) && interval>=0) { + currentTimeout = $timeout(timerFn, interval); + } + } + + function resetTimer() { + if (currentTimeout) { + $timeout.cancel(currentTimeout); + currentTimeout = null; + } + } + + function timerFn() { + if (isPlaying) { + $scope.next(); + restartTimer(); + } else { + $scope.pause(); + } + } + + $scope.play = function() { + if (!isPlaying) { + isPlaying = true; + restartTimer(); + } + }; + $scope.pause = function() { + if (!$scope.noPause) { + isPlaying = false; + resetTimer(); + } + }; + + self.addSlide = function(slide, element) { + slide.$element = element; + slides.push(slide); + //if this is the first slide or the slide is set to active, select it + if(slides.length === 1 || slide.active) { + self.select(slides[slides.length-1]); + if (slides.length == 1) { + $scope.play(); + } + } else { + slide.active = false; + } + }; + + self.removeSlide = function(slide) { + //get the index of the slide inside the carousel + var index = slides.indexOf(slide); + slides.splice(index, 1); + if (slides.length > 0 && slide.active) { + if (index >= slides.length) { + self.select(slides[index-1]); + } else { + self.select(slides[index]); + } + } else if (currentIndex > index) { + currentIndex--; + } + }; + +}]) + +/** + * @ngdoc directive + * @name ui.bootstrap.carousel.directive:carousel + * @restrict EA + * + * @description + * Carousel is the outer container for a set of image 'slides' to showcase. + * + * @param {number=} interval The time, in milliseconds, that it will take the carousel to go to the next slide. + * @param {boolean=} noTransition Whether to disable transitions on the carousel. + * @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover). + * + * @example + + + + + + + + + + + + + + + .carousel-indicators { + top: auto; + bottom: 15px; + } + + + */ +.directive('carousel', [function() { + return { + restrict: 'EA', + transclude: true, + replace: true, + controller: 'CarouselController', + require: 'carousel', + templateUrl: 'template/carousel/carousel.html', + scope: { + interval: '=', + noTransition: '=', + noPause: '=' + } + }; +}]) + +/** + * @ngdoc directive + * @name ui.bootstrap.carousel.directive:slide + * @restrict EA + * + * @description + * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}. Must be placed as a child of a carousel element. + * + * @param {boolean=} active Model binding, whether or not this slide is currently active. + * + * @example + + +
+ + + + + + + Interval, in milliseconds: +
Enter a negative number to stop the interval. +
+
+ +function CarouselDemoCtrl($scope) { + $scope.myInterval = 5000; +} + + + .carousel-indicators { + top: auto; + bottom: 15px; + } + +
+*/ + +.directive('slide', function() { + return { + require: '^carousel', + restrict: 'EA', + transclude: true, + replace: true, + templateUrl: 'template/carousel/slide.html', + scope: { + active: '=?' + }, + link: function (scope, element, attrs, carouselCtrl) { + carouselCtrl.addSlide(scope, element); + //when the scope is destroyed then remove the slide from the current slides array + scope.$on('$destroy', function() { + carouselCtrl.removeSlide(scope); + }); + + scope.$watch('active', function(active) { + if (active) { + carouselCtrl.select(scope); + } + }); + } + }; +}); + +angular.module('ui.bootstrap.dateparser', []) + +.service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) { + + this.parsers = {}; + + var formatCodeToRegex = { + 'yyyy': { + regex: '\\d{4}', + apply: function(value) { this.year = +value; } + }, + 'yy': { + regex: '\\d{2}', + apply: function(value) { this.year = +value + 2000; } + }, + 'y': { + regex: '\\d{1,4}', + apply: function(value) { this.year = +value; } + }, + 'MMMM': { + regex: $locale.DATETIME_FORMATS.MONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); } + }, + 'MMM': { + regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); } + }, + 'MM': { + regex: '0[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; } + }, + 'M': { + regex: '[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; } + }, + 'dd': { + regex: '[0-2][0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; } + }, + 'd': { + regex: '[1-2]?[0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; } + }, + 'EEEE': { + regex: $locale.DATETIME_FORMATS.DAY.join('|') + }, + 'EEE': { + regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|') + } + }; + + this.createParser = function(format) { + var map = [], regex = format.split(''); + + angular.forEach(formatCodeToRegex, function(data, code) { + var index = format.indexOf(code); + + if (index > -1) { + format = format.split(''); + + regex[index] = '(' + data.regex + ')'; + format[index] = '$'; // Custom symbol to define consumed part of format + for (var i = index + 1, n = index + code.length; i < n; i++) { + regex[i] = ''; + format[i] = '$'; + } + format = format.join(''); + + map.push({ index: index, apply: data.apply }); + } + }); + + return { + regex: new RegExp('^' + regex.join('') + '$'), + map: orderByFilter(map, 'index') + }; + }; + + this.parse = function(input, format) { + if ( !angular.isString(input) ) { + return input; + } + + format = $locale.DATETIME_FORMATS[format] || format; + + if ( !this.parsers[format] ) { + this.parsers[format] = this.createParser(format); + } + + var parser = this.parsers[format], + regex = parser.regex, + map = parser.map, + results = input.match(regex); + + if ( results && results.length ) { + var fields = { year: 1900, month: 0, date: 1, hours: 0 }, dt; + + for( var i = 1, n = results.length; i < n; i++ ) { + var mapper = map[i-1]; + if ( mapper.apply ) { + mapper.apply.call(fields, results[i]); + } + } + + if ( isValid(fields.year, fields.month, fields.date) ) { + dt = new Date( fields.year, fields.month, fields.date, fields.hours); + } + + 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 ( month === 1 && date > 28) { + return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0); + } + + if ( month === 3 || month === 5 || month === 8 || month === 10) { + return date < 31; + } + + return true; + } +}]); + +angular.module('ui.bootstrap.position', []) + +/** + * A set of utility methods that can be use to retrieve position of DOM elements. + * It is meant to be used where we need to absolute-position DOM elements in + * relation to other, existing elements (this is the case for tooltips, popovers, + * typeahead suggestions etc.). + */ + .factory('$position', ['$document', '$window', function ($document, $window) { + + function getStyle(el, cssprop) { + if (el.currentStyle) { //IE + return el.currentStyle[cssprop]; + } else if ($window.getComputedStyle) { + return $window.getComputedStyle(el)[cssprop]; + } + // finally try and get inline style + return el.style[cssprop]; + } + + /** + * Checks if a given element is statically positioned + * @param element - raw DOM element + */ + function isStaticPositioned(element) { + return (getStyle(element, 'position') || 'static' ) === 'static'; + } + + /** + * returns the closest, non-statically positioned parentOffset of a given element + * @param element + */ + var parentOffsetEl = function (element) { + var docDomEl = $document[0]; + var offsetParent = element.offsetParent || docDomEl; + while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { + offsetParent = offsetParent.offsetParent; + } + return offsetParent || docDomEl; + }; + + return { + /** + * Provides read-only equivalent of jQuery's position function: + * http://api.jquery.com/position/ + */ + position: function (element) { + var elBCR = this.offset(element); + var offsetParentBCR = { top: 0, left: 0 }; + var offsetParentEl = parentOffsetEl(element[0]); + if (offsetParentEl != $document[0]) { + offsetParentBCR = this.offset(angular.element(offsetParentEl)); + offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; + offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; + } + + var boundingClientRect = element[0].getBoundingClientRect(); + return { + width: boundingClientRect.width || element.prop('offsetWidth'), + height: boundingClientRect.height || element.prop('offsetHeight'), + top: elBCR.top - offsetParentBCR.top, + left: elBCR.left - offsetParentBCR.left + }; + }, + + /** + * Provides read-only equivalent of jQuery's offset function: + * http://api.jquery.com/offset/ + */ + offset: function (element) { + var boundingClientRect = element[0].getBoundingClientRect(); + return { + width: boundingClientRect.width || element.prop('offsetWidth'), + height: boundingClientRect.height || element.prop('offsetHeight'), + top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop), + left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft) + }; + }, + + /** + * Provides coordinates for the targetEl in relation to hostEl + */ + positionElements: function (hostEl, targetEl, positionStr, appendToBody) { + + var positionStrParts = positionStr.split('-'); + var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center'; + + var hostElPos, + targetElWidth, + targetElHeight, + targetElPos; + + hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl); + + targetElWidth = targetEl.prop('offsetWidth'); + targetElHeight = targetEl.prop('offsetHeight'); + + var shiftWidth = { + center: function () { + return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2; + }, + left: function () { + return hostElPos.left; + }, + right: function () { + return hostElPos.left + hostElPos.width; + } + }; + + var shiftHeight = { + center: function () { + return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2; + }, + top: function () { + return hostElPos.top; + }, + bottom: function () { + return hostElPos.top + hostElPos.height; + } + }; + + switch (pos0) { + case 'right': + targetElPos = { + top: shiftHeight[pos1](), + left: shiftWidth[pos0]() + }; + break; + case 'left': + targetElPos = { + top: shiftHeight[pos1](), + left: hostElPos.left - targetElWidth + }; + break; + case 'bottom': + targetElPos = { + top: shiftHeight[pos0](), + left: shiftWidth[pos1]() + }; + break; + default: + targetElPos = { + top: hostElPos.top - targetElHeight, + left: shiftWidth[pos1]() + }; + break; + } + + return targetElPos; + } + }; + }]); + +angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position']) + +.constant('datepickerConfig', { + formatDay: 'dd', + formatMonth: 'MMMM', + formatYear: 'yyyy', + formatDayHeader: 'EEE', + formatDayTitle: 'MMMM yyyy', + formatMonthTitle: 'yyyy', + datepickerMode: 'day', + minMode: 'day', + maxMode: 'year', + showWeeks: true, + startingDay: 0, + yearRange: 20, + minDate: null, + maxDate: null +}) + +.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) { + var self = this, + ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl; + + // Modes chain + this.modes = ['day', 'month', 'year']; + + // Configuration attributes + angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', + 'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange'], function( key, index ) { + self[key] = angular.isDefined($attrs[key]) ? (index < 8 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key]; + }); + + // Watchable attributes + angular.forEach(['minDate', 'maxDate'], function( key ) { + if ( $attrs[key] ) { + $scope.$parent.$watch($parse($attrs[key]), function(value) { + self[key] = value ? new Date(value) : null; + self.refreshView(); + }); + } else { + self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null; + } + }); + + $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode; + $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000); + this.activeDate = angular.isDefined($attrs.initDate) ? $scope.$parent.$eval($attrs.initDate) : new Date(); + + $scope.isActive = function(dateObject) { + if (self.compare(dateObject.date, self.activeDate) === 0) { + $scope.activeDateId = dateObject.uid; + return true; + } + return false; + }; + + this.init = function( ngModelCtrl_ ) { + ngModelCtrl = ngModelCtrl_; + + ngModelCtrl.$render = function() { + self.render(); + }; + }; + + this.render = function() { + if ( ngModelCtrl.$modelValue ) { + var date = new Date( ngModelCtrl.$modelValue ), + isValid = !isNaN(date); + + if ( isValid ) { + this.activeDate = date; + } else { + $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); + } + ngModelCtrl.$setValidity('date', isValid); + } + this.refreshView(); + }; + + this.refreshView = function() { + if ( this.element ) { + this._refreshView(); + + var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; + ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date))); + } + }; + + this.createDateObject = function(date, format) { + var model = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; + return { + date: date, + label: dateFilter(date, format), + selected: model && this.compare(date, model) === 0, + disabled: this.isDisabled(date), + current: this.compare(date, new Date()) === 0 + }; + }; + + this.isDisabled = function( date ) { + return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($attrs.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode}))); + }; + + // 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.$modelValue ? new Date( ngModelCtrl.$modelValue ) : new Date(0, 0, 0, 0, 0, 0, 0); + dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() ); + ngModelCtrl.$setViewValue( dt ); + ngModelCtrl.$render(); + } else { + self.activeDate = date; + $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ]; + } + }; + + $scope.move = function( direction ) { + var year = self.activeDate.getFullYear() + direction * (self.step.years || 0), + month = self.activeDate.getMonth() + direction * (self.step.months || 0); + self.activeDate.setFullYear(year, month, 1); + self.refreshView(); + }; + + $scope.toggleMode = function( direction ) { + direction = direction || 1; + + if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) { + return; + } + + $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) + direction ]; + }; + + // Key event mapper + $scope.keys = { 13:'enter', 32:'space', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left', 38:'up', 39:'right', 40:'down' }; + + var focusElement = function() { + $timeout(function() { + self.element[0].focus(); + }, 0 , false); + }; + + // Listen for focus requests from popup directive + $scope.$on('datepicker.focus', focusElement); + + $scope.keydown = function( evt ) { + var key = $scope.keys[evt.which]; + + if ( !key || evt.shiftKey || evt.altKey ) { + return; + } + + evt.preventDefault(); + evt.stopPropagation(); + + if (key === 'enter' || key === 'space') { + if ( self.isDisabled(self.activeDate)) { + return; // do nothing + } + $scope.select(self.activeDate); + focusElement(); + } else if (evt.ctrlKey && (key === 'up' || key === 'down')) { + $scope.toggleMode(key === 'up' ? 1 : -1); + focusElement(); + } else { + self.handleKeyDown(key, evt); + self.refreshView(); + } + }; +}]) + +.directive( 'datepicker', function () { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/datepicker.html', + scope: { + datepickerMode: '=?', + dateDisabled: '&' + }, + require: ['datepicker', '?^ngModel'], + controller: 'DatepickerController', + link: function(scope, element, attrs, ctrls) { + var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if ( ngModelCtrl ) { + datepickerCtrl.init( ngModelCtrl ); + } + } + }; +}) + +.directive('daypicker', ['dateFilter', function (dateFilter) { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/day.html', + require: '^datepicker', + link: function(scope, element, attrs, ctrl) { + scope.showWeeks = ctrl.showWeeks; + + ctrl.step = { months: 1 }; + ctrl.element = element; + + var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + function getDaysInMonth( year, month ) { + return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month]; + } + + function getDates(startDate, n) { + var dates = new Array(n), current = new Date(startDate), i = 0; + current.setHours(12); // Prevent repeated dates because of timezone bug + while ( i < n ) { + dates[i++] = new Date(current); + current.setDate( current.getDate() + 1 ); + } + return dates; + } + + ctrl._refreshView = function() { + var year = ctrl.activeDate.getFullYear(), + month = ctrl.activeDate.getMonth(), + firstDayOfMonth = new Date(year, month, 1), + difference = ctrl.startingDay - firstDayOfMonth.getDay(), + numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, + firstDate = new Date(firstDayOfMonth); + + if ( numDisplayedFromPreviousMonth > 0 ) { + firstDate.setDate( - numDisplayedFromPreviousMonth + 1 ); + } + + // 42 is the number of days on a six-month calendar + var days = getDates(firstDate, 42); + for (var i = 0; i < 42; i ++) { + days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), { + secondary: days[i].getMonth() !== month, + uid: scope.uniqueId + '-' + i + }); + } + + scope.labels = new Array(7); + for (var j = 0; j < 7; j++) { + scope.labels[j] = { + abbr: dateFilter(days[j].date, ctrl.formatDayHeader), + full: dateFilter(days[j].date, 'EEEE') + }; + } + + scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle); + scope.rows = ctrl.split(days, 7); + + if ( scope.showWeeks ) { + scope.weekNumbers = []; + var weekNumber = getISO8601WeekNumber( scope.rows[0][0].date ), + numWeeks = scope.rows.length; + while( scope.weekNumbers.push(weekNumber++) < numWeeks ) {} + } + }; + + ctrl.compare = function(date1, date2) { + return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) ); + }; + + function getISO8601WeekNumber(date) { + var checkDate = new Date(date); + checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday + var time = checkDate.getTime(); + checkDate.setMonth(0); // Compare with Jan 1 + checkDate.setDate(1); + return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; + } + + ctrl.handleKeyDown = function( key, evt ) { + var date = ctrl.activeDate.getDate(); + + if (key === 'left') { + date = date - 1; // up + } else if (key === 'up') { + date = date - 7; // down + } else if (key === 'right') { + date = date + 1; // down + } else if (key === 'down') { + date = date + 7; + } else if (key === 'pageup' || key === 'pagedown') { + var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1); + ctrl.activeDate.setMonth(month, 1); + date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date); + } else if (key === 'home') { + date = 1; + } else if (key === 'end') { + date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()); + } + ctrl.activeDate.setDate(date); + }; + + ctrl.refreshView(); + } + }; +}]) + +.directive('monthpicker', ['dateFilter', function (dateFilter) { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/month.html', + require: '^datepicker', + link: function(scope, element, attrs, ctrl) { + ctrl.step = { years: 1 }; + ctrl.element = element; + + ctrl._refreshView = function() { + var months = new Array(12), + year = ctrl.activeDate.getFullYear(); + + for ( var i = 0; i < 12; i++ ) { + months[i] = angular.extend(ctrl.createDateObject(new Date(year, i, 1), ctrl.formatMonth), { + uid: scope.uniqueId + '-' + i + }); + } + + scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle); + scope.rows = ctrl.split(months, 3); + }; + + ctrl.compare = function(date1, date2) { + return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() ); + }; + + ctrl.handleKeyDown = function( key, evt ) { + var date = ctrl.activeDate.getMonth(); + + if (key === 'left') { + date = date - 1; // up + } else if (key === 'up') { + date = date - 3; // down + } else if (key === 'right') { + date = date + 1; // down + } else if (key === 'down') { + date = date + 3; + } else if (key === 'pageup' || key === 'pagedown') { + var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); + ctrl.activeDate.setFullYear(year); + } else if (key === 'home') { + date = 0; + } else if (key === 'end') { + date = 11; + } + ctrl.activeDate.setMonth(date); + }; + + ctrl.refreshView(); + } + }; +}]) + +.directive('yearpicker', ['dateFilter', function (dateFilter) { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/year.html', + require: '^datepicker', + link: function(scope, element, attrs, ctrl) { + var range = ctrl.yearRange; + + ctrl.step = { years: range }; + ctrl.element = element; + + function getStartingYear( year ) { + return parseInt((year - 1) / range, 10) * range + 1; + } + + ctrl._refreshView = function() { + var years = new Array(range); + + for ( var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++ ) { + years[i] = angular.extend(ctrl.createDateObject(new Date(start + i, 0, 1), ctrl.formatYear), { + uid: scope.uniqueId + '-' + i + }); + } + + scope.title = [years[0].label, years[range - 1].label].join(' - '); + scope.rows = ctrl.split(years, 5); + }; + + ctrl.compare = function(date1, date2) { + return date1.getFullYear() - date2.getFullYear(); + }; + + ctrl.handleKeyDown = function( key, evt ) { + var date = ctrl.activeDate.getFullYear(); + + if (key === 'left') { + date = date - 1; // up + } else if (key === 'up') { + date = date - 5; // down + } else if (key === 'right') { + date = date + 1; // down + } else if (key === 'down') { + date = date + 5; + } else if (key === 'pageup' || key === 'pagedown') { + date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years; + } else if (key === 'home') { + date = getStartingYear( ctrl.activeDate.getFullYear() ); + } else if (key === 'end') { + date = getStartingYear( ctrl.activeDate.getFullYear() ) + range - 1; + } + ctrl.activeDate.setFullYear(date); + }; + + ctrl.refreshView(); + } + }; +}]) + +.constant('datepickerPopupConfig', { + datepickerPopup: 'yyyy-MM-dd', + currentText: 'Today', + clearText: 'Clear', + closeText: 'Done', + closeOnDateSelection: true, + appendToBody: false, + showButtonBar: true +}) + +.directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig', +function ($compile, $parse, $document, $position, dateFilter, dateParser, datepickerPopupConfig) { + return { + restrict: 'EA', + require: 'ngModel', + scope: { + isOpen: '=?', + currentText: '@', + clearText: '@', + closeText: '@', + dateDisabled: '&' + }, + link: function(scope, element, attrs, ngModel) { + var dateFormat, + closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection, + appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody; + + scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar; + + scope.getText = function( key ) { + return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; + }; + + attrs.$observe('datepickerPopup', function(value) { + dateFormat = value || datepickerPopupConfig.datepickerPopup; + ngModel.$render(); + }); + + // popup element used to display calendar + var popupEl = angular.element('
'); + popupEl.attr({ + 'ng-model': 'date', + 'ng-change': 'dateSelection()' + }); + + function cameltoDash( string ){ + return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); + } + + // datepicker element + var datepickerEl = angular.element(popupEl.children()[0]); + if ( attrs.datepickerOptions ) { + angular.forEach(scope.$parent.$eval(attrs.datepickerOptions), function( value, option ) { + datepickerEl.attr( cameltoDash(option), value ); + }); + } + + angular.forEach(['minDate', 'maxDate'], function( key ) { + if ( attrs[key] ) { + scope.$parent.$watch($parse(attrs[key]), function(value){ + scope[key] = value; + }); + datepickerEl.attr(cameltoDash(key), key); + } + }); + if (attrs.dateDisabled) { + datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); + } + + function parseDate(viewValue) { + if (!viewValue) { + ngModel.$setValidity('date', true); + return null; + } else if (angular.isDate(viewValue) && !isNaN(viewValue)) { + ngModel.$setValidity('date', true); + return viewValue; + } else if (angular.isString(viewValue)) { + var date = dateParser.parse(viewValue, dateFormat) || new Date(viewValue); + if (isNaN(date)) { + ngModel.$setValidity('date', false); + return undefined; + } else { + ngModel.$setValidity('date', true); + return date; + } + } else { + ngModel.$setValidity('date', false); + return undefined; + } + } + ngModel.$parsers.unshift(parseDate); + + // Inner change + scope.dateSelection = function(dt) { + if (angular.isDefined(dt)) { + scope.date = dt; + } + ngModel.$setViewValue(scope.date); + ngModel.$render(); + + if ( closeOnDateSelection ) { + scope.isOpen = false; + element[0].focus(); + } + }; + + element.bind('input change keyup', function() { + scope.$apply(function() { + scope.date = ngModel.$modelValue; + }); + }); + + // Outter change + ngModel.$render = function() { + var date = ngModel.$viewValue ? dateFilter(ngModel.$viewValue, dateFormat) : ''; + element.val(date); + scope.date = parseDate( ngModel.$modelValue ); + }; + + var documentClickBind = function(event) { + if (scope.isOpen && event.target !== element[0]) { + scope.$apply(function() { + scope.isOpen = false; + }); + } + }; + + var keydown = function(evt, noApply) { + scope.keydown(evt); + }; + element.bind('keydown', keydown); + + scope.keydown = function(evt) { + if (evt.which === 27) { + evt.preventDefault(); + evt.stopPropagation(); + scope.close(); + } else if (evt.which === 40 && !scope.isOpen) { + scope.isOpen = true; + } + }; + + scope.$watch('isOpen', function(value) { + if (value) { + scope.$broadcast('datepicker.focus'); + scope.position = appendToBody ? $position.offset(element) : $position.position(element); + scope.position.top = scope.position.top + element.prop('offsetHeight'); + + $document.bind('click', documentClickBind); + } else { + $document.unbind('click', documentClickBind); + } + }); + + scope.select = function( date ) { + if (date === 'today') { + var today = new Date(); + if (angular.isDate(ngModel.$modelValue)) { + date = new Date(ngModel.$modelValue); + date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); + } else { + date = new Date(today.setHours(0, 0, 0, 0)); + } + } + scope.dateSelection( date ); + }; + + scope.close = function() { + scope.isOpen = false; + element[0].focus(); + }; + + var $popup = $compile(popupEl)(scope); + if ( appendToBody ) { + $document.find('body').append($popup); + } else { + element.after($popup); + } + + scope.$on('$destroy', function() { + $popup.remove(); + element.unbind('keydown', keydown); + $document.unbind('click', documentClickBind); + }); + } + }; +}]) + +.directive('datepickerPopupWrap', function() { + return { + restrict:'EA', + replace: true, + transclude: true, + templateUrl: 'template/datepicker/popup.html', + link:function (scope, element, attrs) { + element.bind('click', function(event) { + event.preventDefault(); + event.stopPropagation(); + }); + } + }; +}); + +angular.module('ui.bootstrap.dropdown', []) + +.constant('dropdownConfig', { + openClass: 'open' +}) + +.service('dropdownService', ['$document', function($document) { + var openScope = null; + + this.open = function( dropdownScope ) { + if ( !openScope ) { + $document.bind('click', closeDropdown); + $document.bind('keydown', escapeKeyBind); + } + + if ( openScope && openScope !== dropdownScope ) { + openScope.isOpen = false; + } + + openScope = dropdownScope; + }; + + this.close = function( dropdownScope ) { + if ( openScope === dropdownScope ) { + openScope = null; + $document.unbind('click', closeDropdown); + $document.unbind('keydown', escapeKeyBind); + } + }; + + var closeDropdown = function( evt ) { + if (evt && evt.isDefaultPrevented()) { + return; + } + + openScope.$apply(function() { + openScope.isOpen = false; + }); + }; + + var escapeKeyBind = function( evt ) { + if ( evt.which === 27 ) { + openScope.focusToggleElement(); + closeDropdown(); + } + }; +}]) + +.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate) { + var self = this, + scope = $scope.$new(), // create a child scope so we are not polluting original one + openClass = dropdownConfig.openClass, + getIsOpen, + setIsOpen = angular.noop, + toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop; + + this.init = function( element ) { + self.$element = element; + + if ( $attrs.isOpen ) { + getIsOpen = $parse($attrs.isOpen); + setIsOpen = getIsOpen.assign; + + $scope.$watch(getIsOpen, function(value) { + scope.isOpen = !!value; + }); + } + }; + + this.toggle = function( open ) { + return scope.isOpen = arguments.length ? !!open : !scope.isOpen; + }; + + // Allow other directives to watch status + this.isOpen = function() { + return scope.isOpen; + }; + + scope.focusToggleElement = function() { + if ( self.toggleElement ) { + self.toggleElement[0].focus(); + } + }; + + scope.$watch('isOpen', function( isOpen, wasOpen ) { + $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass); + + if ( isOpen ) { + scope.focusToggleElement(); + dropdownService.open( scope ); + } else { + dropdownService.close( scope ); + } + + setIsOpen($scope, isOpen); + if (angular.isDefined(isOpen) && isOpen !== wasOpen) { + toggleInvoker($scope, { open: !!isOpen }); + } + }); + + $scope.$on('$locationChangeSuccess', function() { + scope.isOpen = false; + }); + + $scope.$on('$destroy', function() { + scope.$destroy(); + }); +}]) + +.directive('dropdown', function() { + return { + restrict: 'CA', + controller: 'DropdownController', + link: function(scope, element, attrs, dropdownCtrl) { + dropdownCtrl.init( element ); + } + }; +}) + +.directive('dropdownToggle', function() { + return { + restrict: 'CA', + require: '?^dropdown', + link: function(scope, element, attrs, dropdownCtrl) { + if ( !dropdownCtrl ) { + return; + } + + dropdownCtrl.toggleElement = element; + + var toggleDropdown = function(event) { + event.preventDefault(); + + if ( !element.hasClass('disabled') && !attrs.disabled ) { + scope.$apply(function() { + dropdownCtrl.toggle(); + }); + } + }; + + element.bind('click', toggleDropdown); + + // WAI-ARIA + element.attr({ 'aria-haspopup': true, 'aria-expanded': false }); + scope.$watch(dropdownCtrl.isOpen, function( isOpen ) { + element.attr('aria-expanded', !!isOpen); + }); + + scope.$on('$destroy', function() { + element.unbind('click', toggleDropdown); + }); + } + }; +}); + +angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) + +/** + * A helper, internal data structure that acts as a map but also allows getting / removing + * elements in the LIFO order + */ + .factory('$$stackedMap', function () { + return { + createNew: function () { + var stack = []; + + return { + add: function (key, value) { + stack.push({ + key: key, + value: value + }); + }, + get: function (key) { + for (var i = 0; i < stack.length; i++) { + if (key == stack[i].key) { + return stack[i]; + } + } + }, + keys: function() { + var keys = []; + for (var i = 0; i < stack.length; i++) { + keys.push(stack[i].key); + } + return keys; + }, + top: function () { + return stack[stack.length - 1]; + }, + remove: function (key) { + var idx = -1; + for (var i = 0; i < stack.length; i++) { + if (key == stack[i].key) { + idx = i; + break; + } + } + return stack.splice(idx, 1)[0]; + }, + removeTop: function () { + return stack.splice(stack.length - 1, 1)[0]; + }, + length: function () { + return stack.length; + } + }; + } + }; + }) + +/** + * A helper directive for the $modal service. It creates a backdrop element. + */ + .directive('modalBackdrop', ['$timeout', function ($timeout) { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/modal/backdrop.html', + link: function (scope) { + + scope.animate = false; + + //trigger CSS transitions + $timeout(function () { + scope.animate = true; + }); + } + }; + }]) + + .directive('modalWindow', ['$modalStack', '$timeout', function ($modalStack, $timeout) { + return { + restrict: 'EA', + scope: { + index: '@', + animate: '=' + }, + replace: true, + transclude: true, + templateUrl: function(tElement, tAttrs) { + return tAttrs.templateUrl || 'template/modal/window.html'; + }, + link: function (scope, element, attrs) { + element.addClass(attrs.windowClass || ''); + scope.size = attrs.size; + + $timeout(function () { + // trigger CSS transitions + scope.animate = true; + // focus a freshly-opened modal + element[0].focus(); + }); + + 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'); + } + }; + } + }; + }]) + + .factory('$modalStack', ['$transition', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap', + function ($transition, $timeout, $document, $compile, $rootScope, $$stackedMap) { + + var OPENED_MODAL_CLASS = 'modal-open'; + + var backdropDomEl, backdropScope; + var openedWindows = $$stackedMap.createNew(); + var $modalStack = {}; + + function backdropIndex() { + var topBackdropIndex = -1; + var opened = openedWindows.keys(); + for (var i = 0; i < opened.length; i++) { + if (openedWindows.get(opened[i]).value.backdrop) { + topBackdropIndex = i; + } + } + return topBackdropIndex; + } + + $rootScope.$watch(backdropIndex, function(newBackdropIndex){ + if (backdropScope) { + backdropScope.index = newBackdropIndex; + } + }); + + function removeModalWindow(modalInstance) { + + var body = $document.find('body').eq(0); + var modalWindow = openedWindows.get(modalInstance).value; + + //clean up the stack + openedWindows.remove(modalInstance); + + //remove window DOM element + removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, function() { + modalWindow.modalScope.$destroy(); + body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0); + checkRemoveBackdrop(); + }); + } + + function checkRemoveBackdrop() { + //remove backdrop if no longer needed + if (backdropDomEl && backdropIndex() == -1) { + var backdropScopeRef = backdropScope; + removeAfterAnimate(backdropDomEl, backdropScope, 150, function () { + backdropScopeRef.$destroy(); + backdropScopeRef = null; + }); + backdropDomEl = undefined; + backdropScope = undefined; + } + } + + function removeAfterAnimate(domEl, scope, emulateTime, done) { + // Closing animation + scope.animate = false; + + var transitionEndEventName = $transition.transitionEndEventName; + if (transitionEndEventName) { + // transition out + var timeout = $timeout(afterAnimating, emulateTime); + + domEl.bind(transitionEndEventName, function () { + $timeout.cancel(timeout); + afterAnimating(); + scope.$apply(); + }); + } else { + // Ensure this call is async + $timeout(afterAnimating, 0); + } + + function afterAnimating() { + if (afterAnimating.done) { + return; + } + afterAnimating.done = true; + + domEl.remove(); + if (done) { + done(); + } + } + } + + $document.bind('keydown', function (evt) { + var modal; + + if (evt.which === 27) { + modal = openedWindows.top(); + if (modal && modal.value.keyboard) { + evt.preventDefault(); + $rootScope.$apply(function () { + $modalStack.dismiss(modal.key, 'escape key press'); + }); + } + } + }); + + $modalStack.open = function (modalInstance, modal) { + + openedWindows.add(modalInstance, { + deferred: modal.deferred, + modalScope: modal.scope, + backdrop: modal.backdrop, + keyboard: modal.keyboard + }); + + var body = $document.find('body').eq(0), + currBackdropIndex = backdropIndex(); + + if (currBackdropIndex >= 0 && !backdropDomEl) { + backdropScope = $rootScope.$new(true); + backdropScope.index = currBackdropIndex; + backdropDomEl = $compile('
')(backdropScope); + body.append(backdropDomEl); + } + + var angularDomEl = angular.element('
'); + angularDomEl.attr({ + 'template-url': modal.windowTemplateUrl, + 'window-class': modal.windowClass, + 'size': modal.size, + 'index': openedWindows.length() - 1, + 'animate': 'animate' + }).html(modal.content); + + var modalDomEl = $compile(angularDomEl)(modal.scope); + openedWindows.top().value.modalDomEl = modalDomEl; + body.append(modalDomEl); + body.addClass(OPENED_MODAL_CLASS); + }; + + $modalStack.close = function (modalInstance, result) { + var modalWindow = openedWindows.get(modalInstance).value; + if (modalWindow) { + modalWindow.deferred.resolve(result); + removeModalWindow(modalInstance); + } + }; + + $modalStack.dismiss = function (modalInstance, reason) { + var modalWindow = openedWindows.get(modalInstance).value; + if (modalWindow) { + modalWindow.deferred.reject(reason); + removeModalWindow(modalInstance); + } + }; + + $modalStack.dismissAll = function (reason) { + var topModal = this.getTop(); + while (topModal) { + this.dismiss(topModal.key, reason); + topModal = this.getTop(); + } + }; + + $modalStack.getTop = function () { + return openedWindows.top(); + }; + + return $modalStack; + }]) + + .provider('$modal', function () { + + var $modalProvider = { + options: { + backdrop: true, //can be also false or 'static' + keyboard: true + }, + $get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack', + function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) { + + var $modal = {}; + + function getTemplatePromise(options) { + return options.template ? $q.when(options.template) : + $http.get(options.templateUrl, {cache: $templateCache}).then(function (result) { + return result.data; + }); + } + + function getResolvePromises(resolves) { + var promisesArr = []; + angular.forEach(resolves, function (value, key) { + if (angular.isFunction(value) || angular.isArray(value)) { + promisesArr.push($q.when($injector.invoke(value))); + } + }); + return promisesArr; + } + + $modal.open = function (modalOptions) { + + var modalResultDeferred = $q.defer(); + var modalOpenedDeferred = $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, + close: function (result) { + $modalStack.close(modalInstance, result); + }, + dismiss: function (reason) { + $modalStack.dismiss(modalInstance, reason); + } + }; + + //merge and clean up options + modalOptions = angular.extend({}, $modalProvider.options, modalOptions); + modalOptions.resolve = modalOptions.resolve || {}; + + //verify options + if (!modalOptions.template && !modalOptions.templateUrl) { + throw new Error('One of template or templateUrl options is required.'); + } + + var templateAndResolvePromise = + $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve))); + + + templateAndResolvePromise.then(function resolveSuccess(tplAndVars) { + + var modalScope = (modalOptions.scope || $rootScope).$new(); + modalScope.$close = modalInstance.close; + modalScope.$dismiss = modalInstance.dismiss; + + var ctrlInstance, ctrlLocals = {}; + var resolveIter = 1; + + //controllers + if (modalOptions.controller) { + ctrlLocals.$scope = modalScope; + ctrlLocals.$modalInstance = modalInstance; + angular.forEach(modalOptions.resolve, function (value, key) { + ctrlLocals[key] = tplAndVars[resolveIter++]; + }); + + ctrlInstance = $controller(modalOptions.controller, ctrlLocals); + } + + $modalStack.open(modalInstance, { + scope: modalScope, + deferred: modalResultDeferred, + content: tplAndVars[0], + backdrop: modalOptions.backdrop, + keyboard: modalOptions.keyboard, + windowClass: modalOptions.windowClass, + windowTemplateUrl: modalOptions.windowTemplateUrl, + size: modalOptions.size + }); + + }, function resolveError(reason) { + modalResultDeferred.reject(reason); + }); + + templateAndResolvePromise.then(function () { + modalOpenedDeferred.resolve(true); + }, function () { + modalOpenedDeferred.reject(false); + }); + + return modalInstance; + }; + + return $modal; + }] + }; + + return $modalProvider; + }); + +angular.module('ui.bootstrap.pagination', []) + +.controller('PaginationController', ['$scope', '$attrs', '$parse', function ($scope, $attrs, $parse) { + var self = this, + ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl + setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; + + this.init = function(ngModelCtrl_, config) { + ngModelCtrl = ngModelCtrl_; + this.config = config; + + ngModelCtrl.$render = function() { + self.render(); + }; + + if ($attrs.itemsPerPage) { + $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) { + self.itemsPerPage = parseInt(value, 10); + $scope.totalPages = self.calculateTotalPages(); + }); + } else { + this.itemsPerPage = config.itemsPerPage; + } + }; + + this.calculateTotalPages = function() { + var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage); + return Math.max(totalPages || 0, 1); + }; + + this.render = function() { + $scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1; + }; + + $scope.selectPage = function(page) { + if ( $scope.page !== page && page > 0 && page <= $scope.totalPages) { + ngModelCtrl.$setViewValue(page); + ngModelCtrl.$render(); + } + }; + + $scope.getText = function( key ) { + return $scope[key + 'Text'] || self.config[key + 'Text']; + }; + $scope.noPrevious = function() { + return $scope.page === 1; + }; + $scope.noNext = function() { + return $scope.page === $scope.totalPages; + }; + + $scope.$watch('totalItems', function() { + $scope.totalPages = self.calculateTotalPages(); + }); + + $scope.$watch('totalPages', function(value) { + setNumPages($scope.$parent, value); // Readonly variable + + if ( $scope.page > value ) { + $scope.selectPage(value); + } else { + ngModelCtrl.$render(); + } + }); +}]) + +.constant('paginationConfig', { + itemsPerPage: 10, + boundaryLinks: false, + directionLinks: true, + firstText: 'First', + previousText: 'Previous', + nextText: 'Next', + lastText: 'Last', + rotate: true +}) + +.directive('pagination', ['$parse', 'paginationConfig', function($parse, paginationConfig) { + return { + restrict: 'EA', + scope: { + totalItems: '=', + firstText: '@', + previousText: '@', + nextText: '@', + lastText: '@' + }, + require: ['pagination', '?ngModel'], + controller: 'PaginationController', + templateUrl: 'template/pagination/pagination.html', + replace: true, + link: function(scope, element, attrs, ctrls) { + var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if (!ngModelCtrl) { + return; // do nothing if no ng-model + } + + // Setup configuration parameters + var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize, + rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate; + scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks; + scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks; + + paginationCtrl.init(ngModelCtrl, paginationConfig); + + if (attrs.maxSize) { + scope.$parent.$watch($parse(attrs.maxSize), function(value) { + maxSize = parseInt(value, 10); + paginationCtrl.render(); + }); + } + + // Create page object used in template + function makePage(number, text, isActive) { + return { + number: number, + text: text, + active: isActive + }; + } + + function getPages(currentPage, totalPages) { + var pages = []; + + // Default page limits + var startPage = 1, endPage = totalPages; + var isMaxSized = ( angular.isDefined(maxSize) && maxSize < totalPages ); + + // recompute if maxSize + if ( isMaxSized ) { + if ( rotate ) { + // Current page is displayed in the middle of the visible ones + startPage = Math.max(currentPage - Math.floor(maxSize/2), 1); + endPage = startPage + maxSize - 1; + + // Adjust if limit is exceeded + if (endPage > totalPages) { + endPage = totalPages; + startPage = endPage - maxSize + 1; + } + } else { + // Visible pages are paginated with maxSize + startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1; + + // Adjust last page if limit is exceeded + endPage = Math.min(startPage + maxSize - 1, totalPages); + } + } + + // Add page number links + for (var number = startPage; number <= endPage; number++) { + var page = makePage(number, number, number === currentPage); + pages.push(page); + } + + // Add links to move between page sets + if ( isMaxSized && ! rotate ) { + if ( startPage > 1 ) { + var previousPageSet = makePage(startPage - 1, '...', false); + pages.unshift(previousPageSet); + } + + if ( endPage < totalPages ) { + var nextPageSet = makePage(endPage + 1, '...', false); + pages.push(nextPageSet); + } + } + + return pages; + } + + var originalRender = paginationCtrl.render; + paginationCtrl.render = function() { + originalRender(); + if (scope.page > 0 && scope.page <= scope.totalPages) { + scope.pages = getPages(scope.page, scope.totalPages); + } + }; + } + }; +}]) + +.constant('pagerConfig', { + itemsPerPage: 10, + previousText: '« Previous', + nextText: 'Next »', + align: true +}) + +.directive('pager', ['pagerConfig', function(pagerConfig) { + return { + restrict: 'EA', + scope: { + totalItems: '=', + previousText: '@', + nextText: '@' + }, + require: ['pager', '?ngModel'], + controller: 'PaginationController', + templateUrl: 'template/pagination/pager.html', + replace: true, + link: function(scope, element, attrs, ctrls) { + var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if (!ngModelCtrl) { + return; // do nothing if no ng-model + } + + scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align; + paginationCtrl.init(ngModelCtrl, pagerConfig); + } + }; +}]); + +/** + * The following features are still outstanding: animation as a + * function, placement as a function, inside, support for more triggers than + * just mouse enter/leave, html tooltips, and selector delegation. + */ +angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap.bindHtml' ] ) + +/** + * The $tooltip service creates tooltip- and popover-like directives as well as + * houses global options for them. + */ +.provider( '$tooltip', function () { + // The default options tooltip and popover. + var defaultOptions = { + placement: 'top', + animation: true, + popupDelay: 0 + }; + + // Default hide triggers for each show trigger + var triggerMap = { + 'mouseenter': 'mouseleave', + 'click': 'click', + 'focus': 'blur' + }; + + // 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', '$parse', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $parse, $document, $position, $interpolate ) { + return function $tooltip ( type, prefix, defaultTriggerShow ) { + var options = angular.extend( {}, defaultOptions, globalOptions ); + + /** + * 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; + var hide = triggerMap[show] || show; + return { + show: show, + hide: hide + }; + } + + var directiveName = snake_case( type ); + + var startSym = $interpolate.startSymbol(); + var endSym = $interpolate.endSymbol(); + var template = + '
'+ + '
'; + + return { + restrict: 'EA', + scope: true, + compile: function (tElem, tAttrs) { + var tooltipLinker = $compile( template ); + + return function link ( scope, element, attrs ) { + var tooltip; + var transitionTimeout; + var popupTimeout; + var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; + var triggers = getTriggers( undefined ); + var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']); + + var positionTooltip = function () { + + var ttPosition = $position.positionElements(element, tooltip, scope.tt_placement, appendToBody); + ttPosition.top += 'px'; + ttPosition.left += 'px'; + + // Now set the calculated positioning. + tooltip.css( ttPosition ); + }; + + // By default, the tooltip is not open. + // TODO add ability to start tooltip opened + scope.tt_isOpen = false; + + function toggleTooltipBind () { + if ( ! scope.tt_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; + } + if ( scope.tt_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 (!popupTimeout) { + popupTimeout = $timeout( show, scope.tt_popupDelay, false ); + popupTimeout.then(function(reposition){reposition();}); + } + } else { + show()(); + } + } + + function hideTooltipBind () { + scope.$apply(function () { + hide(); + }); + } + + // Show the tooltip popup element. + function show() { + + popupTimeout = null; + + // If there is a pending remove transition, we must cancel it, lest the + // tooltip be mysteriously removed. + if ( transitionTimeout ) { + $timeout.cancel( transitionTimeout ); + transitionTimeout = null; + } + + // Don't show empty tooltips. + if ( ! scope.tt_content ) { + return angular.noop; + } + + createTooltip(); + + // Set the initial positioning. + tooltip.css({ top: 0, left: 0, display: 'block' }); + + // Now we add it to the DOM because need some info about it. But it's not + // visible yet anyway. + if ( appendToBody ) { + $document.find( 'body' ).append( tooltip ); + } else { + element.after( tooltip ); + } + + positionTooltip(); + + // And show the tooltip. + scope.tt_isOpen = true; + scope.$digest(); // digest required as $apply is not called + + // Return positioning function as promise callback for correct + // positioning after draw. + return positionTooltip; + } + + // Hide the tooltip popup element. + function hide() { + // First things first: we don't show it anymore. + scope.tt_isOpen = false; + + //if tooltip is going to be shown after delay, we must cancel this + $timeout.cancel( popupTimeout ); + popupTimeout = null; + + // 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. + if ( scope.tt_animation ) { + if (!transitionTimeout) { + transitionTimeout = $timeout(removeTooltip, 500); + } + } else { + removeTooltip(); + } + } + + function createTooltip() { + // There can only be one tooltip element per directive shown at once. + if (tooltip) { + removeTooltip(); + } + tooltip = tooltipLinker(scope, function () {}); + + // Get contents rendered into the tooltip + scope.$digest(); + } + + function removeTooltip() { + transitionTimeout = null; + if (tooltip) { + tooltip.remove(); + tooltip = null; + } + } + + /** + * Observe the relevant attributes. + */ + attrs.$observe( type, function ( val ) { + scope.tt_content = val; + + if (!val && scope.tt_isOpen ) { + hide(); + } + }); + + attrs.$observe( prefix+'Title', function ( val ) { + scope.tt_title = val; + }); + + attrs.$observe( prefix+'Placement', function ( val ) { + scope.tt_placement = angular.isDefined( val ) ? val : options.placement; + }); + + attrs.$observe( prefix+'PopupDelay', function ( val ) { + var delay = parseInt( val, 10 ); + scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay; + }); + + var unregisterTriggers = function () { + element.unbind(triggers.show, showTooltipBind); + element.unbind(triggers.hide, hideTooltipBind); + }; + + attrs.$observe( prefix+'Trigger', function ( val ) { + unregisterTriggers(); + + triggers = getTriggers( val ); + + if ( triggers.show === triggers.hide ) { + element.bind( triggers.show, toggleTooltipBind ); + } else { + element.bind( triggers.show, showTooltipBind ); + element.bind( triggers.hide, hideTooltipBind ); + } + }); + + var animation = scope.$eval(attrs[prefix + 'Animation']); + scope.tt_animation = angular.isDefined(animation) ? !!animation : options.animation; + + attrs.$observe( prefix+'AppendToBody', function ( val ) { + appendToBody = angular.isDefined( val ) ? $parse( val )( scope ) : appendToBody; + }); + + // if a tooltip is attached to we need to remove it on + // location change as its parent scope will probably not be destroyed + // by the change. + if ( appendToBody ) { + scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () { + if ( scope.tt_isOpen ) { + hide(); + } + }); + } + + // Make sure tooltip is destroyed and removed. + scope.$on('$destroy', function onDestroyTooltip() { + $timeout.cancel( transitionTimeout ); + $timeout.cancel( popupTimeout ); + unregisterTriggers(); + removeTooltip(); + }); + }; + } + }; + }; + }]; +}) + +.directive( 'tooltipPopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/tooltip/tooltip-popup.html' + }; +}) + +.directive( 'tooltip', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); +}]) + +.directive( 'tooltipHtmlUnsafePopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html' + }; +}) + +.directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' ); +}]); + +/** + * 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, html popovers, and selector delegatation. + */ +angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] ) + +.directive( 'popoverPopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/popover/popover.html' + }; +}) + +.directive( 'popover', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'popover', 'popover', 'click' ); +}]); + +angular.module('ui.bootstrap.progressbar', []) + +.constant('progressConfig', { + animate: true, + max: 100 +}) + +.controller('ProgressController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) { + var self = this, + animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate; + + this.bars = []; + $scope.max = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : progressConfig.max; + + this.addBar = function(bar, element) { + if ( !animate ) { + element.css({'transition': 'none'}); + } + + this.bars.push(bar); + + bar.$watch('value', function( value ) { + bar.percent = +(100 * value / $scope.max).toFixed(2); + }); + + bar.$on('$destroy', function() { + element = null; + self.removeBar(bar); + }); + }; + + this.removeBar = function(bar) { + this.bars.splice(this.bars.indexOf(bar), 1); + }; +}]) + +.directive('progress', function() { + return { + restrict: 'EA', + replace: true, + transclude: true, + controller: 'ProgressController', + require: 'progress', + scope: {}, + templateUrl: 'template/progressbar/progress.html' + }; +}) + +.directive('bar', function() { + return { + restrict: 'EA', + replace: true, + transclude: true, + require: '^progress', + scope: { + value: '=', + type: '@' + }, + templateUrl: 'template/progressbar/bar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, element); + } + }; +}) + +.directive('progressbar', function() { + return { + restrict: 'EA', + replace: true, + transclude: true, + controller: 'ProgressController', + scope: { + value: '=', + type: '@' + }, + templateUrl: 'template/progressbar/progressbar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, angular.element(element.children()[0])); + } + }; +}); +angular.module('ui.bootstrap.rating', []) + +.constant('ratingConfig', { + max: 5, + stateOn: null, + stateOff: null +}) + +.controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) { + var ngModelCtrl = { $setViewValue: angular.noop }; + + this.init = function(ngModelCtrl_) { + ngModelCtrl = ngModelCtrl_; + ngModelCtrl.$render = this.render; + + this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn; + this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff; + + var 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 }, states[i]); + } + return states; + }; + + $scope.rate = function(value) { + if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) { + ngModelCtrl.$setViewValue(value); + ngModelCtrl.$render(); + } + }; + + $scope.enter = function(value) { + if ( !$scope.readonly ) { + $scope.value = value; + } + $scope.onHover({value: value}); + }; + + $scope.reset = function() { + $scope.value = ngModelCtrl.$viewValue; + $scope.onLeave(); + }; + + $scope.onKeydown = function(evt) { + if (/(37|38|39|40)/.test(evt.which)) { + evt.preventDefault(); + evt.stopPropagation(); + $scope.rate( $scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1) ); + } + }; + + this.render = function() { + $scope.value = ngModelCtrl.$viewValue; + }; +}]) + +.directive('rating', function() { + return { + restrict: 'EA', + require: ['rating', 'ngModel'], + scope: { + readonly: '=?', + onHover: '&', + onLeave: '&' + }, + controller: 'RatingController', + templateUrl: 'template/rating/rating.html', + replace: true, + link: function(scope, element, attrs, ctrls) { + var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if ( ngModelCtrl ) { + ratingCtrl.init( ngModelCtrl ); + } + } + }; +}); + +/** + * @ngdoc overview + * @name ui.bootstrap.tabs + * + * @description + * AngularJS version of the tabs directive. + */ + +angular.module('ui.bootstrap.tabs', []) + +.controller('TabsetController', ['$scope', function TabsetCtrl($scope) { + var ctrl = this, + tabs = ctrl.tabs = $scope.tabs = []; + + ctrl.select = function(selectedTab) { + angular.forEach(tabs, function(tab) { + if (tab.active && tab !== selectedTab) { + tab.active = false; + tab.onDeselect(); + } + }); + selectedTab.active = true; + selectedTab.onSelect(); + }; + + ctrl.addTab = function addTab(tab) { + tabs.push(tab); + // we can't run the select function on the first tab + // since that would select it twice + if (tabs.length === 1) { + tab.active = true; + } else if (tab.active) { + ctrl.select(tab); + } + }; + + ctrl.removeTab = function removeTab(tab) { + var index = tabs.indexOf(tab); + //Select a new tab if the tab to be removed is selected + if (tab.active && tabs.length > 1) { + //If this is the last tab, select the previous tab. else, the next tab. + var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1; + ctrl.select(tabs[newActiveIndex]); + } + tabs.splice(index, 1); + }; +}]) + +/** + * @ngdoc directive + * @name ui.bootstrap.tabs.directive:tabset + * @restrict EA + * + * @description + * Tabset is the outer container for the tabs directive + * + * @param {boolean=} vertical Whether or not to use vertical styling for the tabs. + * @param {boolean=} justified Whether or not to use justified styling for the tabs. + * + * @example + + + + First Content! + Second Content! + +
+ + First Vertical Content! + Second Vertical Content! + + + First Justified Content! + Second Justified Content! + +
+
+ */ +.directive('tabset', function() { + return { + restrict: 'EA', + transclude: true, + replace: true, + scope: { + type: '@' + }, + controller: 'TabsetController', + templateUrl: 'template/tabs/tabset.html', + link: function(scope, element, attrs) { + scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false; + scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false; + } + }; +}) + +/** + * @ngdoc directive + * @name ui.bootstrap.tabs.directive:tab + * @restrict EA + * + * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}. + * @param {string=} select An expression to evaluate when the tab is selected. + * @param {boolean=} active A binding, telling whether or not this tab is selected. + * @param {boolean=} disabled A binding, telling whether or not this tab is disabled. + * + * @description + * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}. + * + * @example + + +
+ + +
+ + First Tab + + Alert me! + Second Tab, with alert callback and html heading! + + + {{item.content}} + + +
+
+ + function TabsDemoCtrl($scope) { + $scope.items = [ + { title:"Dynamic Title 1", content:"Dynamic Item 0" }, + { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true } + ]; + + $scope.alertMe = function() { + setTimeout(function() { + alert("You've selected the alert tab!"); + }); + }; + }; + +
+ */ + +/** + * @ngdoc directive + * @name ui.bootstrap.tabs.directive:tabHeading + * @restrict EA + * + * @description + * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element. + * + * @example + + + + + HTML in my titles?! + And some content, too! + + + Icon heading?!? + That's right. + + + + + */ +.directive('tab', ['$parse', function($parse) { + return { + require: '^tabset', + restrict: 'EA', + replace: true, + templateUrl: 'template/tabs/tab.html', + transclude: true, + scope: { + active: '=?', + heading: '@', + onSelect: '&select', //This callback is called in contentHeadingTransclude + //once it inserts the tab's content into the dom + onDeselect: '&deselect' + }, + controller: function() { + //Empty controller so other directives can require being 'under' a tab + }, + compile: function(elm, attrs, transclude) { + return function postLink(scope, elm, attrs, tabsetCtrl) { + scope.$watch('active', function(active) { + if (active) { + tabsetCtrl.select(scope); + } + }); + + scope.disabled = false; + if ( attrs.disabled ) { + scope.$parent.$watch($parse(attrs.disabled), function(value) { + scope.disabled = !! value; + }); + } + + scope.select = function() { + if ( !scope.disabled ) { + scope.active = true; + } + }; + + tabsetCtrl.addTab(scope); + scope.$on('$destroy', function() { + tabsetCtrl.removeTab(scope); + }); + + //We need to transclude later, once the content container is ready. + //when this link happens, we're inside a tab heading. + scope.$transcludeFn = transclude; + }; + } + }; +}]) + +.directive('tabHeadingTransclude', [function() { + return { + restrict: 'A', + require: '^tab', + link: function(scope, elm, attrs, tabCtrl) { + scope.$watch('headingElement', function updateHeadingElement(heading) { + if (heading) { + elm.html(''); + elm.append(heading); + } + }); + } + }; +}]) + +.directive('tabContentTransclude', function() { + return { + restrict: 'A', + require: '^tabset', + link: function(scope, elm, attrs) { + var tab = scope.$eval(attrs.tabContentTransclude); + + //Now our tab is ready to be transcluded: both the tab heading area + //and the tab content area are loaded. Transclude 'em both. + tab.$transcludeFn(tab.$parent, function(contents) { + angular.forEach(contents, function(node) { + if (isTabHeading(node)) { + //Let tabHeadingTransclude know. + tab.headingElement = node; + } else { + elm.append(node); + } + }); + }); + } + }; + function isTabHeading(node) { + return node.tagName && ( + node.hasAttribute('tab-heading') || + node.hasAttribute('data-tab-heading') || + node.tagName.toLowerCase() === 'tab-heading' || + node.tagName.toLowerCase() === 'data-tab-heading' + ); + } +}) + +; + +angular.module('ui.bootstrap.timepicker', []) + +.constant('timepickerConfig', { + hourStep: 1, + minuteStep: 1, + showMeridian: true, + meridians: null, + readonlyInput: false, + mousewheel: true +}) + +.controller('TimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'timepickerConfig', function($scope, $attrs, $parse, $log, $locale, timepickerConfig) { + var selected = new Date(), + ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl + meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS; + + this.init = function( ngModelCtrl_, inputs ) { + ngModelCtrl = ngModelCtrl_; + ngModelCtrl.$render = this.render; + + var hoursInputEl = inputs.eq(0), + minutesInputEl = inputs.eq(1); + + var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel; + if ( mousewheel ) { + this.setupMousewheelEvents( hoursInputEl, minutesInputEl ); + } + + $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput; + this.setupInputEvents( hoursInputEl, minutesInputEl ); + }; + + var hourStep = timepickerConfig.hourStep; + if ($attrs.hourStep) { + $scope.$parent.$watch($parse($attrs.hourStep), function(value) { + hourStep = parseInt(value, 10); + }); + } + + var minuteStep = timepickerConfig.minuteStep; + if ($attrs.minuteStep) { + $scope.$parent.$watch($parse($attrs.minuteStep), function(value) { + minuteStep = parseInt(value, 10); + }); + } + + // 12H / 24H mode + $scope.showMeridian = timepickerConfig.showMeridian; + if ($attrs.showMeridian) { + $scope.$parent.$watch($parse($attrs.showMeridian), function(value) { + $scope.showMeridian = !!value; + + if ( ngModelCtrl.$error.time ) { + // Evaluate from template + var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate(); + if (angular.isDefined( hours ) && angular.isDefined( minutes )) { + selected.setHours( hours ); + refresh(); + } + } else { + updateTemplate(); + } + }); + } + + // Get $scope.hours in 24H mode if valid + function getHoursFromTemplate ( ) { + var hours = parseInt( $scope.hours, 10 ); + var valid = ( $scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24); + if ( !valid ) { + return undefined; + } + + if ( $scope.showMeridian ) { + if ( hours === 12 ) { + hours = 0; + } + if ( $scope.meridian === meridians[1] ) { + hours = hours + 12; + } + } + return hours; + } + + function getMinutesFromTemplate() { + var minutes = parseInt($scope.minutes, 10); + return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined; + } + + function pad( value ) { + return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value; + } + + // Respond on mousewheel spin + this.setupMousewheelEvents = function( hoursInputEl, minutesInputEl ) { + var isScrollingUp = function(e) { + if (e.originalEvent) { + e = e.originalEvent; + } + //pick correct delta variable depending on event + var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY; + return (e.detail || delta > 0); + }; + + hoursInputEl.bind('mousewheel wheel', function(e) { + $scope.$apply( (isScrollingUp(e)) ? $scope.incrementHours() : $scope.decrementHours() ); + e.preventDefault(); + }); + + minutesInputEl.bind('mousewheel wheel', function(e) { + $scope.$apply( (isScrollingUp(e)) ? $scope.incrementMinutes() : $scope.decrementMinutes() ); + e.preventDefault(); + }); + + }; + + this.setupInputEvents = function( hoursInputEl, minutesInputEl ) { + if ( $scope.readonlyInput ) { + $scope.updateHours = angular.noop; + $scope.updateMinutes = angular.noop; + return; + } + + var invalidate = function(invalidHours, invalidMinutes) { + ngModelCtrl.$setViewValue( null ); + ngModelCtrl.$setValidity('time', false); + if (angular.isDefined(invalidHours)) { + $scope.invalidHours = invalidHours; + } + if (angular.isDefined(invalidMinutes)) { + $scope.invalidMinutes = invalidMinutes; + } + }; + + $scope.updateHours = function() { + var hours = getHoursFromTemplate(); + + if ( angular.isDefined(hours) ) { + selected.setHours( hours ); + refresh( 'h' ); + } else { + invalidate(true); + } + }; + + hoursInputEl.bind('blur', function(e) { + if ( !$scope.invalidHours && $scope.hours < 10) { + $scope.$apply( function() { + $scope.hours = pad( $scope.hours ); + }); + } + }); + + $scope.updateMinutes = function() { + var minutes = getMinutesFromTemplate(); + + if ( angular.isDefined(minutes) ) { + selected.setMinutes( minutes ); + refresh( 'm' ); + } else { + invalidate(undefined, true); + } + }; + + minutesInputEl.bind('blur', function(e) { + if ( !$scope.invalidMinutes && $scope.minutes < 10 ) { + $scope.$apply( function() { + $scope.minutes = pad( $scope.minutes ); + }); + } + }); + + }; + + this.render = function() { + var date = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : null; + + 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; + } + makeValid(); + updateTemplate(); + } + }; + + // Call internally when we know that model is valid. + function refresh( keyboardChange ) { + makeValid(); + ngModelCtrl.$setViewValue( new Date(selected) ); + updateTemplate( keyboardChange ); + } + + function makeValid() { + ngModelCtrl.$setValidity('time', true); + $scope.invalidHours = false; + $scope.invalidMinutes = false; + } + + function updateTemplate( keyboardChange ) { + var hours = selected.getHours(), minutes = selected.getMinutes(); + + if ( $scope.showMeridian ) { + hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system + } + + $scope.hours = keyboardChange === 'h' ? hours : pad(hours); + $scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes); + $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; + } + + function addMinutes( minutes ) { + var dt = new Date( selected.getTime() + minutes * 60000 ); + selected.setHours( dt.getHours(), dt.getMinutes() ); + refresh(); + } + + $scope.incrementHours = function() { + addMinutes( hourStep * 60 ); + }; + $scope.decrementHours = function() { + addMinutes( - hourStep * 60 ); + }; + $scope.incrementMinutes = function() { + addMinutes( minuteStep ); + }; + $scope.decrementMinutes = function() { + addMinutes( - minuteStep ); + }; + $scope.toggleMeridian = function() { + addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) ); + }; +}]) + +.directive('timepicker', function () { + return { + restrict: 'EA', + require: ['timepicker', '?^ngModel'], + controller:'TimepickerController', + replace: true, + scope: {}, + templateUrl: 'template/timepicker/timepicker.html', + link: function(scope, element, attrs, ctrls) { + var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if ( ngModelCtrl ) { + timepickerCtrl.init( ngModelCtrl, element.find('input') ); + } + } + }; +}); + +angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml']) + +/** + * A helper service that can parse typeahead's syntax (string provided by users) + * Extracted to a separate service for ease of unit testing + */ + .factory('typeaheadParser', ['$parse', function ($parse) { + + // 00000111000000000000022200000000000000003333333333333330000000000044000 + var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\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]) + }; + } + }; +}]) + + .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser', + function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) { + + var HOT_KEYS = [9, 13, 27, 38, 40]; + + return { + require:'ngModel', + link:function (originalScope, element, attrs, modelCtrl) { + + //SUPPORTED ATTRIBUTES (OPTIONS) + + //minimal no of characters that needs to be entered before typeahead kicks-in + var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; + + //minimal wait time after last character typed before typehead 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; + + //binding to a variable that indicates if matches are being retrieved asynchronously + var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; + + //a callback executed when a match is selected + var onSelectCallback = $parse(attrs.typeaheadOnSelect); + + var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; + + var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; + + //INTERNAL VARIABLES + + //model setter executed upon match selection + var $setModelValue = $parse(attrs.ngModel).assign; + + //expressions used by typeahead + var parserResult = typeaheadParser.parse(attrs.typeahead); + + var hasFocus; + + //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(); + originalScope.$on('$destroy', function(){ + scope.$destroy(); + }); + + // WAI-ARIA + var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); + element.attr({ + 'aria-autocomplete': 'list', + 'aria-expanded': false, + 'aria-owns': popupId + }); + + //pop-up element used to display matches + var popUpEl = angular.element('
'); + popUpEl.attr({ + id: popupId, + matches: 'matches', + active: 'activeIdx', + select: 'select(activeIdx)', + query: 'query', + position: 'position' + }); + //custom item template + if (angular.isDefined(attrs.typeaheadTemplateUrl)) { + popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); + } + + var resetMatches = function() { + scope.matches = []; + scope.activeIdx = -1; + element.attr('aria-expanded', false); + }; + + 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 getMatchesAsync = function(inputValue) { + + var locals = {$viewValue: inputValue}; + isLoadingSetter(originalScope, true); + $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.length > 0) { + + scope.activeIdx = 0; + scope.matches.length = 0; + + //transform labels + for(var i=0; i= minSearch) { + if (waitTime > 0) { + if (timeoutPromise) { + $timeout.cancel(timeoutPromise);//cancel previous timeout + } + timeoutPromise = $timeout(function () { + getMatchesAsync(inputValue); + }, waitTime); + } else { + getMatchesAsync(inputValue); + } + } else { + isLoadingSetter(originalScope, false); + resetMatches(); + } + + if (isEditable) { + return inputValue; + } else { + if (!inputValue) { + // Reset in case user had typed something previously. + modelCtrl.$setValidity('editable', true); + return inputValue; + } else { + modelCtrl.$setValidity('editable', false); + return undefined; + } + } + }); + + modelCtrl.$formatters.push(function (modelValue) { + + var candidateViewValue, emptyViewValue; + var locals = {}; + + if (inputFormatter) { + + locals['$model'] = modelValue; + return inputFormatter(originalScope, locals); + + } else { + + //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; + } + }); + + scope.select = function (activeIdx) { + //called from within the $digest() cycle + var locals = {}; + var model, item; + + locals[parserResult.itemName] = item = scope.matches[activeIdx].model; + model = parserResult.modelMapper(originalScope, locals); + $setModelValue(originalScope, model); + modelCtrl.$setValidity('editable', true); + + onSelectCallback(originalScope, { + $item: item, + $model: model, + $label: parserResult.viewMapper(originalScope, locals) + }); + + resetMatches(); + + //return focus to the input element if a match was selected via a mouse click event + // use timeout to avoid $rootScope:inprog error + $timeout(function() { element[0].focus(); }, 0, false); + }; + + //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) + element.bind('keydown', function (evt) { + + //typeahead is open and an "interesting" key was pressed + if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { + return; + } + + evt.preventDefault(); + + if (evt.which === 40) { + scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; + scope.$digest(); + + } else if (evt.which === 38) { + scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1; + scope.$digest(); + + } else if (evt.which === 13 || evt.which === 9) { + scope.$apply(function () { + scope.select(scope.activeIdx); + }); + + } else if (evt.which === 27) { + evt.stopPropagation(); + + resetMatches(); + scope.$digest(); + } + }); + + element.bind('blur', function (evt) { + hasFocus = false; + }); + + // Keep reference to click handler to unbind it. + var dismissClickHandler = function (evt) { + if (element[0] !== evt.target) { + resetMatches(); + scope.$digest(); + } + }; + + $document.bind('click', dismissClickHandler); + + originalScope.$on('$destroy', function(){ + $document.unbind('click', dismissClickHandler); + }); + + var $popup = $compile(popUpEl)(scope); + if ( appendToBody ) { + $document.find('body').append($popup); + } else { + element.after($popup); + } + } + }; + +}]) + + .directive('typeaheadPopup', function () { + return { + restrict:'EA', + scope:{ + matches:'=', + query:'=', + active:'=', + position:'=', + select:'&' + }, + replace:true, + templateUrl:'template/typeahead/typeahead-popup.html', + link:function (scope, element, attrs) { + + scope.templateUrl = attrs.templateUrl; + + scope.isOpen = function () { + return scope.matches.length > 0; + }; + + scope.isActive = function (matchIdx) { + return scope.active == matchIdx; + }; + + scope.selectActive = function (matchIdx) { + scope.active = matchIdx; + }; + + scope.selectMatch = function (activeIdx) { + scope.select({activeIdx:activeIdx}); + }; + } + }; + }) + + .directive('typeaheadMatch', ['$http', '$templateCache', '$compile', '$parse', function ($http, $templateCache, $compile, $parse) { + return { + restrict:'EA', + scope:{ + index:'=', + match:'=', + query:'=' + }, + link:function (scope, element, attrs) { + var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html'; + $http.get(tplUrl, {cache: $templateCache}).success(function(tplContent){ + element.replaceWith($compile(tplContent.trim())(scope)); + }); + } + }; + }]) + + .filter('typeaheadHighlight', function() { + + function escapeRegexp(queryToEscape) { + return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); + } + + return function(matchItem, query) { + return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; + }; + }); + +angular.module("template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/accordion/accordion-group.html", + "
\n" + + "
\n" + + "

\n" + + " {{heading}}\n" + + "

\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
"); +}]); + +angular.module("template/accordion/accordion.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/accordion/accordion.html", + "
"); +}]); + +angular.module("template/alert/alert.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/alert/alert.html", + "
\n" + + " \n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("template/carousel/carousel.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/carousel/carousel.html", + "
\n" + + "
    1\">\n" + + "
  1. \n" + + "
\n" + + "
\n" + + " 1\">\n" + + " 1\">\n" + + "
\n" + + ""); +}]); + +angular.module("template/carousel/slide.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/carousel/slide.html", + "
\n" + + ""); +}]); + +angular.module("template/datepicker/datepicker.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/datepicker/datepicker.html", + "
\n" + + " \n" + + " \n" + + " \n" + + "
"); +}]); + +angular.module("template/datepicker/day.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/datepicker/day.html", + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
{{label.abbr}}
{{ weekNumbers[$index] }}\n" + + " \n" + + "
\n" + + ""); +}]); + +angular.module("template/datepicker/month.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/datepicker/month.html", + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + ""); +}]); + +angular.module("template/datepicker/popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/datepicker/popup.html", + "\n" + + ""); +}]); + +angular.module("template/datepicker/year.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/datepicker/year.html", + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + ""); +}]); + +angular.module("template/modal/backdrop.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/modal/backdrop.html", + "
\n" + + ""); +}]); + +angular.module("template/modal/window.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/modal/window.html", + "
\n" + + "
\n" + + "
"); +}]); + +angular.module("template/pagination/pager.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/pagination/pager.html", + ""); +}]); + +angular.module("template/pagination/pagination.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/pagination/pagination.html", + ""); +}]); + +angular.module("template/tooltip/tooltip-html-unsafe-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/tooltip/tooltip-html-unsafe-popup.html", + "
\n" + + "
\n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("template/tooltip/tooltip-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/tooltip/tooltip-popup.html", + "
\n" + + "
\n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("template/popover/popover.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/popover/popover.html", + "
\n" + + "
\n" + + "\n" + + "
\n" + + "

\n" + + "
\n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("template/progressbar/bar.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/progressbar/bar.html", + "
"); +}]); + +angular.module("template/progressbar/progress.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/progressbar/progress.html", + "
"); +}]); + +angular.module("template/progressbar/progressbar.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/progressbar/progressbar.html", + "
\n" + + "
\n" + + "
"); +}]); + +angular.module("template/rating/rating.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/rating/rating.html", + "\n" + + " \n" + + " ({{ $index < value ? '*' : ' ' }})\n" + + " \n" + + ""); +}]); + +angular.module("template/tabs/tab.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/tabs/tab.html", + "
  • \n" + + " {{heading}}\n" + + "
  • \n" + + ""); +}]); + +angular.module("template/tabs/tabset-titles.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/tabs/tabset-titles.html", + "\n" + + ""); +}]); + +angular.module("template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/tabs/tabset.html", + "\n" + + "
    \n" + + " \n" + + "
    \n" + + "
    \n" + + "
    \n" + + "
    \n" + + "
    \n" + + ""); +}]); + +angular.module("template/timepicker/timepicker.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/timepicker/timepicker.html", + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
     
    \n" + + " \n" + + " :\n" + + " \n" + + "
     
    \n" + + ""); +}]); + +angular.module("template/typeahead/typeahead-match.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/typeahead/typeahead-match.html", + ""); +}]); + +angular.module("template/typeahead/typeahead-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/typeahead/typeahead-popup.html", + ""); +}]); diff --git a/app/bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js b/app/bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js new file mode 100644 index 0000000..fa6a861 --- /dev/null +++ b/app/bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js @@ -0,0 +1,10 @@ +/* + * angular-ui-bootstrap + * http://angular-ui.github.io/bootstrap/ + + * Version: 0.11.0 - 2014-05-01 + * License: MIT + */ +angular.module("ui.bootstrap",["ui.bootstrap.tpls","ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","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",["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/day.html","template/datepicker/month.html","template/datepicker/popup.html","template/datepicker/year.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]),angular.module("ui.bootstrap.transition",[]).factory("$transition",["$q","$timeout","$rootScope",function(a,b,c){function d(a){for(var b in a)if(void 0!==f.style[b])return a[b]}var e=function(d,f,g){g=g||{};var h=a.defer(),i=e[g.animation?"animationEndEventName":"transitionEndEventName"],j=function(){c.$apply(function(){d.unbind(i,j),h.resolve(d)})};return i&&d.bind(i,j),b(function(){angular.isString(f)?d.addClass(f):angular.isFunction(f)?f(d):angular.isObject(f)&&d.css(f),i||h.resolve(d)}),h.promise.cancel=function(){i&&d.unbind(i,j),h.reject("Transition cancelled")},h.promise},f=document.createElement("trans"),g={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",transition:"transitionend"},h={WebkitTransition:"webkitAnimationEnd",MozTransition:"animationend",OTransition:"oAnimationEnd",transition:"animationend"};return e.transitionEndEventName=d(g),e.animationEndEventName=d(h),e}]),angular.module("ui.bootstrap.collapse",["ui.bootstrap.transition"]).directive("collapse",["$transition",function(a){return{link:function(b,c,d){function e(b){function d(){j===e&&(j=void 0)}var e=a(c,b);return j&&j.cancel(),j=e,e.then(d,d),e}function f(){k?(k=!1,g()):(c.removeClass("collapse").addClass("collapsing"),e({height:c[0].scrollHeight+"px"}).then(g))}function g(){c.removeClass("collapsing"),c.addClass("collapse in"),c.css({height:"auto"})}function h(){if(k)k=!1,i(),c.css({height:0});else{c.css({height:c[0].scrollHeight+"px"});{c[0].offsetWidth}c.removeClass("collapse in").addClass("collapsing"),e({height:0}).then(i)}}function i(){c.removeClass("collapsing"),c.addClass("collapse")}var j,k=!0;b.$watch(d.collapse,function(a){a?h():f()})}}}]),angular.module("ui.bootstrap.accordion",["ui.bootstrap.collapse"]).constant("accordionConfig",{closeOthers:!0}).controller("AccordionController",["$scope","$attrs","accordionConfig",function(a,b,c){this.groups=[],this.closeOthers=function(d){var e=angular.isDefined(b.closeOthers)?a.$eval(b.closeOthers):c.closeOthers;e&&angular.forEach(this.groups,function(a){a!==d&&(a.isOpen=!1)})},this.addGroup=function(a){var b=this;this.groups.push(a),a.$on("$destroy",function(){b.removeGroup(a)})},this.removeGroup=function(a){var b=this.groups.indexOf(a);-1!==b&&this.groups.splice(b,1)}}]).directive("accordion",function(){return{restrict:"EA",controller:"AccordionController",transclude:!0,replace:!1,templateUrl:"template/accordion/accordion.html"}}).directive("accordionGroup",function(){return{require:"^accordion",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/accordion/accordion-group.html",scope:{heading:"@",isOpen:"=?",isDisabled:"=?"},controller:function(){this.setHeading=function(a){this.heading=a}},link:function(a,b,c,d){d.addGroup(a),a.$watch("isOpen",function(b){b&&d.closeOthers(a)}),a.toggleOpen=function(){a.isDisabled||(a.isOpen=!a.isOpen)}}}}).directive("accordionHeading",function(){return{restrict:"EA",transclude:!0,template:"",replace:!0,require:"^accordionGroup",link:function(a,b,c,d,e){d.setHeading(e(a,function(){}))}}}).directive("accordionTransclude",function(){return{require:"^accordionGroup",link:function(a,b,c,d){a.$watch(function(){return d[c.accordionTransclude]},function(a){a&&(b.html(""),b.append(a))})}}}),angular.module("ui.bootstrap.alert",[]).controller("AlertController",["$scope","$attrs",function(a,b){a.closeable="close"in b}]).directive("alert",function(){return{restrict:"EA",controller:"AlertController",templateUrl:"template/alert/alert.html",transclude:!0,replace:!0,scope:{type:"@",close:"&"}}}),angular.module("ui.bootstrap.bindHtml",[]).directive("bindHtmlUnsafe",function(){return function(a,b,c){b.addClass("ng-binding").data("$binding",c.bindHtmlUnsafe),a.$watch(c.bindHtmlUnsafe,function(a){b.html(a||"")})}}),angular.module("ui.bootstrap.buttons",[]).constant("buttonConfig",{activeClass:"active",toggleEvent:"click"}).controller("ButtonsController",["buttonConfig",function(a){this.activeClass=a.activeClass||"active",this.toggleEvent=a.toggleEvent||"click"}]).directive("btnRadio",function(){return{require:["btnRadio","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){var e=d[0],f=d[1];f.$render=function(){b.toggleClass(e.activeClass,angular.equals(f.$modelValue,a.$eval(c.btnRadio)))},b.bind(e.toggleEvent,function(){var d=b.hasClass(e.activeClass);(!d||angular.isDefined(c.uncheckable))&&a.$apply(function(){f.$setViewValue(d?null:a.$eval(c.btnRadio)),f.$render()})})}}}).directive("btnCheckbox",function(){return{require:["btnCheckbox","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){function e(){return g(c.btnCheckboxTrue,!0)}function f(){return g(c.btnCheckboxFalse,!1)}function g(b,c){var d=a.$eval(b);return angular.isDefined(d)?d:c}var h=d[0],i=d[1];i.$render=function(){b.toggleClass(h.activeClass,angular.equals(i.$modelValue,e()))},b.bind(h.toggleEvent,function(){a.$apply(function(){i.$setViewValue(b.hasClass(h.activeClass)?f():e()),i.$render()})})}}}),angular.module("ui.bootstrap.carousel",["ui.bootstrap.transition"]).controller("CarouselController",["$scope","$timeout","$transition",function(a,b,c){function d(){e();var c=+a.interval;!isNaN(c)&&c>=0&&(g=b(f,c))}function e(){g&&(b.cancel(g),g=null)}function f(){h?(a.next(),d()):a.pause()}var g,h,i=this,j=i.slides=a.slides=[],k=-1;i.currentSlide=null;var l=!1;i.select=a.select=function(e,f){function g(){if(!l){if(i.currentSlide&&angular.isString(f)&&!a.noTransition&&e.$element){e.$element.addClass(f);{e.$element[0].offsetWidth}angular.forEach(j,function(a){angular.extend(a,{direction:"",entering:!1,leaving:!1,active:!1})}),angular.extend(e,{direction:f,active:!0,entering:!0}),angular.extend(i.currentSlide||{},{direction:f,leaving:!0}),a.$currentTransition=c(e.$element,{}),function(b,c){a.$currentTransition.then(function(){h(b,c)},function(){h(b,c)})}(e,i.currentSlide)}else h(e,i.currentSlide);i.currentSlide=e,k=m,d()}}function h(b,c){angular.extend(b,{direction:"",active:!0,leaving:!1,entering:!1}),angular.extend(c||{},{direction:"",active:!1,leaving:!1,entering:!1}),a.$currentTransition=null}var m=j.indexOf(e);void 0===f&&(f=m>k?"next":"prev"),e&&e!==i.currentSlide&&(a.$currentTransition?(a.$currentTransition.cancel(),b(g)):g())},a.$on("$destroy",function(){l=!0}),i.indexOfSlide=function(a){return j.indexOf(a)},a.next=function(){var b=(k+1)%j.length;return a.$currentTransition?void 0:i.select(j[b],"next")},a.prev=function(){var b=0>k-1?j.length-1:k-1;return a.$currentTransition?void 0:i.select(j[b],"prev")},a.isActive=function(a){return i.currentSlide===a},a.$watch("interval",d),a.$on("$destroy",e),a.play=function(){h||(h=!0,d())},a.pause=function(){a.noPause||(h=!1,e())},i.addSlide=function(b,c){b.$element=c,j.push(b),1===j.length||b.active?(i.select(j[j.length-1]),1==j.length&&a.play()):b.active=!1},i.removeSlide=function(a){var b=j.indexOf(a);j.splice(b,1),j.length>0&&a.active?i.select(b>=j.length?j[b-1]:j[b]):k>b&&k--}}]).directive("carousel",[function(){return{restrict:"EA",transclude:!0,replace:!0,controller:"CarouselController",require:"carousel",templateUrl:"template/carousel/carousel.html",scope:{interval:"=",noTransition:"=",noPause:"="}}}]).directive("slide",function(){return{require:"^carousel",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/carousel/slide.html",scope:{active:"=?"},link:function(a,b,c,d){d.addSlide(a,b),a.$on("$destroy",function(){d.removeSlide(a)}),a.$watch("active",function(b){b&&d.select(a)})}}}),angular.module("ui.bootstrap.dateparser",[]).service("dateParser",["$locale","orderByFilter",function(a,b){function c(a,b,c){return 1===b&&c>28?29===c&&(a%4===0&&a%100!==0||a%400===0):3===b||5===b||8===b||10===b?31>c:!0}this.parsers={};var d={yyyy:{regex:"\\d{4}",apply:function(a){this.year=+a}},yy:{regex:"\\d{2}",apply:function(a){this.year=+a+2e3}},y:{regex:"\\d{1,4}",apply:function(a){this.year=+a}},MMMM:{regex:a.DATETIME_FORMATS.MONTH.join("|"),apply:function(b){this.month=a.DATETIME_FORMATS.MONTH.indexOf(b)}},MMM:{regex:a.DATETIME_FORMATS.SHORTMONTH.join("|"),apply:function(b){this.month=a.DATETIME_FORMATS.SHORTMONTH.indexOf(b)}},MM:{regex:"0[1-9]|1[0-2]",apply:function(a){this.month=a-1}},M:{regex:"[1-9]|1[0-2]",apply:function(a){this.month=a-1}},dd:{regex:"[0-2][0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a}},d:{regex:"[1-2]?[0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a}},EEEE:{regex:a.DATETIME_FORMATS.DAY.join("|")},EEE:{regex:a.DATETIME_FORMATS.SHORTDAY.join("|")}};this.createParser=function(a){var c=[],e=a.split("");return angular.forEach(d,function(b,d){var f=a.indexOf(d);if(f>-1){a=a.split(""),e[f]="("+b.regex+")",a[f]="$";for(var g=f+1,h=f+d.length;h>g;g++)e[g]="",a[g]="$";a=a.join(""),c.push({index:f,apply:b.apply})}}),{regex:new RegExp("^"+e.join("")+"$"),map:b(c,"index")}},this.parse=function(b,d){if(!angular.isString(b))return b;d=a.DATETIME_FORMATS[d]||d,this.parsers[d]||(this.parsers[d]=this.createParser(d));var e=this.parsers[d],f=e.regex,g=e.map,h=b.match(f);if(h&&h.length){for(var i,j={year:1900,month:0,date:1,hours:0},k=1,l=h.length;l>k;k++){var m=g[k-1];m.apply&&m.apply.call(j,h[k])}return c(j.year,j.month,j.date)&&(i=new Date(j.year,j.month,j.date,j.hours)),i}}}]),angular.module("ui.bootstrap.position",[]).factory("$position",["$document","$window",function(a,b){function c(a,c){return a.currentStyle?a.currentStyle[c]:b.getComputedStyle?b.getComputedStyle(a)[c]:a.style[c]}function d(a){return"static"===(c(a,"position")||"static")}var e=function(b){for(var c=a[0],e=b.offsetParent||c;e&&e!==c&&d(e);)e=e.offsetParent;return e||c};return{position:function(b){var c=this.offset(b),d={top:0,left:0},f=e(b[0]);f!=a[0]&&(d=this.offset(angular.element(f)),d.top+=f.clientTop-f.scrollTop,d.left+=f.clientLeft-f.scrollLeft);var g=b[0].getBoundingClientRect();return{width:g.width||b.prop("offsetWidth"),height:g.height||b.prop("offsetHeight"),top:c.top-d.top,left:c.left-d.left}},offset:function(c){var d=c[0].getBoundingClientRect();return{width:d.width||c.prop("offsetWidth"),height:d.height||c.prop("offsetHeight"),top:d.top+(b.pageYOffset||a[0].documentElement.scrollTop),left:d.left+(b.pageXOffset||a[0].documentElement.scrollLeft)}},positionElements:function(a,b,c,d){var e,f,g,h,i=c.split("-"),j=i[0],k=i[1]||"center";e=d?this.offset(a):this.position(a),f=b.prop("offsetWidth"),g=b.prop("offsetHeight");var l={center:function(){return e.left+e.width/2-f/2},left:function(){return e.left},right:function(){return e.left+e.width}},m={center:function(){return e.top+e.height/2-g/2},top:function(){return e.top},bottom:function(){return e.top+e.height}};switch(j){case"right":h={top:m[k](),left:l[j]()};break;case"left":h={top:m[k](),left:e.left-f};break;case"bottom":h={top:m[j](),left:l[k]()};break;default:h={top:e.top-g,left:l[k]()}}return h}}}]),angular.module("ui.bootstrap.datepicker",["ui.bootstrap.dateparser","ui.bootstrap.position"]).constant("datepickerConfig",{formatDay:"dd",formatMonth:"MMMM",formatYear:"yyyy",formatDayHeader:"EEE",formatDayTitle:"MMMM yyyy",formatMonthTitle:"yyyy",datepickerMode:"day",minMode:"day",maxMode:"year",showWeeks:!0,startingDay:0,yearRange:20,minDate:null,maxDate:null}).controller("DatepickerController",["$scope","$attrs","$parse","$interpolate","$timeout","$log","dateFilter","datepickerConfig",function(a,b,c,d,e,f,g,h){var i=this,j={$setViewValue:angular.noop};this.modes=["day","month","year"],angular.forEach(["formatDay","formatMonth","formatYear","formatDayHeader","formatDayTitle","formatMonthTitle","minMode","maxMode","showWeeks","startingDay","yearRange"],function(c,e){i[c]=angular.isDefined(b[c])?8>e?d(b[c])(a.$parent):a.$parent.$eval(b[c]):h[c]}),angular.forEach(["minDate","maxDate"],function(d){b[d]?a.$parent.$watch(c(b[d]),function(a){i[d]=a?new Date(a):null,i.refreshView()}):i[d]=h[d]?new Date(h[d]):null}),a.datepickerMode=a.datepickerMode||h.datepickerMode,a.uniqueId="datepicker-"+a.$id+"-"+Math.floor(1e4*Math.random()),this.activeDate=angular.isDefined(b.initDate)?a.$parent.$eval(b.initDate):new Date,a.isActive=function(b){return 0===i.compare(b.date,i.activeDate)?(a.activeDateId=b.uid,!0):!1},this.init=function(a){j=a,j.$render=function(){i.render()}},this.render=function(){if(j.$modelValue){var a=new Date(j.$modelValue),b=!isNaN(a);b?this.activeDate=a:f.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'),j.$setValidity("date",b)}this.refreshView()},this.refreshView=function(){if(this.element){this._refreshView();var a=j.$modelValue?new Date(j.$modelValue):null;j.$setValidity("date-disabled",!a||this.element&&!this.isDisabled(a))}},this.createDateObject=function(a,b){var c=j.$modelValue?new Date(j.$modelValue):null;return{date:a,label:g(a,b),selected:c&&0===this.compare(a,c),disabled:this.isDisabled(a),current:0===this.compare(a,new Date)}},this.isDisabled=function(c){return this.minDate&&this.compare(c,this.minDate)<0||this.maxDate&&this.compare(c,this.maxDate)>0||b.dateDisabled&&a.dateDisabled({date:c,mode:a.datepickerMode})},this.split=function(a,b){for(var c=[];a.length>0;)c.push(a.splice(0,b));return c},a.select=function(b){if(a.datepickerMode===i.minMode){var c=j.$modelValue?new Date(j.$modelValue):new Date(0,0,0,0,0,0,0);c.setFullYear(b.getFullYear(),b.getMonth(),b.getDate()),j.$setViewValue(c),j.$render()}else i.activeDate=b,a.datepickerMode=i.modes[i.modes.indexOf(a.datepickerMode)-1]},a.move=function(a){var b=i.activeDate.getFullYear()+a*(i.step.years||0),c=i.activeDate.getMonth()+a*(i.step.months||0);i.activeDate.setFullYear(b,c,1),i.refreshView()},a.toggleMode=function(b){b=b||1,a.datepickerMode===i.maxMode&&1===b||a.datepickerMode===i.minMode&&-1===b||(a.datepickerMode=i.modes[i.modes.indexOf(a.datepickerMode)+b])},a.keys={13:"enter",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down"};var k=function(){e(function(){i.element[0].focus()},0,!1)};a.$on("datepicker.focus",k),a.keydown=function(b){var c=a.keys[b.which];if(c&&!b.shiftKey&&!b.altKey)if(b.preventDefault(),b.stopPropagation(),"enter"===c||"space"===c){if(i.isDisabled(i.activeDate))return;a.select(i.activeDate),k()}else!b.ctrlKey||"up"!==c&&"down"!==c?(i.handleKeyDown(c,b),i.refreshView()):(a.toggleMode("up"===c?1:-1),k())}}]).directive("datepicker",function(){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/datepicker.html",scope:{datepickerMode:"=?",dateDisabled:"&"},require:["datepicker","?^ngModel"],controller:"DatepickerController",link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f)}}}).directive("daypicker",["dateFilter",function(a){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/day.html",require:"^datepicker",link:function(b,c,d,e){function f(a,b){return 1!==b||a%4!==0||a%100===0&&a%400!==0?i[b]:29}function g(a,b){var c=new Array(b),d=new Date(a),e=0;for(d.setHours(12);b>e;)c[e++]=new Date(d),d.setDate(d.getDate()+1);return c}function h(a){var b=new Date(a);b.setDate(b.getDate()+4-(b.getDay()||7));var c=b.getTime();return b.setMonth(0),b.setDate(1),Math.floor(Math.round((c-b)/864e5)/7)+1}b.showWeeks=e.showWeeks,e.step={months:1},e.element=c;var i=[31,28,31,30,31,30,31,31,30,31,30,31];e._refreshView=function(){var c=e.activeDate.getFullYear(),d=e.activeDate.getMonth(),f=new Date(c,d,1),i=e.startingDay-f.getDay(),j=i>0?7-i:-i,k=new Date(f);j>0&&k.setDate(-j+1);for(var l=g(k,42),m=0;42>m;m++)l[m]=angular.extend(e.createDateObject(l[m],e.formatDay),{secondary:l[m].getMonth()!==d,uid:b.uniqueId+"-"+m});b.labels=new Array(7);for(var n=0;7>n;n++)b.labels[n]={abbr:a(l[n].date,e.formatDayHeader),full:a(l[n].date,"EEEE")};if(b.title=a(e.activeDate,e.formatDayTitle),b.rows=e.split(l,7),b.showWeeks){b.weekNumbers=[];for(var o=h(b.rows[0][0].date),p=b.rows.length;b.weekNumbers.push(o++)f;f++)c[f]=angular.extend(e.createDateObject(new Date(d,f,1),e.formatMonth),{uid:b.uniqueId+"-"+f});b.title=a(e.activeDate,e.formatMonthTitle),b.rows=e.split(c,3)},e.compare=function(a,b){return new Date(a.getFullYear(),a.getMonth())-new Date(b.getFullYear(),b.getMonth())},e.handleKeyDown=function(a){var b=e.activeDate.getMonth();if("left"===a)b-=1;else if("up"===a)b-=3;else if("right"===a)b+=1;else if("down"===a)b+=3;else if("pageup"===a||"pagedown"===a){var c=e.activeDate.getFullYear()+("pageup"===a?-1:1);e.activeDate.setFullYear(c)}else"home"===a?b=0:"end"===a&&(b=11);e.activeDate.setMonth(b)},e.refreshView()}}}]).directive("yearpicker",["dateFilter",function(){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/year.html",require:"^datepicker",link:function(a,b,c,d){function e(a){return parseInt((a-1)/f,10)*f+1}var f=d.yearRange;d.step={years:f},d.element=b,d._refreshView=function(){for(var b=new Array(f),c=0,g=e(d.activeDate.getFullYear());f>c;c++)b[c]=angular.extend(d.createDateObject(new Date(g+c,0,1),d.formatYear),{uid:a.uniqueId+"-"+c});a.title=[b[0].label,b[f-1].label].join(" - "),a.rows=d.split(b,5)},d.compare=function(a,b){return a.getFullYear()-b.getFullYear()},d.handleKeyDown=function(a){var b=d.activeDate.getFullYear();"left"===a?b-=1:"up"===a?b-=5:"right"===a?b+=1:"down"===a?b+=5:"pageup"===a||"pagedown"===a?b+=("pageup"===a?-1:1)*d.step.years:"home"===a?b=e(d.activeDate.getFullYear()):"end"===a&&(b=e(d.activeDate.getFullYear())+f-1),d.activeDate.setFullYear(b)},d.refreshView()}}}]).constant("datepickerPopupConfig",{datepickerPopup:"yyyy-MM-dd",currentText:"Today",clearText:"Clear",closeText:"Done",closeOnDateSelection:!0,appendToBody:!1,showButtonBar:!0}).directive("datepickerPopup",["$compile","$parse","$document","$position","dateFilter","dateParser","datepickerPopupConfig",function(a,b,c,d,e,f,g){return{restrict:"EA",require:"ngModel",scope:{isOpen:"=?",currentText:"@",clearText:"@",closeText:"@",dateDisabled:"&"},link:function(h,i,j,k){function l(a){return a.replace(/([A-Z])/g,function(a){return"-"+a.toLowerCase()})}function m(a){if(a){if(angular.isDate(a)&&!isNaN(a))return k.$setValidity("date",!0),a;if(angular.isString(a)){var b=f.parse(a,n)||new Date(a);return isNaN(b)?void k.$setValidity("date",!1):(k.$setValidity("date",!0),b)}return void k.$setValidity("date",!1)}return k.$setValidity("date",!0),null}var n,o=angular.isDefined(j.closeOnDateSelection)?h.$parent.$eval(j.closeOnDateSelection):g.closeOnDateSelection,p=angular.isDefined(j.datepickerAppendToBody)?h.$parent.$eval(j.datepickerAppendToBody):g.appendToBody;h.showButtonBar=angular.isDefined(j.showButtonBar)?h.$parent.$eval(j.showButtonBar):g.showButtonBar,h.getText=function(a){return h[a+"Text"]||g[a+"Text"]},j.$observe("datepickerPopup",function(a){n=a||g.datepickerPopup,k.$render()});var q=angular.element("
    ");q.attr({"ng-model":"date","ng-change":"dateSelection()"});var r=angular.element(q.children()[0]);j.datepickerOptions&&angular.forEach(h.$parent.$eval(j.datepickerOptions),function(a,b){r.attr(l(b),a)}),angular.forEach(["minDate","maxDate"],function(a){j[a]&&(h.$parent.$watch(b(j[a]),function(b){h[a]=b}),r.attr(l(a),a))}),j.dateDisabled&&r.attr("date-disabled","dateDisabled({ date: date, mode: mode })"),k.$parsers.unshift(m),h.dateSelection=function(a){angular.isDefined(a)&&(h.date=a),k.$setViewValue(h.date),k.$render(),o&&(h.isOpen=!1,i[0].focus())},i.bind("input change keyup",function(){h.$apply(function(){h.date=k.$modelValue})}),k.$render=function(){var a=k.$viewValue?e(k.$viewValue,n):"";i.val(a),h.date=m(k.$modelValue)};var s=function(a){h.isOpen&&a.target!==i[0]&&h.$apply(function(){h.isOpen=!1})},t=function(a){h.keydown(a)};i.bind("keydown",t),h.keydown=function(a){27===a.which?(a.preventDefault(),a.stopPropagation(),h.close()):40!==a.which||h.isOpen||(h.isOpen=!0)},h.$watch("isOpen",function(a){a?(h.$broadcast("datepicker.focus"),h.position=p?d.offset(i):d.position(i),h.position.top=h.position.top+i.prop("offsetHeight"),c.bind("click",s)):c.unbind("click",s)}),h.select=function(a){if("today"===a){var b=new Date;angular.isDate(k.$modelValue)?(a=new Date(k.$modelValue),a.setFullYear(b.getFullYear(),b.getMonth(),b.getDate())):a=new Date(b.setHours(0,0,0,0))}h.dateSelection(a)},h.close=function(){h.isOpen=!1,i[0].focus()};var u=a(q)(h);p?c.find("body").append(u):i.after(u),h.$on("$destroy",function(){u.remove(),i.unbind("keydown",t),c.unbind("click",s)})}}}]).directive("datepickerPopupWrap",function(){return{restrict:"EA",replace:!0,transclude:!0,templateUrl:"template/datepicker/popup.html",link:function(a,b){b.bind("click",function(a){a.preventDefault(),a.stopPropagation()})}}}),angular.module("ui.bootstrap.dropdown",[]).constant("dropdownConfig",{openClass:"open"}).service("dropdownService",["$document",function(a){var b=null;this.open=function(e){b||(a.bind("click",c),a.bind("keydown",d)),b&&b!==e&&(b.isOpen=!1),b=e},this.close=function(e){b===e&&(b=null,a.unbind("click",c),a.unbind("keydown",d))};var c=function(a){a&&a.isDefaultPrevented()||b.$apply(function(){b.isOpen=!1})},d=function(a){27===a.which&&(b.focusToggleElement(),c())}}]).controller("DropdownController",["$scope","$attrs","$parse","dropdownConfig","dropdownService","$animate",function(a,b,c,d,e,f){var g,h=this,i=a.$new(),j=d.openClass,k=angular.noop,l=b.onToggle?c(b.onToggle):angular.noop;this.init=function(d){h.$element=d,b.isOpen&&(g=c(b.isOpen),k=g.assign,a.$watch(g,function(a){i.isOpen=!!a}))},this.toggle=function(a){return i.isOpen=arguments.length?!!a:!i.isOpen},this.isOpen=function(){return i.isOpen},i.focusToggleElement=function(){h.toggleElement&&h.toggleElement[0].focus()},i.$watch("isOpen",function(b,c){f[b?"addClass":"removeClass"](h.$element,j),b?(i.focusToggleElement(),e.open(i)):e.close(i),k(a,b),angular.isDefined(b)&&b!==c&&l(a,{open:!!b})}),a.$on("$locationChangeSuccess",function(){i.isOpen=!1}),a.$on("$destroy",function(){i.$destroy()})}]).directive("dropdown",function(){return{restrict:"CA",controller:"DropdownController",link:function(a,b,c,d){d.init(b)}}}).directive("dropdownToggle",function(){return{restrict:"CA",require:"?^dropdown",link:function(a,b,c,d){if(d){d.toggleElement=b;var e=function(e){e.preventDefault(),b.hasClass("disabled")||c.disabled||a.$apply(function(){d.toggle()})};b.bind("click",e),b.attr({"aria-haspopup":!0,"aria-expanded":!1}),a.$watch(d.isOpen,function(a){b.attr("aria-expanded",!!a)}),a.$on("$destroy",function(){b.unbind("click",e)})}}}}),angular.module("ui.bootstrap.modal",["ui.bootstrap.transition"]).factory("$$stackedMap",function(){return{createNew:function(){var a=[];return{add:function(b,c){a.push({key:b,value:c})},get:function(b){for(var c=0;c0),i()})}function i(){if(k&&-1==g()){var a=l;j(k,l,150,function(){a.$destroy(),a=null}),k=void 0,l=void 0}}function j(c,d,e,f){function g(){g.done||(g.done=!0,c.remove(),f&&f())}d.animate=!1;var h=a.transitionEndEventName;if(h){var i=b(g,e);c.bind(h,function(){b.cancel(i),g(),d.$apply()})}else b(g,0)}var k,l,m="modal-open",n=f.createNew(),o={};return e.$watch(g,function(a){l&&(l.index=a)}),c.bind("keydown",function(a){var b;27===a.which&&(b=n.top(),b&&b.value.keyboard&&(a.preventDefault(),e.$apply(function(){o.dismiss(b.key,"escape key press")})))}),o.open=function(a,b){n.add(a,{deferred:b.deferred,modalScope:b.scope,backdrop:b.backdrop,keyboard:b.keyboard});var f=c.find("body").eq(0),h=g();h>=0&&!k&&(l=e.$new(!0),l.index=h,k=d("
    ")(l),f.append(k));var i=angular.element("
    ");i.attr({"template-url":b.windowTemplateUrl,"window-class":b.windowClass,size:b.size,index:n.length()-1,animate:"animate"}).html(b.content);var j=d(i)(b.scope);n.top().value.modalDomEl=j,f.append(j),f.addClass(m)},o.close=function(a,b){var c=n.get(a).value;c&&(c.deferred.resolve(b),h(a))},o.dismiss=function(a,b){var c=n.get(a).value;c&&(c.deferred.reject(b),h(a))},o.dismissAll=function(a){for(var b=this.getTop();b;)this.dismiss(b.key,a),b=this.getTop()},o.getTop=function(){return n.top()},o}]).provider("$modal",function(){var a={options:{backdrop:!0,keyboard:!0},$get:["$injector","$rootScope","$q","$http","$templateCache","$controller","$modalStack",function(b,c,d,e,f,g,h){function i(a){return a.template?d.when(a.template):e.get(a.templateUrl,{cache:f}).then(function(a){return a.data})}function j(a){var c=[];return angular.forEach(a,function(a){(angular.isFunction(a)||angular.isArray(a))&&c.push(d.when(b.invoke(a)))}),c}var k={};return k.open=function(b){var e=d.defer(),f=d.defer(),k={result:e.promise,opened:f.promise,close:function(a){h.close(k,a)},dismiss:function(a){h.dismiss(k,a)}};if(b=angular.extend({},a.options,b),b.resolve=b.resolve||{},!b.template&&!b.templateUrl)throw new Error("One of template or templateUrl options is required.");var l=d.all([i(b)].concat(j(b.resolve)));return l.then(function(a){var d=(b.scope||c).$new();d.$close=k.close,d.$dismiss=k.dismiss;var f,i={},j=1;b.controller&&(i.$scope=d,i.$modalInstance=k,angular.forEach(b.resolve,function(b,c){i[c]=a[j++]}),f=g(b.controller,i)),h.open(k,{scope:d,deferred:e,content:a[0],backdrop:b.backdrop,keyboard:b.keyboard,windowClass:b.windowClass,windowTemplateUrl:b.windowTemplateUrl,size:b.size})},function(a){e.reject(a)}),l.then(function(){f.resolve(!0)},function(){f.reject(!1)}),k},k}]};return a}),angular.module("ui.bootstrap.pagination",[]).controller("PaginationController",["$scope","$attrs","$parse",function(a,b,c){var d=this,e={$setViewValue:angular.noop},f=b.numPages?c(b.numPages).assign:angular.noop;this.init=function(f,g){e=f,this.config=g,e.$render=function(){d.render()},b.itemsPerPage?a.$parent.$watch(c(b.itemsPerPage),function(b){d.itemsPerPage=parseInt(b,10),a.totalPages=d.calculateTotalPages()}):this.itemsPerPage=g.itemsPerPage},this.calculateTotalPages=function(){var b=this.itemsPerPage<1?1:Math.ceil(a.totalItems/this.itemsPerPage);return Math.max(b||0,1)},this.render=function(){a.page=parseInt(e.$viewValue,10)||1},a.selectPage=function(b){a.page!==b&&b>0&&b<=a.totalPages&&(e.$setViewValue(b),e.$render())},a.getText=function(b){return a[b+"Text"]||d.config[b+"Text"]},a.noPrevious=function(){return 1===a.page},a.noNext=function(){return a.page===a.totalPages},a.$watch("totalItems",function(){a.totalPages=d.calculateTotalPages()}),a.$watch("totalPages",function(b){f(a.$parent,b),a.page>b?a.selectPage(b):e.$render()})}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("pagination",["$parse","paginationConfig",function(a,b){return{restrict:"EA",scope:{totalItems:"=",firstText:"@",previousText:"@",nextText:"@",lastText:"@"},require:["pagination","?ngModel"],controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:!0,link:function(c,d,e,f){function g(a,b,c){return{number:a,text:b,active:c}}function h(a,b){var c=[],d=1,e=b,f=angular.isDefined(k)&&b>k;f&&(l?(d=Math.max(a-Math.floor(k/2),1),e=d+k-1,e>b&&(e=b,d=e-k+1)):(d=(Math.ceil(a/k)-1)*k+1,e=Math.min(d+k-1,b)));for(var h=d;e>=h;h++){var i=g(h,h,h===a);c.push(i)}if(f&&!l){if(d>1){var j=g(d-1,"...",!1);c.unshift(j)}if(b>e){var m=g(e+1,"...",!1);c.push(m)}}return c}var i=f[0],j=f[1];if(j){var k=angular.isDefined(e.maxSize)?c.$parent.$eval(e.maxSize):b.maxSize,l=angular.isDefined(e.rotate)?c.$parent.$eval(e.rotate):b.rotate;c.boundaryLinks=angular.isDefined(e.boundaryLinks)?c.$parent.$eval(e.boundaryLinks):b.boundaryLinks,c.directionLinks=angular.isDefined(e.directionLinks)?c.$parent.$eval(e.directionLinks):b.directionLinks,i.init(j,b),e.maxSize&&c.$parent.$watch(a(e.maxSize),function(a){k=parseInt(a,10),i.render()});var m=i.render;i.render=function(){m(),c.page>0&&c.page<=c.totalPages&&(c.pages=h(c.page,c.totalPages))}}}}}]).constant("pagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("pager",["pagerConfig",function(a){return{restrict:"EA",scope:{totalItems:"=",previousText:"@",nextText:"@"},require:["pager","?ngModel"],controller:"PaginationController",templateUrl:"template/pagination/pager.html",replace:!0,link:function(b,c,d,e){var f=e[0],g=e[1];g&&(b.align=angular.isDefined(d.align)?b.$parent.$eval(d.align):a.align,f.init(g,a))}}}]),angular.module("ui.bootstrap.tooltip",["ui.bootstrap.position","ui.bootstrap.bindHtml"]).provider("$tooltip",function(){function a(a){var b=/[A-Z]/g,c="-"; +return a.replace(b,function(a,b){return(b?c:"")+a.toLowerCase()})}var b={placement:"top",animation:!0,popupDelay:0},c={mouseenter:"mouseleave",click:"click",focus:"blur"},d={};this.options=function(a){angular.extend(d,a)},this.setTriggers=function(a){angular.extend(c,a)},this.$get=["$window","$compile","$timeout","$parse","$document","$position","$interpolate",function(e,f,g,h,i,j,k){return function(e,l,m){function n(a){var b=a||o.trigger||m,d=c[b]||b;return{show:b,hide:d}}var o=angular.extend({},b,d),p=a(e),q=k.startSymbol(),r=k.endSymbol(),s="
    ';return{restrict:"EA",scope:!0,compile:function(){var a=f(s);return function(b,c,d){function f(){b.tt_isOpen?m():k()}function k(){(!y||b.$eval(d[l+"Enable"]))&&(b.tt_popupDelay?v||(v=g(p,b.tt_popupDelay,!1),v.then(function(a){a()})):p()())}function m(){b.$apply(function(){q()})}function p(){return v=null,u&&(g.cancel(u),u=null),b.tt_content?(r(),t.css({top:0,left:0,display:"block"}),w?i.find("body").append(t):c.after(t),z(),b.tt_isOpen=!0,b.$digest(),z):angular.noop}function q(){b.tt_isOpen=!1,g.cancel(v),v=null,b.tt_animation?u||(u=g(s,500)):s()}function r(){t&&s(),t=a(b,function(){}),b.$digest()}function s(){u=null,t&&(t.remove(),t=null)}var t,u,v,w=angular.isDefined(o.appendToBody)?o.appendToBody:!1,x=n(void 0),y=angular.isDefined(d[l+"Enable"]),z=function(){var a=j.positionElements(c,t,b.tt_placement,w);a.top+="px",a.left+="px",t.css(a)};b.tt_isOpen=!1,d.$observe(e,function(a){b.tt_content=a,!a&&b.tt_isOpen&&q()}),d.$observe(l+"Title",function(a){b.tt_title=a}),d.$observe(l+"Placement",function(a){b.tt_placement=angular.isDefined(a)?a:o.placement}),d.$observe(l+"PopupDelay",function(a){var c=parseInt(a,10);b.tt_popupDelay=isNaN(c)?o.popupDelay:c});var A=function(){c.unbind(x.show,k),c.unbind(x.hide,m)};d.$observe(l+"Trigger",function(a){A(),x=n(a),x.show===x.hide?c.bind(x.show,f):(c.bind(x.show,k),c.bind(x.hide,m))});var B=b.$eval(d[l+"Animation"]);b.tt_animation=angular.isDefined(B)?!!B:o.animation,d.$observe(l+"AppendToBody",function(a){w=angular.isDefined(a)?h(a)(b):w}),w&&b.$on("$locationChangeSuccess",function(){b.tt_isOpen&&q()}),b.$on("$destroy",function(){g.cancel(u),g.cancel(v),A(),s()})}}}}}]}).directive("tooltipPopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html"}}).directive("tooltip",["$tooltip",function(a){return a("tooltip","tooltip","mouseenter")}]).directive("tooltipHtmlUnsafePopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-unsafe-popup.html"}}).directive("tooltipHtmlUnsafe",["$tooltip",function(a){return a("tooltipHtmlUnsafe","tooltip","mouseenter")}]),angular.module("ui.bootstrap.popover",["ui.bootstrap.tooltip"]).directive("popoverPopup",function(){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html"}}).directive("popover",["$tooltip",function(a){return a("popover","popover","click")}]),angular.module("ui.bootstrap.progressbar",[]).constant("progressConfig",{animate:!0,max:100}).controller("ProgressController",["$scope","$attrs","progressConfig",function(a,b,c){var d=this,e=angular.isDefined(b.animate)?a.$parent.$eval(b.animate):c.animate;this.bars=[],a.max=angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max,this.addBar=function(b,c){e||c.css({transition:"none"}),this.bars.push(b),b.$watch("value",function(c){b.percent=+(100*c/a.max).toFixed(2)}),b.$on("$destroy",function(){c=null,d.removeBar(b)})},this.removeBar=function(a){this.bars.splice(this.bars.indexOf(a),1)}}]).directive("progress",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",require:"progress",scope:{},templateUrl:"template/progressbar/progress.html"}}).directive("bar",function(){return{restrict:"EA",replace:!0,transclude:!0,require:"^progress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(a,b,c,d){d.addBar(a,b)}}}).directive("progressbar",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(a,b,c,d){d.addBar(a,angular.element(b.children()[0]))}}}),angular.module("ui.bootstrap.rating",[]).constant("ratingConfig",{max:5,stateOn:null,stateOff:null}).controller("RatingController",["$scope","$attrs","ratingConfig",function(a,b,c){var d={$setViewValue:angular.noop};this.init=function(e){d=e,d.$render=this.render,this.stateOn=angular.isDefined(b.stateOn)?a.$parent.$eval(b.stateOn):c.stateOn,this.stateOff=angular.isDefined(b.stateOff)?a.$parent.$eval(b.stateOff):c.stateOff;var f=angular.isDefined(b.ratingStates)?a.$parent.$eval(b.ratingStates):new Array(angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max);a.range=this.buildTemplateObjects(f)},this.buildTemplateObjects=function(a){for(var b=0,c=a.length;c>b;b++)a[b]=angular.extend({index:b},{stateOn:this.stateOn,stateOff:this.stateOff},a[b]);return a},a.rate=function(b){!a.readonly&&b>=0&&b<=a.range.length&&(d.$setViewValue(b),d.$render())},a.enter=function(b){a.readonly||(a.value=b),a.onHover({value:b})},a.reset=function(){a.value=d.$viewValue,a.onLeave()},a.onKeydown=function(b){/(37|38|39|40)/.test(b.which)&&(b.preventDefault(),b.stopPropagation(),a.rate(a.value+(38===b.which||39===b.which?1:-1)))},this.render=function(){a.value=d.$viewValue}}]).directive("rating",function(){return{restrict:"EA",require:["rating","ngModel"],scope:{readonly:"=?",onHover:"&",onLeave:"&"},controller:"RatingController",templateUrl:"template/rating/rating.html",replace:!0,link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f)}}}),angular.module("ui.bootstrap.tabs",[]).controller("TabsetController",["$scope",function(a){var b=this,c=b.tabs=a.tabs=[];b.select=function(a){angular.forEach(c,function(b){b.active&&b!==a&&(b.active=!1,b.onDeselect())}),a.active=!0,a.onSelect()},b.addTab=function(a){c.push(a),1===c.length?a.active=!0:a.active&&b.select(a)},b.removeTab=function(a){var d=c.indexOf(a);if(a.active&&c.length>1){var e=d==c.length-1?d-1:d+1;b.select(c[e])}c.splice(d,1)}}]).directive("tabset",function(){return{restrict:"EA",transclude:!0,replace:!0,scope:{type:"@"},controller:"TabsetController",templateUrl:"template/tabs/tabset.html",link:function(a,b,c){a.vertical=angular.isDefined(c.vertical)?a.$parent.$eval(c.vertical):!1,a.justified=angular.isDefined(c.justified)?a.$parent.$eval(c.justified):!1}}}).directive("tab",["$parse",function(a){return{require:"^tabset",restrict:"EA",replace:!0,templateUrl:"template/tabs/tab.html",transclude:!0,scope:{active:"=?",heading:"@",onSelect:"&select",onDeselect:"&deselect"},controller:function(){},compile:function(b,c,d){return function(b,c,e,f){b.$watch("active",function(a){a&&f.select(b)}),b.disabled=!1,e.disabled&&b.$parent.$watch(a(e.disabled),function(a){b.disabled=!!a}),b.select=function(){b.disabled||(b.active=!0)},f.addTab(b),b.$on("$destroy",function(){f.removeTab(b)}),b.$transcludeFn=d}}}}]).directive("tabHeadingTransclude",[function(){return{restrict:"A",require:"^tab",link:function(a,b){a.$watch("headingElement",function(a){a&&(b.html(""),b.append(a))})}}}]).directive("tabContentTransclude",function(){function a(a){return a.tagName&&(a.hasAttribute("tab-heading")||a.hasAttribute("data-tab-heading")||"tab-heading"===a.tagName.toLowerCase()||"data-tab-heading"===a.tagName.toLowerCase())}return{restrict:"A",require:"^tabset",link:function(b,c,d){var e=b.$eval(d.tabContentTransclude);e.$transcludeFn(e.$parent,function(b){angular.forEach(b,function(b){a(b)?e.headingElement=b:c.append(b)})})}}}),angular.module("ui.bootstrap.timepicker",[]).constant("timepickerConfig",{hourStep:1,minuteStep:1,showMeridian:!0,meridians:null,readonlyInput:!1,mousewheel:!0}).controller("TimepickerController",["$scope","$attrs","$parse","$log","$locale","timepickerConfig",function(a,b,c,d,e,f){function g(){var b=parseInt(a.hours,10),c=a.showMeridian?b>0&&13>b:b>=0&&24>b;return c?(a.showMeridian&&(12===b&&(b=0),a.meridian===p[1]&&(b+=12)),b):void 0}function h(){var b=parseInt(a.minutes,10);return b>=0&&60>b?b:void 0}function i(a){return angular.isDefined(a)&&a.toString().length<2?"0"+a:a}function j(a){k(),o.$setViewValue(new Date(n)),l(a)}function k(){o.$setValidity("time",!0),a.invalidHours=!1,a.invalidMinutes=!1}function l(b){var c=n.getHours(),d=n.getMinutes();a.showMeridian&&(c=0===c||12===c?12:c%12),a.hours="h"===b?c:i(c),a.minutes="m"===b?d:i(d),a.meridian=n.getHours()<12?p[0]:p[1]}function m(a){var b=new Date(n.getTime()+6e4*a);n.setHours(b.getHours(),b.getMinutes()),j()}var n=new Date,o={$setViewValue:angular.noop},p=angular.isDefined(b.meridians)?a.$parent.$eval(b.meridians):f.meridians||e.DATETIME_FORMATS.AMPMS;this.init=function(c,d){o=c,o.$render=this.render;var e=d.eq(0),g=d.eq(1),h=angular.isDefined(b.mousewheel)?a.$parent.$eval(b.mousewheel):f.mousewheel;h&&this.setupMousewheelEvents(e,g),a.readonlyInput=angular.isDefined(b.readonlyInput)?a.$parent.$eval(b.readonlyInput):f.readonlyInput,this.setupInputEvents(e,g)};var q=f.hourStep;b.hourStep&&a.$parent.$watch(c(b.hourStep),function(a){q=parseInt(a,10)});var r=f.minuteStep;b.minuteStep&&a.$parent.$watch(c(b.minuteStep),function(a){r=parseInt(a,10)}),a.showMeridian=f.showMeridian,b.showMeridian&&a.$parent.$watch(c(b.showMeridian),function(b){if(a.showMeridian=!!b,o.$error.time){var c=g(),d=h();angular.isDefined(c)&&angular.isDefined(d)&&(n.setHours(c),j())}else l()}),this.setupMousewheelEvents=function(b,c){var d=function(a){a.originalEvent&&(a=a.originalEvent);var b=a.wheelDelta?a.wheelDelta:-a.deltaY;return a.detail||b>0};b.bind("mousewheel wheel",function(b){a.$apply(d(b)?a.incrementHours():a.decrementHours()),b.preventDefault()}),c.bind("mousewheel wheel",function(b){a.$apply(d(b)?a.incrementMinutes():a.decrementMinutes()),b.preventDefault()})},this.setupInputEvents=function(b,c){if(a.readonlyInput)return a.updateHours=angular.noop,void(a.updateMinutes=angular.noop);var d=function(b,c){o.$setViewValue(null),o.$setValidity("time",!1),angular.isDefined(b)&&(a.invalidHours=b),angular.isDefined(c)&&(a.invalidMinutes=c)};a.updateHours=function(){var a=g();angular.isDefined(a)?(n.setHours(a),j("h")):d(!0)},b.bind("blur",function(){!a.invalidHours&&a.hours<10&&a.$apply(function(){a.hours=i(a.hours)})}),a.updateMinutes=function(){var a=h();angular.isDefined(a)?(n.setMinutes(a),j("m")):d(void 0,!0)},c.bind("blur",function(){!a.invalidMinutes&&a.minutes<10&&a.$apply(function(){a.minutes=i(a.minutes)})})},this.render=function(){var a=o.$modelValue?new Date(o.$modelValue):null;isNaN(a)?(o.$setValidity("time",!1),d.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.')):(a&&(n=a),k(),l())},a.incrementHours=function(){m(60*q)},a.decrementHours=function(){m(60*-q)},a.incrementMinutes=function(){m(r)},a.decrementMinutes=function(){m(-r)},a.toggleMeridian=function(){m(720*(n.getHours()<12?1:-1))}}]).directive("timepicker",function(){return{restrict:"EA",require:["timepicker","?^ngModel"],controller:"TimepickerController",replace:!0,scope:{},templateUrl:"template/timepicker/timepicker.html",link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f,b.find("input"))}}}),angular.module("ui.bootstrap.typeahead",["ui.bootstrap.position","ui.bootstrap.bindHtml"]).factory("typeaheadParser",["$parse",function(a){var b=/^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;return{parse:function(c){var d=c.match(b);if(!d)throw new Error('Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_" but got "'+c+'".');return{itemName:d[3],source:a(d[4]),viewMapper:a(d[2]||d[1]),modelMapper:a(d[1])}}}}]).directive("typeahead",["$compile","$parse","$q","$timeout","$document","$position","typeaheadParser",function(a,b,c,d,e,f,g){var h=[9,13,27,38,40];return{require:"ngModel",link:function(i,j,k,l){var m,n=i.$eval(k.typeaheadMinLength)||1,o=i.$eval(k.typeaheadWaitMs)||0,p=i.$eval(k.typeaheadEditable)!==!1,q=b(k.typeaheadLoading).assign||angular.noop,r=b(k.typeaheadOnSelect),s=k.typeaheadInputFormatter?b(k.typeaheadInputFormatter):void 0,t=k.typeaheadAppendToBody?i.$eval(k.typeaheadAppendToBody):!1,u=b(k.ngModel).assign,v=g.parse(k.typeahead),w=i.$new();i.$on("$destroy",function(){w.$destroy()});var x="typeahead-"+w.$id+"-"+Math.floor(1e4*Math.random());j.attr({"aria-autocomplete":"list","aria-expanded":!1,"aria-owns":x});var y=angular.element("
    ");y.attr({id:x,matches:"matches",active:"activeIdx",select:"select(activeIdx)",query:"query",position:"position"}),angular.isDefined(k.typeaheadTemplateUrl)&&y.attr("template-url",k.typeaheadTemplateUrl);var z=function(){w.matches=[],w.activeIdx=-1,j.attr("aria-expanded",!1)},A=function(a){return x+"-option-"+a};w.$watch("activeIdx",function(a){0>a?j.removeAttr("aria-activedescendant"):j.attr("aria-activedescendant",A(a))});var B=function(a){var b={$viewValue:a};q(i,!0),c.when(v.source(i,b)).then(function(c){var d=a===l.$viewValue;if(d&&m)if(c.length>0){w.activeIdx=0,w.matches.length=0;for(var e=0;e=n?o>0?(C&&d.cancel(C),C=d(function(){B(a)},o)):B(a):(q(i,!1),z()),p?a:a?void l.$setValidity("editable",!1):(l.$setValidity("editable",!0),a)}),l.$formatters.push(function(a){var b,c,d={};return s?(d.$model=a,s(i,d)):(d[v.itemName]=a,b=v.viewMapper(i,d),d[v.itemName]=void 0,c=v.viewMapper(i,d),b!==c?b:a)}),w.select=function(a){var b,c,e={};e[v.itemName]=c=w.matches[a].model,b=v.modelMapper(i,e),u(i,b),l.$setValidity("editable",!0),r(i,{$item:c,$model:b,$label:v.viewMapper(i,e)}),z(),d(function(){j[0].focus()},0,!1)},j.bind("keydown",function(a){0!==w.matches.length&&-1!==h.indexOf(a.which)&&(a.preventDefault(),40===a.which?(w.activeIdx=(w.activeIdx+1)%w.matches.length,w.$digest()):38===a.which?(w.activeIdx=(w.activeIdx?w.activeIdx:w.matches.length)-1,w.$digest()):13===a.which||9===a.which?w.$apply(function(){w.select(w.activeIdx)}):27===a.which&&(a.stopPropagation(),z(),w.$digest()))}),j.bind("blur",function(){m=!1});var D=function(a){j[0]!==a.target&&(z(),w.$digest())};e.bind("click",D),i.$on("$destroy",function(){e.unbind("click",D)});var E=a(y)(w);t?e.find("body").append(E):j.after(E)}}}]).directive("typeaheadPopup",function(){return{restrict:"EA",scope:{matches:"=",query:"=",active:"=",position:"=",select:"&"},replace:!0,templateUrl:"template/typeahead/typeahead-popup.html",link:function(a,b,c){a.templateUrl=c.templateUrl,a.isOpen=function(){return a.matches.length>0},a.isActive=function(b){return a.active==b},a.selectActive=function(b){a.active=b},a.selectMatch=function(b){a.select({activeIdx:b})}}}}).directive("typeaheadMatch",["$http","$templateCache","$compile","$parse",function(a,b,c,d){return{restrict:"EA",scope:{index:"=",match:"=",query:"="},link:function(e,f,g){var h=d(g.templateUrl)(e.$parent)||"template/typeahead/typeahead-match.html";a.get(h,{cache:b}).success(function(a){f.replaceWith(c(a.trim())(e))})}}}]).filter("typeaheadHighlight",function(){function a(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}return function(b,c){return c?(""+b).replace(new RegExp(a(c),"gi"),"$&"):b}}),angular.module("template/accordion/accordion-group.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion-group.html",'
    \n
    \n

    \n {{heading}}\n

    \n
    \n
    \n
    \n
    \n
    ')}]),angular.module("template/accordion/accordion.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion.html",'
    ')}]),angular.module("template/alert/alert.html",[]).run(["$templateCache",function(a){a.put("template/alert/alert.html",'\n')}]),angular.module("template/carousel/carousel.html",[]).run(["$templateCache",function(a){a.put("template/carousel/carousel.html",'\n')}]),angular.module("template/carousel/slide.html",[]).run(["$templateCache",function(a){a.put("template/carousel/slide.html","
    \n")}]),angular.module("template/datepicker/datepicker.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/datepicker.html",'
    \n \n \n \n
    ')}]),angular.module("template/datepicker/day.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/day.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
    {{label.abbr}}
    {{ weekNumbers[$index] }}\n \n
    \n')}]),angular.module("template/datepicker/month.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/month.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n')}]),angular.module("template/datepicker/popup.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/popup.html",'\n')}]),angular.module("template/datepicker/year.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/year.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n')}]),angular.module("template/modal/backdrop.html",[]).run(["$templateCache",function(a){a.put("template/modal/backdrop.html",'\n')}]),angular.module("template/modal/window.html",[]).run(["$templateCache",function(a){a.put("template/modal/window.html",'')}]),angular.module("template/pagination/pager.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pager.html",'')}]),angular.module("template/pagination/pagination.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pagination.html",'')}]),angular.module("template/tooltip/tooltip-html-unsafe-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-html-unsafe-popup.html",'
    \n
    \n
    \n
    \n')}]),angular.module("template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-popup.html",'
    \n
    \n
    \n
    \n')}]),angular.module("template/popover/popover.html",[]).run(["$templateCache",function(a){a.put("template/popover/popover.html",'
    \n
    \n\n
    \n

    \n
    \n
    \n
    \n')}]),angular.module("template/progressbar/bar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/bar.html",'
    ')}]),angular.module("template/progressbar/progress.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progress.html",'
    ')}]),angular.module("template/progressbar/progressbar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progressbar.html",'
    \n
    \n
    ')}]),angular.module("template/rating/rating.html",[]).run(["$templateCache",function(a){a.put("template/rating/rating.html",'\n \n ({{ $index < value ? \'*\' : \' \' }})\n \n')}]),angular.module("template/tabs/tab.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tab.html",'
  • \n {{heading}}\n
  • \n')}]),angular.module("template/tabs/tabset-titles.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset-titles.html","
      \n
    \n")}]),angular.module("template/tabs/tabset.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset.html",'\n
    \n \n
    \n
    \n
    \n
    \n
    \n')}]),angular.module("template/timepicker/timepicker.html",[]).run(["$templateCache",function(a){a.put("template/timepicker/timepicker.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
     
    \n \n :\n \n
     
    \n')}]),angular.module("template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-match.html",'')}]),angular.module("template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-popup.html",'') +}]); \ No newline at end of file diff --git a/app/bower_components/angular-bootstrap/ui-bootstrap.js b/app/bower_components/angular-bootstrap/ui-bootstrap.js new file mode 100644 index 0000000..c93f0de --- /dev/null +++ b/app/bower_components/angular-bootstrap/ui-bootstrap.js @@ -0,0 +1,3799 @@ +/* + * angular-ui-bootstrap + * http://angular-ui.github.io/bootstrap/ + + * Version: 0.11.0 - 2014-05-01 + * License: MIT + */ +angular.module("ui.bootstrap", ["ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","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.transition', []) + +/** + * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete. + * @param {DOMElement} element The DOMElement that will be animated. + * @param {string|object|function} trigger The thing that will cause the transition to start: + * - As a string, it represents the css class to be added to the element. + * - As an object, it represents a hash of style attributes to be applied to the element. + * - As a function, it represents a function to be called that will cause the transition to occur. + * @return {Promise} A promise that is resolved when the transition finishes. + */ +.factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) { + + var $transition = function(element, trigger, options) { + options = options || {}; + var deferred = $q.defer(); + var endEventName = $transition[options.animation ? 'animationEndEventName' : 'transitionEndEventName']; + + var transitionEndHandler = function(event) { + $rootScope.$apply(function() { + element.unbind(endEventName, transitionEndHandler); + deferred.resolve(element); + }); + }; + + if (endEventName) { + element.bind(endEventName, transitionEndHandler); + } + + // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur + $timeout(function() { + if ( angular.isString(trigger) ) { + element.addClass(trigger); + } else if ( angular.isFunction(trigger) ) { + trigger(element); + } else if ( angular.isObject(trigger) ) { + element.css(trigger); + } + //If browser does not support transitions, instantly resolve + if ( !endEventName ) { + deferred.resolve(element); + } + }); + + // Add our custom cancel function to the promise that is returned + // We can call this if we are about to run a new transition, which we know will prevent this transition from ending, + // i.e. it will therefore never raise a transitionEnd event for that transition + deferred.promise.cancel = function() { + if ( endEventName ) { + element.unbind(endEventName, transitionEndHandler); + } + deferred.reject('Transition cancelled'); + }; + + return deferred.promise; + }; + + // Work out the name of the transitionEnd event + var transElement = document.createElement('trans'); + var transitionEndEventNames = { + 'WebkitTransition': 'webkitTransitionEnd', + 'MozTransition': 'transitionend', + 'OTransition': 'oTransitionEnd', + 'transition': 'transitionend' + }; + var animationEndEventNames = { + 'WebkitTransition': 'webkitAnimationEnd', + 'MozTransition': 'animationend', + 'OTransition': 'oAnimationEnd', + 'transition': 'animationend' + }; + function findEndEventName(endEventNames) { + for (var name in endEventNames){ + if (transElement.style[name] !== undefined) { + return endEventNames[name]; + } + } + } + $transition.transitionEndEventName = findEndEventName(transitionEndEventNames); + $transition.animationEndEventName = findEndEventName(animationEndEventNames); + return $transition; +}]); + +angular.module('ui.bootstrap.collapse', ['ui.bootstrap.transition']) + + .directive('collapse', ['$transition', function ($transition) { + + return { + link: function (scope, element, attrs) { + + var initialAnimSkip = true; + var currentTransition; + + function doTransition(change) { + var newTransition = $transition(element, change); + if (currentTransition) { + currentTransition.cancel(); + } + currentTransition = newTransition; + newTransition.then(newTransitionDone, newTransitionDone); + return newTransition; + + function newTransitionDone() { + // Make sure it's this transition, otherwise, leave it alone. + if (currentTransition === newTransition) { + currentTransition = undefined; + } + } + } + + function expand() { + if (initialAnimSkip) { + initialAnimSkip = false; + expandDone(); + } else { + element.removeClass('collapse').addClass('collapsing'); + doTransition({ height: element[0].scrollHeight + 'px' }).then(expandDone); + } + } + + function expandDone() { + element.removeClass('collapsing'); + element.addClass('collapse in'); + element.css({height: 'auto'}); + } + + function collapse() { + if (initialAnimSkip) { + initialAnimSkip = false; + collapseDone(); + element.css({height: 0}); + } else { + // CSS transitions don't work with height: auto, so we have to manually change the height to a specific value + element.css({ height: element[0].scrollHeight + 'px' }); + //trigger reflow so a browser realizes that height was updated from auto to a specific value + var x = element[0].offsetWidth; + + element.removeClass('collapse in').addClass('collapsing'); + + doTransition({ height: 0 }).then(collapseDone); + } + } + + function collapseDone() { + element.removeClass('collapsing'); + element.addClass('collapse'); + } + + scope.$watch(attrs.collapse, function (shouldCollapse) { + if (shouldCollapse) { + collapse(); + } else { + expand(); + } + }); + } + }; + }]); + +angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) + +.constant('accordionConfig', { + closeOthers: true +}) + +.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', 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('accordion', function () { + return { + restrict:'EA', + controller:'AccordionController', + transclude: true, + replace: false, + templateUrl: 'template/accordion/accordion.html' + }; +}) + +// The accordion-group directive indicates a block of html that will expand and collapse in an accordion +.directive('accordionGroup', function() { + return { + require:'^accordion', // We need this directive to be inside an accordion + restrict:'EA', + transclude:true, // It transcludes the contents of the directive into the template + replace: true, // The element containing the directive will be replaced with the template + templateUrl:'template/accordion/accordion-group.html', + scope: { + heading: '@', // Interpolate the heading attribute onto this scope + isOpen: '=?', + isDisabled: '=?' + }, + controller: function() { + this.setHeading = function(element) { + this.heading = element; + }; + }, + link: function(scope, element, attrs, accordionCtrl) { + accordionCtrl.addGroup(scope); + + scope.$watch('isOpen', function(value) { + if ( value ) { + accordionCtrl.closeOthers(scope); + } + }); + + scope.toggleOpen = function() { + if ( !scope.isDisabled ) { + scope.isOpen = !scope.isOpen; + } + }; + } + }; +}) + +// Use accordion-heading below an accordion-group to provide a heading containing HTML +// +// Heading containing HTML - +// +.directive('accordionHeading', function() { + return { + restrict: 'EA', + transclude: true, // Grab the contents to be used as the heading + template: '', // In effect remove this element! + replace: true, + require: '^accordionGroup', + link: function(scope, element, attr, 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, function() {})); + } + }; +}) + +// 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('accordionTransclude', function() { + return { + require: '^accordionGroup', + link: function(scope, element, attr, controller) { + scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) { + if ( heading ) { + element.html(''); + element.append(heading); + } + }); + } + }; +}); + +angular.module('ui.bootstrap.alert', []) + +.controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) { + $scope.closeable = 'close' in $attrs; +}]) + +.directive('alert', function () { + return { + restrict:'EA', + controller:'AlertController', + templateUrl:'template/alert/alert.html', + transclude:true, + replace:true, + scope: { + type: '@', + close: '&' + } + }; +}); + +angular.module('ui.bootstrap.bindHtml', []) + + .directive('bindHtmlUnsafe', function () { + return function (scope, element, attr) { + element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe); + scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) { + element.html(value || ''); + }); + }; + }); +angular.module('ui.bootstrap.buttons', []) + +.constant('buttonConfig', { + activeClass: 'active', + toggleEvent: 'click' +}) + +.controller('ButtonsController', ['buttonConfig', function(buttonConfig) { + this.activeClass = buttonConfig.activeClass || 'active'; + this.toggleEvent = buttonConfig.toggleEvent || 'click'; +}]) + +.directive('btnRadio', function () { + return { + require: ['btnRadio', 'ngModel'], + controller: 'ButtonsController', + link: function (scope, element, attrs, ctrls) { + var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + //model -> UI + ngModelCtrl.$render = function () { + element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio))); + }; + + //ui->model + element.bind(buttonsCtrl.toggleEvent, function () { + var isActive = element.hasClass(buttonsCtrl.activeClass); + + if (!isActive || angular.isDefined(attrs.uncheckable)) { + scope.$apply(function () { + ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio)); + ngModelCtrl.$render(); + }); + } + }); + } + }; +}) + +.directive('btnCheckbox', function () { + return { + require: ['btnCheckbox', 'ngModel'], + controller: 'ButtonsController', + link: function (scope, element, attrs, ctrls) { + var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + function getTrueValue() { + return getCheckboxValue(attrs.btnCheckboxTrue, true); + } + + function getFalseValue() { + return getCheckboxValue(attrs.btnCheckboxFalse, false); + } + + function getCheckboxValue(attributeValue, defaultValue) { + var val = scope.$eval(attributeValue); + return angular.isDefined(val) ? val : defaultValue; + } + + //model -> UI + ngModelCtrl.$render = function () { + element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue())); + }; + + //ui->model + element.bind(buttonsCtrl.toggleEvent, function () { + scope.$apply(function () { + ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue()); + ngModelCtrl.$render(); + }); + }); + } + }; +}); + +/** +* @ngdoc overview +* @name ui.bootstrap.carousel +* +* @description +* AngularJS version of an image carousel. +* +*/ +angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition']) +.controller('CarouselController', ['$scope', '$timeout', '$transition', function ($scope, $timeout, $transition) { + var self = this, + slides = self.slides = $scope.slides = [], + currentIndex = -1, + currentTimeout, isPlaying; + self.currentSlide = null; + + var destroyed = false; + /* direction: "prev" or "next" */ + self.select = $scope.select = function(nextSlide, direction) { + var nextIndex = slides.indexOf(nextSlide); + //Decide direction if it's not given + if (direction === undefined) { + direction = nextIndex > currentIndex ? 'next' : 'prev'; + } + if (nextSlide && nextSlide !== self.currentSlide) { + if ($scope.$currentTransition) { + $scope.$currentTransition.cancel(); + //Timeout so ng-class in template has time to fix classes for finished slide + $timeout(goNext); + } else { + goNext(); + } + } + function goNext() { + // Scope has been destroyed, stop here. + if (destroyed) { return; } + //If we have a slide to transition from and we have a transition type and we're allowed, go + if (self.currentSlide && angular.isString(direction) && !$scope.noTransition && nextSlide.$element) { + //We shouldn't do class manip in here, but it's the same weird thing bootstrap does. need to fix sometime + nextSlide.$element.addClass(direction); + var reflow = nextSlide.$element[0].offsetWidth; //force reflow + + //Set all other slides to stop doing their stuff for the new transition + angular.forEach(slides, function(slide) { + angular.extend(slide, {direction: '', entering: false, leaving: false, active: false}); + }); + angular.extend(nextSlide, {direction: direction, active: true, entering: true}); + angular.extend(self.currentSlide||{}, {direction: direction, leaving: true}); + + $scope.$currentTransition = $transition(nextSlide.$element, {}); + //We have to create new pointers inside a closure since next & current will change + (function(next,current) { + $scope.$currentTransition.then( + function(){ transitionDone(next, current); }, + function(){ transitionDone(next, current); } + ); + }(nextSlide, self.currentSlide)); + } else { + transitionDone(nextSlide, self.currentSlide); + } + self.currentSlide = nextSlide; + currentIndex = nextIndex; + //every time you change slides, reset the timer + restartTimer(); + } + function transitionDone(next, current) { + angular.extend(next, {direction: '', active: true, leaving: false, entering: false}); + angular.extend(current||{}, {direction: '', active: false, leaving: false, entering: false}); + $scope.$currentTransition = null; + } + }; + $scope.$on('$destroy', function () { + destroyed = true; + }); + + /* Allow outside people to call indexOf on slides array */ + self.indexOfSlide = function(slide) { + return slides.indexOf(slide); + }; + + $scope.next = function() { + var newIndex = (currentIndex + 1) % slides.length; + + //Prevent this user-triggered transition from occurring if there is already one in progress + if (!$scope.$currentTransition) { + return self.select(slides[newIndex], 'next'); + } + }; + + $scope.prev = function() { + var newIndex = currentIndex - 1 < 0 ? slides.length - 1 : currentIndex - 1; + + //Prevent this user-triggered transition from occurring if there is already one in progress + if (!$scope.$currentTransition) { + return self.select(slides[newIndex], 'prev'); + } + }; + + $scope.isActive = function(slide) { + return self.currentSlide === slide; + }; + + $scope.$watch('interval', restartTimer); + $scope.$on('$destroy', resetTimer); + + function restartTimer() { + resetTimer(); + var interval = +$scope.interval; + if (!isNaN(interval) && interval>=0) { + currentTimeout = $timeout(timerFn, interval); + } + } + + function resetTimer() { + if (currentTimeout) { + $timeout.cancel(currentTimeout); + currentTimeout = null; + } + } + + function timerFn() { + if (isPlaying) { + $scope.next(); + restartTimer(); + } else { + $scope.pause(); + } + } + + $scope.play = function() { + if (!isPlaying) { + isPlaying = true; + restartTimer(); + } + }; + $scope.pause = function() { + if (!$scope.noPause) { + isPlaying = false; + resetTimer(); + } + }; + + self.addSlide = function(slide, element) { + slide.$element = element; + slides.push(slide); + //if this is the first slide or the slide is set to active, select it + if(slides.length === 1 || slide.active) { + self.select(slides[slides.length-1]); + if (slides.length == 1) { + $scope.play(); + } + } else { + slide.active = false; + } + }; + + self.removeSlide = function(slide) { + //get the index of the slide inside the carousel + var index = slides.indexOf(slide); + slides.splice(index, 1); + if (slides.length > 0 && slide.active) { + if (index >= slides.length) { + self.select(slides[index-1]); + } else { + self.select(slides[index]); + } + } else if (currentIndex > index) { + currentIndex--; + } + }; + +}]) + +/** + * @ngdoc directive + * @name ui.bootstrap.carousel.directive:carousel + * @restrict EA + * + * @description + * Carousel is the outer container for a set of image 'slides' to showcase. + * + * @param {number=} interval The time, in milliseconds, that it will take the carousel to go to the next slide. + * @param {boolean=} noTransition Whether to disable transitions on the carousel. + * @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover). + * + * @example + + + + + + + + + + + + + + + .carousel-indicators { + top: auto; + bottom: 15px; + } + + + */ +.directive('carousel', [function() { + return { + restrict: 'EA', + transclude: true, + replace: true, + controller: 'CarouselController', + require: 'carousel', + templateUrl: 'template/carousel/carousel.html', + scope: { + interval: '=', + noTransition: '=', + noPause: '=' + } + }; +}]) + +/** + * @ngdoc directive + * @name ui.bootstrap.carousel.directive:slide + * @restrict EA + * + * @description + * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}. Must be placed as a child of a carousel element. + * + * @param {boolean=} active Model binding, whether or not this slide is currently active. + * + * @example + + +
    + + + + + + + Interval, in milliseconds: +
    Enter a negative number to stop the interval. +
    +
    + +function CarouselDemoCtrl($scope) { + $scope.myInterval = 5000; +} + + + .carousel-indicators { + top: auto; + bottom: 15px; + } + +
    +*/ + +.directive('slide', function() { + return { + require: '^carousel', + restrict: 'EA', + transclude: true, + replace: true, + templateUrl: 'template/carousel/slide.html', + scope: { + active: '=?' + }, + link: function (scope, element, attrs, carouselCtrl) { + carouselCtrl.addSlide(scope, element); + //when the scope is destroyed then remove the slide from the current slides array + scope.$on('$destroy', function() { + carouselCtrl.removeSlide(scope); + }); + + scope.$watch('active', function(active) { + if (active) { + carouselCtrl.select(scope); + } + }); + } + }; +}); + +angular.module('ui.bootstrap.dateparser', []) + +.service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) { + + this.parsers = {}; + + var formatCodeToRegex = { + 'yyyy': { + regex: '\\d{4}', + apply: function(value) { this.year = +value; } + }, + 'yy': { + regex: '\\d{2}', + apply: function(value) { this.year = +value + 2000; } + }, + 'y': { + regex: '\\d{1,4}', + apply: function(value) { this.year = +value; } + }, + 'MMMM': { + regex: $locale.DATETIME_FORMATS.MONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); } + }, + 'MMM': { + regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); } + }, + 'MM': { + regex: '0[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; } + }, + 'M': { + regex: '[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; } + }, + 'dd': { + regex: '[0-2][0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; } + }, + 'd': { + regex: '[1-2]?[0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; } + }, + 'EEEE': { + regex: $locale.DATETIME_FORMATS.DAY.join('|') + }, + 'EEE': { + regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|') + } + }; + + this.createParser = function(format) { + var map = [], regex = format.split(''); + + angular.forEach(formatCodeToRegex, function(data, code) { + var index = format.indexOf(code); + + if (index > -1) { + format = format.split(''); + + regex[index] = '(' + data.regex + ')'; + format[index] = '$'; // Custom symbol to define consumed part of format + for (var i = index + 1, n = index + code.length; i < n; i++) { + regex[i] = ''; + format[i] = '$'; + } + format = format.join(''); + + map.push({ index: index, apply: data.apply }); + } + }); + + return { + regex: new RegExp('^' + regex.join('') + '$'), + map: orderByFilter(map, 'index') + }; + }; + + this.parse = function(input, format) { + if ( !angular.isString(input) ) { + return input; + } + + format = $locale.DATETIME_FORMATS[format] || format; + + if ( !this.parsers[format] ) { + this.parsers[format] = this.createParser(format); + } + + var parser = this.parsers[format], + regex = parser.regex, + map = parser.map, + results = input.match(regex); + + if ( results && results.length ) { + var fields = { year: 1900, month: 0, date: 1, hours: 0 }, dt; + + for( var i = 1, n = results.length; i < n; i++ ) { + var mapper = map[i-1]; + if ( mapper.apply ) { + mapper.apply.call(fields, results[i]); + } + } + + if ( isValid(fields.year, fields.month, fields.date) ) { + dt = new Date( fields.year, fields.month, fields.date, fields.hours); + } + + 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 ( month === 1 && date > 28) { + return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0); + } + + if ( month === 3 || month === 5 || month === 8 || month === 10) { + return date < 31; + } + + return true; + } +}]); + +angular.module('ui.bootstrap.position', []) + +/** + * A set of utility methods that can be use to retrieve position of DOM elements. + * It is meant to be used where we need to absolute-position DOM elements in + * relation to other, existing elements (this is the case for tooltips, popovers, + * typeahead suggestions etc.). + */ + .factory('$position', ['$document', '$window', function ($document, $window) { + + function getStyle(el, cssprop) { + if (el.currentStyle) { //IE + return el.currentStyle[cssprop]; + } else if ($window.getComputedStyle) { + return $window.getComputedStyle(el)[cssprop]; + } + // finally try and get inline style + return el.style[cssprop]; + } + + /** + * Checks if a given element is statically positioned + * @param element - raw DOM element + */ + function isStaticPositioned(element) { + return (getStyle(element, 'position') || 'static' ) === 'static'; + } + + /** + * returns the closest, non-statically positioned parentOffset of a given element + * @param element + */ + var parentOffsetEl = function (element) { + var docDomEl = $document[0]; + var offsetParent = element.offsetParent || docDomEl; + while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { + offsetParent = offsetParent.offsetParent; + } + return offsetParent || docDomEl; + }; + + return { + /** + * Provides read-only equivalent of jQuery's position function: + * http://api.jquery.com/position/ + */ + position: function (element) { + var elBCR = this.offset(element); + var offsetParentBCR = { top: 0, left: 0 }; + var offsetParentEl = parentOffsetEl(element[0]); + if (offsetParentEl != $document[0]) { + offsetParentBCR = this.offset(angular.element(offsetParentEl)); + offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; + offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; + } + + var boundingClientRect = element[0].getBoundingClientRect(); + return { + width: boundingClientRect.width || element.prop('offsetWidth'), + height: boundingClientRect.height || element.prop('offsetHeight'), + top: elBCR.top - offsetParentBCR.top, + left: elBCR.left - offsetParentBCR.left + }; + }, + + /** + * Provides read-only equivalent of jQuery's offset function: + * http://api.jquery.com/offset/ + */ + offset: function (element) { + var boundingClientRect = element[0].getBoundingClientRect(); + return { + width: boundingClientRect.width || element.prop('offsetWidth'), + height: boundingClientRect.height || element.prop('offsetHeight'), + top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop), + left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft) + }; + }, + + /** + * Provides coordinates for the targetEl in relation to hostEl + */ + positionElements: function (hostEl, targetEl, positionStr, appendToBody) { + + var positionStrParts = positionStr.split('-'); + var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center'; + + var hostElPos, + targetElWidth, + targetElHeight, + targetElPos; + + hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl); + + targetElWidth = targetEl.prop('offsetWidth'); + targetElHeight = targetEl.prop('offsetHeight'); + + var shiftWidth = { + center: function () { + return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2; + }, + left: function () { + return hostElPos.left; + }, + right: function () { + return hostElPos.left + hostElPos.width; + } + }; + + var shiftHeight = { + center: function () { + return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2; + }, + top: function () { + return hostElPos.top; + }, + bottom: function () { + return hostElPos.top + hostElPos.height; + } + }; + + switch (pos0) { + case 'right': + targetElPos = { + top: shiftHeight[pos1](), + left: shiftWidth[pos0]() + }; + break; + case 'left': + targetElPos = { + top: shiftHeight[pos1](), + left: hostElPos.left - targetElWidth + }; + break; + case 'bottom': + targetElPos = { + top: shiftHeight[pos0](), + left: shiftWidth[pos1]() + }; + break; + default: + targetElPos = { + top: hostElPos.top - targetElHeight, + left: shiftWidth[pos1]() + }; + break; + } + + return targetElPos; + } + }; + }]); + +angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position']) + +.constant('datepickerConfig', { + formatDay: 'dd', + formatMonth: 'MMMM', + formatYear: 'yyyy', + formatDayHeader: 'EEE', + formatDayTitle: 'MMMM yyyy', + formatMonthTitle: 'yyyy', + datepickerMode: 'day', + minMode: 'day', + maxMode: 'year', + showWeeks: true, + startingDay: 0, + yearRange: 20, + minDate: null, + maxDate: null +}) + +.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) { + var self = this, + ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl; + + // Modes chain + this.modes = ['day', 'month', 'year']; + + // Configuration attributes + angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', + 'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange'], function( key, index ) { + self[key] = angular.isDefined($attrs[key]) ? (index < 8 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key]; + }); + + // Watchable attributes + angular.forEach(['minDate', 'maxDate'], function( key ) { + if ( $attrs[key] ) { + $scope.$parent.$watch($parse($attrs[key]), function(value) { + self[key] = value ? new Date(value) : null; + self.refreshView(); + }); + } else { + self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null; + } + }); + + $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode; + $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000); + this.activeDate = angular.isDefined($attrs.initDate) ? $scope.$parent.$eval($attrs.initDate) : new Date(); + + $scope.isActive = function(dateObject) { + if (self.compare(dateObject.date, self.activeDate) === 0) { + $scope.activeDateId = dateObject.uid; + return true; + } + return false; + }; + + this.init = function( ngModelCtrl_ ) { + ngModelCtrl = ngModelCtrl_; + + ngModelCtrl.$render = function() { + self.render(); + }; + }; + + this.render = function() { + if ( ngModelCtrl.$modelValue ) { + var date = new Date( ngModelCtrl.$modelValue ), + isValid = !isNaN(date); + + if ( isValid ) { + this.activeDate = date; + } else { + $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); + } + ngModelCtrl.$setValidity('date', isValid); + } + this.refreshView(); + }; + + this.refreshView = function() { + if ( this.element ) { + this._refreshView(); + + var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; + ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date))); + } + }; + + this.createDateObject = function(date, format) { + var model = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; + return { + date: date, + label: dateFilter(date, format), + selected: model && this.compare(date, model) === 0, + disabled: this.isDisabled(date), + current: this.compare(date, new Date()) === 0 + }; + }; + + this.isDisabled = function( date ) { + return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($attrs.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode}))); + }; + + // 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.$modelValue ? new Date( ngModelCtrl.$modelValue ) : new Date(0, 0, 0, 0, 0, 0, 0); + dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() ); + ngModelCtrl.$setViewValue( dt ); + ngModelCtrl.$render(); + } else { + self.activeDate = date; + $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ]; + } + }; + + $scope.move = function( direction ) { + var year = self.activeDate.getFullYear() + direction * (self.step.years || 0), + month = self.activeDate.getMonth() + direction * (self.step.months || 0); + self.activeDate.setFullYear(year, month, 1); + self.refreshView(); + }; + + $scope.toggleMode = function( direction ) { + direction = direction || 1; + + if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) { + return; + } + + $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) + direction ]; + }; + + // Key event mapper + $scope.keys = { 13:'enter', 32:'space', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left', 38:'up', 39:'right', 40:'down' }; + + var focusElement = function() { + $timeout(function() { + self.element[0].focus(); + }, 0 , false); + }; + + // Listen for focus requests from popup directive + $scope.$on('datepicker.focus', focusElement); + + $scope.keydown = function( evt ) { + var key = $scope.keys[evt.which]; + + if ( !key || evt.shiftKey || evt.altKey ) { + return; + } + + evt.preventDefault(); + evt.stopPropagation(); + + if (key === 'enter' || key === 'space') { + if ( self.isDisabled(self.activeDate)) { + return; // do nothing + } + $scope.select(self.activeDate); + focusElement(); + } else if (evt.ctrlKey && (key === 'up' || key === 'down')) { + $scope.toggleMode(key === 'up' ? 1 : -1); + focusElement(); + } else { + self.handleKeyDown(key, evt); + self.refreshView(); + } + }; +}]) + +.directive( 'datepicker', function () { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/datepicker.html', + scope: { + datepickerMode: '=?', + dateDisabled: '&' + }, + require: ['datepicker', '?^ngModel'], + controller: 'DatepickerController', + link: function(scope, element, attrs, ctrls) { + var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if ( ngModelCtrl ) { + datepickerCtrl.init( ngModelCtrl ); + } + } + }; +}) + +.directive('daypicker', ['dateFilter', function (dateFilter) { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/day.html', + require: '^datepicker', + link: function(scope, element, attrs, ctrl) { + scope.showWeeks = ctrl.showWeeks; + + ctrl.step = { months: 1 }; + ctrl.element = element; + + var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + function getDaysInMonth( year, month ) { + return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month]; + } + + function getDates(startDate, n) { + var dates = new Array(n), current = new Date(startDate), i = 0; + current.setHours(12); // Prevent repeated dates because of timezone bug + while ( i < n ) { + dates[i++] = new Date(current); + current.setDate( current.getDate() + 1 ); + } + return dates; + } + + ctrl._refreshView = function() { + var year = ctrl.activeDate.getFullYear(), + month = ctrl.activeDate.getMonth(), + firstDayOfMonth = new Date(year, month, 1), + difference = ctrl.startingDay - firstDayOfMonth.getDay(), + numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, + firstDate = new Date(firstDayOfMonth); + + if ( numDisplayedFromPreviousMonth > 0 ) { + firstDate.setDate( - numDisplayedFromPreviousMonth + 1 ); + } + + // 42 is the number of days on a six-month calendar + var days = getDates(firstDate, 42); + for (var i = 0; i < 42; i ++) { + days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), { + secondary: days[i].getMonth() !== month, + uid: scope.uniqueId + '-' + i + }); + } + + scope.labels = new Array(7); + for (var j = 0; j < 7; j++) { + scope.labels[j] = { + abbr: dateFilter(days[j].date, ctrl.formatDayHeader), + full: dateFilter(days[j].date, 'EEEE') + }; + } + + scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle); + scope.rows = ctrl.split(days, 7); + + if ( scope.showWeeks ) { + scope.weekNumbers = []; + var weekNumber = getISO8601WeekNumber( scope.rows[0][0].date ), + numWeeks = scope.rows.length; + while( scope.weekNumbers.push(weekNumber++) < numWeeks ) {} + } + }; + + ctrl.compare = function(date1, date2) { + return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) ); + }; + + function getISO8601WeekNumber(date) { + var checkDate = new Date(date); + checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday + var time = checkDate.getTime(); + checkDate.setMonth(0); // Compare with Jan 1 + checkDate.setDate(1); + return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; + } + + ctrl.handleKeyDown = function( key, evt ) { + var date = ctrl.activeDate.getDate(); + + if (key === 'left') { + date = date - 1; // up + } else if (key === 'up') { + date = date - 7; // down + } else if (key === 'right') { + date = date + 1; // down + } else if (key === 'down') { + date = date + 7; + } else if (key === 'pageup' || key === 'pagedown') { + var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1); + ctrl.activeDate.setMonth(month, 1); + date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date); + } else if (key === 'home') { + date = 1; + } else if (key === 'end') { + date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()); + } + ctrl.activeDate.setDate(date); + }; + + ctrl.refreshView(); + } + }; +}]) + +.directive('monthpicker', ['dateFilter', function (dateFilter) { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/month.html', + require: '^datepicker', + link: function(scope, element, attrs, ctrl) { + ctrl.step = { years: 1 }; + ctrl.element = element; + + ctrl._refreshView = function() { + var months = new Array(12), + year = ctrl.activeDate.getFullYear(); + + for ( var i = 0; i < 12; i++ ) { + months[i] = angular.extend(ctrl.createDateObject(new Date(year, i, 1), ctrl.formatMonth), { + uid: scope.uniqueId + '-' + i + }); + } + + scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle); + scope.rows = ctrl.split(months, 3); + }; + + ctrl.compare = function(date1, date2) { + return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() ); + }; + + ctrl.handleKeyDown = function( key, evt ) { + var date = ctrl.activeDate.getMonth(); + + if (key === 'left') { + date = date - 1; // up + } else if (key === 'up') { + date = date - 3; // down + } else if (key === 'right') { + date = date + 1; // down + } else if (key === 'down') { + date = date + 3; + } else if (key === 'pageup' || key === 'pagedown') { + var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); + ctrl.activeDate.setFullYear(year); + } else if (key === 'home') { + date = 0; + } else if (key === 'end') { + date = 11; + } + ctrl.activeDate.setMonth(date); + }; + + ctrl.refreshView(); + } + }; +}]) + +.directive('yearpicker', ['dateFilter', function (dateFilter) { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/year.html', + require: '^datepicker', + link: function(scope, element, attrs, ctrl) { + var range = ctrl.yearRange; + + ctrl.step = { years: range }; + ctrl.element = element; + + function getStartingYear( year ) { + return parseInt((year - 1) / range, 10) * range + 1; + } + + ctrl._refreshView = function() { + var years = new Array(range); + + for ( var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++ ) { + years[i] = angular.extend(ctrl.createDateObject(new Date(start + i, 0, 1), ctrl.formatYear), { + uid: scope.uniqueId + '-' + i + }); + } + + scope.title = [years[0].label, years[range - 1].label].join(' - '); + scope.rows = ctrl.split(years, 5); + }; + + ctrl.compare = function(date1, date2) { + return date1.getFullYear() - date2.getFullYear(); + }; + + ctrl.handleKeyDown = function( key, evt ) { + var date = ctrl.activeDate.getFullYear(); + + if (key === 'left') { + date = date - 1; // up + } else if (key === 'up') { + date = date - 5; // down + } else if (key === 'right') { + date = date + 1; // down + } else if (key === 'down') { + date = date + 5; + } else if (key === 'pageup' || key === 'pagedown') { + date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years; + } else if (key === 'home') { + date = getStartingYear( ctrl.activeDate.getFullYear() ); + } else if (key === 'end') { + date = getStartingYear( ctrl.activeDate.getFullYear() ) + range - 1; + } + ctrl.activeDate.setFullYear(date); + }; + + ctrl.refreshView(); + } + }; +}]) + +.constant('datepickerPopupConfig', { + datepickerPopup: 'yyyy-MM-dd', + currentText: 'Today', + clearText: 'Clear', + closeText: 'Done', + closeOnDateSelection: true, + appendToBody: false, + showButtonBar: true +}) + +.directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig', +function ($compile, $parse, $document, $position, dateFilter, dateParser, datepickerPopupConfig) { + return { + restrict: 'EA', + require: 'ngModel', + scope: { + isOpen: '=?', + currentText: '@', + clearText: '@', + closeText: '@', + dateDisabled: '&' + }, + link: function(scope, element, attrs, ngModel) { + var dateFormat, + closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection, + appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody; + + scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar; + + scope.getText = function( key ) { + return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; + }; + + attrs.$observe('datepickerPopup', function(value) { + dateFormat = value || datepickerPopupConfig.datepickerPopup; + ngModel.$render(); + }); + + // popup element used to display calendar + var popupEl = angular.element('
    '); + popupEl.attr({ + 'ng-model': 'date', + 'ng-change': 'dateSelection()' + }); + + function cameltoDash( string ){ + return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); + } + + // datepicker element + var datepickerEl = angular.element(popupEl.children()[0]); + if ( attrs.datepickerOptions ) { + angular.forEach(scope.$parent.$eval(attrs.datepickerOptions), function( value, option ) { + datepickerEl.attr( cameltoDash(option), value ); + }); + } + + angular.forEach(['minDate', 'maxDate'], function( key ) { + if ( attrs[key] ) { + scope.$parent.$watch($parse(attrs[key]), function(value){ + scope[key] = value; + }); + datepickerEl.attr(cameltoDash(key), key); + } + }); + if (attrs.dateDisabled) { + datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); + } + + function parseDate(viewValue) { + if (!viewValue) { + ngModel.$setValidity('date', true); + return null; + } else if (angular.isDate(viewValue) && !isNaN(viewValue)) { + ngModel.$setValidity('date', true); + return viewValue; + } else if (angular.isString(viewValue)) { + var date = dateParser.parse(viewValue, dateFormat) || new Date(viewValue); + if (isNaN(date)) { + ngModel.$setValidity('date', false); + return undefined; + } else { + ngModel.$setValidity('date', true); + return date; + } + } else { + ngModel.$setValidity('date', false); + return undefined; + } + } + ngModel.$parsers.unshift(parseDate); + + // Inner change + scope.dateSelection = function(dt) { + if (angular.isDefined(dt)) { + scope.date = dt; + } + ngModel.$setViewValue(scope.date); + ngModel.$render(); + + if ( closeOnDateSelection ) { + scope.isOpen = false; + element[0].focus(); + } + }; + + element.bind('input change keyup', function() { + scope.$apply(function() { + scope.date = ngModel.$modelValue; + }); + }); + + // Outter change + ngModel.$render = function() { + var date = ngModel.$viewValue ? dateFilter(ngModel.$viewValue, dateFormat) : ''; + element.val(date); + scope.date = parseDate( ngModel.$modelValue ); + }; + + var documentClickBind = function(event) { + if (scope.isOpen && event.target !== element[0]) { + scope.$apply(function() { + scope.isOpen = false; + }); + } + }; + + var keydown = function(evt, noApply) { + scope.keydown(evt); + }; + element.bind('keydown', keydown); + + scope.keydown = function(evt) { + if (evt.which === 27) { + evt.preventDefault(); + evt.stopPropagation(); + scope.close(); + } else if (evt.which === 40 && !scope.isOpen) { + scope.isOpen = true; + } + }; + + scope.$watch('isOpen', function(value) { + if (value) { + scope.$broadcast('datepicker.focus'); + scope.position = appendToBody ? $position.offset(element) : $position.position(element); + scope.position.top = scope.position.top + element.prop('offsetHeight'); + + $document.bind('click', documentClickBind); + } else { + $document.unbind('click', documentClickBind); + } + }); + + scope.select = function( date ) { + if (date === 'today') { + var today = new Date(); + if (angular.isDate(ngModel.$modelValue)) { + date = new Date(ngModel.$modelValue); + date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); + } else { + date = new Date(today.setHours(0, 0, 0, 0)); + } + } + scope.dateSelection( date ); + }; + + scope.close = function() { + scope.isOpen = false; + element[0].focus(); + }; + + var $popup = $compile(popupEl)(scope); + if ( appendToBody ) { + $document.find('body').append($popup); + } else { + element.after($popup); + } + + scope.$on('$destroy', function() { + $popup.remove(); + element.unbind('keydown', keydown); + $document.unbind('click', documentClickBind); + }); + } + }; +}]) + +.directive('datepickerPopupWrap', function() { + return { + restrict:'EA', + replace: true, + transclude: true, + templateUrl: 'template/datepicker/popup.html', + link:function (scope, element, attrs) { + element.bind('click', function(event) { + event.preventDefault(); + event.stopPropagation(); + }); + } + }; +}); + +angular.module('ui.bootstrap.dropdown', []) + +.constant('dropdownConfig', { + openClass: 'open' +}) + +.service('dropdownService', ['$document', function($document) { + var openScope = null; + + this.open = function( dropdownScope ) { + if ( !openScope ) { + $document.bind('click', closeDropdown); + $document.bind('keydown', escapeKeyBind); + } + + if ( openScope && openScope !== dropdownScope ) { + openScope.isOpen = false; + } + + openScope = dropdownScope; + }; + + this.close = function( dropdownScope ) { + if ( openScope === dropdownScope ) { + openScope = null; + $document.unbind('click', closeDropdown); + $document.unbind('keydown', escapeKeyBind); + } + }; + + var closeDropdown = function( evt ) { + if (evt && evt.isDefaultPrevented()) { + return; + } + + openScope.$apply(function() { + openScope.isOpen = false; + }); + }; + + var escapeKeyBind = function( evt ) { + if ( evt.which === 27 ) { + openScope.focusToggleElement(); + closeDropdown(); + } + }; +}]) + +.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate) { + var self = this, + scope = $scope.$new(), // create a child scope so we are not polluting original one + openClass = dropdownConfig.openClass, + getIsOpen, + setIsOpen = angular.noop, + toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop; + + this.init = function( element ) { + self.$element = element; + + if ( $attrs.isOpen ) { + getIsOpen = $parse($attrs.isOpen); + setIsOpen = getIsOpen.assign; + + $scope.$watch(getIsOpen, function(value) { + scope.isOpen = !!value; + }); + } + }; + + this.toggle = function( open ) { + return scope.isOpen = arguments.length ? !!open : !scope.isOpen; + }; + + // Allow other directives to watch status + this.isOpen = function() { + return scope.isOpen; + }; + + scope.focusToggleElement = function() { + if ( self.toggleElement ) { + self.toggleElement[0].focus(); + } + }; + + scope.$watch('isOpen', function( isOpen, wasOpen ) { + $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass); + + if ( isOpen ) { + scope.focusToggleElement(); + dropdownService.open( scope ); + } else { + dropdownService.close( scope ); + } + + setIsOpen($scope, isOpen); + if (angular.isDefined(isOpen) && isOpen !== wasOpen) { + toggleInvoker($scope, { open: !!isOpen }); + } + }); + + $scope.$on('$locationChangeSuccess', function() { + scope.isOpen = false; + }); + + $scope.$on('$destroy', function() { + scope.$destroy(); + }); +}]) + +.directive('dropdown', function() { + return { + restrict: 'CA', + controller: 'DropdownController', + link: function(scope, element, attrs, dropdownCtrl) { + dropdownCtrl.init( element ); + } + }; +}) + +.directive('dropdownToggle', function() { + return { + restrict: 'CA', + require: '?^dropdown', + link: function(scope, element, attrs, dropdownCtrl) { + if ( !dropdownCtrl ) { + return; + } + + dropdownCtrl.toggleElement = element; + + var toggleDropdown = function(event) { + event.preventDefault(); + + if ( !element.hasClass('disabled') && !attrs.disabled ) { + scope.$apply(function() { + dropdownCtrl.toggle(); + }); + } + }; + + element.bind('click', toggleDropdown); + + // WAI-ARIA + element.attr({ 'aria-haspopup': true, 'aria-expanded': false }); + scope.$watch(dropdownCtrl.isOpen, function( isOpen ) { + element.attr('aria-expanded', !!isOpen); + }); + + scope.$on('$destroy', function() { + element.unbind('click', toggleDropdown); + }); + } + }; +}); + +angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) + +/** + * A helper, internal data structure that acts as a map but also allows getting / removing + * elements in the LIFO order + */ + .factory('$$stackedMap', function () { + return { + createNew: function () { + var stack = []; + + return { + add: function (key, value) { + stack.push({ + key: key, + value: value + }); + }, + get: function (key) { + for (var i = 0; i < stack.length; i++) { + if (key == stack[i].key) { + return stack[i]; + } + } + }, + keys: function() { + var keys = []; + for (var i = 0; i < stack.length; i++) { + keys.push(stack[i].key); + } + return keys; + }, + top: function () { + return stack[stack.length - 1]; + }, + remove: function (key) { + var idx = -1; + for (var i = 0; i < stack.length; i++) { + if (key == stack[i].key) { + idx = i; + break; + } + } + return stack.splice(idx, 1)[0]; + }, + removeTop: function () { + return stack.splice(stack.length - 1, 1)[0]; + }, + length: function () { + return stack.length; + } + }; + } + }; + }) + +/** + * A helper directive for the $modal service. It creates a backdrop element. + */ + .directive('modalBackdrop', ['$timeout', function ($timeout) { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/modal/backdrop.html', + link: function (scope) { + + scope.animate = false; + + //trigger CSS transitions + $timeout(function () { + scope.animate = true; + }); + } + }; + }]) + + .directive('modalWindow', ['$modalStack', '$timeout', function ($modalStack, $timeout) { + return { + restrict: 'EA', + scope: { + index: '@', + animate: '=' + }, + replace: true, + transclude: true, + templateUrl: function(tElement, tAttrs) { + return tAttrs.templateUrl || 'template/modal/window.html'; + }, + link: function (scope, element, attrs) { + element.addClass(attrs.windowClass || ''); + scope.size = attrs.size; + + $timeout(function () { + // trigger CSS transitions + scope.animate = true; + // focus a freshly-opened modal + element[0].focus(); + }); + + 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'); + } + }; + } + }; + }]) + + .factory('$modalStack', ['$transition', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap', + function ($transition, $timeout, $document, $compile, $rootScope, $$stackedMap) { + + var OPENED_MODAL_CLASS = 'modal-open'; + + var backdropDomEl, backdropScope; + var openedWindows = $$stackedMap.createNew(); + var $modalStack = {}; + + function backdropIndex() { + var topBackdropIndex = -1; + var opened = openedWindows.keys(); + for (var i = 0; i < opened.length; i++) { + if (openedWindows.get(opened[i]).value.backdrop) { + topBackdropIndex = i; + } + } + return topBackdropIndex; + } + + $rootScope.$watch(backdropIndex, function(newBackdropIndex){ + if (backdropScope) { + backdropScope.index = newBackdropIndex; + } + }); + + function removeModalWindow(modalInstance) { + + var body = $document.find('body').eq(0); + var modalWindow = openedWindows.get(modalInstance).value; + + //clean up the stack + openedWindows.remove(modalInstance); + + //remove window DOM element + removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, function() { + modalWindow.modalScope.$destroy(); + body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0); + checkRemoveBackdrop(); + }); + } + + function checkRemoveBackdrop() { + //remove backdrop if no longer needed + if (backdropDomEl && backdropIndex() == -1) { + var backdropScopeRef = backdropScope; + removeAfterAnimate(backdropDomEl, backdropScope, 150, function () { + backdropScopeRef.$destroy(); + backdropScopeRef = null; + }); + backdropDomEl = undefined; + backdropScope = undefined; + } + } + + function removeAfterAnimate(domEl, scope, emulateTime, done) { + // Closing animation + scope.animate = false; + + var transitionEndEventName = $transition.transitionEndEventName; + if (transitionEndEventName) { + // transition out + var timeout = $timeout(afterAnimating, emulateTime); + + domEl.bind(transitionEndEventName, function () { + $timeout.cancel(timeout); + afterAnimating(); + scope.$apply(); + }); + } else { + // Ensure this call is async + $timeout(afterAnimating, 0); + } + + function afterAnimating() { + if (afterAnimating.done) { + return; + } + afterAnimating.done = true; + + domEl.remove(); + if (done) { + done(); + } + } + } + + $document.bind('keydown', function (evt) { + var modal; + + if (evt.which === 27) { + modal = openedWindows.top(); + if (modal && modal.value.keyboard) { + evt.preventDefault(); + $rootScope.$apply(function () { + $modalStack.dismiss(modal.key, 'escape key press'); + }); + } + } + }); + + $modalStack.open = function (modalInstance, modal) { + + openedWindows.add(modalInstance, { + deferred: modal.deferred, + modalScope: modal.scope, + backdrop: modal.backdrop, + keyboard: modal.keyboard + }); + + var body = $document.find('body').eq(0), + currBackdropIndex = backdropIndex(); + + if (currBackdropIndex >= 0 && !backdropDomEl) { + backdropScope = $rootScope.$new(true); + backdropScope.index = currBackdropIndex; + backdropDomEl = $compile('
    ')(backdropScope); + body.append(backdropDomEl); + } + + var angularDomEl = angular.element('
    '); + angularDomEl.attr({ + 'template-url': modal.windowTemplateUrl, + 'window-class': modal.windowClass, + 'size': modal.size, + 'index': openedWindows.length() - 1, + 'animate': 'animate' + }).html(modal.content); + + var modalDomEl = $compile(angularDomEl)(modal.scope); + openedWindows.top().value.modalDomEl = modalDomEl; + body.append(modalDomEl); + body.addClass(OPENED_MODAL_CLASS); + }; + + $modalStack.close = function (modalInstance, result) { + var modalWindow = openedWindows.get(modalInstance).value; + if (modalWindow) { + modalWindow.deferred.resolve(result); + removeModalWindow(modalInstance); + } + }; + + $modalStack.dismiss = function (modalInstance, reason) { + var modalWindow = openedWindows.get(modalInstance).value; + if (modalWindow) { + modalWindow.deferred.reject(reason); + removeModalWindow(modalInstance); + } + }; + + $modalStack.dismissAll = function (reason) { + var topModal = this.getTop(); + while (topModal) { + this.dismiss(topModal.key, reason); + topModal = this.getTop(); + } + }; + + $modalStack.getTop = function () { + return openedWindows.top(); + }; + + return $modalStack; + }]) + + .provider('$modal', function () { + + var $modalProvider = { + options: { + backdrop: true, //can be also false or 'static' + keyboard: true + }, + $get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack', + function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) { + + var $modal = {}; + + function getTemplatePromise(options) { + return options.template ? $q.when(options.template) : + $http.get(options.templateUrl, {cache: $templateCache}).then(function (result) { + return result.data; + }); + } + + function getResolvePromises(resolves) { + var promisesArr = []; + angular.forEach(resolves, function (value, key) { + if (angular.isFunction(value) || angular.isArray(value)) { + promisesArr.push($q.when($injector.invoke(value))); + } + }); + return promisesArr; + } + + $modal.open = function (modalOptions) { + + var modalResultDeferred = $q.defer(); + var modalOpenedDeferred = $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, + close: function (result) { + $modalStack.close(modalInstance, result); + }, + dismiss: function (reason) { + $modalStack.dismiss(modalInstance, reason); + } + }; + + //merge and clean up options + modalOptions = angular.extend({}, $modalProvider.options, modalOptions); + modalOptions.resolve = modalOptions.resolve || {}; + + //verify options + if (!modalOptions.template && !modalOptions.templateUrl) { + throw new Error('One of template or templateUrl options is required.'); + } + + var templateAndResolvePromise = + $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve))); + + + templateAndResolvePromise.then(function resolveSuccess(tplAndVars) { + + var modalScope = (modalOptions.scope || $rootScope).$new(); + modalScope.$close = modalInstance.close; + modalScope.$dismiss = modalInstance.dismiss; + + var ctrlInstance, ctrlLocals = {}; + var resolveIter = 1; + + //controllers + if (modalOptions.controller) { + ctrlLocals.$scope = modalScope; + ctrlLocals.$modalInstance = modalInstance; + angular.forEach(modalOptions.resolve, function (value, key) { + ctrlLocals[key] = tplAndVars[resolveIter++]; + }); + + ctrlInstance = $controller(modalOptions.controller, ctrlLocals); + } + + $modalStack.open(modalInstance, { + scope: modalScope, + deferred: modalResultDeferred, + content: tplAndVars[0], + backdrop: modalOptions.backdrop, + keyboard: modalOptions.keyboard, + windowClass: modalOptions.windowClass, + windowTemplateUrl: modalOptions.windowTemplateUrl, + size: modalOptions.size + }); + + }, function resolveError(reason) { + modalResultDeferred.reject(reason); + }); + + templateAndResolvePromise.then(function () { + modalOpenedDeferred.resolve(true); + }, function () { + modalOpenedDeferred.reject(false); + }); + + return modalInstance; + }; + + return $modal; + }] + }; + + return $modalProvider; + }); + +angular.module('ui.bootstrap.pagination', []) + +.controller('PaginationController', ['$scope', '$attrs', '$parse', function ($scope, $attrs, $parse) { + var self = this, + ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl + setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; + + this.init = function(ngModelCtrl_, config) { + ngModelCtrl = ngModelCtrl_; + this.config = config; + + ngModelCtrl.$render = function() { + self.render(); + }; + + if ($attrs.itemsPerPage) { + $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) { + self.itemsPerPage = parseInt(value, 10); + $scope.totalPages = self.calculateTotalPages(); + }); + } else { + this.itemsPerPage = config.itemsPerPage; + } + }; + + this.calculateTotalPages = function() { + var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage); + return Math.max(totalPages || 0, 1); + }; + + this.render = function() { + $scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1; + }; + + $scope.selectPage = function(page) { + if ( $scope.page !== page && page > 0 && page <= $scope.totalPages) { + ngModelCtrl.$setViewValue(page); + ngModelCtrl.$render(); + } + }; + + $scope.getText = function( key ) { + return $scope[key + 'Text'] || self.config[key + 'Text']; + }; + $scope.noPrevious = function() { + return $scope.page === 1; + }; + $scope.noNext = function() { + return $scope.page === $scope.totalPages; + }; + + $scope.$watch('totalItems', function() { + $scope.totalPages = self.calculateTotalPages(); + }); + + $scope.$watch('totalPages', function(value) { + setNumPages($scope.$parent, value); // Readonly variable + + if ( $scope.page > value ) { + $scope.selectPage(value); + } else { + ngModelCtrl.$render(); + } + }); +}]) + +.constant('paginationConfig', { + itemsPerPage: 10, + boundaryLinks: false, + directionLinks: true, + firstText: 'First', + previousText: 'Previous', + nextText: 'Next', + lastText: 'Last', + rotate: true +}) + +.directive('pagination', ['$parse', 'paginationConfig', function($parse, paginationConfig) { + return { + restrict: 'EA', + scope: { + totalItems: '=', + firstText: '@', + previousText: '@', + nextText: '@', + lastText: '@' + }, + require: ['pagination', '?ngModel'], + controller: 'PaginationController', + templateUrl: 'template/pagination/pagination.html', + replace: true, + link: function(scope, element, attrs, ctrls) { + var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if (!ngModelCtrl) { + return; // do nothing if no ng-model + } + + // Setup configuration parameters + var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize, + rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate; + scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks; + scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks; + + paginationCtrl.init(ngModelCtrl, paginationConfig); + + if (attrs.maxSize) { + scope.$parent.$watch($parse(attrs.maxSize), function(value) { + maxSize = parseInt(value, 10); + paginationCtrl.render(); + }); + } + + // Create page object used in template + function makePage(number, text, isActive) { + return { + number: number, + text: text, + active: isActive + }; + } + + function getPages(currentPage, totalPages) { + var pages = []; + + // Default page limits + var startPage = 1, endPage = totalPages; + var isMaxSized = ( angular.isDefined(maxSize) && maxSize < totalPages ); + + // recompute if maxSize + if ( isMaxSized ) { + if ( rotate ) { + // Current page is displayed in the middle of the visible ones + startPage = Math.max(currentPage - Math.floor(maxSize/2), 1); + endPage = startPage + maxSize - 1; + + // Adjust if limit is exceeded + if (endPage > totalPages) { + endPage = totalPages; + startPage = endPage - maxSize + 1; + } + } else { + // Visible pages are paginated with maxSize + startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1; + + // Adjust last page if limit is exceeded + endPage = Math.min(startPage + maxSize - 1, totalPages); + } + } + + // Add page number links + for (var number = startPage; number <= endPage; number++) { + var page = makePage(number, number, number === currentPage); + pages.push(page); + } + + // Add links to move between page sets + if ( isMaxSized && ! rotate ) { + if ( startPage > 1 ) { + var previousPageSet = makePage(startPage - 1, '...', false); + pages.unshift(previousPageSet); + } + + if ( endPage < totalPages ) { + var nextPageSet = makePage(endPage + 1, '...', false); + pages.push(nextPageSet); + } + } + + return pages; + } + + var originalRender = paginationCtrl.render; + paginationCtrl.render = function() { + originalRender(); + if (scope.page > 0 && scope.page <= scope.totalPages) { + scope.pages = getPages(scope.page, scope.totalPages); + } + }; + } + }; +}]) + +.constant('pagerConfig', { + itemsPerPage: 10, + previousText: '« Previous', + nextText: 'Next »', + align: true +}) + +.directive('pager', ['pagerConfig', function(pagerConfig) { + return { + restrict: 'EA', + scope: { + totalItems: '=', + previousText: '@', + nextText: '@' + }, + require: ['pager', '?ngModel'], + controller: 'PaginationController', + templateUrl: 'template/pagination/pager.html', + replace: true, + link: function(scope, element, attrs, ctrls) { + var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if (!ngModelCtrl) { + return; // do nothing if no ng-model + } + + scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align; + paginationCtrl.init(ngModelCtrl, pagerConfig); + } + }; +}]); + +/** + * The following features are still outstanding: animation as a + * function, placement as a function, inside, support for more triggers than + * just mouse enter/leave, html tooltips, and selector delegation. + */ +angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap.bindHtml' ] ) + +/** + * The $tooltip service creates tooltip- and popover-like directives as well as + * houses global options for them. + */ +.provider( '$tooltip', function () { + // The default options tooltip and popover. + var defaultOptions = { + placement: 'top', + animation: true, + popupDelay: 0 + }; + + // Default hide triggers for each show trigger + var triggerMap = { + 'mouseenter': 'mouseleave', + 'click': 'click', + 'focus': 'blur' + }; + + // 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', '$parse', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $parse, $document, $position, $interpolate ) { + return function $tooltip ( type, prefix, defaultTriggerShow ) { + var options = angular.extend( {}, defaultOptions, globalOptions ); + + /** + * 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; + var hide = triggerMap[show] || show; + return { + show: show, + hide: hide + }; + } + + var directiveName = snake_case( type ); + + var startSym = $interpolate.startSymbol(); + var endSym = $interpolate.endSymbol(); + var template = + '
    '+ + '
    '; + + return { + restrict: 'EA', + scope: true, + compile: function (tElem, tAttrs) { + var tooltipLinker = $compile( template ); + + return function link ( scope, element, attrs ) { + var tooltip; + var transitionTimeout; + var popupTimeout; + var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; + var triggers = getTriggers( undefined ); + var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']); + + var positionTooltip = function () { + + var ttPosition = $position.positionElements(element, tooltip, scope.tt_placement, appendToBody); + ttPosition.top += 'px'; + ttPosition.left += 'px'; + + // Now set the calculated positioning. + tooltip.css( ttPosition ); + }; + + // By default, the tooltip is not open. + // TODO add ability to start tooltip opened + scope.tt_isOpen = false; + + function toggleTooltipBind () { + if ( ! scope.tt_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; + } + if ( scope.tt_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 (!popupTimeout) { + popupTimeout = $timeout( show, scope.tt_popupDelay, false ); + popupTimeout.then(function(reposition){reposition();}); + } + } else { + show()(); + } + } + + function hideTooltipBind () { + scope.$apply(function () { + hide(); + }); + } + + // Show the tooltip popup element. + function show() { + + popupTimeout = null; + + // If there is a pending remove transition, we must cancel it, lest the + // tooltip be mysteriously removed. + if ( transitionTimeout ) { + $timeout.cancel( transitionTimeout ); + transitionTimeout = null; + } + + // Don't show empty tooltips. + if ( ! scope.tt_content ) { + return angular.noop; + } + + createTooltip(); + + // Set the initial positioning. + tooltip.css({ top: 0, left: 0, display: 'block' }); + + // Now we add it to the DOM because need some info about it. But it's not + // visible yet anyway. + if ( appendToBody ) { + $document.find( 'body' ).append( tooltip ); + } else { + element.after( tooltip ); + } + + positionTooltip(); + + // And show the tooltip. + scope.tt_isOpen = true; + scope.$digest(); // digest required as $apply is not called + + // Return positioning function as promise callback for correct + // positioning after draw. + return positionTooltip; + } + + // Hide the tooltip popup element. + function hide() { + // First things first: we don't show it anymore. + scope.tt_isOpen = false; + + //if tooltip is going to be shown after delay, we must cancel this + $timeout.cancel( popupTimeout ); + popupTimeout = null; + + // 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. + if ( scope.tt_animation ) { + if (!transitionTimeout) { + transitionTimeout = $timeout(removeTooltip, 500); + } + } else { + removeTooltip(); + } + } + + function createTooltip() { + // There can only be one tooltip element per directive shown at once. + if (tooltip) { + removeTooltip(); + } + tooltip = tooltipLinker(scope, function () {}); + + // Get contents rendered into the tooltip + scope.$digest(); + } + + function removeTooltip() { + transitionTimeout = null; + if (tooltip) { + tooltip.remove(); + tooltip = null; + } + } + + /** + * Observe the relevant attributes. + */ + attrs.$observe( type, function ( val ) { + scope.tt_content = val; + + if (!val && scope.tt_isOpen ) { + hide(); + } + }); + + attrs.$observe( prefix+'Title', function ( val ) { + scope.tt_title = val; + }); + + attrs.$observe( prefix+'Placement', function ( val ) { + scope.tt_placement = angular.isDefined( val ) ? val : options.placement; + }); + + attrs.$observe( prefix+'PopupDelay', function ( val ) { + var delay = parseInt( val, 10 ); + scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay; + }); + + var unregisterTriggers = function () { + element.unbind(triggers.show, showTooltipBind); + element.unbind(triggers.hide, hideTooltipBind); + }; + + attrs.$observe( prefix+'Trigger', function ( val ) { + unregisterTriggers(); + + triggers = getTriggers( val ); + + if ( triggers.show === triggers.hide ) { + element.bind( triggers.show, toggleTooltipBind ); + } else { + element.bind( triggers.show, showTooltipBind ); + element.bind( triggers.hide, hideTooltipBind ); + } + }); + + var animation = scope.$eval(attrs[prefix + 'Animation']); + scope.tt_animation = angular.isDefined(animation) ? !!animation : options.animation; + + attrs.$observe( prefix+'AppendToBody', function ( val ) { + appendToBody = angular.isDefined( val ) ? $parse( val )( scope ) : appendToBody; + }); + + // if a tooltip is attached to we need to remove it on + // location change as its parent scope will probably not be destroyed + // by the change. + if ( appendToBody ) { + scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () { + if ( scope.tt_isOpen ) { + hide(); + } + }); + } + + // Make sure tooltip is destroyed and removed. + scope.$on('$destroy', function onDestroyTooltip() { + $timeout.cancel( transitionTimeout ); + $timeout.cancel( popupTimeout ); + unregisterTriggers(); + removeTooltip(); + }); + }; + } + }; + }; + }]; +}) + +.directive( 'tooltipPopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/tooltip/tooltip-popup.html' + }; +}) + +.directive( 'tooltip', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); +}]) + +.directive( 'tooltipHtmlUnsafePopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html' + }; +}) + +.directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' ); +}]); + +/** + * 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, html popovers, and selector delegatation. + */ +angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] ) + +.directive( 'popoverPopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/popover/popover.html' + }; +}) + +.directive( 'popover', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'popover', 'popover', 'click' ); +}]); + +angular.module('ui.bootstrap.progressbar', []) + +.constant('progressConfig', { + animate: true, + max: 100 +}) + +.controller('ProgressController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) { + var self = this, + animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate; + + this.bars = []; + $scope.max = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : progressConfig.max; + + this.addBar = function(bar, element) { + if ( !animate ) { + element.css({'transition': 'none'}); + } + + this.bars.push(bar); + + bar.$watch('value', function( value ) { + bar.percent = +(100 * value / $scope.max).toFixed(2); + }); + + bar.$on('$destroy', function() { + element = null; + self.removeBar(bar); + }); + }; + + this.removeBar = function(bar) { + this.bars.splice(this.bars.indexOf(bar), 1); + }; +}]) + +.directive('progress', function() { + return { + restrict: 'EA', + replace: true, + transclude: true, + controller: 'ProgressController', + require: 'progress', + scope: {}, + templateUrl: 'template/progressbar/progress.html' + }; +}) + +.directive('bar', function() { + return { + restrict: 'EA', + replace: true, + transclude: true, + require: '^progress', + scope: { + value: '=', + type: '@' + }, + templateUrl: 'template/progressbar/bar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, element); + } + }; +}) + +.directive('progressbar', function() { + return { + restrict: 'EA', + replace: true, + transclude: true, + controller: 'ProgressController', + scope: { + value: '=', + type: '@' + }, + templateUrl: 'template/progressbar/progressbar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, angular.element(element.children()[0])); + } + }; +}); +angular.module('ui.bootstrap.rating', []) + +.constant('ratingConfig', { + max: 5, + stateOn: null, + stateOff: null +}) + +.controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) { + var ngModelCtrl = { $setViewValue: angular.noop }; + + this.init = function(ngModelCtrl_) { + ngModelCtrl = ngModelCtrl_; + ngModelCtrl.$render = this.render; + + this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn; + this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff; + + var 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 }, states[i]); + } + return states; + }; + + $scope.rate = function(value) { + if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) { + ngModelCtrl.$setViewValue(value); + ngModelCtrl.$render(); + } + }; + + $scope.enter = function(value) { + if ( !$scope.readonly ) { + $scope.value = value; + } + $scope.onHover({value: value}); + }; + + $scope.reset = function() { + $scope.value = ngModelCtrl.$viewValue; + $scope.onLeave(); + }; + + $scope.onKeydown = function(evt) { + if (/(37|38|39|40)/.test(evt.which)) { + evt.preventDefault(); + evt.stopPropagation(); + $scope.rate( $scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1) ); + } + }; + + this.render = function() { + $scope.value = ngModelCtrl.$viewValue; + }; +}]) + +.directive('rating', function() { + return { + restrict: 'EA', + require: ['rating', 'ngModel'], + scope: { + readonly: '=?', + onHover: '&', + onLeave: '&' + }, + controller: 'RatingController', + templateUrl: 'template/rating/rating.html', + replace: true, + link: function(scope, element, attrs, ctrls) { + var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if ( ngModelCtrl ) { + ratingCtrl.init( ngModelCtrl ); + } + } + }; +}); + +/** + * @ngdoc overview + * @name ui.bootstrap.tabs + * + * @description + * AngularJS version of the tabs directive. + */ + +angular.module('ui.bootstrap.tabs', []) + +.controller('TabsetController', ['$scope', function TabsetCtrl($scope) { + var ctrl = this, + tabs = ctrl.tabs = $scope.tabs = []; + + ctrl.select = function(selectedTab) { + angular.forEach(tabs, function(tab) { + if (tab.active && tab !== selectedTab) { + tab.active = false; + tab.onDeselect(); + } + }); + selectedTab.active = true; + selectedTab.onSelect(); + }; + + ctrl.addTab = function addTab(tab) { + tabs.push(tab); + // we can't run the select function on the first tab + // since that would select it twice + if (tabs.length === 1) { + tab.active = true; + } else if (tab.active) { + ctrl.select(tab); + } + }; + + ctrl.removeTab = function removeTab(tab) { + var index = tabs.indexOf(tab); + //Select a new tab if the tab to be removed is selected + if (tab.active && tabs.length > 1) { + //If this is the last tab, select the previous tab. else, the next tab. + var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1; + ctrl.select(tabs[newActiveIndex]); + } + tabs.splice(index, 1); + }; +}]) + +/** + * @ngdoc directive + * @name ui.bootstrap.tabs.directive:tabset + * @restrict EA + * + * @description + * Tabset is the outer container for the tabs directive + * + * @param {boolean=} vertical Whether or not to use vertical styling for the tabs. + * @param {boolean=} justified Whether or not to use justified styling for the tabs. + * + * @example + + + + First Content! + Second Content! + +
    + + First Vertical Content! + Second Vertical Content! + + + First Justified Content! + Second Justified Content! + +
    +
    + */ +.directive('tabset', function() { + return { + restrict: 'EA', + transclude: true, + replace: true, + scope: { + type: '@' + }, + controller: 'TabsetController', + templateUrl: 'template/tabs/tabset.html', + link: function(scope, element, attrs) { + scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false; + scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false; + } + }; +}) + +/** + * @ngdoc directive + * @name ui.bootstrap.tabs.directive:tab + * @restrict EA + * + * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}. + * @param {string=} select An expression to evaluate when the tab is selected. + * @param {boolean=} active A binding, telling whether or not this tab is selected. + * @param {boolean=} disabled A binding, telling whether or not this tab is disabled. + * + * @description + * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}. + * + * @example + + +
    + + +
    + + First Tab + + Alert me! + Second Tab, with alert callback and html heading! + + + {{item.content}} + + +
    +
    + + function TabsDemoCtrl($scope) { + $scope.items = [ + { title:"Dynamic Title 1", content:"Dynamic Item 0" }, + { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true } + ]; + + $scope.alertMe = function() { + setTimeout(function() { + alert("You've selected the alert tab!"); + }); + }; + }; + +
    + */ + +/** + * @ngdoc directive + * @name ui.bootstrap.tabs.directive:tabHeading + * @restrict EA + * + * @description + * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element. + * + * @example + + + + + HTML in my titles?! + And some content, too! + + + Icon heading?!? + That's right. + + + + + */ +.directive('tab', ['$parse', function($parse) { + return { + require: '^tabset', + restrict: 'EA', + replace: true, + templateUrl: 'template/tabs/tab.html', + transclude: true, + scope: { + active: '=?', + heading: '@', + onSelect: '&select', //This callback is called in contentHeadingTransclude + //once it inserts the tab's content into the dom + onDeselect: '&deselect' + }, + controller: function() { + //Empty controller so other directives can require being 'under' a tab + }, + compile: function(elm, attrs, transclude) { + return function postLink(scope, elm, attrs, tabsetCtrl) { + scope.$watch('active', function(active) { + if (active) { + tabsetCtrl.select(scope); + } + }); + + scope.disabled = false; + if ( attrs.disabled ) { + scope.$parent.$watch($parse(attrs.disabled), function(value) { + scope.disabled = !! value; + }); + } + + scope.select = function() { + if ( !scope.disabled ) { + scope.active = true; + } + }; + + tabsetCtrl.addTab(scope); + scope.$on('$destroy', function() { + tabsetCtrl.removeTab(scope); + }); + + //We need to transclude later, once the content container is ready. + //when this link happens, we're inside a tab heading. + scope.$transcludeFn = transclude; + }; + } + }; +}]) + +.directive('tabHeadingTransclude', [function() { + return { + restrict: 'A', + require: '^tab', + link: function(scope, elm, attrs, tabCtrl) { + scope.$watch('headingElement', function updateHeadingElement(heading) { + if (heading) { + elm.html(''); + elm.append(heading); + } + }); + } + }; +}]) + +.directive('tabContentTransclude', function() { + return { + restrict: 'A', + require: '^tabset', + link: function(scope, elm, attrs) { + var tab = scope.$eval(attrs.tabContentTransclude); + + //Now our tab is ready to be transcluded: both the tab heading area + //and the tab content area are loaded. Transclude 'em both. + tab.$transcludeFn(tab.$parent, function(contents) { + angular.forEach(contents, function(node) { + if (isTabHeading(node)) { + //Let tabHeadingTransclude know. + tab.headingElement = node; + } else { + elm.append(node); + } + }); + }); + } + }; + function isTabHeading(node) { + return node.tagName && ( + node.hasAttribute('tab-heading') || + node.hasAttribute('data-tab-heading') || + node.tagName.toLowerCase() === 'tab-heading' || + node.tagName.toLowerCase() === 'data-tab-heading' + ); + } +}) + +; + +angular.module('ui.bootstrap.timepicker', []) + +.constant('timepickerConfig', { + hourStep: 1, + minuteStep: 1, + showMeridian: true, + meridians: null, + readonlyInput: false, + mousewheel: true +}) + +.controller('TimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'timepickerConfig', function($scope, $attrs, $parse, $log, $locale, timepickerConfig) { + var selected = new Date(), + ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl + meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS; + + this.init = function( ngModelCtrl_, inputs ) { + ngModelCtrl = ngModelCtrl_; + ngModelCtrl.$render = this.render; + + var hoursInputEl = inputs.eq(0), + minutesInputEl = inputs.eq(1); + + var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel; + if ( mousewheel ) { + this.setupMousewheelEvents( hoursInputEl, minutesInputEl ); + } + + $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput; + this.setupInputEvents( hoursInputEl, minutesInputEl ); + }; + + var hourStep = timepickerConfig.hourStep; + if ($attrs.hourStep) { + $scope.$parent.$watch($parse($attrs.hourStep), function(value) { + hourStep = parseInt(value, 10); + }); + } + + var minuteStep = timepickerConfig.minuteStep; + if ($attrs.minuteStep) { + $scope.$parent.$watch($parse($attrs.minuteStep), function(value) { + minuteStep = parseInt(value, 10); + }); + } + + // 12H / 24H mode + $scope.showMeridian = timepickerConfig.showMeridian; + if ($attrs.showMeridian) { + $scope.$parent.$watch($parse($attrs.showMeridian), function(value) { + $scope.showMeridian = !!value; + + if ( ngModelCtrl.$error.time ) { + // Evaluate from template + var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate(); + if (angular.isDefined( hours ) && angular.isDefined( minutes )) { + selected.setHours( hours ); + refresh(); + } + } else { + updateTemplate(); + } + }); + } + + // Get $scope.hours in 24H mode if valid + function getHoursFromTemplate ( ) { + var hours = parseInt( $scope.hours, 10 ); + var valid = ( $scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24); + if ( !valid ) { + return undefined; + } + + if ( $scope.showMeridian ) { + if ( hours === 12 ) { + hours = 0; + } + if ( $scope.meridian === meridians[1] ) { + hours = hours + 12; + } + } + return hours; + } + + function getMinutesFromTemplate() { + var minutes = parseInt($scope.minutes, 10); + return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined; + } + + function pad( value ) { + return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value; + } + + // Respond on mousewheel spin + this.setupMousewheelEvents = function( hoursInputEl, minutesInputEl ) { + var isScrollingUp = function(e) { + if (e.originalEvent) { + e = e.originalEvent; + } + //pick correct delta variable depending on event + var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY; + return (e.detail || delta > 0); + }; + + hoursInputEl.bind('mousewheel wheel', function(e) { + $scope.$apply( (isScrollingUp(e)) ? $scope.incrementHours() : $scope.decrementHours() ); + e.preventDefault(); + }); + + minutesInputEl.bind('mousewheel wheel', function(e) { + $scope.$apply( (isScrollingUp(e)) ? $scope.incrementMinutes() : $scope.decrementMinutes() ); + e.preventDefault(); + }); + + }; + + this.setupInputEvents = function( hoursInputEl, minutesInputEl ) { + if ( $scope.readonlyInput ) { + $scope.updateHours = angular.noop; + $scope.updateMinutes = angular.noop; + return; + } + + var invalidate = function(invalidHours, invalidMinutes) { + ngModelCtrl.$setViewValue( null ); + ngModelCtrl.$setValidity('time', false); + if (angular.isDefined(invalidHours)) { + $scope.invalidHours = invalidHours; + } + if (angular.isDefined(invalidMinutes)) { + $scope.invalidMinutes = invalidMinutes; + } + }; + + $scope.updateHours = function() { + var hours = getHoursFromTemplate(); + + if ( angular.isDefined(hours) ) { + selected.setHours( hours ); + refresh( 'h' ); + } else { + invalidate(true); + } + }; + + hoursInputEl.bind('blur', function(e) { + if ( !$scope.invalidHours && $scope.hours < 10) { + $scope.$apply( function() { + $scope.hours = pad( $scope.hours ); + }); + } + }); + + $scope.updateMinutes = function() { + var minutes = getMinutesFromTemplate(); + + if ( angular.isDefined(minutes) ) { + selected.setMinutes( minutes ); + refresh( 'm' ); + } else { + invalidate(undefined, true); + } + }; + + minutesInputEl.bind('blur', function(e) { + if ( !$scope.invalidMinutes && $scope.minutes < 10 ) { + $scope.$apply( function() { + $scope.minutes = pad( $scope.minutes ); + }); + } + }); + + }; + + this.render = function() { + var date = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : null; + + 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; + } + makeValid(); + updateTemplate(); + } + }; + + // Call internally when we know that model is valid. + function refresh( keyboardChange ) { + makeValid(); + ngModelCtrl.$setViewValue( new Date(selected) ); + updateTemplate( keyboardChange ); + } + + function makeValid() { + ngModelCtrl.$setValidity('time', true); + $scope.invalidHours = false; + $scope.invalidMinutes = false; + } + + function updateTemplate( keyboardChange ) { + var hours = selected.getHours(), minutes = selected.getMinutes(); + + if ( $scope.showMeridian ) { + hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system + } + + $scope.hours = keyboardChange === 'h' ? hours : pad(hours); + $scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes); + $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; + } + + function addMinutes( minutes ) { + var dt = new Date( selected.getTime() + minutes * 60000 ); + selected.setHours( dt.getHours(), dt.getMinutes() ); + refresh(); + } + + $scope.incrementHours = function() { + addMinutes( hourStep * 60 ); + }; + $scope.decrementHours = function() { + addMinutes( - hourStep * 60 ); + }; + $scope.incrementMinutes = function() { + addMinutes( minuteStep ); + }; + $scope.decrementMinutes = function() { + addMinutes( - minuteStep ); + }; + $scope.toggleMeridian = function() { + addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) ); + }; +}]) + +.directive('timepicker', function () { + return { + restrict: 'EA', + require: ['timepicker', '?^ngModel'], + controller:'TimepickerController', + replace: true, + scope: {}, + templateUrl: 'template/timepicker/timepicker.html', + link: function(scope, element, attrs, ctrls) { + var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if ( ngModelCtrl ) { + timepickerCtrl.init( ngModelCtrl, element.find('input') ); + } + } + }; +}); + +angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml']) + +/** + * A helper service that can parse typeahead's syntax (string provided by users) + * Extracted to a separate service for ease of unit testing + */ + .factory('typeaheadParser', ['$parse', function ($parse) { + + // 00000111000000000000022200000000000000003333333333333330000000000044000 + var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\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]) + }; + } + }; +}]) + + .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser', + function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) { + + var HOT_KEYS = [9, 13, 27, 38, 40]; + + return { + require:'ngModel', + link:function (originalScope, element, attrs, modelCtrl) { + + //SUPPORTED ATTRIBUTES (OPTIONS) + + //minimal no of characters that needs to be entered before typeahead kicks-in + var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; + + //minimal wait time after last character typed before typehead 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; + + //binding to a variable that indicates if matches are being retrieved asynchronously + var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; + + //a callback executed when a match is selected + var onSelectCallback = $parse(attrs.typeaheadOnSelect); + + var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; + + var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; + + //INTERNAL VARIABLES + + //model setter executed upon match selection + var $setModelValue = $parse(attrs.ngModel).assign; + + //expressions used by typeahead + var parserResult = typeaheadParser.parse(attrs.typeahead); + + var hasFocus; + + //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(); + originalScope.$on('$destroy', function(){ + scope.$destroy(); + }); + + // WAI-ARIA + var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); + element.attr({ + 'aria-autocomplete': 'list', + 'aria-expanded': false, + 'aria-owns': popupId + }); + + //pop-up element used to display matches + var popUpEl = angular.element('
    '); + popUpEl.attr({ + id: popupId, + matches: 'matches', + active: 'activeIdx', + select: 'select(activeIdx)', + query: 'query', + position: 'position' + }); + //custom item template + if (angular.isDefined(attrs.typeaheadTemplateUrl)) { + popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); + } + + var resetMatches = function() { + scope.matches = []; + scope.activeIdx = -1; + element.attr('aria-expanded', false); + }; + + 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 getMatchesAsync = function(inputValue) { + + var locals = {$viewValue: inputValue}; + isLoadingSetter(originalScope, true); + $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.length > 0) { + + scope.activeIdx = 0; + scope.matches.length = 0; + + //transform labels + for(var i=0; i= minSearch) { + if (waitTime > 0) { + if (timeoutPromise) { + $timeout.cancel(timeoutPromise);//cancel previous timeout + } + timeoutPromise = $timeout(function () { + getMatchesAsync(inputValue); + }, waitTime); + } else { + getMatchesAsync(inputValue); + } + } else { + isLoadingSetter(originalScope, false); + resetMatches(); + } + + if (isEditable) { + return inputValue; + } else { + if (!inputValue) { + // Reset in case user had typed something previously. + modelCtrl.$setValidity('editable', true); + return inputValue; + } else { + modelCtrl.$setValidity('editable', false); + return undefined; + } + } + }); + + modelCtrl.$formatters.push(function (modelValue) { + + var candidateViewValue, emptyViewValue; + var locals = {}; + + if (inputFormatter) { + + locals['$model'] = modelValue; + return inputFormatter(originalScope, locals); + + } else { + + //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; + } + }); + + scope.select = function (activeIdx) { + //called from within the $digest() cycle + var locals = {}; + var model, item; + + locals[parserResult.itemName] = item = scope.matches[activeIdx].model; + model = parserResult.modelMapper(originalScope, locals); + $setModelValue(originalScope, model); + modelCtrl.$setValidity('editable', true); + + onSelectCallback(originalScope, { + $item: item, + $model: model, + $label: parserResult.viewMapper(originalScope, locals) + }); + + resetMatches(); + + //return focus to the input element if a match was selected via a mouse click event + // use timeout to avoid $rootScope:inprog error + $timeout(function() { element[0].focus(); }, 0, false); + }; + + //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) + element.bind('keydown', function (evt) { + + //typeahead is open and an "interesting" key was pressed + if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { + return; + } + + evt.preventDefault(); + + if (evt.which === 40) { + scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; + scope.$digest(); + + } else if (evt.which === 38) { + scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1; + scope.$digest(); + + } else if (evt.which === 13 || evt.which === 9) { + scope.$apply(function () { + scope.select(scope.activeIdx); + }); + + } else if (evt.which === 27) { + evt.stopPropagation(); + + resetMatches(); + scope.$digest(); + } + }); + + element.bind('blur', function (evt) { + hasFocus = false; + }); + + // Keep reference to click handler to unbind it. + var dismissClickHandler = function (evt) { + if (element[0] !== evt.target) { + resetMatches(); + scope.$digest(); + } + }; + + $document.bind('click', dismissClickHandler); + + originalScope.$on('$destroy', function(){ + $document.unbind('click', dismissClickHandler); + }); + + var $popup = $compile(popUpEl)(scope); + if ( appendToBody ) { + $document.find('body').append($popup); + } else { + element.after($popup); + } + } + }; + +}]) + + .directive('typeaheadPopup', function () { + return { + restrict:'EA', + scope:{ + matches:'=', + query:'=', + active:'=', + position:'=', + select:'&' + }, + replace:true, + templateUrl:'template/typeahead/typeahead-popup.html', + link:function (scope, element, attrs) { + + scope.templateUrl = attrs.templateUrl; + + scope.isOpen = function () { + return scope.matches.length > 0; + }; + + scope.isActive = function (matchIdx) { + return scope.active == matchIdx; + }; + + scope.selectActive = function (matchIdx) { + scope.active = matchIdx; + }; + + scope.selectMatch = function (activeIdx) { + scope.select({activeIdx:activeIdx}); + }; + } + }; + }) + + .directive('typeaheadMatch', ['$http', '$templateCache', '$compile', '$parse', function ($http, $templateCache, $compile, $parse) { + return { + restrict:'EA', + scope:{ + index:'=', + match:'=', + query:'=' + }, + link:function (scope, element, attrs) { + var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html'; + $http.get(tplUrl, {cache: $templateCache}).success(function(tplContent){ + element.replaceWith($compile(tplContent.trim())(scope)); + }); + } + }; + }]) + + .filter('typeaheadHighlight', function() { + + function escapeRegexp(queryToEscape) { + return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); + } + + return function(matchItem, query) { + return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; + }; + }); diff --git a/app/bower_components/angular-bootstrap/ui-bootstrap.min.js b/app/bower_components/angular-bootstrap/ui-bootstrap.min.js new file mode 100644 index 0000000..53fd24e --- /dev/null +++ b/app/bower_components/angular-bootstrap/ui-bootstrap.min.js @@ -0,0 +1,9 @@ +/* + * angular-ui-bootstrap + * http://angular-ui.github.io/bootstrap/ + + * Version: 0.11.0 - 2014-05-01 + * License: MIT + */ +angular.module("ui.bootstrap",["ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","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.transition",[]).factory("$transition",["$q","$timeout","$rootScope",function(a,b,c){function d(a){for(var b in a)if(void 0!==f.style[b])return a[b]}var e=function(d,f,g){g=g||{};var h=a.defer(),i=e[g.animation?"animationEndEventName":"transitionEndEventName"],j=function(){c.$apply(function(){d.unbind(i,j),h.resolve(d)})};return i&&d.bind(i,j),b(function(){angular.isString(f)?d.addClass(f):angular.isFunction(f)?f(d):angular.isObject(f)&&d.css(f),i||h.resolve(d)}),h.promise.cancel=function(){i&&d.unbind(i,j),h.reject("Transition cancelled")},h.promise},f=document.createElement("trans"),g={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",transition:"transitionend"},h={WebkitTransition:"webkitAnimationEnd",MozTransition:"animationend",OTransition:"oAnimationEnd",transition:"animationend"};return e.transitionEndEventName=d(g),e.animationEndEventName=d(h),e}]),angular.module("ui.bootstrap.collapse",["ui.bootstrap.transition"]).directive("collapse",["$transition",function(a){return{link:function(b,c,d){function e(b){function d(){j===e&&(j=void 0)}var e=a(c,b);return j&&j.cancel(),j=e,e.then(d,d),e}function f(){k?(k=!1,g()):(c.removeClass("collapse").addClass("collapsing"),e({height:c[0].scrollHeight+"px"}).then(g))}function g(){c.removeClass("collapsing"),c.addClass("collapse in"),c.css({height:"auto"})}function h(){if(k)k=!1,i(),c.css({height:0});else{c.css({height:c[0].scrollHeight+"px"});{c[0].offsetWidth}c.removeClass("collapse in").addClass("collapsing"),e({height:0}).then(i)}}function i(){c.removeClass("collapsing"),c.addClass("collapse")}var j,k=!0;b.$watch(d.collapse,function(a){a?h():f()})}}}]),angular.module("ui.bootstrap.accordion",["ui.bootstrap.collapse"]).constant("accordionConfig",{closeOthers:!0}).controller("AccordionController",["$scope","$attrs","accordionConfig",function(a,b,c){this.groups=[],this.closeOthers=function(d){var e=angular.isDefined(b.closeOthers)?a.$eval(b.closeOthers):c.closeOthers;e&&angular.forEach(this.groups,function(a){a!==d&&(a.isOpen=!1)})},this.addGroup=function(a){var b=this;this.groups.push(a),a.$on("$destroy",function(){b.removeGroup(a)})},this.removeGroup=function(a){var b=this.groups.indexOf(a);-1!==b&&this.groups.splice(b,1)}}]).directive("accordion",function(){return{restrict:"EA",controller:"AccordionController",transclude:!0,replace:!1,templateUrl:"template/accordion/accordion.html"}}).directive("accordionGroup",function(){return{require:"^accordion",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/accordion/accordion-group.html",scope:{heading:"@",isOpen:"=?",isDisabled:"=?"},controller:function(){this.setHeading=function(a){this.heading=a}},link:function(a,b,c,d){d.addGroup(a),a.$watch("isOpen",function(b){b&&d.closeOthers(a)}),a.toggleOpen=function(){a.isDisabled||(a.isOpen=!a.isOpen)}}}}).directive("accordionHeading",function(){return{restrict:"EA",transclude:!0,template:"",replace:!0,require:"^accordionGroup",link:function(a,b,c,d,e){d.setHeading(e(a,function(){}))}}}).directive("accordionTransclude",function(){return{require:"^accordionGroup",link:function(a,b,c,d){a.$watch(function(){return d[c.accordionTransclude]},function(a){a&&(b.html(""),b.append(a))})}}}),angular.module("ui.bootstrap.alert",[]).controller("AlertController",["$scope","$attrs",function(a,b){a.closeable="close"in b}]).directive("alert",function(){return{restrict:"EA",controller:"AlertController",templateUrl:"template/alert/alert.html",transclude:!0,replace:!0,scope:{type:"@",close:"&"}}}),angular.module("ui.bootstrap.bindHtml",[]).directive("bindHtmlUnsafe",function(){return function(a,b,c){b.addClass("ng-binding").data("$binding",c.bindHtmlUnsafe),a.$watch(c.bindHtmlUnsafe,function(a){b.html(a||"")})}}),angular.module("ui.bootstrap.buttons",[]).constant("buttonConfig",{activeClass:"active",toggleEvent:"click"}).controller("ButtonsController",["buttonConfig",function(a){this.activeClass=a.activeClass||"active",this.toggleEvent=a.toggleEvent||"click"}]).directive("btnRadio",function(){return{require:["btnRadio","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){var e=d[0],f=d[1];f.$render=function(){b.toggleClass(e.activeClass,angular.equals(f.$modelValue,a.$eval(c.btnRadio)))},b.bind(e.toggleEvent,function(){var d=b.hasClass(e.activeClass);(!d||angular.isDefined(c.uncheckable))&&a.$apply(function(){f.$setViewValue(d?null:a.$eval(c.btnRadio)),f.$render()})})}}}).directive("btnCheckbox",function(){return{require:["btnCheckbox","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){function e(){return g(c.btnCheckboxTrue,!0)}function f(){return g(c.btnCheckboxFalse,!1)}function g(b,c){var d=a.$eval(b);return angular.isDefined(d)?d:c}var h=d[0],i=d[1];i.$render=function(){b.toggleClass(h.activeClass,angular.equals(i.$modelValue,e()))},b.bind(h.toggleEvent,function(){a.$apply(function(){i.$setViewValue(b.hasClass(h.activeClass)?f():e()),i.$render()})})}}}),angular.module("ui.bootstrap.carousel",["ui.bootstrap.transition"]).controller("CarouselController",["$scope","$timeout","$transition",function(a,b,c){function d(){e();var c=+a.interval;!isNaN(c)&&c>=0&&(g=b(f,c))}function e(){g&&(b.cancel(g),g=null)}function f(){h?(a.next(),d()):a.pause()}var g,h,i=this,j=i.slides=a.slides=[],k=-1;i.currentSlide=null;var l=!1;i.select=a.select=function(e,f){function g(){if(!l){if(i.currentSlide&&angular.isString(f)&&!a.noTransition&&e.$element){e.$element.addClass(f);{e.$element[0].offsetWidth}angular.forEach(j,function(a){angular.extend(a,{direction:"",entering:!1,leaving:!1,active:!1})}),angular.extend(e,{direction:f,active:!0,entering:!0}),angular.extend(i.currentSlide||{},{direction:f,leaving:!0}),a.$currentTransition=c(e.$element,{}),function(b,c){a.$currentTransition.then(function(){h(b,c)},function(){h(b,c)})}(e,i.currentSlide)}else h(e,i.currentSlide);i.currentSlide=e,k=m,d()}}function h(b,c){angular.extend(b,{direction:"",active:!0,leaving:!1,entering:!1}),angular.extend(c||{},{direction:"",active:!1,leaving:!1,entering:!1}),a.$currentTransition=null}var m=j.indexOf(e);void 0===f&&(f=m>k?"next":"prev"),e&&e!==i.currentSlide&&(a.$currentTransition?(a.$currentTransition.cancel(),b(g)):g())},a.$on("$destroy",function(){l=!0}),i.indexOfSlide=function(a){return j.indexOf(a)},a.next=function(){var b=(k+1)%j.length;return a.$currentTransition?void 0:i.select(j[b],"next")},a.prev=function(){var b=0>k-1?j.length-1:k-1;return a.$currentTransition?void 0:i.select(j[b],"prev")},a.isActive=function(a){return i.currentSlide===a},a.$watch("interval",d),a.$on("$destroy",e),a.play=function(){h||(h=!0,d())},a.pause=function(){a.noPause||(h=!1,e())},i.addSlide=function(b,c){b.$element=c,j.push(b),1===j.length||b.active?(i.select(j[j.length-1]),1==j.length&&a.play()):b.active=!1},i.removeSlide=function(a){var b=j.indexOf(a);j.splice(b,1),j.length>0&&a.active?i.select(b>=j.length?j[b-1]:j[b]):k>b&&k--}}]).directive("carousel",[function(){return{restrict:"EA",transclude:!0,replace:!0,controller:"CarouselController",require:"carousel",templateUrl:"template/carousel/carousel.html",scope:{interval:"=",noTransition:"=",noPause:"="}}}]).directive("slide",function(){return{require:"^carousel",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/carousel/slide.html",scope:{active:"=?"},link:function(a,b,c,d){d.addSlide(a,b),a.$on("$destroy",function(){d.removeSlide(a)}),a.$watch("active",function(b){b&&d.select(a)})}}}),angular.module("ui.bootstrap.dateparser",[]).service("dateParser",["$locale","orderByFilter",function(a,b){function c(a,b,c){return 1===b&&c>28?29===c&&(a%4===0&&a%100!==0||a%400===0):3===b||5===b||8===b||10===b?31>c:!0}this.parsers={};var d={yyyy:{regex:"\\d{4}",apply:function(a){this.year=+a}},yy:{regex:"\\d{2}",apply:function(a){this.year=+a+2e3}},y:{regex:"\\d{1,4}",apply:function(a){this.year=+a}},MMMM:{regex:a.DATETIME_FORMATS.MONTH.join("|"),apply:function(b){this.month=a.DATETIME_FORMATS.MONTH.indexOf(b)}},MMM:{regex:a.DATETIME_FORMATS.SHORTMONTH.join("|"),apply:function(b){this.month=a.DATETIME_FORMATS.SHORTMONTH.indexOf(b)}},MM:{regex:"0[1-9]|1[0-2]",apply:function(a){this.month=a-1}},M:{regex:"[1-9]|1[0-2]",apply:function(a){this.month=a-1}},dd:{regex:"[0-2][0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a}},d:{regex:"[1-2]?[0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a}},EEEE:{regex:a.DATETIME_FORMATS.DAY.join("|")},EEE:{regex:a.DATETIME_FORMATS.SHORTDAY.join("|")}};this.createParser=function(a){var c=[],e=a.split("");return angular.forEach(d,function(b,d){var f=a.indexOf(d);if(f>-1){a=a.split(""),e[f]="("+b.regex+")",a[f]="$";for(var g=f+1,h=f+d.length;h>g;g++)e[g]="",a[g]="$";a=a.join(""),c.push({index:f,apply:b.apply})}}),{regex:new RegExp("^"+e.join("")+"$"),map:b(c,"index")}},this.parse=function(b,d){if(!angular.isString(b))return b;d=a.DATETIME_FORMATS[d]||d,this.parsers[d]||(this.parsers[d]=this.createParser(d));var e=this.parsers[d],f=e.regex,g=e.map,h=b.match(f);if(h&&h.length){for(var i,j={year:1900,month:0,date:1,hours:0},k=1,l=h.length;l>k;k++){var m=g[k-1];m.apply&&m.apply.call(j,h[k])}return c(j.year,j.month,j.date)&&(i=new Date(j.year,j.month,j.date,j.hours)),i}}}]),angular.module("ui.bootstrap.position",[]).factory("$position",["$document","$window",function(a,b){function c(a,c){return a.currentStyle?a.currentStyle[c]:b.getComputedStyle?b.getComputedStyle(a)[c]:a.style[c]}function d(a){return"static"===(c(a,"position")||"static")}var e=function(b){for(var c=a[0],e=b.offsetParent||c;e&&e!==c&&d(e);)e=e.offsetParent;return e||c};return{position:function(b){var c=this.offset(b),d={top:0,left:0},f=e(b[0]);f!=a[0]&&(d=this.offset(angular.element(f)),d.top+=f.clientTop-f.scrollTop,d.left+=f.clientLeft-f.scrollLeft);var g=b[0].getBoundingClientRect();return{width:g.width||b.prop("offsetWidth"),height:g.height||b.prop("offsetHeight"),top:c.top-d.top,left:c.left-d.left}},offset:function(c){var d=c[0].getBoundingClientRect();return{width:d.width||c.prop("offsetWidth"),height:d.height||c.prop("offsetHeight"),top:d.top+(b.pageYOffset||a[0].documentElement.scrollTop),left:d.left+(b.pageXOffset||a[0].documentElement.scrollLeft)}},positionElements:function(a,b,c,d){var e,f,g,h,i=c.split("-"),j=i[0],k=i[1]||"center";e=d?this.offset(a):this.position(a),f=b.prop("offsetWidth"),g=b.prop("offsetHeight");var l={center:function(){return e.left+e.width/2-f/2},left:function(){return e.left},right:function(){return e.left+e.width}},m={center:function(){return e.top+e.height/2-g/2},top:function(){return e.top},bottom:function(){return e.top+e.height}};switch(j){case"right":h={top:m[k](),left:l[j]()};break;case"left":h={top:m[k](),left:e.left-f};break;case"bottom":h={top:m[j](),left:l[k]()};break;default:h={top:e.top-g,left:l[k]()}}return h}}}]),angular.module("ui.bootstrap.datepicker",["ui.bootstrap.dateparser","ui.bootstrap.position"]).constant("datepickerConfig",{formatDay:"dd",formatMonth:"MMMM",formatYear:"yyyy",formatDayHeader:"EEE",formatDayTitle:"MMMM yyyy",formatMonthTitle:"yyyy",datepickerMode:"day",minMode:"day",maxMode:"year",showWeeks:!0,startingDay:0,yearRange:20,minDate:null,maxDate:null}).controller("DatepickerController",["$scope","$attrs","$parse","$interpolate","$timeout","$log","dateFilter","datepickerConfig",function(a,b,c,d,e,f,g,h){var i=this,j={$setViewValue:angular.noop};this.modes=["day","month","year"],angular.forEach(["formatDay","formatMonth","formatYear","formatDayHeader","formatDayTitle","formatMonthTitle","minMode","maxMode","showWeeks","startingDay","yearRange"],function(c,e){i[c]=angular.isDefined(b[c])?8>e?d(b[c])(a.$parent):a.$parent.$eval(b[c]):h[c]}),angular.forEach(["minDate","maxDate"],function(d){b[d]?a.$parent.$watch(c(b[d]),function(a){i[d]=a?new Date(a):null,i.refreshView()}):i[d]=h[d]?new Date(h[d]):null}),a.datepickerMode=a.datepickerMode||h.datepickerMode,a.uniqueId="datepicker-"+a.$id+"-"+Math.floor(1e4*Math.random()),this.activeDate=angular.isDefined(b.initDate)?a.$parent.$eval(b.initDate):new Date,a.isActive=function(b){return 0===i.compare(b.date,i.activeDate)?(a.activeDateId=b.uid,!0):!1},this.init=function(a){j=a,j.$render=function(){i.render()}},this.render=function(){if(j.$modelValue){var a=new Date(j.$modelValue),b=!isNaN(a);b?this.activeDate=a:f.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'),j.$setValidity("date",b)}this.refreshView()},this.refreshView=function(){if(this.element){this._refreshView();var a=j.$modelValue?new Date(j.$modelValue):null;j.$setValidity("date-disabled",!a||this.element&&!this.isDisabled(a))}},this.createDateObject=function(a,b){var c=j.$modelValue?new Date(j.$modelValue):null;return{date:a,label:g(a,b),selected:c&&0===this.compare(a,c),disabled:this.isDisabled(a),current:0===this.compare(a,new Date)}},this.isDisabled=function(c){return this.minDate&&this.compare(c,this.minDate)<0||this.maxDate&&this.compare(c,this.maxDate)>0||b.dateDisabled&&a.dateDisabled({date:c,mode:a.datepickerMode})},this.split=function(a,b){for(var c=[];a.length>0;)c.push(a.splice(0,b));return c},a.select=function(b){if(a.datepickerMode===i.minMode){var c=j.$modelValue?new Date(j.$modelValue):new Date(0,0,0,0,0,0,0);c.setFullYear(b.getFullYear(),b.getMonth(),b.getDate()),j.$setViewValue(c),j.$render()}else i.activeDate=b,a.datepickerMode=i.modes[i.modes.indexOf(a.datepickerMode)-1]},a.move=function(a){var b=i.activeDate.getFullYear()+a*(i.step.years||0),c=i.activeDate.getMonth()+a*(i.step.months||0);i.activeDate.setFullYear(b,c,1),i.refreshView()},a.toggleMode=function(b){b=b||1,a.datepickerMode===i.maxMode&&1===b||a.datepickerMode===i.minMode&&-1===b||(a.datepickerMode=i.modes[i.modes.indexOf(a.datepickerMode)+b])},a.keys={13:"enter",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down"};var k=function(){e(function(){i.element[0].focus()},0,!1)};a.$on("datepicker.focus",k),a.keydown=function(b){var c=a.keys[b.which];if(c&&!b.shiftKey&&!b.altKey)if(b.preventDefault(),b.stopPropagation(),"enter"===c||"space"===c){if(i.isDisabled(i.activeDate))return;a.select(i.activeDate),k()}else!b.ctrlKey||"up"!==c&&"down"!==c?(i.handleKeyDown(c,b),i.refreshView()):(a.toggleMode("up"===c?1:-1),k())}}]).directive("datepicker",function(){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/datepicker.html",scope:{datepickerMode:"=?",dateDisabled:"&"},require:["datepicker","?^ngModel"],controller:"DatepickerController",link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f)}}}).directive("daypicker",["dateFilter",function(a){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/day.html",require:"^datepicker",link:function(b,c,d,e){function f(a,b){return 1!==b||a%4!==0||a%100===0&&a%400!==0?i[b]:29}function g(a,b){var c=new Array(b),d=new Date(a),e=0;for(d.setHours(12);b>e;)c[e++]=new Date(d),d.setDate(d.getDate()+1);return c}function h(a){var b=new Date(a);b.setDate(b.getDate()+4-(b.getDay()||7));var c=b.getTime();return b.setMonth(0),b.setDate(1),Math.floor(Math.round((c-b)/864e5)/7)+1}b.showWeeks=e.showWeeks,e.step={months:1},e.element=c;var i=[31,28,31,30,31,30,31,31,30,31,30,31];e._refreshView=function(){var c=e.activeDate.getFullYear(),d=e.activeDate.getMonth(),f=new Date(c,d,1),i=e.startingDay-f.getDay(),j=i>0?7-i:-i,k=new Date(f);j>0&&k.setDate(-j+1);for(var l=g(k,42),m=0;42>m;m++)l[m]=angular.extend(e.createDateObject(l[m],e.formatDay),{secondary:l[m].getMonth()!==d,uid:b.uniqueId+"-"+m});b.labels=new Array(7);for(var n=0;7>n;n++)b.labels[n]={abbr:a(l[n].date,e.formatDayHeader),full:a(l[n].date,"EEEE")};if(b.title=a(e.activeDate,e.formatDayTitle),b.rows=e.split(l,7),b.showWeeks){b.weekNumbers=[];for(var o=h(b.rows[0][0].date),p=b.rows.length;b.weekNumbers.push(o++)f;f++)c[f]=angular.extend(e.createDateObject(new Date(d,f,1),e.formatMonth),{uid:b.uniqueId+"-"+f});b.title=a(e.activeDate,e.formatMonthTitle),b.rows=e.split(c,3)},e.compare=function(a,b){return new Date(a.getFullYear(),a.getMonth())-new Date(b.getFullYear(),b.getMonth())},e.handleKeyDown=function(a){var b=e.activeDate.getMonth();if("left"===a)b-=1;else if("up"===a)b-=3;else if("right"===a)b+=1;else if("down"===a)b+=3;else if("pageup"===a||"pagedown"===a){var c=e.activeDate.getFullYear()+("pageup"===a?-1:1);e.activeDate.setFullYear(c)}else"home"===a?b=0:"end"===a&&(b=11);e.activeDate.setMonth(b)},e.refreshView()}}}]).directive("yearpicker",["dateFilter",function(){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/year.html",require:"^datepicker",link:function(a,b,c,d){function e(a){return parseInt((a-1)/f,10)*f+1}var f=d.yearRange;d.step={years:f},d.element=b,d._refreshView=function(){for(var b=new Array(f),c=0,g=e(d.activeDate.getFullYear());f>c;c++)b[c]=angular.extend(d.createDateObject(new Date(g+c,0,1),d.formatYear),{uid:a.uniqueId+"-"+c});a.title=[b[0].label,b[f-1].label].join(" - "),a.rows=d.split(b,5)},d.compare=function(a,b){return a.getFullYear()-b.getFullYear()},d.handleKeyDown=function(a){var b=d.activeDate.getFullYear();"left"===a?b-=1:"up"===a?b-=5:"right"===a?b+=1:"down"===a?b+=5:"pageup"===a||"pagedown"===a?b+=("pageup"===a?-1:1)*d.step.years:"home"===a?b=e(d.activeDate.getFullYear()):"end"===a&&(b=e(d.activeDate.getFullYear())+f-1),d.activeDate.setFullYear(b)},d.refreshView()}}}]).constant("datepickerPopupConfig",{datepickerPopup:"yyyy-MM-dd",currentText:"Today",clearText:"Clear",closeText:"Done",closeOnDateSelection:!0,appendToBody:!1,showButtonBar:!0}).directive("datepickerPopup",["$compile","$parse","$document","$position","dateFilter","dateParser","datepickerPopupConfig",function(a,b,c,d,e,f,g){return{restrict:"EA",require:"ngModel",scope:{isOpen:"=?",currentText:"@",clearText:"@",closeText:"@",dateDisabled:"&"},link:function(h,i,j,k){function l(a){return a.replace(/([A-Z])/g,function(a){return"-"+a.toLowerCase()})}function m(a){if(a){if(angular.isDate(a)&&!isNaN(a))return k.$setValidity("date",!0),a;if(angular.isString(a)){var b=f.parse(a,n)||new Date(a);return isNaN(b)?void k.$setValidity("date",!1):(k.$setValidity("date",!0),b)}return void k.$setValidity("date",!1)}return k.$setValidity("date",!0),null}var n,o=angular.isDefined(j.closeOnDateSelection)?h.$parent.$eval(j.closeOnDateSelection):g.closeOnDateSelection,p=angular.isDefined(j.datepickerAppendToBody)?h.$parent.$eval(j.datepickerAppendToBody):g.appendToBody;h.showButtonBar=angular.isDefined(j.showButtonBar)?h.$parent.$eval(j.showButtonBar):g.showButtonBar,h.getText=function(a){return h[a+"Text"]||g[a+"Text"]},j.$observe("datepickerPopup",function(a){n=a||g.datepickerPopup,k.$render()});var q=angular.element("
    ");q.attr({"ng-model":"date","ng-change":"dateSelection()"});var r=angular.element(q.children()[0]);j.datepickerOptions&&angular.forEach(h.$parent.$eval(j.datepickerOptions),function(a,b){r.attr(l(b),a)}),angular.forEach(["minDate","maxDate"],function(a){j[a]&&(h.$parent.$watch(b(j[a]),function(b){h[a]=b}),r.attr(l(a),a))}),j.dateDisabled&&r.attr("date-disabled","dateDisabled({ date: date, mode: mode })"),k.$parsers.unshift(m),h.dateSelection=function(a){angular.isDefined(a)&&(h.date=a),k.$setViewValue(h.date),k.$render(),o&&(h.isOpen=!1,i[0].focus())},i.bind("input change keyup",function(){h.$apply(function(){h.date=k.$modelValue})}),k.$render=function(){var a=k.$viewValue?e(k.$viewValue,n):"";i.val(a),h.date=m(k.$modelValue)};var s=function(a){h.isOpen&&a.target!==i[0]&&h.$apply(function(){h.isOpen=!1})},t=function(a){h.keydown(a)};i.bind("keydown",t),h.keydown=function(a){27===a.which?(a.preventDefault(),a.stopPropagation(),h.close()):40!==a.which||h.isOpen||(h.isOpen=!0)},h.$watch("isOpen",function(a){a?(h.$broadcast("datepicker.focus"),h.position=p?d.offset(i):d.position(i),h.position.top=h.position.top+i.prop("offsetHeight"),c.bind("click",s)):c.unbind("click",s)}),h.select=function(a){if("today"===a){var b=new Date;angular.isDate(k.$modelValue)?(a=new Date(k.$modelValue),a.setFullYear(b.getFullYear(),b.getMonth(),b.getDate())):a=new Date(b.setHours(0,0,0,0))}h.dateSelection(a)},h.close=function(){h.isOpen=!1,i[0].focus()};var u=a(q)(h);p?c.find("body").append(u):i.after(u),h.$on("$destroy",function(){u.remove(),i.unbind("keydown",t),c.unbind("click",s)})}}}]).directive("datepickerPopupWrap",function(){return{restrict:"EA",replace:!0,transclude:!0,templateUrl:"template/datepicker/popup.html",link:function(a,b){b.bind("click",function(a){a.preventDefault(),a.stopPropagation()})}}}),angular.module("ui.bootstrap.dropdown",[]).constant("dropdownConfig",{openClass:"open"}).service("dropdownService",["$document",function(a){var b=null;this.open=function(e){b||(a.bind("click",c),a.bind("keydown",d)),b&&b!==e&&(b.isOpen=!1),b=e},this.close=function(e){b===e&&(b=null,a.unbind("click",c),a.unbind("keydown",d))};var c=function(a){a&&a.isDefaultPrevented()||b.$apply(function(){b.isOpen=!1})},d=function(a){27===a.which&&(b.focusToggleElement(),c())}}]).controller("DropdownController",["$scope","$attrs","$parse","dropdownConfig","dropdownService","$animate",function(a,b,c,d,e,f){var g,h=this,i=a.$new(),j=d.openClass,k=angular.noop,l=b.onToggle?c(b.onToggle):angular.noop;this.init=function(d){h.$element=d,b.isOpen&&(g=c(b.isOpen),k=g.assign,a.$watch(g,function(a){i.isOpen=!!a}))},this.toggle=function(a){return i.isOpen=arguments.length?!!a:!i.isOpen},this.isOpen=function(){return i.isOpen},i.focusToggleElement=function(){h.toggleElement&&h.toggleElement[0].focus()},i.$watch("isOpen",function(b,c){f[b?"addClass":"removeClass"](h.$element,j),b?(i.focusToggleElement(),e.open(i)):e.close(i),k(a,b),angular.isDefined(b)&&b!==c&&l(a,{open:!!b})}),a.$on("$locationChangeSuccess",function(){i.isOpen=!1}),a.$on("$destroy",function(){i.$destroy()})}]).directive("dropdown",function(){return{restrict:"CA",controller:"DropdownController",link:function(a,b,c,d){d.init(b)}}}).directive("dropdownToggle",function(){return{restrict:"CA",require:"?^dropdown",link:function(a,b,c,d){if(d){d.toggleElement=b;var e=function(e){e.preventDefault(),b.hasClass("disabled")||c.disabled||a.$apply(function(){d.toggle()})};b.bind("click",e),b.attr({"aria-haspopup":!0,"aria-expanded":!1}),a.$watch(d.isOpen,function(a){b.attr("aria-expanded",!!a)}),a.$on("$destroy",function(){b.unbind("click",e)})}}}}),angular.module("ui.bootstrap.modal",["ui.bootstrap.transition"]).factory("$$stackedMap",function(){return{createNew:function(){var a=[];return{add:function(b,c){a.push({key:b,value:c})},get:function(b){for(var c=0;c0),i()})}function i(){if(k&&-1==g()){var a=l;j(k,l,150,function(){a.$destroy(),a=null}),k=void 0,l=void 0}}function j(c,d,e,f){function g(){g.done||(g.done=!0,c.remove(),f&&f())}d.animate=!1;var h=a.transitionEndEventName;if(h){var i=b(g,e);c.bind(h,function(){b.cancel(i),g(),d.$apply()})}else b(g,0)}var k,l,m="modal-open",n=f.createNew(),o={};return e.$watch(g,function(a){l&&(l.index=a)}),c.bind("keydown",function(a){var b;27===a.which&&(b=n.top(),b&&b.value.keyboard&&(a.preventDefault(),e.$apply(function(){o.dismiss(b.key,"escape key press")})))}),o.open=function(a,b){n.add(a,{deferred:b.deferred,modalScope:b.scope,backdrop:b.backdrop,keyboard:b.keyboard});var f=c.find("body").eq(0),h=g();h>=0&&!k&&(l=e.$new(!0),l.index=h,k=d("
    ")(l),f.append(k));var i=angular.element("
    ");i.attr({"template-url":b.windowTemplateUrl,"window-class":b.windowClass,size:b.size,index:n.length()-1,animate:"animate"}).html(b.content);var j=d(i)(b.scope);n.top().value.modalDomEl=j,f.append(j),f.addClass(m)},o.close=function(a,b){var c=n.get(a).value;c&&(c.deferred.resolve(b),h(a))},o.dismiss=function(a,b){var c=n.get(a).value;c&&(c.deferred.reject(b),h(a))},o.dismissAll=function(a){for(var b=this.getTop();b;)this.dismiss(b.key,a),b=this.getTop()},o.getTop=function(){return n.top()},o}]).provider("$modal",function(){var a={options:{backdrop:!0,keyboard:!0},$get:["$injector","$rootScope","$q","$http","$templateCache","$controller","$modalStack",function(b,c,d,e,f,g,h){function i(a){return a.template?d.when(a.template):e.get(a.templateUrl,{cache:f}).then(function(a){return a.data})}function j(a){var c=[];return angular.forEach(a,function(a){(angular.isFunction(a)||angular.isArray(a))&&c.push(d.when(b.invoke(a)))}),c}var k={};return k.open=function(b){var e=d.defer(),f=d.defer(),k={result:e.promise,opened:f.promise,close:function(a){h.close(k,a)},dismiss:function(a){h.dismiss(k,a)}};if(b=angular.extend({},a.options,b),b.resolve=b.resolve||{},!b.template&&!b.templateUrl)throw new Error("One of template or templateUrl options is required.");var l=d.all([i(b)].concat(j(b.resolve)));return l.then(function(a){var d=(b.scope||c).$new();d.$close=k.close,d.$dismiss=k.dismiss;var f,i={},j=1;b.controller&&(i.$scope=d,i.$modalInstance=k,angular.forEach(b.resolve,function(b,c){i[c]=a[j++]}),f=g(b.controller,i)),h.open(k,{scope:d,deferred:e,content:a[0],backdrop:b.backdrop,keyboard:b.keyboard,windowClass:b.windowClass,windowTemplateUrl:b.windowTemplateUrl,size:b.size})},function(a){e.reject(a)}),l.then(function(){f.resolve(!0)},function(){f.reject(!1)}),k},k}]};return a}),angular.module("ui.bootstrap.pagination",[]).controller("PaginationController",["$scope","$attrs","$parse",function(a,b,c){var d=this,e={$setViewValue:angular.noop},f=b.numPages?c(b.numPages).assign:angular.noop;this.init=function(f,g){e=f,this.config=g,e.$render=function(){d.render()},b.itemsPerPage?a.$parent.$watch(c(b.itemsPerPage),function(b){d.itemsPerPage=parseInt(b,10),a.totalPages=d.calculateTotalPages()}):this.itemsPerPage=g.itemsPerPage},this.calculateTotalPages=function(){var b=this.itemsPerPage<1?1:Math.ceil(a.totalItems/this.itemsPerPage);return Math.max(b||0,1)},this.render=function(){a.page=parseInt(e.$viewValue,10)||1},a.selectPage=function(b){a.page!==b&&b>0&&b<=a.totalPages&&(e.$setViewValue(b),e.$render())},a.getText=function(b){return a[b+"Text"]||d.config[b+"Text"]},a.noPrevious=function(){return 1===a.page},a.noNext=function(){return a.page===a.totalPages},a.$watch("totalItems",function(){a.totalPages=d.calculateTotalPages()}),a.$watch("totalPages",function(b){f(a.$parent,b),a.page>b?a.selectPage(b):e.$render()})}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("pagination",["$parse","paginationConfig",function(a,b){return{restrict:"EA",scope:{totalItems:"=",firstText:"@",previousText:"@",nextText:"@",lastText:"@"},require:["pagination","?ngModel"],controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:!0,link:function(c,d,e,f){function g(a,b,c){return{number:a,text:b,active:c}}function h(a,b){var c=[],d=1,e=b,f=angular.isDefined(k)&&b>k;f&&(l?(d=Math.max(a-Math.floor(k/2),1),e=d+k-1,e>b&&(e=b,d=e-k+1)):(d=(Math.ceil(a/k)-1)*k+1,e=Math.min(d+k-1,b)));for(var h=d;e>=h;h++){var i=g(h,h,h===a);c.push(i)}if(f&&!l){if(d>1){var j=g(d-1,"...",!1);c.unshift(j)}if(b>e){var m=g(e+1,"...",!1);c.push(m)}}return c}var i=f[0],j=f[1];if(j){var k=angular.isDefined(e.maxSize)?c.$parent.$eval(e.maxSize):b.maxSize,l=angular.isDefined(e.rotate)?c.$parent.$eval(e.rotate):b.rotate;c.boundaryLinks=angular.isDefined(e.boundaryLinks)?c.$parent.$eval(e.boundaryLinks):b.boundaryLinks,c.directionLinks=angular.isDefined(e.directionLinks)?c.$parent.$eval(e.directionLinks):b.directionLinks,i.init(j,b),e.maxSize&&c.$parent.$watch(a(e.maxSize),function(a){k=parseInt(a,10),i.render()});var m=i.render;i.render=function(){m(),c.page>0&&c.page<=c.totalPages&&(c.pages=h(c.page,c.totalPages))}}}}}]).constant("pagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("pager",["pagerConfig",function(a){return{restrict:"EA",scope:{totalItems:"=",previousText:"@",nextText:"@"},require:["pager","?ngModel"],controller:"PaginationController",templateUrl:"template/pagination/pager.html",replace:!0,link:function(b,c,d,e){var f=e[0],g=e[1];g&&(b.align=angular.isDefined(d.align)?b.$parent.$eval(d.align):a.align,f.init(g,a))}}}]),angular.module("ui.bootstrap.tooltip",["ui.bootstrap.position","ui.bootstrap.bindHtml"]).provider("$tooltip",function(){function a(a){var b=/[A-Z]/g,c="-";return a.replace(b,function(a,b){return(b?c:"")+a.toLowerCase()})}var b={placement:"top",animation:!0,popupDelay:0},c={mouseenter:"mouseleave",click:"click",focus:"blur"},d={};this.options=function(a){angular.extend(d,a)},this.setTriggers=function(a){angular.extend(c,a)},this.$get=["$window","$compile","$timeout","$parse","$document","$position","$interpolate",function(e,f,g,h,i,j,k){return function(e,l,m){function n(a){var b=a||o.trigger||m,d=c[b]||b;return{show:b,hide:d}}var o=angular.extend({},b,d),p=a(e),q=k.startSymbol(),r=k.endSymbol(),s="
    ';return{restrict:"EA",scope:!0,compile:function(){var a=f(s);return function(b,c,d){function f(){b.tt_isOpen?m():k()}function k(){(!y||b.$eval(d[l+"Enable"]))&&(b.tt_popupDelay?v||(v=g(p,b.tt_popupDelay,!1),v.then(function(a){a() +})):p()())}function m(){b.$apply(function(){q()})}function p(){return v=null,u&&(g.cancel(u),u=null),b.tt_content?(r(),t.css({top:0,left:0,display:"block"}),w?i.find("body").append(t):c.after(t),z(),b.tt_isOpen=!0,b.$digest(),z):angular.noop}function q(){b.tt_isOpen=!1,g.cancel(v),v=null,b.tt_animation?u||(u=g(s,500)):s()}function r(){t&&s(),t=a(b,function(){}),b.$digest()}function s(){u=null,t&&(t.remove(),t=null)}var t,u,v,w=angular.isDefined(o.appendToBody)?o.appendToBody:!1,x=n(void 0),y=angular.isDefined(d[l+"Enable"]),z=function(){var a=j.positionElements(c,t,b.tt_placement,w);a.top+="px",a.left+="px",t.css(a)};b.tt_isOpen=!1,d.$observe(e,function(a){b.tt_content=a,!a&&b.tt_isOpen&&q()}),d.$observe(l+"Title",function(a){b.tt_title=a}),d.$observe(l+"Placement",function(a){b.tt_placement=angular.isDefined(a)?a:o.placement}),d.$observe(l+"PopupDelay",function(a){var c=parseInt(a,10);b.tt_popupDelay=isNaN(c)?o.popupDelay:c});var A=function(){c.unbind(x.show,k),c.unbind(x.hide,m)};d.$observe(l+"Trigger",function(a){A(),x=n(a),x.show===x.hide?c.bind(x.show,f):(c.bind(x.show,k),c.bind(x.hide,m))});var B=b.$eval(d[l+"Animation"]);b.tt_animation=angular.isDefined(B)?!!B:o.animation,d.$observe(l+"AppendToBody",function(a){w=angular.isDefined(a)?h(a)(b):w}),w&&b.$on("$locationChangeSuccess",function(){b.tt_isOpen&&q()}),b.$on("$destroy",function(){g.cancel(u),g.cancel(v),A(),s()})}}}}}]}).directive("tooltipPopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html"}}).directive("tooltip",["$tooltip",function(a){return a("tooltip","tooltip","mouseenter")}]).directive("tooltipHtmlUnsafePopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-unsafe-popup.html"}}).directive("tooltipHtmlUnsafe",["$tooltip",function(a){return a("tooltipHtmlUnsafe","tooltip","mouseenter")}]),angular.module("ui.bootstrap.popover",["ui.bootstrap.tooltip"]).directive("popoverPopup",function(){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html"}}).directive("popover",["$tooltip",function(a){return a("popover","popover","click")}]),angular.module("ui.bootstrap.progressbar",[]).constant("progressConfig",{animate:!0,max:100}).controller("ProgressController",["$scope","$attrs","progressConfig",function(a,b,c){var d=this,e=angular.isDefined(b.animate)?a.$parent.$eval(b.animate):c.animate;this.bars=[],a.max=angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max,this.addBar=function(b,c){e||c.css({transition:"none"}),this.bars.push(b),b.$watch("value",function(c){b.percent=+(100*c/a.max).toFixed(2)}),b.$on("$destroy",function(){c=null,d.removeBar(b)})},this.removeBar=function(a){this.bars.splice(this.bars.indexOf(a),1)}}]).directive("progress",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",require:"progress",scope:{},templateUrl:"template/progressbar/progress.html"}}).directive("bar",function(){return{restrict:"EA",replace:!0,transclude:!0,require:"^progress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(a,b,c,d){d.addBar(a,b)}}}).directive("progressbar",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(a,b,c,d){d.addBar(a,angular.element(b.children()[0]))}}}),angular.module("ui.bootstrap.rating",[]).constant("ratingConfig",{max:5,stateOn:null,stateOff:null}).controller("RatingController",["$scope","$attrs","ratingConfig",function(a,b,c){var d={$setViewValue:angular.noop};this.init=function(e){d=e,d.$render=this.render,this.stateOn=angular.isDefined(b.stateOn)?a.$parent.$eval(b.stateOn):c.stateOn,this.stateOff=angular.isDefined(b.stateOff)?a.$parent.$eval(b.stateOff):c.stateOff;var f=angular.isDefined(b.ratingStates)?a.$parent.$eval(b.ratingStates):new Array(angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max);a.range=this.buildTemplateObjects(f)},this.buildTemplateObjects=function(a){for(var b=0,c=a.length;c>b;b++)a[b]=angular.extend({index:b},{stateOn:this.stateOn,stateOff:this.stateOff},a[b]);return a},a.rate=function(b){!a.readonly&&b>=0&&b<=a.range.length&&(d.$setViewValue(b),d.$render())},a.enter=function(b){a.readonly||(a.value=b),a.onHover({value:b})},a.reset=function(){a.value=d.$viewValue,a.onLeave()},a.onKeydown=function(b){/(37|38|39|40)/.test(b.which)&&(b.preventDefault(),b.stopPropagation(),a.rate(a.value+(38===b.which||39===b.which?1:-1)))},this.render=function(){a.value=d.$viewValue}}]).directive("rating",function(){return{restrict:"EA",require:["rating","ngModel"],scope:{readonly:"=?",onHover:"&",onLeave:"&"},controller:"RatingController",templateUrl:"template/rating/rating.html",replace:!0,link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f)}}}),angular.module("ui.bootstrap.tabs",[]).controller("TabsetController",["$scope",function(a){var b=this,c=b.tabs=a.tabs=[];b.select=function(a){angular.forEach(c,function(b){b.active&&b!==a&&(b.active=!1,b.onDeselect())}),a.active=!0,a.onSelect()},b.addTab=function(a){c.push(a),1===c.length?a.active=!0:a.active&&b.select(a)},b.removeTab=function(a){var d=c.indexOf(a);if(a.active&&c.length>1){var e=d==c.length-1?d-1:d+1;b.select(c[e])}c.splice(d,1)}}]).directive("tabset",function(){return{restrict:"EA",transclude:!0,replace:!0,scope:{type:"@"},controller:"TabsetController",templateUrl:"template/tabs/tabset.html",link:function(a,b,c){a.vertical=angular.isDefined(c.vertical)?a.$parent.$eval(c.vertical):!1,a.justified=angular.isDefined(c.justified)?a.$parent.$eval(c.justified):!1}}}).directive("tab",["$parse",function(a){return{require:"^tabset",restrict:"EA",replace:!0,templateUrl:"template/tabs/tab.html",transclude:!0,scope:{active:"=?",heading:"@",onSelect:"&select",onDeselect:"&deselect"},controller:function(){},compile:function(b,c,d){return function(b,c,e,f){b.$watch("active",function(a){a&&f.select(b)}),b.disabled=!1,e.disabled&&b.$parent.$watch(a(e.disabled),function(a){b.disabled=!!a}),b.select=function(){b.disabled||(b.active=!0)},f.addTab(b),b.$on("$destroy",function(){f.removeTab(b)}),b.$transcludeFn=d}}}}]).directive("tabHeadingTransclude",[function(){return{restrict:"A",require:"^tab",link:function(a,b){a.$watch("headingElement",function(a){a&&(b.html(""),b.append(a))})}}}]).directive("tabContentTransclude",function(){function a(a){return a.tagName&&(a.hasAttribute("tab-heading")||a.hasAttribute("data-tab-heading")||"tab-heading"===a.tagName.toLowerCase()||"data-tab-heading"===a.tagName.toLowerCase())}return{restrict:"A",require:"^tabset",link:function(b,c,d){var e=b.$eval(d.tabContentTransclude);e.$transcludeFn(e.$parent,function(b){angular.forEach(b,function(b){a(b)?e.headingElement=b:c.append(b)})})}}}),angular.module("ui.bootstrap.timepicker",[]).constant("timepickerConfig",{hourStep:1,minuteStep:1,showMeridian:!0,meridians:null,readonlyInput:!1,mousewheel:!0}).controller("TimepickerController",["$scope","$attrs","$parse","$log","$locale","timepickerConfig",function(a,b,c,d,e,f){function g(){var b=parseInt(a.hours,10),c=a.showMeridian?b>0&&13>b:b>=0&&24>b;return c?(a.showMeridian&&(12===b&&(b=0),a.meridian===p[1]&&(b+=12)),b):void 0}function h(){var b=parseInt(a.minutes,10);return b>=0&&60>b?b:void 0}function i(a){return angular.isDefined(a)&&a.toString().length<2?"0"+a:a}function j(a){k(),o.$setViewValue(new Date(n)),l(a)}function k(){o.$setValidity("time",!0),a.invalidHours=!1,a.invalidMinutes=!1}function l(b){var c=n.getHours(),d=n.getMinutes();a.showMeridian&&(c=0===c||12===c?12:c%12),a.hours="h"===b?c:i(c),a.minutes="m"===b?d:i(d),a.meridian=n.getHours()<12?p[0]:p[1]}function m(a){var b=new Date(n.getTime()+6e4*a);n.setHours(b.getHours(),b.getMinutes()),j()}var n=new Date,o={$setViewValue:angular.noop},p=angular.isDefined(b.meridians)?a.$parent.$eval(b.meridians):f.meridians||e.DATETIME_FORMATS.AMPMS;this.init=function(c,d){o=c,o.$render=this.render;var e=d.eq(0),g=d.eq(1),h=angular.isDefined(b.mousewheel)?a.$parent.$eval(b.mousewheel):f.mousewheel;h&&this.setupMousewheelEvents(e,g),a.readonlyInput=angular.isDefined(b.readonlyInput)?a.$parent.$eval(b.readonlyInput):f.readonlyInput,this.setupInputEvents(e,g)};var q=f.hourStep;b.hourStep&&a.$parent.$watch(c(b.hourStep),function(a){q=parseInt(a,10)});var r=f.minuteStep;b.minuteStep&&a.$parent.$watch(c(b.minuteStep),function(a){r=parseInt(a,10)}),a.showMeridian=f.showMeridian,b.showMeridian&&a.$parent.$watch(c(b.showMeridian),function(b){if(a.showMeridian=!!b,o.$error.time){var c=g(),d=h();angular.isDefined(c)&&angular.isDefined(d)&&(n.setHours(c),j())}else l()}),this.setupMousewheelEvents=function(b,c){var d=function(a){a.originalEvent&&(a=a.originalEvent);var b=a.wheelDelta?a.wheelDelta:-a.deltaY;return a.detail||b>0};b.bind("mousewheel wheel",function(b){a.$apply(d(b)?a.incrementHours():a.decrementHours()),b.preventDefault()}),c.bind("mousewheel wheel",function(b){a.$apply(d(b)?a.incrementMinutes():a.decrementMinutes()),b.preventDefault()})},this.setupInputEvents=function(b,c){if(a.readonlyInput)return a.updateHours=angular.noop,void(a.updateMinutes=angular.noop);var d=function(b,c){o.$setViewValue(null),o.$setValidity("time",!1),angular.isDefined(b)&&(a.invalidHours=b),angular.isDefined(c)&&(a.invalidMinutes=c)};a.updateHours=function(){var a=g();angular.isDefined(a)?(n.setHours(a),j("h")):d(!0)},b.bind("blur",function(){!a.invalidHours&&a.hours<10&&a.$apply(function(){a.hours=i(a.hours)})}),a.updateMinutes=function(){var a=h();angular.isDefined(a)?(n.setMinutes(a),j("m")):d(void 0,!0)},c.bind("blur",function(){!a.invalidMinutes&&a.minutes<10&&a.$apply(function(){a.minutes=i(a.minutes)})})},this.render=function(){var a=o.$modelValue?new Date(o.$modelValue):null;isNaN(a)?(o.$setValidity("time",!1),d.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.')):(a&&(n=a),k(),l())},a.incrementHours=function(){m(60*q)},a.decrementHours=function(){m(60*-q)},a.incrementMinutes=function(){m(r)},a.decrementMinutes=function(){m(-r)},a.toggleMeridian=function(){m(720*(n.getHours()<12?1:-1))}}]).directive("timepicker",function(){return{restrict:"EA",require:["timepicker","?^ngModel"],controller:"TimepickerController",replace:!0,scope:{},templateUrl:"template/timepicker/timepicker.html",link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f,b.find("input"))}}}),angular.module("ui.bootstrap.typeahead",["ui.bootstrap.position","ui.bootstrap.bindHtml"]).factory("typeaheadParser",["$parse",function(a){var b=/^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;return{parse:function(c){var d=c.match(b);if(!d)throw new Error('Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_" but got "'+c+'".');return{itemName:d[3],source:a(d[4]),viewMapper:a(d[2]||d[1]),modelMapper:a(d[1])}}}}]).directive("typeahead",["$compile","$parse","$q","$timeout","$document","$position","typeaheadParser",function(a,b,c,d,e,f,g){var h=[9,13,27,38,40];return{require:"ngModel",link:function(i,j,k,l){var m,n=i.$eval(k.typeaheadMinLength)||1,o=i.$eval(k.typeaheadWaitMs)||0,p=i.$eval(k.typeaheadEditable)!==!1,q=b(k.typeaheadLoading).assign||angular.noop,r=b(k.typeaheadOnSelect),s=k.typeaheadInputFormatter?b(k.typeaheadInputFormatter):void 0,t=k.typeaheadAppendToBody?i.$eval(k.typeaheadAppendToBody):!1,u=b(k.ngModel).assign,v=g.parse(k.typeahead),w=i.$new();i.$on("$destroy",function(){w.$destroy()});var x="typeahead-"+w.$id+"-"+Math.floor(1e4*Math.random());j.attr({"aria-autocomplete":"list","aria-expanded":!1,"aria-owns":x});var y=angular.element("
    ");y.attr({id:x,matches:"matches",active:"activeIdx",select:"select(activeIdx)",query:"query",position:"position"}),angular.isDefined(k.typeaheadTemplateUrl)&&y.attr("template-url",k.typeaheadTemplateUrl);var z=function(){w.matches=[],w.activeIdx=-1,j.attr("aria-expanded",!1)},A=function(a){return x+"-option-"+a};w.$watch("activeIdx",function(a){0>a?j.removeAttr("aria-activedescendant"):j.attr("aria-activedescendant",A(a))});var B=function(a){var b={$viewValue:a};q(i,!0),c.when(v.source(i,b)).then(function(c){var d=a===l.$viewValue;if(d&&m)if(c.length>0){w.activeIdx=0,w.matches.length=0;for(var e=0;e=n?o>0?(C&&d.cancel(C),C=d(function(){B(a)},o)):B(a):(q(i,!1),z()),p?a:a?void l.$setValidity("editable",!1):(l.$setValidity("editable",!0),a)}),l.$formatters.push(function(a){var b,c,d={};return s?(d.$model=a,s(i,d)):(d[v.itemName]=a,b=v.viewMapper(i,d),d[v.itemName]=void 0,c=v.viewMapper(i,d),b!==c?b:a)}),w.select=function(a){var b,c,e={};e[v.itemName]=c=w.matches[a].model,b=v.modelMapper(i,e),u(i,b),l.$setValidity("editable",!0),r(i,{$item:c,$model:b,$label:v.viewMapper(i,e)}),z(),d(function(){j[0].focus()},0,!1)},j.bind("keydown",function(a){0!==w.matches.length&&-1!==h.indexOf(a.which)&&(a.preventDefault(),40===a.which?(w.activeIdx=(w.activeIdx+1)%w.matches.length,w.$digest()):38===a.which?(w.activeIdx=(w.activeIdx?w.activeIdx:w.matches.length)-1,w.$digest()):13===a.which||9===a.which?w.$apply(function(){w.select(w.activeIdx)}):27===a.which&&(a.stopPropagation(),z(),w.$digest()))}),j.bind("blur",function(){m=!1});var D=function(a){j[0]!==a.target&&(z(),w.$digest())};e.bind("click",D),i.$on("$destroy",function(){e.unbind("click",D)});var E=a(y)(w);t?e.find("body").append(E):j.after(E)}}}]).directive("typeaheadPopup",function(){return{restrict:"EA",scope:{matches:"=",query:"=",active:"=",position:"=",select:"&"},replace:!0,templateUrl:"template/typeahead/typeahead-popup.html",link:function(a,b,c){a.templateUrl=c.templateUrl,a.isOpen=function(){return a.matches.length>0},a.isActive=function(b){return a.active==b},a.selectActive=function(b){a.active=b},a.selectMatch=function(b){a.select({activeIdx:b})}}}}).directive("typeaheadMatch",["$http","$templateCache","$compile","$parse",function(a,b,c,d){return{restrict:"EA",scope:{index:"=",match:"=",query:"="},link:function(e,f,g){var h=d(g.templateUrl)(e.$parent)||"template/typeahead/typeahead-match.html";a.get(h,{cache:b}).success(function(a){f.replaceWith(c(a.trim())(e))})}}}]).filter("typeaheadHighlight",function(){function a(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}return function(b,c){return c?(""+b).replace(new RegExp(a(c),"gi"),"$&"):b}}); \ No newline at end of file diff --git a/app/bower_components/geolib/.bower.json b/app/bower_components/geolib/.bower.json new file mode 100644 index 0000000..fbe58d0 --- /dev/null +++ b/app/bower_components/geolib/.bower.json @@ -0,0 +1,32 @@ +{ + "name": "Geolib", + "main": "dist/geolib.js", + "version": "2.0.9", + "homepage": "https://github.com/manuelbieh/Geolib", + "authors": [ + "Manuel Bieh " + ], + "description": "Library to perform geo specific tasks", + "keywords": [ + "geolocation", + "geo", + "distance" + ], + "license": "LGPL", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "_release": "2.0.9", + "_resolution": { + "type": "version", + "tag": "2.0.9", + "commit": "c4222cde0d201e5d03f0d70ae560a5707907761c" + }, + "_source": "git://github.com/manuelbieh/Geolib.git", + "_target": "~2.0.9", + "_originalSource": "geolib" +} \ No newline at end of file diff --git a/app/bower_components/geolib/Gruntfile.js b/app/bower_components/geolib/Gruntfile.js new file mode 100644 index 0000000..695d2d4 --- /dev/null +++ b/app/bower_components/geolib/Gruntfile.js @@ -0,0 +1,171 @@ +/*global module:false*/ +module.exports = function(grunt) { + + require('time-grunt')(grunt); + + require('load-grunt-tasks')(grunt); + + grunt.registerTask('version', function(target, op) { + + var fs = require('fs'); + var data = JSON.parse(fs.readFileSync('package.json', {encoding: 'utf-8'})); + var version = data.version.split('.'); + + var major = parseInt(version[0], 10); + var minor = parseInt(version[1], 10); + var patch = parseInt(version[2].split('+')[0], 10); + var info = version[2].split('+')[1]; + var log = ''; + + if(typeof op == 'undefined' || op === '+') { + op = '+'; + log += 'Incrementing '; + } else if(op === '-') { + op = op; + log += 'Decrementing '; + } else if(!isNaN(parseInt(op, 10))) { + op = parseInt(op, 10); + log += 'Using ' + op + ' as new '; + } else { + grunt.log.fail('Illegal operation.'); + return false; + } + + if(['major', 'minor', 'patch'].indexOf(target) > -1) { + log += target + ' version. '; + } + + switch(target) { + case 'major': + major = op == '-' ? major-1 : (op == '+' ? major+1 : op); + if(major < 0) major = 0; + break; + case 'minor': + minor = op == '-' ? minor-1 : (op == '+' ? minor+1 : op); + if(minor < 0) minor = 0; + break; + case 'patch': + patch = op == '-' ? patch-1 : (op == '+' ? patch+1 : op); + if(patch < 0) patch = 0; + break; + } + + data.version = [major, minor, patch].join('.') + (info ? '+' + info : ''); + + grunt.log.writeln(log + 'New version is ' + data.version); + + fs.writeFileSync('package.json', JSON.stringify(data, null, 2), {encoding: 'utf-8'}) + grunt.config.data.pkg.version = data.version; + + grunt.task.run('default'); + + }); + + // Project configuration. + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + banner: '/*! <%= pkg.name %> <%= pkg.version %> by <%= pkg.author.name %>\n'+ + '* Library to provide geo functions like distance calculation,\n' + + '* conversion of decimal coordinates to sexagesimal and vice versa, etc.\n' + + '* WGS 84 (World Geodetic System 1984)\n' + + '* \n' + + '* @author <%= pkg.author.name %>\n' + + '* @url <%= pkg.author.url %>\n' + + '* @version <%= pkg.version %>\n' + + '* @license <%= _.pluck(pkg.licenses, "type").join(", ") %> \n**/', + lint: { + files: ['src/geolib.js'] + }, + qunit: { + files: ['tests/*.html'] + }, + concat: { + main: { + options: { + banner: '<%= banner %>', + report: false + }, + src: ['src/geolib.js'], + dest: 'dist/geolib.js' + } + }, + copy: { + component: { + files: [{ + src: "package.json", + dest: "component.json" + }] + }, + elev: { + files: [{ + src: ['src/geolib.elevation.js'], + dest: 'dist/geolib.elevation.js' + }] + } + }, + replace: { + version: { + src: ['dist/*.js', 'bower.json'], + overwrite: true, + replacements: [{ + from: '$version$', + to: '<%= pkg.version %>' + }, { + from: /"version": "([0-9a-zA-Z\-\.\+]*)",/, + to: '"version": "<%= pkg.version %>",' + }] + } + }, + uglify: { + options: { + preserveComments: 'some' + }, + main: { + files: { + 'dist/geolib.min.js': ['dist/geolib.js'] + } + }, + elev: { + files: { + 'dist/geolib.elevation.min.js': ['dist/geolib.elevation.js'] + } + } + }, + watch: { + all: { + files: '<%= jshint.all %>', + tasks: ['default'] + } + }, + jshint: { + all: [ + 'src/geolib.js', + 'src/geolib.elevation.js' + ], + options: { + curly: true, + eqeqeq: false, + immed: true, + latedef: true, + newcap: false, + noarg: true, + sub: true, + undef: true, + boss: true, + eqnull: true, + globals: { + module: true, + define: true, + require: true, + elevationResult: true + } + } + } + }); + + // Default task. + grunt.registerTask('default', ['concat:main', 'copy', 'replace', 'uglify']); + grunt.registerTask('travis', ['jshint','qunit']); + grunt.registerTask('test', ['qunit']); + +}; diff --git a/app/bower_components/geolib/LICENSE b/app/bower_components/geolib/LICENSE new file mode 100644 index 0000000..7052a63 --- /dev/null +++ b/app/bower_components/geolib/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2011-2014 Manuel Bieh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/app/bower_components/geolib/README.md b/app/bower_components/geolib/README.md new file mode 100644 index 0000000..14e6aba --- /dev/null +++ b/app/bower_components/geolib/README.md @@ -0,0 +1,262 @@ +# Geolib v2.0.9 +[![Build Status](https://secure.travis-ci.org/manuelbieh/Geolib.png?branch=master)](http://travis-ci.org/manuelbieh/Geolib) + +Library to provide basic geospatial operations like distance calculation, conversion of decimal coordinates to sexagesimal and vice versa, etc. + +[View demo](http://www.manuel-bieh.de/publikationen/scripts/geolib/demo.html) + +

    Methods

    + +

    geolib.getDistance(object start, object end[, int accuracy])

    + +Calculates the distance between two geo coordinates + +Takes 2 or 3 arguments. First 2 arguments must be an object with latitude and a longitude properties (e.g. `{latitude: 52.518611, longitude: 13.408056}`). Coordinates can be in sexagesimal or decimal format. 3rd argument is accuracy (in meters). So a calculated distance of 1248 meters with an accuracy of 100 is returned as `1200` (accuracy 10 = `1250` etc.). + +Return value is always an integer and represents the distance in meters. + +

    Examples

    + +
    geolib.getDistance(
    +	{latitude: 51.5103, longitude: 7.49347}, 
    +	{latitude: "51° 31' N", longitude: "7° 28' E"}
    +);
    +geolib.getDistance(
    +	{latitude: 51.5103, longitude: 7.49347}, 
    +	{latitude: "51° 31' N", longitude: "7° 28' E"}
    +);
    +
    +// Working with W3C Geolocation API
    +navigator.geolocation.getCurrentPosition(
    +	function(position) {
    +		alert('You are ' + geolib.getDistance(position.coords, {
    +			latitude: 51.525, 
    +			longitude: 7.4575
    +		}) + ' meters away from 51.525, 7.4575');
    +	}, 
    +	function() { 
    +		alert('Position could not be determined.')
    +	}, 
    +	{
    +		enableHighAccuracy: true
    +	}
    +);
    +
    + +

    geolib.getCenter(array coords)

    + +Calculates the geographical center of all points in a collection of geo coordinates + +Takes an object or array of coordinates and calculates the center of it. + +Returns an object: `{"latitude": centerLat, "longitude": centerLng, "distance": diagonalDistance}` + +

    Examples

    + +
    var spots = {
    +	"Brandenburg Gate, Berlin": {latitude: 52.516272, longitude: 13.377722},
    +	"Dortmund U-Tower": {latitude: 51.515, longitude: 7.453619},
    +	"London Eye": {latitude: 51.503333, longitude: -0.119722},
    +	"Kremlin, Moscow": {latitude: 55.751667, longitude: 37.617778},
    +	"Eiffel Tower, Paris": {latitude: 48.8583, longitude: 2.2945},
    +	"Riksdag building, Stockholm": {latitude: 59.3275, longitude: 18.0675},
    +	"Royal Palace, Oslo": {latitude: 59.916911, longitude: 10.727567}
    +}
    +
    +geolib.getCenter(spots);
    +
    +geolib.getCenter([
    +	{latitude: 52.516272, longitude: 13.377722}, 
    +	{latitude: 51.515, longitude: 7.453619}, 
    +	{latitude: 51.503333, longitude: -0.119722}
    +]);
    +
    + +

    geolib.isPointInside(object latlng, array coords)

    + +Checks whether a point is inside of a polygon or not. +Note: the polygon coords must be in correct order! + +Returns true or false + +

    Example

    + +
    +geolib.isPointInside(
    +	{latitude: 51.5125, longitude: 7.485}, 
    +	[
    +		{latitude: 51.50, longitude: 7.40},
    +		{latitude: 51.555, longitude: 7.40},
    +		{latitude: 51.555, longitude: 7.625},
    +		{latitude: 51.5125, longitude: 7.625}
    +	]
    +); // -> true
    + +

    geolib.isPointInCircle(object latlng, object center, integer radius)

    + +Similar to is point inside: checks whether a point is inside of a circle or not. + +Returns true or false + +

    Example

    + +
    // checks if 51.525, 7.4575 is within a radius of 5km from 51.5175, 7.4678
    +geolib.isPointInCircle(
    +	{latitude: 51.525, longitude: 7.4575},
    +	{latitude: 51.5175, longitude: 7.4678}, 
    +	5000
    +);
    + +

    geolib.orderByDistance(object latlng, mixed coords)

    + +Sorts an object or array of coords by distance from a reference coordinate + +Returns a sorted array [{latitude: x, longitude: y, distance: z, key: property}] + +

    Examples

    + +
    +// coords array
    +geolib.orderByDistance({latitude: 51.515, longitude: 7.453619}, [
    +	{latitude: 52.516272, longitude: 13.377722}, 
    +	{latitude: 51.518, longitude: 7.45425}, 
    +	{latitude: 51.503333, longitude: -0.119722}
    +]);
    +
    +// coords object
    +geolib.orderByDistance({latitude: 51.515, longitude: 7.453619}, {
    +	a: {latitude: 52.516272, longitude: 13.377722}, 
    +	b: {latitude: 51.518, longitude: 7.45425}, 
    +	c: {latitude: 51.503333, longitude: -0.119722}
    +});
    +
    + +

    geolib.findNearest(object latlng, mixed coords[[, int offset], int limit])

    + +Finds the nearest coordinate to a reference coordinate. + +

    Examples

    + +
    var spots = {
    +	"Brandenburg Gate, Berlin": {latitude: 52.516272, longitude: 13.377722},
    +	"Dortmund U-Tower": {latitude: 51.515, longitude: 7.453619},
    +	"London Eye": {latitude: 51.503333, longitude: -0.119722},
    +	"Kremlin, Moscow": {latitude: 55.751667, longitude: 37.617778},
    +	"Eiffel Tower, Paris": {latitude: 48.8583, longitude: 2.2945},
    +	"Riksdag building, Stockholm": {latitude: 59.3275, longitude: 18.0675},
    +	"Royal Palace, Oslo": {latitude: 59.916911, longitude: 10.727567}
    +}
    +
    +// in this case set offset to 1 otherwise the nearest point will always be your reference point
    +geolib.findNearest(spots['Dortmund U-Tower'], spots, 1) 
    +
    + +

    geolib.getPathLength(mixed coords)

    + +Calculates the length of a collection of coordinates + +Returns the length of the path in meters + +

    Example

    + +
    +// Calculate distance from Berlin via Dortmund to London
    +geolib.getPathLength([
    +	{latitude: 52.516272, longitude: 13.377722}, // Berlin
    +	{latitude: 51.515, longitude: 7.453619}, // Dortmund
    +	{latitude: 51.503333, longitude: -0.119722} // London
    +]); // -> 945235
    + +

    geolib.getSpeed(coords, coords[, options])

    + +Calculates the speed between two points within a given time span. + +Returns the speed in options.unit (default is km/h). + +

    Example

    + +
    +geolib.getSpeed(
    +	{lat: 51.567294, lng: 7.38896, time: 1360231200880}, 
    +	{lat: 52.54944, lng: 13.468509, time: 1360245600880},
    +	{unit: 'mph'}
    +); // -> 66.9408 (mph)
    + +

    geolib.convertUnit(string unit, float distance[, int round])

    + +Converts a given distance (in meters) to another unit. + +

    Parameters

    + +`unit` can be one of: + +- m (meter) +- km (kilometers) +- cm (centimeters) +- mm (millimeters) +- mi (miles) +- sm (seamiles) +- ft (foot) +- in (inch) +- yd (yards) + +`distance` distance to be converted (source must be in meter) + +`round` fractional digits + +

    Example

    + +`geolib.convertUnit('km', 14213, 2) // -> 14,21` + +

    geolib.sexagesimal2decimal(string coord)

    + +Converts a sexagesimal coordinate to decimal format + +

    Example

    + +`geolib.sexagesimal2decimal("51° 29' 46\" N")` + +

    geolib.decimal2sexagesimal(float coord)

    + +Converts a decimal coordinate to sexagesimal format + +

    Example

    + +`geolib.decimal2sexagesimal(51.49611111); // -> 51° 29' 46.00` + +

    geolib.latitude(object latlng)

    +

    geolib.longitude(object latlng)

    +

    geolib.elevation(object latlng)

    + +Returns the latitude/longitude/elevation for a given point and converts it to decimal. + +Works with: +- latitude: `latitude`, `lat`, 0 (GeoJSON array) +- longitude: `longitude`, `lng`, `lon`, 1 (GeoJSON array) +- elevation: `elevation`, `elev`, `alt`, `altitude`, 2 (GeoJSON array) + +

    Examples

    + +`geolib.latitude({lat: 51.49611, lng: 7.38896}); // -> 51.49611` +`geolib.longitude({lat: 51.49611, lng: 7.38896}); // -> 7.38896` + +

    geolib.useDecimal(mixed latlng)

    + +Checks if a coordinate is already in decimal format and, if not, converts it to + +

    Example

    + +
    geolib.useDecimal("51° 29' 46\" N"); // -> 51.59611111
    +geolib.useDecimal(51.59611111) // -> 51.59611111
    + +

    Changelog

    +

    v2.0.0+beta1

    +- Dropped support for IE6, IE7, IE8 +- Added new methods `geolib.latitude()`, `geolib.longitude()`, `geolib.elevation()` to get latitude, longitude or elevation of points. Will be converted to decimal format automatically +- Added new method `geolib.extend()` to extend geolib object +- Added support for GeoJSON format (`[lon, lat, elev]`) +- Added property `geolib.version` to query the currently used version +- Moved `geolib.elevation` to an optional module (`geolib.elevation.js`) +- Using `Object.create(Geolib.prototype)` instead of object literal `{}` +- New folder structure: compiled `geolib.js` can now be found in `dist/` instead of root dir +- Improved Grunt build task \ No newline at end of file diff --git a/app/bower_components/geolib/bower.json b/app/bower_components/geolib/bower.json new file mode 100644 index 0000000..e8c867c --- /dev/null +++ b/app/bower_components/geolib/bower.json @@ -0,0 +1,23 @@ +{ + "name": "Geolib", + "main": "dist/geolib.js", + "version": "2.0.9", + "homepage": "https://github.com/manuelbieh/Geolib", + "authors": [ + "Manuel Bieh " + ], + "description": "Library to perform geo specific tasks", + "keywords": [ + "geolocation", + "geo", + "distance" + ], + "license": "LGPL", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ] +} diff --git a/app/bower_components/geolib/component.json b/app/bower_components/geolib/component.json new file mode 100644 index 0000000..5830e6f --- /dev/null +++ b/app/bower_components/geolib/component.json @@ -0,0 +1,51 @@ +{ + "name": "geolib", + "homepage": "http://github.com/manuelbieh/Geolib", + "author": { + "name": "Manuel Bieh", + "url": "http://www.manuelbieh.com/" + }, + "repository": { + "type": "git", + "url": "http://github.com/manuelbieh/geolib.git" + }, + "devDependencies": { + "grunt": "~0.4", + "grunt-cli": "*", + "grunt-contrib-uglify": "~0.2", + "grunt-contrib-concat": "~0.3", + "grunt-contrib-clean": "~0.5.0", + "grunt-contrib-copy": "~0.4", + "grunt-contrib-qunit": "~0.2.0", + "grunt-text-replace": "~0.3.6", + "grunt-contrib-jshint": "~0.6.2", + "phantomjs": "~1.8.0", + "grunt-jslint": "~1.0.0", + "time-grunt": "~0.3.1", + "load-grunt-tasks": "~0.4.0" + }, + "licenses": [ + { + "type": "MIT", + "url": "http://opensource.org/licenses/MIT" + } + ], + "files": [ + "geolib.js" + ], + "description": "Library to perform geo specific tasks", + "keywords": [ + "geolocation", + "geo", + "distance", + "geojson", + "geospatial", + "lbs", + "location" + ], + "scripts": { + "test": "grunt travis --verbose" + }, + "version": "2.0.9", + "main": "./geolib" +} \ No newline at end of file diff --git a/app/bower_components/geolib/dist/geolib.elevation.js b/app/bower_components/geolib/dist/geolib.elevation.js new file mode 100644 index 0000000..5b2617b --- /dev/null +++ b/app/bower_components/geolib/dist/geolib.elevation.js @@ -0,0 +1,190 @@ +/*! geolib.elevation 2.0.9 by Manuel Bieh +* +* Elevation Addon for Geolib.js +* +* @author Manuel Bieh +* @url http://www.manuelbieh.com/ +* @version 2.0.9 +* @license MIT +*/ +;(function(global, geolib, undefined) { + + var elevation = { + + /*global google:true geolib:true require:true module:true elevationResult:true */ + + /** + * @param Array Collection of coords [{latitude: 51.510, longitude: 7.1321}, {latitude: 49.1238, longitude: "8° 30' W"}, ...] + * @return Array [{lat:#lat, lng:#lng, elev:#elev},....]} + */ + getElevation: function() { + if (typeof global.navigator !== 'undefined') { + this.getElevationClient.apply(this, arguments); + } else { + this.getElevationServer.apply(this, arguments); + } + }, + + + /* Optional elevation addon requires Googlemaps API JS */ + getElevationClient: function(coords, cb) { + + if (!global.google) { + throw new Error("Google maps api not loaded"); + } + + if (coords.length === 0) { + return cb(null, null); + } + + if (coords.length === 1) { + return cb(new Error("getElevation requires at least 2 points.")); + } + + var path = []; + + for(var i = 0; i < coords.length; i++) { + path.push(new google.maps.LatLng( + this.latitude(coords[i]), + this.longitude(coords[i]) + )); + } + + var positionalRequest = { + 'path': path, + 'samples': path.length + }; + + var elevationService = new google.maps.ElevationService(); + var geolib = this; + + elevationService.getElevationAlongPath(positionalRequest, function (results, status) { + geolib.elevationHandler(results, status, coords, cb); + }); + + }, + + + getElevationServer: function(coords, cb) { + + if (coords.length === 0) { + return cb(null, null); + } + + if (coords.length === 1) { + return cb(new Error("getElevation requires at least 2 points.")); + } + + var gm = require('googlemaps'); + var path = []; + + for(var i = 0; i < coords.length; i++) { + path.push( + this.latitude(coords[i]) + ',' + this.longitude(coords[i]) + ); + } + + var geolib = this; + + gm.elevationFromPath(path.join('|'), path.length, function(err, results) { + geolib.elevationHandler(results.results, results.status, coords, cb); + }); + + }, + + + elevationHandler: function(results, status, coords, cb) { + + var latsLngsElevs = []; + + if (status == "OK" ) { + + for (var i = 0; i < results.length; i++) { + latsLngsElevs.push({ + "lat": this.latitude(coords[i]), + "lng": this.longitude(coords[i]), + "elev":results[i].elevation + }); + } + + cb(null, latsLngsElevs); + + } else { + + cb(new Error("Could not get elevation using Google's API"), elevationResult.status); + + } + + }, + + + /** + * @param Array [{lat:#lat, lng:#lng, elev:#elev},....]} + * @return Number % grade + */ + getGrade: function(coords) { + + var rise = Math.abs( + this.elevation(coords[coords.length-1]) - this.elevation(coords[0]) + ); + + var run = this.getPathLength(coords); + + return Math.floor((rise/run)*100); + + }, + + + /** + * @param Array [{lat:#lat, lng:#lng, elev:#elev},....]} + * @return Object {gain:#gain, loss:#loss} + */ + getTotalElevationGainAndLoss: function(coords) { + + var gain = 0; + var loss = 0; + + for(var i = 0; i < coords.length - 1; i++) { + + var deltaElev = this.elevation(coords[i]) - this.elevation(coords[i + 1]); + + if (deltaElev > 0) { + loss += deltaElev; + } else { + gain += Math.abs(deltaElev); + } + + } + + return { + "gain": gain, + "loss": loss + }; + + } + + }; + + // Node module + if (typeof module !== 'undefined' && + typeof module.exports !== 'undefined') { + + geolib = require('geolib'); + geolib.extend(elevation); + + // AMD module + } else if (typeof define === "function" && define.amd) { + + define(["geolib"], function (geolib) { + geolib.extend(elevation); + return geolib; + }); + + // we're in a browser + } else { + + geolib.extend(elevation); + + } + +}(this, this.geolib)); \ No newline at end of file diff --git a/app/bower_components/geolib/dist/geolib.elevation.min.js b/app/bower_components/geolib/dist/geolib.elevation.min.js new file mode 100644 index 0000000..5a7ddb7 --- /dev/null +++ b/app/bower_components/geolib/dist/geolib.elevation.min.js @@ -0,0 +1,10 @@ +/*! geolib.elevation 2.0.9 by Manuel Bieh +* +* Elevation Addon for Geolib.js +* +* @author Manuel Bieh +* @url http://www.manuelbieh.com/ +* @version 2.0.9 +* @license MIT +*/ +!function(a,b){var c={getElevation:function(){"undefined"!=typeof a.navigator?this.getElevationClient.apply(this,arguments):this.getElevationServer.apply(this,arguments)},getElevationClient:function(b,c){if(!a.google)throw new Error("Google maps api not loaded");if(0===b.length)return c(null,null);if(1===b.length)return c(new Error("getElevation requires at least 2 points."));for(var d=[],e=0;e0?c+=e:b+=Math.abs(e)}return{gain:b,loss:c}}};"undefined"!=typeof module&&"undefined"!=typeof module.exports?(b=require("geolib"),b.extend(c)):"function"==typeof define&&define.amd?define(["geolib"],function(a){return a.extend(c),a}):b.extend(c)}(this,this.geolib); \ No newline at end of file diff --git a/app/bower_components/geolib/dist/geolib.js b/app/bower_components/geolib/dist/geolib.js new file mode 100644 index 0000000..75834c8 --- /dev/null +++ b/app/bower_components/geolib/dist/geolib.js @@ -0,0 +1,1150 @@ +/*! geolib 2.0.9 by Manuel Bieh +* Library to provide geo functions like distance calculation, +* conversion of decimal coordinates to sexagesimal and vice versa, etc. +* WGS 84 (World Geodetic System 1984) +* +* @author Manuel Bieh +* @url http://www.manuelbieh.com/ +* @version 2.0.9 +* @license MIT +**/;(function(global, undefined) { + + "use strict"; + + function Geolib() {} + + // Setting readonly defaults + var geolib = Object.create(Geolib.prototype, { + version: { + value: "2.0.9" + }, + radius: { + value: 6378137 + }, + minLat: { + value: -90 + }, + maxLat: { + value: 90 + }, + minLon: { + value: -180 + }, + maxLon: { + value: 180 + }, + sexagesimalPattern: { + value: /^([0-9]{1,3})°\s*([0-9]{1,3}(?:\.(?:[0-9]{1,2}))?)'\s*(([0-9]{1,3}(\.([0-9]{1,2}))?)"\s*)?([NEOSW]?)$/ + }, + measures: { + value: Object.create(Object.prototype, { + "m" : {value: 1}, + "km": {value: 0.001}, + "cm": {value: 100}, + "mm": {value: 1000}, + "mi": {value: (1 / 1609.344)}, + "sm": {value: (1 / 1852.216)}, + "ft": {value: (100 / 30.48)}, + "in": {value: (100 / 2.54)}, + "yd": {value: (1 / 0.9144)} + }) + }, + prototype: { + value: Geolib.prototype + }, + extend: { + value: function(methods, overwrite) { + for(var prop in methods) { + if(typeof geolib.prototype[prop] === 'undefined' || overwrite === true) { + geolib.prototype[prop] = methods[prop]; + } + } + } + } + }); + + if (typeof(Number.prototype.toRad) === "undefined") { + Number.prototype.toRad = function() { + return this * Math.PI / 180; + }; + } + + if (typeof(Number.prototype.toDeg) === "undefined") { + Number.prototype.toDeg = function() { + return this * 180 / Math.PI; + }; + } + + // Here comes the magic + geolib.extend({ + + decimal: {}, + + sexagesimal: {}, + + distance: null, + + getKeys: function(point) { + + // GeoJSON Array [longitude, latitude(, elevation)] + if(Object.prototype.toString.call(point) == '[object Array]') { + + return { + longitude: point.length >= 1 ? 0 : undefined, + latitude: point.length >= 2 ? 1 : undefined, + elevation: point.length >= 3 ? 2 : undefined + }; + + } + + var getKey = function(possibleValues) { + + var key; + + possibleValues.every(function(val) { + // TODO: check if point is an object + if(typeof point != 'object') { + return true; + } + return point.hasOwnProperty(val) ? (function() { key = val; return false; }()) : true; + }); + + return key; + + }; + + var longitude = getKey(['lng', 'lon', 'longitude']); + var latitude = getKey(['lat', 'latitude']); + var elevation = getKey(['alt', 'altitude', 'elevation', 'elev']); + + // return undefined if not at least one valid property was found + if(typeof latitude == 'undefined' && + typeof longitude == 'undefined' && + typeof elevation == 'undefined') { + return undefined; + } + + return { + latitude: latitude, + longitude: longitude, + elevation: elevation + }; + + }, + + // returns latitude of a given point, converted to decimal + // set raw to true to avoid conversion + getLat: function(point, raw) { + return raw === true ? point[this.getKeys(point).latitude] : this.useDecimal(point[this.getKeys(point).latitude]); + }, + + // Alias for getLat + latitude: function(point) { + return this.getLat.call(this, point); + }, + + // returns longitude of a given point, converted to decimal + // set raw to true to avoid conversion + getLon: function(point, raw) { + return raw === true ? point[this.getKeys(point).longitude] : this.useDecimal(point[this.getKeys(point).longitude]); + }, + + // Alias for getLon + longitude: function(point) { + return this.getLon.call(this, point); + }, + + getElev: function(point) { + return point[this.getKeys(point).elevation]; + }, + + // Alias for getElev + elevation: function(point) { + return this.getElev.call(this, point); + }, + + coords: function(point, raw) { + + var retval = { + latitude: raw === true ? point[this.getKeys(point).latitude] : this.useDecimal(point[this.getKeys(point).latitude]), + longitude: raw === true ? point[this.getKeys(point).longitude] : this.useDecimal(point[this.getKeys(point).longitude]) + }; + + var elev = point[this.getKeys(point).elevation]; + + if(typeof elev !== 'undefined') { + retval['elevation'] = elev; + } + + return retval; + + }, + + // checks if a variable contains a valid latlong object + validate: function(point) { + + var keys = this.getKeys(point); + + if(typeof keys === 'undefined' || typeof keys.latitude === 'undefined' || keys.longitude === 'undefined') { + return false; + } + + var lat = point[keys.latitude]; + var lng = point[keys.longitude]; + + if(typeof lat === 'undefined' || !this.isDecimal(lat) && !this.isSexagesimal(lat)) { + return false; + } + + if(typeof lng === 'undefined' || !this.isDecimal(lng) && !this.isSexagesimal(lng)) { + return false; + } + + lat = this.useDecimal(lat); + lng = this.useDecimal(lng); + + if(lat < this.minLat || lat > this.maxLat || lng < this.minLon || lng > this.maxLon) { + return false; + } + + return true; + + }, + + /** + * Calculates geodetic distance between two points specified by latitude/longitude using + * Vincenty inverse formula for ellipsoids + * Vincenty Inverse Solution of Geodesics on the Ellipsoid (c) Chris Veness 2002-2010 + * (Licensed under CC BY 3.0) + * + * @param object Start position {latitude: 123, longitude: 123} + * @param object End position {latitude: 123, longitude: 123} + * @param integer Accuracy (in meters) + * @return integer Distance (in meters) + */ + getDistance: function(start, end, accuracy) { + + accuracy = Math.floor(accuracy) || 1; + + var s = this.coords(start); + var e = this.coords(end); + + var a = 6378137, b = 6356752.314245, f = 1/298.257223563; // WGS-84 ellipsoid params + var L = (e['longitude']-s['longitude']).toRad(); + + var cosSigma, sigma, sinAlpha, cosSqAlpha, cos2SigmaM, sinSigma; + + var U1 = Math.atan((1-f) * Math.tan(parseFloat(s['latitude']).toRad())); + var U2 = Math.atan((1-f) * Math.tan(parseFloat(e['latitude']).toRad())); + var sinU1 = Math.sin(U1), cosU1 = Math.cos(U1); + var sinU2 = Math.sin(U2), cosU2 = Math.cos(U2); + + var lambda = L, lambdaP, iterLimit = 100; + do { + var sinLambda = Math.sin(lambda), cosLambda = Math.cos(lambda); + sinSigma = ( + Math.sqrt( + ( + cosU2 * sinLambda + ) * ( + cosU2 * sinLambda + ) + ( + cosU1 * sinU2 - sinU1 * cosU2 * cosLambda + ) * ( + cosU1 * sinU2 - sinU1 * cosU2 * cosLambda + ) + ) + ); + if (sinSigma === 0) { + return geolib.distance = 0; // co-incident points + } + + cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda; + sigma = Math.atan2(sinSigma, cosSigma); + sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma; + cosSqAlpha = 1 - sinAlpha * sinAlpha; + cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha; + + if (isNaN(cos2SigmaM)) { + cos2SigmaM = 0; // equatorial line: cosSqAlpha=0 (§6) + } + var C = ( + f / 16 * cosSqAlpha * ( + 4 + f * ( + 4 - 3 * cosSqAlpha + ) + ) + ); + lambdaP = lambda; + lambda = ( + L + ( + 1 - C + ) * f * sinAlpha * ( + sigma + C * sinSigma * ( + cos2SigmaM + C * cosSigma * ( + -1 + 2 * cos2SigmaM * cos2SigmaM + ) + ) + ) + ); + + } while (Math.abs(lambda-lambdaP) > 1e-12 && --iterLimit>0); + + if (iterLimit === 0) { + return NaN; // formula failed to converge + } + + var uSq = ( + cosSqAlpha * ( + a * a - b * b + ) / ( + b*b + ) + ); + + var A = ( + 1 + uSq / 16384 * ( + 4096 + uSq * ( + -768 + uSq * ( + 320 - 175 * uSq + ) + ) + ) + ); + + var B = ( + uSq / 1024 * ( + 256 + uSq * ( + -128 + uSq * ( + 74-47 * uSq + ) + ) + ) + ); + + var deltaSigma = ( + B * sinSigma * ( + cos2SigmaM + B / 4 * ( + cosSigma * ( + -1 + 2 * cos2SigmaM * cos2SigmaM + ) -B / 6 * cos2SigmaM * ( + -3 + 4 * sinSigma * sinSigma + ) * ( + -3 + 4 * cos2SigmaM * cos2SigmaM + ) + ) + ) + ); + + var distance = b * A * (sigma - deltaSigma); + + distance = distance.toFixed(3); // round to 1mm precision + + //if (start.hasOwnProperty(elevation) && end.hasOwnProperty(elevation)) { + if (typeof this.elevation(start) !== 'undefined' && typeof this.elevation(end) !== 'undefined') { + var climb = Math.abs(this.elevation(start) - this.elevation(end)); + distance = Math.sqrt(distance * distance + climb * climb); + } + + return this.distance = Math.floor( + Math.round(distance / accuracy) * accuracy + ); + + /* + // note: to return initial/final bearings in addition to distance, use something like: + var fwdAz = Math.atan2(cosU2*sinLambda, cosU1*sinU2-sinU1*cosU2*cosLambda); + var revAz = Math.atan2(cosU1*sinLambda, -sinU1*cosU2+cosU1*sinU2*cosLambda); + + return { distance: s, initialBearing: fwdAz.toDeg(), finalBearing: revAz.toDeg() }; + */ + + }, + + + /** + * Calculates the distance between two spots. + * This method is more simple but also far more inaccurate + * + * @param object Start position {latitude: 123, longitude: 123} + * @param object End position {latitude: 123, longitude: 123} + * @param integer Accuracy (in meters) + * @return integer Distance (in meters) + */ + getDistanceSimple: function(start, end, accuracy) { + + accuracy = Math.floor(accuracy) || 1; + + var distance = + Math.round( + Math.acos( + Math.sin( + this.latitude(end).toRad() + ) * + Math.sin( + this.latitude(start).toRad() + ) + + Math.cos( + this.latitude(end).toRad() + ) * + Math.cos( + this.latitude(start).toRad() + ) * + Math.cos( + this.longitude(start).toRad() - this.longitude(end).toRad() + ) + ) * this.radius + ); + + return geolib.distance = Math.floor(Math.round(distance/accuracy)*accuracy); + + }, + + + /** + * Calculates the center of a collection of geo coordinates + * + * @param array Collection of coords [{latitude: 51.510, longitude: 7.1321}, {latitude: 49.1238, longitude: "8° 30' W"}, ...] + * @return object {latitude: centerLat, longitude: centerLng, distance: diagonalDistance} + */ + getCenter: function(coords) { + + if (!coords.length) { + return false; + } + + var max = function( array ){ + return Math.max.apply( Math, array ); + }; + + var min = function( array ){ + return Math.min.apply( Math, array ); + }; + + var latitude; + var longitude; + var splitCoords = {latitude: [], longitude: []}; + + for(var coord in coords) { + + splitCoords.latitude.push( + this.latitude(coords[coord]) + ); + + splitCoords.longitude.push( + this.longitude(coords[coord]) + ); + + } + + var minLat = min(splitCoords.latitude); + var minLon = min(splitCoords.longitude); + var maxLat = max(splitCoords.latitude); + var maxLon = max(splitCoords.longitude); + + latitude = ((minLat + maxLat)/2).toFixed(6); + longitude = ((minLon + maxLon)/2).toFixed(6); + + // distance from the deepest left to the highest right point (diagonal distance) + var distance = this.convertUnit('km', this.getDistance({latitude: minLat, longitude: minLon}, {latitude: maxLat, longitude: maxLon})); + + return { + latitude: latitude, + longitude: longitude, + distance: distance + }; + + }, + + + + /** + * Gets the max and min, latitude, longitude, and elevation (if provided). + * @param array array with coords e.g. [{latitude: 51.5143, longitude: 7.4138}, {latitude: 123, longitude: 123}, ...] + * @return object {maxLat: maxLat, + * minLat: minLat + * maxLng: maxLng, + * minLng: minLng, + * maxElev: maxElev, + * minElev: minElev} + */ + getBounds: function(coords) { + + if (!coords.length) { + return false; + } + + var useElevation = this.elevation(coords[0]); + + var stats = { + maxLat: -Infinity, + minLat: Infinity, + maxLng: -Infinity, + minLng: Infinity + }; + + if (typeof useElevation != 'undefined') { + stats.maxElev = 0; + stats.minElev = Infinity; + } + + for (var i = 0, l = coords.length; i < l; ++i) { + + stats.maxLat = Math.max(this.latitude(coords[i]), stats.maxLat); + stats.minLat = Math.min(this.latitude(coords[i]), stats.minLat); + stats.maxLng = Math.max(this.longitude(coords[i]), stats.maxLng); + stats.minLng = Math.min(this.longitude(coords[i]), stats.minLng); + + if (useElevation) { + stats.maxElev = Math.max(this.elevation(coords[i]), stats.maxElev); + stats.minElev = Math.min(this.elevation(coords[i]), stats.minElev); + } + + } + + return stats; + + }, + + + /** + * Computes the bounding coordinates of all points on the surface + * of the earth less than or equal to the specified great circle + * distance. + * + * @param object Point position {latitude: 123, longitude: 123} + * @param number Distance (in meters). + * @return array Collection of two points defining the SW and NE corners. + */ + getBoundsOfDistance: function(point, distance) { + + var latitude = this.latitude(point); + var longitude = this.longitude(point); + + var radLat = latitude.toRad(); + var radLon = longitude.toRad(); + + var radDist = distance / this.radius; + var minLat = radLat - radDist; + var maxLat = radLat + radDist; + + var MAX_LAT_RAD = this.maxLat.toRad(); + var MIN_LAT_RAD = this.minLat.toRad(); + var MAX_LON_RAD = this.maxLon.toRad(); + var MIN_LON_RAD = this.minLon.toRad(); + + var minLon; + var maxLon; + + if (minLat > MIN_LAT_RAD && maxLat < MAX_LAT_RAD) { + + var deltaLon = Math.asin(Math.sin(radDist) / Math.cos(radLat)); + minLon = radLon - deltaLon; + + if (minLon < MIN_LON_RAD) { + minLon += 2 * Math.PI; + } + + maxLon = radLon + deltaLon; + + if (maxLon > MAX_LON_RAD) { + maxLon -= 2 * Math.PI; + } + + } else { + // A pole is within the distance. + minLat = Math.max(minLat, MIN_LAT_RAD); + maxLat = Math.min(maxLat, MAX_LAT_RAD); + minLon = MIN_LON_RAD; + maxLon = MAX_LON_RAD; + } + + return [ + // Southwest + { + latitude: minLat.toDeg(), + longitude: minLon.toDeg() + }, + // Northeast + { + latitude: maxLat.toDeg(), + longitude: maxLon.toDeg() + } + ]; + + }, + + + /** + * Checks whether a point is inside of a polygon or not. + * Note that the polygon coords must be in correct order! + * + * @param object coordinate to check e.g. {latitude: 51.5023, longitude: 7.3815} + * @param array array with coords e.g. [{latitude: 51.5143, longitude: 7.4138}, {latitude: 123, longitude: 123}, ...] + * @return bool true if the coordinate is inside the given polygon + */ + isPointInside: function(latlng, coords) { + + for(var c = false, i = -1, l = coords.length, j = l - 1; ++i < l; j = i) { + + if( + ( + (this.longitude(coords[i]) <= this.longitude(latlng) && this.longitude(latlng) < this.longitude(coords[j])) || + (this.longitude(coords[j]) <= this.longitude(latlng) && this.longitude(latlng) < this.longitude(coords[i])) + ) && + ( + this.latitude(latlng) < (this.latitude(coords[j]) - this.latitude(coords[i])) * + (this.longitude(latlng) - this.longitude(coords[i])) / + (this.longitude(coords[j]) - this.longitude(coords[i])) + + this.latitude(coords[i]) + ) + ) { + c = !c; + } + + } + + return c; + + }, + + + /** + * Shortcut for geolib.isPointInside() + */ + isInside: function() { + return this.isPointInside.apply(this, arguments); + }, + + + /** + * Checks whether a point is inside of a circle or not. + * + * @param object coordinate to check (e.g. {latitude: 51.5023, longitude: 7.3815}) + * @param object coordinate of the circle's center (e.g. {latitude: 51.4812, longitude: 7.4025}) + * @param integer maximum radius in meters + * @return bool true if the coordinate is within the given radius + */ + isPointInCircle: function(latlng, center, radius) { + return this.getDistance(latlng, center) < radius; + }, + + + /** + * Shortcut for geolib.isPointInCircle() + */ + withinRadius: function() { + return this.isPointInCircle.apply(this, arguments); + }, + + + /** + * Gets rhumb line bearing of two points. Find out about the difference between rhumb line and + * great circle bearing on Wikipedia. It's quite complicated. Rhumb line should be fine in most cases: + * + * http://en.wikipedia.org/wiki/Rhumb_line#General_and_mathematical_description + * + * Function heavily based on Doug Vanderweide's great PHP version (licensed under GPL 3.0) + * http://www.dougv.com/2009/07/13/calculating-the-bearing-and-compass-rose-direction-between-two-latitude-longitude-coordinates-in-php/ + * + * @param object origin coordinate (e.g. {latitude: 51.5023, longitude: 7.3815}) + * @param object destination coordinate + * @return integer calculated bearing + */ + getRhumbLineBearing: function(originLL, destLL) { + + // difference of longitude coords + var diffLon = this.longitude(destLL).toRad() - this.longitude(originLL).toRad(); + + // difference latitude coords phi + var diffPhi = Math.log( + Math.tan( + this.latitude(destLL).toRad() / 2 + Math.PI / 4 + ) / + Math.tan( + this.latitude(originLL).toRad() / 2 + Math.PI / 4 + ) + ); + + // recalculate diffLon if it is greater than pi + if(Math.abs(diffLon) > Math.PI) { + if(diffLon > 0) { + diffLon = (2 * Math.PI - diffLon) * -1; + } + else { + diffLon = 2 * Math.PI + diffLon; + } + } + + //return the angle, normalized + return (Math.atan2(diffLon, diffPhi).toDeg() + 360) % 360; + + }, + + + /** + * Gets great circle bearing of two points. See description of getRhumbLineBearing for more information + * + * @param object origin coordinate (e.g. {latitude: 51.5023, longitude: 7.3815}) + * @param object destination coordinate + * @return integer calculated bearing + */ + getBearing: function(originLL, destLL) { + + destLL['latitude'] = this.latitude(destLL); + destLL['longitude'] = this.longitude(destLL); + originLL['latitude'] = this.latitude(originLL); + originLL['longitude'] = this.longitude(originLL); + + var bearing = ( + ( + Math.atan2( + Math.sin( + destLL['longitude'].toRad() - + originLL['longitude'].toRad() + ) * + Math.cos( + destLL['latitude'].toRad() + ), + Math.cos( + originLL['latitude'].toRad() + ) * + Math.sin( + destLL['latitude'].toRad() + ) - + Math.sin( + originLL['latitude'].toRad() + ) * + Math.cos( + destLL['latitude'].toRad() + ) * + Math.cos( + destLL['longitude'].toRad() - originLL['longitude'].toRad() + ) + ) + ).toDeg() + 360 + ) % 360; + + return bearing; + + }, + + + /** + * Gets the compass direction from an origin coordinate to a destination coordinate. + * + * @param object origin coordinate (e.g. {latitude: 51.5023, longitude: 7.3815}) + * @param object destination coordinate + * @param string Bearing mode. Can be either circle or rhumbline + * @return object Returns an object with a rough (NESW) and an exact direction (NNE, NE, ENE, E, ESE, etc). + */ + getCompassDirection: function(originLL, destLL, bearingMode) { + + var direction; + var bearing; + + if(bearingMode == 'circle') { + // use great circle bearing + bearing = this.getBearing(originLL, destLL); + } else { + // default is rhumb line bearing + bearing = this.getRhumbLineBearing(originLL, destLL); + } + + switch(Math.round(bearing/22.5)) { + case 1: + direction = {exact: "NNE", rough: "N"}; + break; + case 2: + direction = {exact: "NE", rough: "N"}; + break; + case 3: + direction = {exact: "ENE", rough: "E"}; + break; + case 4: + direction = {exact: "E", rough: "E"}; + break; + case 5: + direction = {exact: "ESE", rough: "E"}; + break; + case 6: + direction = {exact: "SE", rough: "E"}; + break; + case 7: + direction = {exact: "SSE", rough: "S"}; + break; + case 8: + direction = {exact: "S", rough: "S"}; + break; + case 9: + direction = {exact: "SSW", rough: "S"}; + break; + case 10: + direction = {exact: "SW", rough: "S"}; + break; + case 11: + direction = {exact: "WSW", rough: "W"}; + break; + case 12: + direction = {exact: "W", rough: "W"}; + break; + case 13: + direction = {exact: "WNW", rough: "W"}; + break; + case 14: + direction = {exact: "NW", rough: "W"}; + break; + case 15: + direction = {exact: "NNW", rough: "N"}; + break; + default: + direction = {exact: "N", rough: "N"}; + } + + direction['bearing'] = bearing; + return direction; + + }, + + + /** + * Shortcut for getCompassDirection + */ + getDirection: function(originLL, destLL, bearingMode) { + return this.getCompassDirection.apply(this, arguments); + }, + + + /** + * Sorts an array of coords by distance from a reference coordinate + * + * @param object reference coordinate e.g. {latitude: 51.5023, longitude: 7.3815} + * @param mixed array or object with coords [{latitude: 51.5143, longitude: 7.4138}, {latitude: 123, longitude: 123}, ...] + * @return array ordered array + */ + orderByDistance: function(latlng, coords) { + + var coordsArray = []; + + for(var coord in coords) { + + var d = this.getDistance(latlng, coords[coord]); + + coordsArray.push({ + key: coord, + latitude: this.latitude(coords[coord]), + longitude: this.longitude(coords[coord]), + distance: d + }); + + } + + return coordsArray.sort(function(a, b) { return a.distance - b.distance; }); + + }, + + + /** + * Finds the nearest coordinate to a reference coordinate + * + * @param object reference coordinate e.g. {latitude: 51.5023, longitude: 7.3815} + * @param mixed array or object with coords [{latitude: 51.5143, longitude: 7.4138}, {latitude: 123, longitude: 123}, ...] + * @return array ordered array + */ + findNearest: function(latlng, coords, offset, limit) { + + offset = offset || 0; + limit = limit || 1; + var ordered = this.orderByDistance(latlng, coords); + + if(limit === 1) { + return ordered[offset]; + } else { + return ordered.splice(offset, limit); + } + + }, + + + /** + * Calculates the length of a given path + * + * @param mixed array or object with coords [{latitude: 51.5143, longitude: 7.4138}, {latitude: 123, longitude: 123}, ...] + * @return integer length of the path (in meters) + */ + getPathLength: function(coords) { + + var dist = 0; + var last; + + for (var i = 0, l = coords.length; i < l; ++i) { + if(last) { + //console.log(coords[i], last, this.getDistance(coords[i], last)); + dist += this.getDistance(this.coords(coords[i]), last); + } + last = this.coords(coords[i]); + } + + return dist; + + }, + + + /** + * Calculates the speed between to points within a given time span. + * + * @param object coords with javascript timestamp {latitude: 51.5143, longitude: 7.4138, time: 1360231200880} + * @param object coords with javascript timestamp {latitude: 51.5502, longitude: 7.4323, time: 1360245600460} + * @param object options (currently "unit" is the only option. Default: km(h)); + * @return float speed in unit per hour + */ + getSpeed: function(start, end, options) { + + var unit = options && options.unit || 'km'; + + if(unit == 'mph') { + unit = 'mi'; + } else if(unit == 'kmh') { + unit = 'km'; + } + + var distance = geolib.getDistance(start, end); + var time = ((end.time*1)/1000) - ((start.time*1)/1000); + var mPerHr = (distance/time)*3600; + var speed = Math.round(mPerHr * this.measures[unit] * 10000)/10000; + return speed; + + }, + + + /** + * Converts a distance from meters to km, mm, cm, mi, ft, in or yd + * + * @param string Format to be converted in + * @param float Distance in meters + * @param float Decimal places for rounding (default: 4) + * @return float Converted distance + */ + convertUnit: function(unit, distance, round) { + + if(distance === 0 || typeof distance === 'undefined') { + + if(this.distance === 0) { + // throw 'No distance given.'; + return 0; + } else { + distance = this.distance; + } + + } + + unit = unit || 'm'; + round = (null == round ? 4 : round); + + if(typeof this.measures[unit] !== 'undefined') { + return this.round(distance * this.measures[unit], round); + } else { + throw new Error('Unknown unit for conversion.'); + } + + }, + + + /** + * Checks if a value is in decimal format or, if neccessary, converts to decimal + * + * @param mixed Value(s) to be checked/converted (array of latlng objects, latlng object, sexagesimal string, float) + * @return float Input data in decimal format + */ + useDecimal: function(value) { + + if(Object.prototype.toString.call(value) === '[object Array]') { + + var geolib = this; + + value = value.map(function(val) { + + //if(!isNaN(parseFloat(val))) { + if(geolib.isDecimal(val)) { + + return geolib.useDecimal(val); + + } else if(typeof val == 'object') { + + if(geolib.validate(val)) { + + return geolib.coords(val); + + } else { + + for(var prop in val) { + val[prop] = geolib.useDecimal(val[prop]); + } + + return val; + + } + + } else if(geolib.isSexagesimal(val)) { + + return geolib.sexagesimal2decimal(val); + + } else { + + return val; + + } + + }); + + return value; + + } else if(typeof value === 'object' && this.validate(value)) { + + return this.coords(value); + + } else if(typeof value === 'object') { + + for(var prop in value) { + value[prop] = this.useDecimal(value[prop]); + } + + return value; + + } + + + if (this.isDecimal(value)) { + + return parseFloat(value); + + } else if(this.isSexagesimal(value) === true) { + + return parseFloat(this.sexagesimal2decimal(value)); + + } + + throw new Error('Unknown format.'); + + }, + + /** + * Converts a decimal coordinate value to sexagesimal format + * + * @param float decimal + * @return string Sexagesimal value (XX° YY' ZZ") + */ + decimal2sexagesimal: function(dec) { + + if (dec in this.sexagesimal) { + return this.sexagesimal[dec]; + } + + var tmp = dec.toString().split('.'); + + var deg = Math.abs(tmp[0]); + var min = ('0.' + tmp[1])*60; + var sec = min.toString().split('.'); + + min = Math.floor(min); + sec = (('0.' + sec[1]) * 60).toFixed(2); + + this.sexagesimal[dec] = (deg + '° ' + min + "' " + sec + '"'); + + return this.sexagesimal[dec]; + + }, + + + /** + * Converts a sexagesimal coordinate to decimal format + * + * @param float Sexagesimal coordinate + * @return string Decimal value (XX.XXXXXXXX) + */ + sexagesimal2decimal: function(sexagesimal) { + + if (sexagesimal in this.decimal) { + return this.decimal[sexagesimal]; + } + + var regEx = new RegExp(this.sexagesimalPattern); + var data = regEx.exec(sexagesimal); + var min = 0, sec = 0; + + if(data) { + min = parseFloat(data[2]/60); + sec = parseFloat(data[4]/3600) || 0; + } + + var dec = ((parseFloat(data[1]) + min + sec)).toFixed(8); + //var dec = ((parseFloat(data[1]) + min + sec)); + + // South and West are negative decimals + dec = (data[7] == 'S' || data[7] == 'W') ? parseFloat(-dec) : parseFloat(dec); + //dec = (data[7] == 'S' || data[7] == 'W') ? -dec : dec; + + this.decimal[sexagesimal] = dec; + + return dec; + + }, + + + /** + * Checks if a value is in decimal format + * + * @param string Value to be checked + * @return bool True if in sexagesimal format + */ + isDecimal: function(value) { + + value = value.toString().replace(/\s*/, ''); + + // looks silly but works as expected + // checks if value is in decimal format + return (!isNaN(parseFloat(value)) && parseFloat(value) == value); + + }, + + + /** + * Checks if a value is in sexagesimal format + * + * @param string Value to be checked + * @return bool True if in sexagesimal format + */ + isSexagesimal: function(value) { + + value = value.toString().replace(/\s*/, ''); + + return this.sexagesimalPattern.test(value); + + }, + + round: function(value, n) { + var decPlace = Math.pow(10, n); + return Math.round(value * decPlace)/decPlace; + } + + }); + + // Node module + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + + global.geolib = module.exports = geolib; + + // AMD module + } else if (typeof define === "function" && define.amd) { + + define("geolib", [], function () { + return geolib; + }); + + // we're in a browser + } else { + + global.geolib = geolib; + + } + +}(this)); \ No newline at end of file diff --git a/app/bower_components/geolib/dist/geolib.min.js b/app/bower_components/geolib/dist/geolib.min.js new file mode 100644 index 0000000..eddddf9 --- /dev/null +++ b/app/bower_components/geolib/dist/geolib.min.js @@ -0,0 +1,11 @@ +/*! geolib 2.0.9 by Manuel Bieh +* Library to provide geo functions like distance calculation, +* conversion of decimal coordinates to sexagesimal and vice versa, etc. +* WGS 84 (World Geodetic System 1984) +* +* @author Manuel Bieh +* @url http://www.manuelbieh.com/ +* @version 2.0.9 +* @license MIT +**/ +!function(a,b){"use strict";function c(){}var d=Object.create(c.prototype,{version:{value:"2.0.9"},radius:{value:6378137},minLat:{value:-90},maxLat:{value:90},minLon:{value:-180},maxLon:{value:180},sexagesimalPattern:{value:/^([0-9]{1,3})°\s*([0-9]{1,3}(?:\.(?:[0-9]{1,2}))?)'\s*(([0-9]{1,3}(\.([0-9]{1,2}))?)"\s*)?([NEOSW]?)$/},measures:{value:Object.create(Object.prototype,{m:{value:1},km:{value:.001},cm:{value:100},mm:{value:1e3},mi:{value:1/1609.344},sm:{value:1/1852.216},ft:{value:100/30.48},"in":{value:100/2.54},yd:{value:1/.9144}})},prototype:{value:c.prototype},extend:{value:function(a,b){for(var c in a)("undefined"==typeof d.prototype[c]||b===!0)&&(d.prototype[c]=a[c])}}});"undefined"==typeof Number.prototype.toRad&&(Number.prototype.toRad=function(){return this*Math.PI/180}),"undefined"==typeof Number.prototype.toDeg&&(Number.prototype.toDeg=function(){return 180*this/Math.PI}),d.extend({decimal:{},sexagesimal:{},distance:null,getKeys:function(a){if("[object Array]"==Object.prototype.toString.call(a))return{longitude:a.length>=1?0:b,latitude:a.length>=2?1:b,elevation:a.length>=3?2:b};var c=function(b){var c;return b.every(function(b){return"object"!=typeof a?!0:a.hasOwnProperty(b)?function(){return c=b,!1}():!0}),c},d=c(["lng","lon","longitude"]),e=c(["lat","latitude"]),f=c(["alt","altitude","elevation","elev"]);return"undefined"==typeof e&&"undefined"==typeof d&&"undefined"==typeof f?b:{latitude:e,longitude:d,elevation:f}},getLat:function(a,b){return b===!0?a[this.getKeys(a).latitude]:this.useDecimal(a[this.getKeys(a).latitude])},latitude:function(a){return this.getLat.call(this,a)},getLon:function(a,b){return b===!0?a[this.getKeys(a).longitude]:this.useDecimal(a[this.getKeys(a).longitude])},longitude:function(a){return this.getLon.call(this,a)},getElev:function(a){return a[this.getKeys(a).elevation]},elevation:function(a){return this.getElev.call(this,a)},coords:function(a,b){var c={latitude:b===!0?a[this.getKeys(a).latitude]:this.useDecimal(a[this.getKeys(a).latitude]),longitude:b===!0?a[this.getKeys(a).longitude]:this.useDecimal(a[this.getKeys(a).longitude])},d=a[this.getKeys(a).elevation];return"undefined"!=typeof d&&(c.elevation=d),c},validate:function(a){var b=this.getKeys(a);if("undefined"==typeof b||"undefined"==typeof b.latitude||"undefined"===b.longitude)return!1;var c=a[b.latitude],d=a[b.longitude];return"undefined"==typeof c||!this.isDecimal(c)&&!this.isSexagesimal(c)?!1:"undefined"==typeof d||!this.isDecimal(d)&&!this.isSexagesimal(d)?!1:(c=this.useDecimal(c),d=this.useDecimal(d),cthis.maxLat||dthis.maxLon?!1:!0)},getDistance:function(a,b,c){c=Math.floor(c)||1;var e,f,g,h,i,j,k,l=this.coords(a),m=this.coords(b),n=6378137,o=6356752.314245,p=1/298.257223563,q=(m.longitude-l.longitude).toRad(),r=Math.atan((1-p)*Math.tan(parseFloat(l.latitude).toRad())),s=Math.atan((1-p)*Math.tan(parseFloat(m.latitude).toRad())),t=Math.sin(r),u=Math.cos(r),v=Math.sin(s),w=Math.cos(s),x=q,y=100;do{var z=Math.sin(x),A=Math.cos(x);if(j=Math.sqrt(w*z*w*z+(u*v-t*w*A)*(u*v-t*w*A)),0===j)return d.distance=0;e=t*v+u*w*A,f=Math.atan2(j,e),g=u*w*z/j,h=1-g*g,i=e-2*t*v/h,isNaN(i)&&(i=0);var B=p/16*h*(4+p*(4-3*h));k=x,x=q+(1-B)*p*g*(f+B*j*(i+B*e*(-1+2*i*i)))}while(Math.abs(x-k)>1e-12&&--y>0);if(0===y)return 0/0;var C=h*(n*n-o*o)/(o*o),D=1+C/16384*(4096+C*(-768+C*(320-175*C))),E=C/1024*(256+C*(-128+C*(74-47*C))),F=E*j*(i+E/4*(e*(-1+2*i*i)-E/6*i*(-3+4*j*j)*(-3+4*i*i))),G=o*D*(f-F);if(G=G.toFixed(3),"undefined"!=typeof this.elevation(a)&&"undefined"!=typeof this.elevation(b)){var H=Math.abs(this.elevation(a)-this.elevation(b));G=Math.sqrt(G*G+H*H)}return this.distance=Math.floor(Math.round(G/c)*c)},getDistanceSimple:function(a,b,c){c=Math.floor(c)||1;var e=Math.round(Math.acos(Math.sin(this.latitude(b).toRad())*Math.sin(this.latitude(a).toRad())+Math.cos(this.latitude(b).toRad())*Math.cos(this.latitude(a).toRad())*Math.cos(this.longitude(a).toRad()-this.longitude(b).toRad()))*this.radius);return d.distance=Math.floor(Math.round(e/c)*c)},getCenter:function(a){if(!a.length)return!1;var b,c,d=function(a){return Math.max.apply(Math,a)},e=function(a){return Math.min.apply(Math,a)},f={latitude:[],longitude:[]};for(var g in a)f.latitude.push(this.latitude(a[g])),f.longitude.push(this.longitude(a[g]));var h=e(f.latitude),i=e(f.longitude),j=d(f.latitude),k=d(f.longitude);b=((h+j)/2).toFixed(6),c=((i+k)/2).toFixed(6);var l=this.convertUnit("km",this.getDistance({latitude:h,longitude:i},{latitude:j,longitude:k}));return{latitude:b,longitude:c,distance:l}},getBounds:function(a){if(!a.length)return!1;var b=this.elevation(a[0]),c={maxLat:-1/0,minLat:1/0,maxLng:-1/0,minLng:1/0};"undefined"!=typeof b&&(c.maxElev=0,c.minElev=1/0);for(var d=0,e=a.length;e>d;++d)c.maxLat=Math.max(this.latitude(a[d]),c.maxLat),c.minLat=Math.min(this.latitude(a[d]),c.minLat),c.maxLng=Math.max(this.longitude(a[d]),c.maxLng),c.minLng=Math.min(this.longitude(a[d]),c.minLng),b&&(c.maxElev=Math.max(this.elevation(a[d]),c.maxElev),c.minElev=Math.min(this.elevation(a[d]),c.minElev));return c},getBoundsOfDistance:function(a,b){var c,d,e=this.latitude(a),f=this.longitude(a),g=e.toRad(),h=f.toRad(),i=b/this.radius,j=g-i,k=g+i,l=this.maxLat.toRad(),m=this.minLat.toRad(),n=this.maxLon.toRad(),o=this.minLon.toRad();if(j>m&&l>k){var p=Math.asin(Math.sin(i)/Math.cos(g));c=h-p,o>c&&(c+=2*Math.PI),d=h+p,d>n&&(d-=2*Math.PI)}else j=Math.max(j,m),k=Math.min(k,l),c=o,d=n;return[{latitude:j.toDeg(),longitude:c.toDeg()},{latitude:k.toDeg(),longitude:d.toDeg()}]},isPointInside:function(a,b){for(var c=!1,d=-1,e=b.length,f=e-1;++dMath.PI&&(c=c>0?-1*(2*Math.PI-c):2*Math.PI+c),(Math.atan2(c,d).toDeg()+360)%360},getBearing:function(a,b){b.latitude=this.latitude(b),b.longitude=this.longitude(b),a.latitude=this.latitude(a),a.longitude=this.longitude(a);var c=(Math.atan2(Math.sin(b.longitude.toRad()-a.longitude.toRad())*Math.cos(b.latitude.toRad()),Math.cos(a.latitude.toRad())*Math.sin(b.latitude.toRad())-Math.sin(a.latitude.toRad())*Math.cos(b.latitude.toRad())*Math.cos(b.longitude.toRad()-a.longitude.toRad())).toDeg()+360)%360;return c},getCompassDirection:function(a,b,c){var d,e;switch(e="circle"==c?this.getBearing(a,b):this.getRhumbLineBearing(a,b),Math.round(e/22.5)){case 1:d={exact:"NNE",rough:"N"};break;case 2:d={exact:"NE",rough:"N"};break;case 3:d={exact:"ENE",rough:"E"};break;case 4:d={exact:"E",rough:"E"};break;case 5:d={exact:"ESE",rough:"E"};break;case 6:d={exact:"SE",rough:"E"};break;case 7:d={exact:"SSE",rough:"S"};break;case 8:d={exact:"S",rough:"S"};break;case 9:d={exact:"SSW",rough:"S"};break;case 10:d={exact:"SW",rough:"S"};break;case 11:d={exact:"WSW",rough:"W"};break;case 12:d={exact:"W",rough:"W"};break;case 13:d={exact:"WNW",rough:"W"};break;case 14:d={exact:"NW",rough:"W"};break;case 15:d={exact:"NNW",rough:"N"};break;default:d={exact:"N",rough:"N"}}return d.bearing=e,d},getDirection:function(){return this.getCompassDirection.apply(this,arguments)},orderByDistance:function(a,b){var c=[];for(var d in b){var e=this.getDistance(a,b[d]);c.push({key:d,latitude:this.latitude(b[d]),longitude:this.longitude(b[d]),distance:e})}return c.sort(function(a,b){return a.distance-b.distance})},findNearest:function(a,b,c,d){c=c||0,d=d||1;var e=this.orderByDistance(a,b);return 1===d?e[c]:e.splice(c,d)},getPathLength:function(a){for(var b,c=0,d=0,e=a.length;e>d;++d)b&&(c+=this.getDistance(this.coords(a[d]),b)),b=this.coords(a[d]);return c},getSpeed:function(a,b,c){var e=c&&c.unit||"km";"mph"==e?e="mi":"kmh"==e&&(e="km");var f=d.getDistance(a,b),g=1*b.time/1e3-1*a.time/1e3,h=f/g*3600,i=Math.round(h*this.measures[e]*1e4)/1e4;return i},convertUnit:function(a,b,c){if(0===b||"undefined"==typeof b){if(0===this.distance)return 0;b=this.distance}if(a=a||"m",c=null==c?4:c,"undefined"!=typeof this.measures[a])return this.round(b*this.measures[a],c);throw new Error("Unknown unit for conversion.")},useDecimal:function(a){if("[object Array]"===Object.prototype.toString.call(a)){var b=this;return a=a.map(function(a){if(b.isDecimal(a))return b.useDecimal(a);if("object"==typeof a){if(b.validate(a))return b.coords(a);for(var c in a)a[c]=b.useDecimal(a[c]);return a}return b.isSexagesimal(a)?b.sexagesimal2decimal(a):a})}if("object"==typeof a&&this.validate(a))return this.coords(a);if("object"==typeof a){for(var c in a)a[c]=this.useDecimal(a[c]);return a}if(this.isDecimal(a))return parseFloat(a);if(this.isSexagesimal(a)===!0)return parseFloat(this.sexagesimal2decimal(a));throw new Error("Unknown format.")},decimal2sexagesimal:function(a){if(a in this.sexagesimal)return this.sexagesimal[a];var b=a.toString().split("."),c=Math.abs(b[0]),d=60*("0."+b[1]),e=d.toString().split(".");return d=Math.floor(d),e=(60*("0."+e[1])).toFixed(2),this.sexagesimal[a]=c+"° "+d+"' "+e+'"',this.sexagesimal[a]},sexagesimal2decimal:function(a){if(a in this.decimal)return this.decimal[a];var b=new RegExp(this.sexagesimalPattern),c=b.exec(a),d=0,e=0;c&&(d=parseFloat(c[2]/60),e=parseFloat(c[4]/3600)||0);var f=(parseFloat(c[1])+d+e).toFixed(8);return f=parseFloat("S"==c[7]||"W"==c[7]?-f:f),this.decimal[a]=f,f},isDecimal:function(a){return a=a.toString().replace(/\s*/,""),!isNaN(parseFloat(a))&&parseFloat(a)==a},isSexagesimal:function(a){return a=a.toString().replace(/\s*/,""),this.sexagesimalPattern.test(a)},round:function(a,b){var c=Math.pow(10,b);return Math.round(a*c)/c}}),"undefined"!=typeof module&&"undefined"!=typeof module.exports?a.geolib=module.exports=d:"function"==typeof define&&define.amd?define("geolib",[],function(){return d}):a.geolib=d}(this); \ No newline at end of file diff --git a/app/bower_components/geolib/earth.png b/app/bower_components/geolib/earth.png new file mode 100644 index 0000000..6cc1118 Binary files /dev/null and b/app/bower_components/geolib/earth.png differ diff --git a/app/bower_components/geolib/index.html b/app/bower_components/geolib/index.html new file mode 100644 index 0000000..737644e --- /dev/null +++ b/app/bower_components/geolib/index.html @@ -0,0 +1,498 @@ + + + +Geolocation + + + + + + + + + + + + + + + + +Fork me on GitHub + +
    + +
    + +
    +
    + + + +

    geolib.js

    +

    Settings

    + +

    Show all distances in + . +

    +
    + +

    Documentation

    + +
    +
    + +
    +
    +

    Distance calculation

    +

    Calculate distance from
    to
    .

    +

    +
    +
    + +
    +
    +

    Custom distance calculation

    + +

    + Calculate distance from
    + +
    + to
    + + . +

    + + + +

    +
    +
    + +
    +
    +

    What's near?

    +

    Static example

    +

    + Show what's the nearest to +

    +

    +
    +
    + +
    +
    +

    Geolocation example

    +

    Show me some cities near me (uses W3C Geolocation API)
    +

    +
    +
    +
    + +
    +
    +

    Format conversion

    +

    + Convert sexagesimal to decimal
    + +
    + +

    +

    + Convert decimal to sexagesimal
    + +
    + +

    +
    +
    + +
    + +
    + + + + + \ No newline at end of file diff --git a/app/bower_components/geolib/package.json b/app/bower_components/geolib/package.json new file mode 100644 index 0000000..5830e6f --- /dev/null +++ b/app/bower_components/geolib/package.json @@ -0,0 +1,51 @@ +{ + "name": "geolib", + "homepage": "http://github.com/manuelbieh/Geolib", + "author": { + "name": "Manuel Bieh", + "url": "http://www.manuelbieh.com/" + }, + "repository": { + "type": "git", + "url": "http://github.com/manuelbieh/geolib.git" + }, + "devDependencies": { + "grunt": "~0.4", + "grunt-cli": "*", + "grunt-contrib-uglify": "~0.2", + "grunt-contrib-concat": "~0.3", + "grunt-contrib-clean": "~0.5.0", + "grunt-contrib-copy": "~0.4", + "grunt-contrib-qunit": "~0.2.0", + "grunt-text-replace": "~0.3.6", + "grunt-contrib-jshint": "~0.6.2", + "phantomjs": "~1.8.0", + "grunt-jslint": "~1.0.0", + "time-grunt": "~0.3.1", + "load-grunt-tasks": "~0.4.0" + }, + "licenses": [ + { + "type": "MIT", + "url": "http://opensource.org/licenses/MIT" + } + ], + "files": [ + "geolib.js" + ], + "description": "Library to perform geo specific tasks", + "keywords": [ + "geolocation", + "geo", + "distance", + "geojson", + "geospatial", + "lbs", + "location" + ], + "scripts": { + "test": "grunt travis --verbose" + }, + "version": "2.0.9", + "main": "./geolib" +} \ No newline at end of file diff --git a/app/bower_components/geolib/src/geolib.elevation.js b/app/bower_components/geolib/src/geolib.elevation.js new file mode 100644 index 0000000..2dcc2d0 --- /dev/null +++ b/app/bower_components/geolib/src/geolib.elevation.js @@ -0,0 +1,190 @@ +/*! geolib.elevation $version$ by Manuel Bieh +* +* Elevation Addon for Geolib.js +* +* @author Manuel Bieh +* @url http://www.manuelbieh.com/ +* @version $version$ +* @license MIT +*/ +;(function(global, geolib, undefined) { + + var elevation = { + + /*global google:true geolib:true require:true module:true elevationResult:true */ + + /** + * @param Array Collection of coords [{latitude: 51.510, longitude: 7.1321}, {latitude: 49.1238, longitude: "8° 30' W"}, ...] + * @return Array [{lat:#lat, lng:#lng, elev:#elev},....]} + */ + getElevation: function() { + if (typeof global.navigator !== 'undefined') { + this.getElevationClient.apply(this, arguments); + } else { + this.getElevationServer.apply(this, arguments); + } + }, + + + /* Optional elevation addon requires Googlemaps API JS */ + getElevationClient: function(coords, cb) { + + if (!global.google) { + throw new Error("Google maps api not loaded"); + } + + if (coords.length === 0) { + return cb(null, null); + } + + if (coords.length === 1) { + return cb(new Error("getElevation requires at least 2 points.")); + } + + var path = []; + + for(var i = 0; i < coords.length; i++) { + path.push(new google.maps.LatLng( + this.latitude(coords[i]), + this.longitude(coords[i]) + )); + } + + var positionalRequest = { + 'path': path, + 'samples': path.length + }; + + var elevationService = new google.maps.ElevationService(); + var geolib = this; + + elevationService.getElevationAlongPath(positionalRequest, function (results, status) { + geolib.elevationHandler(results, status, coords, cb); + }); + + }, + + + getElevationServer: function(coords, cb) { + + if (coords.length === 0) { + return cb(null, null); + } + + if (coords.length === 1) { + return cb(new Error("getElevation requires at least 2 points.")); + } + + var gm = require('googlemaps'); + var path = []; + + for(var i = 0; i < coords.length; i++) { + path.push( + this.latitude(coords[i]) + ',' + this.longitude(coords[i]) + ); + } + + var geolib = this; + + gm.elevationFromPath(path.join('|'), path.length, function(err, results) { + geolib.elevationHandler(results.results, results.status, coords, cb); + }); + + }, + + + elevationHandler: function(results, status, coords, cb) { + + var latsLngsElevs = []; + + if (status == "OK" ) { + + for (var i = 0; i < results.length; i++) { + latsLngsElevs.push({ + "lat": this.latitude(coords[i]), + "lng": this.longitude(coords[i]), + "elev":results[i].elevation + }); + } + + cb(null, latsLngsElevs); + + } else { + + cb(new Error("Could not get elevation using Google's API"), elevationResult.status); + + } + + }, + + + /** + * @param Array [{lat:#lat, lng:#lng, elev:#elev},....]} + * @return Number % grade + */ + getGrade: function(coords) { + + var rise = Math.abs( + this.elevation(coords[coords.length-1]) - this.elevation(coords[0]) + ); + + var run = this.getPathLength(coords); + + return Math.floor((rise/run)*100); + + }, + + + /** + * @param Array [{lat:#lat, lng:#lng, elev:#elev},....]} + * @return Object {gain:#gain, loss:#loss} + */ + getTotalElevationGainAndLoss: function(coords) { + + var gain = 0; + var loss = 0; + + for(var i = 0; i < coords.length - 1; i++) { + + var deltaElev = this.elevation(coords[i]) - this.elevation(coords[i + 1]); + + if (deltaElev > 0) { + loss += deltaElev; + } else { + gain += Math.abs(deltaElev); + } + + } + + return { + "gain": gain, + "loss": loss + }; + + } + + }; + + // Node module + if (typeof module !== 'undefined' && + typeof module.exports !== 'undefined') { + + geolib = require('geolib'); + geolib.extend(elevation); + + // AMD module + } else if (typeof define === "function" && define.amd) { + + define(["geolib"], function (geolib) { + geolib.extend(elevation); + return geolib; + }); + + // we're in a browser + } else { + + geolib.extend(elevation); + + } + +}(this, this.geolib)); \ No newline at end of file diff --git a/app/bower_components/geolib/src/geolib.js b/app/bower_components/geolib/src/geolib.js new file mode 100644 index 0000000..a77d249 --- /dev/null +++ b/app/bower_components/geolib/src/geolib.js @@ -0,0 +1,1141 @@ +;(function(global, undefined) { + + "use strict"; + + function Geolib() {} + + // Setting readonly defaults + var geolib = Object.create(Geolib.prototype, { + version: { + value: "$version$" + }, + radius: { + value: 6378137 + }, + minLat: { + value: -90 + }, + maxLat: { + value: 90 + }, + minLon: { + value: -180 + }, + maxLon: { + value: 180 + }, + sexagesimalPattern: { + value: /^([0-9]{1,3})°\s*([0-9]{1,3}(?:\.(?:[0-9]{1,2}))?)'\s*(([0-9]{1,3}(\.([0-9]{1,2}))?)"\s*)?([NEOSW]?)$/ + }, + measures: { + value: Object.create(Object.prototype, { + "m" : {value: 1}, + "km": {value: 0.001}, + "cm": {value: 100}, + "mm": {value: 1000}, + "mi": {value: (1 / 1609.344)}, + "sm": {value: (1 / 1852.216)}, + "ft": {value: (100 / 30.48)}, + "in": {value: (100 / 2.54)}, + "yd": {value: (1 / 0.9144)} + }) + }, + prototype: { + value: Geolib.prototype + }, + extend: { + value: function(methods, overwrite) { + for(var prop in methods) { + if(typeof geolib.prototype[prop] === 'undefined' || overwrite === true) { + geolib.prototype[prop] = methods[prop]; + } + } + } + } + }); + + if (typeof(Number.prototype.toRad) === "undefined") { + Number.prototype.toRad = function() { + return this * Math.PI / 180; + }; + } + + if (typeof(Number.prototype.toDeg) === "undefined") { + Number.prototype.toDeg = function() { + return this * 180 / Math.PI; + }; + } + + // Here comes the magic + geolib.extend({ + + decimal: {}, + + sexagesimal: {}, + + distance: null, + + getKeys: function(point) { + + // GeoJSON Array [longitude, latitude(, elevation)] + if(Object.prototype.toString.call(point) == '[object Array]') { + + return { + longitude: point.length >= 1 ? 0 : undefined, + latitude: point.length >= 2 ? 1 : undefined, + elevation: point.length >= 3 ? 2 : undefined + }; + + } + + var getKey = function(possibleValues) { + + var key; + + possibleValues.every(function(val) { + // TODO: check if point is an object + if(typeof point != 'object') { + return true; + } + return point.hasOwnProperty(val) ? (function() { key = val; return false; }()) : true; + }); + + return key; + + }; + + var longitude = getKey(['lng', 'lon', 'longitude']); + var latitude = getKey(['lat', 'latitude']); + var elevation = getKey(['alt', 'altitude', 'elevation', 'elev']); + + // return undefined if not at least one valid property was found + if(typeof latitude == 'undefined' && + typeof longitude == 'undefined' && + typeof elevation == 'undefined') { + return undefined; + } + + return { + latitude: latitude, + longitude: longitude, + elevation: elevation + }; + + }, + + // returns latitude of a given point, converted to decimal + // set raw to true to avoid conversion + getLat: function(point, raw) { + return raw === true ? point[this.getKeys(point).latitude] : this.useDecimal(point[this.getKeys(point).latitude]); + }, + + // Alias for getLat + latitude: function(point) { + return this.getLat.call(this, point); + }, + + // returns longitude of a given point, converted to decimal + // set raw to true to avoid conversion + getLon: function(point, raw) { + return raw === true ? point[this.getKeys(point).longitude] : this.useDecimal(point[this.getKeys(point).longitude]); + }, + + // Alias for getLon + longitude: function(point) { + return this.getLon.call(this, point); + }, + + getElev: function(point) { + return point[this.getKeys(point).elevation]; + }, + + // Alias for getElev + elevation: function(point) { + return this.getElev.call(this, point); + }, + + coords: function(point, raw) { + + var retval = { + latitude: raw === true ? point[this.getKeys(point).latitude] : this.useDecimal(point[this.getKeys(point).latitude]), + longitude: raw === true ? point[this.getKeys(point).longitude] : this.useDecimal(point[this.getKeys(point).longitude]) + }; + + var elev = point[this.getKeys(point).elevation]; + + if(typeof elev !== 'undefined') { + retval['elevation'] = elev; + } + + return retval; + + }, + + // checks if a variable contains a valid latlong object + validate: function(point) { + + var keys = this.getKeys(point); + + if(typeof keys === 'undefined' || typeof keys.latitude === 'undefined' || keys.longitude === 'undefined') { + return false; + } + + var lat = point[keys.latitude]; + var lng = point[keys.longitude]; + + if(typeof lat === 'undefined' || !this.isDecimal(lat) && !this.isSexagesimal(lat)) { + return false; + } + + if(typeof lng === 'undefined' || !this.isDecimal(lng) && !this.isSexagesimal(lng)) { + return false; + } + + lat = this.useDecimal(lat); + lng = this.useDecimal(lng); + + if(lat < this.minLat || lat > this.maxLat || lng < this.minLon || lng > this.maxLon) { + return false; + } + + return true; + + }, + + /** + * Calculates geodetic distance between two points specified by latitude/longitude using + * Vincenty inverse formula for ellipsoids + * Vincenty Inverse Solution of Geodesics on the Ellipsoid (c) Chris Veness 2002-2010 + * (Licensed under CC BY 3.0) + * + * @param object Start position {latitude: 123, longitude: 123} + * @param object End position {latitude: 123, longitude: 123} + * @param integer Accuracy (in meters) + * @return integer Distance (in meters) + */ + getDistance: function(start, end, accuracy) { + + accuracy = Math.floor(accuracy) || 1; + + var s = this.coords(start); + var e = this.coords(end); + + var a = 6378137, b = 6356752.314245, f = 1/298.257223563; // WGS-84 ellipsoid params + var L = (e['longitude']-s['longitude']).toRad(); + + var cosSigma, sigma, sinAlpha, cosSqAlpha, cos2SigmaM, sinSigma; + + var U1 = Math.atan((1-f) * Math.tan(parseFloat(s['latitude']).toRad())); + var U2 = Math.atan((1-f) * Math.tan(parseFloat(e['latitude']).toRad())); + var sinU1 = Math.sin(U1), cosU1 = Math.cos(U1); + var sinU2 = Math.sin(U2), cosU2 = Math.cos(U2); + + var lambda = L, lambdaP, iterLimit = 100; + do { + var sinLambda = Math.sin(lambda), cosLambda = Math.cos(lambda); + sinSigma = ( + Math.sqrt( + ( + cosU2 * sinLambda + ) * ( + cosU2 * sinLambda + ) + ( + cosU1 * sinU2 - sinU1 * cosU2 * cosLambda + ) * ( + cosU1 * sinU2 - sinU1 * cosU2 * cosLambda + ) + ) + ); + if (sinSigma === 0) { + return geolib.distance = 0; // co-incident points + } + + cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda; + sigma = Math.atan2(sinSigma, cosSigma); + sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma; + cosSqAlpha = 1 - sinAlpha * sinAlpha; + cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha; + + if (isNaN(cos2SigmaM)) { + cos2SigmaM = 0; // equatorial line: cosSqAlpha=0 (§6) + } + var C = ( + f / 16 * cosSqAlpha * ( + 4 + f * ( + 4 - 3 * cosSqAlpha + ) + ) + ); + lambdaP = lambda; + lambda = ( + L + ( + 1 - C + ) * f * sinAlpha * ( + sigma + C * sinSigma * ( + cos2SigmaM + C * cosSigma * ( + -1 + 2 * cos2SigmaM * cos2SigmaM + ) + ) + ) + ); + + } while (Math.abs(lambda-lambdaP) > 1e-12 && --iterLimit>0); + + if (iterLimit === 0) { + return NaN; // formula failed to converge + } + + var uSq = ( + cosSqAlpha * ( + a * a - b * b + ) / ( + b*b + ) + ); + + var A = ( + 1 + uSq / 16384 * ( + 4096 + uSq * ( + -768 + uSq * ( + 320 - 175 * uSq + ) + ) + ) + ); + + var B = ( + uSq / 1024 * ( + 256 + uSq * ( + -128 + uSq * ( + 74-47 * uSq + ) + ) + ) + ); + + var deltaSigma = ( + B * sinSigma * ( + cos2SigmaM + B / 4 * ( + cosSigma * ( + -1 + 2 * cos2SigmaM * cos2SigmaM + ) -B / 6 * cos2SigmaM * ( + -3 + 4 * sinSigma * sinSigma + ) * ( + -3 + 4 * cos2SigmaM * cos2SigmaM + ) + ) + ) + ); + + var distance = b * A * (sigma - deltaSigma); + + distance = distance.toFixed(3); // round to 1mm precision + + //if (start.hasOwnProperty(elevation) && end.hasOwnProperty(elevation)) { + if (typeof this.elevation(start) !== 'undefined' && typeof this.elevation(end) !== 'undefined') { + var climb = Math.abs(this.elevation(start) - this.elevation(end)); + distance = Math.sqrt(distance * distance + climb * climb); + } + + return this.distance = Math.floor( + Math.round(distance / accuracy) * accuracy + ); + + /* + // note: to return initial/final bearings in addition to distance, use something like: + var fwdAz = Math.atan2(cosU2*sinLambda, cosU1*sinU2-sinU1*cosU2*cosLambda); + var revAz = Math.atan2(cosU1*sinLambda, -sinU1*cosU2+cosU1*sinU2*cosLambda); + + return { distance: s, initialBearing: fwdAz.toDeg(), finalBearing: revAz.toDeg() }; + */ + + }, + + + /** + * Calculates the distance between two spots. + * This method is more simple but also far more inaccurate + * + * @param object Start position {latitude: 123, longitude: 123} + * @param object End position {latitude: 123, longitude: 123} + * @param integer Accuracy (in meters) + * @return integer Distance (in meters) + */ + getDistanceSimple: function(start, end, accuracy) { + + accuracy = Math.floor(accuracy) || 1; + + var distance = + Math.round( + Math.acos( + Math.sin( + this.latitude(end).toRad() + ) * + Math.sin( + this.latitude(start).toRad() + ) + + Math.cos( + this.latitude(end).toRad() + ) * + Math.cos( + this.latitude(start).toRad() + ) * + Math.cos( + this.longitude(start).toRad() - this.longitude(end).toRad() + ) + ) * this.radius + ); + + return geolib.distance = Math.floor(Math.round(distance/accuracy)*accuracy); + + }, + + + /** + * Calculates the center of a collection of geo coordinates + * + * @param array Collection of coords [{latitude: 51.510, longitude: 7.1321}, {latitude: 49.1238, longitude: "8° 30' W"}, ...] + * @return object {latitude: centerLat, longitude: centerLng, distance: diagonalDistance} + */ + getCenter: function(coords) { + + if (!coords.length) { + return false; + } + + var max = function( array ){ + return Math.max.apply( Math, array ); + }; + + var min = function( array ){ + return Math.min.apply( Math, array ); + }; + + var latitude; + var longitude; + var splitCoords = {latitude: [], longitude: []}; + + for(var coord in coords) { + + splitCoords.latitude.push( + this.latitude(coords[coord]) + ); + + splitCoords.longitude.push( + this.longitude(coords[coord]) + ); + + } + + var minLat = min(splitCoords.latitude); + var minLon = min(splitCoords.longitude); + var maxLat = max(splitCoords.latitude); + var maxLon = max(splitCoords.longitude); + + latitude = ((minLat + maxLat)/2).toFixed(6); + longitude = ((minLon + maxLon)/2).toFixed(6); + + // distance from the deepest left to the highest right point (diagonal distance) + var distance = this.convertUnit('km', this.getDistance({latitude: minLat, longitude: minLon}, {latitude: maxLat, longitude: maxLon})); + + return { + latitude: latitude, + longitude: longitude, + distance: distance + }; + + }, + + + + /** + * Gets the max and min, latitude, longitude, and elevation (if provided). + * @param array array with coords e.g. [{latitude: 51.5143, longitude: 7.4138}, {latitude: 123, longitude: 123}, ...] + * @return object {maxLat: maxLat, + * minLat: minLat + * maxLng: maxLng, + * minLng: minLng, + * maxElev: maxElev, + * minElev: minElev} + */ + getBounds: function(coords) { + + if (!coords.length) { + return false; + } + + var useElevation = this.elevation(coords[0]); + + var stats = { + maxLat: -Infinity, + minLat: Infinity, + maxLng: -Infinity, + minLng: Infinity + }; + + if (typeof useElevation != 'undefined') { + stats.maxElev = 0; + stats.minElev = Infinity; + } + + for (var i = 0, l = coords.length; i < l; ++i) { + + stats.maxLat = Math.max(this.latitude(coords[i]), stats.maxLat); + stats.minLat = Math.min(this.latitude(coords[i]), stats.minLat); + stats.maxLng = Math.max(this.longitude(coords[i]), stats.maxLng); + stats.minLng = Math.min(this.longitude(coords[i]), stats.minLng); + + if (useElevation) { + stats.maxElev = Math.max(this.elevation(coords[i]), stats.maxElev); + stats.minElev = Math.min(this.elevation(coords[i]), stats.minElev); + } + + } + + return stats; + + }, + + + /** + * Computes the bounding coordinates of all points on the surface + * of the earth less than or equal to the specified great circle + * distance. + * + * @param object Point position {latitude: 123, longitude: 123} + * @param number Distance (in meters). + * @return array Collection of two points defining the SW and NE corners. + */ + getBoundsOfDistance: function(point, distance) { + + var latitude = this.latitude(point); + var longitude = this.longitude(point); + + var radLat = latitude.toRad(); + var radLon = longitude.toRad(); + + var radDist = distance / this.radius; + var minLat = radLat - radDist; + var maxLat = radLat + radDist; + + var MAX_LAT_RAD = this.maxLat.toRad(); + var MIN_LAT_RAD = this.minLat.toRad(); + var MAX_LON_RAD = this.maxLon.toRad(); + var MIN_LON_RAD = this.minLon.toRad(); + + var minLon; + var maxLon; + + if (minLat > MIN_LAT_RAD && maxLat < MAX_LAT_RAD) { + + var deltaLon = Math.asin(Math.sin(radDist) / Math.cos(radLat)); + minLon = radLon - deltaLon; + + if (minLon < MIN_LON_RAD) { + minLon += 2 * Math.PI; + } + + maxLon = radLon + deltaLon; + + if (maxLon > MAX_LON_RAD) { + maxLon -= 2 * Math.PI; + } + + } else { + // A pole is within the distance. + minLat = Math.max(minLat, MIN_LAT_RAD); + maxLat = Math.min(maxLat, MAX_LAT_RAD); + minLon = MIN_LON_RAD; + maxLon = MAX_LON_RAD; + } + + return [ + // Southwest + { + latitude: minLat.toDeg(), + longitude: minLon.toDeg() + }, + // Northeast + { + latitude: maxLat.toDeg(), + longitude: maxLon.toDeg() + } + ]; + + }, + + + /** + * Checks whether a point is inside of a polygon or not. + * Note that the polygon coords must be in correct order! + * + * @param object coordinate to check e.g. {latitude: 51.5023, longitude: 7.3815} + * @param array array with coords e.g. [{latitude: 51.5143, longitude: 7.4138}, {latitude: 123, longitude: 123}, ...] + * @return bool true if the coordinate is inside the given polygon + */ + isPointInside: function(latlng, coords) { + + for(var c = false, i = -1, l = coords.length, j = l - 1; ++i < l; j = i) { + + if( + ( + (this.longitude(coords[i]) <= this.longitude(latlng) && this.longitude(latlng) < this.longitude(coords[j])) || + (this.longitude(coords[j]) <= this.longitude(latlng) && this.longitude(latlng) < this.longitude(coords[i])) + ) && + ( + this.latitude(latlng) < (this.latitude(coords[j]) - this.latitude(coords[i])) * + (this.longitude(latlng) - this.longitude(coords[i])) / + (this.longitude(coords[j]) - this.longitude(coords[i])) + + this.latitude(coords[i]) + ) + ) { + c = !c; + } + + } + + return c; + + }, + + + /** + * Shortcut for geolib.isPointInside() + */ + isInside: function() { + return this.isPointInside.apply(this, arguments); + }, + + + /** + * Checks whether a point is inside of a circle or not. + * + * @param object coordinate to check (e.g. {latitude: 51.5023, longitude: 7.3815}) + * @param object coordinate of the circle's center (e.g. {latitude: 51.4812, longitude: 7.4025}) + * @param integer maximum radius in meters + * @return bool true if the coordinate is within the given radius + */ + isPointInCircle: function(latlng, center, radius) { + return this.getDistance(latlng, center) < radius; + }, + + + /** + * Shortcut for geolib.isPointInCircle() + */ + withinRadius: function() { + return this.isPointInCircle.apply(this, arguments); + }, + + + /** + * Gets rhumb line bearing of two points. Find out about the difference between rhumb line and + * great circle bearing on Wikipedia. It's quite complicated. Rhumb line should be fine in most cases: + * + * http://en.wikipedia.org/wiki/Rhumb_line#General_and_mathematical_description + * + * Function heavily based on Doug Vanderweide's great PHP version (licensed under GPL 3.0) + * http://www.dougv.com/2009/07/13/calculating-the-bearing-and-compass-rose-direction-between-two-latitude-longitude-coordinates-in-php/ + * + * @param object origin coordinate (e.g. {latitude: 51.5023, longitude: 7.3815}) + * @param object destination coordinate + * @return integer calculated bearing + */ + getRhumbLineBearing: function(originLL, destLL) { + + // difference of longitude coords + var diffLon = this.longitude(destLL).toRad() - this.longitude(originLL).toRad(); + + // difference latitude coords phi + var diffPhi = Math.log( + Math.tan( + this.latitude(destLL).toRad() / 2 + Math.PI / 4 + ) / + Math.tan( + this.latitude(originLL).toRad() / 2 + Math.PI / 4 + ) + ); + + // recalculate diffLon if it is greater than pi + if(Math.abs(diffLon) > Math.PI) { + if(diffLon > 0) { + diffLon = (2 * Math.PI - diffLon) * -1; + } + else { + diffLon = 2 * Math.PI + diffLon; + } + } + + //return the angle, normalized + return (Math.atan2(diffLon, diffPhi).toDeg() + 360) % 360; + + }, + + + /** + * Gets great circle bearing of two points. See description of getRhumbLineBearing for more information + * + * @param object origin coordinate (e.g. {latitude: 51.5023, longitude: 7.3815}) + * @param object destination coordinate + * @return integer calculated bearing + */ + getBearing: function(originLL, destLL) { + + destLL['latitude'] = this.latitude(destLL); + destLL['longitude'] = this.longitude(destLL); + originLL['latitude'] = this.latitude(originLL); + originLL['longitude'] = this.longitude(originLL); + + var bearing = ( + ( + Math.atan2( + Math.sin( + destLL['longitude'].toRad() - + originLL['longitude'].toRad() + ) * + Math.cos( + destLL['latitude'].toRad() + ), + Math.cos( + originLL['latitude'].toRad() + ) * + Math.sin( + destLL['latitude'].toRad() + ) - + Math.sin( + originLL['latitude'].toRad() + ) * + Math.cos( + destLL['latitude'].toRad() + ) * + Math.cos( + destLL['longitude'].toRad() - originLL['longitude'].toRad() + ) + ) + ).toDeg() + 360 + ) % 360; + + return bearing; + + }, + + + /** + * Gets the compass direction from an origin coordinate to a destination coordinate. + * + * @param object origin coordinate (e.g. {latitude: 51.5023, longitude: 7.3815}) + * @param object destination coordinate + * @param string Bearing mode. Can be either circle or rhumbline + * @return object Returns an object with a rough (NESW) and an exact direction (NNE, NE, ENE, E, ESE, etc). + */ + getCompassDirection: function(originLL, destLL, bearingMode) { + + var direction; + var bearing; + + if(bearingMode == 'circle') { + // use great circle bearing + bearing = this.getBearing(originLL, destLL); + } else { + // default is rhumb line bearing + bearing = this.getRhumbLineBearing(originLL, destLL); + } + + switch(Math.round(bearing/22.5)) { + case 1: + direction = {exact: "NNE", rough: "N"}; + break; + case 2: + direction = {exact: "NE", rough: "N"}; + break; + case 3: + direction = {exact: "ENE", rough: "E"}; + break; + case 4: + direction = {exact: "E", rough: "E"}; + break; + case 5: + direction = {exact: "ESE", rough: "E"}; + break; + case 6: + direction = {exact: "SE", rough: "E"}; + break; + case 7: + direction = {exact: "SSE", rough: "S"}; + break; + case 8: + direction = {exact: "S", rough: "S"}; + break; + case 9: + direction = {exact: "SSW", rough: "S"}; + break; + case 10: + direction = {exact: "SW", rough: "S"}; + break; + case 11: + direction = {exact: "WSW", rough: "W"}; + break; + case 12: + direction = {exact: "W", rough: "W"}; + break; + case 13: + direction = {exact: "WNW", rough: "W"}; + break; + case 14: + direction = {exact: "NW", rough: "W"}; + break; + case 15: + direction = {exact: "NNW", rough: "N"}; + break; + default: + direction = {exact: "N", rough: "N"}; + } + + direction['bearing'] = bearing; + return direction; + + }, + + + /** + * Shortcut for getCompassDirection + */ + getDirection: function(originLL, destLL, bearingMode) { + return this.getCompassDirection.apply(this, arguments); + }, + + + /** + * Sorts an array of coords by distance from a reference coordinate + * + * @param object reference coordinate e.g. {latitude: 51.5023, longitude: 7.3815} + * @param mixed array or object with coords [{latitude: 51.5143, longitude: 7.4138}, {latitude: 123, longitude: 123}, ...] + * @return array ordered array + */ + orderByDistance: function(latlng, coords) { + + var coordsArray = []; + + for(var coord in coords) { + + var d = this.getDistance(latlng, coords[coord]); + + coordsArray.push({ + key: coord, + latitude: this.latitude(coords[coord]), + longitude: this.longitude(coords[coord]), + distance: d + }); + + } + + return coordsArray.sort(function(a, b) { return a.distance - b.distance; }); + + }, + + + /** + * Finds the nearest coordinate to a reference coordinate + * + * @param object reference coordinate e.g. {latitude: 51.5023, longitude: 7.3815} + * @param mixed array or object with coords [{latitude: 51.5143, longitude: 7.4138}, {latitude: 123, longitude: 123}, ...] + * @return array ordered array + */ + findNearest: function(latlng, coords, offset, limit) { + + offset = offset || 0; + limit = limit || 1; + var ordered = this.orderByDistance(latlng, coords); + + if(limit === 1) { + return ordered[offset]; + } else { + return ordered.splice(offset, limit); + } + + }, + + + /** + * Calculates the length of a given path + * + * @param mixed array or object with coords [{latitude: 51.5143, longitude: 7.4138}, {latitude: 123, longitude: 123}, ...] + * @return integer length of the path (in meters) + */ + getPathLength: function(coords) { + + var dist = 0; + var last; + + for (var i = 0, l = coords.length; i < l; ++i) { + if(last) { + //console.log(coords[i], last, this.getDistance(coords[i], last)); + dist += this.getDistance(this.coords(coords[i]), last); + } + last = this.coords(coords[i]); + } + + return dist; + + }, + + + /** + * Calculates the speed between to points within a given time span. + * + * @param object coords with javascript timestamp {latitude: 51.5143, longitude: 7.4138, time: 1360231200880} + * @param object coords with javascript timestamp {latitude: 51.5502, longitude: 7.4323, time: 1360245600460} + * @param object options (currently "unit" is the only option. Default: km(h)); + * @return float speed in unit per hour + */ + getSpeed: function(start, end, options) { + + var unit = options && options.unit || 'km'; + + if(unit == 'mph') { + unit = 'mi'; + } else if(unit == 'kmh') { + unit = 'km'; + } + + var distance = geolib.getDistance(start, end); + var time = ((end.time*1)/1000) - ((start.time*1)/1000); + var mPerHr = (distance/time)*3600; + var speed = Math.round(mPerHr * this.measures[unit] * 10000)/10000; + return speed; + + }, + + + /** + * Converts a distance from meters to km, mm, cm, mi, ft, in or yd + * + * @param string Format to be converted in + * @param float Distance in meters + * @param float Decimal places for rounding (default: 4) + * @return float Converted distance + */ + convertUnit: function(unit, distance, round) { + + if(distance === 0 || typeof distance === 'undefined') { + + if(this.distance === 0) { + // throw 'No distance given.'; + return 0; + } else { + distance = this.distance; + } + + } + + unit = unit || 'm'; + round = (null == round ? 4 : round); + + if(typeof this.measures[unit] !== 'undefined') { + return this.round(distance * this.measures[unit], round); + } else { + throw new Error('Unknown unit for conversion.'); + } + + }, + + + /** + * Checks if a value is in decimal format or, if neccessary, converts to decimal + * + * @param mixed Value(s) to be checked/converted (array of latlng objects, latlng object, sexagesimal string, float) + * @return float Input data in decimal format + */ + useDecimal: function(value) { + + if(Object.prototype.toString.call(value) === '[object Array]') { + + var geolib = this; + + value = value.map(function(val) { + + //if(!isNaN(parseFloat(val))) { + if(geolib.isDecimal(val)) { + + return geolib.useDecimal(val); + + } else if(typeof val == 'object') { + + if(geolib.validate(val)) { + + return geolib.coords(val); + + } else { + + for(var prop in val) { + val[prop] = geolib.useDecimal(val[prop]); + } + + return val; + + } + + } else if(geolib.isSexagesimal(val)) { + + return geolib.sexagesimal2decimal(val); + + } else { + + return val; + + } + + }); + + return value; + + } else if(typeof value === 'object' && this.validate(value)) { + + return this.coords(value); + + } else if(typeof value === 'object') { + + for(var prop in value) { + value[prop] = this.useDecimal(value[prop]); + } + + return value; + + } + + + if (this.isDecimal(value)) { + + return parseFloat(value); + + } else if(this.isSexagesimal(value) === true) { + + return parseFloat(this.sexagesimal2decimal(value)); + + } + + throw new Error('Unknown format.'); + + }, + + /** + * Converts a decimal coordinate value to sexagesimal format + * + * @param float decimal + * @return string Sexagesimal value (XX° YY' ZZ") + */ + decimal2sexagesimal: function(dec) { + + if (dec in this.sexagesimal) { + return this.sexagesimal[dec]; + } + + var tmp = dec.toString().split('.'); + + var deg = Math.abs(tmp[0]); + var min = ('0.' + tmp[1])*60; + var sec = min.toString().split('.'); + + min = Math.floor(min); + sec = (('0.' + sec[1]) * 60).toFixed(2); + + this.sexagesimal[dec] = (deg + '° ' + min + "' " + sec + '"'); + + return this.sexagesimal[dec]; + + }, + + + /** + * Converts a sexagesimal coordinate to decimal format + * + * @param float Sexagesimal coordinate + * @return string Decimal value (XX.XXXXXXXX) + */ + sexagesimal2decimal: function(sexagesimal) { + + if (sexagesimal in this.decimal) { + return this.decimal[sexagesimal]; + } + + var regEx = new RegExp(this.sexagesimalPattern); + var data = regEx.exec(sexagesimal); + var min = 0, sec = 0; + + if(data) { + min = parseFloat(data[2]/60); + sec = parseFloat(data[4]/3600) || 0; + } + + var dec = ((parseFloat(data[1]) + min + sec)).toFixed(8); + //var dec = ((parseFloat(data[1]) + min + sec)); + + // South and West are negative decimals + dec = (data[7] == 'S' || data[7] == 'W') ? parseFloat(-dec) : parseFloat(dec); + //dec = (data[7] == 'S' || data[7] == 'W') ? -dec : dec; + + this.decimal[sexagesimal] = dec; + + return dec; + + }, + + + /** + * Checks if a value is in decimal format + * + * @param string Value to be checked + * @return bool True if in sexagesimal format + */ + isDecimal: function(value) { + + value = value.toString().replace(/\s*/, ''); + + // looks silly but works as expected + // checks if value is in decimal format + return (!isNaN(parseFloat(value)) && parseFloat(value) == value); + + }, + + + /** + * Checks if a value is in sexagesimal format + * + * @param string Value to be checked + * @return bool True if in sexagesimal format + */ + isSexagesimal: function(value) { + + value = value.toString().replace(/\s*/, ''); + + return this.sexagesimalPattern.test(value); + + }, + + round: function(value, n) { + var decPlace = Math.pow(10, n); + return Math.round(value * decPlace)/decPlace; + } + + }); + + // Node module + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + + global.geolib = module.exports = geolib; + + // AMD module + } else if (typeof define === "function" && define.amd) { + + define("geolib", [], function () { + return geolib; + }); + + // we're in a browser + } else { + + global.geolib = geolib; + + } + +}(this)); \ No newline at end of file diff --git a/app/index.html b/app/index.html index d755e7c..88e196d 100644 --- a/app/index.html +++ b/app/index.html @@ -34,6 +34,8 @@ + + @@ -56,6 +58,8 @@ + + diff --git a/app/scripts/app.js b/app/scripts/app.js index 50bbc81..7c24975 100644 --- a/app/scripts/app.js +++ b/app/scripts/app.js @@ -5,7 +5,8 @@ angular.module('ffffng', [ 'ngRoute', 'ng', 'leaflet-directive', - 'templates-main' + 'templates-main', + 'ui.bootstrap' ]) .config(function ($routeProvider) { $routeProvider diff --git a/app/scripts/dialogs/outsideOfCommunityDialog.js b/app/scripts/dialogs/outsideOfCommunityDialog.js new file mode 100644 index 0000000..bd4c01f --- /dev/null +++ b/app/scripts/dialogs/outsideOfCommunityDialog.js @@ -0,0 +1,29 @@ +'use strict'; + +angular.module('ffffng') +.factory('OutsideOfCommunityDialog', function ($modal, config) { + var ctrl = function ($scope, $modalInstance, action) { + $scope.action = action; + $scope.config = config; + + $scope.proceed = function () { + $modalInstance.close(); + }; + + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; + }; + + return { + open: function (action) { + return $modal.open({ + controller: ctrl, + templateUrl: 'views/dialogs/outsideOfCommunityDialog.html', + resolve: { + action: function () { return action; } + } + }); + } + }; +}); diff --git a/app/scripts/directives/nodeForm.js b/app/scripts/directives/nodeForm.js index ea76bc5..06807e9 100644 --- a/app/scripts/directives/nodeForm.js +++ b/app/scripts/directives/nodeForm.js @@ -2,7 +2,17 @@ angular.module('ffffng') .directive('fNodeForm', function () { - var ctrl = function ($scope, $timeout, Constraints, $element, _, config, $window) { + var ctrl = function ( + $scope, + $timeout, + Constraints, + $element, + _, + config, + $window, + geolib, + OutsideOfCommunityDialog + ) { $scope.config = config; angular.extend($scope, { center: { @@ -29,6 +39,29 @@ angular.module('ffffng') } }); + if (config.otherCommunityInfo.showBorderForDebugging) { + $scope.paths = { + border: { + color: '#ff0000', + weight: 3, + latlngs: config.otherCommunityInfo.localCommunityPolygon.concat( + [config.otherCommunityInfo.localCommunityPolygon[0]] + ) + } + }; + } + + var geolibPolygon = _.map(config.otherCommunityInfo.localCommunityPolygon, function (point) { + return { + latitude: point.lat, + longitude: point.lng + }; + }); + + var inCommunityArea = function (lat, lng) { + return geolib.isPointInside({latitude: lat, longitude: lng}, geolibPolygon); + }; + var updateNodePosition = function (lat, lng) { $scope.markers.node = { lat: lat, @@ -45,15 +78,17 @@ angular.module('ffffng') $scope.node.coords = lat + ' ' + lng; }); - function withValidCoords(coords, callback) { + function withValidCoords(coords, callback, invalidCallback) { + invalidCallback = invalidCallback || function () {}; + coords = coords || ''; coords = coords.trim(); if (_.isEmpty(coords)) { - return; + return invalidCallback(); } if ($scope.hasError('coords')) { - return; + return invalidCallback(); } var parts = coords.split(/\s+/); @@ -61,7 +96,7 @@ angular.module('ffffng') var lat = Number(parts[0]); var lng = Number(parts[1]); - callback(lat, lng); + return callback(lat, lng); } $scope.updateMap = function (optCoords) { @@ -90,9 +125,7 @@ angular.module('ffffng') mac: 'Für die MAC-Adresse gibt es bereits einen Eintrag.' }; - $scope.onSubmit = function (node) { - submitted = true; - + var doSubmit = function (node) { if ($scope.nodeForm.$invalid) { var firstInvalid = _.filter($element.find('form').find('input'), function (input) { return $scope.nodeForm[input.name].$invalid; @@ -114,7 +147,27 @@ angular.module('ffffng') } $window.scrollTo(0, 0); }); - }.bind(this); + }; + + $scope.onSubmit = function (node) { + submitted = true; + + withValidCoords( + node.coords, + function (lat, lng) { + if (!config.otherCommunityInfo.showInfo || inCommunityArea(lat, lng)) { + doSubmit(node); + } else { + OutsideOfCommunityDialog.open($scope.action).result.then(function () { + doSubmit(node); + }); + } + }, + function () { + doSubmit(node); + } + ); + }; $scope.updateMap($scope.node.coords); withValidCoords($scope.node.coords, function (lat, lng) { diff --git a/app/scripts/libs.js b/app/scripts/libs.js index 801d854..72798de 100644 --- a/app/scripts/libs.js +++ b/app/scripts/libs.js @@ -1,6 +1,18 @@ 'use strict'; -angular.module('ffffng') -.factory('_', function () { - return window._; -}); +(function () { + var module = angular.module('ffffng'); + + function lib(name, windowField) { + if (!windowField) { + windowField = name; + } + + module.factory(name, function () { + return window[windowField]; + }); + } + + lib('_'); + lib('geolib'); +})(); diff --git a/app/styles/main.scss b/app/styles/main.scss index 795ce7c..1f404f5 100644 --- a/app/styles/main.scss +++ b/app/styles/main.scss @@ -12,6 +12,7 @@ @import "views/directives/_nodeForm"; @import "views/directives/_nodeSaved"; @import "views/directives/_tokenForm"; +@import "views/dialogs/_outsideOfCommunityDialog"; body { padding-bottom: 40px; diff --git a/app/styles/views/dialogs/_outsideOfCommunityDialog.scss b/app/styles/views/dialogs/_outsideOfCommunityDialog.scss new file mode 100644 index 0000000..36da386 --- /dev/null +++ b/app/styles/views/dialogs/_outsideOfCommunityDialog.scss @@ -0,0 +1,37 @@ +.outside-of-community-dialog { + .modal-header { + h3 { + @extend .modal-title; + } + + .cancel-icon { + @extend .fa, .fa-times; + + position: relative; + float: right; + + top: 10px; + right: 0; + + margin-left: 15px; + + cursor: pointer; + color: $gray; + } + } + .modal-footer { + .proceed { + @extend .btn; + + margin-left: 5px; + + &.create { + @extend .btn-info; + } + + &.update { + @extend .btn-primary; + } + } + } +} diff --git a/app/views/dialogs/outsideOfCommunityDialog.html b/app/views/dialogs/outsideOfCommunityDialog.html new file mode 100644 index 0000000..bfd9f19 --- /dev/null +++ b/app/views/dialogs/outsideOfCommunityDialog.html @@ -0,0 +1,35 @@ +
    + + + +
    diff --git a/app/views/directives/nodeForm.html b/app/views/directives/nodeForm.html index a99fd89..70d0177 100644 --- a/app/views/directives/nodeForm.html +++ b/app/views/directives/nodeForm.html @@ -49,7 +49,7 @@
    - +
    diff --git a/bower.json b/bower.json index ea148d1..daeace6 100644 --- a/bower.json +++ b/bower.json @@ -10,11 +10,13 @@ "bootstrap-sass-official": "~3.1.1", "es5-shim": "~3.4.0", "font-awesome": "~4.1.0", + "geolib": "~2.0.9", "jquery": "~2.1.1", "json3": "~3.3.1", "leaflet-dist": "~0.7.2", "sass-bootstrap": "~3.0.2", - "underscore": "~1.6.0" + "underscore": "~1.6.0", + "angular-bootstrap": "~0.11.0" }, "devDependencies": { "angular-mocks": "1.2.16",