diff --git a/Gruntfile.js b/Gruntfile.js index a13445c..a43043a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -29,11 +29,13 @@ module.exports = function (grunt) { watch: { js: { files: ['{.tmp,<%= yeoman.app %>}/scripts/{,*/}*.js'], - tasks: ['newer:jshint:all', 'test'] + //tasks: ['newer:jshint:all', 'test'] + tasks: ['newer:jshint:all'] }, jsTest: { files: ['test/spec/{,*/}*.js'], - tasks: ['newer:jshint:test', 'karma'] + //tasks: ['newer:jshint:test', 'karma'] + tasks: ['karma'] }, compass: { files: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'], diff --git a/app/scripts/aceEditor/AceCtrl.js b/app/scripts/aceEditor/AceCtrl.js new file mode 100644 index 0000000..fa9d32b --- /dev/null +++ b/app/scripts/aceEditor/AceCtrl.js @@ -0,0 +1,114 @@ +'use strict'; + +define(['controllers/controllers'], function(controllers) { + + controllers.controller('AceCtrl', ['$scope', function($scope) { + + $scope.getSelection = function() { + var editor = $scope.editor; + if (editor) { + // TEST: log the text + console.log($scope.editor.session.getTextRange(editor.getSelectionRange())); + + //get selection range + var r = editor.getSelectionRange(); + // Now add some markup to this text + //add marker + var session = editor.session; + r.start = session.doc.createAnchor(r.start); + r.end = session.doc.createAnchor(r.end); + //r.id = session.addMarker(r, "ace_step", "text") + + // the last element tells us whether to put the marker in front of the text or behind it + // true = in front, false = behind + // there are two marker layers + r.id = session.addMarker(r, "was-selected", "text", false); + + // TODO: what does addDynamicMarker do? + //r.id = session.addDynamicMarker(r, "was-selected", "text", false); + + // TODO: move this UI-logic into a dynamically-creatable directive + // Another option is adding the directive into the Ace marker creation template + $('.was-selected').css('background-color', 'yellow'); + $('.was-selected').addClass('selectedByJquery'); + + $('.was-selected').draggable({ + revert: function(droppable) { + if (!droppable) { + d("reverting to orginal position"); + return true; + } else { + return false; + } + }, + start: function(ev, ui) { + var $elem = $(ev.target); + d("you started dragging a draggable"); + // TODO: disable any droppables attached to this element when the whole element is being dragged + var $gaps = $elem.children('.ui-droppable'); + $gaps.droppable('option', 'disabled', true); + + var $token = $(ev.target); + + // TODO: tests only! + //$token.addClass('in-drag'); + $token.addClass('i-was-dragged'); + //$token.removeClass('i-was-dragged'); + }, + stop: function(ev, ui) { + var $elem = $(ev.target); + var $gaps = $elem.children('.ui-droppable'); + $gaps.droppable('option', 'disabled', false); + + var $token = $(ev.target); + //$token.removeClass('in-drag'); + + } + }); + // TODO: add logic to handle splitting the marker when user types + } else { + d("ERROR: no editor on the scope!"); + } + } + + $scope.insertText = function(text) { + var editor = $scope.editor; + editor.insert(text); + } + + $scope.currentPrefix = function() { + var editor = $scope.editor; + console.log(editor.getValue()); + } + + // Use this function to configure the ace editor instance + $scope.aceLoaded = function (_editor) { + var editor = _editor; + // TODO: move styling to the view + editor.setShowPrintMargin(false); + + // TODO: how to limit the Ace editor to a certain number of lines? + //editor.setOption("maxLines", 1); + //editor.setOptions({ + // maxLines: 1 + //}); + + $scope.startText = "gloss over source to see the target phrase alignment"; +// TODO: see moses - how to get translation alignment? + $scope.editor = editor; + editor.setFontSize(20); + editor.setValue($scope.startText); + //editor.setTheme("ace/theme/twilight"); // Note: the editor themes are called by their string names (these are not paths) + //console.log("here's the _renderer theme:") + //console.log(_renderer.getTheme()); + // interact with the ace session using editor, session, etc... + var _session = _editor.getSession(); + var _renderer = _editor.renderer; + _session.on("change", function(){ + console.log(editor.getValue()); + console.log("the ace session change event fired") }); + } + + }]); +}); + diff --git a/app/scripts/app.js b/app/scripts/app.js index 09af9eb..ba6e12f 100644 --- a/app/scripts/app.js +++ b/app/scripts/app.js @@ -7,9 +7,11 @@ define( 'ngSanitize', 'ngRoute', 'controllers/controllers', - 'directives/directives' - //'services/services', + 'directives/directives', + 'services/services', //'filters/filters', + 'uiAce', + 'uiBootstrap' ], function(angular){ var app = angular.module('editorComponentsApp', @@ -17,8 +19,11 @@ define( 'ngRoute', 'ngResource', 'controllers', - 'directives' - //'services', + 'directives', + 'services', + 'ui.ace', + 'ui.bootstrap', + 'services' //'filters', //'ui.bootstrap', //'ngTouch' diff --git a/app/scripts/directives/ngFileSelect.js b/app/scripts/directives/ngFileSelect.js new file mode 100644 index 0000000..23b9b93 --- /dev/null +++ b/app/scripts/directives/ngFileSelect.js @@ -0,0 +1,14 @@ +'use strict'; +define(['../directives/directives'], function(directives){ + directives.directive('ngFileSelect', function() { + return { + link: function($scope,el){ + + el.bind("change", function(e){ + $scope.file = (e.srcElement || e.target).files[0]; + $scope.getFile(); + }) + } + }; + }); +}); diff --git a/app/scripts/glossary/Glossary.js b/app/scripts/glossary/Glossary.js new file mode 100644 index 0000000..772cfcb --- /dev/null +++ b/app/scripts/glossary/Glossary.js @@ -0,0 +1,19 @@ +// Controller for any element that uses the glossary service +// TODO: define live template before proceeding + +'use strict'; + +define(['../controllers/controllers'], function(controllers) { + + controllers.controller('GlossaryCtrl', ['$scope', 'Glossary', function($scope, Glossary) { + + $scope.makeHtml = function() { + var out = ""; + Glossary.words.forEach(function(word) { + out += '
' + word + '
'; + }); + return out; + } + + }]); +}); diff --git a/app/scripts/glossary/glossary-template.html b/app/scripts/glossary/glossary-template.html new file mode 100644 index 0000000..7ebf4d9 --- /dev/null +++ b/app/scripts/glossary/glossary-template.html @@ -0,0 +1,5 @@ +
+
dog
+
cat
+
fish
+
diff --git a/app/scripts/glossary/glossaryFileUpload.js b/app/scripts/glossary/glossaryFileUpload.js new file mode 100644 index 0000000..f4bc4b2 --- /dev/null +++ b/app/scripts/glossary/glossaryFileUpload.js @@ -0,0 +1,35 @@ +'use strict'; +define(['controllers/controllers'], function(controllers) { + + controllers.controller('UploadCtrl', function($scope, fileReader) { + +// TODO: parse a local xml file - see the escriba project +// TODO: think about how to implement the client side parser(s) +// Parsing on the server is also a perfectly valid option +// server needs to know how to synchronize with the file format at all times + + // reader.readAsText(f); + + + console.log(fileReader) + $scope.getFile = function () { + $scope.progress = 0; + // FOR A TEXT FILE + fileReader.readAsText($scope.file, $scope) + .then(function(result) { + $scope.textFromFile = result; + }); + + // FOR IMAGES + //fileReader.readAsDataUrl($scope.file, $scope) + // .then(function(result) { + // $scope.imageSrc = result; + // }); + }; + + $scope.$on("fileProgress", function(e, progress) { + $scope.progress = progress.loaded / progress.total; + }); + }); +}); + diff --git a/app/scripts/main.js b/app/scripts/main.js index af150f5..365c2e6 100644 --- a/app/scripts/main.js +++ b/app/scripts/main.js @@ -13,10 +13,15 @@ require.config({ ngResource: '../bower_components/angular-resource/angular-resource', jquery: '../bower_components/jquery/jquery.min', jqueryui: '../bower_components/jqueryui/ui/jquery-ui', - underscore: '../bower_components/underscore/underscore-min', + underscore: '../bower_components/underscore/underscore-min', domReady: '../bower_components/domready/ready', ngCookies: '../bower_components/angular-cookies/angular-cookies', - ngSanitize: '../bower_components/angular-sanitize/angular-sanitize' + ngSanitize: '../bower_components/angular-sanitize/angular-sanitize', + uiAce: '../bower_components/angular-ui-ace/ui-ace', + ace: '../bower_components/ace-builds/src/ace', + //uiBootstrap: '../bower_components/angular-ui-bootstrap/dist/ui-bootstrap-tpls-0.10.0' + // TODO: added to get access to the new popover template directive + uiBootstrap: 'vendor/ui-bootstrap-tpls-0.10.0' }, shim: { angular: { @@ -26,6 +31,10 @@ require.config({ jqueryui: { deps: ['jquery'], exports: 'jqueryui' + }, + uiBootstrap: { + deps: ['angular'], + exports: 'uiBootstrap' }, ngRoute: { deps: ['angular'], @@ -42,6 +51,10 @@ require.config({ ngSanitize: { deps: ['angular'], exports: 'ngSanitize' + }, + uiAce: { + deps: ['angular', 'ace'], + exports: 'uiAce' } } }); @@ -55,14 +68,20 @@ require( 'ngResource', 'ngCookies', 'ngSanitize', - 'underscore', 'controllers/controllers', + 'services/services', + 'directives/directives', 'tokenRow/tokenRow', 'tokenRow/draggable', 'tokenRow/gap', 'tokenRow/token', + 'aceEditor/AceCtrl', + 'glossary/Glossary', 'controllers/main', - 'directives/directives' + 'services/glossary', + 'services/fileReader', + 'glossary/glossaryFileUpload', + 'directives/ngFileSelect' ], function(angular, app, domReady){ app.config(['$routeProvider', diff --git a/app/scripts/services/document.js b/app/scripts/services/document.js new file mode 100644 index 0000000..0fb89af --- /dev/null +++ b/app/scripts/services/document.js @@ -0,0 +1,13 @@ +'use strict'; + +// A Document Service which manages syncing with the server +// TODO: create a segment object model - i.e. "bilingualPhrase" + +// properties = { segments: , docTree: { } } + +define(['services/services'], function(services) { + + services.factory('Document', ['', function( ) { + + }]); +}); \ No newline at end of file diff --git a/app/scripts/services/fileReader.js b/app/scripts/services/fileReader.js new file mode 100644 index 0000000..b09aacf --- /dev/null +++ b/app/scripts/services/fileReader.js @@ -0,0 +1,70 @@ +// TODO: add to require +'use strict'; + +define(['services/services'], function(services) { + + var fileReader = function ($q, $log) { + + var onLoad = function(reader, deferred, scope) { + return function () { + scope.$apply(function () { + deferred.resolve(reader.result); + }); + }; + }; + + var onError = function (reader, deferred, scope) { + return function () { + scope.$apply(function () { + deferred.reject(reader.result); + }); + }; + }; + + var onProgress = function(reader, scope) { + return function (event) { + scope.$broadcast("fileProgress", + { + total: event.total, + loaded: event.loaded + }); + }; + }; + + var getReader = function(deferred, scope) { + var reader = new FileReader(); + reader.onload = onLoad(reader, deferred, scope); + reader.onerror = onError(reader, deferred, scope); + reader.onprogress = onProgress(reader, scope); + return reader; + }; + + var readAsDataURL = function (file, scope) { + var deferred = $q.defer(); + + var reader = getReader(deferred, scope); + reader.readAsDataURL(file); + + return deferred.promise; + }; + + var readAsText = function (file, scope) { + var deferred = $q.defer(); + + var reader = getReader(deferred, scope); + reader.readAsText(file); + + return deferred.promise; + }; + + return { + readAsDataUrl: readAsDataURL, + readAsText: readAsText + }; + }; + + services.factory('fileReader', + ['$q', '$log', fileReader]); + + +}); diff --git a/app/scripts/services/glossary.js b/app/scripts/services/glossary.js new file mode 100644 index 0000000..1422182 --- /dev/null +++ b/app/scripts/services/glossary.js @@ -0,0 +1,11 @@ +'use strict'; + +define(['services/services'], function(services) { + + services.factory('Glossary', [ function() { + return { + words: ["apple", "pear", "peach"] + } + + }]); +}); \ No newline at end of file diff --git a/app/scripts/services/services.js b/app/scripts/services/services.js new file mode 100644 index 0000000..92672fc --- /dev/null +++ b/app/scripts/services/services.js @@ -0,0 +1,7 @@ +'use strict'; + +define(['angular'], function(angular){ + 'use strict'; + + return angular.module('services', []); +}); \ No newline at end of file diff --git a/app/scripts/tokenRow/draggable.js b/app/scripts/tokenRow/draggable.js index 9b3349b..4ff8f46 100644 --- a/app/scripts/tokenRow/draggable.js +++ b/app/scripts/tokenRow/draggable.js @@ -31,6 +31,7 @@ define(['../directives/directives'], function(directives){ // TODO: tests only! //$token.addClass('in-drag'); $token.addClass('i-was-dragged'); + //$token.removeClass('i-was-dragged'); }, stop: function(ev, ui) { diff --git a/app/scripts/tokenRow/token.js b/app/scripts/tokenRow/token.js index 583fa58..2f5fb44 100644 --- a/app/scripts/tokenRow/token.js +++ b/app/scripts/tokenRow/token.js @@ -12,6 +12,10 @@ define(['../directives/directives'], function(directives){ // TODO: enable this once tokens are editable // require: 'ngModel', link: function(scope, elm, attrs, ctrl) { + // Working notes: + // tokens must come through a tokenizer + // tokens constantly change as the text changes + // - see how ace editor does tokenization } }; diff --git a/app/scripts/vendor/ui-bootstrap-tpls-0.10.0.js b/app/scripts/vendor/ui-bootstrap-tpls-0.10.0.js new file mode 100644 index 0000000..cc64127 --- /dev/null +++ b/app/scripts/vendor/ui-bootstrap-tpls-0.10.0.js @@ -0,0 +1,3725 @@ +/* + * angular-ui-bootstrap + * http://angular-ui.github.io/bootstrap/ + + * Version: 0.10.0 - 2014-01-13 + * License: MIT + */ +angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.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.tpls", ["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/popup.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/popover/popover-template.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]); +angular.module('ui.bootstrap.transition', []) + +/** + * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete. + * @param {DOMElement} element The DOMElement that will be animated. + * @param {string|object|function} trigger The thing that will cause the transition to start: + * - As a string, it represents the css class to be added to the element. + * - As an object, it represents a hash of style attributes to be applied to the element. + * - As a function, it represents a function to be called that will cause the transition to occur. + * @return {Promise} A promise that is resolved when the transition finishes. + */ +.factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) { + + var $transition = function(element, trigger, options) { + options = options || {}; + var deferred = $q.defer(); + var endEventName = $transition[options.animation ? "animationEndEventName" : "transitionEndEventName"]; + + var transitionEndHandler = function(event) { + $rootScope.$apply(function() { + element.unbind(endEventName, transitionEndHandler); + deferred.resolve(element); + }); + }; + + if (endEventName) { + element.bind(endEventName, transitionEndHandler); + } + + // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur + $timeout(function() { + if ( angular.isString(trigger) ) { + element.addClass(trigger); + } else if ( angular.isFunction(trigger) ) { + trigger(element); + } else if ( angular.isObject(trigger) ) { + element.css(trigger); + } + //If browser does not support transitions, instantly resolve + if ( !endEventName ) { + deferred.resolve(element); + } + }); + + // Add our custom cancel function to the promise that is returned + // We can call this if we are about to run a new transition, which we know will prevent this transition from ending, + // i.e. it will therefore never raise a transitionEnd event for that transition + deferred.promise.cancel = function() { + if ( endEventName ) { + element.unbind(endEventName, transitionEndHandler); + } + deferred.reject('Transition cancelled'); + }; + + return deferred.promise; + }; + + // Work out the name of the transitionEnd event + var transElement = document.createElement('trans'); + var transitionEndEventNames = { + 'WebkitTransition': 'webkitTransitionEnd', + 'MozTransition': 'transitionend', + 'OTransition': 'oTransitionEnd', + 'transition': 'transitionend' + }; + var animationEndEventNames = { + 'WebkitTransition': 'webkitAnimationEnd', + 'MozTransition': 'animationend', + 'OTransition': 'oAnimationEnd', + 'transition': 'animationend' + }; + function findEndEventName(endEventNames) { + for (var name in endEventNames){ + if (transElement.style[name] !== undefined) { + return endEventNames[name]; + } + } + } + $transition.transitionEndEventName = findEndEventName(transitionEndEventNames); + $transition.animationEndEventName = findEndEventName(animationEndEventNames); + return $transition; +}]); + +angular.module('ui.bootstrap.collapse', ['ui.bootstrap.transition']) + + .directive('collapse', ['$transition', function ($transition, $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 +// +// Heading containing HTML - +// +.directive('accordionHeading', function() { + return { + restrict: 'EA', + transclude: true, // Grab the contents to be used as the heading + template: '', // In effect remove this element! + replace: true, + require: '^accordionGroup', + 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 +//
+// +// ... +//
+.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 + + + + + + + + + + + + + + + .carousel-indicators { + top: auto; + bottom: 15px; + } + + + */ +.directive('carousel', [function() { + return { + restrict: 'EA', + transclude: true, + replace: true, + controller: 'CarouselController', + require: 'carousel', + templateUrl: 'template/carousel/carousel.html', + scope: { + interval: '=', + noTransition: '=', + noPause: '=' + } + }; +}]) + +/** + * @ngdoc directive + * @name ui.bootstrap.carousel.directive:slide + * @restrict EA + * + * @description + * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}. Must be placed as a child of a carousel element. + * + * @param {boolean=} active Model binding, whether or not this slide is currently active. + * + * @example + + +
+ + + + + + +
+
+
    +
  • + + {{$index}}: {{slide.text}} +
  • +
+ Add Slide +
+
+ Interval, in milliseconds: +
Enter a negative number to stop the interval. +
+
+
+
+ +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(); +} + + + .carousel-indicators { + top: auto; + bottom: 15px; + } + +
+*/ + +.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('
'); + 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: + + */ + +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 + if (backdropDomEl && backdropIndex() == -1) { + var backdropScopeRef = backdropScope; + removeAfterAnimate(backdropDomEl, backdropScope, 150, function () { + backdropScopeRef.$destroy(); + backdropScopeRef = null; + }); + backdropDomEl = undefined; + backdropScope = undefined; + } + } + + function removeAfterAnimate(domEl, scope, emulateTime, done) { + // Closing animation + scope.animate = false; + + var transitionEndEventName = $transition.transitionEndEventName; + if (transitionEndEventName) { + // transition out + var timeout = $timeout(afterAnimating, emulateTime); + + domEl.bind(transitionEndEventName, function () { + $timeout.cancel(timeout); + afterAnimating(); + scope.$apply(); + }); + } else { + // Ensure this call is async + $timeout(afterAnimating, 0); + } + + function afterAnimating() { + if (afterAnimating.done) { + return; + } + afterAnimating.done = true; + + domEl.remove(); + if (done) { + done(); + } + } + } + + $document.bind('keydown', function (evt) { + var modal; + + if (evt.which === 27) { + modal = openedWindows.top(); + if (modal && modal.value.keyboard) { + $rootScope.$apply(function () { + $modalStack.dismiss(modal.key); + }); + } + } + }); + + $modalStack.open = function (modalInstance, modal) { + + openedWindows.add(modalInstance, { + deferred: modal.deferred, + modalScope: modal.scope, + backdrop: modal.backdrop, + keyboard: modal.keyboard + }); + + var body = $document.find('body').eq(0), + currBackdropIndex = backdropIndex(); + + if (currBackdropIndex >= 0 && !backdropDomEl) { + backdropScope = $rootScope.$new(true); + backdropScope.index = currBackdropIndex; + backdropDomEl = $compile('
')(backdropScope); + body.append(backdropDomEl); + } + + var angularDomEl = angular.element('
'); + angularDomEl.attr('window-class', modal.windowClass); + angularDomEl.attr('index', openedWindows.length() - 1); + angularDomEl.attr('animate', 'animate'); + angularDomEl.html(modal.content); + + var modalDomEl = $compile(angularDomEl)(modal.scope); + openedWindows.top().value.modalDomEl = modalDomEl; + body.append(modalDomEl); + body.addClass(OPENED_MODAL_CLASS); + }; + + $modalStack.close = function (modalInstance, result) { + var modalWindow = openedWindows.get(modalInstance).value; + if (modalWindow) { + modalWindow.deferred.resolve(result); + removeModalWindow(modalInstance); + } + }; + + $modalStack.dismiss = function (modalInstance, reason) { + var modalWindow = openedWindows.get(modalInstance).value; + if (modalWindow) { + modalWindow.deferred.reject(reason); + removeModalWindow(modalInstance); + } + }; + + $modalStack.dismissAll = function (reason) { + var topModal = this.getTop(); + while (topModal) { + this.dismiss(topModal.key, reason); + topModal = this.getTop(); + } + }; + + $modalStack.getTop = function () { + return openedWindows.top(); + }; + + return $modalStack; + }]) + + .provider('$modal', function () { + + var $modalProvider = { + options: { + backdrop: true, //can be also false or 'static' + keyboard: true + }, + $get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack', + function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) { + + var $modal = {}; + + function getTemplatePromise(options) { + return options.template ? $q.when(options.template) : + $http.get(options.templateUrl, {cache: $templateCache}).then(function (result) { + return result.data; + }); + } + + function getResolvePromises(resolves) { + var promisesArr = []; + angular.forEach(resolves, function (value, key) { + if (angular.isFunction(value) || angular.isArray(value)) { + promisesArr.push($q.when($injector.invoke(value))); + } + }); + return promisesArr; + } + + $modal.open = function (modalOptions) { + + var modalResultDeferred = $q.defer(); + var modalOpenedDeferred = $q.defer(); + + //prepare an instance of a modal to be injected into controllers and returned to a caller + var modalInstance = { + result: modalResultDeferred.promise, + opened: modalOpenedDeferred.promise, + close: function (result) { + $modalStack.close(modalInstance, result); + }, + dismiss: function (reason) { + $modalStack.dismiss(modalInstance, reason); + } + }; + + //merge and clean up options + modalOptions = angular.extend({}, $modalProvider.options, modalOptions); + modalOptions.resolve = modalOptions.resolve || {}; + + //verify options + if (!modalOptions.template && !modalOptions.templateUrl) { + throw new Error('One of template or templateUrl options is required.'); + } + + var templateAndResolvePromise = + $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve))); + + + templateAndResolvePromise.then(function resolveSuccess(tplAndVars) { + + var modalScope = (modalOptions.scope || $rootScope).$new(); + modalScope.$close = modalInstance.close; + modalScope.$dismiss = modalInstance.dismiss; + + var ctrlInstance, ctrlLocals = {}; + var resolveIter = 1; + + //controllers + if (modalOptions.controller) { + ctrlLocals.$scope = modalScope; + ctrlLocals.$modalInstance = modalInstance; + angular.forEach(modalOptions.resolve, function (value, key) { + ctrlLocals[key] = tplAndVars[resolveIter++]; + }); + + ctrlInstance = $controller(modalOptions.controller, ctrlLocals); + } + + $modalStack.open(modalInstance, { + scope: modalScope, + deferred: modalResultDeferred, + content: tplAndVars[0], + backdrop: modalOptions.backdrop, + keyboard: modalOptions.keyboard, + windowClass: modalOptions.windowClass + }); + + }, function resolveError(reason) { + modalResultDeferred.reject(reason); + }); + + templateAndResolvePromise.then(function () { + modalOpenedDeferred.resolve(true); + }, function () { + modalOpenedDeferred.reject(false); + }); + + return modalInstance; + }; + + return $modal; + }] + }; + + return $modalProvider; + }); + +angular.module('ui.bootstrap.pagination', []) + +.controller('PaginationController', ['$scope', '$attrs', '$parse', '$interpolate', function ($scope, $attrs, $parse, $interpolate) { + var self = this, + setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; + + this.init = function(defaultItemsPerPage) { + if ($attrs.itemsPerPage) { + $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) { + self.itemsPerPage = parseInt(value, 10); + $scope.totalPages = self.calculateTotalPages(); + }); + } else { + this.itemsPerPage = defaultItemsPerPage; + } + }; + + this.noPrevious = function() { + return this.page === 1; + }; + this.noNext = function() { + return this.page === $scope.totalPages; + }; + + this.isActive = function(page) { + return this.page === page; + }; + + this.calculateTotalPages = function() { + var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage); + return Math.max(totalPages || 0, 1); + }; + + this.getAttributeValue = function(attribute, defaultValue, interpolate) { + return angular.isDefined(attribute) ? (interpolate ? $interpolate(attribute)($scope.$parent) : $scope.$parent.$eval(attribute)) : defaultValue; + }; + + this.render = function() { + this.page = parseInt($scope.page, 10) || 1; + if (this.page > 0 && this.page <= $scope.totalPages) { + $scope.pages = this.getPages(this.page, $scope.totalPages); + } + }; + + $scope.selectPage = function(page) { + if ( ! self.isActive(page) && page > 0 && page <= $scope.totalPages) { + $scope.page = page; + $scope.onSelectPage({ page: page }); + } + }; + + $scope.$watch('page', function() { + self.render(); + }); + + $scope.$watch('totalItems', function() { + $scope.totalPages = self.calculateTotalPages(); + }); + + $scope.$watch('totalPages', function(value) { + setNumPages($scope.$parent, value); // Readonly variable + + if ( self.page > value ) { + $scope.selectPage(value); + } else { + self.render(); + } + }); +}]) + +.constant('paginationConfig', { + itemsPerPage: 10, + boundaryLinks: false, + directionLinks: true, + firstText: 'First', + previousText: 'Previous', + nextText: 'Next', + lastText: 'Last', + rotate: true +}) + +.directive('pagination', ['$parse', 'paginationConfig', function($parse, config) { + return { + restrict: 'EA', + scope: { + page: '=', + totalItems: '=', + onSelectPage:' &' + }, + controller: 'PaginationController', + templateUrl: 'template/pagination/pagination.html', + replace: true, + link: function(scope, element, attrs, paginationCtrl) { + + // Setup configuration parameters + var maxSize, + boundaryLinks = paginationCtrl.getAttributeValue(attrs.boundaryLinks, config.boundaryLinks ), + directionLinks = paginationCtrl.getAttributeValue(attrs.directionLinks, config.directionLinks ), + firstText = paginationCtrl.getAttributeValue(attrs.firstText, config.firstText, true), + previousText = paginationCtrl.getAttributeValue(attrs.previousText, config.previousText, true), + nextText = paginationCtrl.getAttributeValue(attrs.nextText, config.nextText, true), + lastText = paginationCtrl.getAttributeValue(attrs.lastText, config.lastText, true), + rotate = paginationCtrl.getAttributeValue(attrs.rotate, config.rotate); + + paginationCtrl.init(config.itemsPerPage); + + if (attrs.maxSize) { + scope.$parent.$watch($parse(attrs.maxSize), function(value) { + maxSize = parseInt(value, 10); + paginationCtrl.render(); + }); + } + + // Create page object used in template + function makePage(number, text, isActive, isDisabled) { + return { + number: number, + text: text, + active: isActive, + disabled: isDisabled + }; + } + + paginationCtrl.getPages = function(currentPage, totalPages) { + var pages = []; + + // Default page limits + var startPage = 1, endPage = totalPages; + var isMaxSized = ( angular.isDefined(maxSize) && maxSize < totalPages ); + + // recompute if maxSize + if ( isMaxSized ) { + if ( rotate ) { + // Current page is displayed in the middle of the visible ones + startPage = Math.max(currentPage - Math.floor(maxSize/2), 1); + endPage = startPage + maxSize - 1; + + // Adjust if limit is exceeded + if (endPage > totalPages) { + endPage = totalPages; + startPage = endPage - maxSize + 1; + } + } else { + // Visible pages are paginated with maxSize + startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1; + + // Adjust last page if limit is exceeded + endPage = Math.min(startPage + maxSize - 1, totalPages); + } + } + + // Add page number links + for (var number = startPage; number <= endPage; number++) { + var page = makePage(number, number, paginationCtrl.isActive(number), false); + pages.push(page); + } + + // Add links to move between page sets + if ( isMaxSized && ! rotate ) { + if ( startPage > 1 ) { + var previousPageSet = makePage(startPage - 1, '...', false, false); + pages.unshift(previousPageSet); + } + + if ( endPage < totalPages ) { + var nextPageSet = makePage(endPage + 1, '...', false, false); + pages.push(nextPageSet); + } + } + + // Add previous & next links + if (directionLinks) { + var previousPage = makePage(currentPage - 1, previousText, false, paginationCtrl.noPrevious()); + pages.unshift(previousPage); + + var nextPage = makePage(currentPage + 1, nextText, false, paginationCtrl.noNext()); + pages.push(nextPage); + } + + // Add first & last links + if (boundaryLinks) { + var firstPage = makePage(1, firstText, false, paginationCtrl.noPrevious()); + pages.unshift(firstPage); + + var lastPage = makePage(totalPages, lastText, false, paginationCtrl.noNext()); + pages.push(lastPage); + } + + return pages; + }; + } + }; +}]) + +.constant('pagerConfig', { + itemsPerPage: 10, + previousText: '« Previous', + nextText: 'Next »', + align: true +}) + +.directive('pager', ['pagerConfig', function(config) { + return { + restrict: 'EA', + scope: { + page: '=', + totalItems: '=', + onSelectPage:' &' + }, + controller: 'PaginationController', + templateUrl: 'template/pagination/pager.html', + replace: true, + link: function(scope, element, attrs, paginationCtrl) { + + // Setup configuration parameters + var previousText = paginationCtrl.getAttributeValue(attrs.previousText, config.previousText, true), + nextText = paginationCtrl.getAttributeValue(attrs.nextText, config.nextText, true), + align = paginationCtrl.getAttributeValue(attrs.align, config.align); + + paginationCtrl.init(config.itemsPerPage); + + // 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 ) + }; + } + + paginationCtrl.getPages = function(currentPage) { + return [ + makePage(currentPage - 1, previousText, paginationCtrl.noPrevious(), true, false), + makePage(currentPage + 1, nextText, paginationCtrl.noNext(), false, true) + ]; + }; + } + }; +}]); + +/** + * The following features are still outstanding: animation as a + * function, placement as a function, inside, support for more triggers than + * just mouse enter/leave, html tooltips, and selector delegation. + */ +angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap.bindHtml' ] ) + +/** + * The $tooltip service creates tooltip- and popover-like directives as well as + * houses global options for them. + */ +.provider( '$tooltip', function () { + // The default options tooltip and popover. + var defaultOptions = { + placement: 'top', + animation: true, + popupDelay: 0 + }; + + // Default hide triggers for each show trigger + var triggerMap = { + 'mouseenter': 'mouseleave', + 'click': 'click', + 'focus': 'blur' + }; + + // The options specified to the provider globally. + var globalOptions = {}; + + /** + * `options({})` allows global configuration of all tooltips in the + * application. + * + * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) { + * // place tooltips left instead of top by default + * $tooltipProvider.options( { placement: 'left' } ); + * }); + */ + this.options = function( value ) { + angular.extend( globalOptions, value ); + }; + + /** + * This allows you to extend the set of trigger mappings available. E.g.: + * + * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ); + */ + this.setTriggers = function setTriggers ( triggers ) { + angular.extend( triggerMap, triggers ); + }; + + /** + * This is a helper function for translating camel-case to snake-case. + */ + function snake_case(name){ + var regexp = /[A-Z]/g; + var separator = '-'; + return name.replace(regexp, function(letter, pos) { + return (pos ? separator : '') + letter.toLowerCase(); + }); + } + + /** + * Returns the actual instance of the $tooltip service. + * TODO support multiple triggers + */ + this.$get = [ '$window', '$compile', '$timeout', '$parse', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $parse, $document, $position, $interpolate ) { + return function $tooltip ( type, prefix, defaultTriggerShow ) { + var options = angular.extend( {}, defaultOptions, globalOptions ); + + /** + * Returns an object of show and hide triggers. + * + * If a trigger is supplied, + * it is used to show the tooltip; otherwise, it will use the `trigger` + * option passed to the `$tooltipProvider.options` method; else it will + * default to the trigger supplied to this directive factory. + * + * The hide trigger is based on the show trigger. If the `trigger` option + * was passed to the `$tooltipProvider.options` method, it will use the + * mapped trigger from `triggerMap` or the passed trigger if the map is + * undefined; otherwise, it uses the `triggerMap` value of the show + * trigger; else it will just use the show trigger. + */ + function getTriggers ( trigger ) { + var show = trigger || options.trigger || defaultTriggerShow; + var hide = triggerMap[show] || show; + return { + show: show, + hide: hide + }; + } + + var directiveName = snake_case( type ); + + var startSym = $interpolate.startSymbol(); + var endSym = $interpolate.endSymbol(); + var template = + '
'+ + '
'; + + return { + restrict: 'EA', + scope: true, + compile: function (tElem, tAttrs) { + var tooltipLinker = $compile( template ); + + return function link ( scope, element, attrs ) { + var tooltip; + var transitionTimeout; + var popupTimeout; + var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; + var triggers = getTriggers( undefined ); + var hasRegisteredTriggers = false; + var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']); + + var positionTooltip = function (){ + var position, + ttWidth, + ttHeight, + ttPosition; + // Get the position of the directive element. + position = 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 '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 ); + + }; + + // By default, the tooltip is not open. + // TODO add ability to start tooltip opened + scope.tt_isOpen = false; + + function toggleTooltipBind () { + if ( ! scope.tt_isOpen ) { + showTooltipBind(); + } else { + hideTooltipBind(); + } + } + + // Show the tooltip with delay if specified, otherwise show it immediately + function showTooltipBind() { + if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) { + return; + } + if ( scope.tt_popupDelay ) { + popupTimeout = $timeout( show, scope.tt_popupDelay, false ); + popupTimeout.then(function(reposition){reposition();}); + } else { + show()(); + } + } + + function hideTooltipBind () { + scope.$apply(function () { + hide(); + }); + } + + // Show the tooltip popup element. + function show() { + + + // Don't show empty tooltips. + if ( ! scope.tt_content ) { + return angular.noop; + } + + createTooltip(); + + // 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 ) { + $document.find( 'body' ).append( tooltip ); + } else { + element.after( tooltip ); + } + + positionTooltip(); + + // And show the tooltip. + scope.tt_isOpen = true; + scope.$digest(); // digest required as $apply is not called + + // Return positioning function as promise callback for correct + // positioning after draw. + return positionTooltip; + } + + // Hide the tooltip popup element. + function hide() { + // First things first: we don't show it anymore. + scope.tt_isOpen = false; + + //if tooltip is going to be shown after delay, we must cancel this + $timeout.cancel( popupTimeout ); + + // And now we remove it from the DOM. However, if we have animation, we + // need to wait for it to expire beforehand. + // FIXME: this is a placeholder for a port of the transitions library. + if ( scope.tt_animation ) { + transitionTimeout = $timeout(removeTooltip, 500); + } else { + removeTooltip(); + } + } + + function createTooltip() { + // There can only be one tooltip element per directive shown at once. + if (tooltip) { + return; + } + tooltip = tooltipLinker(scope, function () {}); + + // Get contents rendered into the tooltip + scope.$digest(); + } + + function removeTooltip( destroy ) { + if (tooltip) { + if (destroy) { + tooltip.remove(); + tooltip = null; + } else { + // equals to "tooltip.detach();" + angular.forEach( tooltip, function( e ) { + if (e.parentNode) { + e.parentNode.removeChild( e ); + } + } ); + } + } + } + + /** + * Observe the relevant attributes. + */ + attrs.$observe( type, function ( val ) { + scope.tt_content = val; + + if (!val && scope.tt_isOpen ) { + hide(); + } + }); + + attrs.$observe( prefix+'Title', function ( val ) { + scope.tt_title = val; + }); + + attrs.$observe( prefix+'Placement', function ( val ) { + scope.tt_placement = angular.isDefined( val ) ? val : options.placement; + }); + + attrs.$observe( prefix+'PopupDelay', function ( val ) { + var delay = parseInt( val, 10 ); + scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay; + }); + + var unregisterTriggers = function() { + if (hasRegisteredTriggers) { + element.unbind( triggers.show, showTooltipBind ); + element.unbind( triggers.hide, hideTooltipBind ); + } + }; + + attrs.$observe( prefix+'Trigger', function ( val ) { + unregisterTriggers(); + + triggers = getTriggers( val ); + + if ( triggers.show === triggers.hide ) { + element.bind( triggers.show, toggleTooltipBind ); + } else { + element.bind( triggers.show, showTooltipBind ); + element.bind( triggers.hide, hideTooltipBind ); + } + + hasRegisteredTriggers = true; + }); + + var animation = scope.$eval(attrs[prefix + 'Animation']); + scope.tt_animation = angular.isDefined(animation) ? !!animation : options.animation; + + attrs.$observe( prefix+'AppendToBody', function ( val ) { + appendToBody = angular.isDefined( val ) ? $parse( val )( scope ) : appendToBody; + }); + + // if a tooltip is attached to we need to remove it on + // location change as its parent scope will probably not be destroyed + // by the change. + if ( appendToBody ) { + scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () { + if ( scope.tt_isOpen ) { + hide(); + } + }); + } + + // Make sure tooltip is destroyed and removed. + scope.$on('$destroy', function onDestroyTooltip() { + $timeout.cancel( transitionTimeout ); + $timeout.cancel( popupTimeout ); + unregisterTriggers(); + removeTooltip( true ); + }); + }; + } + }; + }; + }]; +}) + +.directive( 'tooltipPopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/tooltip/tooltip-popup.html' + }; +}) + +.directive( 'tooltip', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); +}]) + +.directive( 'tooltipHtmlUnsafePopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html' + }; +}) + +.directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' ); +}]); + +/** + * The following features are still outstanding: popup delay, animation as a + * function, placement as a function, inside, support for more triggers than + * just mouse enter/leave, html popovers, and selector delegatation. + */ +angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] ) + +.directive( 'popoverPopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/popover/popover.html' + }; +}) + +.directive( 'popover', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'popover', 'popover', 'click' ); +}]) + +.directive( 'popoverTemplatePopup', [ '$http', '$templateCache', '$compile', '$timeout', function ( $http, $templateCache, $compile, $timeout ) { + return { + restrict: 'EA', + replace: true, + scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&', compileScope: '&' }, + templateUrl: 'template/popover/popover-template.html', + link: function( scope, iElement ) { + scope.$watch( 'content', function( templateUrl ) { + if ( !templateUrl ) { return; } + $http.get( templateUrl, { cache: $templateCache } ) + .then( function( response ) { + var contentEl = angular.element( iElement[0].querySelector( '.popover-content' ) ); + contentEl.children().remove(); + contentEl.append( $compile( response.data.trim() )( scope.compileScope() ) ); + $timeout(function(){ scope.compileScope().$digest(); }); + }); + }); + } + }; +}]) + +.directive( 'popoverTemplate', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'popoverTemplate', 'popover', 'click' ); +}]); + +angular.module('ui.bootstrap.progressbar', ['ui.bootstrap.transition']) + +.constant('progressConfig', { + animate: true, + max: 100 +}) + +.controller('ProgressController', ['$scope', '$attrs', 'progressConfig', '$transition', function($scope, $attrs, progressConfig, $transition) { + var self = this, + bars = [], + max = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : progressConfig.max, + animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate; + + this.addBar = function(bar, element) { + var oldValue = 0, index = bar.$parent.$index; + if ( angular.isDefined(index) && bars[index] ) { + oldValue = bars[index].value; + } + bars.push(bar); + + this.update(element, bar.value, oldValue); + + bar.$watch('value', function(value, oldValue) { + if (value !== oldValue) { + self.update(element, value, oldValue); + } + }); + + bar.$on('$destroy', function() { + self.removeBar(bar); + }); + }; + + // Update bar element width + this.update = function(element, newValue, oldValue) { + var percent = this.getPercentage(newValue); + + if (animate) { + element.css('width', this.getPercentage(oldValue) + '%'); + $transition(element, {width: percent + '%'}); + } else { + element.css({'transition': 'none', 'width': percent + '%'}); + } + }; + + this.removeBar = function(bar) { + bars.splice(bars.indexOf(bar), 1); + }; + + this.getPercentage = function(value) { + return Math.round(100 * value / max); + }; +}]) + +.directive('progress', function() { + return { + restrict: 'EA', + replace: true, + transclude: true, + controller: 'ProgressController', + require: 'progress', + scope: {}, + template: '
' + //templateUrl: 'template/progressbar/progress.html' // Works in AngularJS 1.2 + }; +}) + +.directive('bar', function() { + return { + restrict: 'EA', + replace: true, + transclude: true, + require: '^progress', + scope: { + value: '=', + type: '@' + }, + templateUrl: 'template/progressbar/bar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, element); + } + }; +}) + +.directive('progressbar', function() { + return { + restrict: 'EA', + replace: true, + transclude: true, + controller: 'ProgressController', + scope: { + value: '=', + type: '@' + }, + templateUrl: 'template/progressbar/progressbar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, angular.element(element.children()[0])); + } + }; +}); +angular.module('ui.bootstrap.rating', []) + +.constant('ratingConfig', { + max: 5, + stateOn: null, + stateOff: null +}) + +.controller('RatingController', ['$scope', '$attrs', '$parse', 'ratingConfig', function($scope, $attrs, $parse, ratingConfig) { + + this.maxRange = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max; + this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn; + this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff; + + this.createRateObjects = function(states) { + var defaultOptions = { + stateOn: this.stateOn, + stateOff: this.stateOff + }; + + for (var i = 0, n = states.length; i < n; i++) { + states[i] = angular.extend({ index: i }, defaultOptions, states[i]); + } + return states; + }; + + // Get objects used in template + $scope.range = angular.isDefined($attrs.ratingStates) ? this.createRateObjects(angular.copy($scope.$parent.$eval($attrs.ratingStates))): this.createRateObjects(new Array(this.maxRange)); + + $scope.rate = function(value) { + if ( $scope.value !== value && !$scope.readonly ) { + $scope.value = value; + } + }; + + $scope.enter = function(value) { + if ( ! $scope.readonly ) { + $scope.val = value; + } + $scope.onHover({value: value}); + }; + + $scope.reset = function() { + $scope.val = angular.copy($scope.value); + $scope.onLeave(); + }; + + $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; + }); + } +}]) + +.directive('rating', function() { + return { + restrict: 'EA', + scope: { + value: '=', + onHover: '&', + onLeave: '&' + }, + controller: 'RatingController', + templateUrl: 'template/rating/rating.html', + replace: true + }; +}); + +/** + * @ngdoc overview + * @name ui.bootstrap.tabs + * + * @description + * AngularJS version of the tabs directive. + */ + +angular.module('ui.bootstrap.tabs', []) + +.controller('TabsetController', ['$scope', function TabsetCtrl($scope) { + var ctrl = this, + tabs = ctrl.tabs = $scope.tabs = []; + + ctrl.select = function(tab) { + angular.forEach(tabs, function(tab) { + tab.active = false; + }); + tab.active = true; + }; + + ctrl.addTab = function addTab(tab) { + tabs.push(tab); + if (tabs.length === 1 || tab.active) { + ctrl.select(tab); + } + }; + + ctrl.removeTab = function removeTab(tab) { + var index = tabs.indexOf(tab); + //Select a new tab if the tab to be removed is selected + if (tab.active && tabs.length > 1) { + //If this is the last tab, select the previous tab. else, the next tab. + var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1; + ctrl.select(tabs[newActiveIndex]); + } + tabs.splice(index, 1); + }; +}]) + +/** + * @ngdoc directive + * @name ui.bootstrap.tabs.directive:tabset + * @restrict EA + * + * @description + * Tabset is the outer container for the tabs directive + * + * @param {boolean=} vertical Whether or not to use vertical styling for the tabs. + * @param {boolean=} justified Whether or not to use justified styling for the tabs. + * + * @example + + + + First Content! + Second Content! + +
+ + First Vertical Content! + Second Vertical Content! + + + First Justified Content! + Second Justified Content! + +
+
+ */ +.directive('tabset', function() { + return { + restrict: 'EA', + transclude: true, + replace: true, + scope: {}, + controller: 'TabsetController', + templateUrl: 'template/tabs/tabset.html', + link: function(scope, element, attrs) { + scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false; + scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false; + 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', function($parse) { + 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 + onDeselect: '&deselect' + }, + controller: function() { + //Empty controller so other directives can require being 'under' a tab + }, + compile: function(elm, attrs, transclude) { + return function postLink(scope, elm, attrs, tabsetCtrl) { + var getActive, setActive; + if (attrs.active) { + getActive = $parse(attrs.active); + setActive = getActive.assign; + scope.$parent.$watch(getActive, function updateActive(value, oldVal) { + // Avoid re-initializing scope.active as it is already initialized + // below. (watcher is called async during init with value === + // oldVal) + if (value !== oldVal) { + scope.active = !!value; + } + }); + scope.active = getActive(scope.$parent); + } else { + setActive = getActive = angular.noop; + } + + scope.$watch('active', function(active) { + // Note this watcher also initializes and assigns scope.active to the + // attrs.active expression. + setActive(scope.$parent, active); + if (active) { + tabsetCtrl.select(scope); + scope.onSelect(); + } else { + scope.onDeselect(); + } + }); + + scope.disabled = false; + if ( attrs.disabled ) { + scope.$parent.$watch($parse(attrs.disabled), function(value) { + scope.disabled = !! value; + }); + } + + scope.select = function() { + if ( ! scope.disabled ) { + scope.active = true; + } + }; + + tabsetCtrl.addTab(scope); + scope.$on('$destroy', function() { + tabsetCtrl.removeTab(scope); + }); + + + //We need to transclude later, once the content container is ready. + //when this link happens, we're inside a tab heading. + scope.$transcludeFn = transclude; + }; + } + }; +}]) + +.directive('tabHeadingTransclude', [function() { + return { + restrict: 'A', + require: '^tab', + link: function(scope, elm, attrs, tabCtrl) { + scope.$watch('headingElement', function updateHeadingElement(heading) { + if (heading) { + elm.html(''); + elm.append(heading); + } + }); + } + }; +}]) + +.directive('tabContentTransclude', function() { + return { + restrict: 'A', + require: '^tabset', + link: function(scope, elm, attrs) { + var tab = scope.$eval(attrs.tabContentTransclude); + + //Now our tab is ready to be transcluded: both the tab heading area + //and the tab content area are loaded. Transclude 'em both. + tab.$transcludeFn(tab.$parent, function(contents) { + angular.forEach(contents, function(node) { + if (isTabHeading(node)) { + //Let tabHeadingTransclude know. + tab.headingElement = node; + } else { + elm.append(node); + } + }); + }); + } + }; + function isTabHeading(node) { + return node.tagName && ( + node.hasAttribute('tab-heading') || + node.hasAttribute('data-tab-heading') || + node.tagName.toLowerCase() === 'tab-heading' || + node.tagName.toLowerCase() === 'data-tab-heading' + ); + } +}) + +; + +angular.module('ui.bootstrap.timepicker', []) + +.constant('timepickerConfig', { + hourStep: 1, + minuteStep: 1, + showMeridian: true, + meridians: null, + readonlyInput: false, + mousewheel: true +}) + +.directive('timepicker', ['$parse', '$log', 'timepickerConfig', '$locale', function ($parse, $log, timepickerConfig, $locale) { + return { + restrict: 'EA', + require:'?^ngModel', + replace: true, + scope: {}, + templateUrl: 'template/timepicker/timepicker.html', + link: function(scope, element, attrs, ngModel) { + if ( !ngModel ) { + return; // do nothing if no ng-model + } + + var selected = new Date(), + meridians = angular.isDefined(attrs.meridians) ? scope.$parent.$eval(attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS; + + 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 ( ngModel.$error.time ) { + // Evaluate from template + var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate(); + if (angular.isDefined( hours ) && angular.isDefined( minutes )) { + selected.setHours( hours ); + refresh(); + } + } else { + updateTemplate(); + } + }); + } + + // Get scope.hours in 24H mode if valid + function getHoursFromTemplate ( ) { + var hours = parseInt( scope.hours, 10 ); + var valid = ( scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24); + if ( !valid ) { + return undefined; + } + + if ( scope.showMeridian ) { + if ( hours === 12 ) { + hours = 0; + } + if ( scope.meridian === meridians[1] ) { + hours = hours + 12; + } + } + return hours; + } + + function getMinutesFromTemplate() { + var minutes = parseInt(scope.minutes, 10); + return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined; + } + + function pad( value ) { + return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value; + } + + // Input elements + var inputs = element.find('input'), 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; + } + //pick correct delta variable depending on event + var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY; + return (e.detail || delta > 0); + }; + + hoursInputEl.bind('mousewheel wheel', function(e) { + scope.$apply( (isScrollingUp(e)) ? scope.incrementHours() : scope.decrementHours() ); + e.preventDefault(); + }); + + minutesInputEl.bind('mousewheel wheel', function(e) { + scope.$apply( (isScrollingUp(e)) ? scope.incrementMinutes() : scope.decrementMinutes() ); + e.preventDefault(); + }); + } + + scope.readonlyInput = (angular.isDefined(attrs.readonlyInput)) ? scope.$eval(attrs.readonlyInput) : timepickerConfig.readonlyInput; + if ( ! scope.readonlyInput ) { + + var invalidate = function(invalidHours, invalidMinutes) { + ngModel.$setViewValue( null ); + ngModel.$setValidity('time', false); + if (angular.isDefined(invalidHours)) { + scope.invalidHours = invalidHours; + } + if (angular.isDefined(invalidMinutes)) { + scope.invalidMinutes = invalidMinutes; + } + }; + + scope.updateHours = function() { + var hours = getHoursFromTemplate(); + + if ( angular.isDefined(hours) ) { + selected.setHours( hours ); + refresh( 'h' ); + } else { + invalidate(true); + } + }; + + hoursInputEl.bind('blur', function(e) { + if ( !scope.validHours && scope.hours < 10) { + scope.$apply( function() { + scope.hours = pad( scope.hours ); + }); + } + }); + + scope.updateMinutes = function() { + var minutes = getMinutesFromTemplate(); + + if ( angular.isDefined(minutes) ) { + selected.setMinutes( minutes ); + refresh( 'm' ); + } else { + invalidate(undefined, true); + } + }; + + minutesInputEl.bind('blur', function(e) { + if ( !scope.invalidMinutes && scope.minutes < 10 ) { + scope.$apply( function() { + scope.minutes = pad( scope.minutes ); + }); + } + }); + } else { + scope.updateHours = angular.noop; + scope.updateMinutes = angular.noop; + } + + ngModel.$render = function() { + var date = ngModel.$modelValue ? new Date( ngModel.$modelValue ) : null; + + if ( isNaN(date) ) { + ngModel.$setValidity('time', false); + $log.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); + } else { + if ( date ) { + selected = date; + } + makeValid(); + updateTemplate(); + } + }; + + // Call internally when we know that model is valid. + function refresh( keyboardChange ) { + makeValid(); + ngModel.$setViewValue( new Date(selected) ); + updateTemplate( keyboardChange ); + } + + function makeValid() { + ngModel.$setValidity('time', true); + scope.invalidHours = false; + scope.invalidMinutes = false; + } + + function updateTemplate( keyboardChange ) { + var hours = selected.getHours(), minutes = selected.getMinutes(); + + if ( scope.showMeridian ) { + hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system + } + scope.hours = keyboardChange === 'h' ? hours : pad(hours); + scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes); + scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; + } + + function addMinutes( minutes ) { + var dt = new Date( selected.getTime() + minutes * 60000 ); + selected.setHours( dt.getHours(), dt.getMinutes() ); + refresh(); + } + + scope.incrementHours = function() { + addMinutes( hourStep * 60 ); + }; + scope.decrementHours = function() { + addMinutes( - hourStep * 60 ); + }; + scope.incrementMinutes = function() { + addMinutes( minuteStep ); + }; + scope.decrementMinutes = function() { + addMinutes( - minuteStep ); + }; + scope.toggleMeridian = function() { + addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) ); + }; + } + }; +}]); + +angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml']) + +/** + * A helper service that can parse typeahead's syntax (string provided by users) + * Extracted to a separate service for ease of unit testing + */ + .factory('typeaheadParser', ['$parse', function ($parse) { + + // 00000111000000000000022200000000000000003333333333333330000000000044000 + var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/; + + return { + parse:function (input) { + + var match = input.match(TYPEAHEAD_REGEXP), 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) { + + //SUPPORTED ATTRIBUTES (OPTIONS) + + //minimal no of characters that needs to be entered before typeahead kicks-in + var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; + + //minimal wait time after last character typed before typehead kicks-in + var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; + + //should it restrict model values to the ones selected from the popup only? + var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; + + //binding to a variable that indicates if matches are being retrieved asynchronously + var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; + + //a callback executed when a match is selected + var onSelectCallback = $parse(attrs.typeaheadOnSelect); + + var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; + + var appendToBody = attrs.typeaheadAppendToBody ? $parse(attrs.typeaheadAppendToBody) : false; + + //INTERNAL VARIABLES + + //model setter executed upon match selection + var $setModelValue = $parse(attrs.ngModel).assign; + + //expressions used by typeahead + var parserResult = typeaheadParser.parse(attrs.typeahead); + + var hasFocus; + + //pop-up element used to display matches + var popUpEl = angular.element('
'); + popUpEl.attr({ + matches: 'matches', + active: 'activeIdx', + select: 'select(activeIdx)', + query: 'query', + position: 'position' + }); + //custom item template + if (angular.isDefined(attrs.typeaheadTemplateUrl)) { + popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); + } + + //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(originalScope, locals)).then(function(matches) { + + //it might happen that several async queries were in progress if a user were typing fast + //but we are interested only in responses that correspond to the current view value + if (inputValue === modelCtrl.$viewValue && hasFocus) { + if (matches.length > 0) { + + scope.activeIdx = 0; + scope.matches.length = 0; + + //transform labels + for(var i=0; i= minSearch) { + if (waitTime > 0) { + if (timeoutPromise) { + $timeout.cancel(timeoutPromise);//cancel previous timeout + } + timeoutPromise = $timeout(function () { + getMatchesAsync(inputValue); + }, waitTime); + } else { + getMatchesAsync(inputValue); + } + } else { + isLoadingSetter(originalScope, false); + resetMatches(); + } + + if (isEditable) { + return inputValue; + } else { + if (!inputValue) { + // Reset in case user had typed something previously. + modelCtrl.$setValidity('editable', true); + return inputValue; + } else { + modelCtrl.$setValidity('editable', false); + return undefined; + } + } + }); + + modelCtrl.$formatters.push(function (modelValue) { + + var candidateViewValue, emptyViewValue; + var locals = {}; + + if (inputFormatter) { + + locals['$model'] = modelValue; + return inputFormatter(originalScope, locals); + + } else { + + //it might happen that we don't have enough info to properly render input value + //we need to check for this situation and simply return model value if we can't apply custom formatting + locals[parserResult.itemName] = modelValue; + candidateViewValue = parserResult.viewMapper(originalScope, locals); + locals[parserResult.itemName] = undefined; + emptyViewValue = parserResult.viewMapper(originalScope, locals); + + return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue; + } + }); + + scope.select = function (activeIdx) { + //called from within the $digest() cycle + var locals = {}; + var model, item; + + locals[parserResult.itemName] = item = scope.matches[activeIdx].model; + model = parserResult.modelMapper(originalScope, locals); + $setModelValue(originalScope, model); + modelCtrl.$setValidity('editable', true); + + onSelectCallback(originalScope, { + $item: item, + $model: model, + $label: parserResult.viewMapper(originalScope, locals) + }); + + resetMatches(); + + //return focus to the input element if a mach was selected via a mouse click event + 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(); + } + }); + + element.bind('blur', function (evt) { + hasFocus = false; + }); + + // Keep reference to click handler to unbind it. + var dismissClickHandler = function (evt) { + if (element[0] !== evt.target) { + resetMatches(); + scope.$digest(); + } + }; + + $document.bind('click', dismissClickHandler); + + originalScope.$on('$destroy', function(){ + $document.unbind('click', dismissClickHandler); + }); + + var $popup = $compile(popUpEl)(scope); + if ( appendToBody ) { + $document.find('body').append($popup); + } else { + element.after($popup); + } + } + }; + +}]) + + .directive('typeaheadPopup', function () { + return { + restrict:'EA', + scope:{ + matches:'=', + query:'=', + active:'=', + position:'=', + select:'&' + }, + replace:true, + templateUrl:'template/typeahead/typeahead-popup.html', + link:function (scope, element, attrs) { + + scope.templateUrl = attrs.templateUrl; + + scope.isOpen = function () { + return scope.matches.length > 0; + }; + + scope.isActive = function (matchIdx) { + return scope.active == matchIdx; + }; + + scope.selectActive = function (matchIdx) { + scope.active = matchIdx; + }; + + scope.selectMatch = function (activeIdx) { + scope.select({activeIdx:activeIdx}); + }; + } + }; + }) + + .directive('typeaheadMatch', ['$http', '$templateCache', '$compile', '$parse', function ($http, $templateCache, $compile, $parse) { + return { + restrict:'EA', + scope:{ + index:'=', + match:'=', + query:'=' + }, + link:function (scope, element, attrs) { + var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html'; + $http.get(tplUrl, {cache: $templateCache}).success(function(tplContent){ + element.replaceWith($compile(tplContent.trim())(scope)); + }); + } + }; + }]) + + .filter('typeaheadHighlight', function() { + + function escapeRegexp(queryToEscape) { + return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); + } + + return function(matchItem, query) { + return query ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; + }; + }); +angular.module("template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/accordion/accordion-group.html", + "
\n" + + "
\n" + + "

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

\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
"); +}]); + +angular.module("template/accordion/accordion.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/accordion/accordion.html", + "
"); +}]); + +angular.module("template/alert/alert.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/alert/alert.html", + "
\n" + + " \n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("template/carousel/carousel.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/carousel/carousel.html", + "
\n" + + "
    1\">\n" + + "
  1. \n" + + "
\n" + + "
\n" + + " 1\">\n" + + " 1\">\n" + + "
\n" + + ""); +}]); + +angular.module("template/carousel/slide.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/carousel/slide.html", + "
\n" + + ""); +}]); + +angular.module("template/datepicker/datepicker.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/datepicker/datepicker.html", + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 0\" class=\"h6\">\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
#{{label}}
{{ getWeekNumber(row) }}\n" + + " \n" + + "
\n" + + ""); +}]); + +angular.module("template/datepicker/popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/datepicker/popup.html", + "
    \n" + + "
  • \n" + + "
  • \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
  • \n" + + "
\n" + + ""); +}]); + +angular.module("template/modal/backdrop.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/modal/backdrop.html", + "
"); +}]); + +angular.module("template/modal/window.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/modal/window.html", + "
\n" + + "
\n" + + "
"); +}]); + +angular.module("template/pagination/pager.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/pagination/pager.html", + ""); +}]); + +angular.module("template/pagination/pagination.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/pagination/pagination.html", + ""); +}]); + +angular.module("template/tooltip/tooltip-html-unsafe-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/tooltip/tooltip-html-unsafe-popup.html", + "
\n" + + "
\n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("template/tooltip/tooltip-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/tooltip/tooltip-popup.html", + "
\n" + + "
\n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("template/popover/popover.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/popover/popover.html", + "
\n" + + "
\n" + + "\n" + + "
\n" + + "

\n" + + "
\n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("template/popover/popover-template.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/popover/popover-template.html", + "
\n" + + "
\n" + + "\n" + + "
\n" + + "

\n" + + "
\n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("template/progressbar/bar.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/progressbar/bar.html", + "
"); +}]); + +angular.module("template/progressbar/progress.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/progressbar/progress.html", + "
"); +}]); + +angular.module("template/progressbar/progressbar.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/progressbar/progressbar.html", + "
"); +}]); + +angular.module("template/rating/rating.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/rating/rating.html", + "\n" + + " \n" + + ""); +}]); + +angular.module("template/tabs/tab.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/tabs/tab.html", + "
  • \n" + + " {{heading}}\n" + + "
  • \n" + + ""); +}]); + +angular.module("template/tabs/tabset-titles.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/tabs/tabset-titles.html", + "
      \n" + + "
    \n" + + ""); +}]); + +angular.module("template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/tabs/tabset.html", + "\n" + + "
    \n" + + "
      \n" + + "
      \n" + + "
      \n" + + "
      \n" + + "
      \n" + + "
      \n" + + ""); +}]); + +angular.module("template/timepicker/timepicker.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/timepicker/timepicker.html", + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
       
      \n" + + " \n" + + " :\n" + + " \n" + + "
       
      \n" + + ""); +}]); + +angular.module("template/typeahead/typeahead-match.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/typeahead/typeahead-match.html", + ""); +}]); + +angular.module("template/typeahead/typeahead-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/typeahead/typeahead-popup.html", + "
        \n" + + "
      • \n" + + "
        \n" + + "
      • \n" + + "
      "); +}]); \ No newline at end of file diff --git a/app/scripts/vendor/ui-bootstrap-tpls-0.10.0.min.js b/app/scripts/vendor/ui-bootstrap-tpls-0.10.0.min.js new file mode 100644 index 0000000..9e040be --- /dev/null +++ b/app/scripts/vendor/ui-bootstrap-tpls-0.10.0.min.js @@ -0,0 +1 @@ +angular.module("ui.bootstrap",["ui.bootstrap.tpls","ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.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.tpls",["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/popup.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/popover/popover-template.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]);angular.module("ui.bootstrap.transition",[]).factory("$transition",["$q","$timeout","$rootScope",function(e,t,n){function u(e){for(var t in e){if(i.style[t]!==undefined){return e[t]}}}var r=function(i,s,o){o=o||{};var u=e.defer();var a=r[o.animation?"animationEndEventName":"transitionEndEventName"];var f=function(e){n.$apply(function(){i.unbind(a,f);u.resolve(i)})};if(a){i.bind(a,f)}t(function(){if(angular.isString(s)){i.addClass(s)}else if(angular.isFunction(s)){s(i)}else if(angular.isObject(s)){i.css(s)}if(!a){u.resolve(i)}});u.promise.cancel=function(){if(a){i.unbind(a,f)}u.reject("Transition cancelled")};return u.promise};var i=document.createElement("trans");var s={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",transition:"transitionend"};var o={WebkitTransition:"webkitAnimationEnd",MozTransition:"animationend",OTransition:"oAnimationEnd",transition:"animationend"};r.transitionEndEventName=u(s);r.animationEndEventName=u(o);return r}]);angular.module("ui.bootstrap.collapse",["ui.bootstrap.transition"]).directive("collapse",["$transition",function(e,t){return{link:function(t,n,r){function o(t){function i(){if(s===r){s=undefined}}var r=e(n,t);if(s){s.cancel()}s=r;r.then(i,i);return r}function u(){if(i){i=false;a()}else{n.removeClass("collapse").addClass("collapsing");o({height:n[0].scrollHeight+"px"}).then(a)}}function a(){n.removeClass("collapsing");n.addClass("collapse in");n.css({height:"auto"})}function f(){if(i){i=false;l();n.css({height:0})}else{n.css({height:n[0].scrollHeight+"px"});var e=n[0].offsetWidth;n.removeClass("collapse in").addClass("collapsing");o({height:0}).then(l)}}function l(){n.removeClass("collapsing");n.addClass("collapse")}var i=true;var s;t.$watch(r.collapse,function(e){if(e){f()}else{u()}})}}}]);angular.module("ui.bootstrap.accordion",["ui.bootstrap.collapse"]).constant("accordionConfig",{closeOthers:true}).controller("AccordionController",["$scope","$attrs","accordionConfig",function(e,t,n){this.groups=[];this.closeOthers=function(r){var i=angular.isDefined(t.closeOthers)?e.$eval(t.closeOthers):n.closeOthers;if(i){angular.forEach(this.groups,function(e){if(e!==r){e.isOpen=false}})}};this.addGroup=function(e){var t=this;this.groups.push(e);e.$on("$destroy",function(n){t.removeGroup(e)})};this.removeGroup=function(e){var t=this.groups.indexOf(e);if(t!==-1){this.groups.splice(this.groups.indexOf(e),1)}}}]).directive("accordion",function(){return{restrict:"EA",controller:"AccordionController",transclude:true,replace:false,templateUrl:"template/accordion/accordion.html"}}).directive("accordionGroup",["$parse",function(e){return{require:"^accordion",restrict:"EA",transclude:true,replace:true,templateUrl:"template/accordion/accordion-group.html",scope:{heading:"@"},controller:function(){this.setHeading=function(e){this.heading=e}},link:function(t,n,r,i){var s,o;i.addGroup(t);t.isOpen=false;if(r.isOpen){s=e(r.isOpen);o=s.assign;t.$parent.$watch(s,function(e){t.isOpen=!!e})}t.$watch("isOpen",function(e){if(e){i.closeOthers(t)}if(o){o(t.$parent,e)}})}}}]).directive("accordionHeading",function(){return{restrict:"EA",transclude:true,template:"",replace:true,require:"^accordionGroup",compile:function(e,t,n){return function(t,r,i,s){s.setHeading(n(t,function(){}))}}}}).directive("accordionTransclude",function(){return{require:"^accordionGroup",link:function(e,t,n,r){e.$watch(function(){return r[n.accordionTransclude]},function(e){if(e){t.html("");t.append(e)}})}}});angular.module("ui.bootstrap.alert",[]).controller("AlertController",["$scope","$attrs",function(e,t){e.closeable="close"in t}]).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(e,t,n){t.addClass("ng-binding").data("$binding",n.bindHtmlUnsafe);e.$watch(n.bindHtmlUnsafe,function(n){t.html(n||"")})}});angular.module("ui.bootstrap.buttons",[]).constant("buttonConfig",{activeClass:"active",toggleEvent:"click"}).controller("ButtonsController",["buttonConfig",function(e){this.activeClass=e.activeClass||"active";this.toggleEvent=e.toggleEvent||"click"}]).directive("btnRadio",function(){return{require:["btnRadio","ngModel"],controller:"ButtonsController",link:function(e,t,n,r){var i=r[0],s=r[1];s.$render=function(){t.toggleClass(i.activeClass,angular.equals(s.$modelValue,e.$eval(n.btnRadio)))};t.bind(i.toggleEvent,function(){if(!t.hasClass(i.activeClass)){e.$apply(function(){s.$setViewValue(e.$eval(n.btnRadio));s.$render()})}})}}}).directive("btnCheckbox",function(){return{require:["btnCheckbox","ngModel"],controller:"ButtonsController",link:function(e,t,n,r){function o(){return a(n.btnCheckboxTrue,true)}function u(){return a(n.btnCheckboxFalse,false)}function a(t,n){var r=e.$eval(t);return angular.isDefined(r)?r:n}var i=r[0],s=r[1];s.$render=function(){t.toggleClass(i.activeClass,angular.equals(s.$modelValue,o()))};t.bind(i.toggleEvent,function(){e.$apply(function(){s.$setViewValue(t.hasClass(i.activeClass)?u():o());s.$render()})})}}});angular.module("ui.bootstrap.carousel",["ui.bootstrap.transition"]).controller("CarouselController",["$scope","$timeout","$transition","$q",function(e,t,n,r){function l(){c();var n=+e.interval;if(!isNaN(n)&&n>=0){u=t(h,n)}}function c(){if(u){t.cancel(u);u=null}}function h(){if(a){e.next();l()}else{e.pause()}}var i=this,s=i.slides=[],o=-1,u,a;i.currentSlide=null;var f=false;i.select=function(r,u){function c(){if(f){return}if(i.currentSlide&&angular.isString(u)&&!e.noTransition&&r.$element){r.$element.addClass(u);var t=r.$element[0].offsetWidth;angular.forEach(s,function(e){angular.extend(e,{direction:"",entering:false,leaving:false,active:false})});angular.extend(r,{direction:u,active:true,entering:true});angular.extend(i.currentSlide||{},{direction:u,leaving:true});e.$currentTransition=n(r.$element,{});(function(t,n){e.$currentTransition.then(function(){h(t,n)},function(){h(t,n)})})(r,i.currentSlide)}else{h(r,i.currentSlide)}i.currentSlide=r;o=a;l()}function h(t,n){angular.extend(t,{direction:"",active:true,leaving:false,entering:false});angular.extend(n||{},{direction:"",active:false,leaving:false,entering:false});e.$currentTransition=null}var a=s.indexOf(r);if(u===undefined){u=a>o?"next":"prev"}if(r&&r!==i.currentSlide){if(e.$currentTransition){e.$currentTransition.cancel();t(c)}else{c()}}};e.$on("$destroy",function(){f=true});i.indexOfSlide=function(e){return s.indexOf(e)};e.next=function(){var t=(o+1)%s.length;if(!e.$currentTransition){return i.select(s[t],"next")}};e.prev=function(){var t=o-1<0?s.length-1:o-1;if(!e.$currentTransition){return i.select(s[t],"prev")}};e.select=function(e){i.select(e)};e.isActive=function(e){return i.currentSlide===e};e.slides=function(){return s};e.$watch("interval",l);e.$on("$destroy",c);e.play=function(){if(!a){a=true;l()}};e.pause=function(){if(!e.noPause){a=false;c()}};i.addSlide=function(t,n){t.$element=n;s.push(t);if(s.length===1||t.active){i.select(s[s.length-1]);if(s.length==1){e.play()}}else{t.active=false}};i.removeSlide=function(e){var t=s.indexOf(e);s.splice(t,1);if(s.length>0&&e.active){if(t>=s.length){i.select(s[t-1])}else{i.select(s[t])}}else if(o>t){o--}}}]).directive("carousel",[function(){return{restrict:"EA",transclude:true,replace:true,controller:"CarouselController",require:"carousel",templateUrl:"template/carousel/carousel.html",scope:{interval:"=",noTransition:"=",noPause:"="}}}]).directive("slide",["$parse",function(e){return{require:"^carousel",restrict:"EA",transclude:true,replace:true,templateUrl:"template/carousel/slide.html",scope:{},link:function(t,n,r,i){if(r.active){var s=e(r.active);var o=s.assign;var u=t.active=s(t.$parent);t.$watch(function(){var n=s(t.$parent);if(n!==t.active){if(n!==u){u=t.active=n}else{o(t.$parent,n=u=t.active)}}return n})}i.addSlide(t,n);t.$on("$destroy",function(){i.removeSlide(t)});t.$watch("active",function(e){if(e){i.select(t)}})}}}]);angular.module("ui.bootstrap.position",[]).factory("$position",["$document","$window",function(e,t){function n(e,n){if(e.currentStyle){return e.currentStyle[n]}else if(t.getComputedStyle){return t.getComputedStyle(e)[n]}return e.style[n]}function r(e){return(n(e,"position")||"static")==="static"}var i=function(t){var n=e[0];var i=t.offsetParent||n;while(i&&i!==n&&r(i)){i=i.offsetParent}return i||n};return{position:function(t){var n=this.offset(t);var r={top:0,left:0};var s=i(t[0]);if(s!=e[0]){r=this.offset(angular.element(s));r.top+=s.clientTop-s.scrollTop;r.left+=s.clientLeft-s.scrollLeft}var o=t[0].getBoundingClientRect();return{width:o.width||t.prop("offsetWidth"),height:o.height||t.prop("offsetHeight"),top:n.top-r.top,left:n.left-r.left}},offset:function(n){var r=n[0].getBoundingClientRect();return{width:r.width||n.prop("offsetWidth"),height:r.height||n.prop("offsetHeight"),top:r.top+(t.pageYOffset||e[0].body.scrollTop||e[0].documentElement.scrollTop),left:r.left+(t.pageXOffset||e[0].body.scrollLeft||e[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(e,t,n,r){function u(t,n){return angular.isDefined(t)?e.$parent.$eval(t):n}function a(e,t){return(new Date(e,t,0)).getDate()}function f(e,t){var n=new Array(t);var r=e,i=0;while(i0?7-c:-c,p=new Date(u),d=0;if(h>0){p.setDate(-h+1);d+=h}d+=a(r,o+1);d+=(7-d%7)%7;var v=f(p,d),m=new Array(7);for(var g=0;g0||e.dateDisabled&&e.dateDisabled({date:t,mode:r.name})}}]).directive("datepicker",["dateFilter","$parse","datepickerConfig","$log",function(e,t,n,r){return{restrict:"EA",replace:true,templateUrl:"template/datepicker/datepicker.html",scope:{dateDisabled:"&"},require:["datepicker","?^ngModel"],controller:"DatepickerController",link:function(e,i,s,o){function h(){e.showWeekNumbers=f===0&&c}function p(e,t){var n=[];while(e.length>0){n.push(e.splice(0,t))}return n}function d(t){var n=null,i=true;if(a.$modelValue){n=new Date(a.$modelValue);if(isNaN(n)){i=false;r.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(t){l=n}}a.$setValidity("date",i);var s=u.modes[f],o=s.getVisibleDates(l,n);angular.forEach(o.objects,function(e){e.disabled=u.isDisabled(e.date,f)});a.$setValidity("date-disabled",!n||!u.isDisabled(n));e.rows=p(o.objects,s.split);e.labels=o.labels||[];e.title=o.title}function v(e){f=e;h();d()}function m(e){var t=new Date(e);t.setDate(t.getDate()+4-(t.getDay()||7));var n=t.getTime();t.setMonth(0);t.setDate(1);return Math.floor(Math.round((n-t)/864e5)/7)+1}var u=o[0],a=o[1];if(!a){return}var f=0,l=new Date,c=n.showWeeks;if(s.showWeeks){e.$parent.$watch(t(s.showWeeks),function(e){c=!!e;h()})}else{h()}if(s.min){e.$parent.$watch(t(s.min),function(e){u.minDate=e?new Date(e):null;d()})}if(s.max){e.$parent.$watch(t(s.max),function(e){u.maxDate=e?new Date(e):null;d()})}a.$render=function(){d(true)};e.select=function(e){if(f===0){var t=a.$modelValue?new Date(a.$modelValue):new Date(0,0,0,0,0,0,0);t.setFullYear(e.getFullYear(),e.getMonth(),e.getDate());a.$setViewValue(t);d(true)}else{l=e;v(f-1)}};e.move=function(e){var t=u.modes[f].step;l.setMonth(l.getMonth()+e*(t.months||0));l.setFullYear(l.getFullYear()+e*(t.years||0));d()};e.toggleMode=function(){v((f+1)%u.modes.length)};e.getWeekNumber=function(t){return f===0&&e.showWeekNumbers&&t.length===7?m(t[0].date):null}}}}]).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(e,t,n,r,i,s,o){return{restrict:"EA",require:"ngModel",link:function(u,a,f,l){function g(e){if(m){m(u,!!e)}else{c.isOpen=!!e}}function x(e){if(!e){l.$setValidity("date",true);return null}else if(angular.isDate(e)){l.$setValidity("date",true);return e}else if(angular.isString(e)){var t=new Date(e);if(isNaN(t)){l.$setValidity("date",false);return undefined}else{l.$setValidity("date",true);return t}}else{l.$setValidity("date",false);return undefined}}function T(e,n,r){if(e){u.$watch(t(e),function(e){c[n]=e});E.attr(r||n,n)}}function N(){c.position=d?r.offset(a):r.position(a);c.position.top=c.position.top+a.prop("offsetHeight")}var c=u.$new(),h,p=angular.isDefined(f.closeOnDateSelection)?u.$eval(f.closeOnDateSelection):s.closeOnDateSelection,d=angular.isDefined(f.datepickerAppendToBody)?u.$eval(f.datepickerAppendToBody):s.appendToBody;f.$observe("datepickerPopup",function(e){h=e||s.dateFormat;l.$render()});c.showButtonBar=angular.isDefined(f.showButtonBar)?u.$eval(f.showButtonBar):s.showButtonBar;u.$on("$destroy",function(){L.remove();c.$destroy()});f.$observe("currentText",function(e){c.currentText=angular.isDefined(e)?e:s.currentText});f.$observe("toggleWeeksText",function(e){c.toggleWeeksText=angular.isDefined(e)?e:s.toggleWeeksText});f.$observe("clearText",function(e){c.clearText=angular.isDefined(e)?e:s.clearText});f.$observe("closeText",function(e){c.closeText=angular.isDefined(e)?e:s.closeText});var v,m;if(f.isOpen){v=t(f.isOpen);m=v.assign;u.$watch(v,function(t){c.isOpen=!!t})}c.isOpen=v?v(u):false;var y=function(e){if(c.isOpen&&e.target!==a[0]){c.$apply(function(){g(false)})}};var b=function(){c.$apply(function(){g(true)})};var w=angular.element("
      ");w.attr({"ng-model":"date","ng-change":"dateSelection()"});var E=angular.element(w.children()[0]),S={};if(f.datepickerOptions){S=u.$eval(f.datepickerOptions);E.attr(angular.extend({},S))}l.$parsers.unshift(x);c.dateSelection=function(e){if(angular.isDefined(e)){c.date=e}l.$setViewValue(c.date);l.$render();if(p){g(false)}};a.bind("input change keyup",function(){c.$apply(function(){c.date=l.$modelValue})});l.$render=function(){var e=l.$viewValue?i(l.$viewValue,h):"";a.val(e);c.date=l.$modelValue};T(f.min,"min");T(f.max,"max");if(f.showWeeks){T(f.showWeeks,"showWeeks","show-weeks")}else{c.showWeeks="show-weeks"in S?S["show-weeks"]:o.showWeeks;E.attr("show-weeks","showWeeks")}if(f.dateDisabled){E.attr("date-disabled",f.dateDisabled)}var C=false,k=false;c.$watch("isOpen",function(e){if(e){N();n.bind("click",y);if(k){a.unbind("focus",b)}a[0].focus();C=true}else{if(C){n.unbind("click",y)}a.bind("focus",b);k=true}if(m){m(u,e)}});c.today=function(){c.dateSelection(new Date)};c.clear=function(){c.dateSelection(null)};var L=e(w)(c);if(d){n.find("body").append(L)}else{a.after(L)}}}}]).directive("datepickerPopupWrap",function(){return{restrict:"EA",replace:true,transclude:true,templateUrl:"template/datepicker/popup.html",link:function(e,t,n){t.bind("click",function(e){e.preventDefault();e.stopPropagation()})}}});angular.module("ui.bootstrap.dropdownToggle",[]).directive("dropdownToggle",["$document","$location",function(e,t){var n=null,r=angular.noop;return{restrict:"CA",link:function(t,i,s){t.$watch("$location.path",function(){r()});i.parent().bind("click",function(){r()});i.bind("click",function(t){var s=i===n;t.preventDefault();t.stopPropagation();if(!!n){r()}if(!s&&!i.hasClass("disabled")&&!i.prop("disabled")){i.parent().addClass("open");n=i;r=function(t){if(t){t.preventDefault();t.stopPropagation()}e.unbind("click",r);i.parent().removeClass("open");r=angular.noop;n=null};e.bind("click",r)}})}}}]);angular.module("ui.bootstrap.modal",["ui.bootstrap.transition"]).factory("$$stackedMap",function(){return{createNew:function(){var e=[];return{add:function(t,n){e.push({key:t,value:n})},get:function(t){for(var n=0;n0)}function p(){if(u&&c()==-1){var e=a;d(u,a,150,function(){e.$destroy();e=null});u=undefined;a=undefined}}function d(n,r,i,s){function a(){if(a.done){return}a.done=true;n.remove();if(s){s()}}r.animate=false;var o=e.transitionEndEventName;if(o){var u=t(a,i);n.bind(o,function(){t.cancel(u);a();r.$apply()})}else{t(a,0)}}var o="modal-open";var u,a;var f=s.createNew();var l={};i.$watch(c,function(e){if(a){a.index=e}});n.bind("keydown",function(e){var t;if(e.which===27){t=f.top();if(t&&t.value.keyboard){i.$apply(function(){l.dismiss(t.key)})}}});l.open=function(e,t){f.add(e,{deferred:t.deferred,modalScope:t.scope,backdrop:t.backdrop,keyboard:t.keyboard});var s=n.find("body").eq(0),l=c();if(l>=0&&!u){a=i.$new(true);a.index=l;u=r("
      ")(a);s.append(u)}var h=angular.element("
      ");h.attr("window-class",t.windowClass);h.attr("index",f.length()-1);h.attr("animate","animate");h.html(t.content);var p=r(h)(t.scope);f.top().value.modalDomEl=p;s.append(p);s.addClass(o)};l.close=function(e,t){var n=f.get(e).value;if(n){n.deferred.resolve(t);h(e)}};l.dismiss=function(e,t){var n=f.get(e).value;if(n){n.deferred.reject(t);h(e)}};l.dismissAll=function(e){var t=this.getTop();while(t){this.dismiss(t.key,e);t=this.getTop()}};l.getTop=function(){return f.top()};return l}]).provider("$modal",function(){var e={options:{backdrop:true,keyboard:true},$get:["$injector","$rootScope","$q","$http","$templateCache","$controller","$modalStack",function(t,n,r,i,s,o,u){function f(e){return e.template?r.when(e.template):i.get(e.templateUrl,{cache:s}).then(function(e){return e.data})}function l(e){var n=[];angular.forEach(e,function(e,i){if(angular.isFunction(e)||angular.isArray(e)){n.push(r.when(t.invoke(e)))}});return n}var a={};a.open=function(t){var i=r.defer();var s=r.defer();var a={result:i.promise,opened:s.promise,close:function(e){u.close(a,e)},dismiss:function(e){u.dismiss(a,e)}};t=angular.extend({},e.options,t);t.resolve=t.resolve||{};if(!t.template&&!t.templateUrl){throw new Error("One of template or templateUrl options is required.")}var c=r.all([f(t)].concat(l(t.resolve)));c.then(function(r){var s=(t.scope||n).$new();s.$close=a.close;s.$dismiss=a.dismiss;var f,l={};var c=1;if(t.controller){l.$scope=s;l.$modalInstance=a;angular.forEach(t.resolve,function(e,t){l[t]=r[c++]});f=o(t.controller,l)}u.open(a,{scope:s,deferred:i,content:r[0],backdrop:t.backdrop,keyboard:t.keyboard,windowClass:t.windowClass})},function(t){i.reject(t)});c.then(function(){s.resolve(true)},function(){s.reject(false)});return a};return a}]};return e});angular.module("ui.bootstrap.pagination",[]).controller("PaginationController",["$scope","$attrs","$parse","$interpolate",function(e,t,n,r){var i=this,s=t.numPages?n(t.numPages).assign:angular.noop;this.init=function(r){if(t.itemsPerPage){e.$parent.$watch(n(t.itemsPerPage),function(t){i.itemsPerPage=parseInt(t,10);e.totalPages=i.calculateTotalPages()})}else{this.itemsPerPage=r}};this.noPrevious=function(){return this.page===1};this.noNext=function(){return this.page===e.totalPages};this.isActive=function(e){return this.page===e};this.calculateTotalPages=function(){var t=this.itemsPerPage<1?1:Math.ceil(e.totalItems/this.itemsPerPage);return Math.max(t||0,1)};this.getAttributeValue=function(t,n,i){return angular.isDefined(t)?i?r(t)(e.$parent):e.$parent.$eval(t):n};this.render=function(){this.page=parseInt(e.page,10)||1;if(this.page>0&&this.page<=e.totalPages){e.pages=this.getPages(this.page,e.totalPages)}};e.selectPage=function(t){if(!i.isActive(t)&&t>0&&t<=e.totalPages){e.page=t;e.onSelectPage({page:t})}};e.$watch("page",function(){i.render()});e.$watch("totalItems",function(){e.totalPages=i.calculateTotalPages()});e.$watch("totalPages",function(t){s(e.$parent,t);if(i.page>t){e.selectPage(t)}else{i.render()}})}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:false,directionLinks:true,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:true}).directive("pagination",["$parse","paginationConfig",function(e,t){return{restrict:"EA",scope:{page:"=",totalItems:"=",onSelectPage:" &"},controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:true,link:function(n,r,i,s){function d(e,t,n,r){return{number:e,text:t,active:n,disabled:r}}var o,u=s.getAttributeValue(i.boundaryLinks,t.boundaryLinks),a=s.getAttributeValue(i.directionLinks,t.directionLinks),f=s.getAttributeValue(i.firstText,t.firstText,true),l=s.getAttributeValue(i.previousText,t.previousText,true),c=s.getAttributeValue(i.nextText,t.nextText,true),h=s.getAttributeValue(i.lastText,t.lastText,true),p=s.getAttributeValue(i.rotate,t.rotate);s.init(t.itemsPerPage);if(i.maxSize){n.$parent.$watch(e(i.maxSize),function(e){o=parseInt(e,10);s.render()})}s.getPages=function(e,t){var n=[];var r=1,i=t;var v=angular.isDefined(o)&&ot){i=t;r=i-o+1}}else{r=(Math.ceil(e/o)-1)*o+1;i=Math.min(r+o-1,t)}}for(var m=r;m<=i;m++){var g=d(m,m,s.isActive(m),false);n.push(g)}if(v&&!p){if(r>1){var y=d(r-1,"...",false,false);n.unshift(y)}if(i"+"";return{restrict:"EA",scope:true,compile:function(e,t){var n=s(b);return function(t,r,i){function E(){if(!t.tt_isOpen){S()}else{x()}}function S(){if(b&&!t.$eval(i[h+"Enable"])){return}if(t.tt_popupDelay){p=o(T,t.tt_popupDelay,false);p.then(function(e){e()})}else{T()()}}function x(){t.$apply(function(){N()})}function T(){if(!t.tt_content){return angular.noop}C();if(l){o.cancel(l)}s.css({top:0,left:0,display:"block"});if(m){a.find("body").append(s)}else{r.after(s)}w();t.tt_isOpen=true;t.$digest();return w}function N(){t.tt_isOpen=false;o.cancel(p);if(t.tt_animation){l=o(k,500)}else{k()}}function C(){if(s){return}s=n(t,function(){});t.$digest()}function k(e){if(s){if(e){s.remove();s=null}else{angular.forEach(s,function(e){if(e.parentNode){e.parentNode.removeChild(e)}})}}}var s;var l;var p;var m=angular.isDefined(d.appendToBody)?d.appendToBody:false;var g=v(undefined);var y=false;var b=angular.isDefined(i[h+"Enable"]);var w=function(){var e,n,i,o;e=m?f.offset(r):f.position(r);n=s.prop("offsetWidth");i=s.prop("offsetHeight");switch(t.tt_placement){case"right":o={top:e.top+e.height/2-i/2,left:e.left+e.width};break;case"bottom":o={top:e.top+e.height,left:e.left+e.width/2-n/2};break;case"left":o={top:e.top+e.height/2-i/2,left:e.left-n};break;default:o={top:e.top-i,left:e.left+e.width/2-n/2};break}o.top+="px";o.left+="px";s.css(o)};t.tt_isOpen=false;i.$observe(c,function(e){t.tt_content=e;if(!e&&t.tt_isOpen){N()}});i.$observe(h+"Title",function(e){t.tt_title=e});i.$observe(h+"Placement",function(e){t.tt_placement=angular.isDefined(e)?e:d.placement});i.$observe(h+"PopupDelay",function(e){var n=parseInt(e,10);t.tt_popupDelay=!isNaN(n)?n:d.popupDelay});var L=function(){if(y){r.unbind(g.show,S);r.unbind(g.hide,x)}};i.$observe(h+"Trigger",function(e){L();g=v(e);if(g.show===g.hide){r.bind(g.show,E)}else{r.bind(g.show,S);r.bind(g.hide,x)}y=true});var A=t.$eval(i[h+"Animation"]);t.tt_animation=angular.isDefined(A)?!!A:d.animation;i.$observe(h+"AppendToBody",function(e){m=angular.isDefined(e)?u(e)(t):m});if(m){t.$on("$locationChangeSuccess",function(){if(t.tt_isOpen){N()}})}t.$on("$destroy",function(){o.cancel(l);o.cancel(p);L();k(true)})}}}}}]}).directive("tooltipPopup",function(){return{restrict:"EA",replace:true,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html"}}).directive("tooltip",["$tooltip",function(e){return e("tooltip","tooltip","mouseenter")}]).directive("tooltipHtmlUnsafePopup",function(){return{restrict:"EA",replace:true,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-unsafe-popup.html"}}).directive("tooltipHtmlUnsafe",["$tooltip",function(e){return e("tooltipHtmlUnsafe","tooltip","mouseenter")}]);angular.module("ui.bootstrap.popover",["ui.bootstrap.tooltip"]).directive("popoverPopup",function(){return{restrict:"EA",replace:true,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html"}}).directive("popover",["$tooltip",function(e){return e("popover","popover","click")}]).directive("popoverTemplatePopup",["$http","$templateCache","$compile","$timeout",function(e,t,n,r){return{restrict:"EA",replace:true,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&",compileScope:"&"},templateUrl:"template/popover/popover-template.html",link:function(i,s){i.$watch("content",function(o){if(!o){return}e.get(o,{cache:t}).then(function(e){var t=angular.element(s[0].querySelector(".popover-content"));t.children().remove();t.append(n(e.data.trim())(i.compileScope()));r(function(){i.compileScope().$digest()})})})}}}]).directive("popoverTemplate",["$tooltip",function(e){return e("popoverTemplate","popover","click")}]);angular.module("ui.bootstrap.progressbar",["ui.bootstrap.transition"]).constant("progressConfig",{animate:true,max:100}).controller("ProgressController",["$scope","$attrs","progressConfig","$transition",function(e,t,n,r){var i=this,s=[],o=angular.isDefined(t.max)?e.$parent.$eval(t.max):n.max,u=angular.isDefined(t.animate)?e.$parent.$eval(t.animate):n.animate;this.addBar=function(e,t){var n=0,r=e.$parent.$index;if(angular.isDefined(r)&&s[r]){n=s[r].value}s.push(e);this.update(t,e.value,n);e.$watch("value",function(e,n){if(e!==n){i.update(t,e,n)}});e.$on("$destroy",function(){i.removeBar(e)})};this.update=function(e,t,n){var i=this.getPercentage(t);if(u){e.css("width",this.getPercentage(n)+"%");r(e,{width:i+"%"})}else{e.css({transition:"none",width:i+"%"})}};this.removeBar=function(e){s.splice(s.indexOf(e),1)};this.getPercentage=function(e){return Math.round(100*e/o)}}]).directive("progress",function(){return{restrict:"EA",replace:true,transclude:true,controller:"ProgressController",require:"progress",scope:{},template:'
      '}}).directive("bar",function(){return{restrict:"EA",replace:true,transclude:true,require:"^progress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(e,t,n,r){r.addBar(e,t)}}}).directive("progressbar",function(){return{restrict:"EA",replace:true,transclude:true,controller:"ProgressController",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(e,t,n,r){r.addBar(e,angular.element(t.children()[0]))}}});angular.module("ui.bootstrap.rating",[]).constant("ratingConfig",{max:5,stateOn:null,stateOff:null}).controller("RatingController",["$scope","$attrs","$parse","ratingConfig",function(e,t,n,r){this.maxRange=angular.isDefined(t.max)?e.$parent.$eval(t.max):r.max;this.stateOn=angular.isDefined(t.stateOn)?e.$parent.$eval(t.stateOn):r.stateOn;this.stateOff=angular.isDefined(t.stateOff)?e.$parent.$eval(t.stateOff):r.stateOff;this.createRateObjects=function(e){var t={stateOn:this.stateOn,stateOff:this.stateOff};for(var n=0,r=e.length;n1){var s=i==r.length-1?i-1:i+1;n.select(r[s])}r.splice(i,1)}}]).directive("tabset",function(){return{restrict:"EA",transclude:true,replace:true,scope:{},controller:"TabsetController",templateUrl:"template/tabs/tabset.html",link:function(e,t,n){e.vertical=angular.isDefined(n.vertical)?e.$parent.$eval(n.vertical):false;e.justified=angular.isDefined(n.justified)?e.$parent.$eval(n.justified):false;e.type=angular.isDefined(n.type)?e.$parent.$eval(n.type):"tabs"}}}).directive("tab",["$parse",function(e){return{require:"^tabset",restrict:"EA",replace:true,templateUrl:"template/tabs/tab.html",transclude:true,scope:{heading:"@",onSelect:"&select",onDeselect:"&deselect"},controller:function(){},compile:function(t,n,r){return function(n,i,s,o){var u,a;if(s.active){u=e(s.active);a=u.assign;n.$parent.$watch(u,function(t,r){if(t!==r){n.active=!!t}});n.active=u(n.$parent)}else{a=u=angular.noop}n.$watch("active",function(e){a(n.$parent,e);if(e){o.select(n);n.onSelect()}else{n.onDeselect()}});n.disabled=false;if(s.disabled){n.$parent.$watch(e(s.disabled),function(e){n.disabled=!!e})}n.select=function(){if(!n.disabled){n.active=true}};o.addTab(n);n.$on("$destroy",function(){o.removeTab(n)});n.$transcludeFn=r}}}}]).directive("tabHeadingTransclude",[function(){return{restrict:"A",require:"^tab",link:function(e,t,n,r){e.$watch("headingElement",function(n){if(n){t.html("");t.append(n)}})}}}]).directive("tabContentTransclude",function(){function e(e){return e.tagName&&(e.hasAttribute("tab-heading")||e.hasAttribute("data-tab-heading")||e.tagName.toLowerCase()==="tab-heading"||e.tagName.toLowerCase()==="data-tab-heading")}return{restrict:"A",require:"^tabset",link:function(t,n,r){var i=t.$eval(r.tabContentTransclude);i.$transcludeFn(i.$parent,function(t){angular.forEach(t,function(t){if(e(t)){i.headingElement=t}else{n.append(t)}})})}}});angular.module("ui.bootstrap.timepicker",[]).constant("timepickerConfig",{hourStep:1,minuteStep:1,showMeridian:true,meridians:null,readonlyInput:false,mousewheel:true}).directive("timepicker",["$parse","$log","timepickerConfig","$locale",function(e,t,n,r){return{restrict:"EA",require:"?^ngModel",replace:true,scope:{},templateUrl:"template/timepicker/timepicker.html",link:function(i,s,o,u){function h(){var e=parseInt(i.hours,10);var t=i.showMeridian?e>0&&e<13:e>=0&&e<24;if(!t){return undefined}if(i.showMeridian){if(e===12){e=0}if(i.meridian===f[1]){e=e+12}}return e}function p(){var e=parseInt(i.minutes,10);return e>=0&&e<60?e:undefined}function d(e){return angular.isDefined(e)&&e.toString().length<2?"0"+e:e}function E(e){S();u.$setViewValue(new Date(a));x(e)}function S(){u.$setValidity("time",true);i.invalidHours=false;i.invalidMinutes=false}function x(e){var t=a.getHours(),n=a.getMinutes();if(i.showMeridian){t=t===0||t===12?12:t%12}i.hours=e==="h"?t:d(t);i.minutes=e==="m"?n:d(n);i.meridian=a.getHours()<12?f[0]:f[1]}function T(e){var t=new Date(a.getTime()+e*6e4);a.setHours(t.getHours(),t.getMinutes());E()}if(!u){return}var a=new Date,f=angular.isDefined(o.meridians)?i.$parent.$eval(o.meridians):n.meridians||r.DATETIME_FORMATS.AMPMS;var l=n.hourStep;if(o.hourStep){i.$parent.$watch(e(o.hourStep),function(e){l=parseInt(e,10)})}var c=n.minuteStep;if(o.minuteStep){i.$parent.$watch(e(o.minuteStep),function(e){c=parseInt(e,10)})}i.showMeridian=n.showMeridian;if(o.showMeridian){i.$parent.$watch(e(o.showMeridian),function(e){i.showMeridian=!!e;if(u.$error.time){var t=h(),n=p();if(angular.isDefined(t)&&angular.isDefined(n)){a.setHours(t);E()}}else{x()}})}var v=s.find("input"),m=v.eq(0),g=v.eq(1);var y=angular.isDefined(o.mousewheel)?i.$eval(o.mousewheel):n.mousewheel;if(y){var b=function(e){if(e.originalEvent){e=e.originalEvent}var t=e.wheelDelta?e.wheelDelta:-e.deltaY;return e.detail||t>0};m.bind("mousewheel wheel",function(e){i.$apply(b(e)?i.incrementHours():i.decrementHours());e.preventDefault()});g.bind("mousewheel wheel",function(e){i.$apply(b(e)?i.incrementMinutes():i.decrementMinutes());e.preventDefault()})}i.readonlyInput=angular.isDefined(o.readonlyInput)?i.$eval(o.readonlyInput):n.readonlyInput;if(!i.readonlyInput){var w=function(e,t){u.$setViewValue(null);u.$setValidity("time",false);if(angular.isDefined(e)){i.invalidHours=e}if(angular.isDefined(t)){i.invalidMinutes=t}};i.updateHours=function(){var e=h();if(angular.isDefined(e)){a.setHours(e);E("h")}else{w(true)}};m.bind("blur",function(e){if(!i.validHours&&i.hours<10){i.$apply(function(){i.hours=d(i.hours)})}});i.updateMinutes=function(){var e=p();if(angular.isDefined(e)){a.setMinutes(e);E("m")}else{w(undefined,true)}};g.bind("blur",function(e){if(!i.invalidMinutes&&i.minutes<10){i.$apply(function(){i.minutes=d(i.minutes)})}})}else{i.updateHours=angular.noop;i.updateMinutes=angular.noop}u.$render=function(){var e=u.$modelValue?new Date(u.$modelValue):null;if(isNaN(e)){u.$setValidity("time",false);t.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.')}else{if(e){a=e}S();x()}};i.incrementHours=function(){T(l*60)};i.decrementHours=function(){T(-l*60)};i.incrementMinutes=function(){T(c)};i.decrementMinutes=function(){T(-c)};i.toggleMeridian=function(){T(12*60*(a.getHours()<12?1:-1))}}}}]);angular.module("ui.bootstrap.typeahead",["ui.bootstrap.position","ui.bootstrap.bindHtml"]).factory("typeaheadParser",["$parse",function(e){var t=/^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;return{parse:function(n){var r=n.match(t),i,s,o;if(!r){throw new Error("Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_'"+" but got '"+n+"'.")}return{itemName:r[3],source:e(r[4]),viewMapper:e(r[2]||r[1]),modelMapper:e(r[1])}}}}]).directive("typeahead",["$compile","$parse","$q","$timeout","$document","$position","typeaheadParser",function(e,t,n,r,i,s,o){var u=[9,13,27,38,40];return{require:"ngModel",link:function(a,f,l,c){var h=a.$eval(l.typeaheadMinLength)||1;var p=a.$eval(l.typeaheadWaitMs)||0;var d=a.$eval(l.typeaheadEditable)!==false;var v=t(l.typeaheadLoading).assign||angular.noop;var m=t(l.typeaheadOnSelect);var g=l.typeaheadInputFormatter?t(l.typeaheadInputFormatter):undefined;var y=l.typeaheadAppendToBody?t(l.typeaheadAppendToBody):false;var b=t(l.ngModel).assign;var w=o.parse(l.typeahead);var E;var S=angular.element("
      ");S.attr({matches:"matches",active:"activeIdx",select:"select(activeIdx)",query:"query",position:"position"});if(angular.isDefined(l.typeaheadTemplateUrl)){S.attr("template-url",l.typeaheadTemplateUrl)}var x=a.$new();a.$on("$destroy",function(){x.$destroy()});var T=function(){x.matches=[];x.activeIdx=-1};var N=function(e){var t={$viewValue:e};v(a,true);n.when(w.source(a,t)).then(function(n){if(e===c.$viewValue&&E){if(n.length>0){x.activeIdx=0;x.matches.length=0;for(var r=0;r=h){if(p>0){if(C){r.cancel(C)}C=r(function(){N(e)},p)}else{N(e)}}else{v(a,false);T()}if(d){return e}else{if(!e){c.$setValidity("editable",true);return e}else{c.$setValidity("editable",false);return undefined}}});c.$formatters.push(function(e){var t,n;var r={};if(g){r["$model"]=e;return g(a,r)}else{r[w.itemName]=e;t=w.viewMapper(a,r);r[w.itemName]=undefined;n=w.viewMapper(a,r);return t!==n?t:e}});x.select=function(e){var t={};var n,r;t[w.itemName]=r=x.matches[e].model;n=w.modelMapper(a,t);b(a,n);c.$setValidity("editable",true);m(a,{$item:r,$model:n,$label:w.viewMapper(a,t)});T();f[0].focus()};f.bind("keydown",function(e){if(x.matches.length===0||u.indexOf(e.which)===-1){return}e.preventDefault();if(e.which===40){x.activeIdx=(x.activeIdx+1)%x.matches.length;x.$digest()}else if(e.which===38){x.activeIdx=(x.activeIdx?x.activeIdx:x.matches.length)-1;x.$digest()}else if(e.which===13||e.which===9){x.$apply(function(){x.select(x.activeIdx)})}else if(e.which===27){e.stopPropagation();T();x.$digest()}});f.bind("blur",function(e){E=false});var k=function(e){if(f[0]!==e.target){T();x.$digest()}};i.bind("click",k);a.$on("$destroy",function(){i.unbind("click",k)});var L=e(S)(x);if(y){i.find("body").append(L)}else{f.after(L)}}}}]).directive("typeaheadPopup",function(){return{restrict:"EA",scope:{matches:"=",query:"=",active:"=",position:"=",select:"&"},replace:true,templateUrl:"template/typeahead/typeahead-popup.html",link:function(e,t,n){e.templateUrl=n.templateUrl;e.isOpen=function(){return e.matches.length>0};e.isActive=function(t){return e.active==t};e.selectActive=function(t){e.active=t};e.selectMatch=function(t){e.select({activeIdx:t})}}}}).directive("typeaheadMatch",["$http","$templateCache","$compile","$parse",function(e,t,n,r){return{restrict:"EA",scope:{index:"=",match:"=",query:"="},link:function(i,s,o){var u=r(o.templateUrl)(i.$parent)||"template/typeahead/typeahead-match.html";e.get(u,{cache:t}).success(function(e){s.replaceWith(n(e.trim())(i))})}}}]).filter("typeaheadHighlight",function(){function e(e){return e.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}return function(t,n){return n?t.replace(new RegExp(e(n),"gi"),"$&"):t}});angular.module("template/accordion/accordion-group.html",[]).run(["$templateCache",function(e){e.put("template/accordion/accordion-group.html",'
      \n'+'
      \n'+'

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

      \n"+"
      \n"+'
      \n'+'
      \n'+"
      \n"+"
      ")}]);angular.module("template/accordion/accordion.html",[]).run(["$templateCache",function(e){e.put("template/accordion/accordion.html",'
      ')}]);angular.module("template/alert/alert.html",[]).run(["$templateCache",function(e){e.put("template/alert/alert.html","
      \n"+" \n"+"
      \n"+"
      \n"+"")}]);angular.module("template/carousel/carousel.html",[]).run(["$templateCache",function(e){e.put("template/carousel/carousel.html",'\n"+"")}]);angular.module("template/carousel/slide.html",[]).run(["$templateCache",function(e){e.put("template/carousel/slide.html",'
      \n'+"")}]);angular.module("template/datepicker/datepicker.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/datepicker.html","\n"+" \n"+" \n"+' \n'+' \n'+' \n'+" \n"+' \n'+' \n'+' \n'+" \n"+" \n"+" \n"+' \n'+' \n'+' \n"+" \n"+" \n"+"
      #{{label}}
      {{ getWeekNumber(row) }}\n'+' \n'+"
      \n"+"")}]);angular.module("template/datepicker/popup.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/popup.html","
        \n"+"
      • \n"+'
      • \n'+' \n'+' \n'+' \n'+' \n'+" \n"+' \n'+"
      • \n"+"
      \n"+"")}]);angular.module("template/modal/backdrop.html",[]).run(["$templateCache",function(e){e.put("template/modal/backdrop.html",'')}]);angular.module("template/modal/window.html",[]).run(["$templateCache",function(e){e.put("template/modal/window.html",'")}]);angular.module("template/pagination/pager.html",[]).run(["$templateCache",function(e){e.put("template/pagination/pager.html",'")}]);angular.module("template/pagination/pagination.html",[]).run(["$templateCache",function(e){e.put("template/pagination/pagination.html",'")}]);angular.module("template/tooltip/tooltip-html-unsafe-popup.html",[]).run(["$templateCache",function(e){e.put("template/tooltip/tooltip-html-unsafe-popup.html",'
      \n'+'
      \n'+'
      \n'+"
      \n"+"")}]);angular.module("template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(e){e.put("template/tooltip/tooltip-popup.html",'
      \n'+'
      \n'+'
      \n'+"
      \n"+"")}]);angular.module("template/popover/popover.html",[]).run(["$templateCache",function(e){e.put("template/popover/popover.html",'
      \n'+'
      \n'+"\n"+'
      \n'+'

      \n'+'
      \n'+"
      \n"+"
      \n"+"")}]);angular.module("template/popover/popover-template.html",[]).run(["$templateCache",function(e){e.put("template/popover/popover-template.html",'
      \n'+'
      \n'+"\n"+'
      \n'+'

      \n'+'
      \n'+"
      \n"+"
      \n"+"")}]);angular.module("template/progressbar/bar.html",[]).run(["$templateCache",function(e){e.put("template/progressbar/bar.html",'
      ')}]);angular.module("template/progressbar/progress.html",[]).run(["$templateCache",function(e){e.put("template/progressbar/progress.html",'
      ')}]);angular.module("template/progressbar/progressbar.html",[]).run(["$templateCache",function(e){e.put("template/progressbar/progressbar.html",'
      ')}]);angular.module("template/rating/rating.html",[]).run(["$templateCache",function(e){e.put("template/rating/rating.html",'\n'+' \n'+"")}]);angular.module("template/tabs/tab.html",[]).run(["$templateCache",function(e){e.put("template/tabs/tab.html",'
    • \n'+' {{heading}}\n'+"
    • \n"+"")}]);angular.module("template/tabs/tabset-titles.html",[]).run(["$templateCache",function(e){e.put("template/tabs/tabset-titles.html","
        \n"+"
      \n"+"")}]);angular.module("template/tabs/tabset.html",[]).run(["$templateCache",function(e){e.put("template/tabs/tabset.html","\n"+'
      \n'+"
        \n"+'
        \n'+'
        \n'+"
        \n"+"
        \n"+"
        \n"+"")}]);angular.module("template/timepicker/timepicker.html",[]).run(["$templateCache",function(e){e.put("template/timepicker/timepicker.html","\n"+" \n"+' \n'+' \n'+" \n"+' \n'+' \n'+" \n"+" \n"+' \n"+" \n"+' \n"+' \n'+" \n"+' \n'+' \n'+" \n"+' \n'+' \n'+" \n"+" \n"+"
         
        \n'+' \n'+" :\n'+' \n'+"
         
        \n"+"")}]);angular.module("template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(e){e.put("template/typeahead/typeahead-match.html",'')}]);angular.module("template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(e){e.put("template/typeahead/typeahead-popup.html","
          \n"+'
        • \n'+'
          \n'+"
        • \n"+"
        ")}]) \ No newline at end of file diff --git a/app/styles/main.css b/app/styles/main.css index bd598d6..03e831f 100644 --- a/app/styles/main.css +++ b/app/styles/main.css @@ -5000,6 +5000,28 @@ td.visible-print { h1 { color: red; } +/* Ace Editor Styles */ +.ace_editor { + height: 200px; } + +/* % percent sign prefixes sass pseudo classes */ +.ace_marker-layer .was-selected:hover { + cursor: crosshair; + background: green; } + +/* hack to hide the scrollbars in the ace editor */ +.ace_scrollbar { + display: none !important; } + +/* TODO: use marker.renderer to find a solution that can handle future edits */ +.ace_marker-layer .was-selected { + background-color: red; + position: absolute; + z-index: 5; + pointer-events: auto; + cursor: pointer; } + +/* End Ace Editor Styles */ .inline { display: inline-block; } diff --git a/app/styles/style.scss b/app/styles/style.scss index a54d5d4..39d0b19 100644 --- a/app/styles/style.scss +++ b/app/styles/style.scss @@ -2,6 +2,35 @@ h1 { color: red; } +/* Ace Editor Styles */ +.ace_editor { height: 200px; } + +/* % percent sign prefixes sass pseudo classes */ +%foo { + cursor: crosshair; + background: green; +} + +/* hack to hide the scrollbars in the ace editor */ +.ace_scrollbar { + display: none !important; +} + +/* TODO: use marker.renderer to find a solution that can handle future edits */ +.ace_marker-layer .was-selected { + background-color: red; + position: absolute; + z-index: 5; + pointer-events: auto; + cursor: pointer; +} + +.ace_marker-layer .was-selected:hover { + @extend %foo; +} + +/* End Ace Editor Styles */ + .inline { display: inline-block; } diff --git a/app/views/main.html b/app/views/main.html index 51f9389..fbc714c 100644 --- a/app/views/main.html +++ b/app/views/main.html @@ -8,7 +8,30 @@

        editor_components


        -

        Testing Directives

        +

        Testing file upload

        +
        +
        + + + +
        + Preview:
        + + No image chosen yet +
        + + No text uploaded yet +

        {{ textFromFile }}

        + Progress: + +
        + + +
        +

        Testing Ace with directives

        + + +
        @@ -23,6 +46,42 @@

        Testing Directives

         
        + + +
        +
        + +
        +

        Source Area

        +

        TODO: draggables currently cannot revert inside the edit area

        +
        + +
        + +
        +

        Target Area

        +

        TODO: draggables currently cannot revert inside the edit area

        +
        + +
        +
        +
        + + + +
        +

        Testing popovers

        +
        +

        + + + +

        + Popover on span element +

        +

        +
        + diff --git a/bower.json b/bower.json index 37cd5ee..8ff3110 100644 --- a/bower.json +++ b/bower.json @@ -15,7 +15,9 @@ "underscore": "~1.5.2", "domready": "*", "jqueryui": "~1.10.3", - "jquery-simulate": "*" + "jquery-simulate": "*", + "angular-ui-ace": "bower", + "angular-ui-bootstrap": "~0.10.0" }, "devDependencies": { "angular-mocks": "~1.2.0", diff --git a/karma.conf.js b/karma.conf.js index 8ba0a4a..fca56a5 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -45,8 +45,8 @@ module.exports = function(config) { // level of logging // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG - //logLevel: config.LOG_INFO, - logLevel: config.LOG_DEBUG, + logLevel: config.LOG_INFO, + //logLevel: config.LOG_DEBUG, // enable / disable watching file and executing tests whenever any file changes //autoWatch: true, @@ -59,8 +59,9 @@ module.exports = function(config) { // - Safari (only Mac) // - PhantomJS // - IE (only Windows) - //browsers: ['Chrome'], - browsers: ['PhantomJS'], + // Chris - apparently some UI tests only work with Chrome + browsers: ['Chrome'], + //browsers: ['PhantomJS'], //browsers: ['Firefox'], // Continuous Integration mode diff --git a/test/spec/tokenRow/draggable-spec.js b/test/spec/tokenRow/draggable-spec.js index 2f5031d..ecbb203 100644 --- a/test/spec/tokenRow/draggable-spec.js +++ b/test/spec/tokenRow/draggable-spec.js @@ -32,15 +32,19 @@ define(['angular', // TODO: change to actual tests it('Should contain text', inject(function($compile, $rootScope) { + d("inside draggable test"); var $elem = $(elm); var text = $elem.text(); // how to drag with jquery? - expect(text).toBe("Drag Me"); + //expect(text).toBe("Drag Me"); //expect(text).toBe("Balls"); + expect(text).toBe("Drag Me"); + console.error($elem); // use jquery simulate to simulate dragging $elem.simulate('drag', { dx: 200, dy: 100 }); expect($elem.hasClass('i-was-dragged')).toBeTruthy(); + console.error($elem); }) ); diff --git a/test/test-main.js b/test/test-main.js index 2815561..07d71b9 100644 --- a/test/test-main.js +++ b/test/test-main.js @@ -33,7 +33,7 @@ requirejs.config({ //domReady: '../bower_components/domready/ready', ngCookies: '../bower_components/angular-cookies/angular-cookies', ngSanitize: '../bower_components/angular-sanitize/angular-sanitize', - //angularMocks: '../bower_components/angular-mocks/angular-mocks' + angularMocks: '../bower_components/angular-mocks/angular-mocks' }, shim: { angular: { @@ -64,10 +64,10 @@ requirejs.config({ deps: ['angular'], exports: 'ngSanitize' }, - //angularMocks: { - // deps: ['ngResource'], - // exports: 'angularMocks' - //} + angularMocks: { + deps: ['ngResource'], + exports: 'angularMocks' + } }, // deps: ask Require.js to load these files (all our tests)