insight-ui
Version:
An open-source frontend for the Insight API. The Insight API provides you with a convenient, powerful and simple way to query and broadcast data on the bitcoin network and build your own services with it.
1,515 lines (1,346 loc) • 111 kB
JavaScript
/*
* angular-ui-bootstrap
* http://angular-ui.github.io/bootstrap/
* Version: 0.10.0 - 2014-01-13
* 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.position","ui.bootstrap.datepicker","ui.bootstrap.dropdownToggle","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, $timeout) {
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(this.groups.indexOf(group), 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', ['$parse', function($parse) {
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:'@' }, // Create an isolated scope and interpolate the heading attribute onto this scope
controller: function() {
this.setHeading = function(element) {
this.heading = element;
};
},
link: function(scope, element, attrs, accordionCtrl) {
var getIsOpen, setIsOpen;
accordionCtrl.addGroup(scope);
scope.isOpen = false;
if ( attrs.isOpen ) {
getIsOpen = $parse(attrs.isOpen);
setIsOpen = getIsOpen.assign;
scope.$parent.$watch(getIsOpen, function(value) {
scope.isOpen = !!value;
});
}
scope.$watch('isOpen', function(value) {
if ( value ) {
accordionCtrl.closeOthers(scope);
}
if ( setIsOpen ) {
setIsOpen(scope.$parent, value);
}
});
}
};
}])
// Use accordion-heading below an accordion-group to provide a heading containing HTML
// <accordion-group>
// <accordion-heading>Heading containing HTML - <img src="..."></accordion-heading>
// </accordion-group>
.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',
compile: function(element, attr, transclude) {
return function link(scope, element, attr, accordionGroupCtrl) {
// 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
// <div class="accordion-group">
// <div class="accordion-heading" ><a ... accordion-transclude="heading">...</a></div>
// ...
// </div>
.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 () {
if (!element.hasClass(buttonsCtrl.activeClass)) {
scope.$apply(function () {
ngModelCtrl.$setViewValue(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', '$q', function ($scope, $timeout, $transition, $q) {
var self = this,
slides = self.slides = [],
currentIndex = -1,
currentTimeout, isPlaying;
self.currentSlide = null;
var destroyed = false;
/* direction: "prev" or "next" */
self.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.select = function(slide) {
self.select(slide);
};
$scope.isActive = function(slide) {
return self.currentSlide === slide;
};
$scope.slides = function() {
return slides;
};
$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
<example module="ui.bootstrap">
<file name="index.html">
<carousel>
<slide>
<img src="http://placekitten.com/150/150" style="margin:auto;">
<div class="carousel-caption">
<p>Beautiful!</p>
</div>
</slide>
<slide>
<img src="http://placekitten.com/100/150" style="margin:auto;">
<div class="carousel-caption">
<p>D'aww!</p>
</div>
</slide>
</carousel>
</file>
<file name="demo.css">
.carousel-indicators {
top: auto;
bottom: 15px;
}
</file>
</example>
*/
.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
<example module="ui.bootstrap">
<file name="index.html">
<div ng-controller="CarouselDemoCtrl">
<carousel>
<slide ng-repeat="slide in slides" active="slide.active">
<img ng-src="{{slide.image}}" style="margin:auto;">
<div class="carousel-caption">
<h4>Slide {{$index}}</h4>
<p>{{slide.text}}</p>
</div>
</slide>
</carousel>
<div class="row-fluid">
<div class="span6">
<ul>
<li ng-repeat="slide in slides">
<button class="btn btn-mini" ng-class="{'btn-info': !slide.active, 'btn-success': slide.active}" ng-disabled="slide.active" ng-click="slide.active = true">select</button>
{{$index}}: {{slide.text}}
</li>
</ul>
<a class="btn" ng-click="addSlide()">Add Slide</a>
</div>
<div class="span6">
Interval, in milliseconds: <input type="number" ng-model="myInterval">
<br />Enter a negative number to stop the interval.
</div>
</div>
</div>
</file>
<file name="script.js">
function CarouselDemoCtrl($scope) {
$scope.myInterval = 5000;
var slides = $scope.slides = [];
$scope.addSlide = function() {
var newWidth = 200 + ((slides.length + (25 * slides.length)) % 150);
slides.push({
image: 'http://placekitten.com/' + newWidth + '/200',
text: ['More','Extra','Lots of','Surplus'][slides.length % 4] + ' '
['Cats', 'Kittys', 'Felines', 'Cutes'][slides.length % 4]
});
};
for (var i=0; i<4; i++) $scope.addSlide();
}
</file>
<file name="demo.css">
.carousel-indicators {
top: auto;
bottom: 15px;
}
</file>
</example>
*/
.directive('slide', ['$parse', function($parse) {
return {
require: '^carousel',
restrict: 'EA',
transclude: true,
replace: true,
templateUrl: 'template/carousel/slide.html',
scope: {
},
link: function (scope, element, attrs, carouselCtrl) {
//Set up optional 'active' = binding
if (attrs.active) {
var getActive = $parse(attrs.active);
var setActive = getActive.assign;
var lastValue = scope.active = getActive(scope.$parent);
scope.$watch(function parentActiveWatch() {
var parentActive = getActive(scope.$parent);
if (parentActive !== scope.active) {
// we are out of sync and need to copy
if (parentActive !== lastValue) {
// parent changed and it has precedence
lastValue = scope.active = parentActive;
} else {
// if the parent can be assigned then do so
setActive(scope.$parent, parentActive = lastValue = scope.active);
}
}
return parentActive;
});
}
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.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].body.scrollTop || $document[0].documentElement.scrollTop),
left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft || $document[0].documentElement.scrollLeft)
};
}
};
}]);
angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position'])
.constant('datepickerConfig', {
dayFormat: 'dd',
monthFormat: 'MMMM',
yearFormat: 'yyyy',
dayHeaderFormat: 'EEE',
dayTitleFormat: 'MMMM yyyy',
monthTitleFormat: 'yyyy',
showWeeks: true,
startingDay: 0,
yearRange: 20,
minDate: null,
maxDate: null
})
.controller('DatepickerController', ['$scope', '$attrs', 'dateFilter', 'datepickerConfig', function($scope, $attrs, dateFilter, dtConfig) {
var format = {
day: getValue($attrs.dayFormat, dtConfig.dayFormat),
month: getValue($attrs.monthFormat, dtConfig.monthFormat),
year: getValue($attrs.yearFormat, dtConfig.yearFormat),
dayHeader: getValue($attrs.dayHeaderFormat, dtConfig.dayHeaderFormat),
dayTitle: getValue($attrs.dayTitleFormat, dtConfig.dayTitleFormat),
monthTitle: getValue($attrs.monthTitleFormat, dtConfig.monthTitleFormat)
},
startingDay = getValue($attrs.startingDay, dtConfig.startingDay),
yearRange = getValue($attrs.yearRange, dtConfig.yearRange);
this.minDate = dtConfig.minDate ? new Date(dtConfig.minDate) : null;
this.maxDate = dtConfig.maxDate ? new Date(dtConfig.maxDate) : null;
function getValue(value, defaultValue) {
return angular.isDefined(value) ? $scope.$parent.$eval(value) : defaultValue;
}
function getDaysInMonth( year, month ) {
return new Date(year, month, 0).getDate();
}
function getDates(startDate, n) {
var dates = new Array(n);
var current = startDate, i = 0;
while (i < n) {
dates[i++] = new Date(current);
current.setDate( current.getDate() + 1 );
}
return dates;
}
function makeDate(date, format, isSelected, isSecondary) {
return { date: date, label: dateFilter(date, format), selected: !!isSelected, secondary: !!isSecondary };
}
this.modes = [
{
name: 'day',
getVisibleDates: function(date, selected) {
var year = date.getFullYear(), month = date.getMonth(), firstDayOfMonth = new Date(year, month, 1);
var difference = startingDay - firstDayOfMonth.getDay(),
numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference,
firstDate = new Date(firstDayOfMonth), numDates = 0;
if ( numDisplayedFromPreviousMonth > 0 ) {
firstDate.setDate( - numDisplayedFromPreviousMonth + 1 );
numDates += numDisplayedFromPreviousMonth; // Previous
}
numDates += getDaysInMonth(year, month + 1); // Current
numDates += (7 - numDates % 7) % 7; // Next
var days = getDates(firstDate, numDates), labels = new Array(7);
for (var i = 0; i < numDates; i ++) {
var dt = new Date(days[i]);
days[i] = makeDate(dt, format.day, (selected && selected.getDate() === dt.getDate() && selected.getMonth() === dt.getMonth() && selected.getFullYear() === dt.getFullYear()), dt.getMonth() !== month);
}
for (var j = 0; j < 7; j++) {
labels[j] = dateFilter(days[j].date, format.dayHeader);
}
return { objects: days, title: dateFilter(date, format.dayTitle), labels: labels };
},
compare: function(date1, date2) {
return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) );
},
split: 7,
step: { months: 1 }
},
{
name: 'month',
getVisibleDates: function(date, selected) {
var months = new Array(12), year = date.getFullYear();
for ( var i = 0; i < 12; i++ ) {
var dt = new Date(year, i, 1);
months[i] = makeDate(dt, format.month, (selected && selected.getMonth() === i && selected.getFullYear() === year));
}
return { objects: months, title: dateFilter(date, format.monthTitle) };
},
compare: function(date1, date2) {
return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() );
},
split: 3,
step: { years: 1 }
},
{
name: 'year',
getVisibleDates: function(date, selected) {
var years = new Array(yearRange), year = date.getFullYear(), startYear = parseInt((year - 1) / yearRange, 10) * yearRange + 1;
for ( var i = 0; i < yearRange; i++ ) {
var dt = new Date(startYear + i, 0, 1);
years[i] = makeDate(dt, format.year, (selected && selected.getFullYear() === dt.getFullYear()));
}
return { objects: years, title: [years[0].label, years[yearRange - 1].label].join(' - ') };
},
compare: function(date1, date2) {
return date1.getFullYear() - date2.getFullYear();
},
split: 5,
step: { years: yearRange }
}
];
this.isDisabled = function(date, mode) {
var currentMode = this.modes[mode || 0];
return ((this.minDate && currentMode.compare(date, this.minDate) < 0) || (this.maxDate && currentMode.compare(date, this.maxDate) > 0) || ($scope.dateDisabled && $scope.dateDisabled({date: date, mode: currentMode.name})));
};
}])
.directive( 'datepicker', ['dateFilter', '$parse', 'datepickerConfig', '$log', function (dateFilter, $parse, datepickerConfig, $log) {
return {
restrict: 'EA',
replace: true,
templateUrl: 'template/datepicker/datepicker.html',
scope: {
dateDisabled: '&'
},
require: ['datepicker', '?^ngModel'],
controller: 'DatepickerController',
link: function(scope, element, attrs, ctrls) {
var datepickerCtrl = ctrls[0], ngModel = ctrls[1];
if (!ngModel) {
return; // do nothing if no ng-model
}
// Configuration parameters
var mode = 0, selected = new Date(), showWeeks = datepickerConfig.showWeeks;
if (attrs.showWeeks) {
scope.$parent.$watch($parse(attrs.showWeeks), function(value) {
showWeeks = !! value;
updateShowWeekNumbers();
});
} else {
updateShowWeekNumbers();
}
if (attrs.min) {
scope.$parent.$watch($parse(attrs.min), function(value) {
datepickerCtrl.minDate = value ? new Date(value) : null;
refill();
});
}
if (attrs.max) {
scope.$parent.$watch($parse(attrs.max), function(value) {
datepickerCtrl.maxDate = value ? new Date(value) : null;
refill();
});
}
function updateShowWeekNumbers() {
scope.showWeekNumbers = mode === 0 && showWeeks;
}
// Split array into smaller arrays
function split(arr, size) {
var arrays = [];
while (arr.length > 0) {
arrays.push(arr.splice(0, size));
}
return arrays;
}
function refill( updateSelected ) {
var date = null, valid = true;
if ( ngModel.$modelValue ) {
date = new Date( ngModel.$modelValue );
if ( isNaN(date) ) {
valid = false;
$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.');
} else if ( updateSelected ) {
selected = date;
}
}
ngModel.$setValidity('date', valid);
var currentMode = datepickerCtrl.modes[mode], data = currentMode.getVisibleDates(selected, date);
angular.forEach(data.objects, function(obj) {
obj.disabled = datepickerCtrl.isDisabled(obj.date, mode);
});
ngModel.$setValidity('date-disabled', (!date || !datepickerCtrl.isDisabled(date)));
scope.rows = split(data.objects, currentMode.split);
scope.labels = data.labels || [];
scope.title = data.title;
}
function setMode(value) {
mode = value;
updateShowWeekNumbers();
refill();
}
ngModel.$render = function() {
refill( true );
};
scope.select = function( date ) {
if ( mode === 0 ) {
var dt = ngModel.$modelValue ? new Date( ngModel.$modelValue ) : new Date(0, 0, 0, 0, 0, 0, 0);
dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() );
ngModel.$setViewValue( dt );
refill( true );
} else {
selected = date;
setMode( mode - 1 );
}
};
scope.move = function(direction) {
var step = datepickerCtrl.modes[mode].step;
selected.setMonth( selected.getMonth() + direction * (step.months || 0) );
selected.setFullYear( selected.getFullYear() + direction * (step.years || 0) );
refill();
};
scope.toggleMode = function() {
setMode( (mode + 1) % datepickerCtrl.modes.length );
};
scope.getWeekNumber = function(row) {
return ( mode === 0 && scope.showWeekNumbers && row.length === 7 ) ? getISO8601WeekNumber(row[0].date) : null;
};
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;
}
}
};
}])
.constant('datepickerPopupConfig', {
dateFormat: 'yyyy-MM-dd',
currentText: 'Today',
toggleWeeksText: 'Weeks',
clearText: 'Clear',
closeText: 'Done',
closeOnDateSelection: true,
appendToBody: false,
showButtonBar: true
})
.directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'datepickerPopupConfig', 'datepickerConfig',
function ($compile, $parse, $document, $position, dateFilter, datepickerPopupConfig, datepickerConfig) {
return {
restrict: 'EA',
require: 'ngModel',
link: function(originalScope, element, attrs, ngModel) {
var scope = originalScope.$new(), // create a child scope so we are not polluting original one
dateFormat,
closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? originalScope.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection,
appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? originalScope.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody;
attrs.$observe('datepickerPopup', function(value) {
dateFormat = value || datepickerPopupConfig.dateFormat;
ngModel.$render();
});
scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? originalScope.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar;
originalScope.$on('$destroy', function() {
$popup.remove();
scope.$destroy();
});
attrs.$observe('currentText', function(text) {
scope.currentText = angular.isDefined(text) ? text : datepickerPopupConfig.currentText;
});
attrs.$observe('toggleWeeksText', function(text) {
scope.toggleWeeksText = angular.isDefined(text) ? text : datepickerPopupConfig.toggleWeeksText;
});
attrs.$observe('clearText', function(text) {
scope.clearText = angular.isDefined(text) ? text : datepickerPopupConfig.clearText;
});
attrs.$observe('closeText', function(text) {
scope.closeText = angular.isDefined(text) ? text : datepickerPopupConfig.closeText;
});
var getIsOpen, setIsOpen;
if ( attrs.isOpen ) {
getIsOpen = $parse(attrs.isOpen);
setIsOpen = getIsOpen.assign;
originalScope.$watch(getIsOpen, function updateOpen(value) {
scope.isOpen = !! value;
});
}
scope.isOpen = getIsOpen ? getIsOpen(originalScope) : false; // Initial state
function setOpen( value ) {
if (setIsOpen) {
setIsOpen(originalScope, !!value);
} else {
scope.isOpen = !!value;
}
}
var documentClickBind = function(event) {
if (scope.isOpen && event.target !== element[0]) {
scope.$apply(function() {
setOpen(false);
});
}
};
var elementFocusBind = function() {
scope.$apply(function() {
setOpen( true );
});
};
// popup element used to display calendar
var popupEl = angular.element('<div datepicker-popup-wrap><div datepicker></div></div>');
popupEl.attr({
'ng-model': 'date',
'ng-change': 'dateSelection()'
});
var datepickerEl = angular.element(popupEl.children()[0]),
datepickerOptions = {};
if (attrs.datepickerOptions) {
datepickerOptions = originalScope.$eval(attrs.datepickerOptions);
datepickerEl.attr(angular.extend({}, datepickerOptions));
}
// TODO: reverse from dateFilter string to Date object
function parseDate(viewValue) {
if (!viewValue) {
ngModel.$setValidity('date', true);
return null;
} else if (angular.isDate(viewValue)) {
ngModel.$setValidity('date', true);
return viewValue;
} else if (angular.isString(viewValue)) {
var date = 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) {
setOpen( false );
}
};
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 = ngModel.$modelValue;
};
function addWatchableAttribute(attribute, scopeProperty, datepickerAttribute) {
if (attribute) {
originalScope.$watch($parse(attribute), function(value){
scope[scopeProperty] = value;
});
datepickerEl.attr(datepickerAttribute || scopeProperty, scopeProperty);
}
}
addWatchableAttribute(attrs.min, 'min');
addWatchableAttribute(attrs.max, 'max');
if (attrs.showWeeks) {
addWatchableAttribute(attrs.showWeeks, 'showWeeks', 'show-weeks');
} else {
scope.showWeeks = 'show-weeks' in datepickerOptions ? datepickerOptions['show-weeks'] : datepickerConfig.showWeeks;
datepickerEl.attr('show-weeks', 'showWeeks');
}
if (attrs.dateDisabled) {
datepickerEl.attr('date-disabled', attrs.dateDisabled);
}
function updatePosition() {
scope.position = appendToBody ? $position.offset(element) : $position.position(element);
scope.position.top = scope.position.top + element.prop('offsetHeight');
}
var documentBindingInitialized = false, elementFocusInitialized = false;
scope.$watch('isOpen', function(value) {
if (value) {
updatePosition();
$document.bind('click', documentClickBind);
if(elementFocusInitialized) {
element.unbind('focus', elementFocusBind);
}
element[0].focus();
documentBindingInitialized = true;
} else {
if(documentBindingInitialized) {
$document.unbind('click', documentClickBind);
}
element.bind('focus', elementFocusBind);
elementFocusInitialized = true;
}
if ( setIsOpen ) {
setIsOpen(originalScope, value);
}
});
scope.today = function() {
scope.dateSelection(new Date());
};
scope.clear = function() {
scope.dateSelection(null);
};
var $popup = $compile(popupEl)(scope);
if ( appendToBody ) {
$document.find('body').append($popup);
} else {
element.after($popup);
}
}
};
}])
.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();
});
}
};
});
/*
* dropdownToggle - Provides dropdown menu functionality in place of bootstrap js
* @restrict class or attribute
* @example:
<li class="dropdown">
<a class="dropdown-toggle">My Dropdown Menu</a>
<ul class="dropdown-menu">
<li ng-repeat="choice in dropChoices">
<a ng-href="{{choice.href}}">{{choice.text}}</a>
</li>
</ul>
</li>
*/
angular.module('ui.bootstrap.dropdownToggle', []).directive('dropdownToggle', ['$document', '$location', function ($document, $location) {
var openElement = null,
closeMenu = angular.noop;
return {
restrict: 'CA',
link: function(scope, element, attrs) {
scope.$watch('$location.path', function() { closeMenu(); });
element.parent().bind('click', function() { closeMenu(); });
element.bind('click', function (event) {
var elementWasOpen = (element === openElement);
event.preventDefault();
event.stopPropagation();
if (!!openElement) {
closeMenu();
}
if (!elementWasOpen && !element.hasClass('disabled') && !element.prop('disabled')) {
element.parent().addClass('open');
openElement = element;
closeMenu = function (event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
$document.unbind('click', closeMenu);
element.parent().removeClass('open');
closeMenu = angular.noop;
openElement = null;
};
$document.bind('click', closeMenu);
}
});
}
};
}]);
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: 'template/modal/window.html',
link: function (scope, element, attrs) {
scope.windowClass = attrs.windowClass || '';
$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, checkRemoveBackdrop);
body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0);
}
function checkRemoveBackdrop() {
//remove backdrop if no longer needed