From 8a9972a71dfc4c2e31e70fb376677beab109b4bc Mon Sep 17 00:00:00 2001 From: James Strachan Date: Wed, 24 Apr 2013 12:02:43 +0100 Subject: [PATCH] added basic completion of header names for JMS for #5 --- .../webapp/app/camel/html/sendMessage.html | 2 +- .../main/webapp/app/camel/js/camelPlugin.ts | 2 +- .../src/main/webapp/app/camel/js/data.ts | 41 + .../src/main/webapp/app/camel/js/send.ts | 179 +- hawtio-web/src/main/webapp/index.html | 3 +- .../webapp/lib/debug/ui-bootstrap-0.2.0.js | 1695 +++++++++++++++++ ...ls-0.1.0.js => ui-bootstrap-tpls-0.2.0.js} | 1056 ++++++---- 7 files changed, 2560 insertions(+), 418 deletions(-) create mode 100644 hawtio-web/src/main/webapp/app/camel/js/data.ts create mode 100755 hawtio-web/src/main/webapp/lib/debug/ui-bootstrap-0.2.0.js rename hawtio-web/src/main/webapp/lib/debug/{ui-bootstrap-tpls-0.1.0.js => ui-bootstrap-tpls-0.2.0.js} (71%) mode change 100644 => 100755 diff --git a/hawtio-web/src/main/webapp/app/camel/html/sendMessage.html b/hawtio-web/src/main/webapp/app/camel/html/sendMessage.html index c818e7e6ed..6c5a7c59cc 100644 --- a/hawtio-web/src/main/webapp/app/camel/html/sendMessage.html +++ b/hawtio-web/src/main/webapp/app/camel/html/sendMessage.html @@ -12,7 +12,7 @@
- + diff --git a/hawtio-web/src/main/webapp/app/camel/js/camelPlugin.ts b/hawtio-web/src/main/webapp/app/camel/js/camelPlugin.ts index 34ff301d6e..431869641a 100644 --- a/hawtio-web/src/main/webapp/app/camel/js/camelPlugin.ts +++ b/hawtio-web/src/main/webapp/app/camel/js/camelPlugin.ts @@ -10,7 +10,7 @@ module Camel { var contextToolBar = "app/camel/html/attributeToolBarContext.html"; - angular.module(pluginName, ['bootstrap', 'ngResource', 'hawtioCore']). + angular.module(pluginName, ['bootstrap', 'ui.bootstrap', 'ngResource', 'hawtioCore']). config(($routeProvider) => { $routeProvider. when('/camel/browseEndpoint', {templateUrl: 'app/camel/html/browseEndpoint.html'}). diff --git a/hawtio-web/src/main/webapp/app/camel/js/data.ts b/hawtio-web/src/main/webapp/app/camel/js/data.ts new file mode 100644 index 0000000000..1755288e26 --- /dev/null +++ b/hawtio-web/src/main/webapp/app/camel/js/data.ts @@ -0,0 +1,41 @@ +module Camel { + export var jmsHeaderSchema = { + definitions: { + headers: { + properties: { + JMSCorrelationID: { + type: "java.lang.String" + }, + JMSDeliveryMode: { + "type": "string", + "enum": [ + "PERSISTENT", + "NON_PERSISTENT" + ] + }, + JMSDestination: { + type: "javax.jms.Destination" + }, + JMSExpiration: { + type: "long" + }, + JMSPriority: { + type: "int" + }, + JMSReplyTo: { + type: "javax.jms.Destination" + }, + JMSType: { + type: "java.lang.String" + }, + JMSXGroupId: { + type: "java.lang.String" + } + } + }, + "javax.jms.Destination": { + type: "java.lang.String" + } + } + }; +} diff --git a/hawtio-web/src/main/webapp/app/camel/js/send.ts b/hawtio-web/src/main/webapp/app/camel/js/send.ts index 52c252a349..57eb55f846 100644 --- a/hawtio-web/src/main/webapp/app/camel/js/send.ts +++ b/hawtio-web/src/main/webapp/app/camel/js/send.ts @@ -1,102 +1,119 @@ module Camel { - export function SendMessageController($scope, workspace:Workspace, localStorage) { - var LANGUAGE_FORMAT_PREFERENCE = "defaultLanguageFormat"; - var sourceFormat = workspace.getLocalStorage(LANGUAGE_FORMAT_PREFERENCE) || "javascript"; - $scope.message = ""; - // TODO Remove this if possible - $scope.codeMirror = undefined; - var options = { - mode: { - name: sourceFormat - }, - // Quick hack to get the codeMirror instance. - onChange: function(codeMirror) { - if(!$scope.codeMirror) { - $scope.codeMirror = codeMirror; - } + export function SendMessageController($scope, workspace:Workspace, localStorage) { + var LANGUAGE_FORMAT_PREFERENCE = "defaultLanguageFormat"; + var sourceFormat = workspace.getLocalStorage(LANGUAGE_FORMAT_PREFERENCE) || "javascript"; + $scope.message = ""; + // TODO Remove this if possible + $scope.codeMirror = undefined; + var options = { + mode: { + name: sourceFormat + }, + // Quick hack to get the codeMirror instance. + onChange: function (codeMirror) { + if (!$scope.codeMirror) { + $scope.codeMirror = codeMirror; } - }; - $scope.codeMirrorOptions = CodeEditor.createEditorSettings(options); + } + }; + $scope.codeMirrorOptions = CodeEditor.createEditorSettings(options); - $scope.headers = []; + $scope.headers = []; - $scope.addHeader = () => { - $scope.headers.push({name: "", value: ""}); - }; + $scope.addHeader = () => { + $scope.headers.push({name: "", value: ""}); + }; - // lets add a default header - $scope.addHeader(); + // lets add a default header + $scope.addHeader(); - $scope.removeHeader = (header) => { - $scope.headers = $scope.headers.remove(header); - }; + $scope.removeHeader = (header) => { + $scope.headers = $scope.headers.remove(header); + }; - // TODO Find out what this does - $scope.$watch('workspace.selection', function () { - workspace.moveIfViewInvalid(); - }); + //$scope.defaultHeaderNames = [ "JMSPriority", "JMSType", "breadCrumbId" ]; + $scope.defaultHeaderNames = () => { + var answer = []; + if (isJmsEndpoint()) { + angular.forEach(Camel.jmsHeaderSchema.definitions.headers.properties, (value, name) => { + answer.push(name); + }); + } + return answer; + }; - /** save the sourceFormat in preferences for later - * Note, this would be controller specific preferences and not the global, overriding, preferences */ - // TODO Use ng-selected="changeSourceFormat()" - Although it seemed to fire multiple times.. - $scope.$watch('codeMirrorOptions.mode.name', function(newValue, oldValue) { - workspace.setLocalStorage(LANGUAGE_FORMAT_PREFERENCE, newValue) - }); + // TODO Find out what this does + $scope.$watch('workspace.selection', function () { + workspace.moveIfViewInvalid(); + }); - var sendWorked = () => { - $scope.message = ""; - notification("success", "Message sent!"); - }; + /** save the sourceFormat in preferences for later + * Note, this would be controller specific preferences and not the global, overriding, preferences */ + // TODO Use ng-selected="changeSourceFormat()" - Although it seemed to fire multiple times.. + $scope.$watch('codeMirrorOptions.mode.name', function (newValue, oldValue) { + workspace.setLocalStorage(LANGUAGE_FORMAT_PREFERENCE, newValue) + }); - $scope.autoFormat = () => { - setTimeout(() => { - CodeEditor.autoFormatEditor($scope.codeMirror); - }, 50); - }; + var sendWorked = () => { + $scope.message = ""; + notification("success", "Message sent!"); + }; - $scope.sendMessage = () => { - var body = $scope.message; - var selection = workspace.selection; - if (selection) { - var mbean = selection.objectName; - if (mbean) { - var headers = null; - if ($scope.headers.length) { - headers = {}; - angular.forEach($scope.headers, (object) => { - var key = object.name; - if (key) { - headers[key] = object.value; - } - }); - } + $scope.autoFormat = () => { + setTimeout(() => { + CodeEditor.autoFormatEditor($scope.codeMirror); + }, 50); + }; - var jolokia = workspace.jolokia; - // if camel then use a different operation on the camel context mbean - var callback = onSuccess(sendWorked); - if (selection.domain === "org.apache.camel") { - var uri = selection.title; - mbean = getSelectionCamelContextMBean(workspace); - if (mbean) { - if (headers) { - jolokia.execute(mbean, "sendBodyAndHeaders(java.lang.String, java.lang.Object, java.util.Map)", uri, body, headers, callback); - } else { - jolokia.execute(mbean, "sendStringBody(java.lang.String, java.lang.String)", uri, body, callback); - } - } else { - notification("error", "Could not find CamelContext MBean!"); + $scope.sendMessage = () => { + var body = $scope.message; + var selection = workspace.selection; + if (selection) { + var mbean = selection.objectName; + if (mbean) { + var headers = null; + if ($scope.headers.length) { + headers = {}; + angular.forEach($scope.headers, (object) => { + var key = object.name; + if (key) { + headers[key] = object.value; } - } else { - var user = localStorage["activemqUserName"]; - var pwd = localStorage["activemqPassword"]; + }); + } + + var jolokia = workspace.jolokia; + // if camel then use a different operation on the camel context mbean + var callback = onSuccess(sendWorked); + if (selection.domain === "org.apache.camel") { + var uri = selection.title; + mbean = getSelectionCamelContextMBean(workspace); + if (mbean) { if (headers) { - jolokia.execute(mbean, "sendTextMessage(java.util.Map, java.lang.String, java.lang.String, java.lang.String)", headers, body, user, pwd, callback); + jolokia.execute(mbean, "sendBodyAndHeaders(java.lang.String, java.lang.Object, java.util.Map)", uri, body, headers, callback); } else { - jolokia.execute(mbean, "sendTextMessage(java.lang.String, java.lang.String, java.lang.String)", body, user, pwd, callback); + jolokia.execute(mbean, "sendStringBody(java.lang.String, java.lang.String)", uri, body, callback); } + } else { + notification("error", "Could not find CamelContext MBean!"); + } + } else { + var user = localStorage["activemqUserName"]; + var pwd = localStorage["activemqPassword"]; + if (headers) { + jolokia.execute(mbean, "sendTextMessage(java.util.Map, java.lang.String, java.lang.String, java.lang.String)", headers, body, user, pwd, callback); + } else { + jolokia.execute(mbean, "sendTextMessage(java.lang.String, java.lang.String, java.lang.String)", body, user, pwd, callback); } } } - }; + } + }; + + function isJmsEndpoint() { + // TODO check for the jms/activemq endpoint in camel or if its an activemq endpoint + //var selection = workspace.selection; + return true; } + } } \ No newline at end of file diff --git a/hawtio-web/src/main/webapp/index.html b/hawtio-web/src/main/webapp/index.html index 5b89e0dd63..ae26a9a877 100644 --- a/hawtio-web/src/main/webapp/index.html +++ b/hawtio-web/src/main/webapp/index.html @@ -157,7 +157,8 @@ - + + diff --git a/hawtio-web/src/main/webapp/lib/debug/ui-bootstrap-0.2.0.js b/hawtio-web/src/main/webapp/lib/debug/ui-bootstrap-0.2.0.js new file mode 100755 index 0000000000..1ae2bae7d6 --- /dev/null +++ b/hawtio-web/src/main/webapp/lib/debug/ui-bootstrap-0.2.0.js @@ -0,0 +1,1695 @@ +angular.module("ui.bootstrap", ["ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.collapse","ui.bootstrap.dialog","ui.bootstrap.dropdownToggle","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.popover","ui.bootstrap.tabs","ui.bootstrap.tooltip","ui.bootstrap.transition","ui.bootstrap.typeahead"]); + +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', '$transition', '$timeout', function($parse, $transition, $timeout) { + 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: ['$scope', function($scope) { + 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.$watch( + function watchIsOpen() { return getIsOpen(scope.$parent); }, + function updateOpen(value) { scope.isOpen = value; } + ); + + scope.isOpen = getIsOpen ? getIsOpen(scope.$parent) : false; + } + + 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: 'E', + 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", []).directive('alert', function () { + return { + restrict:'EA', + templateUrl:'template/alert/alert.html', + transclude:true, + replace:true, + scope:{ + type:'=', + close:'&' + } + }; +}); +angular.module('ui.bootstrap.buttons', []) + + .constant('buttonConfig', { + activeClass:'active', + toggleEvent:'click' + }) + + .directive('btnRadio', ['buttonConfig', function (buttonConfig) { + var activeClass = buttonConfig.activeClass || 'active'; + var toggleEvent = buttonConfig.toggleEvent || 'click'; + + return { + + require:'ngModel', + link:function (scope, element, attrs, ngModelCtrl) { + + var value = scope.$eval(attrs.btnRadio); + + //model -> UI + scope.$watch(function () { + return ngModelCtrl.$modelValue; + }, function (modelValue) { + if (angular.equals(modelValue, value)){ + element.addClass(activeClass); + } else { + element.removeClass(activeClass); + } + }); + + //ui->model + element.bind(toggleEvent, function () { + if (!element.hasClass(activeClass)) { + scope.$apply(function () { + ngModelCtrl.$setViewValue(value); + }); + } + }); + } + }; +}]) + + .directive('btnCheckbox', ['buttonConfig', function (buttonConfig) { + + var activeClass = buttonConfig.activeClass || 'active'; + var toggleEvent = buttonConfig.toggleEvent || 'click'; + + return { + require:'ngModel', + link:function (scope, element, attrs, ngModelCtrl) { + + var trueValue = scope.$eval(attrs.btnCheckboxTrue); + var falseValue = scope.$eval(attrs.btnCheckboxFalse); + + trueValue = angular.isDefined(trueValue) ? trueValue : true; + falseValue = angular.isDefined(falseValue) ? falseValue : false; + + //model -> UI + scope.$watch(function () { + return ngModelCtrl.$modelValue; + }, function (modelValue) { + if (angular.equals(modelValue, trueValue)) { + element.addClass(activeClass); + } else { + element.removeClass(activeClass); + } + }); + + //ui->model + element.bind(toggleEvent, function () { + scope.$apply(function () { + ngModelCtrl.$setViewValue(element.hasClass(activeClass) ? falseValue : trueValue); + }); + }); + } + }; +}]); +/* +* +* AngularJS Bootstrap Carousel +* +* A pure AngularJS carousel. +* +* For no interval set the interval to non-number, or milliseconds of desired interval +* Template: {{anything}} +* To change the carousel's active slide set the active attribute to true +* Template: {{anything}} +*/ +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; + + /* 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() { + //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); + nextSlide.$element[0].offsetWidth = 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; + } + }; + + /* 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; + return self.select(slides[newIndex], 'next'); + }; + + $scope.prev = function() { + var newIndex = currentIndex - 1 < 0 ? slides.length - 1 : currentIndex - 1; + 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); + function restartTimer() { + if (currentTimeout) { + $timeout.cancel(currentTimeout); + } + function go() { + if (isPlaying) { + $scope.next(); + restartTimer(); + } else { + $scope.pause(); + } + } + var interval = +$scope.interval; + if (!isNaN(interval) && interval>=0) { + currentTimeout = $timeout(go, interval); + } + } + $scope.play = function() { + if (!isPlaying) { + isPlaying = true; + restartTimer(); + } + }; + $scope.pause = function() { + isPlaying = false; + if (currentTimeout) { + $timeout.cancel(currentTimeout); + } + }; + + 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]); + } + } + }; +}]) +.directive('carousel', [function() { + return { + restrict: 'EA', + transclude: true, + replace: true, + controller: 'CarouselController', + require: 'carousel', + templateUrl: 'template/carousel/carousel.html', + scope: { + interval: '=', + noTransition: '=' + } + }; +}]) +.directive('slide', [function() { + return { + require: '^carousel', + restrict: 'EA', + transclude: true, + replace: true, + templateUrl: 'template/carousel/slide.html', + scope: { + active: '=' + }, + link: function (scope, element, attrs, carouselCtrl) { + carouselCtrl.addSlide(scope, element); + //when the scope is destroyed then remove the slide from the current slides array + scope.$on('$destroy', function() { + carouselCtrl.removeSlide(scope); + }); + + scope.$watch('active', function(active) { + if (active) { + carouselCtrl.select(scope); + } + }); + } + }; +}]); + +angular.module('ui.bootstrap.collapse',['ui.bootstrap.transition']) + +// The collapsible directive indicates a block of html that will expand and collapse +.directive('collapse', ['$transition', function($transition) { + // CSS transitions don't work with height: auto, so we have to manually change the height to a + // specific value and then once the animation completes, we can reset the height to auto. + // Unfortunately if you do this while the CSS transitions are specified (i.e. in the CSS class + // "collapse") then you trigger a change to height 0 in between. + // The fix is to remove the "collapse" CSS class while changing the height back to auto - phew! + var fixUpHeight = function(scope, element, height) { + // We remove the collapse CSS class to prevent a transition when we change to height: auto + element.removeClass('collapse'); + element.css({ height: height }); + // It appears that reading offsetWidth makes the browser realise that we have changed the + // height already :-/ + var x = element[0].offsetWidth; + element.addClass('collapse'); + }; + + return { + link: function(scope, element, attrs) { + + var isCollapsed; + var initialAnimSkip = true; + scope.$watch(function (){ return element[0].scrollHeight; }, function (value) { + //The listener is called when scollHeight changes + //It actually does on 2 scenarios: + // 1. Parent is set to display none + // 2. angular bindings inside are resolved + //When we have a change of scrollHeight we are setting again the correct height if the group is opened + if (element[0].scrollHeight !== 0) { + if (!isCollapsed) { + if (initialAnimSkip) { + fixUpHeight(scope, element, element[0].scrollHeight + 'px'); + } else { + fixUpHeight(scope, element, 'auto'); + } + } + } + }); + + scope.$watch(attrs.collapse, function(value) { + if (value) { + collapse(); + } else { + expand(); + } + }); + + + var currentTransition; + var doTransition = function(change) { + if ( currentTransition ) { + currentTransition.cancel(); + } + currentTransition = $transition(element,change); + currentTransition.then( + function() { currentTransition = undefined; }, + function() { currentTransition = undefined; } + ); + return currentTransition; + }; + + var expand = function() { + if (initialAnimSkip) { + initialAnimSkip = false; + if ( !isCollapsed ) { + fixUpHeight(scope, element, 'auto'); + } + } else { + doTransition({ height : element[0].scrollHeight + 'px' }) + .then(function() { + // This check ensures that we don't accidentally update the height if the user has closed + // the group while the animation was still running + if ( !isCollapsed ) { + fixUpHeight(scope, element, 'auto'); + } + }); + } + isCollapsed = false; + }; + + var collapse = function() { + isCollapsed = true; + if (initialAnimSkip) { + initialAnimSkip = false; + fixUpHeight(scope, element, 0); + } else { + fixUpHeight(scope, element, element[0].scrollHeight + 'px'); + doTransition({'height':'0'}); + } + }; + } + }; +}]); + +// The `$dialogProvider` can be used to configure global defaults for your +// `$dialog` service. +var dialogModule = angular.module('ui.bootstrap.dialog', ['ui.bootstrap.transition']); + +dialogModule.controller('MessageBoxController', ['$scope', 'dialog', 'model', function($scope, dialog, model){ + $scope.title = model.title; + $scope.message = model.message; + $scope.buttons = model.buttons; + $scope.close = function(res){ + dialog.close(res); + }; +}]); + +dialogModule.provider("$dialog", function(){ + + // The default options for all dialogs. + var defaults = { + backdrop: true, + dialogClass: 'modal', + backdropClass: 'modal-backdrop', + transitionClass: 'fade', + triggerClass: 'in', + dialogOpenClass: 'modal-open', + resolve:{}, + backdropFade: false, + dialogFade:false, + keyboard: true, // close with esc key + backdropClick: true // only in conjunction with backdrop=true + /* other options: template, templateUrl, controller */ + }; + + var globalOptions = {}; + + var activeBackdrops = {value : 0}; + + // The `options({})` allows global configuration of all dialogs in the application. + // + // var app = angular.module('App', ['ui.bootstrap.dialog'], function($dialogProvider){ + // // don't close dialog when backdrop is clicked by default + // $dialogProvider.options({backdropClick: false}); + // }); + this.options = function(value){ + globalOptions = value; + }; + + // Returns the actual `$dialog` service that is injected in controllers + this.$get = ["$http", "$document", "$compile", "$rootScope", "$controller", "$templateCache", "$q", "$transition", "$injector", + function ($http, $document, $compile, $rootScope, $controller, $templateCache, $q, $transition, $injector) { + + var body = $document.find('body'); + + function createElement(clazz) { + var el = angular.element("
"); + el.addClass(clazz); + return el; + } + + // The `Dialog` class represents a modal dialog. The dialog class can be invoked by providing an options object + // containing at lest template or templateUrl and controller: + // + // var d = new Dialog({templateUrl: 'foo.html', controller: 'BarController'}); + // + // Dialogs can also be created using templateUrl and controller as distinct arguments: + // + // var d = new Dialog('path/to/dialog.html', MyDialogController); + function Dialog(opts) { + + var self = this, options = this.options = angular.extend({}, defaults, globalOptions, opts); + + this.backdropEl = createElement(options.backdropClass); + if(options.backdropFade){ + this.backdropEl.addClass(options.transitionClass); + this.backdropEl.removeClass(options.triggerClass); + } + + this.modalEl = createElement(options.dialogClass); + if(options.dialogFade){ + this.modalEl.addClass(options.transitionClass); + this.modalEl.removeClass(options.triggerClass); + } + + this.handledEscapeKey = function(e) { + if (e.which === 27) { + self.close(); + e.preventDefault(); + self.$scope.$apply(); + } + }; + + this.handleBackDropClick = function(e) { + self.close(); + e.preventDefault(); + self.$scope.$apply(); + }; + } + + // The `isOpen()` method returns wether the dialog is currently visible. + Dialog.prototype.isOpen = function(){ + return this._open; + }; + + // The `open(templateUrl, controller)` method opens the dialog. + // Use the `templateUrl` and `controller` arguments if specifying them at dialog creation time is not desired. + Dialog.prototype.open = function(templateUrl, controller){ + var self = this, options = this.options; + + if(templateUrl){ + options.templateUrl = templateUrl; + } + if(controller){ + options.controller = controller; + } + + if(!(options.template || options.templateUrl)) { + throw new Error('Dialog.open expected template or templateUrl, neither found. Use options or open method to specify them.'); + } + + this._loadResolves().then(function(locals) { + var $scope = locals.$scope = self.$scope = locals.$scope ? locals.$scope : $rootScope.$new(); + + self.modalEl.html(locals.$template); + + if (self.options.controller) { + var ctrl = $controller(self.options.controller, locals); + self.modalEl.contents().data('ngControllerController', ctrl); + } + + $compile(self.modalEl)($scope); + self._addElementsToDom(); + body.addClass(self.options.dialogOpenClass); + + // trigger tranisitions + setTimeout(function(){ + if(self.options.dialogFade){ self.modalEl.addClass(self.options.triggerClass); } + if(self.options.backdropFade){ self.backdropEl.addClass(self.options.triggerClass); } + }); + + self._bindEvents(); + }); + + this.deferred = $q.defer(); + return this.deferred.promise; + }; + + // closes the dialog and resolves the promise returned by the `open` method with the specified result. + Dialog.prototype.close = function(result){ + var self = this; + var fadingElements = this._getFadingElements(); + + body.removeClass(self.options.dialogOpenClass); + if(fadingElements.length > 0){ + for (var i = fadingElements.length - 1; i >= 0; i--) { + $transition(fadingElements[i], removeTriggerClass).then(onCloseComplete); + } + return; + } + + this._onCloseComplete(result); + + function removeTriggerClass(el){ + el.removeClass(self.options.triggerClass); + } + + function onCloseComplete(){ + if(self._open){ + self._onCloseComplete(result); + } + } + }; + + Dialog.prototype._getFadingElements = function(){ + var elements = []; + if(this.options.dialogFade){ + elements.push(this.modalEl); + } + if(this.options.backdropFade){ + elements.push(this.backdropEl); + } + + return elements; + }; + + Dialog.prototype._bindEvents = function() { + if(this.options.keyboard){ body.bind('keydown', this.handledEscapeKey); } + if(this.options.backdrop && this.options.backdropClick){ this.backdropEl.bind('click', this.handleBackDropClick); } + }; + + Dialog.prototype._unbindEvents = function() { + if(this.options.keyboard){ body.unbind('keydown', this.handledEscapeKey); } + if(this.options.backdrop && this.options.backdropClick){ this.backdropEl.unbind('click', this.handleBackDropClick); } + }; + + Dialog.prototype._onCloseComplete = function(result) { + this._removeElementsFromDom(); + this._unbindEvents(); + + this.deferred.resolve(result); + }; + + Dialog.prototype._addElementsToDom = function(){ + body.append(this.modalEl); + + if(this.options.backdrop) { + if (activeBackdrops.value === 0) { + body.append(this.backdropEl); + } + activeBackdrops.value++; + } + + this._open = true; + }; + + Dialog.prototype._removeElementsFromDom = function(){ + this.modalEl.remove(); + + if(this.options.backdrop) { + activeBackdrops.value--; + if (activeBackdrops.value === 0) { + this.backdropEl.remove(); + } + } + this._open = false; + }; + + // Loads all `options.resolve` members to be used as locals for the controller associated with the dialog. + Dialog.prototype._loadResolves = function(){ + var values = [], keys = [], templatePromise, self = this; + + if (this.options.template) { + templatePromise = $q.when(this.options.template); + } else if (this.options.templateUrl) { + templatePromise = $http.get(this.options.templateUrl, {cache:$templateCache}) + .then(function(response) { return response.data; }); + } + + angular.forEach(this.options.resolve || [], function(value, key) { + keys.push(key); + values.push(angular.isString(value) ? $injector.get(value) : $injector.invoke(value)); + }); + + keys.push('$template'); + values.push(templatePromise); + + return $q.all(values).then(function(values) { + var locals = {}; + angular.forEach(values, function(value, index) { + locals[keys[index]] = value; + }); + locals.dialog = self; + return locals; + }); + }; + + // The actual `$dialog` service that is injected in controllers. + return { + // Creates a new `Dialog` with the specified options. + dialog: function(opts){ + return new Dialog(opts); + }, + // creates a new `Dialog` tied to the default message box template and controller. + // + // Arguments `title` and `message` are rendered in the modal header and body sections respectively. + // The `buttons` array holds an object with the following members for each button to include in the + // modal footer section: + // + // * `result`: the result to pass to the `close` method of the dialog when the button is clicked + // * `label`: the label of the button + // * `cssClass`: additional css class(es) to apply to the button for styling + messageBox: function(title, message, buttons){ + return new Dialog({templateUrl: 'template/dialog/message.html', controller: 'MessageBoxController', resolve: + {model: function() { + return { + title: title, + message: message, + buttons: buttons + }; + } + }}); + } + }; + }]; +}); + +/* + * dropdownToggle - Provides dropdown menu functionality in place of bootstrap js + * @restrict class or attribute + * @example: + + */ + +angular.module('ui.bootstrap.dropdownToggle', []).directive('dropdownToggle', +['$document', '$location', '$window', function ($document, $location, $window) { + var openElement = null, close; + return { + restrict: 'CA', + link: function(scope, element, attrs) { + scope.$watch(function dropdownTogglePathWatch(){return $location.path();}, function dropdownTogglePathWatchAction() { + if (close) { close(); } + }); + + element.parent().bind('click', function(event) { + if (close) { close(); } + }); + + element.bind('click', function(event) { + event.preventDefault(); + event.stopPropagation(); + + var iWasOpen = false; + + if (openElement) { + iWasOpen = openElement === element; + close(); + } + + if (!iWasOpen){ + element.parent().addClass('open'); + openElement = element; + + close = function (event) { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + $document.unbind('click', close); + element.parent().removeClass('open'); + close = null; + openElement = null; + }; + + $document.bind('click', close); + } + }); + } + }; +}]); + +angular.module('ui.bootstrap.modal', ['ui.bootstrap.dialog']) +.directive('modal', ['$parse', '$dialog', function($parse, $dialog) { + var backdropEl; + var body = angular.element(document.getElementsByTagName('body')[0]); + return { + restrict: 'EA', + terminal: true, + link: function(scope, elm, attrs) { + var opts = angular.extend({}, scope.$eval(attrs.uiOptions || attrs.bsOptions || attrs.options)); + var shownExpr = attrs.modal || attrs.show; + var setClosed; + + // Create a dialog with the template as the contents of the directive + // Add the current scope as the resolve in order to make the directive scope as a dialog controller scope + opts = angular.extend(opts, { + template: elm.html(), + resolve: { $scope: function() { return scope; } } + }); + var dialog = $dialog.dialog(opts); + + elm.remove(); + + if (attrs.close) { + setClosed = function() { + $parse(attrs.close)(scope); + }; + } else { + setClosed = function() { + if (angular.isFunction($parse(shownExpr).assign)) { + $parse(shownExpr).assign(scope, false); + } + }; + } + + scope.$watch(shownExpr, function(isShown, oldShown) { + if (isShown) { + dialog.open().then(function(){ + setClosed(); + }); + } else { + //Make sure it is not opened + if (dialog.isOpen()){ + dialog.close(); + } + } + }); + } + }; +}]); +angular.module('ui.bootstrap.pagination', []) + +.constant('paginationConfig', { + boundaryLinks: false, + directionLinks: true, + firstText: 'First', + previousText: 'Previous', + nextText: 'Next', + lastText: 'Last' +}) + +.directive('pagination', ['paginationConfig', function(paginationConfig) { + return { + restrict: 'EA', + scope: { + numPages: '=', + currentPage: '=', + maxSize: '=', + onSelectPage: '&' + }, + templateUrl: 'template/pagination/pagination.html', + replace: true, + link: function(scope, element, attrs) { + + // Setup configuration parameters + var boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks; + var directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$eval(attrs.directionLinks) : paginationConfig.directionLinks; + var firstText = angular.isDefined(attrs.firstText) ? attrs.firstText : paginationConfig.firstText; + var previousText = angular.isDefined(attrs.previousText) ? attrs.previousText : paginationConfig.previousText; + var nextText = angular.isDefined(attrs.nextText) ? attrs.nextText : paginationConfig.nextText; + var lastText = angular.isDefined(attrs.lastText) ? attrs.lastText : paginationConfig.lastText; + + // Create page object used in template + function makePage(number, text, isActive, isDisabled) { + return { + number: number, + text: text, + active: isActive, + disabled: isDisabled + }; + } + + scope.$watch('numPages + currentPage + maxSize', function() { + scope.pages = []; + + //set the default maxSize to numPages + var maxSize = ( scope.maxSize && scope.maxSize < scope.numPages ) ? scope.maxSize : scope.numPages; + var startPage = scope.currentPage - Math.floor(maxSize/2); + + //adjust the startPage within boundary + if(startPage < 1) { + startPage = 1; + } + if ((startPage + maxSize - 1) > scope.numPages) { + startPage = startPage - ((startPage + maxSize - 1) - scope.numPages ); + } + + // Add page number links + for (var number = startPage, max = startPage + maxSize; number < max; number++) { + var page = makePage(number, number, scope.isActive(number), false); + scope.pages.push(page); + } + + // Add previous & next links + if (directionLinks) { + var previousPage = makePage(scope.currentPage - 1, previousText, false, scope.noPrevious()); + scope.pages.unshift(previousPage); + + var nextPage = makePage(scope.currentPage + 1, nextText, false, scope.noNext()); + scope.pages.push(nextPage); + } + + // Add first & last links + if (boundaryLinks) { + var firstPage = makePage(1, firstText, false, scope.noPrevious()); + scope.pages.unshift(firstPage); + + var lastPage = makePage(scope.numPages, lastText, false, scope.noNext()); + scope.pages.push(lastPage); + } + + + if ( scope.currentPage > scope.numPages ) { + scope.selectPage(scope.numPages); + } + }); + scope.noPrevious = function() { + return scope.currentPage === 1; + }; + scope.noNext = function() { + return scope.currentPage === scope.numPages; + }; + scope.isActive = function(page) { + return scope.currentPage === page; + }; + + scope.selectPage = function(page) { + if ( ! scope.isActive(page) && page > 0 && page <= scope.numPages) { + scope.currentPage = page; + scope.onSelectPage({ page: page }); + } + }; + } + }; +}]); +/** + * 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', [] ) +.directive( 'popoverPopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { popoverTitle: '@', popoverContent: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/popover/popover.html' + }; +}) +.directive( 'popover', [ '$compile', '$timeout', '$parse', '$window', function ( $compile, $timeout, $parse, $window ) { + + var template = + ''+ + ''; + + return { + scope: true, + link: function ( scope, element, attr ) { + var popover = $compile( template )( scope ), + transitionTimeout; + + attr.$observe( 'popover', function ( val ) { + scope.tt_popover = val; + }); + + attr.$observe( 'popoverTitle', function ( val ) { + scope.tt_title = val; + }); + + attr.$observe( 'popoverPlacement', function ( val ) { + // If no placement was provided, default to 'top'. + scope.tt_placement = val || 'top'; + }); + + attr.$observe( 'popoverAnimation', function ( val ) { + scope.tt_animation = $parse( val ); + }); + + // By default, the popover is not open. + scope.tt_isOpen = false; + + // Calculate the current position and size of the directive element. + function getPosition() { + var boundingClientRect = element[0].getBoundingClientRect(); + return { + width: element.prop( 'offsetWidth' ), + height: element.prop( 'offsetHeight' ), + top: boundingClientRect.top + $window.pageYOffset, + left: boundingClientRect.left + $window.pageXOffset + }; + } + + function show() { + var position, + ttWidth, + ttHeight, + ttPosition; + + // If there is a pending remove transition, we must cancel it, lest the + // toolip be mysteriously removed. + if ( transitionTimeout ) { + $timeout.cancel( transitionTimeout ); + } + + // Set the initial positioning. + popover.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. + element.after( popover ); + + // Get the position of the directive element. + position = getPosition(); + + // Get the height and width of the popover so we can center it. + ttWidth = popover.prop( 'offsetWidth' ); + ttHeight = popover.prop( 'offsetHeight' ); + + // Calculate the popover'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) + 'px', + left: (position.left + position.width) + 'px' + }; + break; + case 'bottom': + ttPosition = { + top: (position.top + position.height) + 'px', + left: (position.left + position.width / 2 - ttWidth / 2) + 'px' + }; + break; + case 'left': + ttPosition = { + top: (position.top + position.height / 2 - ttHeight / 2) + 'px', + left: (position.left - ttWidth) + 'px' + }; + break; + default: + ttPosition = { + top: (position.top - ttHeight) + 'px', + left: (position.left + position.width / 2 - ttWidth / 2) + 'px' + }; + break; + } + + // Now set the calculated positioning. + popover.css( ttPosition ); + + // And show the popover. + scope.tt_isOpen = true; + } + + // Hide the popover popup element. + function hide() { + // First things first: we don't show it anymore. + //popover.removeClass( 'in' ); + scope.tt_isOpen = false; + + // And now we remove it from the DOM. However, if we have animation, we + // need to wait for it to expire beforehand. + // FIXME: this is a placeholder for a port of the transitions library. + if ( angular.isDefined( scope.tt_animation ) && scope.tt_animation() ) { + transitionTimeout = $timeout( function () { popover.remove(); }, 500 ); + } else { + popover.remove(); + } + } + + // Register the event listeners. + element.bind( 'click', function() { + if(scope.tt_isOpen){ + scope.$apply( hide ); + } else { + scope.$apply( show ); + } + + }); + } + }; +}]); + + +angular.module('ui.bootstrap.tabs', []) +.controller('TabsController', ['$scope', '$element', function($scope, $element) { + var panes = $scope.panes = []; + + this.select = $scope.select = function selectPane(pane) { + angular.forEach(panes, function(pane) { + pane.selected = false; + }); + pane.selected = true; + }; + + this.addPane = function addPane(pane) { + if (!panes.length) { + $scope.select(pane); + } + panes.push(pane); + }; + + this.removePane = function removePane(pane) { + var index = panes.indexOf(pane); + panes.splice(index, 1); + //Select a new pane if removed pane was selected + if (pane.selected && panes.length > 0) { + $scope.select(panes[index < panes.length ? index : index-1]); + } + }; +}]) +.directive('tabs', function() { + return { + restrict: 'EA', + transclude: true, + scope: {}, + controller: 'TabsController', + templateUrl: 'template/tabs/tabs.html', + replace: true + }; +}) +.directive('pane', ['$parse', function($parse) { + return { + require: '^tabs', + restrict: 'EA', + transclude: true, + scope:{ + heading:'@' + }, + link: function(scope, element, attrs, tabsCtrl) { + var getSelected, setSelected; + scope.selected = false; + if (attrs.active) { + getSelected = $parse(attrs.active); + setSelected = getSelected.assign; + scope.$watch( + function watchSelected() {return getSelected(scope.$parent);}, + function updateSelected(value) {scope.selected = value;} + ); + scope.selected = getSelected ? getSelected(scope.$parent) : false; + } + scope.$watch('selected', function(selected) { + if(selected) { + tabsCtrl.select(scope); + } + if(setSelected) { + setSelected(scope.$parent, selected); + } + }); + + tabsCtrl.addPane(scope); + scope.$on('$destroy', function() { + tabsCtrl.removePane(scope); + }); + }, + templateUrl: 'template/tabs/pane.html', + replace: true + }; +}]); + +/** + * The following features are still outstanding: popup delay, animation as a + * function, placement as a function, inside, support for more triggers than + * just mouse enter/leave, html tooltips, and selector delegatation. + */ +angular.module( 'ui.bootstrap.tooltip', [] ) +.directive( 'tooltipPopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { tooltipTitle: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/tooltip/tooltip-popup.html' + }; +}) +.directive( 'tooltip', [ '$compile', '$timeout', '$parse', '$window', function ( $compile, $timeout, $parse, $window) { + + var template = + ''+ + ''; + + return { + scope: true, + link: function ( scope, element, attr ) { + var tooltip = $compile( template )( scope ), + transitionTimeout; + + attr.$observe( 'tooltip', function ( val ) { + scope.tt_tooltip = val; + }); + + attr.$observe( 'tooltipPlacement', function ( val ) { + // If no placement was provided, default to 'top'. + scope.tt_placement = val || 'top'; + }); + + attr.$observe( 'tooltipAnimation', function ( val ) { + scope.tt_animation = $parse( val ); + }); + + // By default, the tooltip is not open. + scope.tt_isOpen = false; + + // Calculate the current position and size of the directive element. + function getPosition() { + var boundingClientRect = element[0].getBoundingClientRect(); + return { + width: element.prop( 'offsetWidth' ), + height: element.prop( 'offsetHeight' ), + top: boundingClientRect.top + $window.pageYOffset, + left: boundingClientRect.left + $window.pageXOffset + }; + } + + // Show the tooltip popup element. + function show() { + var position, + ttWidth, + ttHeight, + ttPosition; + + //don't show empty tooltips + if (!scope.tt_tooltip) { + return; + } + + // If there is a pending remove transition, we must cancel it, lest the + // toolip 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. + element.after( tooltip ); + + // Get the position of the directive element. + position = getPosition(); + + // 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) + 'px', + left: (position.left + position.width) + 'px' + }; + break; + case 'bottom': + ttPosition = { + top: (position.top + position.height) + 'px', + left: (position.left + position.width / 2 - ttWidth / 2) + 'px' + }; + break; + case 'left': + ttPosition = { + top: (position.top + position.height / 2 - ttHeight / 2) + 'px', + left: (position.left - ttWidth) + 'px' + }; + break; + default: + ttPosition = { + top: (position.top - ttHeight) + 'px', + left: (position.left + position.width / 2 - ttWidth / 2) + 'px' + }; + break; + } + + // Now set the calculated positioning. + tooltip.css( ttPosition ); + + // And show the tooltip. + scope.tt_isOpen = true; + } + + // Hide the tooltip popup element. + function hide() { + // First things first: we don't show it anymore. + //tooltip.removeClass( 'in' ); + scope.tt_isOpen = false; + + // And now we remove it from the DOM. However, if we have animation, we + // need to wait for it to expire beforehand. + // FIXME: this is a placeholder for a port of the transitions library. + if ( angular.isDefined( scope.tt_animation ) && scope.tt_animation() ) { + transitionTimeout = $timeout( function () { tooltip.remove(); }, 500 ); + } else { + tooltip.remove(); + } + } + + // Register the event listeners. + element.bind( 'mouseenter', function() { + scope.$apply( show ); + }); + element.bind( 'mouseleave', function() { + scope.$apply( hide ); + }); + } + }; +}]); + + +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', + 'msTransition': 'MSTransitionEnd', + 'transition': 'transitionend' + }; + var animationEndEventNames = { + 'WebkitTransition': 'webkitAnimationEnd', + 'MozTransition': 'animationend', + 'OTransition': 'oAnimationEnd', + 'msTransition': 'MSAnimationEnd', + '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.typeahead', []) + +/** + * 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]) + }; + } + }; +}]) + + //options - min length + .directive('typeahead', ['$compile', '$q', 'typeaheadParser', function ($compile, $q, typeaheadParser) { + + var HOT_KEYS = [9, 13, 27, 38, 40]; + + return { + require:'ngModel', + link:function (originalScope, element, attrs, modelCtrl) { + + var selected = modelCtrl.$modelValue; + + //minimal no of characters that needs to be entered before typeahead kicks-in + var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; + + //expressions used by typeahead + var parserResult = typeaheadParser.parse(attrs.typeahead); + + //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}; + $q.when(parserResult.source(scope, locals)).then(function(matches) { + + //it might happen that several async queries were in progress if a user were typing fast + //but we are interested only in responses that correspond to the current view value + if (inputValue === modelCtrl.$viewValue) { + if (matches.length > 0) { + + scope.activeIdx = 0; + scope.matches.length = 0; + + //transform labels + for(var i=0; i= minSearch) { + getMatchesAsync(inputValue); + } + } + + return undefined; + }); + + modelCtrl.$render = function () { + var locals = {}; + locals[parserResult.itemName] = selected; + element.val(parserResult.viewMapper(scope, locals) || modelCtrl.$viewValue); + selected = undefined; + }; + + scope.select = function (activeIdx) { + //called from within the $digest() cycle + var locals = {}; + locals[parserResult.itemName] = selected = scope.matches[activeIdx].model; + + modelCtrl.$setViewValue(parserResult.modelMapper(scope, locals)); + modelCtrl.$render(); + }; + + //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(9) + 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) { + scope.matches = []; + scope.$digest(); + } + }); + + var tplElCompiled = $compile("")(scope); + element.after(tplElCompiled); + } + }; + +}]) + + .directive('typeaheadPopup', function () { + return { + restrict:'E', + scope:{ + matches:'=', + query:'=', + active:'=', + select:'&' + }, + replace:true, + templateUrl:'template/typeahead/typeahead.html', + link:function (scope, element, attrs) { + + scope.isOpen = function () { + return scope.matches.length > 0; + }; + + scope.isActive = function (matchIdx) { + return scope.active == matchIdx; + }; + + scope.selectActive = function (matchIdx) { + scope.active = matchIdx; + }; + + scope.selectMatch = function (activeIdx) { + scope.select({activeIdx:activeIdx}); + }; + } + }; + }) + + .filter('typeaheadHighlight', function() { + return function(matchItem, query) { + return (query) ? matchItem.replace(new RegExp(query, 'gi'), '$&') : query; + }; + }); \ No newline at end of file diff --git a/hawtio-web/src/main/webapp/lib/debug/ui-bootstrap-tpls-0.1.0.js b/hawtio-web/src/main/webapp/lib/debug/ui-bootstrap-tpls-0.2.0.js old mode 100644 new mode 100755 similarity index 71% rename from hawtio-web/src/main/webapp/lib/debug/ui-bootstrap-tpls-0.1.0.js rename to hawtio-web/src/main/webapp/lib/debug/ui-bootstrap-tpls-0.2.0.js index 589432bb60..83011f5edd --- a/hawtio-web/src/main/webapp/lib/debug/ui-bootstrap-tpls-0.1.0.js +++ b/hawtio-web/src/main/webapp/lib/debug/ui-bootstrap-tpls-0.2.0.js @@ -1,103 +1,149 @@ -angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.carousel","ui.bootstrap.collapse","ui.bootstrap.dialog","ui.bootstrap.dropdownToggle","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.popover","ui.bootstrap.tabs","ui.bootstrap.tooltip","ui.bootstrap.transition"]); - -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/dialog/message.html","template/pagination/pagination.html","template/popover/popover.html","template/tabs/pane.html","template/tabs/tabs.html","template/tooltip/tooltip-popup.html"]); - -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. -angular.module('ui.bootstrap.accordion').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 -angular.module('ui.bootstrap.accordion').directive('accordionGroup', ['$parse', '$transition', '$timeout', function($parse, $transition, $timeout) { - 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 - 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.$watch( - function watchIsOpen() { return getIsOpen(scope.$parent); }, - function updateOpen(value) { scope.isOpen = value; } - ); - - scope.isOpen = getIsOpen ? getIsOpen(scope.$parent) : false; - } - - scope.$watch('isOpen', function(value) { - if ( value ) { - accordionCtrl.closeOthers(scope); - } - if ( setIsOpen ) { - setIsOpen(scope.$parent, value); - } - }); - - } - }; -}]); - +angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.collapse","ui.bootstrap.dialog","ui.bootstrap.dropdownToggle","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.popover","ui.bootstrap.tabs","ui.bootstrap.tooltip","ui.bootstrap.transition","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/dialog/message.html","template/pagination/pagination.html","template/popover/popover.html","template/tabs/pane.html","template/tabs/tabs.html","template/tooltip/tooltip-popup.html","template/typeahead/typeahead.html"]); + +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', '$transition', '$timeout', function($parse, $transition, $timeout) { + 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: ['$scope', function($scope) { + 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.$watch( + function watchIsOpen() { return getIsOpen(scope.$parent); }, + function updateOpen(value) { scope.isOpen = value; } + ); + + scope.isOpen = getIsOpen ? getIsOpen(scope.$parent) : false; + } + + 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: 'E', + 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", []).directive('alert', function () { return { restrict:'EA', @@ -109,12 +155,88 @@ angular.module("ui.bootstrap.alert", []).directive('alert', function () { close:'&' } }; -}); +}); +angular.module('ui.bootstrap.buttons', []) + + .constant('buttonConfig', { + activeClass:'active', + toggleEvent:'click' + }) + + .directive('btnRadio', ['buttonConfig', function (buttonConfig) { + var activeClass = buttonConfig.activeClass || 'active'; + var toggleEvent = buttonConfig.toggleEvent || 'click'; + + return { + + require:'ngModel', + link:function (scope, element, attrs, ngModelCtrl) { + + var value = scope.$eval(attrs.btnRadio); + + //model -> UI + scope.$watch(function () { + return ngModelCtrl.$modelValue; + }, function (modelValue) { + if (angular.equals(modelValue, value)){ + element.addClass(activeClass); + } else { + element.removeClass(activeClass); + } + }); + + //ui->model + element.bind(toggleEvent, function () { + if (!element.hasClass(activeClass)) { + scope.$apply(function () { + ngModelCtrl.$setViewValue(value); + }); + } + }); + } + }; +}]) + + .directive('btnCheckbox', ['buttonConfig', function (buttonConfig) { + + var activeClass = buttonConfig.activeClass || 'active'; + var toggleEvent = buttonConfig.toggleEvent || 'click'; + + return { + require:'ngModel', + link:function (scope, element, attrs, ngModelCtrl) { + + var trueValue = scope.$eval(attrs.btnCheckboxTrue); + var falseValue = scope.$eval(attrs.btnCheckboxFalse); + + trueValue = angular.isDefined(trueValue) ? trueValue : true; + falseValue = angular.isDefined(falseValue) ? falseValue : false; + + //model -> UI + scope.$watch(function () { + return ngModelCtrl.$modelValue; + }, function (modelValue) { + if (angular.equals(modelValue, trueValue)) { + element.addClass(activeClass); + } else { + element.removeClass(activeClass); + } + }); + + //ui->model + element.bind(toggleEvent, function () { + scope.$apply(function () { + ngModelCtrl.$setViewValue(element.hasClass(activeClass) ? falseValue : trueValue); + }); + }); + } + }; +}]); /* * -* Angular Bootstrap Carousel +* AngularJS Bootstrap Carousel * -* The carousel has all of the function that the original Bootstrap carousel has, except for animations. +* A pure AngularJS carousel. * * For no interval set the interval to non-number, or milliseconds of desired interval * Template: {{anything}} @@ -197,6 +319,18 @@ angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition']) 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); function restartTimer() { if (currentTimeout) { @@ -294,7 +428,7 @@ angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition']) } }; }]); - + angular.module('ui.bootstrap.collapse',['ui.bootstrap.transition']) // The collapsible directive indicates a block of html that will expand and collapse @@ -327,7 +461,11 @@ angular.module('ui.bootstrap.collapse',['ui.bootstrap.transition']) //When we have a change of scrollHeight we are setting again the correct height if the group is opened if (element[0].scrollHeight !== 0) { if (!isCollapsed) { - fixUpHeight(scope, element, element[0].scrollHeight + 'px'); + if (initialAnimSkip) { + fixUpHeight(scope, element, element[0].scrollHeight + 'px'); + } else { + fixUpHeight(scope, element, 'auto'); + } } } }); @@ -386,7 +524,7 @@ angular.module('ui.bootstrap.collapse',['ui.bootstrap.transition']) } }; }]); - + // The `$dialogProvider` can be used to configure global defaults for your // `$dialog` service. var dialogModule = angular.module('ui.bootstrap.dialog', ['ui.bootstrap.transition']); @@ -403,22 +541,25 @@ dialogModule.controller('MessageBoxController', ['$scope', 'dialog', 'model', fu dialogModule.provider("$dialog", function(){ // The default options for all dialogs. - var defaults = { - backdrop: true, - modalClass: 'modal', - backdropClass: 'modal-backdrop', + var defaults = { + backdrop: true, + dialogClass: 'modal', + backdropClass: 'modal-backdrop', transitionClass: 'fade', triggerClass: 'in', - resolve:{}, - backdropFade: false, - modalFade:false, - keyboard: true, // close with esc key - backdropClick: true // only in conjunction with backdrop=true + dialogOpenClass: 'modal-open', + resolve:{}, + backdropFade: false, + dialogFade:false, + keyboard: true, // close with esc key + backdropClick: true // only in conjunction with backdrop=true /* other options: template, templateUrl, controller */ }; var globalOptions = {}; + var activeBackdrops = {value : 0}; + // The `options({})` allows global configuration of all dialogs in the application. // // var app = angular.module('App', ['ui.bootstrap.dialog'], function($dialogProvider){ @@ -430,8 +571,8 @@ dialogModule.provider("$dialog", function(){ }; // Returns the actual `$dialog` service that is injected in controllers - this.$get = ["$http", "$document", "$compile", "$rootScope", "$controller", "$templateCache", "$q", "$transition", - function ($http, $document, $compile, $rootScope, $controller, $templateCache, $q, $transition) { + this.$get = ["$http", "$document", "$compile", "$rootScope", "$controller", "$templateCache", "$q", "$transition", "$injector", + function ($http, $document, $compile, $rootScope, $controller, $templateCache, $q, $transition, $injector) { var body = $document.find('body'); @@ -443,9 +584,9 @@ dialogModule.provider("$dialog", function(){ // The `Dialog` class represents a modal dialog. The dialog class can be invoked by providing an options object // containing at lest template or templateUrl and controller: - // + // // var d = new Dialog({templateUrl: 'foo.html', controller: 'BarController'}); - // + // // Dialogs can also be created using templateUrl and controller as distinct arguments: // // var d = new Dialog('path/to/dialog.html', MyDialogController); @@ -459,8 +600,8 @@ dialogModule.provider("$dialog", function(){ this.backdropEl.removeClass(options.triggerClass); } - this.modalEl = createElement(options.modalClass); - if(options.modalFade){ + this.modalEl = createElement(options.dialogClass); + if(options.dialogFade){ this.modalEl.addClass(options.transitionClass); this.modalEl.removeClass(options.triggerClass); } @@ -496,13 +637,13 @@ dialogModule.provider("$dialog", function(){ if(controller){ options.controller = controller; } - + if(!(options.template || options.templateUrl)) { throw new Error('Dialog.open expected template or templateUrl, neither found. Use options or open method to specify them.'); } this._loadResolves().then(function(locals) { - var $scope = locals.$scope = self.$scope = $rootScope.$new(); + var $scope = locals.$scope = self.$scope = locals.$scope ? locals.$scope : $rootScope.$new(); self.modalEl.html(locals.$template); @@ -511,12 +652,13 @@ dialogModule.provider("$dialog", function(){ self.modalEl.contents().data('ngControllerController', ctrl); } - $compile(self.modalEl.contents())($scope); + $compile(self.modalEl)($scope); self._addElementsToDom(); + body.addClass(self.options.dialogOpenClass); // trigger tranisitions setTimeout(function(){ - if(self.options.modalFade){ self.modalEl.addClass(self.options.triggerClass); } + if(self.options.dialogFade){ self.modalEl.addClass(self.options.triggerClass); } if(self.options.backdropFade){ self.backdropEl.addClass(self.options.triggerClass); } }); @@ -532,6 +674,7 @@ dialogModule.provider("$dialog", function(){ var self = this; var fadingElements = this._getFadingElements(); + body.removeClass(self.options.dialogOpenClass); if(fadingElements.length > 0){ for (var i = fadingElements.length - 1; i >= 0; i--) { $transition(fadingElements[i], removeTriggerClass).then(onCloseComplete); @@ -554,7 +697,7 @@ dialogModule.provider("$dialog", function(){ Dialog.prototype._getFadingElements = function(){ var elements = []; - if(this.options.modalFade){ + if(this.options.dialogFade){ elements.push(this.modalEl); } if(this.options.backdropFade){ @@ -583,13 +726,26 @@ dialogModule.provider("$dialog", function(){ Dialog.prototype._addElementsToDom = function(){ body.append(this.modalEl); - if(this.options.backdrop) { body.append(this.backdropEl); } + + if(this.options.backdrop) { + if (activeBackdrops.value === 0) { + body.append(this.backdropEl); + } + activeBackdrops.value++; + } + this._open = true; }; Dialog.prototype._removeElementsFromDom = function(){ this.modalEl.remove(); - if(this.options.backdrop) { this.backdropEl.remove(); } + + if(this.options.backdrop) { + activeBackdrops.value--; + if (activeBackdrops.value === 0) { + this.backdropEl.remove(); + } + } this._open = false; }; @@ -606,7 +762,7 @@ dialogModule.provider("$dialog", function(){ angular.forEach(this.options.resolve || [], function(value, key) { keys.push(key); - values.push(value); + values.push(angular.isString(value) ? $injector.get(value) : $injector.invoke(value)); }); keys.push('$template'); @@ -638,16 +794,20 @@ dialogModule.provider("$dialog", function(){ // * `label`: the label of the button // * `cssClass`: additional css class(es) to apply to the button for styling messageBox: function(title, message, buttons){ - return new Dialog({templateUrl: 'template/dialog/message.html', controller: 'MessageBoxController', resolve: {model: { - title: title, - message: message, - buttons: buttons - }}}); + return new Dialog({templateUrl: 'template/dialog/message.html', controller: 'MessageBoxController', resolve: + {model: function() { + return { + title: title, + message: message, + buttons: buttons + }; + } + }}); } }; }]; }); - + /* * dropdownToggle - Provides dropdown menu functionality in place of bootstrap js * @restrict class or attribute @@ -708,101 +868,98 @@ angular.module('ui.bootstrap.dropdownToggle', []).directive('dropdownToggle', } }; }]); - -angular.module('ui.bootstrap.modal', []).directive('modal', ['$parse',function($parse) { + +angular.module('ui.bootstrap.modal', ['ui.bootstrap.dialog']) +.directive('modal', ['$parse', '$dialog', function($parse, $dialog) { var backdropEl; var body = angular.element(document.getElementsByTagName('body')[0]); - var defaultOpts = { - backdrop: true, - escape: true - }; return { restrict: 'EA', + terminal: true, link: function(scope, elm, attrs) { - var opts = angular.extend(defaultOpts, scope.$eval(attrs.uiOptions || attrs.bsOptions || attrs.options)); + var opts = angular.extend({}, scope.$eval(attrs.uiOptions || attrs.bsOptions || attrs.options)); var shownExpr = attrs.modal || attrs.show; var setClosed; + // Create a dialog with the template as the contents of the directive + // Add the current scope as the resolve in order to make the directive scope as a dialog controller scope + opts = angular.extend(opts, { + template: elm.html(), + resolve: { $scope: function() { return scope; } } + }); + var dialog = $dialog.dialog(opts); + + elm.remove(); + if (attrs.close) { setClosed = function() { - scope.$apply(attrs.close); + $parse(attrs.close)(scope); }; } else { - setClosed = function() { - scope.$apply(function() { - $parse(shownExpr).assign(scope, false); - }); - }; - } - elm.addClass('modal'); - - if (opts.backdrop && !backdropEl) { - backdropEl = angular.element(''); - backdropEl.css('display','none'); - body.append(backdropEl); - } - - function setShown(shown) { - scope.$apply(function() { - model.assign(scope, shown); - }); - } - - function escapeClose(evt) { - if (evt.which === 27) { setClosed(); } - } - function clickClose() { - setClosed(); - } - - function close() { - if (opts.escape) { body.unbind('keyup', escapeClose); } - if (opts.backdrop) { - backdropEl.css('display', 'none').removeClass('in'); - backdropEl.unbind('click', clickClose); - } - elm.css('display', 'none').removeClass('in'); - body.removeClass('modal-open'); - } - function open() { - if (opts.escape) { body.bind('keyup', escapeClose); } - if (opts.backdrop) { - backdropEl.css('display', 'block').addClass('in'); - if(opts.backdrop != "static") { - backdropEl.bind('click', clickClose); + setClosed = function() { + if (angular.isFunction($parse(shownExpr).assign)) { + $parse(shownExpr).assign(scope, false); } - } - elm.css('display', 'block').addClass('in'); - body.addClass('modal-open'); + }; } scope.$watch(shownExpr, function(isShown, oldShown) { if (isShown) { - open(); + dialog.open().then(function(){ + setClosed(); + }); } else { - close(); + //Make sure it is not opened + if (dialog.isOpen()){ + dialog.close(); + } } }); } }; }]); - angular.module('ui.bootstrap.pagination', []) -.directive('pagination', function() { +.constant('paginationConfig', { + boundaryLinks: false, + directionLinks: true, + firstText: 'First', + previousText: 'Previous', + nextText: 'Next', + lastText: 'Last' +}) + +.directive('pagination', ['paginationConfig', function(paginationConfig) { return { restrict: 'EA', scope: { numPages: '=', currentPage: '=', maxSize: '=', - onSelectPage: '&', - nextText: '@', - previousText: '@' + onSelectPage: '&' }, templateUrl: 'template/pagination/pagination.html', replace: true, - link: function(scope) { + link: function(scope, element, attrs) { + + // Setup configuration parameters + var boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks; + var directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$eval(attrs.directionLinks) : paginationConfig.directionLinks; + var firstText = angular.isDefined(attrs.firstText) ? attrs.firstText : paginationConfig.firstText; + var previousText = angular.isDefined(attrs.previousText) ? attrs.previousText : paginationConfig.previousText; + var nextText = angular.isDefined(attrs.nextText) ? attrs.nextText : paginationConfig.nextText; + var lastText = angular.isDefined(attrs.lastText) ? attrs.lastText : paginationConfig.lastText; + + // Create page object used in template + function makePage(number, text, isActive, isDisabled) { + return { + number: number, + text: text, + active: isActive, + disabled: isDisabled + }; + } + scope.$watch('numPages + currentPage + maxSize', function() { scope.pages = []; @@ -818,9 +975,31 @@ angular.module('ui.bootstrap.pagination', []) startPage = startPage - ((startPage + maxSize - 1) - scope.numPages ); } - for(var i=0; i < maxSize && i < scope.numPages ;i++) { - scope.pages.push(startPage + i); + // Add page number links + for (var number = startPage, max = startPage + maxSize; number < max; number++) { + var page = makePage(number, number, scope.isActive(number), false); + scope.pages.push(page); + } + + // Add previous & next links + if (directionLinks) { + var previousPage = makePage(scope.currentPage - 1, previousText, false, scope.noPrevious()); + scope.pages.unshift(previousPage); + + var nextPage = makePage(scope.currentPage + 1, nextText, false, scope.noNext()); + scope.pages.push(nextPage); + } + + // Add first & last links + if (boundaryLinks) { + var firstPage = makePage(1, firstText, false, scope.noPrevious()); + scope.pages.unshift(firstPage); + + var lastPage = makePage(scope.numPages, lastText, false, scope.noNext()); + scope.pages.push(lastPage); } + + if ( scope.currentPage > scope.numPages ) { scope.selectPage(scope.numPages); } @@ -836,25 +1015,14 @@ angular.module('ui.bootstrap.pagination', []) }; scope.selectPage = function(page) { - if ( ! scope.isActive(page) ) { + if ( ! scope.isActive(page) && page > 0 && page <= scope.numPages) { scope.currentPage = page; scope.onSelectPage({ page: page }); } }; - - scope.selectPrevious = function() { - if ( !scope.noPrevious() ) { - scope.selectPage(scope.currentPage-1); - } - }; - scope.selectNext = function() { - if ( !scope.noNext() ) { - scope.selectPage(scope.currentPage+1); - } - }; } }; -}); +}]); /** * The following features are still outstanding: popup delay, animation as a * function, placement as a function, inside, support for more triggers than @@ -869,7 +1037,7 @@ angular.module( 'ui.bootstrap.popover', [] ) templateUrl: 'template/popover/popover.html' }; }) -.directive( 'popover', [ '$compile', '$timeout', '$parse', function ( $compile, $timeout, $parse ) { +.directive( 'popover', [ '$compile', '$timeout', '$parse', '$window', function ( $compile, $timeout, $parse, $window ) { var template = '" + - " " + - "
" + - "
" + - "
"); -}]); - -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", - "
" + - " " + - "
" + - "
"); -}]); - -angular.module("template/carousel/carousel.html", []).run(["$templateCache", function($templateCache){ - $templateCache.put("template/carousel/carousel.html", - "
" + - "
" + - " " + - " " + - "
" + - ""); -}]); - -angular.module("template/carousel/slide.html", []).run(["$templateCache", function($templateCache){ - $templateCache.put("template/carousel/slide.html", - "
" + - ""); -}]); - -angular.module("template/dialog/message.html", []).run(["$templateCache", function($templateCache){ - $templateCache.put("template/dialog/message.html", - "
" + - "

{{ title }}

" + - "
" + - "
" + - "

{{ message }}

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

" + - "
" + - "
" + - "
" + - ""); -}]); - -angular.module("template/tabs/pane.html", []).run(["$templateCache", function($templateCache){ - $templateCache.put("template/tabs/pane.html", - "
" + - ""); -}]); - -angular.module("template/tabs/tabs.html", []).run(["$templateCache", function($templateCache){ - $templateCache.put("template/tabs/tabs.html", - "
" + - " " + - "
" + - "
" + - ""); -}]); - -angular.module("template/tooltip/tooltip-popup.html", []).run(["$templateCache", function($templateCache){ - $templateCache.put("template/tooltip/tooltip-popup.html", - "
" + - "
" + - "
" + - "
" + - ""); -}]); + +angular.module('ui.bootstrap.typeahead', []) + +/** + * 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]) + }; + } + }; +}]) + + //options - min length + .directive('typeahead', ['$compile', '$q', 'typeaheadParser', function ($compile, $q, typeaheadParser) { + + var HOT_KEYS = [9, 13, 27, 38, 40]; + + return { + require:'ngModel', + link:function (originalScope, element, attrs, modelCtrl) { + + var selected = modelCtrl.$modelValue; + + //minimal no of characters that needs to be entered before typeahead kicks-in + var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; + + //expressions used by typeahead + var parserResult = typeaheadParser.parse(attrs.typeahead); + + //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}; + $q.when(parserResult.source(scope, locals)).then(function(matches) { + + //it might happen that several async queries were in progress if a user were typing fast + //but we are interested only in responses that correspond to the current view value + if (inputValue === modelCtrl.$viewValue) { + if (matches.length > 0) { + + scope.activeIdx = 0; + scope.matches.length = 0; + + //transform labels + for(var i=0; i= minSearch) { + getMatchesAsync(inputValue); + } + } + + return undefined; + }); + + modelCtrl.$render = function () { + var locals = {}; + locals[parserResult.itemName] = selected; + element.val(parserResult.viewMapper(scope, locals) || modelCtrl.$viewValue); + selected = undefined; + }; + + scope.select = function (activeIdx) { + //called from within the $digest() cycle + var locals = {}; + locals[parserResult.itemName] = selected = scope.matches[activeIdx].model; + + modelCtrl.$setViewValue(parserResult.modelMapper(scope, locals)); + modelCtrl.$render(); + }; + + //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(9) + 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) { + scope.matches = []; + scope.$digest(); + } + }); + + var tplElCompiled = $compile("")(scope); + element.after(tplElCompiled); + } + }; + +}]) + + .directive('typeaheadPopup', function () { + return { + restrict:'E', + scope:{ + matches:'=', + query:'=', + active:'=', + select:'&' + }, + replace:true, + templateUrl:'template/typeahead/typeahead.html', + link:function (scope, element, attrs) { + + scope.isOpen = function () { + return scope.matches.length > 0; + }; + + scope.isActive = function (matchIdx) { + return scope.active == matchIdx; + }; + + scope.selectActive = function (matchIdx) { + scope.active = matchIdx; + }; + + scope.selectMatch = function (activeIdx) { + scope.select({activeIdx:activeIdx}); + }; + } + }; + }) + + .filter('typeaheadHighlight', function() { + return function(matchItem, query) { + return (query) ? matchItem.replace(new RegExp(query, 'gi'), '$&') : query; + }; + }); +angular.module("template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache){ + $templateCache.put("template/accordion/accordion-group.html", + "
" + + " " + + "
" + + "
" + + "
"); +}]); + +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", + "
" + + " " + + "
" + + "
"); +}]); + +angular.module("template/carousel/carousel.html", []).run(["$templateCache", function($templateCache){ + $templateCache.put("template/carousel/carousel.html", + "
" + + "
    " + + "
  1. " + + "
" + + "
" + + " " + + " " + + "
" + + ""); +}]); + +angular.module("template/carousel/slide.html", []).run(["$templateCache", function($templateCache){ + $templateCache.put("template/carousel/slide.html", + "
" + + ""); +}]); + +angular.module("template/dialog/message.html", []).run(["$templateCache", function($templateCache){ + $templateCache.put("template/dialog/message.html", + "
" + + "

{{ title }}

" + + "
" + + "
" + + "

{{ message }}

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

" + + "
" + + "
" + + "
" + + ""); +}]); + +angular.module("template/tabs/pane.html", []).run(["$templateCache", function($templateCache){ + $templateCache.put("template/tabs/pane.html", + "
" + + ""); +}]); + +angular.module("template/tabs/tabs.html", []).run(["$templateCache", function($templateCache){ + $templateCache.put("template/tabs/tabs.html", + "
" + + " " + + "
" + + "
" + + ""); +}]); + +angular.module("template/tooltip/tooltip-popup.html", []).run(["$templateCache", function($templateCache){ + $templateCache.put("template/tooltip/tooltip-popup.html", + "
" + + "
" + + "
" + + "
" + + ""); +}]); + +angular.module("template/typeahead/typeahead.html", []).run(["$templateCache", function($templateCache){ + $templateCache.put("template/typeahead/typeahead.html", + "
" + + "
    " + + "
  • " + + " " + + "
  • " + + "
" + + "
"); +}]);