");
+ el.addClass(clazz);
+ return el;
+ }
+
+ // The `Dialog` class represents a modal dialog. The dialog class can be invoked by providing an options object
+ // containing at lest template or templateUrl and controller:
+ //
+ // var d = new Dialog({templateUrl: 'foo.html', controller: 'BarController'});
+ //
+ // Dialogs can also be created using templateUrl and controller as distinct arguments:
+ //
+ // var d = new Dialog('path/to/dialog.html', MyDialogController);
+ function Dialog(opts) {
+
+ var self = this, options = this.options = angular.extend({}, defaults, globalOptions, opts);
+ this._open = false;
+
+ this.backdropEl = createElement(options.backdropClass);
+ if(options.backdropFade){
+ this.backdropEl.addClass(options.transitionClass);
+ this.backdropEl.removeClass(options.triggerClass);
+ }
+
+ this.modalEl = createElement(options.dialogClass);
+ if(options.dialogFade){
+ this.modalEl.addClass(options.transitionClass);
+ this.modalEl.removeClass(options.triggerClass);
+ }
+
+ this.handledEscapeKey = function(e) {
+ if (e.which === 27) {
+ self.close();
+ e.preventDefault();
+ self.$scope.$apply();
+ }
+ };
+
+ this.handleBackDropClick = function(e) {
+ self.close();
+ e.preventDefault();
+ self.$scope.$apply();
+ };
+
+ this.handleLocationChange = function() {
+ self.close();
+ };
+ }
+
+ // The `isOpen()` method returns wether the dialog is currently visible.
+ Dialog.prototype.isOpen = function(){
+ return this._open;
+ };
+
+ // The `open(templateUrl, controller)` method opens the dialog.
+ // Use the `templateUrl` and `controller` arguments if specifying them at dialog creation time is not desired.
+ Dialog.prototype.open = function(templateUrl, controller){
+ var self = this, options = this.options;
+
+ if(templateUrl){
+ options.templateUrl = templateUrl;
+ }
+ if(controller){
+ options.controller = controller;
+ }
+
+ if(!(options.template || options.templateUrl)) {
+ throw new Error('Dialog.open expected template or templateUrl, neither found. Use options or open method to specify them.');
+ }
+
+ this._loadResolves().then(function(locals) {
+ var $scope = locals.$scope = self.$scope = locals.$scope ? locals.$scope : $rootScope.$new();
+
+ self.modalEl.html(locals.$template);
+
+ if (self.options.controller) {
+ var ctrl = $controller(self.options.controller, locals);
+ self.modalEl.children().data('ngControllerController', ctrl);
+ }
+
+ $compile(self.modalEl)($scope);
+ self._addElementsToDom();
+
+ // trigger tranisitions
+ setTimeout(function(){
+ if(self.options.dialogFade){ self.modalEl.addClass(self.options.triggerClass); }
+ if(self.options.backdropFade){ self.backdropEl.addClass(self.options.triggerClass); }
+ });
+
+ self._bindEvents();
+ });
+
+ this.deferred = $q.defer();
+ return this.deferred.promise;
+ };
+
+ // closes the dialog and resolves the promise returned by the `open` method with the specified result.
+ Dialog.prototype.close = function(result){
+ var self = this;
+ var fadingElements = this._getFadingElements();
+
+ if(fadingElements.length > 0){
+ for (var i = fadingElements.length - 1; i >= 0; i--) {
+ $transition(fadingElements[i], removeTriggerClass).then(onCloseComplete);
+ }
+ return;
+ }
+
+ this._onCloseComplete(result);
+
+ function removeTriggerClass(el){
+ el.removeClass(self.options.triggerClass);
+ }
+
+ function onCloseComplete(){
+ if(self._open){
+ self._onCloseComplete(result);
+ }
+ }
+ };
+
+ Dialog.prototype._getFadingElements = function(){
+ var elements = [];
+ if(this.options.dialogFade){
+ elements.push(this.modalEl);
+ }
+ if(this.options.backdropFade){
+ elements.push(this.backdropEl);
+ }
+
+ return elements;
+ };
+
+ Dialog.prototype._bindEvents = function() {
+ if(this.options.keyboard){ body.bind('keydown', this.handledEscapeKey); }
+ if(this.options.backdrop && this.options.backdropClick){ this.backdropEl.bind('click', this.handleBackDropClick); }
+ };
+
+ Dialog.prototype._unbindEvents = function() {
+ if(this.options.keyboard){ body.unbind('keydown', this.handledEscapeKey); }
+ if(this.options.backdrop && this.options.backdropClick){ this.backdropEl.unbind('click', this.handleBackDropClick); }
+ };
+
+ Dialog.prototype._onCloseComplete = function(result) {
+ this._removeElementsFromDom();
+ this._unbindEvents();
+
+ this.deferred.resolve(result);
+ };
+
+ Dialog.prototype._addElementsToDom = function(){
+ body.append(this.modalEl);
+
+ if(this.options.backdrop) {
+ if (activeBackdrops.value === 0) {
+ body.append(this.backdropEl);
+ }
+ activeBackdrops.value++;
+ }
+
+ this._open = true;
+ };
+
+ Dialog.prototype._removeElementsFromDom = function(){
+ this.modalEl.remove();
+
+ if(this.options.backdrop) {
+ activeBackdrops.value--;
+ if (activeBackdrops.value === 0) {
+ this.backdropEl.remove();
+ }
+ }
+ this._open = false;
+ };
+
+ // Loads all `options.resolve` members to be used as locals for the controller associated with the dialog.
+ Dialog.prototype._loadResolves = function(){
+ var values = [], keys = [], templatePromise, self = this;
+
+ if (this.options.template) {
+ templatePromise = $q.when(this.options.template);
+ } else if (this.options.templateUrl) {
+ templatePromise = $http.get(this.options.templateUrl, {cache:$templateCache})
+ .then(function(response) { return response.data; });
+ }
+
+ angular.forEach(this.options.resolve || [], function(value, key) {
+ keys.push(key);
+ values.push(angular.isString(value) ? $injector.get(value) : $injector.invoke(value));
+ });
+
+ keys.push('$template');
+ values.push(templatePromise);
+
+ return $q.all(values).then(function(values) {
+ var locals = {};
+ angular.forEach(values, function(value, index) {
+ locals[keys[index]] = value;
+ });
+ locals.dialog = self;
+ return locals;
+ });
+ };
+
+ // The actual `$dialog` service that is injected in controllers.
+ return {
+ // Creates a new `Dialog` with the specified options.
+ dialog: function(opts){
+ return new Dialog(opts);
+ },
+ // creates a new `Dialog` tied to the default message box template and controller.
+ //
+ // Arguments `title` and `message` are rendered in the modal header and body sections respectively.
+ // The `buttons` array holds an object with the following members for each button to include in the
+ // modal footer section:
+ //
+ // * `result`: the result to pass to the `close` method of the dialog when the button is clicked
+ // * `label`: the label of the button
+ // * `cssClass`: additional css class(es) to apply to the button for styling
+ messageBox: function(title, message, buttons){
+ return new Dialog({templateUrl: 'template/dialog/message.html', controller: 'MessageBoxController', resolve:
+ {model: function() {
+ return {
+ title: title,
+ message: message,
+ buttons: buttons
+ };
+ }
+ }});
+ }
+ };
+ }];
+});
+
+/*
+ * dropdownToggle - Provides dropdown menu functionality in place of bootstrap js
+ * @restrict class or attribute
+ * @example:
+
+ My Dropdown Menu
+
+
+ */
+
+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.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.dialog'])
+.directive('modal', ['$parse', '$dialog', function($parse, $dialog) {
+ return {
+ restrict: 'EA',
+ terminal: true,
+ link: function(scope, elm, attrs) {
+ var opts = angular.extend({}, scope.$eval(attrs.uiOptions || attrs.bsOptions || attrs.options));
+ var shownExpr = attrs.modal || attrs.show;
+ var setClosed;
+
+ // Create a dialog with the template as the contents of the directive
+ // Add the current scope as the resolve in order to make the directive scope as a dialog controller scope
+ opts = angular.extend(opts, {
+ template: elm.html(),
+ resolve: { $scope: function() { return scope; } }
+ });
+ var dialog = $dialog.dialog(opts);
+
+ elm.remove();
+
+ if (attrs.close) {
+ setClosed = function() {
+ $parse(attrs.close)(scope);
+ };
+ } else {
+ setClosed = function() {
+ if (angular.isFunction($parse(shownExpr).assign)) {
+ $parse(shownExpr).assign(scope, false);
+ }
+ };
+ }
+
+ scope.$watch(shownExpr, function(isShown, oldShown) {
+ if (isShown) {
+ dialog.open().then(function(){
+ setClosed();
+ });
+ } else {
+ //Make sure it is not opened
+ if (dialog.isOpen()){
+ dialog.close();
+ }
+ }
+ });
+ }
+ };
+}]);
+angular.module('ui.bootstrap.pagination', [])
+
+.controller('PaginationController', ['$scope', function (scope) {
+
+ scope.noPrevious = function() {
+ return scope.currentPage === 1;
+ };
+ scope.noNext = function() {
+ return scope.currentPage === scope.numPages;
+ };
+
+ scope.isActive = function(page) {
+ return scope.currentPage === page;
+ };
+
+ scope.selectPage = function(page) {
+ if ( ! scope.isActive(page) && page > 0 && page <= scope.numPages) {
+ scope.currentPage = page;
+ scope.onSelectPage({ page: page });
+ }
+ };
+}])
+
+.constant('paginationConfig', {
+ boundaryLinks: false,
+ directionLinks: true,
+ firstText: 'First',
+ previousText: 'Previous',
+ nextText: 'Next',
+ lastText: 'Last',
+ rotate: true
+})
+
+.directive('pagination', ['paginationConfig', function(paginationConfig) {
+ return {
+ restrict: 'EA',
+ scope: {
+ numPages: '=',
+ currentPage: '=',
+ maxSize: '=',
+ onSelectPage: '&'
+ },
+ controller: 'PaginationController',
+ templateUrl: 'template/pagination/pagination.html',
+ replace: true,
+ link: function(scope, element, attrs) {
+
+ // Setup configuration parameters
+ var boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks;
+ var directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$eval(attrs.directionLinks) : paginationConfig.directionLinks;
+ var firstText = angular.isDefined(attrs.firstText) ? scope.$parent.$eval(attrs.firstText) : paginationConfig.firstText;
+ var previousText = angular.isDefined(attrs.previousText) ? scope.$parent.$eval(attrs.previousText) : paginationConfig.previousText;
+ var nextText = angular.isDefined(attrs.nextText) ? scope.$parent.$eval(attrs.nextText) : paginationConfig.nextText;
+ var lastText = angular.isDefined(attrs.lastText) ? scope.$parent.$eval(attrs.lastText) : paginationConfig.lastText;
+ var rotate = angular.isDefined(attrs.rotate) ? scope.$eval(attrs.rotate) : paginationConfig.rotate;
+
+ // Create page object used in template
+ function makePage(number, text, isActive, isDisabled) {
+ return {
+ number: number,
+ text: text,
+ active: isActive,
+ disabled: isDisabled
+ };
+ }
+
+ scope.$watch('numPages + currentPage + maxSize', function() {
+ scope.pages = [];
+
+ // Default page limits
+ var startPage = 1, endPage = scope.numPages;
+ var isMaxSized = ( angular.isDefined(scope.maxSize) && scope.maxSize < scope.numPages );
+
+ // recompute if maxSize
+ if ( isMaxSized ) {
+ if ( rotate ) {
+ // Current page is displayed in the middle of the visible ones
+ startPage = Math.max(scope.currentPage - Math.floor(scope.maxSize/2), 1);
+ endPage = startPage + scope.maxSize - 1;
+
+ // Adjust if limit is exceeded
+ if (endPage > scope.numPages) {
+ endPage = scope.numPages;
+ startPage = endPage - scope.maxSize + 1;
+ }
+ } else {
+ // Visible pages are paginated with maxSize
+ startPage = ((Math.ceil(scope.currentPage / scope.maxSize) - 1) * scope.maxSize) + 1;
+
+ // Adjust last page if limit is exceeded
+ endPage = Math.min(startPage + scope.maxSize - 1, scope.numPages);
+ }
+ }
+
+ // Add page number links
+ for (var number = startPage; number <= endPage; number++) {
+ var page = makePage(number, number, scope.isActive(number), false);
+ scope.pages.push(page);
+ }
+
+ // Add links to move between page sets
+ if ( isMaxSized && ! rotate ) {
+ if ( startPage > 1 ) {
+ var previousPageSet = makePage(startPage - 1, '...', false, false);
+ scope.pages.unshift(previousPageSet);
+ }
+
+ if ( endPage < scope.numPages ) {
+ var nextPageSet = makePage(endPage + 1, '...', false, false);
+ scope.pages.push(nextPageSet);
+ }
+ }
+
+ // Add previous & next links
+ if (directionLinks) {
+ var previousPage = makePage(scope.currentPage - 1, previousText, false, scope.noPrevious());
+ scope.pages.unshift(previousPage);
+
+ var nextPage = makePage(scope.currentPage + 1, nextText, false, scope.noNext());
+ scope.pages.push(nextPage);
+ }
+
+ // Add first & last links
+ if (boundaryLinks) {
+ var firstPage = makePage(1, firstText, false, scope.noPrevious());
+ scope.pages.unshift(firstPage);
+
+ var lastPage = makePage(scope.numPages, lastText, false, scope.noNext());
+ scope.pages.push(lastPage);
+ }
+
+ if ( scope.currentPage > scope.numPages ) {
+ scope.selectPage(scope.numPages);
+ }
+ });
+ }
+ };
+}])
+
+.constant('pagerConfig', {
+ previousText: '« Previous',
+ nextText: 'Next »',
+ align: true
+})
+
+.directive('pager', ['pagerConfig', function(config) {
+ return {
+ restrict: 'EA',
+ scope: {
+ numPages: '=',
+ currentPage: '=',
+ onSelectPage: '&'
+ },
+ controller: 'PaginationController',
+ templateUrl: 'template/pagination/pager.html',
+ replace: true,
+ link: function(scope, element, attrs, paginationCtrl) {
+
+ // Setup configuration parameters
+ var previousText = angular.isDefined(attrs.previousText) ? scope.$parent.$eval(attrs.previousText) : config.previousText;
+ var nextText = angular.isDefined(attrs.nextText) ? scope.$parent.$eval(attrs.nextText) : config.nextText;
+ var align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : config.align;
+
+ // Create page object used in template
+ function makePage(number, text, isDisabled, isPrevious, isNext) {
+ return {
+ number: number,
+ text: text,
+ disabled: isDisabled,
+ previous: ( align && isPrevious ),
+ next: ( align && isNext )
+ };
+ }
+
+ scope.$watch('numPages + currentPage', function() {
+ scope.pages = [];
+
+ // Add previous & next links
+ var previousPage = makePage(scope.currentPage - 1, previousText, scope.noPrevious(), true, false);
+ scope.pages.unshift(previousPage);
+
+ var nextPage = makePage(scope.currentPage + 1, nextText, scope.noNext(), false, true);
+ scope.pages.push(nextPage);
+
+ if ( scope.currentPage > scope.numPages ) {
+ scope.selectPage(scope.numPages);
+ }
+ });
+ }
+ };
+}]);
+
+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) {
+
+ var mouseX, mouseY;
+
+ $document.bind('mousemove', function mouseMoved(event) {
+ mouseX = event.pageX;
+ mouseY = event.pageY;
+ });
+
+ 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;
+ offsetParentBCR.left += offsetParentEl.clientLeft;
+ }
+
+ return {
+ width: element.prop('offsetWidth'),
+ 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: element.prop('offsetWidth'),
+ height: element.prop('offsetHeight'),
+ top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop),
+ left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft)
+ };
+ },
+
+ /**
+ * Provides the coordinates of the mouse
+ */
+ mouse: function () {
+ return {x: mouseX, y: mouseY};
+ }
+ };
+ }]);
+
+/**
+ * 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' ] )
+
+/**
+ * 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 setTriggers ( trigger ) {
+ var show, hide;
+
+ show = trigger || options.trigger || defaultTriggerShow;
+ if ( angular.isDefined ( options.trigger ) ) {
+ hide = triggerMap[options.trigger] || show;
+ } else {
+ hide = triggerMap[show] || show;
+ }
+
+ return {
+ show: show,
+ hide: hide
+ };
+ }
+
+ var directiveName = snake_case( type );
+ var triggers = setTriggers( undefined );
+
+ var startSym = $interpolate.startSymbol();
+ var endSym = $interpolate.endSymbol();
+ var template =
+ '<'+ directiveName +'-popup '+
+ 'title="'+startSym+'tt_title'+endSym+'" '+
+ 'content="'+startSym+'tt_content'+endSym+'" '+
+ 'placement="'+startSym+'tt_placement'+endSym+'" '+
+ 'animation="tt_animation()" '+
+ 'is-open="tt_isOpen"'+
+ '>'+
+ ''+ directiveName +'-popup>';
+
+ return {
+ restrict: 'EA',
+ scope: true,
+ link: function link ( scope, element, attrs ) {
+ var tooltip = $compile( template )( scope );
+ var transitionTimeout;
+ var popupTimeout;
+ var $body;
+ var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false;
+
+ // 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 ( scope.tt_popupDelay ) {
+ popupTimeout = $timeout( show, scope.tt_popupDelay );
+ } else {
+ scope.$apply( show );
+ }
+ }
+
+ function hideTooltipBind () {
+ scope.$apply(function () {
+ hide();
+ });
+ }
+
+ // Show the tooltip popup element.
+ function show() {
+ var position,
+ ttWidth,
+ ttHeight,
+ ttPosition;
+
+ // Don't show empty tooltips.
+ if ( ! scope.tt_content ) {
+ return;
+ }
+
+ // If there is a pending remove transition, we must cancel it, lest the
+ // tooltip be mysteriously removed.
+ if ( transitionTimeout ) {
+ $timeout.cancel( transitionTimeout );
+ }
+
+ // 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 ) {
+ $body = $body || $document.find( 'body' );
+ $body.append( tooltip );
+ } else {
+ element.after( tooltip );
+ }
+
+ // Get the position of the directive element.
+ position = options.appendToBody ? $position.offset( element ) : $position.position( element );
+
+ // Get the height and width of the tooltip so we can center it.
+ ttWidth = tooltip.prop( 'offsetWidth' );
+ ttHeight = tooltip.prop( 'offsetHeight' );
+
+ // Calculate the tooltip's top and left coordinates to center it with
+ // this directive.
+ switch ( scope.tt_placement ) {
+ case 'mouse':
+ var mousePos = $position.mouse();
+ ttPosition = {
+ top: mousePos.y,
+ left: mousePos.x
+ };
+ break;
+ case 'right':
+ ttPosition = {
+ top: position.top + position.height / 2 - ttHeight / 2,
+ left: position.left + position.width
+ };
+ break;
+ case 'bottom':
+ ttPosition = {
+ top: position.top + position.height,
+ left: position.left + position.width / 2 - ttWidth / 2
+ };
+ break;
+ case 'left':
+ ttPosition = {
+ top: position.top + position.height / 2 - ttHeight / 2,
+ left: position.left - ttWidth
+ };
+ break;
+ default:
+ ttPosition = {
+ top: position.top - ttHeight,
+ left: position.left + position.width / 2 - ttWidth / 2
+ };
+ break;
+ }
+
+ ttPosition.top += 'px';
+ ttPosition.left += 'px';
+
+ // Now set the calculated positioning.
+ tooltip.css( ttPosition );
+
+ // And show the tooltip.
+ scope.tt_isOpen = true;
+ }
+
+ // 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 );
+
+ // 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 ( angular.isDefined( scope.tt_animation ) && scope.tt_animation() ) {
+ transitionTimeout = $timeout( function () { tooltip.remove(); }, 500 );
+ } else {
+ tooltip.remove();
+ }
+ }
+
+ /**
+ * Observe the relevant attributes.
+ */
+ attrs.$observe( type, function ( val ) {
+ scope.tt_content = val;
+ });
+
+ 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+'Animation', function ( val ) {
+ scope.tt_animation = angular.isDefined( val ) ? $parse( val ) : function(){ return options.animation; };
+ });
+
+ attrs.$observe( prefix+'PopupDelay', function ( val ) {
+ var delay = parseInt( val, 10 );
+ scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay;
+ });
+
+ attrs.$observe( prefix+'Trigger', function ( val ) {
+ element.unbind( triggers.show );
+ element.unbind( triggers.hide );
+
+ triggers = setTriggers( val );
+
+ if ( triggers.show === triggers.hide ) {
+ element.bind( triggers.show, toggleTooltipBind );
+ } else {
+ element.bind( triggers.show, showTooltipBind );
+ element.bind( triggers.hide, hideTooltipBind );
+ }
+ });
+
+ 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() {
+ if ( scope.tt_isOpen ) {
+ hide();
+ } else {
+ tooltip.remove();
+ }
+ });
+ }
+ };
+ };
+ }];
+})
+
+.directive( 'tooltipPopup', function () {
+ return {
+ restrict: 'E',
+ 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: 'E',
+ 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', [ '$compile', '$timeout', '$parse', '$window', '$tooltip', function ( $compile, $timeout, $parse, $window, $tooltip ) {
+ return $tooltip( 'popover', 'popover', 'click' );
+}]);
+
+
+angular.module('ui.bootstrap.progressbar', ['ui.bootstrap.transition'])
+
+.constant('progressConfig', {
+ animate: true,
+ autoType: false,
+ stackedTypes: ['success', 'info', 'warning', 'danger']
+})
+
+.controller('ProgressBarController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) {
+
+ // Whether bar transitions should be animated
+ var animate = angular.isDefined($attrs.animate) ? $scope.$eval($attrs.animate) : progressConfig.animate;
+ var autoType = angular.isDefined($attrs.autoType) ? $scope.$eval($attrs.autoType) : progressConfig.autoType;
+ var stackedTypes = angular.isDefined($attrs.stackedTypes) ? $scope.$eval('[' + $attrs.stackedTypes + ']') : progressConfig.stackedTypes;
+
+ // Create bar object
+ this.makeBar = function(newBar, oldBar, index) {
+ var newValue = (angular.isObject(newBar)) ? newBar.value : (newBar || 0);
+ var oldValue = (angular.isObject(oldBar)) ? oldBar.value : (oldBar || 0);
+ var type = (angular.isObject(newBar) && angular.isDefined(newBar.type)) ? newBar.type : (autoType) ? getStackedType(index || 0) : null;
+
+ return {
+ from: oldValue,
+ to: newValue,
+ type: type,
+ animate: animate
+ };
+ };
+
+ function getStackedType(index) {
+ return stackedTypes[index];
+ }
+
+ this.addBar = function(bar) {
+ $scope.bars.push(bar);
+ $scope.totalPercent += bar.to;
+ };
+
+ this.clearBars = function() {
+ $scope.bars = [];
+ $scope.totalPercent = 0;
+ };
+ this.clearBars();
+}])
+
+.directive('progress', function() {
+ return {
+ restrict: 'EA',
+ replace: true,
+ controller: 'ProgressBarController',
+ scope: {
+ value: '=percent',
+ onFull: '&',
+ onEmpty: '&'
+ },
+ templateUrl: 'template/progressbar/progress.html',
+ link: function(scope, element, attrs, controller) {
+ scope.$watch('value', function(newValue, oldValue) {
+ controller.clearBars();
+
+ if (angular.isArray(newValue)) {
+ // Stacked progress bar
+ for (var i=0, n=newValue.length; i < n; i++) {
+ controller.addBar(controller.makeBar(newValue[i], oldValue[i], i));
+ }
+ } else {
+ // Simple bar
+ controller.addBar(controller.makeBar(newValue, oldValue));
+ }
+ }, true);
+
+ // Total percent listeners
+ scope.$watch('totalPercent', function(value) {
+ if (value >= 100) {
+ scope.onFull();
+ } else if (value <= 0) {
+ scope.onEmpty();
+ }
+ }, true);
+ }
+ };
+})
+
+.directive('progressbar', ['$transition', function($transition) {
+ return {
+ restrict: 'EA',
+ replace: true,
+ scope: {
+ width: '=',
+ old: '=',
+ type: '=',
+ animate: '='
+ },
+ templateUrl: 'template/progressbar/bar.html',
+ link: function(scope, element) {
+ scope.$watch('width', function(value) {
+ if (scope.animate) {
+ element.css('width', scope.old + '%');
+ $transition(element, {width: value + '%'});
+ } else {
+ element.css('width', value + '%');
+ }
+ });
+ }
+ };
+}]);
+angular.module('ui.bootstrap.rating', [])
+
+.constant('ratingConfig', {
+ max: 5
+})
+
+.directive('rating', ['ratingConfig', '$parse', function(ratingConfig, $parse) {
+ return {
+ restrict: 'EA',
+ scope: {
+ value: '='
+ },
+ templateUrl: 'template/rating/rating.html',
+ replace: true,
+ link: function(scope, element, attrs) {
+
+ var maxRange = angular.isDefined(attrs.max) ? scope.$eval(attrs.max) : ratingConfig.max;
+
+ scope.range = [];
+ for (var i = 1; i <= maxRange; i++) {
+ scope.range.push(i);
+ }
+
+ scope.rate = function(value) {
+ if ( ! scope.readonly ) {
+ scope.value = value;
+ }
+ };
+
+ scope.enter = function(value) {
+ if ( ! scope.readonly ) {
+ scope.val = value;
+ }
+ };
+
+ scope.reset = function() {
+ scope.val = angular.copy(scope.value);
+ };
+ scope.reset();
+
+ scope.$watch('value', function(value) {
+ scope.val = value;
+ });
+
+ scope.readonly = false;
+ if (attrs.readonly) {
+ scope.$parent.$watch($parse(attrs.readonly), function(value) {
+ scope.readonly = !!value;
+ });
+ }
+ }
+ };
+}]);
+
+/**
+ * @ngdoc overview
+ * @name ui.bootstrap.tabs
+ *
+ * @description
+ * AngularJS version of the tabs directive.
+ */
+
+angular.module('ui.bootstrap.tabs', [])
+
+.directive('tabs', function() {
+ return function() {
+ throw new Error("The `tabs` directive is deprecated, please migrate to `tabset`. Instructions can be found at http://github.com/angular-ui/bootstrap/tree/master/CHANGELOG.md");
+ };
+})
+
+.controller('TabsetController', ['$scope', '$element',
+function TabsetCtrl($scope, $element) {
+ var ctrl = this,
+ tabs = ctrl.tabs = $scope.tabs = [];
+
+ ctrl.select = function(tab) {
+ angular.forEach(tabs, function(tab) {
+ tab.active = false;
+ });
+ tab.active = true;
+ };
+
+ ctrl.addTab = function addTab(tab) {
+ tabs.push(tab);
+ if (tabs.length == 1) {
+ 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.
+ *
+ * @example
+
+
+
+ First Content!
+ Second Content!
+
+
+
+ First Vertical Content!
+ Second Vertical Content!
+
+
+
+ */
+.directive('tabset', function() {
+ return {
+ restrict: 'EA',
+ transclude: true,
+ scope: {},
+ controller: 'TabsetController',
+ templateUrl: 'template/tabs/tabset.html',
+ link: function(scope, element, attrs) {
+ scope.vertical = angular.isDefined(attrs.vertical) ? scope.$eval(attrs.vertical) : false;
+ scope.type = angular.isDefined(attrs.type) ? scope.$parent.$eval(attrs.type) : 'tabs';
+ }
+ };
+})
+
+/**
+ * @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', '$http', '$templateCache', '$compile',
+function($parse, $http, $templateCache, $compile) {
+ return {
+ require: '^tabset',
+ restrict: 'EA',
+ replace: true,
+ templateUrl: 'template/tabs/tab.html',
+ transclude: true,
+ scope: {
+ heading: '@',
+ onSelect: '&select' //This callback is called in contentHeadingTransclude
+ //once it inserts the tab's content into the dom
+ },
+ 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) {
+ var getActive, setActive;
+ scope.active = false; // default value
+ if (attrs.active) {
+ getActive = $parse(attrs.active);
+ setActive = getActive.assign;
+ scope.$parent.$watch(getActive, function updateActive(value) {
+ if ( !!value && scope.disabled ) {
+ setActive(scope.$parent, false); // Prevent active assignment
+ } else {
+ scope.active = !!value;
+ }
+ });
+ } else {
+ setActive = getActive = angular.noop;
+ }
+
+ scope.$watch('active', function(active) {
+ setActive(scope.$parent, active);
+ if (active) {
+ tabsetCtrl.select(scope);
+ scope.onSelect();
+ }
+ });
+
+ 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);
+ });
+ //If the tabset sets this tab to active, set the parent scope's active
+ //binding too. We do this so the watch for the parent's initial active
+ //value won't overwrite what is initially set by the tabset
+ if (scope.active) {
+ setActive(scope.$parent, true);
+ }
+
+ //Transclude the collection of sibling elements. Use forEach to find
+ //the heading if it exists. We don't use a directive for tab-heading
+ //because it is problematic. Discussion @ http://git.io/MSNPwQ
+ transclude(scope.$parent, function(clone) {
+ //Look at every element in the clone collection. If it's tab-heading,
+ //mark it as that. If it's not tab-heading, mark it as tab contents
+ var contents = [], heading;
+ angular.forEach(clone, function(el) {
+ //See if it's a tab-heading attr or element directive
+ //First make sure it's a normal element, one that has a tagName
+ if (el.tagName &&
+ (el.hasAttribute("tab-heading") ||
+ el.hasAttribute("data-tab-heading") ||
+ el.tagName.toLowerCase() == "tab-heading" ||
+ el.tagName.toLowerCase() == "data-tab-heading"
+ )) {
+ heading = el;
+ } else {
+ contents.push(el);
+ }
+ });
+ //Share what we found on the scope, so our tabHeadingTransclude and
+ //tabContentTransclude directives can find out what the heading and
+ //contents are.
+ if (heading) {
+ scope.headingElement = angular.element(heading);
+ }
+ scope.contentElement = angular.element(contents);
+ });
+ };
+ }
+ };
+}])
+
+.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', ['$parse', function($parse) {
+ return {
+ restrict: 'A',
+ require: '^tabset',
+ link: function(scope, elm, attrs, tabsetCtrl) {
+ scope.$watch($parse(attrs.tabContentTransclude), function(tab) {
+ elm.html('');
+ if (tab) {
+ elm.append(tab.contentElement);
+ }
+ });
+ }
+ };
+}])
+
+;
+
+
+angular.module('ui.bootstrap.timepicker', [])
+
+.filter('pad', function() {
+ return function(input) {
+ if ( angular.isDefined(input) && input.toString().length < 2 ) {
+ input = '0' + input;
+ }
+ return input;
+ };
+})
+
+.constant('timepickerConfig', {
+ hourStep: 1,
+ minuteStep: 1,
+ showMeridian: true,
+ meridians: ['AM', 'PM'],
+ readonlyInput: false,
+ mousewheel: true
+})
+
+.directive('timepicker', ['padFilter', '$parse', 'timepickerConfig', function (padFilter, $parse, timepickerConfig) {
+ return {
+ restrict: 'EA',
+ require:'ngModel',
+ replace: true,
+ templateUrl: 'template/timepicker/timepicker.html',
+ scope: {
+ model: '=ngModel'
+ },
+ link: function(scope, element, attrs, ngModelCtrl) {
+ var selected = new Date(), meridians = timepickerConfig.meridians;
+
+ 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 ( ! scope.model ) {
+ // Reset
+ var dt = new Date( selected );
+ var hours = getScopeHours();
+ if (angular.isDefined( hours )) {
+ dt.setHours( hours );
+ }
+ scope.model = new Date( dt );
+ } else {
+ refreshTemplate();
+ }
+ });
+ }
+
+ // Get scope.hours in 24H mode if valid
+ function getScopeHours ( ) {
+ var hours = parseInt( scope.hours, 10 );
+ var valid = ( scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24);
+ if ( !valid ) {
+ return;
+ }
+
+ if ( scope.showMeridian ) {
+ if ( hours === 12 ) {
+ hours = 0;
+ }
+ if ( scope.meridian === meridians[1] ) {
+ hours = hours + 12;
+ }
+ }
+ return hours;
+ }
+
+ // Input elements
+ var inputs = element.find('input');
+ var hoursInputEl = inputs.eq(0), minutesInputEl = inputs.eq(1);
+
+ // Respond on mousewheel spin
+ var mousewheel = (angular.isDefined(attrs.mousewheel)) ? scope.$eval(attrs.mousewheel) : timepickerConfig.mousewheel;
+ if ( mousewheel ) {
+
+ var isScrollingUp = function(e) {
+ if (e.originalEvent) {
+ e = e.originalEvent;
+ }
+ return (e.detail || e.wheelDelta > 0);
+ };
+
+ hoursInputEl.bind('mousewheel', function(e) {
+ scope.$apply( (isScrollingUp(e)) ? scope.incrementHours() : scope.decrementHours() );
+ e.preventDefault();
+ });
+
+ minutesInputEl.bind('mousewheel', function(e) {
+ scope.$apply( (isScrollingUp(e)) ? scope.incrementMinutes() : scope.decrementMinutes() );
+ e.preventDefault();
+ });
+ }
+
+ var keyboardChange = false;
+ scope.readonlyInput = (angular.isDefined(attrs.readonlyInput)) ? scope.$eval(attrs.readonlyInput) : timepickerConfig.readonlyInput;
+ if ( ! scope.readonlyInput ) {
+ scope.updateHours = function() {
+ var hours = getScopeHours();
+
+ if ( angular.isDefined(hours) ) {
+ keyboardChange = 'h';
+ if ( scope.model === null ) {
+ scope.model = new Date( selected );
+ }
+ scope.model.setHours( hours );
+ } else {
+ scope.model = null;
+ scope.validHours = false;
+ }
+ };
+
+ hoursInputEl.bind('blur', function(e) {
+ if ( scope.validHours && scope.hours < 10) {
+ scope.$apply( function() {
+ scope.hours = padFilter( scope.hours );
+ });
+ }
+ });
+
+ scope.updateMinutes = function() {
+ var minutes = parseInt(scope.minutes, 10);
+ if ( minutes >= 0 && minutes < 60 ) {
+ keyboardChange = 'm';
+ if ( scope.model === null ) {
+ scope.model = new Date( selected );
+ }
+ scope.model.setMinutes( minutes );
+ } else {
+ scope.model = null;
+ scope.validMinutes = false;
+ }
+ };
+
+ minutesInputEl.bind('blur', function(e) {
+ if ( scope.validMinutes && scope.minutes < 10 ) {
+ scope.$apply( function() {
+ scope.minutes = padFilter( scope.minutes );
+ });
+ }
+ });
+ } else {
+ scope.updateHours = angular.noop;
+ scope.updateMinutes = angular.noop;
+ }
+
+ scope.$watch( function getModelTimestamp() {
+ return +scope.model;
+ }, function( timestamp ) {
+ if ( !isNaN( timestamp ) && timestamp > 0 ) {
+ selected = new Date( timestamp );
+ refreshTemplate();
+ }
+ });
+
+ function refreshTemplate() {
+ var hours = selected.getHours();
+ if ( scope.showMeridian ) {
+ // Convert 24 to 12 hour system
+ hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12;
+ }
+ scope.hours = ( keyboardChange === 'h' ) ? hours : padFilter(hours);
+ scope.validHours = true;
+
+ var minutes = selected.getMinutes();
+ scope.minutes = ( keyboardChange === 'm' ) ? minutes : padFilter(minutes);
+ scope.validMinutes = true;
+
+ scope.meridian = ( scope.showMeridian ) ? (( selected.getHours() < 12 ) ? meridians[0] : meridians[1]) : '';
+
+ keyboardChange = false;
+ }
+
+ function addMinutes( minutes ) {
+ var dt = new Date( selected.getTime() + minutes * 60000 );
+ if ( dt.getDate() !== selected.getDate()) {
+ dt.setDate( dt.getDate() - 1 );
+ }
+ selected.setTime( dt.getTime() );
+ scope.model = new Date( selected );
+ }
+
+ 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) );
+ };
+ }
+ };
+}]);
+angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position'])
+
+/**
+ * A helper service that can parse typeahead's syntax (string provided by users)
+ * Extracted to a separate service for ease of unit testing
+ */
+ .factory('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), modelMapper, viewMapper, source;
+ 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) {
+
+ var selected;
+
+ //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;
+
+ //expressions used by typeahead
+ var parserResult = typeaheadParser.parse(attrs.typeahead);
+
+ //should it restrict model values to the ones selected from the popup only?
+ var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
+
+ var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
+
+ var onSelectCallback = $parse(attrs.typeaheadOnSelect);
+
+ //pop-up element used to display matches
+ var popUpEl = angular.element('
');
+ popUpEl.attr({
+ matches: 'matches',
+ active: 'activeIdx',
+ select: 'select(activeIdx)',
+ query: 'query',
+ position: 'position'
+ });
+
+ //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();
+ });
+
+ var resetMatches = function() {
+ scope.matches = [];
+ scope.activeIdx = -1;
+ };
+
+ var getMatchesAsync = function(inputValue) {
+
+ var locals = {$viewValue: inputValue};
+ isLoadingSetter(originalScope, true);
+ $q.when(parserResult.source(scope, 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
+ if (inputValue === modelCtrl.$viewValue) {
+ if (matches.length > 0) {
+
+ scope.activeIdx = 0;
+ scope.matches.length = 0;
+
+ //transform labels
+ for(var i=0; i
= minSearch) {
+ if (waitTime > 0) {
+ if (timeoutId) {
+ $timeout.cancel(timeoutId);//cancel previous timeout
+ }
+ timeoutId = $timeout(function () {
+ getMatchesAsync(inputValue);
+ }, waitTime);
+ } else {
+ getMatchesAsync(inputValue);
+ }
+ }
+ }
+
+ return isEditable ? inputValue : undefined;
+ });
+
+ modelCtrl.$render = function () {
+ var locals = {};
+ locals[parserResult.itemName] = selected || modelCtrl.$viewValue;
+ element.val(parserResult.viewMapper(scope, locals) || modelCtrl.$viewValue);
+ selected = undefined;
+ };
+
+ scope.select = function (activeIdx) {
+ //called from within the $digest() cycle
+ var locals = {};
+ var model, item;
+ locals[parserResult.itemName] = item = selected = scope.matches[activeIdx].model;
+
+ model = parserResult.modelMapper(scope, locals);
+ modelCtrl.$setViewValue(model);
+ modelCtrl.$render();
+ onSelectCallback(scope, {
+ $item: item,
+ $model: model,
+ $label: parserResult.viewMapper(scope, locals)
+ });
+
+ element[0].focus();
+ };
+
+ //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();
+ }
+ });
+
+ $document.bind('click', function(){
+ resetMatches();
+ scope.$digest();
+ });
+
+ element.after($compile(popUpEl)(scope));
+ }
+ };
+
+}])
+
+ .directive('typeaheadPopup', function () {
+ return {
+ restrict:'E',
+ scope:{
+ matches:'=',
+ query:'=',
+ active:'=',
+ position:'=',
+ select:'&'
+ },
+ replace:true,
+ templateUrl:'template/typeahead/typeahead.html',
+ link:function (scope, element, attrs) {
+
+ 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});
+ };
+ }
+ };
+ })
+
+ .filter('typeaheadHighlight', function() {
+
+ function escapeRegexp(queryToEscape) {
+ return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
+ }
+
+ return function(matchItem, query) {
+ return query ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : query;
+ };
+ });
+
+angular.module("template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache) {
+ $templateCache.put("template/accordion/accordion-group.html",
+ "\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" +
+ " \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" +
+ " | \n" +
+ " | \n" +
+ "
\n" +
+ " 0\">\n" +
+ " # | \n" +
+ " {{label}} | \n" +
+ "
\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " {{ getWeekNumber(row) }} | \n" +
+ " \n" +
+ " \n" +
+ " | \n" +
+ "
\n" +
+ " \n" +
+ "
\n" +
+ "");
+}]);
+
+angular.module("template/dialog/message.html", []).run(["$templateCache", function($templateCache) {
+ $templateCache.put("template/dialog/message.html",
+ "\n" +
+ "\n" +
+ "
{{ message }}
\n" +
+ "
\n" +
+ "\n" +
+ "");
+}]);
+
+angular.module("template/modal/backdrop.html", []).run(["$templateCache", function($templateCache) {
+ $templateCache.put("template/modal/backdrop.html",
+ "");
+}]);
+
+angular.module("template/modal/window.html", []).run(["$templateCache", function($templateCache) {
+ $templateCache.put("template/modal/window.html",
+ "");
+}]);
+
+angular.module("template/pagination/pager.html", []).run(["$templateCache", function($templateCache) {
+ $templateCache.put("template/pagination/pager.html",
+ "\n" +
+ "");
+}]);
+
+angular.module("template/pagination/pagination.html", []).run(["$templateCache", function($templateCache) {
+ $templateCache.put("template/pagination/pagination.html",
+ "\n" +
+ "");
+}]);
+
+angular.module("template/tooltip/tooltip-html-unsafe-popup.html", []).run(["$templateCache", function($templateCache) {
+ $templateCache.put("template/tooltip/tooltip-html-unsafe-popup.html",
+ "\n" +
+ "");
+}]);
+
+angular.module("template/tooltip/tooltip-popup.html", []).run(["$templateCache", function($templateCache) {
+ $templateCache.put("template/tooltip/tooltip-popup.html",
+ "\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/rating/rating.html", []).run(["$templateCache", function($templateCache) {
+ $templateCache.put("template/rating/rating.html",
+ "\n" +
+ " val}\">\n" +
+ "\n" +
+ "");
+}]);
+
+angular.module("template/tabs/pane.html", []).run(["$templateCache", function($templateCache) {
+ $templateCache.put("template/tabs/pane.html",
+ "\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/tabs.html", []).run(["$templateCache", function($templateCache) {
+ $templateCache.put("template/tabs/tabs.html",
+ "\n" +
+ "
\n" +
+ "
\n" +
+ "
\n" +
+ "");
+}]);
+
+angular.module("template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) {
+ $templateCache.put("template/tabs/tabset.html",
+ "\n" +
+ "\n" +
+ "
\n" +
+ "
\n" +
+ "
\n" +
+ "
\n" +
+ "
\n" +
+ "
\n" +
+ "");
+}]);
+
+angular.module("template/timepicker/timepicker.html", []).run(["$templateCache", function($templateCache) {
+ $templateCache.put("template/timepicker/timepicker.html",
+ "");
+}]);
+
+angular.module("template/typeahead/typeahead.html", []).run(["$templateCache", function($templateCache) {
+ $templateCache.put("template/typeahead/typeahead.html",
+ "\n" +
+ " - \n" +
+ " \n" +
+ "
\n" +
+ "
");
+}]);
\ No newline at end of file
diff --git a/docs-web/src/main/webapp/lib/angular.ui-router.js b/docs-web/src/main/webapp/lib/angular.ui-router.js
new file mode 100644
index 00000000..285410c2
--- /dev/null
+++ b/docs-web/src/main/webapp/lib/angular.ui-router.js
@@ -0,0 +1,1037 @@
+/**
+ * State-based routing for AngularJS
+ * @version v0.0.1
+ * @link http://angular-ui.github.com/
+ * @license MIT License, http://www.opensource.org/licenses/MIT
+ */
+(function (window, angular, undefined) {
+/*jshint globalstrict:true*/
+/*global angular:false*/
+'use strict';
+
+var isDefined = angular.isDefined,
+ isFunction = angular.isFunction,
+ isString = angular.isString,
+ isObject = angular.isObject,
+ isArray = angular.isArray,
+ forEach = angular.forEach,
+ extend = angular.extend,
+ copy = angular.copy;
+
+function inherit(parent, extra) {
+ return extend(new (extend(function() {}, { prototype: parent }))(), extra);
+}
+
+/**
+ * Extends the destination object `dst` by copying all of the properties from the `src` object(s)
+ * to `dst` if the `dst` object has no own property of the same name. You can specify multiple
+ * `src` objects.
+ *
+ * @param {Object} dst Destination object.
+ * @param {...Object} src Source object(s).
+ * @see angular.extend
+ */
+function merge(dst) {
+ forEach(arguments, function(obj) {
+ if (obj !== dst) {
+ forEach(obj, function(value, key) {
+ if (!dst.hasOwnProperty(key)) dst[key] = value;
+ });
+ }
+ });
+ return dst;
+}
+
+angular.module('ui.util', ['ng']);
+angular.module('ui.router', ['ui.util']);
+angular.module('ui.state', ['ui.router', 'ui.util']);
+angular.module('ui.compat', ['ui.state']);
+
+/**
+ * Service. Manages loading of templates.
+ * @constructor
+ * @name $templateFactory
+ * @requires $http
+ * @requires $templateCache
+ * @requires $injector
+ */
+$TemplateFactory.$inject = ['$http', '$templateCache', '$injector'];
+function $TemplateFactory( $http, $templateCache, $injector) {
+
+ /**
+ * Creates a template from a configuration object.
+ * @function
+ * @name $templateFactory#fromConfig
+ * @methodOf $templateFactory
+ * @param {Object} config Configuration object for which to load a template. The following
+ * properties are search in the specified order, and the first one that is defined is
+ * used to create the template:
+ * @param {string|Function} config.template html string template or function to load via
+ * {@link $templateFactory#fromString fromString}.
+ * @param {string|Function} config.templateUrl url to load or a function returning the url
+ * to load via {@link $templateFactory#fromUrl fromUrl}.
+ * @param {Function} config.templateProvider function to invoke via
+ * {@link $templateFactory#fromProvider fromProvider}.
+ * @param {Object} params Parameters to pass to the template function.
+ * @param {Object} [locals] Locals to pass to `invoke` if the template is loaded via a
+ * `templateProvider`. Defaults to `{ params: params }`.
+ * @return {string|Promise.} The template html as a string, or a promise for that string,
+ * or `null` if no template is configured.
+ */
+ this.fromConfig = function (config, params, locals) {
+ return (
+ isDefined(config.template) ? this.fromString(config.template, params) :
+ isDefined(config.templateUrl) ? this.fromUrl(config.templateUrl, params) :
+ isDefined(config.templateProvider) ? this.fromProvider(config.templateProvider, params, locals) :
+ null
+ );
+ };
+
+ /**
+ * Creates a template from a string or a function returning a string.
+ * @function
+ * @name $templateFactory#fromString
+ * @methodOf $templateFactory
+ * @param {string|Function} template html template as a string or function that returns an html
+ * template as a string.
+ * @param {Object} params Parameters to pass to the template function.
+ * @return {string|Promise.} The template html as a string, or a promise for that string.
+ */
+ this.fromString = function (template, params) {
+ return isFunction(template) ? template(params) : template;
+ };
+
+ /**
+ * Loads a template from the a URL via `$http` and `$templateCache`.
+ * @function
+ * @name $templateFactory#fromUrl
+ * @methodOf $templateFactory
+ * @param {string|Function} url url of the template to load, or a function that returns a url.
+ * @param {Object} params Parameters to pass to the url function.
+ * @return {string|Promise.} The template html as a string, or a promise for that string.
+ */
+ this.fromUrl = function (url, params) {
+ if (isFunction(url)) url = url(params);
+ if (url == null) return null;
+ else return $http
+ .get(url, { cache: $templateCache })
+ .then(function(response) { return response.data; });
+ };
+
+ /**
+ * Creates a template by invoking an injectable provider function.
+ * @function
+ * @name $templateFactory#fromUrl
+ * @methodOf $templateFactory
+ * @param {Function} provider Function to invoke via `$injector.invoke`
+ * @param {Object} params Parameters for the template.
+ * @param {Object} [locals] Locals to pass to `invoke`. Defaults to `{ params: params }`.
+ * @return {string|Promise.} The template html as a string, or a promise for that string.
+ */
+ this.fromProvider = function (provider, params, locals) {
+ return $injector.invoke(provider, null, locals || { params: params });
+ };
+}
+
+angular.module('ui.util').service('$templateFactory', $TemplateFactory);
+
+/**
+ * Matches URLs against patterns and extracts named parameters from the path or the search
+ * part of the URL. A URL pattern consists of a path pattern, optionally followed by '?' and a list
+ * of search parameters. Multiple search parameter names are separated by '&'. Search parameters
+ * do not influence whether or not a URL is matched, but their values are passed through into
+ * the matched parameters returned by {@link UrlMatcher#exec exec}.
+ *
+ * Path parameter placeholders can be specified using simple colon/catch-all syntax or curly brace
+ * syntax, which optionally allows a regular expression for the parameter to be specified:
+ *
+ * * ':' name - colon placeholder
+ * * '*' name - catch-all placeholder
+ * * '{' name '}' - curly placeholder
+ * * '{' name ':' regexp '}' - curly placeholder with regexp. Should the regexp itself contain
+ * curly braces, they must be in matched pairs or escaped with a backslash.
+ *
+ * Parameter names may contain only word characters (latin letters, digits, and underscore) and
+ * must be unique within the pattern (across both path and search parameters). For colon
+ * placeholders or curly placeholders without an explicit regexp, a path parameter matches any
+ * number of characters other than '/'. For catch-all placeholders the path parameter matches
+ * any number of characters.
+ *
+ * ### Examples
+ *
+ * * '/hello/' - Matches only if the path is exactly '/hello/'. There is no special treatment for
+ * trailing slashes, and patterns have to match the entire path, not just a prefix.
+ * * '/user/:id' - Matches '/user/bob' or '/user/1234!!!' or even '/user/' but not '/user' or
+ * '/user/bob/details'. The second path segment will be captured as the parameter 'id'.
+ * * '/user/{id}' - Same as the previous example, but using curly brace syntax.
+ * * '/user/{id:[^/]*}' - Same as the previous example.
+ * * '/user/{id:[0-9a-fA-F]{1,8}}' - Similar to the previous example, but only matches if the id
+ * parameter consists of 1 to 8 hex digits.
+ * * '/files/{path:.*}' - Matches any URL starting with '/files/' and captures the rest of the
+ * path into the parameter 'path'.
+ * * '/files/*path' - ditto.
+ *
+ * @constructor
+ * @param {string} pattern the pattern to compile into a matcher.
+ *
+ * @property {string} prefix A static prefix of this pattern. The matcher guarantees that any
+ * URL matching this matcher (i.e. any string for which {@link UrlMatcher#exec exec()} returns
+ * non-null) will start with this prefix.
+ */
+function UrlMatcher(pattern) {
+
+ // Find all placeholders and create a compiled pattern, using either classic or curly syntax:
+ // '*' name
+ // ':' name
+ // '{' name '}'
+ // '{' name ':' regexp '}'
+ // The regular expression is somewhat complicated due to the need to allow curly braces
+ // inside the regular expression. The placeholder regexp breaks down as follows:
+ // ([:*])(\w+) classic placeholder ($1 / $2)
+ // \{(\w+)(?:\:( ... ))?\} curly brace placeholder ($3) with optional regexp ... ($4)
+ // (?: ... | ... | ... )+ the regexp consists of any number of atoms, an atom being either
+ // [^{}\\]+ - anything other than curly braces or backslash
+ // \\. - a backslash escape
+ // \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms
+ var placeholder = /([:*])(\w+)|\{(\w+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,
+ names = {}, compiled = '^', last = 0, m,
+ segments = this.segments = [],
+ params = this.params = [];
+
+ function addParameter(id) {
+ if (!/^\w+$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'");
+ if (names[id]) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'");
+ names[id] = true;
+ params.push(id);
+ }
+
+ function quoteRegExp(string) {
+ return string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&");
+ }
+
+ this.source = pattern;
+
+ // Split into static segments separated by path parameter placeholders.
+ // The number of segments is always 1 more than the number of parameters.
+ var id, regexp, segment;
+ while ((m = placeholder.exec(pattern))) {
+ id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null
+ regexp = m[4] || (m[1] == '*' ? '.*' : '[^/]*');
+ segment = pattern.substring(last, m.index);
+ if (segment.indexOf('?') >= 0) break; // we're into the search part
+ compiled += quoteRegExp(segment) + '(' + regexp + ')';
+ addParameter(id);
+ segments.push(segment);
+ last = placeholder.lastIndex;
+ }
+ segment = pattern.substring(last);
+
+ // Find any search parameter names and remove them from the last segment
+ var i = segment.indexOf('?');
+ if (i >= 0) {
+ var search = this.sourceSearch = segment.substring(i);
+ segment = segment.substring(0, i);
+ this.sourcePath = pattern.substring(0, last+i);
+
+ // Allow parameters to be separated by '?' as well as '&' to make concat() easier
+ forEach(search.substring(1).split(/[&?]/), addParameter);
+ } else {
+ this.sourcePath = pattern;
+ this.sourceSearch = '';
+ }
+
+ compiled += quoteRegExp(segment) + '$';
+ segments.push(segment);
+ this.regexp = new RegExp(compiled);
+ this.prefix = segments[0];
+}
+
+/**
+ * Returns a new matcher for a pattern constructed by appending the path part and adding the
+ * search parameters of the specified pattern to this pattern. The current pattern is not
+ * modified. This can be understood as creating a pattern for URLs that are relative to (or
+ * suffixes of) the current pattern.
+ *
+ * ### Example
+ * The following two matchers are equivalent:
+ * ```
+ * new UrlMatcher('/user/{id}?q').concat('/details?date');
+ * new UrlMatcher('/user/{id}/details?q&date');
+ * ```
+ *
+ * @param {string} pattern The pattern to append.
+ * @return {UrlMatcher} A matcher for the concatenated pattern.
+ */
+UrlMatcher.prototype.concat = function (pattern) {
+ // Because order of search parameters is irrelevant, we can add our own search
+ // parameters to the end of the new pattern. Parse the new pattern by itself
+ // and then join the bits together, but it's much easier to do this on a string level.
+ return new UrlMatcher(this.sourcePath + pattern + this.sourceSearch);
+};
+
+UrlMatcher.prototype.toString = function () {
+ return this.source;
+};
+
+/**
+ * Tests the specified path against this matcher, and returns an object containing the captured
+ * parameter values, or null if the path does not match. The returned object contains the values
+ * of any search parameters that are mentioned in the pattern, but their value may be null if
+ * they are not present in `searchParams`. This means that search parameters are always treated
+ * as optional.
+ *
+ * ### Example
+ * ```
+ * new UrlMatcher('/user/{id}?q&r').exec('/user/bob', { x:'1', q:'hello' });
+ * // returns { id:'bob', q:'hello', r:null }
+ * ```
+ *
+ * @param {string} path The URL path to match, e.g. `$location.path()`.
+ * @param {Object} searchParams URL search parameters, e.g. `$location.search()`.
+ * @return {Object} The captured parameter values.
+ */
+UrlMatcher.prototype.exec = function (path, searchParams) {
+ var m = this.regexp.exec(path);
+ if (!m) return null;
+
+ var params = this.params, nTotal = params.length,
+ nPath = this.segments.length-1,
+ values = {}, i;
+
+ for (i=0; i} An array of parameter names. Must be treated as read-only. If the
+ * pattern has no parameters, an empty array is returned.
+ */
+UrlMatcher.prototype.parameters = function () {
+ return this.params;
+};
+
+/**
+ * Creates a URL that matches this pattern by substituting the specified values
+ * for the path and search parameters. Null values for path parameters are
+ * treated as empty strings.
+ *
+ * ### Example
+ * ```
+ * new UrlMatcher('/user/{id}?q').format({ id:'bob', q:'yes' });
+ * // returns '/user/bob?q=yes'
+ * ```
+ *
+ * @param {Object} values the values to substitute for the parameters in this pattern.
+ * @return {string} the formatted URL (path and optionally search part).
+ */
+UrlMatcher.prototype.format = function (values) {
+ var segments = this.segments, params = this.params;
+ if (!values) return segments.join('');
+
+ var nPath = segments.length-1, nTotal = params.length,
+ result = segments[0], i, search, value;
+
+ for (i=0; i= 0) throw new Error("State must have a valid name");
+ if (states[name]) throw new Error("State '" + name + "'' is already defined");
+
+ // Derive parent state from a hierarchical name only if 'parent' is not explicitly defined.
+ var parent = root;
+ if (!isDefined(state.parent)) {
+ // regex matches any valid composite state name
+ // would match "contact.list" but not "contacts"
+ var compositeName = /^(.+)\.[^.]+$/.exec(name);
+ if (compositeName != null) {
+ parent = findState(compositeName[1]);
+ }
+ } else if (state.parent != null) {
+ parent = findState(state.parent);
+ }
+ state.parent = parent;
+ // state.children = [];
+ // if (parent) parent.children.push(state);
+
+ // Build a URLMatcher if necessary, either via a relative or absolute URL
+ var url = state.url;
+ if (isString(url)) {
+ if (url.charAt(0) == '^') {
+ url = state.url = $urlMatcherFactory.compile(url.substring(1));
+ } else {
+ url = state.url = (parent.navigable || root).url.concat(url);
+ }
+ } else if (isObject(url) &&
+ isFunction(url.exec) && isFunction(url.format) && isFunction(url.concat)) {
+ /* use UrlMatcher (or compatible object) as is */
+ } else if (url != null) {
+ throw new Error("Invalid url '" + url + "' in state '" + state + "'");
+ }
+
+ // Keep track of the closest ancestor state that has a URL (i.e. is navigable)
+ state.navigable = url ? state : parent ? parent.navigable : null;
+
+ // Derive parameters for this state and ensure they're a super-set of parent's parameters
+ var params = state.params;
+ if (params) {
+ if (!isArray(params)) throw new Error("Invalid params in state '" + state + "'");
+ if (url) throw new Error("Both params and url specicified in state '" + state + "'");
+ } else {
+ params = state.params = url ? url.parameters() : state.parent.params;
+ }
+
+ var paramNames = {}; forEach(params, function (p) { paramNames[p] = true; });
+ if (parent) {
+ forEach(parent.params, function (p) {
+ if (!paramNames[p]) {
+ throw new Error("Missing required parameter '" + p + "' in state '" + name + "'");
+ }
+ paramNames[p] = false;
+ });
+
+ var ownParams = state.ownParams = [];
+ forEach(paramNames, function (own, p) {
+ if (own) ownParams.push(p);
+ });
+ } else {
+ state.ownParams = params;
+ }
+
+ // If there is no explicit multi-view configuration, make one up so we don't have
+ // to handle both cases in the view directive later. Note that having an explicit
+ // 'views' property will mean the default unnamed view properties are ignored. This
+ // is also a good time to resolve view names to absolute names, so everything is a
+ // straight lookup at link time.
+ var views = {};
+ forEach(isDefined(state.views) ? state.views : { '': state }, function (view, name) {
+ if (name.indexOf('@') < 0) name = name + '@' + state.parent.name;
+ views[name] = view;
+ });
+ state.views = views;
+
+ // Keep a full path from the root down to this state as this is needed for state activation.
+ state.path = parent ? parent.path.concat(state) : []; // exclude root from path
+
+ // Speed up $state.contains() as it's used a lot
+ var includes = state.includes = parent ? extend({}, parent.includes) : {};
+ includes[name] = true;
+
+ if (!state.resolve) state.resolve = {}; // prevent null checks later
+
+ // Register the state in the global state list and with $urlRouter if necessary.
+ if (!state['abstract'] && url) {
+ $urlRouterProvider.when(url, ['$match', function ($match) {
+ $state.transitionTo(state, $match, false);
+ }]);
+ }
+ states[name] = state;
+ return state;
+ }
+
+ // Implicit root state that is always active
+ root = registerState({
+ name: '',
+ url: '^',
+ views: null,
+ 'abstract': true
+ });
+ root.locals = { globals: { $stateParams: {} } };
+ root.navigable = null;
+
+
+ // .state(state)
+ // .state(name, state)
+ this.state = state;
+ function state(name, definition) {
+ /*jshint validthis: true */
+ if (isObject(name)) definition = name;
+ else definition.name = name;
+ registerState(definition);
+ return this;
+ }
+
+ // $urlRouter is injected just to ensure it gets instantiated
+ this.$get = $get;
+ $get.$inject = ['$rootScope', '$q', '$templateFactory', '$injector', '$stateParams', '$location', '$urlRouter'];
+ function $get( $rootScope, $q, $templateFactory, $injector, $stateParams, $location, $urlRouter) {
+
+ var TransitionSuperseded = $q.reject(new Error('transition superseded'));
+ var TransitionPrevented = $q.reject(new Error('transition prevented'));
+
+ $state = {
+ params: {},
+ current: root.self,
+ $current: root,
+ transition: null
+ };
+
+ // $state.go = function go(to, params) {
+ // };
+
+ $state.transitionTo = function transitionTo(to, toParams, updateLocation) {
+ if (!isDefined(updateLocation)) updateLocation = true;
+
+ to = findState(to);
+ if (to['abstract']) throw new Error("Cannot transition to abstract state '" + to + "'");
+ var toPath = to.path,
+ from = $state.$current, fromParams = $state.params, fromPath = from.path;
+
+ // Starting from the root of the path, keep all levels that haven't changed
+ var keep, state, locals = root.locals, toLocals = [];
+ for (keep = 0, state = toPath[keep];
+ state && state === fromPath[keep] && equalForKeys(toParams, fromParams, state.ownParams);
+ keep++, state = toPath[keep]) {
+ locals = toLocals[keep] = state.locals;
+ }
+
+ // If we're going to the same state and all locals are kept, we've got nothing to do.
+ // But clear 'transition', as we still want to cancel any other pending transitions.
+ // TODO: We may not want to bump 'transition' if we're called from a location change that we've initiated ourselves,
+ // because we might accidentally abort a legitimate transition initiated from code?
+ if (to === from && locals === from.locals) {
+ $state.transition = null;
+ return $q.when($state.current);
+ }
+
+ // Normalize/filter parameters before we pass them to event handlers etc.
+ toParams = normalize(to.params, toParams || {});
+
+ // Broadcast start event and cancel the transition if requested
+ if ($rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams)
+ .defaultPrevented) return TransitionPrevented;
+
+ // Resolve locals for the remaining states, but don't update any global state just
+ // yet -- if anything fails to resolve the current state needs to remain untouched.
+ // We also set up an inheritance chain for the locals here. This allows the view directive
+ // to quickly look up the correct definition for each view in the current state. Even
+ // though we create the locals object itself outside resolveState(), it is initially
+ // empty and gets filled asynchronously. We need to keep track of the promise for the
+ // (fully resolved) current locals, and pass this down the chain.
+ var resolved = $q.when(locals);
+ for (var l=keep; l=keep; l--) {
+ exiting = fromPath[l];
+ if (exiting.self.onExit) {
+ $injector.invoke(exiting.self.onExit, exiting.self, exiting.locals.globals);
+ }
+ exiting.locals = null;
+ }
+
+ // Enter 'to' states not kept
+ for (l=keep; l ').html(locals.$template).contents();
+ animate.enter(contents, element);
+ } else {
+ element.html(locals.$template);
+ contents = element.contents();
+ }
+
+ var link = $compile(contents);
+ viewScope = scope.$new();
+ if (locals.$$controller) {
+ locals.$scope = viewScope;
+ var controller = $controller(locals.$$controller, locals);
+ element.children().data('$ngControllerController', controller);
+ }
+ link(viewScope);
+ viewScope.$emit('$viewContentLoaded');
+ viewScope.$eval(onloadExp);
+
+ // TODO: This seems strange, shouldn't $anchorScroll listen for $viewContentLoaded if necessary?
+ // $anchorScroll might listen on event...
+ $anchorScroll();
+ } else {
+ viewLocals = null;
+ view.state = null;
+ }
+ }
+ }
+ };
+ return directive;
+}
+
+angular.module('ui.state').directive('uiView', $ViewDirective);
+
+$RouteProvider.$inject = ['$stateProvider', '$urlRouterProvider'];
+function $RouteProvider( $stateProvider, $urlRouterProvider) {
+
+ var routes = [];
+
+ onEnterRoute.$inject = ['$$state'];
+ function onEnterRoute( $$state) {
+ /*jshint validthis: true */
+ this.locals = $$state.locals.globals;
+ this.params = this.locals.$stateParams;
+ }
+
+ function onExitRoute() {
+ /*jshint validthis: true */
+ this.locals = null;
+ this.params = null;
+ }
+
+ this.when = when;
+ function when(url, route) {
+ /*jshint validthis: true */
+ if (route.redirectTo != null) {
+ // Redirect, configure directly on $urlRouterProvider
+ var redirect = route.redirectTo, handler;
+ if (isString(redirect)) {
+ handler = redirect; // leave $urlRouterProvider to handle
+ } else if (isFunction(redirect)) {
+ // Adapt to $urlRouterProvider API
+ handler = function (params, $location) {
+ return redirect(params, $location.path(), $location.search());
+ };
+ } else {
+ throw new Error("Invalid 'redirectTo' in when()");
+ }
+ $urlRouterProvider.when(url, handler);
+ } else {
+ // Regular route, configure as state
+ $stateProvider.state(inherit(route, {
+ parent: null,
+ name: 'route:' + encodeURIComponent(url),
+ url: url,
+ onEnter: onEnterRoute,
+ onExit: onExitRoute
+ }));
+ }
+ routes.push(route);
+ return this;
+ }
+
+ this.$get = $get;
+ $get.$inject = ['$state', '$rootScope', '$routeParams'];
+ function $get( $state, $rootScope, $routeParams) {
+
+ var $route = {
+ routes: routes,
+ params: $routeParams,
+ current: undefined
+ };
+
+ function stateAsRoute(state) {
+ return (state.name !== '') ? state : undefined;
+ }
+
+ $rootScope.$on('$stateChangeStart', function (ev, to, toParams, from, fromParams) {
+ $rootScope.$broadcast('$routeChangeStart', stateAsRoute(to), stateAsRoute(from));
+ });
+
+ $rootScope.$on('$stateChangeSuccess', function (ev, to, toParams, from, fromParams) {
+ $route.current = stateAsRoute(to);
+ $rootScope.$broadcast('$routeChangeSuccess', stateAsRoute(to), stateAsRoute(from));
+ copy(toParams, $route.params);
+ });
+
+ $rootScope.$on('$stateChangeError', function (ev, to, toParams, from, fromParams, error) {
+ $rootScope.$broadcast('$routeChangeError', stateAsRoute(to), stateAsRoute(from), error);
+ });
+
+ return $route;
+ }
+}
+
+angular.module('ui.compat')
+ .provider('$route', $RouteProvider)
+ .directive('ngView', $ViewDirective);
+})(window, window.angular);
\ No newline at end of file
diff --git a/docs-web/src/main/webapp/lib/angular/angular-cookies.js b/docs-web/src/main/webapp/lib/angular/angular-cookies.js
new file mode 100644
index 00000000..78686e23
--- /dev/null
+++ b/docs-web/src/main/webapp/lib/angular/angular-cookies.js
@@ -0,0 +1,185 @@
+/**
+ * @license AngularJS v1.1.5
+ * (c) 2010-2012 Google, Inc. http://angularjs.org
+ * License: MIT
+ */
+(function(window, angular, undefined) {
+'use strict';
+
+/**
+ * @ngdoc overview
+ * @name ngCookies
+ */
+
+
+angular.module('ngCookies', ['ng']).
+ /**
+ * @ngdoc object
+ * @name ngCookies.$cookies
+ * @requires $browser
+ *
+ * @description
+ * Provides read/write access to browser's cookies.
+ *
+ * Only a simple Object is exposed and by adding or removing properties to/from
+ * this object, new cookies are created/deleted at the end of current $eval.
+ *
+ * @example
+